手写SpringMVC(简易版)

在上一篇博客中说到这里我们要进行手写SpringMVC,因此最好是将上一篇博客中的SpringMVC源码分析那一块部分搞懂,或者观看动力节点老杜的SpringMVC源码分析再来看这里的书写框架。

首先我们要知道对于一个完整系统的参与者(即一个完整的web项目包括了什么)

一,所需要的类以及建的包

我们需要先将空壳搭好,根据之前的阅读源码部分可知:

1,HandlerExecutionChain 类

2,HandlerMapping处理器映射器接口,其中专门为@RequestMapping注解服务的处理器映射器:RequestMappingHandlerMapping(根据URI找到对应的Controller)

3,HandlerInterceptor 拦截器接口

4,HandlerAdapter 处理器适配器接口,我们这边只实现其中的给@RequestMapping注解使用的实现类,因此我们需要RequestmappingHandlerAdapter实现类

5,ModelAndView类

6,ViewResolver接口(实现类有ThymeleafViewResolver... ...)

View接口(实现类有ThymeleafView... ...)

这边我们使用JSP模板引擎,因为其对应的View和ViewResolver接口的实现类都是内置的,SpringMVC框架内部提供好了:InternalResourceViewResolver, InternalResourceView

7,还有我们在编写Controller时常用的两个注解:@Controller和@RequestMapping

整个项目的结构目录如下图所示:

在这里我们最核心的一个类应该是DispatcherServlet(前端控制器),其下的doDispatch是最核心的方法。而所有的Servlet都要实现Servlet接口,或者直接继承HttpServlet(javaweb规范),因此我们的DispatcherServlet要去继承HttpServlet。重写service(带http的)

二,站在web项目开发者的角度

在前面我们开发SpringMVC的web项目的时候,我们需要先配置web.xml中的前端控制器servlet,配置<init-param>的时候让它定位到springmvc.xml配置文件中。

然后我们需要来到springmvc.xml中分别配置组件扫描,视图解析器,拦截器。

在配置组件扫描的时候我们又发现我们需要一个controller包来供扫描。因此我们又来到Controller下,写一个方法上面带上@RequestMapping(value = "/", method = RequestMethod.GET)注解,类上带上Controller注解纳入IoC容器管理。

然后编写拦截器的时候我们又发现我们需要Interceptor类拦截器,因此我们创建出来实现HandlerInterceptor,并重写接口下的三个方法:preHandle,postHandle,afterCompletion。

因此我们在项目下创建一个包,里面写下Controller层的对应实现:

java 复制代码
import org.springmvc.stereotype.Controller;
import org.springmvc.ui.ModelMap;
import org.springmvc.web.bind.annotation.RequestMapping;
import org.springmvc.web.bind.annotation.RequestMethod;

@Controller
public class UserController {
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public String Index(ModelMap modelMap){
        modelMap.addAttribute("username", "lisi");
        return "index";
    }
}

包结构如下图所示:

提供一下springmvc.xml里的具体配置,后续手写框架的时候可以根据这个配置文件中的标签来编写:

XML 复制代码
<?xml version="1.0" encoding="UTF-8" ?>

<beans>
    <!--组件扫描-->
    <component-scan base-package="com.ryy.oa.controller" />

    <!--视图解析器-->
    <bean class="org.springmvc.web.servlet.view.InternalResourceViewResolver">
        <!--前缀-->
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <!--后缀-->
        <property name="suffix" value=".jsp"/>
    </bean>

    <!--拦截器-->
    <interceptors>
        <bean class="com.ryy.oa.interceptors.Interceptor1"/>
        <bean class="com.ryy.oa.interceptors.Interceptor2"/>
    </interceptors>
</beans>

三,编写具体框架

1,找到Springmvc.xml配置文件

编写框架肯定避免不了读取配置文件这一步,因此我们先来到springmvc.xml配置文件,Tomcat解析项目的时候发现<load-on-startup>就要初始化DispatcherServlet并调用init方法来初始化SpringWeb容器,那初始化了哪些对象呢?

解析springmvc.xml下的组件扫描并创建所有的controller对象,视图解析器要创建出来,拦截器对象创建出来放入IoC容器管理。

那我们在DispatcherServlet中要根据 :

<init-param>

<param-name>contextConfigLocation</param-name>

<param-value>classpath:springmvc.xml</param-value>

</init-param>

找到springmvc.xml配置文件。

对于Servlet来说以上信息是封装在ServletConfig对象中,该对象是服务器创建好的,并且Tomcat调用init的时候会自动将创建好的ServletConfig对象传递给init方法。因此我们可以直接调用this.getServletConfig()方法来获取ServletConfig对象。

然后是第二个参数<param-value>,要判断是否是以classpath:开头的,若条件成立表示配置文件要从类的路径中查找:

