各位读友们好,最近已经很久没有更新文章了,并不是觉得写文章没意思之类的,笔者很希望能在"乱七八糟"的互联上做一些开源(能力有限,先做现有技术和思想开源。除了靠编程赚钱以外,这可能是支撑我一直学习的动力,希望能学到更多的内容开源出去)。之所以没有持续更新的原因——真的没时间。白天上班,晚上回去学习(为了以后给各位读者写更有深度的文章)。至于每天在学习什么内容,这个以后会无条件分享给大家(大概是偏底层方面的,各种中间件的源码和Linux内核源码等等)。
好了,多的不提了,回归正题,今天也是在公司接手了一个老员工的项目。由于公司都是使用war包的形式,运行在公司的服务器的tomcat中。并不是现在流行的jar包内嵌tomcat的形式,两者恰恰相反。就看到项目中以下的代码。
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(TelecomApplication.class);
}
}
比较好奇的我,接手这个项目并不是直接看业务层面的内容,而是在思考出几个问题:
抱着问题,笔者第一时间的思考是:我们项目打包成一个war包丢入tomcat中,运行和生命周期都是依赖于tomcat,而我们的项目又是一个spring boot的项目,就必须使用spring boot启动逻辑来初始化项目(run方法)。而对于一个jar包项目都是运行我们项目中写的启动类中的main方法逻辑(也就是运行SpringApplication.run())。而tomcat自身启动肯定也是main方法,而你自身的项目也是一个main方法,那肯定行不通,所以笔者大胆猜测是存在一些接口来设置当前项目的启动逻辑(不走main方法),还有一些接口来做特定时期回调启动当前项目。
说了这么多,好像还没开始介绍我们的SpringBootServletInitializer类,那么了解一个类的入口在哪里,没错就是从注释入手。
很明显的意思就是说打war包的时候才需要这个类。并且上面的注释还说,最后实现当前类,重写configure方法,并且调用SpringApplicationBuilder.sources方法,将@Configuration类传入(Spring boot启动注解,也就是一个@Configuration)。所以就出现了本文章开头的那段代码逻辑
何时回调?我们暂时并不清楚,所以就先看SpringBootServletInitializer类中onStartup方法
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
servletContext.setAttribute(LoggingApplicationListener.REGISTER_SHUTDOWN_HOOK_PROPERTY, false);
// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
WebApplicationContext rootApplicationContext = createRootApplicationContext(servletContext);
if (rootApplicationContext != null) {
servletContext.addListener(new SpringBootContextLoaderListener(rootApplicationContext, servletContext));
}
else {
this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not "
+ "return an application context");
}
}
所有逻辑都在createRootApplicationContext()方法中,继续追进去。
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
// 创建SpringApplicationBuilder来整合一些配置项,然后生成SpringApplication类。
SpringApplicationBuilder builder = createSpringApplicationBuilder();类
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
builder.contextFactory((webApplicationType) -> new AnnotationConfigServletWebServerApplicationContext());
// configure方法就是我们重写的方法,把我们当前项目的启动类传入
builder = configure(builder);
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
// 熟悉的SpringApplication,项目中启动类main方法中也是用这个类调用run方法启动项目
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty()
&& MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
application.addPrimarySources(Collections.singleton(getClass()));
}
Assert.state(!application.getAllSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {
application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}
application.setRegisterShutdownHook(false);
// 内部逻辑调用SpringApplication.run方法启动项目。
return run(application);
}
对以上代码做一个总结:
以上代码是告诉读者Spring boot项目的另一种启动方式,所以接下来我们要找到onStartup方法的回调时机就能完美闭环。
而war包是运行在tomcat中,所以回调时机肯定是在tomcat源码中的某一个位置。这里不明白tomcat源码的读者也无关紧要,不过建议大家有时间去学习tomcat的源码。
我们看到tomcat源码中ServletContainerInitializer接口(这是servlet的接口)。确切的说,Spring boot是通过ServletContainerInitializer接口来完成的回调。
然后看到StandardContext中启动的生命周期startInternal回调函数中一部分代码逻辑。
// Call ServletContainerInitializers
for (Map.Entry>> entry :
initializers.entrySet()) {
try {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
这里遍历所有的ServletContainerInitializer接口,然后回调onStartup方法(这里是一个嵌套回调,这个onStartup并不是上面介绍的),而Spring通过SpringServletContainerInitializer实现了ServletContainerInitializer接口,重写了onStartup。然后一个ServletContainerInitializer接口又对应一个set集合(存放的是WebApplicationInitializer,也就是SpringBootServletInitializer的父类,也就是我们项目启动的回调类)。
所以我们回到Spring boot中先找到ServletContainerInitializer子类SpringServletContainerInitializer查看回调的具体逻辑。
@Override
public void onStartup(@Nullable Set> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List initializers = Collections.emptyList();
if (webAppInitializerClasses != null) {
initializers = new ArrayList<>(webAppInitializerClasses.size());
for (Class> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
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);
}
}
}
}
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);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
对以上代码做一个总结:
并不复杂,首先先合理分析jar和war包的区别,就能很快的定位会在哪里处理回调。
比较困难的就是定位tomcat的源码,这必须要明白他的架构(就是一个递归架构)。其实对于这个源码分析,就算读者不懂tomcat源码也不会很影响读者来理解,能明白tomcat会回调接口来初始化用户的Spring boot的项目就足够了。只不过懂tomcat源码能够完美闭环。
最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!