Article Directory
foreword
The focus of this chapter is to understand what the main process of springMvc does. Some details are not very detailed, and its source code is not obscure.
SpringMvc startup instructions
A process of Tomcat startup (simple may not be accurate, mainly to understand what Tomcat does, in fact, the initialization of springMvc is linked to Tomcat startup, so it is still necessary to understand):
- Tomcat startup will parse server.xml, generate
servletContext
(servlet context), - Parse web.xml, then merge Tomcat's web.xml and application's web.xml, and then read the configuration
ContextLoaderListener
andDispatcherServlet
content into the container - Then look for the jar package under Tomcat, and look for
ServletContainerInitializer
the interface implementation class. This interface is provided for the application to initialize. SpringMvc has implemented this interface (springMVC is suchMETA-INF/services/javax.servlet.ServletContainerInitializer
a file under the classpath), but it does not do other operations. Through Implement this interface and configure it/META-INF/services/
below , it can be read and executed by Tomcat. The specific loading logic is inWebappServiceLoader
ContextLoaderListener
Initialize the spring container through the listener in web.xml- Finally, by
DisapatcherServlet
initializing the servlet container, web.xml provides the defaultDefaultServlet``JspServlet
and executes here
There is another knowledge point:
The priority of servlet-mapping is as follows:
/* > / > *.jsp
There is a default web.xml in Tomcat, which is configured with two processors defaultServlet
and JspServlet
ones for processing static resources and jsp respectively.
These two defaults are used as the default configuration. When there is no configuration in our project, Tomcat's default configuration will connect it.
Therefore, in the configuration of the project, it is necessary to pay attention not to configure the servlet-mapper as /
and /*
, because the configuration in our project is DispatcherServlet
to handle requests of the controller type. If it is configured /*
, the priority is high, and the default DefualtServlet
one will also be intercepted.
The following configuration, interception /*
, in the controller hasRequestMapping("/index")
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
The page request xxx/index will go DispatcherServlet
. When the resource cannot be found, it will not go DefaultServlet
. It is equivalent to missing a default servlet.
And Tomcat also has a servlet-mapping, which intercepts the suffix. As long as the request has the suffix, it will go.
Therefore, the general configuration in the project is configuration *.html
or *.do
as a distinction, so that independent
springMvc main process
[SpringMVC process architecture diagram_gmvc diagram](https://blog.csdn.net/menglixiazhiweizhi/article/details/85318012?ops_request_misc=%7B%22request%5Fid%22%3A%22166495705016782417032134%22%2C%22scm% 22 %3A%2220140713.130102334…%22%7D&request_id=166495705016782417032134&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2 all sobaiduend~default-2-853180 12-null-null.142 v51 new_blog_pos_by_title,201 v3 control_1 & utm_term = springmvc graph &spm= 1018.2226.3001.4187)
such as container initialization
There is a listener ContextLoaderListener
Tomcat in springMvc that will call this initialization containerinitWebApplicationContext
位置:org.springframework.web.context.ContextLoader#initWebApplicationContext
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 1. 判断是否存在父容器,因为他需要初始化,当然已经存在父容器就是有问题的
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException("Cannot initialize context because there is already a root application context present - check whether you have multiple ContextLoader* definitions in your web.xml!");
} else {
// 记录日志
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
// 记录时间
long startTime = System.currentTimeMillis();
try {
// 创建父容器,也可以说是spring容器
if (this.context == null) {
this.context = this.createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = this.loadParentContext(servletContext);
cwac.setParent(parent);
}
// 接下来看这个方法,设置并初始化spring容器(bean扫描、实例化、后置处理器、国际化等)
this.configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 将如容器设置到servlet上下文
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
} else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
} catch (Error | RuntimeException var8) {
logger.error("Context initialization failed", var8);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8);
throw var8;
}
}
}
The method of creating a spring container createWebApplicationContext
is as follows
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 读取容器类
Class<?> contextClass = this.determineContextClass(sc);
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
} else {
// 实例化
return (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
}
}
See how the bottom layer reads the container class
protected Class<?> determineContextClass(ServletContext servletContext) {
// 从servlet上下文获取容器类全名
String contextClassName = servletContext.getInitParameter("contextClass");
if (contextClassName != null) {
try
// 不等于空就反射
return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
} catch (ClassNotFoundException var4) {
throw new ApplicationContextException("Failed to load custom context class [" + contextClassName + "]", var4);
}
} else {
// 等于空,就从默认配置中获取
contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
try {
return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
} catch (ClassNotFoundException var5) {
throw new ApplicationContextException("Failed to load default context class [" + contextClassName + "]", var5);
}
}
}
-
First, find the container class from the spring container,
-
Get it reflectively
-
If not, read from the default location, see the following sentence
java contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
It has a default, look for all references, it is
static
directly loaded under the block, you can see that it is under the bytecode file directoryContextLoader.properties
static { try { ClassPathResource resource = new ClassPathResource("ContextLoader.properties", ContextLoader.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException var1) { throw new IllegalStateException("Could not load 'ContextLoader.properties': " + var1.getMessage()); } currentContextPerThread = new ConcurrentHashMap(1); }
Locate the file location, there is really one
The file content is:
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
The class configured in this file is XmlWebApplicationContext
, we directly locate the past
back toorg.springframework.web.context.ContextLoader#initWebApplicationContext
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = this.loadParentContext(servletContext);
cwac.setParent(parent);
}
// 接下来看这个方法,设置并初始化spring容器
this.configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
String configLocationParam;
// 设置id
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
configLocationParam = sc.getInitParameter("contextId");
if (configLocationParam != null) {
wac.setId(configLocationParam);
} else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// 将spring容器作为父容器,将servlet容器作为子容器设置
wac.setServletContext(sc);
// 这里的`contextConfigLocation`就是web.xml配置里的那个`contextConfigLocation`,下面给了截图
configLocationParam = sc.getInitParameter("contextConfigLocation");
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// 初始化spring容器的配置到servlet容器
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment)env).initPropertySources(sc, (ServletConfig)null);
}
// 初始化操作
this.customizeContext(sc, wac);
// spring 的启动时调用的初始方法,这个在spring篇章讲过的AnnotationConfigApplicationContext类里
// 就是做了扫描bean,创建bean,postProcesser,国际化等操作
wac.refresh();
}
Child container initialization
DispatcherServlet
Inheritance HttpServletBean
, container initialization is done by the init() method.
Location: org.springframework.web.servlet.HttpServletBean#init
public final void init() throws ServletException {
// 这里是将servlet的配置信息设置的pvs,往细的看扯到了Tomcat的过程, ̄□ ̄||,之后在仔细看Tomcat
PropertyValues pvs = new ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
// beanWrapper包装该类
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
// 创建servlet资源加载器
ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
// 根据名字看是一个编辑器,在这里的作用是将变量替换,环境加载等操作
// ResourceEditor 资源编辑器,它是对如:file:E:/xxx/xxx,classpaht:xxxx, ${xxx}等这样的资源进行处理
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
// 这里并没有实现
this.initBeanWrapper(bw);
// 这里的方法在spring中也有,就是将属性值设置进去
bw.setPropertyValues(pvs, true);
} catch (BeansException var4) {
if (this.logger.isErrorEnabled()) {
this.logger.error("Failed to set bean properties on servlet '" + this.getServletName() + "'", var4);
}
throw var4;
}
}
// 初始化改servletBean 重点看这个方法,这个是让子类去实现的
this.initServletBean();
}
initServletBean()
realize, that isFrameworkServlet
protected final void initServletBean() throws ServletException {
// 打日志
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
// 时间记录
long startTime = System.currentTimeMillis();
try {
// 初始化web容器,也就是servlet容器,上文我说的是servlet容器,一样的
this.webApplicationContext = initWebApplicationContext();
// 这里是空实现
initFrameworkServlet();
}
// 下面就是日志一堆
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
位置:org.springframework.web.servlet.FrameworkServlet#initWebApplicationContext
protected WebApplicationContext initWebApplicationContext() {
// 查找当前servlet上下文对应的跟容器(spring容器)
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
// 暂定servlet容器对象为wac
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// 走这里是构造器实例化就已经传入的
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// 这里它有一个标志 active 这个标志着实例化完,并设置好了web容器需要的环境及工具
if (cwac.getParent() == null) {
// 如果目前获取到的servlet容器是没有父容器的,那么就把刚刚获取到的设置进去
cwac.setParent(rootContext);
}
// 开始刷新(设置各种容器环境:bean的扫描、注册、国际化等)
// 注意,这里在刷新完(设置完)会发布一个`ContextRefreshedEvent`事件
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// 构造器没有传入,它就会到servlet 上下文中找,然后返回
wac = findWebApplicationContext();
}
if (wac == null) {
// 如果到这里还没有,就自己创建一个,并刷新(设置容器环境)
// 注意,这里在刷新完(设置各种容器环境:bean的扫描、注册、国际化等)会发布一个`ContextRefreshedEvent`事件
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// 这里判断如果没有刷新,就会在刷新一次,这里再刷新,和`ContextRefreshedEvent`事件监听器里的一样,都是调用子类`DispatcherServlet`的实现方法
// 事件监听器位置在:org.springframework.web.servlet.FrameworkServlet.ContextRefreshListener#onApplicationEvent
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
onRefresh(wac);
The actual execution is the following code
位置:org.springframework.web.servlet.DispatcherServlet#initStrategies
You can see that it initializes many parsers: multi-file upload, internationalization, dynamic style, mapping processor, mapping processor adapter, exception handler, view parser...
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
In the above demo we simulated HandlerMappings
and handlerAdapters
, we focus on it.
request process
When the request comes, tomcat will call DispatcherServlet
to process, the location is: org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
It also corresponds to: javax.servlet.http.HttpServlet#service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
There is no need to post the method code. Its specific function is to request verification and distribution, get method calls doGet
, post calls doPost
like this
map processor
In the springMvc flowchart, it can be known that the HandlerAdapter obtains the corresponding mapping processor according to the request, but it is an interface,
Create a request handler
In springMvc, the interface processor:
- @Controller @RequestMapping
- Controller interface
- HttpRequestHandler
Create servlet request
Method 1: Inheritance HttpServlet
, rewrite doGet or doPost to process requests, but this is a Tomcat servlet directly used, so you need to configure servlet-mapping in web.xml
public class IndexController2 extends HttpServlet {
private static final long serialVersionUID = -2964194399437247271L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("in httpServlet");
super.doGet(req, resp);
}
}
<servlet>
<servlet-name>indexController2</servlet-name>
<servlet-class>com.liry.controller.IndexController2</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>indexController2</servlet-name>
<url-pattern>/index2.html</url-pattern>
</servlet-mapping>
Method 2: Implement Controller
the interface
public class IndexController3 implements Controller {
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
System.out.println("in controller");
ModelAndView result = new ModelAndView();
result.setViewName("index");
return result;
}
}
Then configure the mapping
There are also two ways to configure the mapping:
@Component("/index3.html")
: Annotation method defines beanName not url<bean id="/index3.html" class="com.liry.controller.IndexController3"/>
This method is the classic xml configuration
Although there are two types, it is actually one type, the two are the same, and the controller is saved as a bean
Method 3:@Controller@RequestMapping
The most common and convenient way is to use annotations. Multiple requests can be processed in one class
HandlerMapping
In the SpringMvc process, this is the request -> DisaptcherServlet -> HandlerAdapter -> HandlerMapping -> hander
It is not difficult to see that after going to the DispatcherServlet, or adapt the corresponding HandlerMapping through the HandlerAdapter, and then use the HandlerMapping to process the request. Here, the HandlerMapping can be regarded as a controller, so we can reverse it through the Adapter.
As you can see from the figure, it has obtained 3 types of HandlerMapping:
- RequestMappingHandlerMapping
- BeanNameUrlHandlerMapping
- SimpleUrlHandlerMapping
RequestMappingHandlerMapping
When we use @Controller
it, it will be scanned by it.
When starting the application, initialize the spring container and go to the callback function:
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#afterPropertiesSet
Then go to the parent class:
org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#initHandlerMethods
getCandidateBeanNames()
This method will get the beanName in the IOC container, and then processCandidateBean(beanName)
analyze the controller to see the internal method:
protected void processCandidateBean(String beanName) {
Class<?> beanType = null;
try {
// 获取class
beanType = obtainApplicationContext().getType(beanName);
}
catch (Throwable ex) {
// An unresolvable bean type, probably from a lazy bean - let's ignore it.
if (logger.isTraceEnabled()) {
logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
}
}
// 这个是判断是否是一个Controller
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
The judgment condition is existence: @Controller@RequestMapping
, here it should be noted that the following judgment is based on ||
the premise that @RequestMapping
the marked bean can be scanned by spring, and there is no problem in judging between @Controller
the current package and one .@Component
[External link picture transfer failed, the source site may have an anti-theft link mechanism, it is recommended to save the picture and upload it directly (img-VkzN9wCB-1665802757031)(E:/ALI/Documents/%E5%BE%85%E5%8F%91 %E5%B8%83%E6%96%87%E7%AB%A0/Typora/typora/images/image-20221014000036670.png)]
Then look at the process of parsing the processor:
protected void detectHandlerMethods(Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
Class<?> userType = ClassUtils.getUserClass(handlerType);
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
// 匿名函数,处理指定class和method对象
(MethodIntrospector.MetadataLookup<T>) method -> {
try {
return getMappingForMethod(method, userType);
}
catch (Throwable ex) {
throw new IllegalStateException("Invalid mapping on handler class [" +
userType.getName() + "]: " + method, ex);
}
});
if (logger.isTraceEnabled()) {
logger.trace(formatMappings(userType, methods));
}
methods.forEach((method, mapping) -> {
Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
registerHandlerMethod(handler, invocableMethod, mapping);
});
}
}
The logic of finding the method marked with RequestMapping is as follows:
What it finally found was RequestMapping
, but we also used GetMapping, PostMapping, DeleteMapping
these annotations, is there any other way to find it?
Unfortunately not, look at GetMapping
the annotation definition, GetMapping is RequestMapping
marked, then the method marked GetMapping is also marked RequestMapping, and the properties of GetMapping are @AliasFor
associated through annotations, so this is why using GetMapping has the same effect as using RequestMapping.
You can still take a look at its internal method logic. It uses the object parsed by the method RequestMappingInfo
as the value and the method as the key, and stores it in the map.
public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
final Map<Method, T> methodMap = new LinkedHashMap<>();
Set<Class<?>> handlerTypes = new LinkedHashSet<>();
Class<?> specificHandlerType = null;
// 判断是否是代理类
if (!Proxy.isProxyClass(targetType)) {
specificHandlerType = ClassUtils.getUserClass(targetType);
handlerTypes.add(specificHandlerType);
}
// 获取所有的接口的class
handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType));
for (Class<?> currentHandlerType : handlerTypes) {
// 这里还不太懂,代理对象要这样处理
final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);
ReflectionUtils.doWithMethods(currentHandlerType, method -> {
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
T result = metadataLookup.inspect(specificMethod);
if (result != null) {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
methodMap.put(specificMethod, result);
}
}
}, ReflectionUtils.USER_DECLARED_METHODS);
}
return methodMap;
}
Most have mapping (url) as the key, registered in the mapperRegistry, the internal division pathLookup, nameLookup, corsLookup
, the only way to get the handler is to get it pathLookUp
from
SimpleUrlHandlerMapping
Here is one more thing to mention. At the beginning, we briefly explained tomcat: Tomcat starts to read web.xml, first reads the application's web.xml, and then merges Tomcat's web. Two Servlets are configured in it, one is DefaultServlet, the other is JspServlt, and the two handle static resources and jsp pages respectively.
And here SimpleUrlHandlerMapping is for processing static resources, here are 4, namely:
/css/**
/js/**
/image/**
/
The first three are corresponding to the static resource mapping configured in spring-mvc.xml:
<!--静态资源映射-->
<!--本项目把静态资源放在了webapp的statics目录下,资源映射如下-->
<mvc:resources mapping="/css/**" location="/statics/css/"/>
<mvc:resources mapping="/js/**" location="/statics/js/"/>
<mvc:resources mapping="/image/**" location="/statics/images/"/>
<mvc:default-servlet-handler/> <!--这句要加上,要不然可能会访问不到静态资源,具体作用自行百度-->
And the last one is the DefaultServlet interception configured in web.xml in tomcat, as the last interception.
BeanNameUrlHandlerMapping
As the name suggests, this is a processor that uses beanName as url mapping. The configuration method of creating a processor above is already understandable. It uses beanName as the url interception address.
The parent classes of SimpleURLHandlerMapping and BeanNameURLHandlerMapping are bothorg.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMapping
They all belong to the method of matching processors through url addresses, but the final processed objects are different, resulting in different implementations
Summarize
HandlerMapping can be classified into two ways,
- Scan the specified annotation as the request handler's identifier
- Scan for beans and
/
use beans whose beanName starts with
HandlerAdapter
Corresponding to HandlerMapping, it also has 3 Adapters
-
RequestMappingHandlerAdapter
-
HttpRequestHandlerAdapter
-
SimpleControllerHandlerAdapter
位置:org.springframework.web.servlet.DispatcherServlet#getHandlerAdapter
It will traverse the Adapter, and then adapter.supports()
judge whether the Adapter supports the handler by calling. supports
The method is no longer suspenseful.
After that, reflection execution is called HandlerAdatper.handler
, which is one of its main processes.
demo - Simulate the main process of SpringMvc
Based on what we know above, write a springMvc based on spring container + Tomcat, just try it