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

综述

  本文主要参考了昵称为“枕边书”的大佬手写的一个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> 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> scanClasses(String basePackageName) throws IOException, ClassNotFoundException {
        List> classList = new ArrayList<>();

        //这里的packageName是 用户应用程序入口类所在的package名称,以它为基础进行组件扫描
        String path = basePackageName.replace(".", "/");
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        Enumeration 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> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
        List> classes = new ArrayList<>();
        JarFile jarFile = new JarFile(jarFilePath);
        Enumeration 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> getClassFromPath(String fileOrPath, String packageName) throws ClassNotFoundException {
        List> list = new ArrayList<>();
        List 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 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、类扫描)