SpringBoot-WebApplicationinitializer启动过程及原理分析(ServletContainerInitializer加载及HandlesType源码分析)
Spring WebApplicationinitializer位于 Sring web下的 package org.springframework.web;容器启动之后会调用该接口的on startup方法;代码如下为什么容器启动之后会调用该接口的on startup 方法,主要是由于位于它同包下的另一个类 SpringServletContainerInitializer...
WebApplicationinitializer 转:https://blog.csdn.net/zq17865815296/article/details/79464403
Spring WebApplicationinitializer位于 Sring web下的 package org.springframework.web;
容器启动之后会调用该接口的on startup方法;代码如下
我们可以利用这个特性在初始化时做一些前置操作,最常见的就是spring-boot war包启动时 ,会继承 SpringBootServletInitializer,很明显因为这个特性,实现了war包启动。 先是启动Servlet服务器,服务器启动Springboot应用(springBootServletInitizer),然后启动IOC容器。
SpringBootServletInitializer实例执行onStartup方法的时候会通过createRootApplicationContext方法来执行run方法,接下来的过程就同以jar包形式启动的应用的run过程一样了,在内部会创建IOC容器并返回,只是以war包形式的应用在创建IOC容器过程中,不再创建Servlet容器了。
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
protected Log logger; // Don't initialize early
private boolean registerErrorPageFilter = true;
....
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext);
为什么容器启动之后会调用该接口的on startup 方法,主要是由于位于它同包下的另一个类 SpringServletContainerInitializer
可以看到SpringServletContainerInitializer 主要是继承了ServletContainerInitializer 会在容器启动之后调用onStrartUp方法,该方法有两个参数,一个是Set<Class>,一个是容器上下文ServletContext。Set<Class>的值由类头部的注解 @HandlesTypes(WebApplicationInitializer.class)控制,会传入所有继承了注解的类的class。
这里解释一下@HandleTypes是如何将所有实现了WebApplicationInitializer接口的Class找到的,转自https://www.cnblogs.com/feixuefubing/p/11593411.html
ServletContainerInitializer加载机制:https://blog.csdn.net/z69183787/article/details/104681096
------------------------------------------------------------------------------------------------------------------------------------------------
@ServletContainerInitializer加载机制及@HandlesTypes的实现原理及过程:
首先这个注解最开始令我非常困惑,他的作用是将注解指定的Class对象作为参数传递到onStartup(ServletContainerInitializer)方法中。
然而这个注解是要留给用户扩展的,他指定的Class对象并没有要继承ServletContainerInitializer,更没有写入META-INF/services/的文件(也不可能写入)中,那么Tomcat是怎么扫描到指定的类的呢。
答案是Byte Code Engineering Library (BCEL),这是Apache Software Foundation 的Jakarta 项目的一部分,作用同ASM类似,是字节码操纵框架。
源码可以参考tomcat中的类:org.apache.catalina.startup.ContextConfig # webConfig 方法,详细叙述了整个过程,代码中的注释也详细标注了每一个Step,大家可以自行阅读源码:
protected void processServletContainerInitializers() {
List<ServletContainerInitializer> detectedScis;
try {
WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
detectedScis = loader.load(ServletContainerInitializer.class);
public class WebappServiceLoader<T> {
private static final String LIB = "/WEB-INF/lib/";
private static final String SERVICES = "META-INF/services/";
public List<T> load(Class<T> serviceType) throws IOException {
String configFile = SERVICES + serviceType.getName();
1、processServletContainerInitializers:根据SPI机制获取所有实现ServletContainerInitializer接口的类。 并在
- initializerClassMap:Map<ServletContainerInitializer, Set<Class<?>>>(存储ServletContainerInitializer实现类A 与 A上@HandlesType注解取值接口的 实现类或子类,本案例中即WebApplicationInitializer的实现类)
- typeInitializerMap:Map<Class<?>, Set<ServletContainerInitializer>>(记录 ServletContainerInitializer的实现类A上的HandlesType注解值 与 实现类A的对应关系)
2、来到源码中标注的Step4,首先申明一个Map<String,JavaClassCacheEntry> javaClassCache(JavaClassCacheEntry记录了某一个类A的接口、超类和一个SciSet Set<ServletContainerInitializer> sciSet。如果A的父类和接口匹配@HandlesType的注解值,则在SciSet中添加typeInitializerMap对应的sci集合,初始化时为空)。
3、然后,根据classpath找出所有WebResource对象,逐个调用 processAnnotationsWebResource 、processAnnotationsStream方法(使用BCEL的ClassParser在字节码层面读取了/WEB-INF/classes和某些jar(应该可以在叫做fragments的概念中指定)),将相应的WebResource对象解析为JavaClass。接着进入checkHandlesTypes,顾名思义,接下来应该就是识别和匹配满足条件的HandlesType了。
4、在checkHandlesTypes方法中,首先做了一些判断后调用了 populateJavaClassCache 方法,这个方法将步骤3中的JavaClass构造成JavaClassCacheEntry对象(构造函数中填充了父类与接口),并加至步骤2中提到的javaClassCache中。随即对父类与接口递归调用 populateJavaClassCache 方法,最终效果:某个Class A 及 A的父类和接口 均添加到了 javaClassCache 中。
5、回到checkHandlesTypes方法,继续往下,由于初始化后当前类的JavaCLassCacheEntry中SciSet是空的,所以进入populateSCIsForCacheEntry方法,同样的,这个方法内部也对类A及它的父类和子类进行了populateSCIsForCacheEntry的递归调用。在调用过程中,执行了一个关键方法getSCIsForClass。
6、getSCIsForClass 接受了当前类的实现接口名interfaceName或父类作为入参,与步骤1中的typeInitializerMap的key进行匹配,最终得到了一个集合,这个集合里面的sci均满足如下条件:
- 实现了ServletContainerInitializer接口
- @HandlesType注解中的class取值 等于 传入的 interfaceName
7、最后,回到populateSCIsForCacheEntry方法,将步骤6中的返回值填充进当前类的JavaClassCacheEntry对象的sciSet中。这样一来,JavaClassCacheEntry就保存了当前类A的实现接口,父类以及@HandlesType匹配的ServletContainerInitializer集合。
8、再次回到checkHandlesTypes方法,正像前面说的那样,这个方法解决了@HandlesType的匹配问题。如果当前entry的sciSet不为空,则Introspection.loadClass 当前ClassName 生成 Class-A对象,并根据当前类A的JavaClassEntry对象中的sciSet 与 步骤1中的 initializerClassMap依次对比,将满足条件的当前Class-A 添加至Map中的Value-Set集合中
for (ServletContainerInitializer sci : entry.getSciSet()) {
Set<Class<?>> classes = initializerClassMap.get(sci);
if (classes == null) {
classes = new HashSet<>();
initializerClassMap.put(sci, classes);
}
classes.add(clazz);
}
9、于Step 11中交给org.apache.catalina.core.StandardContext,也就是tomcat 在 StandardContext中 执行 startInternal() 方法 实际调用ServletContainerInitializer.onStartup()的地方。
StandardContext 核心代码
@Override
public void addServletContainerInitializer(
ServletContainerInitializer sci, Set<Class<?>> classes) {
initializers.put(sci, classes);
}
// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
10、完成
------------------------------------------------------------------------------------------------------------------------------------------------
之后将set<class>和ServletContext传入该方法后,会通过放射的方式创建每一个类的实例保存到list中,并调用每一个类的onStartUp方法,执行初始化操作。
ServletContainerInitializer 是位于javax.servlet包下的类,容器启动之后会调用该类的onStartup方法。
可以看到类上的注释,大致意思为:继承这个接口必须在jar文件的 META-INF/services目录下声明一个文件,文件的名字是这个接口的完全限定类名称,并将被运行时的服务提供者查找机制或者被容器中特定的类似机制发现。在任一情况下,来自web服务器的jar排除的独立命令必须被忽略,发现这些服务的顺序必须遵循应用程序的加载委托模式。
去Spring的源码去找,就会发现接口声明的文件
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------
现在JavaConfig配置方式在逐步取代xml配置方式。而WebApplicationInitializer可以看做是Web.xml的替代,它是一个接口。通过实现WebApplicationInitializer,在其中可以添加servlet,listener等,在加载Web项目的时候会加载这个接口实现类,从而起到web.xml相同的作用。下面就看一下这个接口的详细内容。
首先打开这个接口,如下:
public interface WebApplicationInitializer {
void onStartup(ServletContext var1) throws ServletException;
}
只有一个方法,看不出什么头绪。但是,在这个包下有另外一个类,SpringServletContainerInitializer。它的实现如下:
package org.springframework.web;
import java.lang.reflect.Modifier;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.HandlesTypes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public SpringServletContainerInitializer() {
}
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList();
Iterator var4;
if(webAppInitializerClasses != null) {
var4 = webAppInitializerClasses.iterator();
while(var4.hasNext()) {
Class<?> waiClass = (Class)var4.next();
if(!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)waiClass.newInstance());
} catch (Throwable var7) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", var7);
}
}
}
}
if(initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
} else {
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
var4 = initializers.iterator();
while(var4.hasNext()) {
WebApplicationInitializer initializer = (WebApplicationInitializer)var4.next();
initializer.onStartup(servletContext);
}
}
}
}
这个类就比较有意思了,先不管其他的,读一下这段代码,可以得到这样的意思。
先判断webAppInitializerClasses这个Set是否为空。如果不为空的话,找到这个set中不是接口,不是抽象类,并且是
WebApplicationInitializer接口实现类的类,将它们保存到list中。当这个list为空的时候,抛出异常。不为空的话就按照一定的顺序排序,并将它们按照一定的顺序实例化。调用其onStartup方法执行。到这里,就可以解释WebApplicationInitializer实现类的工作过程了。但是,在web项目运行的时候,SpringServletContainerInitializer这个类又是怎样被调用的呢。
它只有一个接口,ServletContainerInitializer,通过它就可以解释SpringServletContainerInitializer是如何被调用的。它的内容如下,
package javax.servlet;
import java.util.Set;
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> var1, ServletContext var2) throws ServletException;
}
首先,这个接口是javax.servlet下的。官方的解释是这样的:为了支持可以不使用web.xml。提供了ServletContainerInitializer,它可以通过SPI机制,当启动web容器的时候,会自动到添加的相应jar包下找到META-INF/services下以ServletContainerInitializer的全路径名称命名的文件,它的内容为ServletContainerInitializer实现类的全路径,将它们实例化。既然这样的话,那么SpringServletContainerInitializer作为ServletContainerInitializer的实现类,它的jar包下也应该有相应的文件。打开查看如下:
哈,现在就可以解释清楚了。首先,SpringServletContainerInitializer作为ServletContainerInitializer的实现类,通过SPI机制,在web容器加载的时候会自动的被调用。(这个类上还有一个注解@HandlesTypes,它的作用是将感兴趣的一些类注入到ServletContainerInitializerde), 而这个类的方法又会扫描找到WebApplicationInitializer的实现类,调用它的onStartup方法,从而起到启动web.xml相同的作用。
然后,我们自己通过一个实例来实现相同的功能,通过一样的方式来访问一个servlet。
1、定义接口WebParameter,它就相当于WebApplicationInitializer。内容如下:
public interface WebParameter {
void loadOnstarp(ServletContext servletContext);
}
可以在这里面添加servlet,listener等。
2、定义Servlet。
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("TestSetvlet");
}
}
3、定义MyWebParameter作为WebParameter的实现类,将Servlet添加到上下文,并设置好映射。
public class MyWebParameter implements WebParameter {
public void loadOnstarp(ServletContext servletContext) {
ServletRegistration.Dynamic testSetvelt=servletContext.addServlet("test","com.test.servlet.MyServlet");
testSetvelt.setLoadOnStartup(1);
testSetvelt.addMapping("/test");
}
}
4、定义好WebConfig作为ServletContainerInitializer的实现类,它的作用是扫描找到WebParameter的实现类,并调用其方法。
@HandlesTypes({WebParameter.class})
public class WebConfig implements ServletContainerInitializer {
public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
Iterator var4;
if (set!=null){
var4=set.iterator();
while(var4.hasNext()){
Class<?> clazz= (Class<?>) var4.next();
if (!clazz.isInterface()&& !Modifier.isAbstract(clazz.getModifiers())&&WebParameter.class.isAssignableFrom(clazz)){
try {
((WebParameter) clazz.newInstance()).loadOnstarp(servletContext);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}
}
5、根据SPI机制,定义一个META-INF/services文件夹,并在其下定义相关文件名称,并将WebConfig的类全名称填入其中。
至此,相关内容就完成了,因为我用的maven,通过install将其作为jar包上传到本地仓库。从另外一个web项目调用这个包进行访问。
6、最终结果:
相关代码请转至我的github点击打开链接
ps:其中涉及到了SPI相关的内容,如果不懂请自行百度。如果认识有误,请大佬指出,谢谢。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)