String springMvcConfigPath = Thread.currentThread().getContextClassLoader()

.getResource(contextConfigLocation.substring(Const.PREFIX_CLASSPATH.length())).getPath();

表示获取配置文件的绝对路径。但是这样写有个问题就是输出的绝对路径由于编码问题会出现百分号,因此我们还需要一步解码操作:

springMvcConfigPath = URLDecoder.decode(springMvcConfigPath, Charset.defaultCharset()); Charset.defaultCharset()可以理解为UTF-8。

java 复制代码
public class DispatcherServlet extends HttpServlet {
    //DispatcherServlet对象不需要我们去new,这个对象中的方法也不需要我们去调用,由Tomcat服务器来调用
    /**
     * 视图解析器
     */
    private ViewResolver viewResolver;
    /**
     * 处理器适配器
     */
    private HandlerAdapter handlerAdapter;
    /**
     * 处理器映射器
     */
    private HandlerMapping handlerMapping;

    @Override
    public void init() throws ServletException {
        //由于底层源码是初始化时先调用有参数的init方法,然后有参数的init再调用无参数的init,我们在这边重写无参数的init方法,程序会自动调用到这里
        /**
         * <init-param>
         *    <param-name>contextConfigLocation</param-name>
         *    <param-value>classpath:springmvc.xml</param-value>
         * </init-param>
         */
        //根据以上配置找springmvc.xml配置文件
        //获取ServletConfig对象(Servlet配置信息对象,该对象由web容器自动创建,并且将其传递给init方法,调用以下方法可以获取该对象)
        ServletConfig servletConfig = this.getServletConfig();
        //获取初始化参数
        String contextConfigLocation = servletConfig.getInitParameter(Const.CONTEXT_CONFIG_LOCATION);
        System.out.println("contextConfigLocation-->" + contextConfigLocation);
        String springMvcConfigPath = null;
        if (contextConfigLocation.trim().startsWith(Const.PREFIX_CLASSPATH)) {
            //条件成立,从类路径中找springmvc.xml
            springMvcConfigPath = Thread.currentThread().getContextClassLoader()
                    .getResource(contextConfigLocation.substring(Const.PREFIX_CLASSPATH.length())).getPath();
            //对路径中的特殊字符进行解码操作,让其正常显示
            springMvcConfigPath = URLDecoder.decode(springMvcConfigPath, Charset.defaultCharset());
            System.out.println("SpringMVC配置文件的绝对路径:" + springMvcConfigPath);
        }
}

开发规范:我们不建议在配置<param-name>这些初始化参数名的时候直接把名字写上去如:

String contextConfigLocation = servletConfig.getInitParameter("contextConfigLocation");

而是最好创建一个常量类用来代替"contextConfigLocation",如:Const.CONTEXT_CONFIG_LOCATION。因此Const类如下:(我把后续我们需要用到的常量类全部定义在这里了)

java 复制代码
/**
 * SpringMVC框架的系统常量类,所有的常量全部放到该常量类中
 */
public class Const {
    public static final String CONTEXT_CONFIG_LOCATION = "contextConfigLocation";
    /**
     * contextConfigLocation的前缀
     */
    public static final String PREFIX_CLASSPATH = "classpath:";
    public static final String WEB_APPLICATION_CONTEXT = "webApplicationContext";
    /**
     * HandlerMapping和HandlerAdapter实现类都在这个默认包下
     */
    public static final String DEFAULT_PACKAGE = "org.springmvc.web.servlet.mvc.method.annotation";
    public static final String BASE_PACKAGE = "base-package";
    /**
     * .class结尾
     */
    public static final String SUFFIX_CLASS = ".class";
    /**
     * springmvc中bean标签的class属性
     */
    public static final String BEAN_TAG_CLASS_ATTRIBUTE = "class";
    /**
     * property标签的名字
     */
    public static final String PROPERTY_TAG_NAME = "property";
    public static final String PROPERTY_NAME = "name";
    public static final String PROPERTY_VALUE = "value";
    public static final String VIEW_RESOLVER = "viewResolver";
    public static final String INTERCEPTORS = "interceptors";
    public static final String HANDLER_MAPPING = "handlerMapping";
    public static final String HANDLER_ADAPTER = "handlerAdapter";
}

2,初始化Spring Web容器

其中包含两个容器分别是ApplicationContext和WebApplicationContext(它们是父类和子类之间的关系),我们创建WebApplicationContext,里面存入springmvc.xml的上下文路径和Servlet的上下文路径(ServletContext)。

细节:我们在初始化WebApplicationContext中的属性时,构造方法里我们将xml的上下文路径传递给父类,在父类的构造方法中解析xml配置文件。

java 复制代码
        //初始化Spring Web容器(将所有该创建的对象全部创建出来,交给IoC容器管理)
        WebApplicationContext webApplicationContext = new WebApplicationContext(this.getServletContext(), springMvcConfigPath);
        //webApplicationContext代表的就是Spring Web容器,我们最好将其存储到Servlet上下文中,以便后期使用
        this.getServletContext().setAttribute(Const.WEB_APPLICATION_CONTEXT, webApplicationContext);

3,编写ApplicationContext

在编写ApplicationContext时我们需要根据xml中的配置进行对应方法的编写,比如这里我们需要:解析xml文件,组件扫描,创建视图解析器,创建拦截器,这些是为了初始化让IoC容器管理起来。还有在服务器启动阶段就会创建好的HandlerAdapter和HandlerMapping对象。

在这里我们需要一个构造器来帮我们调用上面说到的各个需求的实现方法:

java 复制代码
    public ApplicationContext(String xmlPath){
        try {
            //解析xml文件
            SAXReader reader = new SAXReader();
            Document document = reader.read(new File(xmlPath));
            //组件扫描
            Element componentScanElement = (Element) document.selectSingleNode("/beans/component-scan");
            Map<RequestMappingInfo, HandlerMethod> map = componentScan(componentScanElement);

            //创建视图解析器
            Element viewResolverElement = (Element) document.selectSingleNode("/beans/bean");
            createViewResolver(viewResolverElement);

            //创建拦截器
            Element interceptorsElement = (Element) document.selectSingleNode("/beans/interceptors");
            createInterceptors(interceptorsElement);

            //创建org.springmvc.web.servlet.mvc.method.annotation下的所有HandlerMapping
            createHandlerMapping(Const.DEFAULT_PACKAGE, map);

            //创建org.springmvc.web.servlet.mvc.method.annotation下的所有HandlerAdapter
            createHandlerAdapter(Const.DEFAULT_PACKAGE);

            System.out.println(beanMap);

        }catch (Exception e){
            e.printStackTrace();
        }
    }

* 组件扫描:

我们需要根据Controller类上是否有@Controller标签来判断是否要放入到Application类创建的Map集合中(即纳入IoC容器管理)。

我们根据/beans/component-scan来获取到controller包的包名,然后通过包名获取到绝对路径,再获取该目录旗下的所有子文件,这样就能够拿到UserController对象了。然后就是熟悉的反射来实例化类对象,然后放入到Map集合中实现纳入IoC容器管理,切记要判断类上是否有标签:clazz.isAnnotationPresent(Controller.class)。

java 复制代码
    private Map<RequestMappingInfo, HandlerMethod> componentScan(Element componentScanElement) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //创建处理器映射器大Map
        Map<RequestMappingInfo, HandlerMethod> map = new HashMap<>();

