一、服务预热-引入
注意: 效果类似于Redis中实现的自动预热缓存,此处我们不进行预热数据,而是将线程池等资源进行预热!
情况说明: 无论是线程池还HttpClient,我们可能在生产中发现,我们的服务如果一开始就接收到了大流量,那么我们服务的承载能力是慢慢的起来的,而不是上来就满状态运行,这在大流量的情况下可能刚启动的时候会导致服务负载不够或者CPU瞬间飙高的情况!
思考: 如果我们可以提前进行服务预热,例如将线程池核心线程创建好(而不是流量来了再慢慢创建),那么我们就可以避免相关的问题产生!
package com.ssm.user.init;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
@Slf4j
public class ApplicationInit implements ApplicationListener<ApplicationReadyEvent> {
//我们使用函数式编程方式,来优雅地实现将预热的情况放入Map<方法描述,方法>中,最终在运行时取出并执行,顺便抛出出错的代码!
Map<String, InitFunction> initFunctionMap = new HashMap<>();
// 静态代码块随着类的加载而加载,非静态代码块随着对象的加载而加载
// 所以静态代码块再调用类的时候执行。非静态代码块创建对象时或者通过反射获取其类信息的时候执行
{
initFunctionMap.put("预热fastjson", this::initFastJson); //this::方法名 可以令类中方法实现接口中的抽象方法
initFunctionMap.put("预热线程池", this::initThreadPool);
}
/**
* 服务启动完成后,所有想要执行的操作,都可以在此执行
*/
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
initFunctionMap.forEach((desc, function) -> {
try {
long start = System.currentTimeMillis();
function.invoke();
log.info("ApplicationInit{}.costTime{}", desc, System.currentTimeMillis() - start);
} catch (Exception e) {
log.error("ApplicationInit{}.error", desc, e);
}
});
}
private void initFastJson() {
System.out.println("预热fastjson");
}
private void initThreadPool() {
System.out.println("预热线程池");
}
interface InitFunction {
void invoke();
}
}
二、异同
开发过程中会有这样的场景:需要在容器启动的时候执行一些内容,比如:读取配置文件信息,数据库连接,删除临时文件,清除缓存信息等相关需求。我们在前面使用Redis实现的自动预热缓存的时候,我们采用的是CommandLineRunner;然而在刚刚我们进行服务预热的代码中却使用了ApplicationListener,还有一个ApplicationRunner,这三者间其有什么相关性或者异同点呢?
在Spring框架下是通过ApplicationListener监听器来实现的,在Spring Boot中给我们提供了两个接口来帮助我们实现这样的需求,这两个接口就是CommandLineRunner和ApplicationRunner,他们的执行时机为容器启动完成的时候。
2.1 CommandLineRunner 接口 和 ApplicationRunner 接口
1)执行时机: SpringBoot 完全初始化完毕后
2)使用方式:
(1)CommandLineRunner
@Component
public class ServerDispatcher implements CommandLineRunner {
@Override
public void run(String... args){
// 逻辑代码
}
}
(2)ApplicationRunner
@Component
public class ServerDispatcher implements ApplicationRunner {
@Override
public void run(ApplicationArguments args){
// 逻辑代码
}
}
3)区别: 参数不同(CommandLineRunner中run方法的参数为String数组,而ApplicationRunner中run方法的参数为ApplicationArguments)
4)特点: 可以添加 @Order 注解等注解:指定执行的顺序(数字小的优先)
5)执行顺序:
先根据 @Order 注解的值,@Order 的 value 值越小,则优先,没有该注解则默认最后执行。
@Order 值相同时,ApplicationRunner 优先于 CommandLineRunner。
@Order 值和继承的接口类型都相同时,按照注入容器的顺序(应该是按照类的名称)。
2.2 ApplicationListener 接口
1)实现原理: 监听机制
2)监听 ContextEvent 或者 ApplicationEvent
在日常开发里我们常常会监听ContextRefreshedEvent 以及ApplicationReadyEvent ,来做一些内容初始化的操作,因为我们通常理解为在这两类Event发生的时候,容器的bean是可用的状态,且在容器启动的过程中,这两个事件只会发生一次,但是事实并非如此。
ApplicationReadyEvent :是SpringBoot事件,只会被调用一次 (推荐) ,在run方法的执行流程中被调用;
ContextRefreshedEvent :可能被调用多次,如果容器是AbstractRefreshableApplicationContext的子类(默认SpringBoot启动的容器是GenericWebApplicationContext,不是AbstractRefreshableApplicationContext的子类)
2.3 监听 ApplicationReadyEvent
@Component
public class ApplicationInit implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
// 逻辑代码
}
}
2.4 监听 ContextRefreshedEvent
@Component
public class ServerDispatcher implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
// 逻辑代码
}
}
注意事项: 在 IOC 容器的启动过程,当所有的 bean 都已经处理完成之后,Spring的 IOC 容器会有一个发布 ContextRefreshedEvent 事件的动作。系统会存在两个容器,一个是 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) {
//逻辑代码
}
}
}
三、拓展
ApplicationContext事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,可以实现ApplicationContext事件处理。如果容器中有一个ApplicationListener Bean,每当ApplicationContext发布ApplicationEvent时,ApplicationListener Bean将自动被触发。这种事件机制都必须需要程序显示的触发。
3.1 ApplicationEvent监听事件的类型
ApplicationFailedEvent :该事件为spring boot启动失败时的操作
ApplicationPreparedEvent :上下文context准备时触发
ApplicationReadyEvent :上下文已经准备完毕的时候触发
ApplicationStartedEvent :SpringBoot 启动监听类
SpringApplicationEvent :获取SpringApplication
ApplicationEnvironmentPreparedEvent :环境事先准备
3.2 ContextEvent监听事件的类型
ContextRefreshedEvent :ApplicationContext 被初始化或刷新时,该事件被发布。这也可以在 ConfigurableApplicationContext 接口中使用 refresh() 方法来发生。此处的初始化是指:所有的 Bean 被成功装载,后处理 Bean 被检测并激活,所有 Singleton Bean 被预实例化,ApplicationContext 容器已就绪可用。
ContextStartedEvent :当使用 ConfigurableApplicationContext (ApplicationContext子接口)接口中的 start() 方法启动 ApplicationContext 时,该事件被发布。你可以调查你的数据库,或者你可以在接受到这个事件后重启任何停止的应用程序。
ContextStoppedEvent :当使用 ConfigurableApplicationContext 接口中的 stop() 停止 ApplicationContext 时,发布这个事件。你可以在接受到这个事件后做必要的清理的工作。
ContextClosedEvent :当使用 ConfigurableApplicationContext 接口中的 close() 方法关闭 ApplicationContext 时,该事件被发布。一个已关闭的上下文到达生命周期末端;它不能被刷新或重启。
RequestHandledEvent :这是一个 web-specific 事件,告诉所有 bean HTTP 请求已经被服务。只能应用于使用DispatcherServlet的Web应用。在使用Spring作为前端的MVC控制器时,当Spring处理用户请求结束后,系统会自动触发该事件。