您的位置:首页 > 游戏 > 游戏 > 手写Spring MVC中DispatcherServlet与核心组件底层

手写Spring MVC中DispatcherServlet与核心组件底层

2024/11/17 18:28:33 来源:https://blog.csdn.net/m0_67496588/article/details/140177472  浏览:    关键词:手写Spring MVC中DispatcherServlet与核心组件底层

Spring MVC中DispatcherServlet与核心组件底层

DispatcherServlet找到配置类,通过配置类信息拿到包信息,扫描包(扫描包的目的是找到配置类和controller类,并创建对象保存起来)。

扫描包的思路为:利用当前线程获取上下文的类加载器,将包名变成文件的URL地址,再从URL地址中获取文件的路径。对于路径中的每一个文件进行查找,找到.class文件后创建对象并将对象保存起来。

创建对象和保存的思路为:查看类是否是配置类,即检查类上是否有configuration注解,如果有则创建配置类的实例,获取配置类中定义的公开方法,对于每个方法获取方法上的bean注解,获取bean的名字,然后通过method.invoke(配置类实例)无参方法执行得到bean对象,并将bean对象存储在IOC容器中。

查看类是否是处理器类,即检查类上是否有controller注解,如果有则获取注解的值,即bean名字,如果为空那么就使用类名作为bean的名字,创建bean对象存储到IOC容器中。

IOC容器:拥有一个静态的hashmap属性,映射的键值分别为bean对象的名字和bean对象。

扫描完包后,从IOC容器中拿到所有的bean对象,对bean对象进行判断,如果bean对象属于处理器映射器类,则用自己的处理器映射器容器属性添加此bean对象;如果是处理器适配器类则用自己的处理器适配器容器属性添加此bean对象;如果是视图解析器类,则将自己的视图解析器属性赋值为此bean对象。

处理器映射器类都实现HandlerMapping接口,此接口只有一个getHandler方法用于获得一个处理器链,而处理器链有一个处理器属性。相当于就是返回一个处理器。

HandlerMapping接口下有若干处理器映射器实现类,如(这里举例说明两个处理器映射器类)

BeanNameUrlHandlerMapping,用bean的名字与url进行匹配,其重写的getHandler方法的思路就是从请求地址中拼接出url,然后将此url作为bean对象名去Ioc容器中获取bean对象并返回。

DefautAnnotationHandlerMapping,根据注解来获取对应的bean对象,思路为先获取所有的Ioc容器中的bean对象,利用stream流筛选含有Controller注解的控制器类,先通过RequestMapping注解获得父级url,即类上的url,然后获取控制器类中的所有公开方法,判断方法属于什么请求方式,即判断方法的注解为GetMapping、PostMapping、PutMapping、DeleteMapping中的哪一个,并获取对应注解内容中的子级url,拼接成url。然后将请求中的url和注解拼接的url进行匹配,注解中的url可能有{},因此匹配逻辑如果有花括号则不管,其余部分都必须一样。如果匹配成功并且请求方式和方法的请求方式一致则返回方法级别的处理器对象,即返回控制器对象、方法、url。

方法级别的处理器对象构建:一个表示方法级别的处理器类,只针对使用Controller或者RestController注解标识的控制器,有三个属性,分别为控制器对象、方法对象、返回值对象。

处理器适配器类都实现了HandlerAdapter接口,这个接口拥有两个方法,一个是表示当前处理器适配的处理器类型,返回布尔值,另外一个方法是返回ModelAndView类型的handle方法。ModelAndView类拥有视图名和map两个属性。只有当第一个方法满足之后才会执行handle方法。

HandlerAdapter接口下有若干处理器适配器类,如(列举两个处理器适配器类):

ControllerHandlerAdapter,这个适配器只支持实现Controller接口定义的控制器,Controller接口中只有一个handle方法,用户的controller类可以实现这个接口方法,而ControllerHandlerAdapter适配器就是首先判断当前处理器是否是Controller接口的实例,如果是则调用接口的方法,即调用用户写的方法。