        //获取包名
        String basePackage = componentScanElement.attributeValue(Const.BASE_PACKAGE);
        String basePath = basePackage.replace(".", "/");
        //获取绝对路径,然后根据绝对路径来获取到那个UserController对象
        String absolutePath = Thread.currentThread().getContextClassLoader().getResource(basePath).getPath();
        absolutePath = URLDecoder.decode(absolutePath, Charset.defaultCharset());
        //封装file对象
        File file = new File(absolutePath);
        //获取该目录下的所有子文件
        File[] files = file.listFiles();
        //遍历数组
        for(File f : files){
            String classFileName = f.getName();
            System.out.println(classFileName);
            //判断是否是以.class结尾的,如果是的话我们需要裁掉.class后缀
            if(classFileName.endsWith(Const.SUFFIX_CLASS)){
                String simpleClassName = classFileName.substring(0, classFileName.lastIndexOf("."));
                System.out.println(simpleClassName);
                String className = basePackage + "." + simpleClassName;
                //如果类上有@Controller注解,则实例化Controller对象,并且将其存储到IoC容器当中
                Class<?> clazz = Class.forName(className);
                if(clazz.isAnnotationPresent(Controller.class)){
                    //创建了Controller对象
                    Object bean = clazz.newInstance();
                    //将其存储到IoC容器中(map集合)
                    beanMap.put(firstCharLowCase(simpleClassName), bean);
}
}

* 创建视图解析器

先通过viewResolverElement.attributeValue方法找到class属性,然后获取class属性的值,即视图解析器的类路径,然后我们根据反射机制能够为视图解析器对象创建实例。

然后我们通过viewResolverElement.elements方法获取property属性,由于有多个property,因此返回list集合。然后通过循环遍历得到其中的name和value属性。

获取属性名之后我们需要将value值注入给name,这时我们使用set注入,通过拼接字符串来获取默认的set方法名,然后通过反射机制将值通过调用set方法注入。最后纳入IoC容器(即加入到集合当中)。

java 复制代码
    /**
     * 创建视图解析器
     * @param viewResolverElement
     */
    private void createViewResolver(Element viewResolverElement) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        String className = viewResolverElement.attributeValue(Const.BEAN_TAG_CLASS_ATTRIBUTE);
        //通过反射机制创建对象
        Class<?> clazz = Class.forName(className);
        //视图解析器对象
        Object bean = clazz.newInstance();
        //获取当前bean节点下的子节点property
        List<Element> propertyElements = viewResolverElement.elements(Const.PROPERTY_TAG_NAME);
        for(Element propertyElement : propertyElements){
            //属性名
            String fieldName = propertyElement.attributeValue(Const.PROPERTY_NAME);
            //将属性名转换为set方法名进行set注入
            String setMethodName = fieldNameToSetMethodName(fieldName);
            //属性值
            String fieldValue = propertyElement.attributeValue(Const.PROPERTY_VALUE);
            System.out.println("属性名:" + fieldName);
            System.out.println("set方法名:" + setMethodName);
            System.out.println("属性值:" + fieldValue);
            //通过方法名获取方法
            Method setMethod = clazz.getDeclaredMethod(setMethodName, String.class);
            //通过反射机制调用方法
            setMethod.invoke(bean, fieldValue);
        }
        //添加到IoC容器
        //beanMap.put(firstCharLowCase(clazz.getSimpleName()), bean);
        beanMap.put(Const.VIEW_RESOLVER, bean);
    }

我们还需要一些辅助方法来帮助我们获取到set方法,以及转换大小写的方法:

java 复制代码
    /**
     * 将属性名转换为set方法的方法名
     * @param fieldName
     * @return
     */
    private String fieldNameToSetMethodName(String fieldName) {
        return "set" + firstCharUpperCase(fieldName);
    }

