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

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

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

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