HandlerMethodAdapter,这个适配器支持方法级别的处理器,首先判断处理器是否是HandlerMethod类的实例,即是否是方法级别的处理器,如果是则获取方法对象,获取参数列表并转换为参数数组,然后用method.invoke()调用方法,判断返回值类型,如果方法有ResponseBody注解则将返回值作为对象添加到modelandview对象中,如果没有则表明返回的是视图名,则作为视图名添加到modelandview对象中。

其中将参数列表转换为参数数组需要对于参数列表进行遍历,先获取参数的类型,然后根据不同情况分别进行对应的参数获取,具体为先判断参数的注解类型:

如果是RequestParam注解类,则从请求中获取给定参数名(注解内容表示参数名)的参数值并进行相应的类型解析转换。

如果是RequestBody注解,则表明参数为JSON格式,需要先获取请求体内容的字符流进行写入,然后转换成参数类型的对象。

如果是PathVariable注解,则表明是从路径中获得参数,需要拼接出url,然后寻找花括号{}里面的值,拿到后进行相应的解析转换。

如果是RequestHeader注解,则直接从请求头中获取参数值,并解析转换。

如果是CookieValue注解,则从cookie中获取参数值并解析转换。

然后判断参数类型是否是特殊类型,如请求类型、响应类型、session类型,如果是这三种类型则直接获取。

如果是其他类型则利用请求的getParameterMap()方法获取所有的参数,然后遍历判断类型是字符串还是字符串数组,利用JSON格式(JSON本身是一个map)获取并封装为对象返回。

视图解析器类实现ViewResolver接口,该接口有两个方法,getPrefix和getSuffix,这里以InternalResourceViewResolver实现类为例:

InternalResourceViewResolver类有两个属性,就是prefix和suffix,重写的方法就是两个属性的get方法,此外还有set方法。

DispatcherServlet的init方法到此结束,总结就是通过扫描包创建了处理器映射器的list,处理器适配器的list以及设置了视图解析器。之后要重写service方法,首先根据用户的请求地址和请求方式拿到处理器链,如果为空就会向前端发送一个404,如果不为空就说明有处理器可以处理,但是不能确定处理器的类型,所以需要处理器适配器进行适配,在处理器适配器中实现方法的调用,最后对返回值进行处理,如果返回的是数据则利用字符流进行输出,如果是视图则根据视图名和前后缀找到视图文件,用流读取文件的内容进行渲染。

其中,根据用户而定请求地址和请求方式拿到处理器链的方式为遍历处理器映射器,拿到其中的处理器链(处理器),如果不为空就返回,因此会找到第一个处理器链。

而处理器适配器进行适配的逻辑则为遍历处理器适配器,调用该处理器适配器的匹配类型的方法,如果匹配成功就返回,因此也是返回第一个处理器适配器。

整个DispatcherServlet的代码:

