从头开始实现一个小型spring框架——控制器controller实现mvc请求拦截和响应

写在前面

最近学习了一下spring的相关内容,所以也就想要照猫画虎地记录和实现一下spring的框架,通过阅读这些也希望能够消除对Spring框架的恐惧,其实细心阅读框架也很容易理解。
mini-spring尽量实现spring的核心功能。文章写得很仓促,可能不是很全面,在全部完成之后我们再来完善表达吧,见谅~

项目的源码我放在了github上:源码地址

我会在这里整理文章的系列目录:

  1. 从头开始实现一个小型spring框架——手写Spring之实现SpringBoot启动
  2. 从头开始实现一个小型spring框架——手写Spring之集成Tomcat服务器
  3. 从头开始实现一个小型spring框架——控制器controller的实现
  4. 从头开始实现一个小型spring框架——实现Bean管理(IOC与DI)

一、容器内对请求的处理过程

我们在上一篇博客里讲了,容器内具体请求的处理流程对于容器是一个黑盒,根据具体的实现而有所不同。下面我们就对典型的容器内部的处理流程进行简单的总结和叙述。

1.1 请求典型流程

先看这么一张图片:

熟悉Servlet开发流程的都知道,我们需要在web.xml中配置我们的Servlet路径和请求路径,或者使用注解进行配置,才可以完成URL和Servlet之间的映射。

我们把URI到Servlet的映射配置到Xml里,当一个http请求到达服务器时,服务器会获取这个请求的URI,然后去Web.xml中查找,通过映射表找到对应的Servlet,并把这个请求转发到对应的Servlet,Servlet处理完后,再把结响应回去。这就是整个业务的处理流程了。

1.2 存在的问题

  • 服务器调度的问题
    配置集中,大而且杂乱,维护成本高
  • 需要多次实现Servlet接口

1.3 Spring的改进

Spring对这个问题的改进,是引入的一个总管一样的方式进行管理,也就是我们非常熟悉的DispatcherServlet,用来受理所有的业务。

其中的差异可以对比下面这张图片

tomcat统一将请求发送给DispatcherServlet,再由DispatcherServlet将这些请求派发给具体的Mapping Handler,处理完毕后返回。其中Servlet接口仅实现了一次。并且通过注解的方式进行配置,更简化了我们的实现流程,分散配置,业务逻辑也就变得清晰明确。

二、mvc实现

2.1 变化后的包结构

本次实现新增加了

  1. framework模块
  • core包中增加ClassScanner实现包扫描

  • 增加handler包实现反射获取类信息

  • mvc包增加注解

    • Controller注解标志类
    • RequestMapping注解方法
    • RequestParam注解参数
  • 修改servlet包实现全局 / 路径下的uri拦截

  1. test模块
  • 增加controller实现测试请求

2.2 framework模块实现DispatcherServlet和反射获取类信息

core包下的ClassScanner类


package com.qcby.core;

import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author kevinlyz
 * @ClassName ClassScanner
 * @Description 通过类加载器获取目录下的类列表
 * @Date 2019-06-08 17:26
 **/
public class ClassScanner {

    public static List<Class<?>> scannClasses(String packageName) throws IOException, ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();
        String path = packageName.replace(".","/");
        //获取类加载器
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Enumeration<URL> resources = classLoader.getResources(path);
        while (resources.hasMoreElements()){
            URL resource = resources.nextElement();
            //处理资源类型是jar包的情况
            if (resource.getProtocol().contains("jar")){
                JarURLConnection jarURLConnection =
                        (JarURLConnection) resource.openConnection();
                String jarFilePath = jarURLConnection.getJarFile().getName();
                classes.addAll(getClassFromJar(jarFilePath,path));
            }else{
                // TODO: 处理jar包以外的情况
            }
        }
        return classes;
    }

    /**
     * @Author kevinlyz
     * @Description 从jar包中获取资源
     * @Date 17:37 2019-06-08
     * @Param
     * @return List<Class<?>>
     **/
    public static List<Class<?>> getClassFromJar(String jarFilePath,String path) throws IOException, ClassNotFoundException {
        List<Class<?>> classes = new ArrayList<>();
        //获取jar实例
        JarFile jarFile = new JarFile(jarFilePath);
        Enumeration<JarEntry> jarEntrys = jarFile.entries();
        while (jarEntrys.hasMoreElements()){
            JarEntry jarEntry = jarEntrys.nextElement();
            //获取类路径名  如  com/qcby/test/Test.class
            String entryName = jarEntry.getName();
            //获取的
            if (entryName.startsWith(path)&&entryName.endsWith(".class")){
                //路径替换
                String classFullName = entryName.replace("/",".").substring(0,entryName.length()-6);
                //反射获取类信息并添加至list
                classes.add(Class.forName(classFullName));
            }
        }
        return classes;
    }
}

