SpringMvc源码分析学习
SpringMvc源码分析学习
Spring MVC是基于Servlet API构建的原始Web框架,从一开始就包含在对应为spring-webmvc包。它遵循Model-View-Controller (MVC) 设计模式
DispatcherServlet初始化
前端控制器,负责接受客户端请求,并将请求分给合适的处理程序。作为请求处理流程的入口,其实现了Servlet接口。其实就是一个Servlet

所以servlet初始化时,会调用init方法,一直向上父类找,初始化时会调用HttpServletBean的init方法,进行一些bean参数配置然后调用initServletBean()方法

这里实例化了一个Spring容器 ,FrameWorkServlet(DispatcherServlet的父类)的属性WebApplicationContext。也就相当于在DispatcherServlet内部有一个Spring容器,这个Spring容器管理了一些bean以供DispatcherServlet使用。
调试,Tomcat启动时会来调用init方法调用initServletBean()创建spring容器,初始化servlet


我调试使用的springboot环境
容器先收集当前 Servlet 在 web.xml(或注解 )中配置的初始化参数(如 <init-param> ),这里springboot自己用实现类配置了后面会说,封装成 ServletConfig 对象,springboot配置了默认Dispathcherservlet

获取了注解实现类的spring容器AnnotationConfigServletWebServerApplicationContext,包含bean工厂,包含自己写的bean。
外部访问,DispatcherServlet即可调用这里bean

如果使用的不是注解类型,则创建spring容器的xml实现类XmlWebApplicationContext

private Class<?> contextClass = DEFAULT_CONTEXT_CLASS;
public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class
//可通过xml或者注解指定默认值,更换spring容器实现类
容器创建后会调用onRefresh

而onRefresh调用了initStrategies,初始化HandlerMappings或HandlerAdapters等

如initHandlerMappings,在spring容器中找HandlerMapping,多个则排序,然后如果没有handlerMapping,则会去取一些默认的handlerMapping
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerMapping later.
}
}
// Ensure we have at least one HandlerMapping, by registering
// a default HandlerMapping if no other mappings are found.
if (this.handlerMappings == null) {
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerMappings declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
for (HandlerMapping mapping : this.handlerMappings) {
if (mapping.usesPathPatterns()) {
this.parseRequestPath = true;
break;
}
}
}
getDefaultStratrgies这里会通过查找资源文件然后反射加载默认的配置


调用createDefaultStrategy工厂的方式创建bean

protected Object createDefaultStrategy(ApplicationContext context, Class<?> clazz) {
return context.getAutowireCapableBeanFactory().createBean(clazz);
}
如RequestMappingHandlerMapping继承于InitializingBean,通过工厂创建bean会调用初始化操作。接着调用父类afterPropertiesSet()

@Override
public void afterPropertiesSet() {
initHandlerMethods();
}
初始化HandlerMethods,拿到所有的bean调用processCandidateBean
protected void initHandlerMethods() {
for (String beanName : getCandidateBeanNames()) {
if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
processCandidateBean(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
其中会判断是否为Controller或RequestMapping

如果是,则进入函数detectHandlerMethods,其中会再判断这个控制里所有方法里面带RequestMapping的方法,封装为RequestMappingInfo包括参数,方法名等等

再注册在mappingRegistry中,这就是spring,controller内存马的原理,在写恶意controller时就是将写好的恶意controller放入mappingRegistry中

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
this.mappingRegistry.register(mapping, handler, method);
}
RequestMappingHandlerAdapter的初始化也是一样,这个类主要是处理请求是一个加了@RequestMapping的方法。
其他的适配器如HttpRequestHandlerAdapter则是处理继承了HttpRequestHandler(也能作为控制器)的bean

supports判断是否支持

initControllerAdviceCache主要找到@ControllerAdvice注解存放,这个注解常用于异常处理器

再分别找其中对应注解的方法,分别存放供以后使用。如ModelAttribute.class,InitBinder.class
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
public static final MethodFilter MODEL_ATTRIBUTE_METHODS = method ->
(!AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class) &&
AnnotatedElementUtils.hasAnnotation(method, ModelAttribute.class));
Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
public static final MethodFilter INIT_BINDER_METHODS = method ->
AnnotatedElementUtils.hasAnnotation(method, InitBinder.class);
DispatcherServlet如何创建的
这里从正序的角度描述,一步步断点倒着调试过来即可
Tomcat是如何找到Spring并加载?
spring继承Tomcat的ServletContainerInitializer,Tomcat通过SPI机制加载到spring的 SpringServletContainerInitializer,这就把tomcat和spring联系起来了。
SPI机制 我之前SnakeYaml文章中的一条链子介绍过
SPI(Service Provider Interface), JDK内置的一种服务提供发现机制。它的利用方式是通过在ClassPath路径下的META-INF/services文件夹下查找文件,自动加载文件中所定义的类

通过上图注释@HandlesTypes(WebApplicationInitializer.class),找到WebApplicationInitializer的所有实现类并调用onStartup方法

所以我们可以自己写WebApplicationInitializer的实现类配置DispatcherServlet,而springboot已实现SpringBootServletInitializer,因此不用配置springmvc

可以通过继承SpringBootServletInitializer配置configure简化配置
public class CustomServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(MyApplication.class)
.properties("spring.mvc.throw-exception-if-no-handler-found=true");
}
}
DispatcherServlet处理请求
Springboot默认配置的DispatcherServlet映射路径是/,可以通过配置server.servlet.context-path配置

