分享
  1. 首页
  2. 文章

框架—记一次手写简易MVC框架的过程 附源代码

苡仁ilss · · 988 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

0.环境

Java : JDK 1.8
IDE : IDEA 2019
构建工具 : Gradle

1.整体思路

1.1 一些点

  • 使用DispatcherServlet统一接收请求
  • 自定义@Controller、@RequestMapping、@RequestParam注解来实现对应不同URI的方法调用
  • 使用反射用HandlerMapping调用对应的方法
  • 使用tomcat-embed-core内嵌web容器Tomcat.
  • 自定义简单的BeanFactory实现依赖注入DI,实现@Bean注解和@Controller注解的Bean管理

1.2 整体调用图

image

1.3 启动加载顺序

image

2.具体实现

2.1 项目整体工程目录

image
  • 创建项目就不说了,IDEA自行创建gradle项目就好。

2.2 具体实现

  1. 在web.server下创建TomcatServer类
  • 简单来说就是实例化一个tomcat服务,并实例化一个DispatcherServlet加入到context中,设置支持异步,处理所有的请求
public class TomcatServer {
 private Tomcat tomcat;
 private String[] args;
 public TomcatServer(String[] args) {
 this.args = args;
 }
 public void startServer() throws LifecycleException {
 // instantiated Tomcat
 tomcat = new Tomcat();
 tomcat.setPort(6699);
 tomcat.start();
 Context context = new StandardContext();
 context.setPath("");
 context.addLifecycleListener(new Tomcat.FixContextListener());
 // register Servlet
 DispatcherServlet dispatcherServlet = new DispatcherServlet();
 Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet).setAsyncSupported(true);
 context.addServletMappingDecoded("/", "dispatcherServlet");
 tomcat.getHost().addChild(context);
 Thread awaitThread = new Thread(() -> TomcatServer.this.tomcat.getServer().await(), "tomcat_await_thread");
 awaitThread.setDaemon(false);
 awaitThread.start();
 }
}
  1. 在web.servlet中新建DispatcherServlet实现Servlet接口.
  • 因为是做一个简单的MVC,这里我直接处理所有请求,不分GET和POST,可以自行改进。
  • 处理所有请求 只需要在service方法中处理即可。
  • 简单的思路是,用HandlerManager通过URI在Map对象中获取到对应MappingHandler对象,然后调用handle方法。
 @Override
 public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
 try {
 MappingHandler mappingHandler = HandlerManager.getMappingHandlerByURI(((HttpServletRequest) req).getRequestURI());
 if (mappingHandler.handle(req, res)) {
 return;
 }
 } catch (IllegalAccessException | InstantiationException | InvocationTargetException | ClassNotFoundException e) {
 e.printStackTrace();
 }
 }
  1. 在web.handler中分别新建MappingHandler和HandlerManager两个类。
  • MappingHandler用来存储URI调用信息,像URI、Method 和 调用参数 这些。如下
public class MappingHandler {
 private String uri;
 private Method method;
 private Class<?> controller;
 private String[] args;
 public MappingHandler(String uri, Method method, Class<?> controller, String[] args) {
 this.uri = uri;
 this.method = method;
 this.controller = controller;
 this.args = args;
 }
}
  • 而HandlerManager则是负责把对应的URI和处理的MappingHandler对应起来
  • 实现就是用自己定义的类扫描器把所有扫描到的类传进来遍历,找出带有Controller注解的类
  • 然后针对每个Controller中含有RequestMapping注解的方法信息构建MappingHandler对象进行注册,放入Map中。
public class HandlerManager {
 public static Map<String, MappingHandler> handleMap = new HashMap<>();
 public static void resolveMappingHandler(List<Class<?>> classList) {
 for (Class<?> cls : classList) {
 if (cls.isAnnotationPresent(Controller.class)) {
 parseHandlerFromController(cls);
 }
 }
 }
 private static void parseHandlerFromController(Class<?> cls) {
 Method[] methods = cls.getDeclaredMethods();
 for (Method method : methods) {
 if (!method.isAnnotationPresent(RequestMapping.class)) {
 continue;
 }
 String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
 List<String> paramNameList = new ArrayList<>();
 for (Parameter parameter : method.getParameters()) {
 if (parameter.isAnnotationPresent(RequestParam.class)) {
 paramNameList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
 }
 }
 String[] params = paramNameList.toArray(new String[paramNameList.size()]);
 MappingHandler mappingHandler = new MappingHandler(uri, method, cls, params);
 HandlerManager.handleMap.put(uri, mappingHandler);
 }
 }
 public static MappingHandler getMappingHandlerByURI(String uri) throws ClassNotFoundException {
 MappingHandler handler = handleMap.get(uri);
 if (null == handler) {
 throw new ClassNotFoundException("MappingHandler was not exist!");
 } else {
 return handler;
 }
 }
}
  • 然后在MappingHandler中加入handle方法,对请求进行处理。
 public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
 String requestUri = ((HttpServletRequest) req).getRequestURI();
 if (!uri.equals(requestUri)) {
 return false;
 }
 // read parameters.
 Object[] parameters = new Object[args.length];
 for (int i = 0; i < args.length; i++) {
 parameters[i] = req.getParameter(args[i]);
 }
 // instantiated Controller.
 Object ctl = BeanFactory.getBean(controller);
 // invoke method.
 Object response = method.invoke(ctl, parameters);
 res.getWriter().println(response.toString());
 return true;
 }
  • 几个注解在web.mvc包中,定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestMapping {
 String value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestParam {
 String value();
}
  1. 创建类扫描器ClassScanner
  • 思路也简单,用Java的类加载器,把类信息读入,放到一个List中返回即可。
  • 我只处理了jar包类型。
public class ClassScanner {
 public static List<Class<?>> scanClasses(String packageName) throws IOException, ClassNotFoundException {
 List<Class<?>> classList = new ArrayList<>();
 String path = packageName.replace(".", "/");
 ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
 Enumeration<URL> resources = classLoader.getResources(path);
 while (resources.hasMoreElements()) {
 URL resource = resources.nextElement();
 if (resource.getProtocol().contains("jar")) {
 // get Class from jar package.
 JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();
 String jarFilePath = jarURLConnection.getJarFile().getName();
 classList.addAll(getClassesFromJar(jarFilePath, path));
 } else {
 // todo other way.
 }
 }
 return classList;
 }
 private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
 List<Class<?>> classes = new ArrayList<>();
 JarFile jarFile = new JarFile(jarFilePath);
 Enumeration<JarEntry> jarEntries = jarFile.entries();
 while (jarEntries.hasMoreElements()) {
 JarEntry jarEntry = jarEntries.nextElement();
 String entryName = jarEntry.getName();
 if (entryName.startsWith(path) && entryName.endsWith(".class")) {
 String classFullName = entryName.replace("/", ".").substring(0, entryName.length() - 6);
 classes.add(Class.forName(classFullName));
 }
 }
 return classes;
 }
}
  1. 在beans包下创建BeanFactory类和@Autowired @Bean注解
  • 注解定义
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Bean {
}
  • 相信细心的一定看到了我MappingHandler里面的handle方法其实是用BeanFactory调用的getBean。
  • BeanFactory的实现其实也很简单。就是把类扫描器扫描到的类传进来,吧带有Controller和Bean注解的类放入map中,如果内部用Autowired注解就用内部依赖注入。只有单例模式。
