手写一个MINI SpringMVC

对于一个只会使用SpringBoot而不知道其内部原理的菜鸡,准备系统学习一下Spring的实现。于是第一节课学习的是《仅需2小时 手写MINI Spring MVC框架》。

一、开发准备

1.Gradle

在Web开发时,常用的依赖管理工具通常是Maven。不过Gradle相比于Maven语法更加简单,且不需要安装,更加灵活,所以此项目使用Gradle。

新建项目时特别简单,只需要在idea中使用Gradle创建项目即可。创建完毕项目后,在项目中添加两个模块:framework,test。在build.gradle中,可配置项目或模块的依赖。

子模块可以继承父模块的配置,需要配置在父模块build.gradle中的subprojects项中

2.仿Spring的项目结构

由于Spring有很多功能,所以在我们写的这个mini-spring的framework模块中,以不同的包来划分不同的功能。
Spring的包结构大致如下:
在这里插入图片描述
在我们自己的设计中,要实现以下结构:

  • Core模块,核心层:Beans、Core、Context。(SpEL是提供表达式的,暂且不实现)
  • 实现Web,集成Web和WebMVC
  • 添加starter,实现类似于spring-boot的启动方式

于是在framework下创建以下包:
在这里插入图片描述
并且在test模块下设置启动类。并且在test中的build.gradle中添加下列配置:

jar{
    manifest{
        attributes "Main-Class":"com.dlc.Appliation"
    }
    from{
        configurations.compile.collect{
            it.isDirectory() ? it : zipTree(it)
        }
    }
}

其中,manifest即“主清单”,manifest是打出jar包的配置文件,它会被放在META-INF文件夹内。from中是自动打入依赖包的配置。

为了让test模块可以依赖framework模块,还需要在build.gradle的dependencies中添加配置compile(project(':framework'))。这样在test打出的jar包中就会自动包含framework模块。不过除此之外,还需要在代码层面添加联系

在framework模块starter包中,添加一个MiniApplication类,作为框架的入口。

public class MiniApplication {
    public static void run(Class<?> cls,String[] args){
        System.out.println("hello mini-spring");
    }
}

在应用(test)的入口类调用这个run方法,是否可以成功运行呢?

public class Application {
    public static void main(String[] args) {
        System.out.println("hello world");
        MiniApplication.run(Application.class,args);
    }
}

在这里插入图片描述
可以看到成功运行,其实这里的Application也就是Spring项目中的启动类

二、Web功能开发

1.Web服务器

众所周知,我们的项目想要向外提供服务需要Web服务器

类似于Tomcat、Nginx、Netty,他们通常都是监听一个TCP端口,然后转发请求、回收响应。它们本身没有逻辑,只是负责连接操作系统和应用程序代码

2.Web服务模型

在这里插入图片描述
我们要实现的就是Web服务器的功能,首先我们要了解,Web服务器是如何连接Web请求和应用程序的。这个过程叫做请求分发
请求分发其实就像是快递员一样,它负责根据地址转发给不同的应用程序,然后再回收响应返回给请求者。

3.Servlet

Servlet其实是一个广泛的定义,他可以表示一种规范、一个接口,但最常用的定义还是任何实现了javax.servlet.Servlet这个接口的类

所以Web服务器其实就是接收ServletRequest,经过应用程序处理后,返回ServletResponse的一个功能。

4.集成Tomcat

要自己去实现一个Web服务器,还是挺不现实滴,所以这里选择在项目中集成Tomcat,这是因为Tomcat有以下优点:

  • Java原生,运行在JVM上;
  • 多种并发模型,高性能;
  • 支持嵌入式应用程序

首先在framework的build.gradle添加依赖:compile group: 'org.apache.tomcat.embed',name: 'tomcat-embed-core',version: '8.5.23'
然后新建类web.server.TomcatServer:

public class TomcatServer {
    private Tomcat tomcat;
    private String[] args;

    public TomcatServer(String[] args) {
        this.args = args;
    }

    /**
     * 启动Tomcat的方法
     */
    public void startServer() throws LifecycleException {
        tomcat = new Tomcat();
        tomcat.setPort(6699);
        tomcat.start();
        //增加一个常驻线程,防止中途退出
        Thread awaitThread = new Thread("tomcat_await_thread"){
            @Override
            public void run(){
                TomcatServer.this.tomcat.getServer().await();
            }
        };
        awaitThread.setDaemon(false); //设为非守护线程
        awaitThread.start();
    }
}