    /**
     * 将一个字符串的首字母变成大写
     * @param fieldName
     * @return
     */
    private String firstCharUpperCase(String fieldName) {
        return (fieldName.charAt(0) + "").toUpperCase() + fieldName.substring(1);
    }

*创建拦截器

依旧是根据配置文件进行读取,通过interceptorsElement.elements获取bean标签,并遍历bean标签获取到class属性,获取到全类名之后即可通过反射机制创建对象并存储到IoC容器进行管理。

java 复制代码
    /**
     * 创建拦截器
     * @param interceptorsElement
     */
    private void createInterceptors(Element interceptorsElement) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //准备一个List集合,存储拦截器对象
        List<HandlerInterceptor> interceptors = new ArrayList<>();
        //获取该标签下的所有bean标签
        List<Element> beans = interceptorsElement.elements("bean");
        //遍历bean标签
        for(Element beanElement : beans){
            String className = beanElement.attributeValue(Const.BEAN_TAG_CLASS_ATTRIBUTE);
            //通过反射机制创建对象
            Class<?> clazz = Class.forName(className);
            Object interceptor = clazz.newInstance();
            interceptors.add((HandlerInterceptor) interceptor);
        }
        //存储到IoC容器中
        beanMap.put(Const.INTERCEPTORS, interceptors);
    }

*创建HandlerMapping和HandlerAdapter对象

这里我们和写组件扫描方法时的写法差不多,也是通过传入的包名获取绝对路径名,再根据绝对路径找到类名并通过反射机制创建对象。

通过Thread.currentThread().getContextClassLoader().getResource(defaultPath).getPath()来获取绝对路径

这里需要注意使用到了一个新的api:isAssignableFrom。if(HandlerAdapter/HandlerMapping.class.isAssignableFrom(clazz))表示只有实现了HandlerMapping和HandlerAdapter接口的,再创建对象。