package com.qf.mvc;import com.alibaba.fastjson.JSON;
import com.qf.anno.ComponentScan;
import com.qf.container.IocContainer;
import com.qf.data.ModelAndView;
import com.qf.handler.HandlerAdapter;
import com.qf.handler.HandlerExecutionChain;
import com.qf.mapping.HandlerMapping;
import com.qf.utils.PackageScanner;import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;public class DispatcherServlet extends HttpServlet {//当前用户配置的处理器映射器private List<HandlerMapping> mappings = new ArrayList<>();private List<HandlerAdapter> adapters = new ArrayList<>();private ViewResolver viewResolver;@Overridepublic void init(ServletConfig config) throws ServletException {//找到配置类String configClass = config.getInitParameter("appConfig");try {Class<?> clazz = Class.forName(configClass);//获取配置类的字节码信息List<String> packages = new ArrayList<>();packages.add(clazz.getPackage().getName());//首先保证配置类所在的包会被扫描ComponentScan scan = clazz.getAnnotation(ComponentScan.class);//获取含有扫描包信息的注解if(scan!=null){String[] scanPackages = scan.value();//获取要扫描的包if(scanPackages.length ==0){//如果没有配置扫描的包,那么就扫描这个配置类所在的包scanPackages = new String[]{clazz.getPackage().getName()};packages.addAll(Arrays.asList(scanPackages));}for (String pk : packages) {PackageScanner.scan(pk);//循环扫描每一个配置的包}}Collection<Object> beans = IocContainer.getAllBeans();for (Object bean : beans) {if(bean instanceof HandlerMapping){mappings.add((HandlerMapping) bean);} else if (bean instanceof HandlerAdapter) {adapters.add((HandlerAdapter) bean);}else if(bean instanceof ViewResolver){viewResolver = (ViewResolver) bean;}}} catch (ClassNotFoundException e) {throw new RuntimeException(e);} catch (Exception e) {throw new RuntimeException(e);}}@Overrideprotected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//当拿到请求的时候,我们需要根据请求的地址和请求的方式去找到匹配的处理器//如何来查找处理器呢?就需要根据用户的配置来决定try {HandlerExecutionChain handlerExecutionChain = getHandlerExecutionChain(req);if(handlerExecutionChain == null || handlerExecutionChain.getHandler() == null){//没有找到匹配的处理器执行链,那么此时就会向前端发送一个404resp.setContentType("text/html;charset=UTF-8");PrintWriter writer = resp.getWriter();writer.write("404  Not Found");writer.flush();writer.close();return;}//只要代码执行到这里,说明处理器也不为空,但是此时不能确定处理器的类型,因此需要处理器适配器HandlerAdapter adapter = getHandlerAdapter(handlerExecutionChain.getHandler());if(adapter == null){//如果说处理器适配器为空,说明存在配置问题return;}ModelAndView view = adapter.handle(req, resp, handlerExecutionChain.getHandler());if(view.hasData()){Object data = view.getData("data");resp.setContentType("application/json;charset=UTF-8");PrintWriter writer = resp.getWriter();writer.write(JSON.toJSONString(data));writer.flush();writer.close();} else {String viewName = view.getViewName();String location = viewResolver.getPrefix() + viewName + viewResolver.getSuffix();//根据位置用流读取文件的内容InputStream in = DispatcherServlet.class.getResourceAsStream(location);InputStreamReader isr = new InputStreamReader(in);BufferedReader reader = new BufferedReader(isr);PrintWriter writer = resp.getWriter();String line;while ((line = reader.readLine()) != null){writer.write(line);}writer.flush();writer.close();}} catch (Exception e) {e.printStackTrace();}}private HandlerExecutionChain getHandlerExecutionChain(HttpServletRequest req) throws Exception {for (HandlerMapping mapping : mappings) {HandlerExecutionChain handlerExecutionChain = mapping.getHandler(req);if(handlerExecutionChain != null){return handlerExecutionChain;}}return null;}private HandlerAdapter getHandlerAdapter(Object handler){for (HandlerAdapter adapter : adapters) {if(adapter.supports(handler)) return adapter;}return null;}
}

梳理:首先根据配置文件参数拿到appconfig配置类,然后根据其Component-Scan注解获取要扫描的包(如果注解有内容就需要扫描内容的包,没有就需要扫描拥有Component-Scan注解的包,【这里的Component-Scan注解如果没有内容则说明是配置在控制器层的吗?–Component-Scan所在配置类和指向的类都要扫描】)。扫描包找到.class文件创建bean对象并保存到IOC容器里,这里只扫描configuration注解和controller注解【但是mapping和adapter以及viewResolver类没有这两个注解,那么是如何扫描到并创建对象的?–因为在配置类里面会创建这三个组件类的方法,返回组件类,并且拥有bean注解】,如果是configuration注解则说明是方法执行后得到bean对象,如果是controller注解则说明是类作为bean对象。然后遍历ioc容器判断是否属于三个组件类,并提取到集合或进行设置。

然后是service方法,就是根据请求遍历集合,查看是否有适合的映射器,有的话就执行方法得到处理器执行链,然后用执行链的get方法获取处理器作为参数寻找处理器适配器,再于适配器中执行方法返回视图名或者数据。最后输出数据或者渲染视图。

改bug:

在扫描包的方法类(PackageScanner)里:

1.如果bean名字为空,就用方法名作为bean的名字。

2.扫描包中使用类加载器定位资源的时候需要将包名中的”·“转换为“/”。

在DispatcherServlet中:

3.获取配置类字节码信息后,首先保证配置类所在的包会被扫描,也就是配置类以及Component-Scan注解中的包都会被扫。

在DefaultAnnotationHandlderMapping中:

4.请求地址prefixUrl和rm要放进循环里。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com