然后在启动类中使用TomcatServer。(即在MiniApplication中调用startServer方法)这样,在启动项目时,Tomcat便也启动了。
此时项目还是不能运行的,因为Tomcat不知道怎么处理这个Servlet。所以在framework模块的web包下,新建一个servlet包,在其中新建一个实现Servlet接口的TestServlet类。
然后在TomcatServer 中建立Tomcat和Servlet的联系:

        Context context = new StandardContext();
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());
        TestServlet servlet = new TestServlet();
        Tomcat.addServlet(context,"testServlet",servlet).setAsyncSupported(true);
        context.addServletMappingDecoded("/test.json","testServlet");
        tomcat.getHost().addChild(context);

此时项目就可以跑起来了,访问/test.json路径就可以看到写入的值。

三、实现Controller

上面的例子中,直接将Servlet与路径的映射关系写死在框架里,这不用多说,肯定是不对的。而作为MVC中的C——Controller,就可以实现请求分发的功能,那么这是怎么实现的呢?

1.DispatcherServlet

在SpringMVC中,DispatcherServlet就是负责请求转发的Servlet。
DispatcherServlet 对请求URL进行解析,得到请求资源标识符(URI),然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括一个Handler处理器对象、多个HandlerInterceptor拦截器对象),最后以HandlerExecutionChain对象的形式返回。
  DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)
  Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;根据返回的ModelAndView,选择一个适合的ViewResolver返回给DispatcherServlet;ViewResolver 结合Model和View,来渲染视图,最后将渲染结果返回给客户端。

2.实现Controller注解

将之前TestServlet与Tomcat的对应关系进行修改,将TestServlet改为DispatcherServlet。然后新建一个web.mvc包,在其中就进入了激动人心的实现@Controller@RequestMapping@RequestParam注解环节。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {
}

这是Controller注解的代码,其余两个类似,区别在于其修饰的类型不同,而且@RequestMapping@RequestParam还需要保存值。
然后在test模块写一个controller出来试试:

@Controller
public class SalaryController {
    @RequestMapping("getSalary.json")
    public Integer getSalary(@RequestParam("name") String name, @RequestParam("experience") String experience){
        return 10000;
    }
}

是不是真有那味儿了。但是这样还是不行的,因为框架还不知道添加了哪些Controller,所以还要实现类扫描的功能。

3.实现类扫描器
public class ClassScanner {
    public static List<Class<?>> scanClass(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")) { //如果资源是jar包
                JarURLConnection jarURLConnection = (JarURLConnection) resource.openConnection();
                String jarFilePath = jarURLConnection.getJarFile().getName();
                classList.addAll(getClassesFromJar(jarFilePath, path));
            } else {
                //todo
            }
        }
        return classList;
    }

    private static List<Class<?>> getClassesFromJar(String jarFilePath, String path) throws IOException, ClassNotFoundException {
        List<Class<?>> classList = new ArrayList<>();
        JarFile jarFile = new JarFile(jarFilePath);
        Enumeration<JarEntry> jarEntries = jarFile.entries();
        while (jarEntries.hasMoreElements()) {
            JarEntry jarEntry = jarEntries.nextElement();
            String entryName = jarEntry.getName();  //com/dlc/test/Test.class
            if (entryName.startsWith(path) && entryName.endsWith(".class")) {
                String classFullName = entryName.replace("/", ".")
                        .substring(0, entryName.length() - 6);
                classList.add(Class.forName(classFullName));
            }
        }
        return classList;
    }
}
4.MappingHandler

需要将Controller中定义的MappingHandler提取出来,使用反射。反射活跃于运行时,可以在运行时动态地获取类的属性和方法,动态实例化类。

定义MappingHandler类,其包括uri,method,controller,args这几种属性,作为一种映射。
然后定义管理它的HandlerManager类:

public class HandlerManager {
    public static List<MappingHandler> mappingHandlerList = new ArrayList<>();

    public static void resolveMappingHandler(List<Class<?>> classList){
    	//一步一步找到类、方法、属性·
        for (Class<?> cls:classList){
            if(cls.isAnnotationPresent(Controller.class)){
                //找到被Controller注解的类
                parseHandlerFromController(cls);
            }
        }
    }