java 复制代码
    /**
     * 创建HandlerAdapter
     * @param defaultPackage
     */
    private void createHandlerAdapter(String defaultPackage) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //将包名中的"."替换成"/"
        String defaultPath = defaultPackage.replace(".", "/");
        String absolutePath = Thread.currentThread().getContextClassLoader().getResource(defaultPath).getPath();
        absolutePath = URLDecoder.decode(absolutePath, Charset.defaultCharset());
        File file = new File(absolutePath);
        File[] files = file.listFiles();
        for(File f : files){
            String classFileName = f.getName();
            //截掉最后的class后缀
            String simpleClassName = classFileName.substring(0, classFileName.lastIndexOf("."));
            //获取类的全路径
            String className = defaultPackage + "." + simpleClassName;
            //获取Class
            Class<?> clazz = Class.forName(className);
            //只有实现了HandlerMapping接口的,再创建对象
            if(HandlerAdapter.class.isAssignableFrom(clazz)){
                Object bean = clazz.newInstance();
                beanMap.put(Const.HANDLER_ADAPTER, bean);
                return;
            }
        }
    }

    /**
     * 创建HandlerMapping
     * @param defaultPackage
     */
    private void createHandlerMapping(String defaultPackage, Map<RequestMappingInfo, HandlerMethod> map) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        //将包名中的"."替换成"/"
        String defaultPath = defaultPackage.replace(".", "/");
        System.out.println(defaultPath);
        String absolutePath = Thread.currentThread().getContextClassLoader().getResource(defaultPath).getPath();
        absolutePath = URLDecoder.decode(absolutePath, Charset.defaultCharset());
        File file = new File(absolutePath);
        File[] files = file.listFiles();
        for(File f : files){
            String classFileName = f.getName();
            //截掉最后的class后缀
            String simpleClassName = classFileName.substring(0, classFileName.lastIndexOf("."));
            //获取类的全路径
            String className = defaultPackage + "." + simpleClassName;
            //获取Class
            Class<?> clazz = Class.forName(className);
            //只有实现了HandlerMapping接口的,再创建对象
            if(HandlerMapping.class.isAssignableFrom(clazz)){
                //Object bean = clazz.newInstance();
                //现在我们需要调用有参数构造方法来创建对象
                Constructor<?> con = clazz.getDeclaredConstructor(Map.class);
                Object bean = con.newInstance(map);
                beanMap.put(Const.HANDLER_MAPPING, bean);
                return;
            }
        }
    }

4,核心的doDispatcher方法如何实现

我们根据先前的源码阅读可以知道源码的doDispatch方法执行有这样7步:

1,根据请求对象获取对应的处理器执行链对象

2,根据"处理器方法"获取对应的处理器适配器对象

3,执行拦截器中的preHandle方法

4,执行处理器方法,并返回ModelAndView

5,执行拦截器中的postHandle方法

6,响应

7,执行拦截器中的afterCompletion方法

java 复制代码
    /**
     * DispatcherServlet前端控制器最核心的方法
     *
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException
     */
    private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //处理用户的请求
        try {

            //1,根据请求对象获取对应的处理器执行链对象(根据请求路径+请求方式来映射一个处理器方法HandlerMethod)
            HandlerExecutionChain mappedHandler = handlerMapping.getHandler(request);

            //2,根据"处理器方法"获取对应的处理器适配器对象
            HandlerAdapter ha = this.handlerAdapter;

            //3,执行拦截器中的preHandle方法
            if (!mappedHandler.applyPreHandle(request, response)) {
                return;
            }

            //4,执行处理器方法,并返回ModelAndView
            ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());

            //5,执行拦截器中的postHandle方法
            mappedHandler.applyPostHandle(request, response, mv);

            //6,响应
            //通过视图解析器进行解析,返回View对象
            View view = viewResolver.resolveViewName(mv.getView().toString(), Locale.CHINA);
            //渲染
            view.render(mv.getModel(), request, response);

            //7,执行拦截器中的afterCompletion方法
            mappedHandler.triggerAfterCompletion(request, response, null);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

handlerMapping解释:通过前端提交的"请求",来映射底层要执行的HandlerMethod。前端提交的请求包括请求路径,请求方式。我们可以把它们封装成一个RequestMappingInfo对象,然后把这个对象作为key,HandlerMethod作为value存储到一个Map集合当中就能实现映射了。

然后我们来到RequestMappingHandlerMapping类下的getHandler方法:在这里我们需要一个处理器执行链对象,而一个处理器执行链需要一个HandlerMethod和一个拦截器对象。

细节1:在获取拦截器对象的时候我们先通过 request.getServletContext().getAttribute(Const.WEB_APPLICATION_CONTEXT)获取IoC容器对象,再通过这个IoC容器对象拿到拦截器放入到执行链中。

细节2:在给RequestMappingInfo对象赋值的时候,我们可以通过通过request对象,获取请求路径,获取请求方式,将其封装成RequestMappingInfo对象。

注意:这里的map集合在我们服务器启动的时候就要创建所有的HandlerMethod对象,将其存储在map集合中。

java 复制代码
import jakarta.servlet.http.HttpServletRequest;
import org.springmvc.web.constant.Const;
import org.springmvc.web.context.WebApplicationContext;
import org.springmvc.web.method.HandlerMethod;
import org.springmvc.web.servlet.HandlerExecutionChain;
import org.springmvc.web.servlet.HandlerInterceptor;
import org.springmvc.web.servlet.HandlerMapping;
import org.springmvc.web.servlet.mvc.RequestMappingInfo;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 处理器映射器,专门为@RequestMapping注解服务的处理器映射器
 */
public class RequestMappingHandlerMapping implements HandlerMapping {
    /**
     * 处理器映射器主要就是通过以下的map集合进行映射
     */
    private Map<RequestMappingInfo, HandlerMethod> map;

    /**
     * 在创建HandlerMapping对象的时候给map集合赋值
     * @param map
     */
    public RequestMappingHandlerMapping(Map<RequestMappingInfo, HandlerMethod> map) {
        this.map = map;
    }