public class BeanFactory {
 private static Map<Class<?>, Object> classToBean = new ConcurrentHashMap<>();
 public static Object getBean(Class<?> cls) {
 return classToBean.get(cls);
 }
 public static void initBean(List<Class<?>> classList) throws Exception {
 List<Class<?>> toCreate = new ArrayList<>(classList);
 while (toCreate.size() != 0) {
 int remainSize = toCreate.size();
 for (int i = 0; i < toCreate.size(); i++) {
 if (finishCreate(toCreate.get(i))) {
 toCreate.remove(i);
 }
 }
 if (toCreate.size() == remainSize) {
 throw new Exception("cycle dependency!");
 }
 }
 }
 private static boolean finishCreate(Class<?> cls) throws IllegalAccessException, InstantiationException {
 if (!cls.isAnnotationPresent(Bean.class) && !cls.isAnnotationPresent(Controller.class)) {
 return true;
 }
 Object bean = cls.newInstance();
 for (Field field : cls.getDeclaredFields()) {
 if (field.isAnnotationPresent(Autowired.class)) {
 Class<?> fieldType = field.getType();
 Object reliantBean = BeanFactory.getBean(fieldType);
 if (null == reliantBean) {
 return false;
 }
 field.setAccessible(true);
 field.set(bean, reliantBean);
 }
 }
 classToBean.put(cls, bean);
 return true;
 }
}
  1. 启动类
public class IlssApplication {
 public static void run(Class<?> cls, String[] args) {
 TomcatServer tomcatServer = new TomcatServer(args);
 try {
 tomcatServer.startServer();
 List<Class<?>> classList = ClassScanner.scanClasses(cls.getPackage().getName());
 BeanFactory.initBean(classList);
 HandlerManager.resolveMappingHandler(classList);
 } catch (Exception e) {
 e.printStackTrace();
 }
 }
}

2.3 关于测试模块 test

  • 做测试 内部打包的时候需要在test项目中的build.gradle加入下面配置
jar {
 manifest {
 attributes "Main-Class": "io.ilss.framework.Application"
 }
 from {
 configurations.compile.collect {
 it.isDirectory() ? it : zipTree(it)
 }
 }
}
  1. 创建Service类
@Bean
public class NumberService {
 public Integer calNumber(Integer num) {
 return num;
 }
}
  1. 创建Controller类

@Controller
public class TestController {
 @Autowired
 private NumberService numberService;
 @RequestMapping("/getNumber")
 public String getSalary(@RequestParam("name") String name, @RequestParam("num") String num) {
 return numberService.calNumber(11111) + name + num ;
 }
}
  1. 创建Application启动类
public class Application {
 public static void main(String[] args) {
 IlssApplication.run(Application.class, args);
 }
}
  1. 控制台
gradle clean install 
java -jar mvc-test/build/libs/mvc-test-1.0-SNAPSHOT.jar
  1. 访问网址
  • http://localhost:6699/getNumber?name=aaa&num=123
image

待改进的一些点

  • 异常处理,框架里面的异常我很多都是直接答应堆栈信息,并没有处理。
  • BeanFactory很简陋,因为是简易,所以真的很简易。不支持多例。大家可以试试加
  • 扩展性很差,小弟能力有限,希望大佬轻喷。
  • .......

写在最后

  • 项目的github:https://github.com/ilssio/ilss-mvc
  • 项目很简单,有很多地方还有不足,大家可以一起来改改。后面等我内功深厚了,我会再战它的。

关于我

  • 坐标杭州,普通本科在读,计算机科学与技术专业,20年毕业,目前处于实习阶段。
  • 主要做Java开发,会写点Golang、Shell。对微服务、大数据比较感兴趣,预备做这个方向。
  • 目前处于菜鸟阶段,各位大佬轻喷,小弟正在疯狂学习。
  • 欢迎大家和我交流鸭!!!

有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:苡仁ilss

查看原文:框架—记一次手写简易MVC框架的过程 附源代码

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
988 次点击
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