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

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/imyiren/ilss-mvc
  • 项目很简单,有很多地方还有不足,大家可以一起来改改。后面等我内功深厚了,我会再战它的。

关于我

  • 坐标杭州,普通本科在读,计算机科学与技术专业,20年毕业,目前处于实习阶段。
  • 主要做Java开发,会写点Golang、Shell。对微服务、大数据比较感兴趣,预备做这个方向。
  • 目前处于菜鸟阶段,各位大佬轻喷,小弟正在疯狂学习。
  • 欢迎大家和我交流鸭!!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352

推荐阅读更多精彩内容