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