手写SpringMVC(简易版)

news2024/11/15 11:16:17

在上一篇博客中说到这里我们要进行手写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层的对应实现:

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 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。

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类如下:(我把后续我们需要用到的常量类全部定义在这里了)

/**
 * 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配置文件。
 

        //初始化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对象。

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

    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)。

    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容器(即加入到集合当中)。

    /**
     * 创建视图解析器
     * @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方法,以及转换大小写的方法:

    /**
     * 将属性名转换为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容器进行管理。

    /**
     * 创建拦截器
     * @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接口的,再创建对象。

    /**
     * 创建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方法

    /**
     * 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集合中。

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类来给类中的属性赋值)

                    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集合。因此我们将方法中的创建实例对象步骤改为:

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

新增代码后如下: 

    /**
     * 组件扫描
     * @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:

/**
 * 请求映射信息:包含请求路径,还有请求方式... ...
 */
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方法:(在处理器执行链类中编写)

    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

/**
 * 内部资源的视图解析器,可以解析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。

/**
 * 视图接口的实现类
 */
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逻辑视图名字(其实是便于封装)具体代码如下:

        //需要调用处理器方法的
        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);
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;
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2163470.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Cloud :Hystrix实现优雅的服务容错

目录 Hystrix概述&#xff1a;第一个Hystrix程序步骤1&#xff1a;创建父工程hystrix-1步骤2&#xff1a;改造服务提供者步骤3&#xff1a;改造服务消费者为Hystrix客户端&#xff08;1&#xff09;添加Hystrix依赖&#xff08;2&#xff09;添加EnableHystrix注解&#xff08;…

共享单车轨迹数据分析:以厦门市共享单车数据为例(六)

副标题&#xff1a;.基于POI数据的站点功能混合度探究——以厦门市为例 为了保证数据时间尺度上的一致性&#xff0c;我们从互联网上下载了2020年的POI数据&#xff0c;POI数据来源于高德地图 API平台,包括名称、大小类、地理坐标等。并将高德地图 POI数据的火星坐标 系 GCJ-0…

轻松搭建企业报修平台,零代码解决方案

在当今竞争激烈的商业环境中&#xff0c;企业的高效运营离不开稳定的设备和设施。而当设备出现故障时&#xff0c;一个高效的维修报修平台就显得至关重要。那么&#xff0c;如何零代码搭建企业内部维修报修平台呢&#xff1f; 一、明确需求与目标 在搭建维修报修平台之前&…

Apache APISIX学习(2):安装Grafana、prometheus

一、Grafana安装 1、介绍 Grafana 是一个监控仪表系统&#xff0c;它是由 Grafana Labs 公司开源的的一个系统监测 (System Monitoring) 工具。它可以大大帮助你简化监控的复杂度&#xff0c;你只需要提供你需要监控的数据&#xff0c;它就可以帮你生成各种可视化仪表。同时它…

day01——通过git进行管理项目

新建仓库 IDEA新建本地仓库&#xff0c;并上传本地代码将代码上传到远程仓库 不同版本的idea&#xff0c;方式不同&#xff0c;自行摸索

鸿蒙界面开发——组件(10):单选框Radio复选框checkbox 下拉框select 多条件筛选Filter

单选框Radio 单选框一直会有这个圆圈&#xff0c;在选中和未选中之间切换状态。 Radio通过调用接口来创建&#xff0c;接口调用形式如下&#xff1a; Radio(options: RadioOptions) Radio(options: {value: string, group: string ,indicatorType:RadioIndicatorType,(新增) …

基于SSM的家政服务网站【附源码】

基于SSM的家政服务网站&#xff08;源码L文说明文档&#xff09; 目录 4 系统设计 4.1 系统概述 4.2 系统结构 4.3. 数据库设计 4.3.1 数据库实体 4.3.2 数据库设计表 4.4 数据表 第5章 系统详细设计 5.1管理员功能模块 5.2用户功能模块 5.3前…

STM32F407单片机编程入门(二十四) USB口介绍及CDC类虚拟串口通讯详解及源码

文章目录 一.概要二.USB2.0基本介绍及虚拟串口介绍三.STM32F407VET6单片机USB模块框图四.STM32单片机USB从机模式五.STM32F407VET6 USB设备CDC类六.CubeMX配置一个USB虚拟串口收发例程七.CubeMX工程源代码下载八.小结 一.概要 STM32F407VET6 USB虚拟串口是一种采用STM32F407VE…

【测试】什么是需求?

测试的概念&#xff1a;验证软件的特性是否满足用户的需求。软件测试领域&#xff1a;软件测试工程师、软件测试开发工程师 需求的概念 在企业中&#xff0c;经常会听到俩个词&#xff1a;用户需求和软件需求。 例如&#xff1a;开发人员与产品人员出现冲突... 产品同学要求开…

