使用 Netty 实现一个 MVC 框架
NettyMVC
上面介绍 Netty 能做是什么时我们说过,相比于 SpringMVC 等框架,Netty 没提供路由等功能,这也契合和 Netty 的设计思路,它更贴近底层。下面我们在 Netty Http 的基础上加入路由和 IOC 等功能,来实现一个 MVC 框架。
NettyMVC 是一个 MVC 框架,它实现了 IOC 容器的相关功能。
- 支持 @Controller,@RequestParam,@RequestMapping 等 MVC 注解。
- 支持 @Service,@Repositry,@Autowired 等 IOC 注解。
- URI 路由解析,参数映射。
- Request 中支持多种参数类型,包括基本数据类型,List,Array,Map等等。
结构图
如何使用
在项目中引入 netty-mvc-core 模块。
在 Maven 项目中的 resources 文件夹下创建 applicationContext.xml, 用来配置 IOC 的包扫描路径。
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<package-scan component-scan="org.test.demo" />
</beans>
@Controller 对应控制层注解,@Service 对应服务层注解,@Respostry 对应持有层注解,
@Autowired 做自动注入,@RequestMapping 做路由,@RequestParam 做参数映射。
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/getUser")
public FullHttpResponse getUserById(FullHttpRequest request,@RequestParam("userId") int id,@RequestParam("name") String name){
String res = userService.getUser(id);
return HttpUtil.constructText(res);
}
}
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired("userDao")
private UserDao userDao;
@Override
public String getUser(int id) {
return userDao.get(id);
}
}
@Repository
public class UserDao {
public String get(int id){
if(id == 1){
return "paul";
}else{
return "wang";
}
}
}
代码实现
既然我们的 MVC 框架是基于 Netty Http 的,我们首先实现 Netty Http 的相关功能。
服务启动类,单例:
package com.paul.http;
import com.paul.ioc.bean.AnnotationApplicationContext;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyHttpServer {
private static NettyHttpServer instance = new NettyHttpServer();
private NettyHttpServer(){};
public static NettyHttpServer getInstance(){
return instance;
}
private final int port = 8012;
public void start(AnnotationApplicationContext applicationContext) throws InterruptedException {
//定义两个线程组,事件循环组,可以类比与 Tomcat 就是死循环,不断接收客户端的连接
// boss 线程组不断从客户端接受连接,但不处理,由 worker 线程组对连接进行真正的处理
// 一个线程组其实也能完成,推荐使用两个
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 服务端启动器
ServerBootstrap serverBootstrap = new ServerBootstrap();
//group 方法有两个,一个接收一个参数,另一个接收两个参数
// childhandler 是我们自己写的请求处理器
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ServerInitializer(applicationContext));
//绑定端口
ChannelFuture future = serverBootstrap.bind(port).sync();
System.out.println("服务端启动,监听端口:8012");
//channel 关闭的监听
future.channel().closeFuture().sync();
}finally {
//优雅的关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
对应的 Initializer:
public class ServerInitializer extends ChannelInitializer<SocketChannel> {
private AnnotationApplicationContext applicationContext;
public ServerInitializer(AnnotationApplicationContext applicationContext){
this.applicationContext = applicationContext;
}
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//管道,管道里面可以有很多 handler,handler 可以理解为一层层过滤的网
ChannelPipeline pipeline = socketChannel.pipeline();
//HttpServerCodec 是 HttpRequestDecoder 和 HttpReponseEncoder 的组合,编码和解码的 handler
pipeline.addLast("httpServerCodec", new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(10*1024*1024));
pipeline.addLast("handler", new DispatcherHandler(applicationContext));
}
}
```
上面这部分代码除了 DispatcherHandler 没有什么特殊的。通过名字就能知道
DispatcherHandler 是我们的核心路由控制类。在看这个类之前我们先定义几个注解,用于
IOC 和请求路径的 mapping。
```java
/**
* 自定义自动注入的注解
* @author swang18
*
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
String value() default "";
}
@Controller, @Service, @Respostry 与 SpringMVC 注解含义相同,分别对应控制层,业
务层和持久层。
@RequestMapping 用作请求路径的 mapping,@RequestParam 用作参数的映射。此处代
码都一样,就不一一列举了。
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestMapping {
String value() default "";
}
首先我们实现 IOC 相关功能,将对应类的实例放入容器中保存起来。我们通过解析 applicationContext.xml 的包名,解析包里面有对应注解的类,将这些类的实例加入到容器中,有 @Autowired 注解的地方要把实例注入进来。
说到容器,很多人可能有点疑惑,到底用什么来实现容器,其实用最简单的 Map 就可以保存名字和对应的实例。
先看 xml 解析类,将 component-scan 中的包名解析出来:
public class XmlUtil {
public String handlerXMLForScanPackage(String configuration){
System.out.println(System.getProperty("user.dir"));//user.dir指定了当前的路径
System.out.println(configuration);
InputStream ins = this.getClass().getClassLoader().getResourceAsStream(configuration);
SAXReader reader = new SAXReader();
try{
Document document = reader.read(ins);
Element root = document.getRootElement();
Element ele = root.element("package-scan");
String res = ele.attributeValue("component-scan");
return res;
}catch(Exception e){
e.printStackTrace();
}
return null;
}
}
容器的实现,我们在 IOC 容器启动时,也做了 MVC 注解的扫描,将路径和方法存入
Map 中,并且启动了 Netty 服务器:
public abstract class BeanFactory {
public Object getBean(String beanName){
return doGetBean(beanName);
}
//交给子类,容器实现类去完成
protected abstract Object doGetBean(String beanName);
}
public abstract class ApplicationContext extends BeanFactory {
protected String configuration;
protected XmlUtil xmlUtil;
public ApplicationContext(String configuration){
this.configuration = configuration;
this.xmlUtil = new XmlUtil();
}
}
public class AnnotationApplicationContext extends ApplicationContext {
//保存类路径的缓存
private List<String> classCache = Collections.synchronizedList(new ArrayList<String>());
//保存需要注入的类的缓存
private List<Class<?>> beanDefinition = Collections.synchronizedList(new ArrayList<Class<?>>());
//保存类实例的容器
public Map<String,Object> beanFactory = new ConcurrentHashMap<>();
// 完整路径和 方法的 mapping
public Map<String,Object> handleMapping = new ConcurrentHashMap<>();
// 类路径和controller 的 mapping
public Map<String,Object> controllerMapping = new ConcurrentHashMap<>();
public AnnotationApplicationContext(String configuration) {
super(configuration);
String path = xmlUtil.handlerXMLForScanPackage(configuration);
//执行包的扫描操作
scanPackage(path);
//注册bean
registerBean();
//把对象创建出来,忽略依赖关系
doCreateBean();
//执行容器管理实例对象运行期间的依赖装配
diBean();
//mvc 相关注解扫描
mappingMVC();
//启动 netty 服务器
NettyHttpServer instance = NettyHttpServer.getInstance();
try {
instance.start(this);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/*
* MVC 注解和路径扫描
*/
private void mappingMVC() {
//上一步已经完成了 Controller,service,respostry,autowired 等注解的扫描和注入
//遍历容器,将 requestmapping 注解的路径和对应的方法以及 contoller 实例对应起来
for(Map.Entry<String,Object> entry:beanFactory.entrySet()){
Object instance = entry.getValue();
Class<?> clazz = instance.getClass();
if(clazz.isAnnotationPresent(Controller.class)){
RequestMapping requestMapping = clazz.getAnnotation(RequestMapping.class);
String classPath = requestMapping.value();
Method[] methods = clazz.getMethods();
for(Method method:methods){
if(method.isAnnotationPresent(RequestMapping.class)){
RequestMapping requestMapping2 = method.getAnnotation(RequestMapping.class);
String methodPath = requestMapping2.value();
String requestPath = classPath + methodPath;
handleMapping.put(requestPath,method);
controllerMapping.put(requestPath,instance);
}else{
continue;
}
}
}else{
continue;
}
}
}
@Override
protected Object doGetBean(String beanName) {
return beanFactory.get(beanName);
}
/**
* 扫描包下面所有的 .class 文件的类路径到上面的List中
*
*/
private void scanPackage(final String path) {
System.out.println("path:"+path);
URL url = this.getClass().getClassLoader().getResource(path.replaceAll("\\.", "/"));
System.out.println("scanPackage:" + url.getPath());
try {
File file = new File(url.toURI());
file.listFiles(new FileFilter(){
//pathname 表示当前目录下的所有文件
@Override
public boolean accept(File pathname) {
//递归查找文件
if(pathname.isDirectory()){
scanPackage(path+"."+pathname.getName());
}else{
if(pathname.getName().endsWith(".class")){
String classPath = path + "." + pathname.getName().replace(".class","");
System.out.println("addClassPath:" +classPath );
classCache.add(classPath);
}
}
return true;
}
});
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* 根据类路径获得 class 对象
*/
private void registerBean() {
if(classCache.isEmpty()){
return;
}
for(String path:classCache){
try {
//使用反射,通过类路径获取class 对象
Class<?> clazz = Class.forName(path);
//找出需要被容器管理的类,比如,@Component,@org.test.demo.Controller,@org.test.demo.Service,@Repository
if(clazz.isAnnotationPresent(Repository.class)||clazz.isAnnotationPresent(Service.class)
||clazz.isAnnotationPresent(Controller.class)|| clazz.isAnnotationPresent(Component.class)){
beanDefinition.add(clazz);
}
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
*
* 根据类对象,创建实例
*/
private void doCreateBean() {
if(beanDefinition.isEmpty()){
return;
}
for(Class clazz:beanDefinition){
try {
Object instance = clazz.newInstance();
//将首字母小写的类名作为默认的 bean 的名字
String aliasName = lowerClass(clazz.getSimpleName());
//先判断@ 注解里面是否给了 Bean 名字,有的话,这个就作为 Bean 的名字
if(clazz.isAnnotationPresent(Repository.class)){
Repository repository = (Repository) clazz.getAnnotation(Repository.class);
if(!"".equals(repository.value())){
aliasName = repository.value();
}
}
if(clazz.isAnnotationPresent(Service.class)){
Service service = (Service) clazz.getAnnotation(Service.class);
if(!"".equals(service.value())){
aliasName = service.value();
}
}
if(clazz.isAnnotationPresent(Controller.class)){
Controller controller = (Controller) clazz.getAnnotation(Controller.class);
if(!"".equals(controller.value())){
aliasName = controller.value();
}
}
if(clazz.isAnnotationPresent(Component.class)){
Component component = (Component) clazz.getAnnotation(Component.class);
if(!"".equals(component.value())){
aliasName = component.value();
}
}
if(beanFactory.get(aliasName)== null){
beanFactory.put(aliasName, instance);
}
//判断当前类是否实现了接口
Class<?>[] interfaces = clazz.getInterfaces();
if(interfaces == null){
continue;
}
//把当前接口的路径作为key存储到容器中
for(Class<?> interf:interfaces){
beanFactory.put(interf.getName(), instance);
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
for (Map.Entry<String, Object> entry : beanFactory.entrySet()) {
System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
}
}
/**
* 对创建好的对象进行依赖注入
*/
private void diBean() {
if(beanFactory.isEmpty()){
return;
}
for(Class<?> clazz:beanDefinition){
String aliasName = lowerClass(clazz.getSimpleName());
//先判断@ 注解里面是否给了 Bean 名字,有的话,这个就作为 Bean 的名字
if(clazz.isAnnotationPresent(Repository.class)){
Repository repository = clazz.getAnnotation(Repository.class);
if(!"".equals(repository.value())){
aliasName = repository.value();
}
}
if(clazz.isAnnotationPresent(Service.class)){
Service service = clazz.getAnnotation(Service.class);
if(!"".equals(service.value())){
aliasName = service.value();
}
}
if(clazz.isAnnotationPresent(Controller.class)){
Controller controller = clazz.getAnnotation(Controller.class);
if(!"".equals(controller.value())){
aliasName = controller.value();
}
}
if(clazz.isAnnotationPresent(Component.class)){
Component component = clazz.getAnnotation(Component.class);
if(!"".equals(component.value())){
aliasName = component.value();
}
}
//根据别名获取到被装配的 bean 的实例
Object instance = beanFactory.get(aliasName);
try{
//从类中获取参数,判断是否有 @Autowired 注解
Field[] fields = clazz.getDeclaredFields();
for(Field f:fields){
if(f.isAnnotationPresent(Autowired.class)){
//开启字段的访问权限
f.setAccessible(true);
Autowired autoWired = f.getAnnotation(Autowired.class);
if(!"".equals(autoWired.value())){
//注解里写了别名
f.set(instance, beanFactory.get(autoWired.value()));
}else{
//按类型名称
String fieldName = f.getType().getName();
f.set(instance, beanFactory.get(fieldName));
}
}
}
}catch(Exception e){
e.printStackTrace();
}
}
}
private String lowerClass(String simpleName) {
char[] chars = simpleName.toCharArray();
chars[0] += 32;
String res = String.valueOf(chars);
return res;
}
}
核心路由控制 handler:
public class DispatcherHandler extends SimpleChannelInboundHandler {
private static final String CONNECTION_KEEP_ALIVE = "keep-alive";
private static final String CONNECTION_CLOSE = "close";
private AnnotationApplicationContext annotationApplicationContext;
private FullHttpRequest request;
private FullHttpResponse response;
private Channel channel;
public DispatcherHandler(AnnotationApplicationContext annotationApplicationContext){
this.annotationApplicationContext = annotationApplicationContext;
}
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {
if(o instanceof FullHttpRequest) {
channel = channelHandlerContext.channel();
request = (FullHttpRequest) o;
String uri = request.uri(); // /paul-mvc/com.paul.controller/method-com.paul.controller
System.out.println("uri: " + uri);
if(uri.contains("?")){
int index = uri.indexOf("?");
uri = uri.substring(0,index);
}
Method m = (Method) annotationApplicationContext.handleMapping.get(uri);
if (null == m) {
response = com.paul.http.HttpUtil.getNotFoundResponse();
writeResponse(true);
return;
}
//从容器里拿到controller 实例
Object instance = annotationApplicationContext.controllerMapping.get(uri);
Object[] args = handle(request, response, m);
for (Object a : args) {
System.out.println("Object:" + a);
}
try {
response = (FullHttpResponse) m.invoke(instance, args);
writeResponse(false);
} catch (Exception e) {
e.printStackTrace();
response = HttpUtil.getErroResponse();
writeResponse(true);
}
}
}
private static Object[] handle(FullHttpRequest req, FullHttpResponse resp,Method method) throws IOException, IllegalAccessException, InstantiationException {
Map<String, List<String>> parameters = RequestParseUtil.getParamMap(req);
//拿到当前执行的方法有哪些参数
Class<?>[] paramClazzs = method.getParameterTypes();
//根据参数的个数,new 一个参数的数据
Object[] args = new Object[paramClazzs.length];
int args_i = 0;
int index = 0;
for(Class<?> paramClazz:paramClazzs){
if(FullHttpRequest.class.isAssignableFrom(paramClazz)){
args[args_i++] = req;
}
if(FullHttpResponse.class.isAssignableFrom(paramClazz)){
args[args_i++] = resp;
}
//判断requestParam 注解
Annotation[] paramAns = method.getParameterAnnotations()[index];
if(paramAns.length > 0){
for(Annotation paramAn:paramAns){
if(RequestParam.class.isAssignableFrom(paramAn.getClass())){
RequestParam rp = (RequestParam) paramAn;
args[args_i++] = RequestParseUtil.getParamValue(parameters, paramClazz, rp, method, index);
}
}
}
index ++;
}
return args;
}
private void writeResponse(boolean forceClose){
boolean close = isClose();
if(!close && !forceClose){
response.headers().add("Content-Length", String.valueOf(response.content().readableBytes()));
}
ChannelFuture future = channel.writeAndFlush(response);
if(close || forceClose){
future.addListener(ChannelFutureListener.CLOSE);
}
}
private boolean isClose(){
if(request.headers().contains("Connection", CONNECTION_CLOSE, true) ||
(request.protocolVersion().equals(HttpVersion.HTTP_1_0) &&
!request.headers().contains("Connection", CONNECTION_KEEP_ALIVE, true)))
return true;
return false;
}
}
这样我们就通过 Netty Http 实现了一个 MVC 框架,当然这个框架还有待改进的地方。
目前方法参数与 request 匹配时必须使用 RequestParam 注解。
对于没有实现接口的类如果注入时,@Autowired 注解必须指定实例名称。 以上两个问题因为目前无法获取参数名(不是参数类型),有兴趣的可以一起来改进。 最后给出源码地址:源码。
编程和码字不易,如果您觉得学到了东西,请帮忙点一下推荐或者在 github 加个 start。
使用 Netty 实现一个 MVC 框架的更多相关文章
- 老司机教你用原生JDK 撸一个 MVC 框架!!!
其实 Spring MVC 是一个基于请求驱动的 Web 框架,并且也使用了前端控制器模式来进行设计,再根据请求映射规则分发给相应的页面控制器进行处理,具体工作原理见下图. 在这里,就不详细谈相关的原 ...
- 2015年3月26日 - Javascript MVC 框架DerbyJS DerbyJS 是一个 MVC 框架,帮助编写实时,交互的应用。
2015年3月26日 - Javascript MVC 框架DerbyJS DerbyJS 是一个 MVC 框架,帮助编写实时,交互的应用.
- 一个 MVC 框架以 MVVM 之「魂」复活了!
GitHub: https://github.com/houfeng/mokit Mokit 最初编写于 2012 年,是一个面向移动应用的前端 mvc 框架,v3 版本进行了大量的重构或重写,并尽可 ...
- springMVC和struts2有什么不同?为什么要用springMVC或者struts2?让你实现一个MVC框架大概如何设计?
[问题一:不同] (1)框架机制 1.Struts2采用Filter(StrutsPrepareAndExecuteFilter)实现,SpringMVC(DispatcherServlet)则采用S ...
- AsMVC:一个简单的MVC框架的Java实现
当初看了<从零开始写一个Java Web框架>,也跟着写了一遍,但当时学艺不精,真正进脑子里的并不是很多,作者将依赖注入框架和MVC框架写在一起也给我造成了不小的困扰.最近刚好看了一遍sp ...
- 自己写一个java的mvc框架吧(五)
自己写一个mvc框架吧(五) 给框架添加注解的支持 一段废话 上一章本来是说这一章要写视图处理的部分,但是由于我在测试代码的时候需要频繁的修改配置文件,太麻烦了.所以这一章先把支持注解的功能加上,这样 ...
- 自己写一个java的mvc框架吧(四)
自己写一个mvc框架吧(四) 写一个请求的入口,以及初始化框架 上一章写了获取方法的入参,并根据入参的参数类型进行数据转换.这时候,我们已经具备了通过反射调用方法的一切必要条件.现在我们缺少一个htt ...
- 自己写一个java的mvc框架吧(三)
自己写一个mvc框架吧(三) 根据Method获取参数并转换参数类型 上一篇我们将url与Method的映射创建完毕,并成功的将映射关系创建起来了.这一篇我们将根据Method的入参参数名称.参数类型 ...
- 自己写一个java的mvc框架吧(二)
自己写一个mvc框架吧(二) 自己写代码的习惯 写一个框架吧,如果这个框架会用到一些配置上的东西,我自己习惯是先不用考虑这个配置文件应该是怎样的,什么形式的,先用一个java对象(比如叫 Config ...
随机推荐
- CSS3文本与字体
一.CSS3 换行 1.word-break(规定自动换行的处理方法) word-break: normal / break-all / keep-all; /* normal:使用浏览器默认的换行规 ...
- spring boot 2.0 thymeleaf调试时正常,打包后运行报错. 找不到模板文件.
使用th:fragment 定义模板 使用 th:replace 来添加模板到需要的地方. 使用时发现一个非常奇怪的问题. 本机idea 调试环境一切正常, 但是打成jar包以后报错,提示找不到对 ...
- python的数据类型之字符串(一)
字符串(str) 双引号或者单引号中的数据,就是字符串. 注意事项 1.反斜杠可以用来转义,使用r可以让反斜杠不发生转义. 2.字符串可以用+运算符连接在一起,用*运算符重复. 3.Python中的字 ...
- SpringBoot系列——Logback日志,输出到文件以及实时输出到web页面
前言 SpringBoot对所有内部日志使用通用日志记录,但保留底层日志实现.为Java Util Logging.Log4J2和Logback提供了默认配置.在不同的情况下,日志记录器都预先配置为使 ...
- 一张图带你了解webpack的require.context
很多人应该像我一样,对于webpack的require.context都是一知半解吧.网上很多关于require.context的使用案例,但是我没找到可以帮助我理解这个知识点的,于是也决定自己来探索 ...
- Codeforces Gym100502A:Amanda Lounges(DFS染色)
http://codeforces.com/gym/100502/attachments 题意:有n个地点,m条边,每条边有一个边权,0代表两个顶点都染成白色,2代表两个顶点都染成黑色,1代表两个顶点 ...
- kuangbin专题 专题二 搜索进阶 Escape HDU - 3533
题目链接:https://vjudge.net/problem/HDU-3533 题目分析: 1.人不能经过碉堡; 2.敌军碉堡可能建到我军基地 3.子弹碰到碉堡就没了,说明子弹会被别的城堡给拦截下来 ...
- C++学习书籍推荐《C++编程思想第二版第二卷》下载
百度云及其他网盘下载地址:点我 编辑推荐 “经典原版书库”是响应教育部提出的使用原版国外教材的号召,为国内高校的计算机教学度身订造的.<C++编程思想>(英文版第2版)是书库中的一本,在广 ...
- springcloud-eureka客户端服务注册(含demo源码)
1. 场景描述 前几天介绍了下springcloud的Eureka注册中心(springcloud-注册中心快速构建),今天结合springboot-web介绍下eureka客户端服务注册. 2. 解 ...
- JDK1.8--体验Stream表达式,从一个对象集合中获取每一个对象的某一个值返回新集合
xl_echo编辑整理,欢迎转载,转载请声明文章来源.更多IT.编程案例.资料请联系QQ:1280023003 百战不败,依不自称常胜,百败不颓,依能奋力前行.——这才是真正的堪称强大!! --- 开 ...