    @Override
    public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        //假设这个map已经有数据了,整个map集合中都存储了"请求信息"与"HandlerMethod"的映射关系
        //通过request对象,获取请求路径,获取请求方式,将其封装成RequestMappingInfo对象
        RequestMappingInfo requestMappingInfo = new RequestMappingInfo(request.getServletPath(), request.getMethod());
        //创建处理器执行链对象
        HandlerExecutionChain handlerExecutionChain = new HandlerExecutionChain();
        //给执行链设置HandlerMethod
        handlerExecutionChain.setHandler(map.get(requestMappingInfo));
        //获取所有拦截器
        WebApplicationContext webApplicationContext = (WebApplicationContext) request.getServletContext().getAttribute(Const.WEB_APPLICATION_CONTEXT);
        //给执行链设置拦截器
        List<HandlerInterceptor> interceptors = (List<HandlerInterceptor>)webApplicationContext.getBean(Const.INTERCEPTORS);
        handlerExecutionChain.setInterceptors(interceptors);
        return handlerExecutionChain;
    }
}

*启动时初始化处理器映射器

我们要给我们创建出来的map集合赋值,即附上HandlerMethod和RequestMappingInfo,而HandlerMethod就是我们Controller下的各个方法,而获取到Controller就需要我们去扫描Controller包,通过组件扫描来获取其下所有的Controller类。因此我们需要修改componentScan方法让其返回一个包含着RequestMappingInfo和HandlerMethod属性的map集合,新增代码如下:(核心就是我们需要根据创建的RequestMappingInfo和HandlerMethod的pojo类来给类中的属性赋值)

java 复制代码
                    Method[] methods = clazz.getDeclaredMethods();
                    for(Method method : methods){
                        if(method.isAnnotationPresent(RequestMapping.class)){
                            //获取方法上的注解
                            RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                            //创建RequestMappingInfo对象
                            RequestMappingInfo requestMappingInfo = new RequestMappingInfo();
                            requestMappingInfo.setRequestURI(requestMapping.value()[0]); //请求路径
                            requestMappingInfo.setMethod(requestMapping.method().toString()); //请求方式
                            //创建HandlerMethod对象
                            HandlerMethod handlerMethod = new HandlerMethod();
                            handlerMethod.setHandler(bean);  //传入Controller对象
                            handlerMethod.setMethod(method); //传入带有RequestMapping注解的方法
                            //放到map集合
                            map.put(requestMappingInfo, handlerMethod);
                        }
                    }

细节:在createHandlerMapping方法当中我们需要根据有参构造器创建对象了,而不能再是无参构造,因为:

public RequestMappingHandlerMapping(Map<RequestMappingInfo, HandlerMethod> map) {

this.map = map;

}

有要求需要一个map集合。因此我们将方法中的创建实例对象步骤改为:

java 复制代码
                //Object bean = clazz.newInstance();
                //现在我们需要调用有参数构造方法来创建对象
                Constructor<?> con = clazz.getDeclaredConstructor(Map.class);
                Object bean = con.newInstance(map);

新增代码后如下:

java 复制代码
    /**
     * 组件扫描
     * @param componentScanElement
     */
    private Map<RequestMappingInfo, HandlerMethod> componentScan(Element componentScanElement) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //创建处理器映射器大Map
        Map<RequestMappingInfo, HandlerMethod> map = new HashMap<>();

        //获取包名
        String basePackage = componentScanElement.attributeValue(Const.BASE_PACKAGE);
        String basePath = basePackage.replace(".", "/");
        //获取绝对路径,然后根据绝对路径来获取到那个UserController对象
        String absolutePath = Thread.currentThread().getContextClassLoader().getResource(basePath).getPath();
        absolutePath = URLDecoder.decode(absolutePath, Charset.defaultCharset());
        //封装file对象
        File file = new File(absolutePath);
        //获取该目录下的所有子文件
        File[] files = file.listFiles();
        //遍历数组
        for(File f : files){
            String classFileName = f.getName();
            System.out.println(classFileName);
            //判断是否是以.class结尾的,如果是的话我们需要裁掉.class后缀
            if(classFileName.endsWith(Const.SUFFIX_CLASS)){
                String simpleClassName = classFileName.substring(0, classFileName.lastIndexOf("."));
                System.out.println(simpleClassName);
                String className = basePackage + "." + simpleClassName;
                //如果类上有@Controller注解,则实例化Controller对象,并且将其存储到IoC容器当中
                Class<?> clazz = Class.forName(className);
                if(clazz.isAnnotationPresent(Controller.class)){
                    //创建了Controller对象
                    Object bean = clazz.newInstance();
                    //将其存储到IoC容器中(map集合)
                    beanMap.put(firstCharLowCase(simpleClassName), bean);
                    //创建这个bean中所有的HandlerMethod对象,将其放到map集合中
                    Method[] methods = clazz.getDeclaredMethods();
                    for(Method method : methods){
                        if(method.isAnnotationPresent(RequestMapping.class)){
                            //获取方法上的注解
                            RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                            //创建RequestMappingInfo对象
                            RequestMappingInfo requestMappingInfo = new RequestMappingInfo();
                            requestMappingInfo.setRequestURI(requestMapping.value()[0]); //请求路径
                            requestMappingInfo.setMethod(requestMapping.method().toString()); //请求方式
                            //创建HandlerMethod对象
                            HandlerMethod handlerMethod = new HandlerMethod();
                            handlerMethod.setHandler(bean);  //传入Controller对象
                            handlerMethod.setMethod(method); //传入带有RequestMapping注解的方法
                            //放到map集合
                            map.put(requestMappingInfo, handlerMethod);
                        }
                    }
                }
            }
        }
        return map;
    }

