简介 在SpringBoot还没兴起时,写SpringMVC代码还是基于xml配置的形式来做,但是由于当时对Spring只停留在一知半解和抄袭别人代码阶段,导致xml复制错误,导致项目跑步起来,其中一个就是Spring的父子容器问题,最近结合源码来做了一次彻底的分析,这里记录了一下分析源码的思路和过程
构建项目 分析源码的前提是会使用,所以这里使用Maven去构建一个Spring项目
首先先从配置文件看起
项目是基于Maven去构建的,首先当然是引入Spring依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>SpringMVCDemo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <dependencies> <!-- 使用 Servlet 3.1 API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.17.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.17.RELEASE</version> </dependency> </dependencies> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>11</java.version> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> </properties> <build> <plugins> <!-- Tomcat 8 Maven 插件用于构建可执行 war --> <!-- https://mvnrepository.com/artifact/org.apache.tomcat.maven/tomcat8-maven-plugin --> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat8-maven-plugin</artifactId> <version>3.0-r1655215</version> <executions> <execution> <id>tomcat-run</id> <goals> <!-- 最终打包成可执行的jar包 --> <goal>exec-war-only</goal> </goals> <phase>package</phase> <configuration> <!-- ServletContext 路径 --> <path>/</path> </configuration> </execution> </executions> </plugin> </plugins> </build> <pluginRepositories> <pluginRepository> <!-- tomcat8-maven-plugin 所在仓库 --> <id>Alfresco</id> <name>Alfresco Repository</name> <url>https://artifacts.alfresco.com/nexus/content/repositories/public/</url> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> </project>
为了找到当时的痛点,采用web.xml去构建MVC项目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/root-context.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/dispatcher.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
接着就是Spring上下文的配置文件 /WEB-INF/root-context.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.liu"/> </beans>
然后是SpringMVC上下文配置文件 /WEB-INF/dispatcher.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!-- 激活 Spring 注解驱动 --> <context:component-scan base-package="com.liu"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/"/> <property name="suffix" value=".jsp"/> </bean> </beans>
核心概念 分析源码的另一个关键点是去官网查看其项目架构设计,这样能极大缩短我们上手的时间
Spring是个依赖注入和依赖查找的框架,其核心是依靠ApplicationContext和BeanFactory容器来管理Bean的生命周期,其中ApplicationContext是对BeanFactory的抽象和封装,并对其做了扩展,其底层仍然也是使用BeanFactory来管理bean对象。ApplicationContext也可以类比为Classloader,一个Bean对象在多个ApplicationContext容器中具有不同的生命周期,就好比java中类的比较只在同一个Classloader中才有意义,一个类经由多个Classloader进行加载,其实不同的类一样。而在很早以前刚刚接触Spring和Spring MVC时,当时还在使用xml文件对Bean对象进行注册时,就经常因为<context:component-scan base-package=””/>标签使用错误导致Bean对象无法注入,这是由于Spring 的ApplicationContext是存在父子关系的,但子ApplicationContext找不到时其就会去父ApplicationContext中去查找,而配置component-scan错误导致子父容器的上下文发生了错误,导致Bean对象无法注入,最近结合了源码来具体分析这部分内容。
SpringMVC核心
这是官网的图片,SpringMVC 的核心就是DispatcherServlet,其就是一个Servlet的实现。如果之前有接触过Servlet就会知道在很早很早以前,我们写一个HTTP的接口,要去实现一个叫HttpServlet 的接口,并且重写doGet,doPost方法,来对应HTTP的get ,post的请求等等,写完的Servlet接口还必须要在web.xml中注册对应访问的URL对应处理逻辑的接口,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/dispatcher.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
久而久处web.xml的配置就越来越浮肿,而SpringMVC 索性就利用Spring 依赖查找和依赖注入去管理我们实现的对象,然后统一定义一个Servlet,就是DispatcherServlet,然后我们写的http的只需要注入到Srping中,再由DispatcherServlet完成接口的分发,让其去决定调用哪个接口。
Spring容器的构建 ContextLoaderListener是Spring上下文容器创建的地方,该容器是SpringMVC容器的父容器
我们从项目入口web.xml开始分析,再servlet容器启动时最先调用的是ContextLoaderListener ,并且其传入了contextConfigLocation参数,其值为我们Spring的上下文配置文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ContextLoaderListener extends ContextLoader implements ServletContextListener { public ContextLoaderListener() { } public ContextLoaderListener(WebApplicationContext context) { super(context); } public void contextInitialized(ServletContextEvent event) { this.initWebApplicationContext(event.getServletContext()); } public void contextDestroyed(ServletContextEvent event) { this.closeWebApplicationContext(event.getServletContext()); ContextCleanupListener.cleanupAttributes(event.getServletContext()); } }
我们看到这部分逻辑我们并看不懂,没关系我们可以从初始化的contextConfigLocation参数入手,我们在其父类ContextLoader中找到了这个参数,在web.xml中其注入的就是这个变量。接着我们在ContextLoader中查找contextConfigLocation在哪里被调用,根据这个参数我在代码中找打了initWebApplicationContext方法,而这个方法又在ContextLoaderListener类中调用,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { 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 { 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); } this.configureAndRefreshWebApplicationContext(cwac, servletContext); } } 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; } } }
由于我们知道Spring核心其实就是ApplicationContext构建的问题,我们看到代码里有一行
1 this.context = this.createWebApplicationContext(servletContext);
看上去是像构建ApplicationContext的方法,我们再进去
1 2 3 4 5 6 7 8 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); } }
再进入determineContextClass方法看下是如何获取容器喝获取的容器是具体的哪个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 protected Class<?> determineContextClass(ServletContext servletContext) { 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); } } }
由于我们并没哟传入contextClass这个参数,所以我们直接看以下代码,我们知道这个类似由defaultStrategies从查找出来的其是一个配置类
1 contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
那么我们只要查到defaultStrategies是在哪里被初始化的我们就知道我们初始化的是哪个ApplicationContext
我们在ContextLoader类中找到静态初始化方法,我们找到了实在DEFAULT_STRATEGIES_PATH这个属性中的值为ContextLoader.properties
1 2 3 4 5 6 7 8 9 10 11 12 static { // Load default strategy implementations from properties file. // This is currently strictly internal and not meant to be customized // by application developers. try { ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException ex) { throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage()); } }
我们我们只要全局搜索这个文件就知道我们注入的具体ApplicationContex了,搜索到的ContextLoader.properties文件如下
1 rg.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext
所以到这里我们就知道我们注入的是XmlWebApplicationContext
接着我们再回到ContextLoader 中的 initWebApplicationContext方法接着往下看,看到以下一行
1 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
我们知道了在初始化XmlWebApplicationContext后将其放在了servletContext中供后面调用。
MVC容器的构建 DispatcherServlet
上面我们看到了Spring 上下文容器是如何创建的,现在我们看下MVC容器DispatcherServlet是如何创建的,进入DispatcherServlet,里面代码非常多,很多东西短时间内无从入手,由于我们想要查看的是Spring容器和SpringMVC容器的关系,那我们就猜测这部分应该是在初始化中就初始化了两个容器的关系,而DispatcherServlet又是继承与Servlet,而Servlet初始化方法又在init()方法中,于是我们改变思路从init方法上往下找,看下具体初始化 的过程。最后我们在DispatcherServlet 的父类FrameworkServlet的initWebApplicationContext方法中找到了初始化的方法,该方法经过漫长的继承链后最终被Servliet的init方法调用从而完成初始化,其获从ServletContext中获取了Spring的上下文容器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 protected WebApplicationContext initWebApplicationContext() { //获取Spring上下文容易 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac; if (!cwac.isActive()) { if (cwac.getParent() == null) { cwac.setParent(rootContext); } this.configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { wac = this.findWebApplicationContext(); } if (wac == null) { //将Spring上下文容器作为MVC的父容器 wac = this.createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { synchronized(this.onRefreshMonitor) { this.onRefresh(wac); } } if (this.publishContext) { String attrName = this.getServletContextAttributeName(); this.getServletContext().setAttribute(attrName, wac); } return wac; }
接着 我们再进入createWebApplicationContext这个方法,我们看到其将Spring的容器作为MVC的父容器set进去了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { Class<?> contextClass = this.getContextClass(); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext"); } else { ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass); wac.setEnvironment(this.getEnvironment()); wac.setParent(parent); String configLocation = this.getContextConfigLocation(); if (configLocation != null) { wac.setConfigLocation(configLocation); } this.configureAndRefreshWebApplicationContext(wac); return wac; } }
关于依赖查找 Spring的依赖查找的具体逻辑是先在子容器中查找,如果子容器中没有bean对象则到父容器中查找,如果父容器中也没有则抛出异常,
具体代码逻辑在AbstractBeanFactory下的doGetBean方法中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { // Not found -> check parent. String nameToLookup = originalBeanName(name); if (parentBeanFactory instanceof AbstractBeanFactory) { return ((AbstractBeanFactory) parentBeanFactory).doGetBean( nameToLookup, requiredType, args, typeCheckOnly); } else if (args != null) { // Delegation to parent with explicit args. return (T) parentBeanFactory.getBean(nameToLookup, args); } else if (requiredType != null) { // No args -> delegate to standard getBean method. return parentBeanFactory.getBean(nameToLookup, requiredType); } else { return (T) parentBeanFactory.getBean(nameToLookup); } }
想当然的猜想 让我们回到开头容器配置的地方,在我的配置Spring容器和MVC容器的地方我都配置了一个扫描,这两容器都会扫描同一个包及子包的注解来注入Bean
1 <context:component-scan base-package="com.liu"/>
那么我这里猜想既然MVC的父容器是Spring容器,而Spring容器的查找策略是子容器查找不到依赖则去父容器查找那么我可以把MVC配置文件中的扫描给去掉,让项目依赖注入和查找时去父容器(Spring)中去查找依赖?
经过本人实践,答案是否定的,再我去掉MVC容器的扫描代码后访问http接口直接404,所以接下来我又继续去查找这部分的原因。
Controller和RestController 的处理 我第一点想到的就是从这两个注解入手,因为我是基于注解扫描的方式去注册Bean那么SpringMVC肯定要去扫描Controller,RestController,所以通过IDEA去全局去找这两个注解的引用,发现只在RequestMappingHandlerMapping中有存在以下代码
1 2 3 4 5 @Override protected boolean isHandler(Class<?> beanType) { return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); }
根据我们写代码和类命名的套路,这里应该就是扫描MVC注解和处理逻辑的地方,虽然我们找到了处理扫码MVC注解逻辑的代码当时我们大致扫了一眼里面的代码基本上是一头雾水,而且我对其继承体系和使用根本不了解,也不知道他是如何被Spring去调用的,于是我这里又想到DispatcherServlet,根据网上大量资料显示其是处理SpringMVC核心分发业务的地方,且其又实现了Servlet,Servlet的生命周期我们就熟悉了,所以我又从这边入手。
DispatcherServlet 通过源码查找在Servlet在调用doGet ,doPost类是会调用 DispatcherServlet#doDispatch方法,于是我们发现了新大陆,继续从这个类进行分析。我们找到了顺着代码往下找,找到getHandler方法,这是我们发现了我们之前的RequestMappingHandlerMapping,其去处理了我们注入的Controller和RestController对象,那么这里一定就是我们重点关注的地方
1 2 3 4 5 6 7 8 9 10 11 protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { for (HandlerMapping mapping : this.handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; }
当时RequestMappingHandlerMapping又是如何赋值给DispatcherServlet的呢,我们在DispatcherServlet中去搜索handlerMappings变量,找到了initHandlerMappings方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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"); } } }
找到其中的,我们看源码注释,这里写着保证最少要有一个HandlerMapping,如果没有的话就使用默认HandlerMapping,但是代码里有很多if逻辑,我们不知道是不是会进入到这个分支,在我们debug后发现确实会进入这个分支
1 2 3 4 5 6 7 8 9 // 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"); } }
接着再进入 getDefaultStrategies方法查看具体是怎么初始化HandlerMapping的,我看到貌似是从defaultStrategies读出来的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) { String key = strategyInterface.getName(); String value = defaultStrategies.getProperty(key); if (value != null) { String[] classNames = StringUtils.commaDelimitedListToStringArray(value); List<T> strategies = new ArrayList<>(classNames.length); for (String className : classNames) { try { Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader()); Object strategy = createDefaultStrategy(context, clazz); strategies.add((T) strategy); } catch (ClassNotFoundException ex) { throw new BeanInitializationException( "Could not find DispatcherServlet's default strategy class [" + className + "] for interface [" + key + "]", ex); } catch (LinkageError err) { throw new BeanInitializationException( "Unresolvable class definition for DispatcherServlet's default strategy class [" + className + "] for interface [" + key + "]", err); } } return strategies; } else { return new LinkedList<>(); } }
于是继续查找,找到其配置文件DEFAULT_STRATEGIES_PATH (DispatcherServlet.properties)
1 2 3 4 5 6 7 8 9 10 11 12 static { // Load default strategy implementations from properties file. // This is currently strictly internal and not meant to be customized // by application developers. try { ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException ex) { throw new IllegalStateException("Could not load '" + DEFAULT_STRATEGIES_PATH + "': " + ex.getMessage()); } }
找到如下配置,到此我们就已经了解到RequestMappingHandlerMapping是如何注入到DispatcherServlet的了
1 2 3 org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\ org.springframework.web.servlet.function.support.RouterFunctionMapping
至此我们已经成功了一大半,在知道RequestMappingHandlerMapping的初始化逻辑后我们回到getHandler其调用了mapping.getHandler,目测HandlerExecutionChain就是处理核心代码的地方
1 2 3 4 5 6 7 8 9 10 11 12 @Nullable protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { for (HandlerMapping mapping : this.handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; }
RequestMappingHandlerMapping 接着上面我们查看getHandler代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Override @Nullable public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); if (handler == null) { handler = getDefaultHandler(); } if (handler == null) { return null; } // Bean name or resolved handler? if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (logger.isTraceEnabled()) { logger.trace("Mapped to " + handler); } else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) { logger.debug("Mapped to " + executionChain.getHandler()); } if (hasCorsConfigurationSource(handler)) { CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); config = (config != null ? config.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; }
接着进入getHandlerInternal其调用了lookupHandlerMethod
1 2 3 4 5 6 7 8 9 @Nullable protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<>(); List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); . . .
从代码中我们又可以看到this.mappingRegistry.getMappingsByUrl(lookupPath);方法,从方法名上看这里就是处理注册Controllerd 地方,于是我们继续寻找其是如何初始化的我们发现RequestMappingHandlerMapping实现了Spring的InitializingBean 接口,其是在Spring初始化后自动回执行的地方 其回调方法afterPropertiesSet -又调用了initHandlerMethods
1 2 3 4 5 6 7 8 protected void initHandlerMethods() { for (String beanName : getCandidateBeanNames()) { if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { processCandidateBean(beanName); } } handlerMethodsInitialized(getHandlerMethods()); }
继续进入getCandidateBeanNames(),我们看到代码里面找出了所以Spring中Object的之类,由于所有JAVA类都继承了Object,所以这里找出了所有注入到Spring的Bean对象
1 2 3 4 5 protected String[] getCandidateBeanNames() { return (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) : obtainApplicationContext().getBeanNamesForType(Object.class)); }
接着找到继续进入handlerMethodsInitialized方法找到processCandidateBean方法,注意到isHandler方法,这里就是我们最早之前看到的如果类是带有Controller或者RestController的注解的话将会交由DispatcherServlet去做分发处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 protected void processCandidateBean(String beanName) { Class<?> beanType = null; try { 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); } } if (beanType != null && isHandler(beanType)) { detectHandlerMethods(beanName); } }
接着再顺藤摸瓜找到detectHandlerMethods,这里的所及就是找到Spring中所有的类,将其方法和和url地址封装起来供后面DispatcherServlet去分发时查找路由路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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, (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); }); } }
结论 那么这里总结一下,之所以我们SpringMVC中配置错component-scan扫描的包后扫描不到Bean对象是因为我们配置了2个容器,一个是Spring容器,一个是MVC容器,那么有以下几种情况
1.Spring容器是MVC的父容器如果MVC扫描的包配置错误,那么扫描不到Controller注解和RestController注解,那么DispatcherServlet无法分发,直接就404
2.不配置Spring容器的扫描路径,在MVC中配置根包,扫描所有Spring的注解,那么项目正常工作,因为所有的Bean对象都注入到MVC容器(子容器中),后续所有查找都在子容器中能找到,自然就不会去父容器查找,那么这时父容器就没有用了
3.同理如果MVC上下文的扫描路径和Spring的相同,那么只会在子容器中MVC中进行依赖查找,父容器同样没用
4.如果将MVC容器中的所有注解都配置在MVC上下文扫描路径中,其余的注解都配置在Spring容器可扫描的路径中,那么在读取MVC注解时访问子容器,访问其他注解Bean对象时访问父容器(Spring的容器)
5.不用配置父容器,直接使用MVC的容器,这样做也很简单我们直接将web.xml中,ContextLoaderListener监听器删除即可,这样我们就没有父容器了
1 2 3 <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
解析源码经验
第一要素就是会使用,至少能跑的起来,用起来后才知道如何去查找项目的入口,才有切入点
第二要素是了解项目的架构和设计,大型的框架项目源码庞杂难懂,读取官网的架构设计能大大加快我们阅读的进度
第三要素是刚开始读源码时没必要看懂所有细节,刚刚接触时太注重时我们根本就没有头绪,因为要记住的东西太多,所以我们看懂主要逻辑即可
第四要素是DEBUG,在不清楚细节的时候DEBUG一下,看清楚流程走向
第五要素是善用编译工具,IDEA等工具大大方便了源码阅读,使用idea download source和 find usage能非常方便的找到源码应用和入口。