1. 背景
1.1. 背景介绍
在web项目中我们有时会遇到这种需求,在web项目启动后需要开启线程去完成一些重要的工作,例如:往数据库中初始化一些数据,开启线程,初始化消息队列等,在这种需求下,如何在web容器启动后执行这些工作就成为了本文的重点。
1.2. 测试项目搭建
首先我们新建一个web项目来模拟这种需求,这里我们选择创建一个maven项目
在项目的pom文件中添加以下properties项
<properties>
<!--web--> <servlet.version>3.1.0</servlet.version> <!--spring--> <spring-framework.version>4.3.8.RELEASE</spring-framework.version> <!--logging--> <logback.version>1.1.7</logback.version> <slf4j.version>1.7.5</slf4j.version> </properties>添加以下依赖
<dependencies>
<dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.3.8.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.8.RELEASE</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-access</artifactId> <version>${logback.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>3.8.1</version> <scope>test</scope> </dependency> </dependencies>等待maven构建完成之后我们就可以开始搭建一个用于测试的web项目
常规项目中基本上会搭配spring框架来构建,这里我们也不例外,这里我们使用0配置文件来构建一个web项目
有关spring0配置文件构建项目可以在网上找到很多资料,这里就至简单的搭建一个,不作详细解释
1.首先在项目src目录下建立包结构如下
2.在config目录下建立以下两个文件用于配置本项目
public class WebInitializer implements WebApplicationInitializer {
Logger logger = LoggerFactory.getLogger(WebInitializer.class); public void onStartup(ServletContext servletContext) throws ServletException { AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); ctx.register(MyConfig.class); logger.debug("Boot sequence:开始"); ctx.setServletContext(servletContext); //ctx.refresh(); ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx)); servlet.addMapping("/"); servlet.setLoadOnStartup(1); servlet.setAsyncSupported(true); } }
@Configuration
@EnableWebMvc @ComponentScan("com.hei123") public class MyConfig extends WebMvcConfigurerAdapter { }到这里为止,这个测试项目就已经搭建完成了,接下来介绍几种常见的解决方案
2. 几种解决方案
2.1. 基于javaweb的ServletContextListener
1、在listener包下新建类SimpleServletListener实现ServletContextListener接口
public class SimpleServletListener implements ServletContextListener {
Logger logger = LoggerFactory.getLogger(SimpleConsumer.class); public void contextInitialized(ServletContextEvent sce) { logger.debug("Boot Sequence:监听ServletContext的监听器监听到ServletContext初始化"); /** * 在这里写需要执行的代码 */ } public void contextDestroyed(ServletContextEvent sce) { } }2、在WebInitializer类中的onStartup尾部添加如下代码
servletContext.addListener(SimpleServletListener.class);
2.2. 基于javaweb的Filter
- 在filter包下新建SimpleFilter类实现Filter接口
public class SimpleFilter implements Filter {
Logger logger = LoggerFactory.getLogger(SimpleConsumer.class); public void init(FilterConfig filterConfig) throws ServletException { logger.debug("Boot Sequence:在web初始化配置中配置的Filter初始化");//在这里写需要执行的代码
} public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { } public void destroy() { } }
- 在WebInitializer类中的onStartup尾部添加如下代码
servletContext.addFilter("SimpleFilter", SimpleFilter.class);
2.3. 基于javaweb的servlet
- 在servlet包下新建SimpleServlet继承HttpServlet
public class SimpleServlet extends HttpServlet {
Logger logger = LoggerFactory.getLogger(WebInitializer.class); @Override public void init() throws ServletException { logger.debug("Boot Sequence:在web初始化配置中配置的Servlet初始化"); //在这里写需要执行的代码 super.init(); } }- 在WebInitializer类中的onStartup尾部添加如下代码
ServletRegistration.Dynamic simpleServlet = servletContext.addServlet("SimpleServlet", new SimpleServlet());
simpleServlet.setLoadOnStartup(2);//这里设置为2是因为需要先启动springMVC的dispatcherServlet
2.4. 基于Spring的ApplicationListener
- 在listener包下新建SimpleApplicationListener类实现ApplicationListener<ContextRefreshedEvent>接口来监听spring容器启动完成的事件
@Component
public class SimpleApplicationListener implements ApplicationListener<ContextRefreshedEvent> { Logger logger = LoggerFactory.getLogger(SimpleConsumer.class); public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { logger.debug("Boot Sequence: 监听spring 容器Context初始化的的方法调用"); //在这里写需要执行的代码 } }
2.5. 基于Spring的PostProcessor
2.5.1 BeanFactoryPostProcessor
在postprocessor包下新建SimpleBeanFactoryPostProcessor类实现BeanFactoryPostProcessor接口
@Component
public class SimpleBeanFactoryPostProcessor implements BeanFactoryPostProcessor { Logger logger = LoggerFactory.getLogger(WebInitializer.class); public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException { logger.debug("Boot Sequence:bootFactory的后置处理器执行");//在这里编写需要执行的代码
} }2.5.2 BeanPostProcessor
在postprocessor包下新建SimpleBeanPostProcessor类实现BeanPostProcessor接口
@Component
public class SimpleBeanPostProcessor implements BeanPostProcessor { //注意:此接口中的方法会在初始化每一个Bean时都执行一次 Logger logger = LoggerFactory.getLogger(WebInitializer.class); public Object postProcessBeforeInitialization(Object o, String s) throws BeansException { return o; } public Object postProcessAfterInitialization(Object o, String s) throws BeansException { if(o instanceof SimpleConsumer){ logger.debug("Boot Sequence:SimpleConsumer的初始化之后执行");//在这里编写需要执行的代码
}else{ logger.debug("Other Bean:的初始化之后执行"); } return o; } }
2.6. 基于Spring的InitializingBean
- 在initializingbean包下新建SimpleConsumer类实现InitializingBean接口
@Component
public class SimpleConsumer implements InitializingBean { Logger logger = LoggerFactory.getLogger(SimpleConsumer.class); public void afterPropertiesSet() throws Exception { logger.debug("Boot sequence:SimpleConsumer Bean 依赖注入完成"); //在这里编写需要执行的代码 } }3. 解决方案之间的对比
3.1. 执行顺序
我在测试项目中添加了以上所有的几种解决方案从日志中可以看到几种方案的执行顺序
启动顺序 | 方案名称 | 解释 |
1 | 基于javaweb的ServletContextListener | 监听webContext初始化 |
2 | 基于javaweb的filter | WebContext初始化后会先加载定义的过滤器,然后才会加载定义的Servlet,而这里的spring容器也是借助定义的DispatcherServlet来初始化的。 |
3 | 基于spring的BeanFactoryPostProcessor |
|
4 | 基于spring的InitializingBean | 在SimpleConsumer的属性注入完成后执行 |
5 | 基于spring的BeanPostProcessor | 在SimplerConsumer初始化完成后执行 |
6 | 基于spring的ApplicationContextListener | 在spring容器初始化完所有的Bean后执行 |
7 | 基于javaweb的servlet | 在配置中我们将其执行顺序设置为2,此servlet将会在DispatcherServlet初始化完成后才会去初始化,因此会落在最后 |
3.2. 横向对比
| Servlet ContextListener | filter | BeanFactory PostProcessor | Initializing Bean | Bean PostProcessor | Application ContextListener | servlet |
自动执行 | √ | √ | √ | √ | √ | √ | √ |
引用其他类的变量或方法 | × | × | × | *不确定 | *不确定 | √ | √ |
Spring容器是否已进行属性注入 | × | × | × | 当前Bean所有属性已注入,且其属性中引用的其他属性也已注入 | 当前Bean所有属性已注入,且其属性中引用的其他属性也已注入 | √ | √ |
Web容器完全启动 | × | × | × | × | × | × | × |
*指若其他Bean已经初始化完成可引用,未初始化完成的Bean不可引用
3.3. 细节详解
3.3.1 BeanFactoryPostProcessor与BeanPostProcessor的区别
BeanFactoryPostProcessor与BeanPostProcessor别看名字长的差不多,其实里面的内容差距很大,
- BeanFactoryPostProcessor是在当Spring容器已经获取到所有的Bean初始化列表,并创建BeanFactory后才调用的后置处理器,此时所有的Bean都还未初始化
- BeanPostProcessor是会在每初始化一个Bean都会调用其中的postProcessAfterInitialization方法,此时可能已经初始化了一些Bean
3.3.2 Initializing中的AfterPropertiesSet与xml配置的init-method以及BeanPostProcessor之间的区别
如果我们通过xml配置文件来配置spring中的Bean的话,其中可以通过init-method配置一个用于初始化方法,那这三者之间有什么区别呢?
执行顺序
其实可以在本项目中再加上init-method来验证执行顺序,这里就不再去加了,直接解释好了,其实仅从名称上就可以看出执行顺序应为
AfterPropertiesSetàinitMethodàbeanPostProcessor
只有当属性设置完成之后,此Bean才算基本上创建完成,即afterPropertiesSet,
在Bean创建完成之后,可以对此Bean进行一些初始化操作,即init-method
在初始化完成之后,调用Bean的后置处理器来完成一些其他的操作
这一点可以在源码中查看,这里就不做多的解释了
3.3.3 web容器的启动顺序
1.在web容器启动时会所有的webContextListener会收到web容器启动的通知,并可以执行其中的监听方法
2.接下来web容器会先去初始化所有的filter过滤器
3.然后web容器会根据servlet的初始化顺序去初始化所有的Servlet,在本例中DispacherServlet启动顺序为1最大,
4.DispacherServlet中会去初始化spring容器
5.初始化其他的Servlet
6.web容器启动完成
3.3.4 如何选择
- 如果我们需要在web容器刚初始化就执行程序的话需要采用实现ServletContextListenre的方案来执行
- 如果我们需要spring容器中的所有内容都加载完毕的话要采用实现ApplicationContextListener的方案来执行。
总之,我们需要根据自己的实际情况来选择对应的方案来达到最好的效果
4. 总结
本文主要介绍了几种在web容器启动后自动执行代码的解决方案,并对这些解决方案进行了一些大概的分析,对其中的一些细节内容进行了一些解释,详细的解释需要通过观察源码才能对这些内容有更好的理解,不足之处,还请指正。
任何问题请联系hei12138@outlook.com