scannClasses之后会在MiniApplication中被调用,用于获取类列表。

MiniApplication类,添加获取类列表和反射调用。

package com.qcby.starter;

import com.qcby.core.ClassScanner;
import com.qcby.web.handler.HandlerManagger;
import com.qcby.web.server.TomcatServer;

import java.util.List;

/**
 * @author kevinlyz
 * @ClassName MiniApplication
 * @Description 框架的入口类
 * @Date 2019-06-04 19:21
 **/
public class MiniApplication {
    public static void run(Class<?> cls,String[] args){
        System.out.println("Hello mini-spring application!");
        TomcatServer tomcatServer = new TomcatServer(args);
        try {
            tomcatServer.startServer();
            List<Class<?>> classList = ClassScanner.scannClasses(cls.getPackage().getName());
            classList.forEach(it->System.out.println(it.getName()));
            HandlerManagger.resolveMappingHandler(classList);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

HandlerManager类

package com.qcby.web.handler;

import com.qcby.web.mvc.Controller;
import com.qcby.web.mvc.RequestMapping;
import com.qcby.web.mvc.RequestParam;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.List;

/**
 * @author kevinlyz
 * @ClassName HandlerManagger
 * @Description 反射获取类信息
 * @Date 2019-06-08 18:34
 **/
public class HandlerManagger {

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

    public static void resolveMappingHandler(List<Class<?>>  classList){
        for (Class<?> cls : classList){
            if (cls.isAnnotationPresent(Controller.class)){
                presentHandlerFromController(cls);
            }
        }
    }

    private static void presentHandlerFromController(Class<?> cls) {
        //获取方法
        Method[] methods= cls.getDeclaredMethods();
        for (Method method : methods){
           if(!method.isAnnotationPresent(RequestMapping.class))
               continue;
           //获取uri路径
           String uri = method.getDeclaredAnnotation(RequestMapping.class).value();
           List<String> paramList = new ArrayList<>();
           //获取参数值
           for (Parameter parameter : method.getParameters()){
               if (parameter.isAnnotationPresent(RequestParam.class)){
                   paramList.add(parameter.getDeclaredAnnotation(RequestParam.class).value());
               }
           }
           String[] params= paramList.toArray(new String[paramList.size()]);
           MappingHandler mappingHandler = new MappingHandler(uri,method,cls,params);
           HandlerManagger.mappingHandlerList.add(mappingHandler);
        }

    }

}

MappingHandler

package com.qcby.web.handler;


import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author kevinlyz
 * @ClassName MappingHandler
 * @Description 包含uri,类的方法信息,类信息和参数
 * @Date 2019-06-08 18:32
 **/
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;
    }

    public boolean handle(ServletRequest req, ServletResponse res) throws IllegalAccessException, InstantiationException, InvocationTargetException, IOException {
        String reqUri = ((HttpServletRequest)req).getRequestURI();
        if (!this.uri.equals(reqUri))
            return false;

        //相等则调用Handler的resolveMappingHandler方法,实例化并返回
        Object[] parameters = new Object[args.length];
        for(int i=0;i<args.length;i++){
            parameters[i] = req.getParameter(args[i]);
        }

        Object ctl = controller.newInstance();
        Object response = method.invoke(ctl,parameters);
        res.getWriter().println(response.toString());
        return true;
    }
}

mvc包下

Controller自定义注解

package com.qcby.web.mvc;


import java.lang.annotation.*;


/**
 * @Author kevinlyz
 * @Description 控制器注解,添加在Controller上
 * @Date 17:10 2019-06-08
 **/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Controller {

}

RequestMapping自定义注解

package com.qcby.web.mvc;


import java.lang.annotation.*;

/**
 * @Author kevinlyz
 * @Description 映射注解,添加在方法上
 * @Date 17:11 2019-06-08

 **/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestMapping {
    String value();
}

RequestParam自定义注解

package com.qcby.web.mvc;

import java.lang.annotation.*;

/**
 * @Author kevinlyz
 * @Description 参数注解,添加在方法参数上
 * @Date 17:10 2019-06-08
 **/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RequestParam {
    String value();
}

server包下的TomcatServer(修改拦截uri为 /,并将请求转发至DispatcherServlet)

package com.qcby.web.server;

import com.qcby.web.servlet.DispatcherServlet;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;

/**
 * @author kevinlyz
 * @ClassName TomcatServer
 * @Description 集成Tomcat服务器,将请求转发至DispatcherServlet
 * @Date 2019-06-05 13:10
 **/
public class TomcatServer {
    private Tomcat tomcat;
    private String[] agrs;

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

