我们在开发过程中会有这样的场景:需要在项目启动后执行一些操作,比如:读取配置文件信息,数据库连接,,删除临时文件,清除缓存信息,工厂类初始化,加载活动数据,或者缓存的同步等。我们会用多种实现方式,例如@PostConstruct、CommandLineRunner、ApplicationRunner、ApplicationListener都可以实现在springboot启动后执行我们特定的逻辑,接下来对比他们的区别
一、@PostConstruct
该注解被用来修饰一个非静态的void方法,被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务区执行一次
触发时机:
SpringBoot会把标记了Bean相关的注解(例如@Component、@Service、@Repository等)的类或接口自动初始化全局的单一实例,如果标记了初始化顺序会按照用户标记的顺序,否则按照默认顺序初始化。在初始化的过程中,执行完一个Bean。
spring中bean的创建过程
配置Bean(@Component、@Service、@Controller等注解配置)-->解析为Bean的元数据(Bean容器中的BeanDefinition对象)-->根据Bean的元数据生成Bean(创建Bean)
创建Bean的时候执行顺序
Constructor(构造方法)–>@Autowired(依赖注入)–>@PostConstruct(注释的方法)
优点:
使用简单,在spring容器管理的类中添加此注解即可
缺点:
1.在spring创建Bean的时候触发,此时容器还未完全初始化完毕,如果逻辑中引用了还未完成初始化的bean会导致异常,所以需要考虑加载顺序。
2.如果@PostConstruct方法内的逻辑处理时间较长,就会增加SpringBoot应用初始化Bean的时间,进而增加应用启动的时间。因为只有在Bean初始化完成后,SpringBoot应用才会打开端口提供服务,所以在此之前,应用不可访问。
3.一句话,会影响你程序启动的时间。
1、执行时机
springboot 完全初始化完毕后
2、SpringApplication.run 方法源码
public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); //设置线程启动计时器 stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); //配置系统属性:默认缺失外部显示屏等允许启动 configureHeadlessProperty(); //获取并启动事件监听器,如果项目中没有其他监听器,则默认只有EventPublishingRunListener SpringApplicationRunListeners listeners = getRunListeners(args); //将事件广播给listeners listeners.starting(); try { //对于实现ApplicationRunner接口,用户设置ApplicationArguments参数进行封装 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); //配置运行环境:例如激活应用***.yml配置文件 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); //加载配置的banner(gif,txt...),即控制台图样 Banner printedBanner = printBanner(environment); //创建上下文对象,并实例化 context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); //配置SPring容器 prepareContext(context, environment, listeners, applicationArguments, printedBanner); //刷新Spring上下文,创建bean过程中 refreshContext(context); //空方法,子类实现 afterRefresh(context, applicationArguments); //停止计时器:计算线程启动共用时间 stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } //停止事件监听器 listeners.started(context); //开始加载资源 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, listeners, exceptionReporters, ex); throw new IllegalStateException(ex); } listeners.running(context); return context; }
callRunner方法:
private void callRunners(ApplicationContext context, ApplicationArguments args) { //将实现ApplicationRunner和CommandLineRunner接口的类,存储到集合中 List<Object> runners = new ArrayList<>(); runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); //按照加载先后顺序排序 AnnotationAwareOrderComparator.sort(runners); for (Object runner : new LinkedHashSet<>(runners)) { if (runner instanceof ApplicationRunner) { callRunner((ApplicationRunner) runner, args); } if (runner instanceof CommandLineRunner) { callRunner((CommandLineRunner) runner, args); } } }
3、使用
从上面源码可以看到,在springboot完全初始化完毕后,会执行CommandLineRunner和ApplicationRunner,两者唯一的区别是参数不同,但是不会影响,都可以获取到执行参数。
@Component public class ServerDispatcher implements CommandLineRunner { @Override public void run(String... args){ // 逻辑代码 } } @Component public class ServerDispatcher2 implements ApplicationRunner { @Override public void run(ApplicationArguments args){ // 逻辑代码 } }
4、特点
可以添加 @Order 注解等注解:指定执行的顺序 (数字小的优先)
@Priority 或其他排序的接口也可
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Documented public @interface Order { /** * The order value. * <p>Default is {@link Ordered#LOWEST_PRECEDENCE}. * @see Ordered#getOrder() */ int value() default Ordered.LOWEST_PRECEDENCE; } public interface Ordered { int HIGHEST_PRECEDENCE = Integer.MIN_VALUE; int LOWEST_PRECEDENCE = Integer.MAX_VALUE; int getOrder(); }
顺序
- 先根据 @Order 注解的值,@Order 的 value 值越小,则优先,没有该注解则默认最后执行。
- @Order 值相同时,ApplicationRunner 优先于 CommandLineRunner。
- @Order 值和继承的接口类型都相同时,按照注入容器的顺序(应该是按照类的名称)。
三、ApplicationListener 接口
1、执行时机
在 IOC 容器的启动过程,当所有的 bean 都已经处理完成之后,spring 的 ioc 容器会有一个发布 ContextRefreshedEvent 事件的动作。
2、使用
//使用示例:监听 ContextRefreshedEvent @Component public class ServerDispatcher implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { // 逻辑代码 } }
3、注意
系统会存在两个容器,一个是 root application context , 另一个就是我们自己的 projectName-servlet context(作为root application context的子容器)
这种情况下,就会造成 onApplicationEvent 方法被执行两次。为了避免上面提到的问题,我们可以只在 root application context 初始化完成后调用逻辑代码,其他的容器的初始化完成,则不做任何处理
加入判断即可:if (event.getApplicationContext().getParent() == null)
@Component public class ServerDispatcher implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { // 当 spring 容器初始化完成后就会执行该方法 if (event.getApplicationContext().getParent() == null) { //逻辑代码 } } }
监听事件的类型
a、ContextRefreshedEvent:ApplicationContext 被初始化或刷新时,该事件被发布。这也可以在 ConfigurableApplicationContext 接口中使用 refresh() 方法来发生。此处的初始化是指:所有的 Bean 被成功装载,后处理 Bean 被检测并激活,所有 Singleton Bean 被预实例化,ApplicationContext 容器已就绪可用。
b、ContextStartedEvent:当使用 ConfigurableApplicationContext (ApplicationContext子接口)接口中的 start() 方法启动 ApplicationContext 时,该事件被发布。你可以调查你的数据库,或者你可以在接受到这个事件后重启任何停止的应用程序。
c、ContextStoppedEvent:当使用 ConfigurableApplicationContext 接口中的 stop() 停止 ApplicationContext 时,发布这个事件。你可以在接受到这个事件后做必要的清理的工作。
d、ContextClosedEvent:当使用 ConfigurableApplicationContext 接口中的 close() 方法关闭 ApplicationContext 时,该事件被发布。一个已关闭的上下文到达生命周期末端;它不能被刷新或重启。
e、RequestHandledEvent:这是一个 web-specific 事件,告诉所有 bean HTTP 请求已经被服务。只能应用于使用DispatcherServlet的Web应用。在使用Spring作为前端的MVC控制器时,当Spring处理用户请求结束后,系统会自动触发该事件。
四、小结
1、@PostConstruct
不建议用来初始化数据。是在spring启动完成之前执行的,除非必要,不要用这两个去加载初始化数据。
2、如果刚好可以在一个方法内执行 Spring 启动后的动作,可以让 SpringBoot 的启动类继承上述的某个接口,然后重写相应的方法即可。
3、在数据初始化层面,不推荐@PostConstruct和ApplicationListener,原因是两者都会影响程序的启动,如果执行逻辑耗时很长,启动服务时间就很长。
4、建议使用CommandLintRunner、ApplicationRunner的方式,不会影响服务的启动速度,处理起来也比较简单。