概述
Spring
的核心思想就是容器,具有 控制反转(IoC) 和 面向切面(AOP) 两大核心;
Spring Boot
更是封装了Spring
,遵循 约定大于配置,加上自动装配的机制。很多时候我们只要引用了一个依赖,几乎是零配置就能完成一个功能的装配。一个高级开发者如果想精通
Spring
,不能仅仅局限于对框架的使用,而是需要对Spring
进行深入理解,了解Spring
对Bean
是如何进行管理的,熟知Spring
的各个扩展点。
在网上搜索Spring
扩展点,发现很少有博文说的很全的,只有一些常用的扩展点的说明。
因此想通过本篇文章,详细总结一下Spring
&Spring Boot
所有的扩展接口,以及各个扩展点的使用场景。通过不同的实例和图片给每一个扩展点加以说明,并且整理出了一个bean
在Spring
内部从被加载到最后初始化完成所有可扩展点的顺序调用图。从而我们也能窥探到bean
是如何一步步加载到Spring
容器中的。
👨🎓废话不多说,下面直接进入今天的主题。
1. 容器刷新之回调ApplicationContextInitializer
ApplicationContextInitializer
是Spring
框架原有的东西,这个类的主要作用就是在ConfigurableApplicationContext
类型(或者子类型)的ApplicationContext
做refresh
刷新之前,允许我们对ConfiurableApplicationContext
的实例做进一步的设置和处理。它是在Spring
容器刷新之前执行的一个回调函数。是在ConfigurableApplicationContext#refresh()
之前调用(当Spring
框架内部执行ConfigurableApplicationContext#refresh()
方法的时候或者在Spring Boot
的run()
执行时),作用是初始化Spring ConfigurableApplicationContext
的回调接口。
简单理解就是:用于在刷新容器之前初始化Spring的回调接口,通常用于Spring/SpringBoot容器中注入属性。
ApplicationContextInitializer的作用
通常用于需要对应用程序上下文进行编程初始化的web
应用程序中。例如,根据上下文环境注册属性源或激活概要文件。
ApplicationContextInitializer扩展
自定义ApplicationContextInitializerExtension扩展类
@Slf4j
public class ApplicationContextInitializerExtension implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment env = applicationContext.getEnvironment();
HashMap<String, Object> map = new HashMap<>();
map.put("name","austin");
MapPropertySource mapPropertySource = new MapPropertySource("applicationContextInitializerExtension", map);
env.getPropertySources().addLast(mapPropertySource);
log.info("[ApplicationContextInitializerExtension] Loading ...");
}
}
复制代码
手动在启动类添加initialier
@SpringBootApplication
public class SpringExtendPointsApplication {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(SpringExtendPointsApplication.class);
springApplication.addInitializers(new ApplicationContextInitializerExtension());
springApplication.run(args);
}
}
复制代码
run
方法是SpringApplication
的静态方法,其中会生成SpringApplication
实例对象,真正执行的是实例对象的run
方法。SpringFactoriesLoader
加载ApplicationContextInitializer
的过程就发生在生成 SpringApplication
实例的过程中。类加载完毕,且生成了实例,那这些初始化器什么时候生效呢?如下是run
方法执行流程。
ApplicationContextInitializer
是在准备Application
的上下文阶段被执行的。我们知道,Spring
是在刷新上下文的时候开始通过BeanFactory
加载Bean
,所以,ApplicationContextInitializer
的执行发生在 Bean
加载之前,但是此时的Environment
已经初始化完毕,我们可以在该阶段获得Environment
的实例,方便增加或修改一些值;此时ApplicationContext
实例也创建好了,可以预先在上下文中加入一些监听器,处理器等。
启动项目,访问http://localhost:36878/getAttributes
查看控制台输出:
2. 获取Spring容器对象
Spring
通过依赖注入方式能自动装配Bean
,在我们日常开发中,经常需要从Spring
容器中获取Bean
,获取Spring
对象主要提供了三种扩展实现:
FactoryBean
接口BeanFactoryAware
接口ApplicationContextAware
接口
FactoryBean接口
BeanFactory
是Bean
的工厂,可以帮我们生成想要的Bean
,而FactoryBean
就是一种Bean
的类型。当往容器中注入class
类型为FactoryBean
的类型的时候,最终生成的Bean
是通过FactoryBean
的getObject
获取的。
定义一个UserService(FactoryBean)
定义一个UserFactoryBeanExtension
,实现FactoryBean
接口,getObject
方法返回一个User
对象:
public class UserFactoryBeanExtension implements FactoryBean<User> {
private BeanFactory beanFactory;
@Override
public User getObject() throws Exception {
User user = new User();
user.setId("123");
user.setUsername("austin");
user.setEmail("austin@gmail.com");
user.setAge(26);
System.out.println("调用[UserFactoryBeanExtension]的getObject方法生成Bean:" + user);
return user;
}
@Override
public Class<?> getObjectType() {
// 这个UserFactoryBeanExtension返回的Bean的类型
return User.class;
}
}
复制代码
启动测试类
@Slf4j
@SpringBootApplication
public class SpringExtendPointsApplication {
public static void main(String[] args) {
SpringApplication.run(SpringExtendPointsApplication.class, args);
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(UserFactoryBeanExtension.class);
applicationContext.refresh();
log.info("获取到的Bean为:" + applicationContext.getBean(User.class));
}
}
复制代码
控制台打印:
调用[UserFactoryBeanExtension]的getObject方法生成Bean:User(id=123, username=austin, email=austin@gmail.com, age=26)
[main] c.a.s.SpringExtendPointsApplication: 获取到的Bean为:User(id=123, username=austin, email=austin@gmail.com, age=26)
复制代码
从结果可以看出,明明注册到Spring
容器的是UserFactoryBeanExtension
,但是却能从容器中获取到User
类型的Bean
,User
这个Bean
就是通过UserFactoryBeanExtension
的getObject
方法返回的。
3. Spring Aware接口
🔥几种常用的Aware
接口如下:
ApplicationContextAware
能获取- Application Context`调用容器的服务
- ApplicationEventPublisherAware`应用事件发布器,可以用来发布事件
- BeanClassLoaderAware
能获取加载当前
Bean`的类加载器 - BeanFactoryAware
能获取
Bean Factory`调用容器的服务 - BeanNameAware`能获取当前Bean的名称
- EnvironmentAware`能获取当前容器的环境属性信息
- MessageSourceAware`能获取国际化文本信息
- ResourceLoaderAware`获取资源加载器读取资源文件
- ServletConfigAware`能获取到
- ServletConfigServletContextAware
能获取到
ServletContext`
Spring
为了扩展性,不仅提供了容器刷新时会处理的后置处理器,而且提供了运行时感知的Aware来提供访问容器资源的接口。让开发者根据需求访问容器资源。
如果Spring
检测到一个bean
实现了Aware
接口,则能在bean
中获取相应的Spring
资源;如果某个对象实现了某个Aware
接口,比如需要依赖Spring
的上下文容器(ApplicationContext)
,则可以实现ApplicationContextAware
接口。
Spring
在Bean
进行初始化(注意与实例化的区别)之前,会将依赖的ApplicationContext
对象通过调用ApplicationContextAware#setApplicationContext
方法实现注入。
4. 自定义拦截器
拦截器的本质是面向切面编程,符合横切关注点的功能都可以放在拦截器中实现,主要的使用场景包括:
- 日志记录
- 登录授权、验证
- 权限检查
- 性能检测(记录业务执行的耗时)
一般可以通过实现HandlerInterceptor
接口,重新preHandle、postHandle、afterCompletion
方法来实现,也可以通过继承HandlerInterceptorAdapter
抽象类实现,但是最新版本的Spring
该类已经废弃。
代码示例:
/**
* 自定义HandlerInterceptor扩展点,从5.3开始支持直接实现HandlerInterceptor和/或AsyncHandlerInterceptor,HandlerInterceptorAdapter被弃用
*
* @author: austin
* @since: 2023/2/14 3:58
*/
@Slf4j
@Component
public class InterceptorExtension implements HandlerInterceptor {
/**
* 预处理,controller方法执行前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("[CustomInterceptor] preHandle...");
return true;
}
/**
* 后处理方法,controller方法执行后,在success执行之前,可以通过给定的ModelAndView向视图公开其他模型对象
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("[CustomInterceptor] postHandle...");
}
/**
* 方法执行完成后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("[CustomInterceptor] afterCompletion...");
}
}
复制代码
5. 自定义工具获取Spring容器对象
举个例子🌰:SpringContextUtils
通过实现ApplicationContextAware
类,实现根据名称getBean()
获取IOC
的Bean
对象,或者根据类Class
去获取Bean
对象。
@Component
public class SpringContextUtils implements ApplicationContextAware {
private static ConfigurableApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
applicationContext = (ConfigurableApplicationContext) context;
}
/**
* 获取ApplicationContext对象
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 根据bean的名称获取bean
*/
public static Object getBean(String name) {
return applicationContext.getBean(name);
}
/**
* 根据bean的class来查找对象
*/
public static <T> T getBeanByClass(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
/**
* 根据bean的class来查找所有的对象(包括子类)
*/
public static <T> Map<String, T> getBeansByClass(Class<T> c) {
return applicationContext.getBeansOfType(c);
}
/**
* 获取HttpServletRequest
*/
public static HttpServletRequest getRequest() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attributes.getRequest();
}
/**
* 获取HttpSession
*/
public static HttpSession getSession() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attributes.getRequest().getSession();
}
}
复制代码
6. 初始化方法
Spring
中使用比较多的初始化bean
的方法有:
- 使用@PostConstruct注解
- 实现InitializingBean接口
@PostConstruct
源码所在位置:javax.annotation.PostConstruct
初始化方法应该算是Spring的一个扩展点之一,@PostConstruct
注解的方法在项目启动的时候执行这个方法,也可以理解为在Spring
容器启动的时候执行,可作为一些数据的常规化加载,比如:数据预热、字段数据加载、区域信息初始化 等等,被@PostConstruct
修饰的方法会在服务器加载Servlet
的时候运行,并且只会被服务器执行一次。
@PostConstruct
注解的方法将会在依赖注入完成后被自动调用。对应的执行顺序是:Construcutor > @Autowired > @PostConstruct。
举个示例🌰:
@Component
public class DataRreHeating {
@PostConstruct
public void customerFieldInit() {
// 1.初始化区域信息
initRegionData();
// 2.加载字典数据
loadDictionaryInfo();
}
private void loadDictionaryInfo() {
// TODO do something
}
private void initRegionData() {
// TODO do something
}
}
复制代码
initializingBean接口实现初始化
通过实现InitializingBean
接口,继而实现afterPropertiesSet
的方法,项目启动的时候会执行afterPropertiesSet
方法,从而实现数据的初始加载,实际上它跟@PostConstruct
功能非常差不多。
举个例子🌰:
@Slf4j
@Service
public class AirportDataService implements InitializingBean {
// 根据不同的语言来初始化机场信息
@Override
public void afterPropertiesSet() throws Exception {
CompletableFuture.runAsync(() -> {
String[] langs = new String[]{"en","es","pt","fr-ca"};
for(String lang : langs) {
try {
final String key = String.format(lockFormat, lang);
if(!lockService.grabLock(key, 30, TimeUnit.SECONDS)){
continue;
}
doInit(lang);
lockService.releaseLock(key);
} catch (Exception e){
log.error("[{}, {}] Init airport data error", , lang, e);
}
}
});
}
}
复制代码
7. 当工程启动时
有时候我们需要在项目启动的时候自定义一些额外的功能,比如加载一些系统参数,完成初始化,预热本地缓存等。 我们应该做什么?
SpringBoot 提供了:
CommandLineRunner
ApplicationRunner
简单的以ApplicationRunner
接口为🌰:
@Slf4j
@Component
public class ApplicationInit implements ApplicationRunner {
@Resource
private IAreaService iAreaService;
@Override
public void run(ApplicationArguments args) throws Exception {
iAreaService.initArea();
}
}
复制代码
只需要实现ApplicationRunner
接口,重写run
方法,在该方法中实现你的自定义需求。如果项目中有多个类实现了ApplicationRunner
接口,如何指定它们的执行顺序?答案是使用@Order(n)
注解,n的值越小越早执行。 当然,顺序也可以通过@Priority
注解来指定。
8. 类的转换器
源码位置:org.springframework.cglib.core.Converter
如果接口中接收参数的实体对象中,有一个字段类型为Date
,但实际传递的参数是字符串类型:2023-02-14 10:20:15
,该如何处理?
Spring
提供了一个扩展点,类型转换器Type Converter
,具体分为3类:
Converter<S,T>
: 将类型 S 的对象转换为类型 T 的对象ConverterFactory<S, R>
: 将 S 类型对象转换为 R 类型或其子类对象GenericConverter
:它支持多种源和目标类型的转换,还提供了源和目标类型的上下文。 此上下文允许根据注释或属性信息执行类型转换。
Component
public class StringToDateConverter implements Converter<String, Date> {
public static final Logger logger = LoggerFactory.getLogger(StringToDateConverter.class);
static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 时间格式转换
*/
@Override
public Date convert(String source) {
Date date = new Date();
try {
date = simpleDateFormat.parse(source);
} catch (ParseException e) {
logger.info(source + "转换异常");
e.printStackTrace();
}
return date;
}
}
复制代码
9. 容器初始化和容器销毁
有时候,我们需要在关闭Spring
容器前,做一些额外的工作,比如:关闭资源文件等。这时可以实现DisposableBean
接口,并且重写它的destroy
方法:
@Component
public class Application implements InitializingBean, DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("DisposableBean destroy");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("InitializingBean afterPropertiesSet");
}
}
复制代码
运行项目,查看控制台打印,一开始项目启动的时候容器会初始化执行afterPropertiesSet()
方法,当我们Stop Appcation
的时候,会触发destroy
销毁的方法:
[InitializingBean] afterPropertiesSet...
2023-02-14 17:11:05.024 INFO 14048 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 36878 (http) with context path ''
2023-02-14 17:11:05.631 INFO 14048 --- [ main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
2023-02-14 17:11:05.638 INFO 14048 --- [ main] c.a.s.SpringExtendPointsApplication : Started SpringExtendPointsApplication in 2.63 seconds (JVM running for 3.345)
[DisposableBean] destroy...
复制代码
10. 事件之ApplicationListener
准确的说,这个应该不算Spring&Spring Boot
当中的一个扩展点,ApplicationListener
可以监听某个事件的event
,触发时机可以穿插在业务方法执行过程中,用户可以自定义某个业务事件。但是Spring
内部也有一些内置事件,这种事件,可以穿插在启动调用中。我们也可以利用这个特性,来自己做一些内置事件的监听器来达到和前面一些触发点大致相同的事情。
Spring
事件是基于事件/监听器编程模型,在这个模型中,有几个重要的角色,事件ApplicationEvent
,应用事件监听器ApplicationListener
,以及事件发布者ApplicationContext
。
Spring其他所有的扩展接口
🤔思考:
Spring
还有很多可扩展的接口,在本文并没有提及到,如果有兴趣的,可以自行去了解一下对于接口的特性,可以思考一下不同的接口在适合什么情况下做扩展?
总结
通过本篇文章,我总结了
Spring
在实际开发中常用的扩展点,可以发现其实Spring
的扩展能力是非常强的,正因为Spring
自身的这个优势,让Spring
拥有了强大的拓展能力,能让很多第三方应该轻松的接入Spring
,与Spring
做整合,比如:JDBC、MyBatis、Redis、RabbitMQ
等