TestController
@Controller
public class TestController {
@GetMapping("/a")
@ResponseBody
public String test(String name){
return "hello "+name;
}
}
当访问到DispatcherServlet对应的路径(/)后,tomcat便会调用对应Service方法,调用父类FrameworkServlet#service方法,解析方法请求后继续调用父类service方法

调用HttpServlet(Tomcat规范的Servlet方法),调用doGet方法

FrameworkServlet#doGet方法

核心就是调用了doService方法

而doService方法中的核心方法doDispatch处理分发。

首先调用getHandler获取请求对应的handler

getHandler,遍历每一个HandlerMapping,这里主要是用的RequestMappingHandlerMapping处理,所以这里只说其里面做了什么重要处理。其他HandlerMapping如BeanNameUrlHandlerMapping负责的是继承了Controller类且以("/")开头的bean名称
使用getPathWithinApplication获取访问路径,分别获取DispatcherServlet的映射和对uri做处理,这里能联想到由于spring和shiro对路径处理的不同导致权限绕过


lookupHandlerMethod
首先通过路径直接拿RequestMappinginfo,如果不是再将所有mapping遍历匹配,如有多个再选择排序一个最合适的。这里访问/a存在此路径,直接拿到了

通过getMattchingMappings将信息封装在RequestMappinginfo,通过mappingRegistry中的找到对应mapping的handler方法


在获取到handler后会创建一个执行链,包括拦截器和handler

接下来通过HandlerMethod获取对应的HandlerAdapter适配器

同样循环调用遍历HandlerAdapter调用supports方法找对应的adapter,adapter便会通过handlerMethod的类型去调用它适配的方法

调用applyPreHandler

执行所有拦截器的preHandle方法,如果其一个返回false则中断

接下来调用适配器的handler方法,其中会调用真正的方法返回ModelAndView对象
进入handlerInternal,调用invokeHandlerMethod

getDataBinderFactory 找出所有的@InitBinder注解(参数绑定转换)以及生成ModelFactroy,以生成Model对象(包括@ModelAtrribute等注解对Model的处理),然后设置参数解析器,返回值解析器。创建并初始化ModelAndView容器

调用invokeAndHandle,其中会获取参数,解析然后调用真实方法。然后使用返回值处理器解析返回值

invokeAndHandle 中调用getArgumentResolver,其中会遍历解析参数解析器,找到支持的解析器

如一些带注解的解析器

然后调用解析器解析参数

这一步就是获得真实的参数名

调用resolveName去拿值,如果为空则寻找默认值。然后如果设置了类型转换器,如string转date,即会调用转换,以及一些注解的解析。调用doInvoke反射执行原始方法。

接下来就是返回值处理逻辑
直接返回给浏览器或者返回页面
returnValueHandlers初始化了一些返回值处理器,调用handleReturnValue

handleReturnValue调用selectHanler选择返回值解析器,

如存在注解ResponseBody便会使此解析器

RequestResponseBodyyMethodProcessor调用handleReturnValue处理返回,调用writeWithMessageConverters

首先判断是否为String类型 设置值类型以及参数类型

接下来会调用循环找消息转换器也是同样的方式,这里处理String使用的是StringHttpMessageConverter。然后调用getAdvice().beforeBodyWrite.这里就是找有没有advice,在写入Body前会先运行Advice中的beforeBodyWrite

这个转换器支持返回纯文本以及任意类型,在上述的canWrite中,valueType以及这个选择的MediaType(浏览器同意返回的格式)都是支持的

最后直接写入即可

还有很多其他的返回值处理器,如ViewNameMethodReturnValueHandler,确认需要重定向,以及存入mavContainer,后面跳转等

DispatcherServlet页面渲染
在调用invokeAndHandle处理返回值后会调用getModelAndView获取mav

首先会调用updateModel,将带有@SessionAttributes中的值存入model中,设置视图名。创建mav返回

如带有@ResponseBody会在处理返回值时就设置RequestHandled为true,不再返回视图。

接下调用processDispatchResult进行渲染

其中调用render,用初始化的视图解析器解析视图名

后面这一点调试器坏了,只能直接看代码了
demo中核心的处理是这里,创建视图,前面是重定向和转发的处理,然后进入父类createView

一直进入到UrlBasedViewResolver#buildView,创建view,设置url。这里前缀和后缀都没有设置所以view的url就是index

view.rend寻找渲染文件,重点是这个方法

exposeModelAsRequestAttributes,遍历model设置在request中,跳转页面后即可使用request中的值

再调用这个函数找路径,即为之前设置的index

使用此路径生成RequestDispatcher(Tomcat的后端转发器)此时转发的也就是Dispatcher的根路径加上index --> http://..../app/index,转发此请求 但是显然是找不到此路径的

能找到index.jsp的方法,或者通过forward:/index.jsp
springmvc通过添加前后缀找到对应的jsp文件
@EnableWebMvc
public class AppConfig implements WebMvcConfigurer{
@Bean
public InternalResourceViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
把整个springmvc的流程简单梳理了一遍,里面的细节实在太多,只能粗略的看一遍,有些看了一遍但是没有记录,篇幅太长了,以后遇到需要深究的地方会再仔细看的,对框架的理解又稍微多了点。
学习视频:
【目前B站讲的最好的SpringMVC源码解析课(2023最新版)】https://www.bilibili.com/video/BV1wg41147yL?p=29&vd_source=2884b80d333f3bfc8048b360e6195550

浙公网安备 33010602011771号