ROG NUC:重塑未来,从“芯”开始

ROG NUC以令人惊叹的紧凑身形、强大配置&#xff0c;从“芯”出发&#xff0c;重塑我们对科技生活的想象&#xff0c;让玩家感受由微缩主机带来的性能风暴。 至强“芯”脏&#xff0c;性能巅峰 ROG NUC至高搭载英特尔酷睿Ultra9处理器&#xff0c;采用先进的制程工艺&#xff…

【C++】STL详解之string类

目录 什么是STL STL的版本 STL的六大组件 STL的缺陷 一.string的定义方式 二. string的插入 1.使用push_back进行尾插 2.使用insert插入 三.string的拼接 四.string的删除 1.使用pop_back进行尾删 2.使用erase进行删除 五.string的查找 1.使用find正向搜索第一个…

设计模式 策略模式(Strategy Pattern)

策略模式简绍 策略模式&#xff08;Strategy Pattern&#xff09;是一种行为设计模式&#xff0c;它使你能在运行时改变对象的行为。该模式定义了一系列的算法&#xff0c;并将每一个算法封装起来&#xff0c;使它们可以相互替换。策略模式让算法独立于使用它的客户而变化。 …

【计网】从零开始掌握序列化 --- 实现网络计算器项目

​​​请各位保持头脑清醒&#xff0c; ​​​读些好书&#xff0c;做点有用的事&#xff0c; ​​​快快乐乐地生活。 ​​​ --- 斯蒂芬金 《肖申克的救赎》--- 从零开始掌握序列化 1 知识回顾2 服务器框架3 客户端框架4 运行测试 1 知识回顾 前面两篇文章学习中基础知识…

CSS 实现文本溢出省略号显示,含单行与多行文本溢出

&#x1f680; 个人简介&#xff1a;某大型国企资深软件研发工程师&#xff0c;信息系统项目管理师、CSDN优质创作者、阿里云专家博主&#xff0c;华为云云享专家&#xff0c;分享前端后端相关技术与工作常见问题~ &#x1f49f; 作 者&#xff1a;码喽的自我修养&#x1f9…

[翟旭发射器]python-推导式-列表list表达式练习

# 简单的列表生成 numbers00[x for x in range(1,11)] print(numbers00) # 带条件的列表生成 numbers01[x for x in range(1,11) if x%20] print(numbers01) # 带表达式的列表生成 numbers10[x**2 for x in range(1,11)] print(numbers10) # 嵌套循环的列表生成 coordinates[(x…

UE4_Niagara基础实例—使用自定义参数

实现的功能&#xff1a;使用自定义的参数来调整粒子远离发射器后粒子大小的变化 效果图&#xff1a; 操作步骤&#xff1a; 1、创建Niagara系统&#xff0c;使用Simple Sprite Burst模板作为新系统的发射器&#xff0c;更名为NS_Custompara。 2、双击打开Niagara系统编辑界面…

Shopee 大促想爆单如何准备?EasyBoss ERP为你准备了一份攻略!

Shopee下半年第二个大促节点——10.10品牌大促即将来到&#xff0c;根据Shopee的官方的数据&#xff0c;9.9大促当天&#xff0c;Shopee Mall单量增至平日4倍。 老板们&#xff0c;准备好自己的热卖爆款冲击10.10大促了吗&#xff1f; 图源&#xff1a;Shopee 为助力大家迎战大…

NetApp EF 系列全闪存阵列 EF600 和 EF300

功能强大且经济实惠的性能 NetApp EF600 全闪存阵列专为需要最高性能的工作负载而设计。NetApp EF300阵列专为大数据分析和数据库等混合工作负载环境而设计。这些NVMe全闪存阵列的性能是以前SAS全闪存阵列的两倍。您可以使用专为高性能工作负载打造的端到端 NVMe 存储平台来加速…

node-rtsp-stream、jsmpeg.min.js实现rtsp视频在web端播放

1. 服务地址&#xff08;私有&#xff09;&#xff1a;https://gitee.com/nnlss/video-node-server 2.node-rtsp-stream 需要安装FFMPEG&#xff1b; 3.给推拉流做了开关&#xff0c;可借助http请求&#xff0c;有更好方式可联系&#xff1b; 4.存在问题&#xff1a; 1&…

Vue中集中常见的布局方式

布局叠加 完整代码最外层的Container设置为relative&#xff0c;内部的几个box设置为absolute <template><div class"container"><div class"box box1">Box 1</div><div class"box box2">Box 2</div><d…