在我们写代码的时候,很多时候难免碰到一些需求是需要我们在项目启动的时候来 启动线程/读取缓存/开启某个服务等等操作,这时候难免会犹豫该怎么做,究竟在哪里执行这个操作?是加载spring bean的时候?还是项目刚启动的时候?
接下来我会介绍几种方法来实现spring项目启动时执行任务。
@PostConstruct注解
假设一个简单的需求,在项目启动时需要把产品的编号与对应的产品名称缓存到一个Map里面,程序中要用的时候就直接从这个Map里面拿。
先上代码再来分析:
@Service("prouctCacheService")
public class ProuctCacheServiceImpl implements ProuctCacheService {
private static final Map<Integer, String> cache = new HashMap<Integer, String>();
@PostConstruct
public void init() {
// 实际工作情况中更多的情况应该是从配置文件中读取出来数据然后存到Map中,这里只是简单的写了出来而已
cache.put(1, "product1");
cache.put(2, "product2");
cache.put(3, "product3");
}
@Override
public String getProduct(int num) {
return cache.get(num);
}
}
在使用缓存的时候直接调用getProduct(int num)即可,在代码中我们需要注意的是@PostConstruct注解,有该注解的方法所在bean会在初始化的时候会被执行一次,注意该方法所在的类必须是被spring管理的bean,就是得有@Component/@Service等注解或者在xml配置文件中进行了配置才生效,那么另外一个问题是,有该注解的方法内部如果需要使用某个dao层的bean或者其他service层的bean的方法的话,可以使用@Autowired注解注入的bean吗?答案是可以的,我们来分析下:首先,这是一个spring的bean,所以自动注入其他的bean必然是可以的,问题的关键是究竟是先@Autowired还是先执行了@PostConstruct注解的方法呢?经过测试发现是先@Autowired,然后在@PostConstruct,因此初始化这个bean的时候的顺序是:
该bean的构造方法constructor --> @Autowired --> @PostConstruct
这种方法用起来比较简单快捷,还有另外两个方法跟这个方法很相似,也很容易跟@PostConstruct的执行顺序弄混了,第一个是实现InitializingBean接口,重写afterPropertiesSet()方法,作用其实与@PostConstruct一样,也是在这个bean初始化的时候执行该方法的,下面看代码:
@Service("prouctCacheService2")
public class ProuctCacheServiceImpl2 implements ProuctCacheService, InitializingBean {
private static final Map<Integer, String> cache = new HashMap<Integer, String>();
@Override
public String getProduct(int num) {
return cache.get(num);
}
@Override
public void afterPropertiesSet() throws Exception {
// 实际工作情况中更多的情况应该是从配置文件中读取出来数据然后存到Map中,这里只是简单的写了出来而已
cache.put(1, "product1");
cache.put(2, "product2");
cache.put(3, "product3");
}
}
另一种方法是在xml配置中配置bean的init-method属性,在实际工作中经常是这种方式与@PostConstruct二选其一,也很简单容易理解,一个是基于注解,一个是基于xml配置文件,xml配置文件中应该这么写:
<bean id="ProuctCacheServiceImpl" class="cn.com.demo.test.ProuctCacheServiceImpl" init-method="init"></bean>
代码与上面的init()方法基本一致就不展示了。
加上这额外的两个方法,我们上面介绍了三种方式,这三种方式在我们使用时基本大都是三选一,因此推荐直接使用@PostConstruct,比较简洁,但是他们其实是可以共存的,他们共存时的执行顺序其实是:
该bean的构造方法constructor --> @Autowired --> @PostConstruct --> InitializingBean --> init-method
实现ApplicationContextAware接口
该方法就是实现ApplicationContextAware接口并重写setApplicationContext()方法,先看代码:
@Component("springBeanUtils")
public class springBeanUtils implements ApplicationContextAware {
Logger logger = LoggerFactory.getLogger(springBeanUtils.class);
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
springBeanUtils.applicationContext = applicationContext;
/* 在加载该bean是会执行一次的业务代码 */
}
public static Object getBeanByName(String beanName) {
if (applicationContext == null) {
return null;
}
return applicationContext.getBean(beanName);
}
public static <T> T getBean(Class<T> type) {
return applicationContext.getBean(type);
}
public static <T> Map<String, T> getBeans(Class<T> type) {
return applicationContext.getBeansOfType(type);
}
public static <T> T getBean(String beanName, Class<T> type) {
if (applicationContext.containsBean(beanName)) {
return applicationContext.getBean(beanName, type);
} else {
return null;
}
}
public static Object getBean(String beanName) {
if (applicationContext.containsBean(beanName)) {
return applicationContext.getBean(beanName);
} else {
return null;
}
}
}
首先与上面的@PostConstruct 方法相同,都是要求是一个spring管理的bean,并且也是在该bean加载的时候会执行一次我们重写的setApplicationContext()方法来给ApplicationContext对象赋值,而ApplicationContext对象是一个spring的上下文,可以把它理解为spring容器,换句话说,我们就在这个方法里面获取到了spring容器,并且我们把它保存到了一个成员变量中,这样做的原因是我们写了很多获取spring bean的工具方法,以后如果要获取某个bean的时候,并且不方便使用@Autowired注入bean的话(譬如说是在一个servlet类中),可以调用这里定义的工具方法来获取spring的bean。
使用这个方法在项目启动的时候执行一些业务代码的话,看起来是有一些投机取巧的,虽然可以实现我们的目标,其实一开始之所以想到在setApplicationContext()中执行代码,主要原因是在我们需要执行的代码中有用到了service/dao层的相关方法,因此需要得到相关的bean,而这里可以得到spring容器(也就相当于得到了所有bean),所以在该方法中是肯定可以用该类中定义的静态方法来获取我们需要的相关bean的。上面的思考是我最开始的想法,但是稍微深入思考一个问题:我们得到的spring容器是在spring加载这个bean的时候,那么会不会存在一种情况,此时我们获取到的spring容器中其实只有一部分的bean,另外还有一部分bean还没加载到容器中,所以我们在该方法中调用该类中定义的静态工具方法获取bean的时候会不会有可能失败(因为可能我们要获取的bean还没加载进来)。
查阅资料并思考后,我自己的理解如下:首先明确ApplicationContext是什么时候生成的,我们会在spring的xml配置文件中定义需要扫描注解的包,在这个包下所有的含spring注解的bean会被扫描出来(另一种麻烦的方式是在xml配置文件中列出来所有的bean,太麻烦了,现在基本已经不用了),而ApplicationContext是根据这个xml文件生成的spring上下文,可以理解为这时候所有的bean的定义已经拿到了,但是注意这时候并没有初始化(实例化)bean,也就是没有真正的生成每一个bean对象,而我们上面所说的加载的意思指的是初始化(实例化)bean,这个时候所有的bean的定义已经装载到spring容器(ApplicationContext)中了,并且所有已经初始化(实例化)过的bean会放到一个Map中,因此我们在调用该类的静态工厂方法获取某个bean的时候,假如该bean已经加载过了,则直接从Map中返回即可,如果还没有加载(初始化),则会在我们获取的时候(其实也是第一次调用该bean的时候)利用反射来实例化目标bean并放到Map中方便下次调用。
ps:以上只是我自己的理解而已,可能与真正情况有所出入,欢迎留言讨论。
利用Listener监听器实现
从标题就可以看出来,这种方式是利用监听器Listener来实现的,由于监听器的配置是在项目中的web.xml中的,因此理所应当的这种方式只适用于打包成war包的项目。
在web.xml中的配置:
<listener>
<listener-class>cn.com.***.***.EvContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/spring/spring-*.xml</param-value>
</context-param>
监听器类:
public class EvContextLoaderListener extends ContextLoaderListener{
Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void contextInitialized(ServletContextEvent event) {
logger.info("contextInitialized...");
logger.info("加载配置。。。");
/* 执行初始化操作 */
super.contextInitialized(event);
}
@Override
public void contextDestroyed(ServletContextEvent event) {
super.contextDestroyed(event);
}
}
首先我们要明确一点,在这里配置的监听器的作用是什么,监听器顾名思义就是监听一些事件的发生从而进行一些操作,譬如说监听ServletContext,HttpSession的创建,销毁,从而执行一些初始化加载配置文件的操作。当Web容器(譬如说tomcat)启动后,Spring的监听器会启动监听,监听是否创建ServletContext的对象,如果发生了创建ServletContext对象这个事件(当web容器启动后一定会生成一个ServletContext对象,所以监听事件一定会发生),会将继承ContextLoaderListener类的监听类实例化并且执行初始化方法,将spring的配置文件(也就是上面的配置文件中的context-param的value)中配置的bean注册到Spring容器中,也就是先执行了Listener,在Listener中的初始化方法中才开始向spring容器中注入bean,也就是上面的两种方式(@PostConstruct、ApplicationContextAware)均是在Listener初始化之后才进行的。正是因为这个原因,又带来了另一个问题:如果想在Listener中调用service/dao层的某个方法该怎么办?经过度娘的查找,我发现有两种方式,一是继承某个ContextLoaderListener相关类然后重写某个方法,该方法是在监听完spring容器加载完所有bean后调用的,这种方法我看了下比较麻烦,在这里就不展示出来了;第二种方法是写两个Listener,让第一个Listener启动并加载spring容器完毕,然后在第二个Listener中进行我们自定义的一些操作,要注意web.xml中配置多个Listener的话,Listener的执行顺序就是在web.xml中的顺序。
xml文件中的配置两个Listener:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>cn.com.evlink.evcharge.utils.EvContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/spring/spring-*.xml</param-value>
</context-param>
我们在自定义的Listener类中(上面配置中的第二个Listener)获取到service/dao的bean的方法也很多,譬如说把这个Listener类当做一个bean,使用@Autowired自动注入,又或者使用我们上面第二种方法中定义的springBeanUtils类中的静态工厂方法来获取。
ps:有一个小细节要注意,我们自定义的那个Listener不用再重写contextDestroyed方法,只需要重写contextInitialized来执行初始化操作即可,也不需要写super.contextInitialized(event),因为这样会造成初始化两次spring容器,会报错。
小结
以上三种方法,分别有各自的优缺点,我们在实际开发过程中应该灵活选用,如果是类似于缓存项目中的配置文件的配置项等适用于Listener,如果类似于项目启动时要启动某个线程/线程池的话就使用@PostConstruct注解好一些,而实现ApplicationContextAware接口这种方法一般是用来作为一个工具类在项目中的其他地方来使用的。