    public void startServer() throws LifecycleException {
        //实例化tomcat
        tomcat = new Tomcat();
        tomcat.setPort(9999);
        tomcat.start();
        //实例化context容器
        Context context = new StandardContext();
        context.setPath("");
        context.addLifecycleListener(new Tomcat.FixContextListener());
        DispatcherServlet servlet = new DispatcherServlet();
        Tomcat.addServlet(context,"dispatcherServlet",servlet).setAsyncSupported(true);
        //添加映射
        context.addServletMappingDecoded("/","dispatcherServlet");
        tomcat.getHost().addChild(context);

        //设置常驻线程防止tomcat中途退出
        Thread awaitThread = new Thread("tomcat_await_thread."){
            @Override
            public void run() {
                TomcatServer.this.tomcat.getServer().await();
            }
        };
        //设置为非守护线程
        awaitThread.setDaemon(false);
        awaitThread.start();
    }
}

servlet包:

package com.qcby.web.servlet;

import com.qcby.web.handler.HandlerManagger;
import com.qcby.web.handler.MappingHandler;

import javax.servlet.*;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

/**
 * @author kevinlyz
 * @ClassName TestServlet
 * @Description 处理请求,请求拦截和匹配,若不存在对应uri则直接返回
 * @Date 2019-06-05 13:28
 **/
public class DispatcherServlet implements Servlet {
    @Override
    public void init(ServletConfig config) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

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

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

至此,framework核心的请求拦截就处理完成了

2.3 test模块测试

我们在test模块下新建一个UserController类用于测试我们的请求过程。

package com.qcby.controller;

import com.qcby.web.mvc.Controller;
import com.qcby.web.mvc.RequestMapping;
import com.qcby.web.mvc.RequestParam;

import java.lang.annotation.Retention;

/**
 * @author kevinlyz
 * @ClassName UserController
 * @Description 测试请求
 * @Date 2019-06-08 17:14
 **/
@Controller
public class UserController {

    @RequestMapping("/test")
    public String test(@RequestParam("name") String name,@RequestParam("desc") String desc){
        System.out.println("test访问了!");
        return "hello controller!";
    }
}

同样gradle build

java -jar test/build/libs/test-1.0-SNAPSHOT.jar 

打开我们的浏览器,输入http://localhost:9999/test
看到输出的hello controller!

深藏功与名!!!

三、小结

今天我们对SpringMvc的核心功能进行了实现,通过使用包扫描和反射机制获取到注解的信息,并进行实例化,最终实现请求的拦截和相应。
(写的不是很详细,改日再补)
test的Application向framework模块的MiniApplication传递类信息,MiniApplication调用ClassScanner进行包扫描,通过类加载器扫描包中的类路径(其中对jar包的信息做了进一步处理),并返回classList类列表至MiniApplication;进而使用HandlerManagger获取类信息,并通过HandlerManager进行组装。
TomcatServer则更改其拦截的请求uri路径为/,以拦截所有通过tomcat的请求,并转发至DispatcherServlet,DispatcherServlet的service方法进行uri匹配,匹配成功则调用Handler的resolveMappingHandler方法,实例化并返回响应信息。
然后我们又通过test模块书写了UserController进行测试,成功返回hello controller的字符串。至此我们的mvc的请求控制功能就完全实现了,仔细想想是不是很简单呢~~

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值