Spring Web零xml配置原理以及父子容器关系详解
前言
在使用Spring和SpringMVC的老版本进行开发时,我们需要配置很多的xml文件,非常的繁琐,总是让用户自行选择配置也是非常不好的。基于约定大于配置的规定,Spring提供了很多注解帮助我们简化了大量的xml配置;但是在使用SpringMVC时,我们还会使用到WEB-INF/web.xml,但实际上我们是完全可以使用Java类来取代xml配置的,这也是后来SpringBoott的实现原理。本篇就来看看Spring是如何实现完全的零XML配置。
正文
先来看一下原始的web.xml配置:
contextConfigLocation classpath:spring.xml webAppRootKey ServicePlatform.root org.springframework.web.context.ContextLoaderListener spring-dispatcher org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring-dispatcher.xml 0 spring-dispatcher /
这里各个配置的作用简单说下,context-param是加载我们主的sping.xml配置,比如一些bean的配置和开启注解扫描等;listener是配置监听器,Tomcat启动会触发监听器调用;servlet则是配置我们自定义的Servlet实现,比如DispatcherServlet。还有其它很多配置就不一一说明了,在这里主要看到记住context-param和servlet配置,这是SpringIOC父子容器的体现。
在之前的I文章中讲过IOC容器是以父子关系组织的,但估计大部分人都不能理解,除了看到复杂的继承体系,并没有看到父容器作用的体现,稍后来分析。
了解了配置,我们就需要思考如何替换掉这些繁琐的配置。实际上Tomcat提供了一个规范,有一个ServletContainerInitializer接口:
publicinterfaceServletContainerInitializer{
voidonStartup(Set>var1,ServletContextvar2)throwsServletException;
}
Tomcat启动时会调用该接口实现类的onStartup方法,这个方法有两个参数,第二个不用说,主要是第一个参数什么?从哪里来?另外我们自定义的实现类又怎么让Tomcat调用呢?
首先解答最后一个问题,这里也是利用SPI来实现的,因此我们实现了该接口后,还需要在META-INF.services下配置。其次,这里传入的第一个参数也是我们自定义的扩展接口的实现类,我们可以通过我们自定义的接口实现很多需要在启动时做的事,比如加载Servlet,但是Tomcat又是怎么知道我们自定义的接口是哪个呢?
这就需要用到@HandlesTypes注解,该注解就是标注在ServletContainerInitializer的实现类上,其值就是我们扩展的接口,这样Tomcat就知道需要传入哪个接口实现类到这个onStartup方法了。
来看一个简单的实现:
@HandlesTypes(LoadServlet.class)
publicclassMyServletContainerInitializerimplementsServletContainerInitializer{
@Override
publicvoidonStartup(Set>set,ServletContextservletContext)throwsServletException{
Iteratorvar4;
if(set!=null){
var4=set.iterator();
while(var4.hasNext()){
Class>clazz=(Class>)var4.next();
if(!clazz.isInterface()&&!Modifier.isAbstract(clazz.getModifiers())&&LoadServlet.class.isAssignableFrom(clazz)){
try{
((LoadServlet)clazz.newInstance()).loadOnstarp(servletContext);
}catch(Exceptione){
e.printStackTrace();
}
}
}
}
}
}
publicinterfaceLoadServlet{
voidloadOnstarp(ServletContextservletContext);
}
publicclassLoadServletImplimplementsLoadServlet{
@Override
publicvoidloadOnstarp(ServletContextservletContext){
ServletRegistration.DynamicinitServlet=servletContext.addServlet("initServlet","org.springframework.web.servlet.DispatcherServlet");
initServlet.setLoadOnStartup(1);
initServlet.addMapping("/init");
}
}
这就是Tomcat给我们提供的规范,通过这个规范我们就能实现Spring的零xml配置启动,直接来看Spring是如何做的。根据上面所说我们可以在spring-web工程下找到META-INF/services/javax.servlet.ServletContainerInitializer配置:
@HandlesTypes(WebApplicationInitializer.class)
publicclassSpringServletContainerInitializerimplementsServletContainerInitializer{
@Override
publicvoidonStartup(@NullableSet>webAppInitializerClasses,ServletContextservletContext)
throwsServletException{
Listinitializers=newLinkedList<>();
if(webAppInitializerClasses!=null){
for(Class>waiClass:webAppInitializerClasses){
//Bedefensive:Someservletcontainersprovideuswithinvalidclasses,
//nomatterwhat@HandlesTypessays...
if(!waiClass.isInterface()&&!Modifier.isAbstract(waiClass.getModifiers())&&
WebApplicationInitializer.class.isAssignableFrom(waiClass)){
try{
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch(Throwableex){
thrownewServletException("FailedtoinstantiateWebApplicationInitializerclass",ex);
}
}
}
}
if(initializers.isEmpty()){
servletContext.log("NoSpringWebApplicationInitializertypesdetectedonclasspath");
return;
}
servletContext.log(initializers.size()+"SpringWebApplicationInitializersdetectedonclasspath");
AnnotationAwareOrderComparator.sort(initializers);
for(WebApplicationInitializerinitializer:initializers){
initializer.onStartup(servletContext);
}
}
}
核心的实现就是WebApplicationInitializer,先看看其继承体系
AbstractReactiveWebInitializer不用管,主要看另外一边,但是都是抽象类,也就是说真的实例也是由我们自己实现,但需要我们实现什么呢?我们一般直接继承AbstractAnnotationConfigDispatcherServletInitializer类,有四个抽象方法需要我们实现:
//父容器
@Override
protectedClass>[]getRootConfigClasses(){
returnnewClass>[]{SpringContainer.class};
}
//SpringMVC配置子容器
@Override
protectedClass>[]getServletConfigClasses(){
returnnewClass>[]{MvcContainer.class};
}
//获取DispatcherServlet的映射信息
@Override
protectedString[]getServletMappings(){
returnnewString[]{"/"};
}
//filter配置
@Override
protectedFilter[]getServletFilters(){
MyFiltermyFilter=newMyFilter();
CorsFiltercorsFilter=newCorsFilter();
returnnewFilter[]{myFilter,corsFilter};
}
这里主要注意getRootConfigClasses和getServletConfigClasses方法,分别加载父、子容器:
@ComponentScan(value="com.dark",excludeFilters={
@ComponentScan.Filter(type=FilterType.ANNOTATION,classes={Controller.class})
})
publicclassSpringContainer{
}
@ComponentScan(value="com.dark",includeFilters={
@ComponentScan.Filter(type=FilterType.ANNOTATION,classes={Controller.class})
},useDefaultFilters=false)
publicclassMvcContainer{
}
看到这两个类上的注解应该不陌生了吧,父容器扫描装载了所有不带@Controller注解的类,子容器则相反,但需要对象时首先从当前容器中找,如果没有则从父容器中获取,为什么要这么设计呢?
直接放到一个容器中不行么?先思考下,稍后解答。回到onStartup方法中,直接回调用到AbstractDispatcherServletInitializer类:
publicvoidonStartup(ServletContextservletContext)throwsServletException{
super.onStartup(servletContext);
//注册DispatcherServlet
registerDispatcherServlet(servletContext);
}
先是调用父类:
publicvoidonStartup(ServletContextservletContext)throwsServletException{
registerContextLoaderListener(servletContext);
}
protectedvoidregisterContextLoaderListener(ServletContextservletContext){
//创建spring上下文,注册了SpringContainer
WebApplicationContextrootAppContext=createRootApplicationContext();
if(rootAppContext!=null){
//创建监听器
ContextLoaderListenerlistener=newContextLoaderListener(rootAppContext);
listener.setContextInitializers(getRootApplicationContextInitializers());
servletContext.addListener(listener);
}
}
然后调用createRootApplicationContext创建父容器:
protectedWebApplicationContextcreateRootApplicationContext(){
Class>[]configClasses=getRootConfigClasses();
if(!ObjectUtils.isEmpty(configClasses)){
AnnotationConfigWebApplicationContextcontext=newAnnotationConfigWebApplicationContext();
context.register(configClasses);
returncontext;
}
else{
returnnull;
}
}
可以看到就是创建了一个AnnotationConfigWebApplicationContext对象,并将我们的配置类SpringContainer注册了进去。接着创建Tomcat启动加载监听器ContextLoaderListener,该监听器有一个contextInitialized方法,会在Tomcat启动时调用。
publicvoidcontextInitialized(ServletContextEventevent){
initWebApplicationContext(event.getServletContext());
}
*/
publicWebApplicationContextinitWebApplicationContext(ServletContextservletContext){
longstartTime=System.currentTimeMillis();
try{
//Storecontextinlocalinstancevariable,toguaranteethat
//itisavailableonServletContextshutdown.
if(this.context==null){
this.context=createWebApplicationContext(servletContext);
}
if(this.contextinstanceofConfigurableWebApplicationContext){
ConfigurableWebApplicationContextcwac=(ConfigurableWebApplicationContext)this.context;
if(!cwac.isActive()){
//Thecontexthasnotyetbeenrefreshed->provideservicessuchas
//settingtheparentcontext,settingtheapplicationcontextid,etc
if(cwac.getParent()==null){
//Thecontextinstancewasinjectedwithoutanexplicitparent->
//determineparentforrootwebapplicationcontext,ifany.
ApplicationContextparent=loadParentContext(servletContext);
cwac.setParent(parent);
}
configureAndRefreshWebApplicationContext(cwac,servletContext);
}
}
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,this.context);
ClassLoaderccl=Thread.currentThread().getContextClassLoader();
if(ccl==ContextLoader.class.getClassLoader()){
currentContext=this.context;
}
elseif(ccl!=null){
currentContextPerThread.put(ccl,this.context);
}
returnthis.context;
}
}
可以看到就是去初始化容器,这个和之前分析xml解析是一样的,主要注意这里封装了ServletContext对象,并将父容器设置到了该对象中。
父容器创建完成后自然就是子容器的创建,来到registerDispatcherServlet方法:
protectedvoidregisterDispatcherServlet(ServletContextservletContext){
StringservletName=getServletName();
Assert.hasLength(servletName,"getServletName()mustnotreturnnullorempty");
//创建springmvc的上下文,注册了MvcContainer类
WebApplicationContextservletAppContext=createServletApplicationContext();
Assert.notNull(servletAppContext,"createServletApplicationContext()mustnotreturnnull");
//创建DispatcherServlet
FrameworkServletdispatcherServlet=createDispatcherServlet(servletAppContext);
Assert.notNull(dispatcherServlet,"createDispatcherServlet(WebApplicationContext)mustnotreturnnull");
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
ServletRegistration.Dynamicregistration=servletContext.addServlet(servletName,dispatcherServlet);
if(registration==null){
thrownewIllegalStateException("Failedtoregisterservletwithname'"+servletName+"'."+
"Checkifthereisanotherservletregisteredunderthesamename.");
}
/*
*如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,
值越小,servlet的优先级越高,就越先被加载
**/
registration.setLoadOnStartup(1);
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());
Filter[]filters=getServletFilters();
if(!ObjectUtils.isEmpty(filters)){
for(Filterfilter:filters){
registerServletFilter(servletContext,filter);
}
}
customizeRegistration(registration);
}
protectedWebApplicationContextcreateServletApplicationContext(){
AnnotationConfigWebApplicationContextcontext=newAnnotationConfigWebApplicationContext();
Class>[]configClasses=getServletConfigClasses();
if(!ObjectUtils.isEmpty(configClasses)){
context.register(configClasses);
}
returncontext;
}
这里也是创建了一个AnnotationConfigWebApplicationContext对象,不同的只是这里注册的配置类就是我们的Servlet配置了。然后创建了DispatcherServlet对象,并将上下文对象设置了进去。
看到这你可能会疑惑,既然父子容器创建的都是相同类的对象,何来的父子容器之说?
别急,这个在初始化该上文时就明白了。但是这里的初始化入口在哪呢?没有看到任何监听器的创建和调用。
实际上这里的上下文对象初始化是在Servlet初始化时实现的,即init方法,直接来到HttpServletBean的init方法(分析SpringMVC源码时讲过):
publicfinalvoidinit()throwsServletException{
...省略
//Letsubclassesdowhateverinitializationtheylike.
initServletBean();
}
protectedfinalvoidinitServletBean()throwsServletException{
try{
this.webApplicationContext=initWebApplicationContext();
initFrameworkServlet();
}
}
protectedWebApplicationContextinitWebApplicationContext(){
//这里会从servletContext中获取到父容器,就是通过监听器加载的容器
WebApplicationContextrootContext=
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContextwac=null;
if(this.webApplicationContext!=null){
//Acontextinstancewasinjectedatconstructiontime->useit
wac=this.webApplicationContext;
if(wacinstanceofConfigurableWebApplicationContext){
ConfigurableWebApplicationContextcwac=(ConfigurableWebApplicationContext)wac;
if(!cwac.isActive()){
if(cwac.getParent()==null){
cwac.setParent(rootContext);
}
//容器加载
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if(wac==null){
wac=findWebApplicationContext();
}
if(wac==null){
wac=createWebApplicationContext(rootContext);
}
if(!this.refreshEventReceived){
synchronized(this.onRefreshMonitor){
onRefresh(wac);
}
}
if(this.publishContext){
//Publishthecontextasaservletcontextattribute.
StringattrName=getServletContextAttributeName();
getServletContext().setAttribute(attrName,wac);
}
returnwac;
}
看到这里想你也应该明白了,首先从ServletContext中拿到父容器,然后设置到当前容器的parent中,实现了父子容器的组织,而这样设计好处我想也是很清楚的,子容器目前装载的都是MVC的配置和Bean,简单点说就是Controller,父容器中都是Service,Controller是依赖于Service的,如果不构建这样的层级关系并优先实例化父容器,你怎么实现Controller层的依赖注入成功呢?
总结
本篇结合之前的文章,分析了SpringMVC零XML配置的实现原理,也补充了之前未分析到父子容器关系,让我们能从细节上更加全面的理解SpringIOC的实现原理,相信看完本篇对于SpringBoot的实现你也会有自己的想法。希望能给大家一个参考,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。