美文网首页spring-boot
手写简易SpringMVC框架(一):注解、内嵌Tomcat、类

手写简易SpringMVC框架(一):注解、内嵌Tomcat、类

作者: 饿了就下楼 | 来源:发表于2019-10-26 22:25 被阅读0次

    综述

      本文主要参考了昵称为“枕边书”的大佬手写的一个mini springmvc视频。我的代码放在了这里。在本文中我将尽可能对于源代码进行比较细致的讲解,当然个人能力有限,有些错误在所难免,希望大家批评指正。

    目录结构

      整个工程整体的代码结构如下,整个工程包含了两个部分:


    代码目录结构.png

      mvc-framework是本文所要讲的springmvc这个简易框架,其中记录了这个简易的springmvc是如何工作的。而my-application则是个人应用程序,它将mvc-framework包导入。通俗理解一下就是,你可以把mvc-framework理解成我们平时使用的SpringMVC的jar包,然后my-application就是基于SpringMVC自己开发的java web应用,而我们在应用开发的时候需要导入SpringMVC的jar包。

    1 framework代码详解

      在讲解之前我们首先看一下mvc-framework模块下的每个包中都放了些什么东西。

    mvc-framwork包结构
      annotation中是模拟的一些SpringMVC中的注解,当然这些注解的功能没有SpringMVC中的那么全面。由于是模拟了相应的注解,所以在我的代码中我都以'My'开头。beans下的BeanFactory类用于管理bean,例如,一个@MyController注解的实例,就是一个bean。core中的ClassScanner主要用于执行组件的扫描。handler用于管理url映射和对应的Controller或者method间的对应关系。server中放了一个TomcatServer,我们这里是采用了内嵌tomcat的形式(这也让我想到了springboot中也内嵌了tomcat)。servlet中的MyDispatcherServlet就是模拟了DispatcherServlet。另外我们还为当前框架设置了一个启动入口类放在starter包下。

    1.1 注解

       我们先来看一下注解部分,@MyAutoWired注解的代码如下,关于注解的相关参数,大家可以去网上找一些资料查看一下,在这里这个注解添加在字段上。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface MyAutoWired {
    
    }
    

       其他的几个注解我也一并放在这里。注解这一块比较简单,大家可以直接看源码,如何对添加了注解的实例进行扫描和解析,后文会讲。

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyBean {
    }
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface MyController {
    }
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)  //为了简单,此注解只能添加在 方法上,且只支持一个uri
    public @interface MyRequestMapping {
        String value();  //value可以指定映射路径
    }
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface MyRequestParam {
        String value();   //value可以指定请求参数中需要的参数名
    }
    

    1.2 启动入口类MVCApplication

       说道这个,可能有些时候不能反应过来这是个啥,但是如果你有过一些SpringBoot的开发经验,那你一定接触过下面的应用启动类:

    @SpringBootApplication
    @ComponentScan(basePackages={"aaa.bbb.ccc.*"})
    @MapperScan("111.222.333.mapper")
    public class XxxYyyApplication {
        public static void main(String[] args) {
            SpringApplication.run(XxxYyyApplication.class,  args);
        }
    }
    

    MVCApplication就相当于上面代码中的SpringApplication,实际上你点开MVCApplication的源码,你会发现它也恰好有一个 run() 方法(hhh确实是有点模拟springboot的味道),注意这里传入的class参数为用户自己的应用程序启动类的类型。

    public class MVCApplication {
        public static void run(Class<?> cls) {
            System.out.println("=============================start!!!!==========================");
            TomcatServer tomcatServer = new TomcatServer();     //1
            try {
                tomcatServer.startServer();
                List<Class<?>> classList = ClassScanner.scanClasses(cls.getPackage().getName());   //2
                BeanFactory.initBean(classList);  //3
                MyMappingHandlerManager.resolveMappingHander(classList);  //4
                for (MyMappingHandler myMappingHandler : MyMappingHandlerManager.mappingHandlerList) {
                    //5
                    System.out.println(myMappingHandler.getUri() + ":" + myMappingHandler.getMethod());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    run 方法整体的思路比较简单:

    • 1 生成内嵌的Tomcat服务器后启动它
    • 2 以传入的用户应用程序启动类所在的包为基础,进行组件扫描
    • 3 将扫描的类,进行bean实例化,交由BeanFactory管理
    • 4 解析url映射,完成这一步就让系统知道出现某个url的请求,应该对应到哪个controller的哪个方法上去。这一部分后文会详解
    • 5 打印映射路径和其对应的方法(可忽略)

    1.3 内嵌的Tomcat的配置和启动

       内嵌tamcat的使用和配置可以参见实现内嵌tomcat,这里不再赘述了。

    public class TomcatServer {
        private Tomcat tomcat;
        public TomcatServer() {
        }
    
        public void startServer() throws LifecycleException {
            this.tomcat = new Tomcat();
            tomcat.setHostname("localhost");
            tomcat.setPort(8080);
            Context context = new StandardContext();
            context.setPath("");
            context.addLifecycleListener(new Tomcat.FixContextListener());
            MyDispatcherServlet dispatcherServlet = new MyDispatcherServlet();
            Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet).setAsyncSupported(true);
            context.addServletMappingDecoded("/", "dispatcherServlet");
            tomcat.getHost().addChild(context);
            tomcat.start();
            Thread awaitThread = new Thread("tomcat_await_thread"){
                @Override
                public void run(){
                    TomcatServer.this.tomcat.getServer().await();
                }
            };
            awaitThread.setDaemon(false);
            awaitThread.start();
        }
    }
    

       startServer方法就做了两件事,一是配置,二是启动tomcat。配置了主机名,端口号,上下文路径为空(意味着8080/后面可以直接跟着请求路径了),并且添加注册了一个servlet,名为dispatcherServlet,具体是MyDispatcherServlet实例(马上就讲)。

    1.4 MyDispatcherServlet

       有过java web开发经验的朋友肯定对DispatcherServlet不陌生。老样子,直接上代码。

    public class MyDispatcherServlet extends HttpServlet{
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String requestURI = req.getRequestURI();
            for (MyMappingHandler mappingHandler : MyMappingHandlerManager.mappingHandlerList) {
                if(requestURI.equals(mappingHandler.getUri())){
                    try {
                        mappingHandler.handle(req, resp);
                    } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    break;
                }
            }
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doGet(req, resp);
        }
    }
    

       为了简单,我们这里不去区分get和post。doGet方法也很清晰,首先要获得请求的请求路径,然后从所有的映射关系封装类型MyMappingHandler的列表中,找到对应该请求的那个MyMappingHandler对象,并由它来处理这个请求。现在也许还不太清楚MyMappingHandler到底是啥(因为我还没讲hhhh),要知道它存放了映射路径的uri和对应的method,以及可以处理相应的请求就ok啦。这里也可以看出DispatcherServlet只是负责的请求的分发,并不处理请求。

    1.5 类扫描ClassScanner

       至此我们算是进入到了比较核心的几个部分。首先就是上文提到的类扫描,主要逻辑在ClassScanner.scanClasses()方法中(如果忘了springmvc是如何启动的,请返回上文看1.2 节)。首先这里我们要明确,所谓的类的扫描,扫描的是哪些类,是framework中的类?还是我们自己写的应用程序中的类?很显然是后者。我们先贴代码。

     public static List<Class<?>> scanClasses(String basePackageName) throws IOException, ClassNotFoundException {
            List<Class<?>> classList = new ArrayList<>();
    
            //这里的packageName是 用户应用程序入口类所在的package名称,以它为基础进行组件扫描
            String path = basePackageName.replace(".", "/");
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    
            Enumeration<URL> resources = classLoader.getResources(path);
            while (resources.hasMoreElements()) {
                URL resource = resources.nextElement();
                String protocol = resource.getProtocol();
                if (protocol.contains("jar")) {
                    //如果应用程序是以jar包的方式运行(java -jar),那么就会进入到这里面。
                    JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();
                    String jarFilePath = jarURLConnection.getJarFile().getName();
                    classList.addAll(getClassesFromJar(jarFilePath, path));
                } else if (protocol.contains("file")) {
                    //有时候我们想要对程序进行调试,而不是以jar方式运行。那么就会进入到这里。
                    String fileOrPath = resource.getFile();
                    classList.addAll(getClassFromPath(fileOrPath, basePackageName));
                }
            }
            return classList;
        }
    

       这里有两种情况,一种是用户的应用程序会打成一个jar包,然后java -jar运行这个jar包,那么scanClasses就需要在jar包中进行类的扫描(后文测试的部分我会演示运行jar包);另一种是有些时候我们想要调试程序,那么编译之后的类会放在target目录中,需要在target中进行类的扫描。关于jar的方式如何进行扫描的,我也没有仔细研究过。不管怎么样,组件扫描的最终结果就是,将应用程序中用户的类都扫描到放入classList中返回,所以这部分就不展开讲了。

    //扫描jar包中的类
    
    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;
        }
    
    //扫描target目录下的类
    
    private static List<Class<?>> getClassFromPath(String fileOrPath, String packageName) throws ClassNotFoundException {
            List<Class<?>> list = new ArrayList<>();
            List<String> listOfClassAbsoluteName = new ArrayList<>();
            File[] files = new File(fileOrPath).listFiles();
            recursiveFindClass(listOfClassAbsoluteName, files);
            int l = fileOrPath.length();
            for (String classAbsoluteName : listOfClassAbsoluteName) {
                String className = packageName + "." + classAbsoluteName.substring(l, classAbsoluteName.length() - 6).replace(String.valueOf(File.separatorChar), ".");
                list.add(Class.forName(className));
            }
            return list;
        }
    
        private static void recursiveFindClass(List<String> list, File[] files) {
            if (files == null) {
                return;
            }
            for (File file : files) {
                if (file.isFile() && file.getName().endsWith(".class")) {
                    list.add(file.getAbsolutePath());
                } else if (file.isDirectory()) {
                    recursiveFindClass(list, file.listFiles());
                }
            }
        }
    

       好了第(一)部分就先写到这里了,辛苦大家观看到这里,(二)部分主要讲解bean的实例化以及MappingHandler等,(三)部分就是一个十分简单的应用。谢谢大家!

    甩一下连接:
    ·手写简易SpringMVC框架(二):bean实例化、MappingHandler
    ·手写简易SpringMVC框架(三):极其简单的应用

    相关文章

      网友评论

        本文标题:手写简易SpringMVC框架(一):注解、内嵌Tomcat、类

        本文链接:https://www.haomeiwen.com/subject/hhgyvctx.html