RequestMappingInfo:

java 复制代码
/**
 * 请求映射信息:包含请求路径,还有请求方式... ...
 */
public class RequestMappingInfo {
    private String requestURI;
    private String method;

    public RequestMappingInfo(String requestURI, String method) {
        this.requestURI = requestURI;
        this.method = method;
    }

    public RequestMappingInfo() {
    }

    public String getRequestURI() {
        return requestURI;
    }

    public void setRequestURI(String requestURI) {
        this.requestURI = requestURI;
    }

    public String getMethod() {
        return method;
    }

    public void setMethod(String method) {
        this.method = method;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        RequestMappingInfo that = (RequestMappingInfo) o;
        return Objects.equals(requestURI, that.requestURI) && Objects.equals(method, that.method);
    }

    @Override
    public int hashCode() {
        return Objects.hash(requestURI, method);
    }
}

*创建拦截器部分代码:

我们在这里可以仿照源码的写法:(例如preHandle)

if(!mappedHandler.applyPreHandle(request, response)){

return;

}

然后具体实现applyPreHandle方法:(在处理器执行链类中编写)

java 复制代码
    public boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
       //遍历拦截器(顺序遍历)
        for (int i = 0; i < interceptors.size(); i++) {
            //取出一个拦截器对象
            HandlerInterceptor handlerInterceptor = interceptors.get(i);
            //调用preHandle方法
            boolean result = handlerInterceptor.preHandle(request, response, handler);
            //根据执行结果,如果为false表示不再继续执行
            if(!result){
                return false;
            }
        }
        return true;
    }

而其中的preHandle方法是需要程序开发者去重写的。后面的postHandle也是类似写法,只不过是将顺序遍历改成了逆序遍历。

*渲染页面:

首先我们参考官方源码我们发现我们在View接口中还需要一个getContentType()方法获取内容类型。然后来到InternalResourceView类(实现jsp模板的)中对这个方法进行实现,其中我们需要两个属性:响应的内容类型和响应的路径,即contentType和path,分别给上setter和getter方法即能实现接口中的getContentType()方法。

总体思路:

我们需要通过视图解析器进行解析,返回View对象:(核心方法为resolveViewName)

View view = viewResolver.resolveViewName(mv.getView().toString(), Locale.CHINA);

因此我们在resolveViewName方法中我们要创建出视图对象,才能在后面使用render方法进行渲染。

在创建视图解析器对象的时候我们才会发现我们刚才为什么要在InternalResourceView中设置两个属性:

return new InternalResourceView("text/html;charset=UTF-8", prefix + viewName + suffix); //第一个参数为contentType,第二个参数为path

java 复制代码
/**
 * 内部资源的视图解析器,可以解析JSP
 */
public class InternalResourceViewResolver implements ViewResolver {

    private String prefix;
    private String suffix;

    public InternalResourceViewResolver(String prefix, String suffix) {
        this.prefix = prefix;
        this.suffix = suffix;
    }

    public InternalResourceViewResolver() {
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }

    /**
     * 将逻辑视图名字转换为物理视图名称,并以View对象形式返回
     * @param viewName
     * @param locale
     * @return
     * @throws Exception
     */
    @Override
    public View resolveViewName(String viewName, Locale locale) throws Exception {
        //视图解析器,将逻辑视图名称转换为物理视图名称
        return new InternalResourceView("text/html;charset=UTF-8", prefix + viewName + suffix);
    }
}

*render方法实现

有了对象之后我们就需要写render方法了(三步实现页面跳转)

1,设置响应的内容类型

使用的api是:response.setContentType