    private static void parseHandlerFromController(Class<?> cls) {
        //使用反射获取这个类的所有方法
        Method[] methods = cls.getDeclaredMethods();
        for (Method method : methods) {
            //找到被RequestMapping注解的方法
            if(!method.isAnnotationPresent(RequestMapping.class)){
                continue;
            }
            String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
            List<String> paramNameList = new ArrayList<>();
            for (Parameter parameter : method.getParameters()) {
                //找到被RequestParam注解的参数
                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.mappingHandlerList.add(mappingHandler);
        }

然后,在负责分发的DispatcherServlet使用这个HandlerManager 。

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        for (MappingHandler mappingHandler : HandlerManager.mappingHandlerList) {
            try {
                if(mappingHandler.handle(req,res)){
                    return;
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

其中,mappingHandler.handle(req,res)是判断路径中的uri和MappingHandler中的uri是否相同。

四、Bean管理

上面已经实现了对Controller的解析,不过还要将项目中复杂的逻辑处理到Model里。在Spring中一般以Service表示,当Service过多,多个线程重复创建会造成损耗。所以Spring使用Bean进行管理。

1.Bean

Bean本质上也是对象,只不过比较特殊:

  • 生命周期较长,从JVM创建到销毁
  • 在整个虚拟机可见
  • 维护成本高,单例存在

由于这些特性,Bean的优势如下:

  • 运行期效率高,使用时不需要初始化
  • 统一维护,便于管理和扩展

就比如在Spring中,我们在Controller里使用Service,只需要@Autowired注释一下Service即可。但是,如果用new的方式,我们就要先new一个Mapper传给Service,再new一个Service。很麻烦,而且性能差。

在Spring中,Bean的实现方式是:

  • 包扫描并自动装配(反射)
  • BeanFactory(统一管理)
  • 依赖注入
2.控制反转/依赖注入

控制反转(IOC)是一种设计思想,用于解耦。依赖注入(DI)是实现控制反转的一种方式。

假如用普通控制方式,A类中聚合一个X类,则创建A时,要先创建一个X。

当使用控制反转时,除了A和X,还要有一个第三方容器存在,在Spring中就是BeanFactory。在A类通过属性确定它的依赖X之后,BeanFactory就会在创建A之前就先创建好一个X对象,并将X注入A的属性中
这两者的区别在于,第一种是A主动控制,而第二种A是被动引入。

3.实现依赖注入
  1. 扫描包获得类定义(之前已完成)
  2. 初始化Bean,并实现依赖注入
  3. 解决Bean初始化顺序问题

解决次序问题:
遍历所有的Class,判断是否有依赖,如果没有依赖直接实例化,放到Bean工厂内。
如果其依赖其他的Bean,就要判断其所依赖的是否已经存在于Bean工厂,如果已经存在就用反射set到正在创建的Bean里。如果其所依赖的Bean在工厂中未找到,就先放弃这个Bean,等下一轮遍历再进行判断。
(还要解决多个Bean循环依赖)

4.实现注解

@Bean@Autowired注解与Controller等类似,主要区别修饰的类型。
最重要的是BeanFactory的编写,其控制着Bean的装载。

public class BeanFactory {
    //类型与Bean的映射关系
    private static Map<Class<?>,Object> clasToBean = new ConcurrentHashMap<>();

    public static Object getBean(Class<?> cls){
        return clasToBean.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)){
            //不是Bean就直接不用创建
            return true;
        }
        Object bean = cls.newInstance();
        for (Field field : cls.getDeclaredFields()) {
            if(field.isAnnotationPresent(Autowired.class)){
                //如果某属性需要被装载,则去Bean工厂中查看
                Class<?> fieldType = field.getType();
                Object reliantBean = getBean(fieldType);
                if(reliantBean==null) return false;
                field.setAccessible(true);
                field.set(bean,reliantBean);
            }
        }
        clasToBean.put(cls,bean);
        return true;
    }
}

5.测试一下
@Controller
public class SalaryController {
    @Autowired
    private SalaryService salaryService;

    @RequestMapping("/get_salary.json")
    public Integer getSalary(@RequestParam("name") String name, @RequestParam("experience") String experience){
        return salaryService.calSalary(Integer.valueOf(experience));
    }
}

这是test中编写的代码。运行之后的结果:
在这里插入图片描述
框架真的奏效了!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值