【spring】spring mvc与spring的整合源码分析
spring mvc与spring的整合简单介绍
- 本文源码基于spring-framework-5.3.10。
- mvc是spring源码中的一个子模块!
- 本文重点是无xml方式的整合springmvc。
- Spring整合SpringMVC唯一的体现就是父子容器。
spring mvc的父子容器
- 父容器(Spring)管理Service、Dao层的Bean, 子容器(SpringMVC)管理Controller的Bean。
- 子容器可以访问父容器的Bean, 父容器无法访问子容器的Bean。
spring整合spring mvc的思路(xml方式)
- 一般会在web.xml中配置下面的代码。核心是一个ContextLoaderListener(加载spring容器)、一个DispatcherServlet(加载spring mvc容器)。
<!--spring 基于web应用的启动-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--全局参数:spring配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-core.xml</param-value>
</context-param>
<!--前端调度器servlet-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--设置配置文件的路径-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<!--设置启动即加载-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
零配置SpringMVC实现方式的基础:SPI
- SPI:我们叫他服务接口扩展,(Service Provider Interface) 直译服务提供商接口。
- 其实就是根据Servlet厂商(服务提供商)提供要求的一个接口,在固定的目录(META-INF/services)放上以接口全类名为命名的文件, 文件中放入接口的实现的全类名,该类由我们自己实现,按照这种约定的方式(即SPI规范),服务提供商会调用文件中实现类的方法,从而完成扩展。
- 在往简单说:定义一个借口,把他的实现的全限定类名配置到一个配置文件,然后通过JDK8提供的方法进行调用。
public class Test {
public static void main(String[] args) {
// 得到你配置的所有实现了IUserDao的接口
ServiceLoader<IUserDao> daos = ServiceLoader.load(IUserDao.class);
// 循环调用每个接口
for (IUserDao dao : daos) {
dao.save();
}
}
}
-
spring mvc与spi的关系:在Serlvet3-1的规范手册中:就提供了一种更加易于扩展可用于共享库可插拔的一种方式,参见8.2.4:
-
简单说就是放一个SPI规范:你在应用META-INF/services 路径下放一个 javax.servlet.ServletContainerInitailizer,里面配置具体的实现的全限定类名!容器(Tomcat)就会去调用
零配置SpringMVC实现方式的基础:HandlesTypes
- 通过HandlesTypes可以将感兴趣的一些类注入到ServletContainerInitializerde的onStartup方法作为参数传入。
- 在SpringServletContainerInitializer的类上配置了@HandlesTypes(WebApplicationInitializer.class)
- tomcat会自动找到程序中所有的实现了WebApplicationInitializer接口的实现类。
- 然后把找到的所有的WebApplicationInitializer实现类传入onStartUp方法的webAppInitializerClasses参数上!
SPI的方式SpringMVC启动原理
- 外置Tomcat启动的时候通过SPI的方式找到我们应用中的/META-INF/service/javax.servlet.ServletContainerInitializer。里面会配置org.springframework.web.SpringServletContainerInitializer。
- 所以会调用SpringServletContainerInitializer.onStartUp()方法。
- 调用onStartUp()前会先找到@HandlesTypes(WebApplicationInitializer.class) 所有实现了WebApplicationInitializer的类,传入到OnStartup的webAppInitializerClasses参数中,并传入Servlet上下文对象。
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
// 得到一个空的数组,里面存放实例化好的WebApplicationInitializer对象
List<WebApplicationInitializer> initializers = Collections.emptyList();
// 找到有WebApplicationInitializer的实现类
if (webAppInitializerClasses != null) {
// 创建一个有长度的数组,长度和找到的WebApplicationInitializer实现类数量一致
initializers = new ArrayList<>(webAppInitializerClasses.size());
for (Class<?> waiClass : webAppInitializerClasses) {
// 接口和抽象类servlet容器也会给我们,但是我们不要
// 排除接口和容器
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// 实例化,然后添加到集合中
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
// 没有实例化的WebApplicationInitializer对象,直接返回。。
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
// 调用initializer.onStartup 进行扩展
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
spring整合spring mvc的思路(无xml方式)
- 直接看默认是没有一个类(不是接口,不是抽象类的)去实现了WebApplicationInitializer接口,所以需要我们自己去实现
/**
* ZhangweiStarterInitializer需要在META-INF/services/javax.servlet.ServletContainerInitailizer进行配置!
* AbstractAnnotationConfigDispatcherServletInitializer 他的父类是WebApplicationInitializer
*/
public class ZhangweiStarterInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 方法实现说明:IOC 父容器的启动类
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
/**
* 方法实现说明 IOC子容器配置 web容器配置
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebAppConfig.class};
}
/**
* 方法实现说明
* @return: 我们前端控制器DispatcherServlet的拦截路径
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
/**
* IOC根容器,不扫描Controller的注解!
*/
@Configuration
@ComponentScan(basePackages = "com.zhangwei",excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,value={Controller.class}),
@ComponentScan.Filter(type = ASSIGNABLE_TYPE,value =WebAppConfig.class ),
})
public class RootConfig {}
/**
* 子容器
*/
@Configuration
@ComponentScan(basePackages = {"com.zhangwei"},includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION,value = {RestController.class, Controller.class})
},useDefaultFilters =false)
@EnableWebMvc // ≈<mvc:annotation-driven/>
public class WebAppConfig implements WebMvcConfigurer{
}
WebApplicationInitializer接口的实现类执行原理:onStartup源码调用逻辑
/**
* 调用起始位置。
* 源码位置:org.springframework.web.servlet.support.AbstractDispatcherServletInitializer.onStartup(ServletContext)
*/
public void onStartup(ServletContext servletContext) throws ServletException {
//注册registerContextLoaderListener
super.onStartup(servletContext);
//注册registerDispatcherServlet
registerDispatcherServlet(servletContext);
}
/**
* 父类的onStartup
* 源码位置:org.springframework.web.context.AbstractContextLoaderInitializer.onStartup(ServletContext)
*/
public void onStartup(ServletContext servletContext) throws ServletException {
// 父容器的注册
registerContextLoaderListener(servletContext);
}
/**
* 注册ApplicationContext
* 源码位置:org.springframework.web.context.AbstractContextLoaderInitializer.onStartup(ServletContext)
*/
protected void registerContextLoaderListener(ServletContext servletContext) {
// 创建父容器 ,
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
// 设置初始化器
listener.setContextInitializers(getRootApplicationContextInitializers());
servletContext.addListener(listener);
}
else {
logger.debug("No ContextLoaderListener registered, as " +
"createRootApplicationContext() did not return an application context");
}
}
/**
* 创建父容器
* 源码位置:org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer.createRootApplicationContext()
*/
protected WebApplicationContext createRootApplicationContext() {
// 这里得到的是我们自己的配置类
Class<?>[] configClasses = getRootConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
// 创建spring容器!
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
// 注册当前的配置类
context.register(configClasses);
return context;
}
else {
return null;
}
}
/**
* 注册DispatcherServlet
* 源码位置:org.springframework.web.servlet.support.AbstractDispatcherServletInitializer.registerDispatcherServlet(ServletContext)
*/
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");
// 创建子容器
WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
// 创建DispatcherServlet
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
// 初始化容器
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
if (registration == null) {
throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
"Check if there is another servlet registered under the same name.");
}
// 启动时立即加载
registration.setLoadOnStartup(1);
// 添加映射
registration.addMapping(getServletMappings());
// 是否异步支持
registration.setAsyncSupported(isAsyncSupported());
// 设置DispatcherServlet的过滤器
Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}
// 空方法, 可以再对DispatcherServlet进行定制
customizeRegistration(registration);
}
/**
* 创建子容器
* 源码位置:org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer.createServletApplicationContext()
*/
protected WebApplicationContext createServletApplicationContext() {
// 创建一个spring容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
// 调用我们重写的配置类:WebAppConfig
Class<?>[] configClasses = getServletConfigClasses();
if (!ObjectUtils.isEmpty(configClasses)) {
// 给容器注册配置类
context.register(configClasses);
}
return context;
}
监听器的初始化源码分析:核心是调用父容器的refresh方法
/**
* 监听器的初始化源码开始位置。tomcat会调用到这里
* 源码位置:org.springframework.web.context.ContextLoaderListener.contextInitialized(ServletContextEvent)
*/
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}
/**
* 初始化ApplicationContext
* 源码位置:org.springframework.web.context.ContextLoader.initWebApplicationContext(ServletContext)
*/
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!");
}
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 {
// xml会在这里创建
if (this.context == null) {
this.context = createWebApplicationContext(servletContext);
}
// 我们配置的注解会生成AnnotationConfigWebApplicationContext对象,属于ConfigurableWebApplicationContext
if (this.context instanceof ConfigurableWebApplicationContext) {
// 得到AnnotationConfigWebApplicationContext对象
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
// 这里在spring5以上,都是null。可以自己扩展!
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent ->
// determine parent for root web application context, if any.
// 默认返回null
ApplicationContext parent = loadParentContext(servletContext);
// 设置父容器为null
cwac.setParent(parent);
}
// 配置刷新容器
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 绑定父子容器!在servlet域中设置根容器(在子容器就可以直接拿到了)
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
// 获取线程上下文类加载器,默认为WebAppClassLoader
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
// 如果spring的jar包放在每个webapp自己的目录中
// 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
// 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的
else if (ccl != null) {
// 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
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 (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
/**
* 配置和刷新容器
* 源码位置:org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext, ServletContext)
*/
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 设置id
String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
if (idParam != null) {
wac.setId(idParam);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
// 设置ServletContext到spring上下文
wac.setServletContext(sc);
// 获得servlet容器中的全局参数contextConfigLocation (xml)
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}
// 在容器加载前 可以通过设置初始化参数contextInitializerClasses、globalInitializerClasses 进行扩展
customizeContext(sc, wac);
// 刷新容器
wac.refresh();
}
初始化DispatcherServlet:核心是调用父容器的refresh方法
/**
* 外置的tomcat会调用这个方法
* 源码位置:org.springframework.web.servlet.HttpServletBean.init()
*/
public final void init() throws ServletException {
// 解析 init-param 并封装只 pvs 中(xml)
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
// 将当前的这个 Servlet 类转化为一个 BeanWrapper,从而能够以 Spring 的方法来对 init-param 的值进行注入
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
// 属性注入
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
}
throw ex;
}
}
// Let subclasses do whatever initialization they like.
// 初始化Servlet的Bean
initServletBean();
}
/**
* 初始化Servlet的Bean
* 源码位置:org.springframework.web.servlet.FrameworkServlet.initServletBean()
*/
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 {
// 初始化容器!
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() {
// 获得ContextLoaderListener存的父容器
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// 获得子容器
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// 如果没有设置父容器 spring的doGetBean使用
if (cwac.getParent() == null) {
// 设置父容器
cwac.setParent(rootContext);
}
// 配置并且加载子容器
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// 从servlet上下文根据<contextAttribute>名字从域里面获取
wac = findWebApplicationContext();
}
if (wac == null) {
// xml会在这里创建
wac = createWebApplicationContext(rootContext);
}
//refreshEventReceived 它会在容器加载完设置为true (通过事件onApplicationEvent)
// springboot在这初始化组件
if (!this.refreshEventReceived) {
synchronized (this.onRefreshMonitor) {
onRefresh(wac);
}
}
if (this.publishContext) {
// 将当前容器放到servlet域中, 可以再创建子容器
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
/**
* 配置并且加载子容器
* 源码位置:org.springframework.web.servlet.FrameworkServlet.configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext)
*/
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// 设置id
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
}
}
// 设置servlet上下文
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
// 监听器(这里注册了很多的springmvc的组件) 委托设计模式
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
// 将init-param设置到Environment中
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
}
// 空方法可扩展
postProcessWebApplicationContext(wac);
// 容器启动前初始化
applyInitializers(wac);
wac.refresh();
}
结束语
- 获取更多本文的前置知识文章,以及新的有价值的文章,让我们一起成为架构师!
- 目前已经完成了并发编程、MySQL、spring源码、Mybatis的源码。可以在公众号下方菜单点击查看之前的文章!
- 接下来的目标是深入分析JVM、tomcat、redis
- 这个公众号,无广告!!!每日更新!!!