2,将model数据存储到request域当中,我们需要把map集合中的元素都设置到request域当中(我们前面在handle方法里已经写死了"username"和"zhangsan"到ModelMap对象,即这里的model集合中,因此这里存入的数据就是"username"和"zhangsan"。

model.forEach(request::setAttribute);

3,转发

request.getRequestDispatcher(path).forward(request,response);实现转发的api。

java 复制代码
/**
 * 视图接口的实现类
 */
public class InternalResourceView implements View {
    /**
     * 响应的内容类型
     */
    private String contentType;
    /**
     * 响应的路径
     */
    private String path;

    public InternalResourceView(String contentType, String path) {
        this.contentType = contentType;
        this.path = path;
    }

    public InternalResourceView() {
    }

    @Override
    public String getContentType() {
        return contentType;
    }

    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    @Override
    public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
        //设置响应的内容类型
        response.setContentType(contentType);
        //将model数据存储到request域当中(默认情况下,数据是存储在request域当中的,即将用户信息传递给视图模板)
        //把map集合中的元素都设置到request域当中
        model.forEach(request::setAttribute);
        //转发
        request.getRequestDispatcher(path).forward(request,response);
    }
}

*执行拦截器中的最后一步:afterCompletion方法

先参考官方源码写出:mappedHandler.triggerAfterCompletion(request, response, null);,然后到处理器执行链类中去实现该方法,这时我们会发现三个拦截器的实现方法都是写在HandlerExecutionChain类中的。

我们依旧是要使用逆序的方式执行拦截器的afterCompletion方法,这时我们需要之前创建的拦截器索引interceptorIndex变量,以下是循环写法:

for (int i = interceptorIndex; i >= 0; i--) {

HandlerInterceptor handlerInterceptor = interceptors.get(i);

handlerInterceptor.afterCompletion(request, response, handler, null);

}

*为@RequestMapping注解服务的处理器适配器

最后一步我们之前把handle方法中的给ModelMap赋值的写法是写死的,现在我们不能将它写死,实际上是需要调用处理器方法的。

我们获取Controller对象,并且获取要调用的方法,然后通过反射机制去调用对应的方法,但是这里我们写了个小限制:要求Controller类中方法必须有ModelMap参数,并且要求Controller类中方法必须返回String逻辑视图名字(其实是便于封装)具体代码如下:

java 复制代码
        //需要调用处理器方法的
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        //获取Controller对象
        Object controller = handlerMethod.getHandler();
        //获取要调用的方法
        Method method = handlerMethod.getMethod();
        //通过反射机制调用方法(我们自己写的springmvc框架,有一个特殊的要求,要求Controller类中方法必须有ModelMap参数)
        //并且要求Controller类中方法必须返回String逻辑视图名字
        ModelMap modelMap = new ModelMap();
        String viewName = (String) method.invoke(controller, modelMap);

        //封装ModelAndView对象
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName(viewName);
        modelAndView.setModel(modelMap);
java 复制代码
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //需要调用处理器方法的
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        //获取Controller对象
        Object controller = handlerMethod.getHandler();
        //获取要调用的方法
        Method method = handlerMethod.getMethod();
        //通过反射机制调用方法(我们自己写的springmvc框架,有一个特殊的要求,要求Controller类中方法必须有ModelMap参数)
        //并且要求Controller类中方法必须返回String逻辑视图名字
        ModelMap modelMap = new ModelMap();
        String viewName = (String) method.invoke(controller, modelMap);

        //封装ModelAndView对象
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName(viewName);
        modelAndView.setModel(modelMap);

//        //先固定死,以后再说
//        ModelAndView modelAndView = new ModelAndView();
//        //给属性赋值
//        modelAndView.setViewName("index");
//
//        ModelMap modelMap = new ModelMap();
//        modelMap.addAttribute("username", "zhangsan");
//        modelAndView.setModel(modelMap);
//
        return modelAndView;
    }
}
相关推荐
憨子周36 分钟前
2M的带宽怎么怎么设置tcp滑动窗口以及连接池
java·网络·网络协议·tcp/ip
霖雨2 小时前
使用Visual Studio Code 快速新建Net项目
java·ide·windows·vscode·编辑器
SRY122404192 小时前
javaSE面试题
java·开发语言·面试
Fiercezm3 小时前
JUC学习
java
无尽的大道3 小时前
Java 泛型详解:参数化类型的强大之处
java·开发语言
ZIM学编程3 小时前
Java基础Day-Sixteen
java·开发语言·windows
我不是星海3 小时前
1.集合体系补充(1)
java·数据结构
P.H. Infinity3 小时前
【RabbitMQ】07-业务幂等处理
java·rabbitmq·java-rabbitmq
爱吃土豆的程序员3 小时前
java XMLStreamConstants.CDATA 无法识别 <![CDATA[]]>
xml·java·cdata
2401_857610034 小时前
多维视角下的知识管理:Spring Boot应用
java·spring boot·后端