1、背景
我们在项目开发过程中,可能会遇到下面的场景:
(1)想在工具类中调用一个http接口请求数据,然后再调用spring容器中托管的service将请求结果保存到数据库
(2)想在工具类中使用spring容器中的环境变量(属性)信息
(3)在spring容器托管的service中只有一个方法需要调用另一个被spring容器托管的service
针对上面的场景,大伙一般会怎么处理呢?是不是也会像下面这样去处理?
/**
* 工具类
*/
@Component
public class AppUtil {
@Autowired
private OperateService operateService;
// 注入spring容器中的属性信息
@Value("${url")
private String url;
//调用外部接口并将信息入库
public static boolean operate(){
//业务。。。。
URL url = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
.....
....
//请求结果入库
operateService.save(...);
}
}
@Service
public class AppService{
@Autowired
private OperateService operateService;
// 注入spring容器中的recordService实例
@Autowired
private RecordService recordService;
//只有本方法会用到recordService
public static boolean record(){
//业务。。。。
....
recordService.record(..);
}
...
其他方法
...
}
上面的方法虽然能解决问题,但是还是有些弊端的。比如:
(1)针对前两种场景,如果在工具类中使用spring容器中托管的信息(bean实例或属性配置信息),那么就要把工具类本身也要被spring容器托管,这样才能在工具类中注入bean实例或者一些属性配置信息。如果有一个工具类还好,那如果有多个呢?而且作为一个工具类,我们使用该工具类时都是直接调用工具类中的静态方法,虽然它(工具类)被托管在了spring容器中,但是并不会有别的bean实例来调用这个工具类对应的bean实例,因此这些工具类对应的bean实例们只会占用内存资源。
(2)针对第三种场景,AppService要依赖RecordService,将RecordService作为AppService类的成员变量而存在,会让人误以为这个变量的重要性很大,虽然它在AppService中只被调用了一次。当然这个也并不是重点。但是如果AppService和RecordService互相依赖,那么这就涉及到spring bean对象的循环依赖问题。考虑到springboot官方自spring boot 2.6.x起,就默认不支持循环依赖了,如果我们再通过这种方式互相注入依赖,也就并不合适了。
其实针对上面的问题,有更合适更优雅的处理办法~
2、功能实现
我们可以利用spring自身的特性,编写一个工具类,来帮我们处理以上的场景。实现代码如下:
package com.xk.spring.util;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
/**
* spring应用工具类
* 可以获取spring容器中的bean实例以及环境变量信息
* @author xk
* @since 2023.05.10 8:34
*/
@Component
public class SpringApplicationUtil implements ApplicationContextAware, EnvironmentAware {
private static ApplicationContext applicationContext;
private static Environment environment;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringApplicationUtil.applicationContext = applicationContext;
}
@Override
public void setEnvironment(Environment environment) {
SpringApplicationUtil.environment = environment;
}
/**
* 根据bean的类型从spring容器中获取bean实例
* @param clazz bean的类型
* @param <T>
* @return 指定类型的bean实例
*/
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
/**
* 根据bean名称和bean的类型从spring容器中获取bean实例
* @param name bean的名称
* @param clazz bean的类型
* @param <T>
* @return 指定类型的bean实例
*/
public static <T> T getBean(String name, Class<T> clazz) {
return applicationContext.getBean(name, clazz);
}
/**
* 根据bean的名称从spring容器中获取bean实例
* @param beanName bean的名称
* @return
*/
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}
/**
* 判断spring容器中是否有指定bean名称的bean实例
* @param name bean的名称
* @return
*/
public static boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
/**
* 根据bean名称从spring容器中获取bean名称对应实例的类型
* @param name bean的名称
* @return
* @throws NoSuchBeanDefinitionException
*/
public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
return applicationContext.getType(name);
}
/**
* 根据配置项key的名称获取对应的配置项的值
* 比如application.properties配置文件中有 url=www.baidu.com的配置,
* 则getProperty("url")的值就是www.baidu.com
* @param key 配置项的名称,比如url
* @return
*/
public static String getProperty(String key){
return environment.getProperty(key);
}
/**
* 根据配置项key的名称获取配置项的值,并将值转换为指定的类型
* 比如application.properties配置文件中有age=18的配置项
* 因为age的值是个整数,所以我们可以直接通过本方法获取整数的age值
* getProperty("age",Integer.class)的结果就是整数18
* @param key 配置项的名称
* @param targetType 配置项对应的值的类型
* @param <T>
* @return 指定类型的配置项的值
*/
public static <T> T getProperty(String key,Class<T> targetType) {
return environment.getProperty(key,targetType);
}
}
如以上代码所述,我们用工具类实现了ApplicationContextAware,实现这个接口是方便我们操作spring容器中的bean实例。而实现EnvironmentAware接口,是为了方便获取spring容器中的环境变量信息。最后,我们在工具类上面还加了@Component注解,让我们工具类由spring容器托管。针对我们之前的描述场景,我们就可以这么使用:
/**
* 工具类
*/
public class AppUtil {
@Autowired
private OperateService operateService;
//调用外部接口并将信息入库
public static boolean operate(){
//业务。。。。
//改动的代码
String url = SpringApplicationUtil.getProperty("url");
URL url = new URL(url);
HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection();
.....
....
//请求结果入库
operateService.save(...);
}
}
@Service
public class AppService{
@Autowired
private OperateService operateService;
//只有本方法会用到recordService
public static boolean record(){
//业务。。。。
....
//改动的代码
RecordService recordService = SpringApplicationUtil.getBean(RecordService.class);
recordService.record(..);
}
...
其他方法
...
}
虽然我们将SpringApplicationUtil工具类托管到了spring容器中,但是我们只需要采用SpringApplicationUtil.getBean()的方式直接调用工具类的静态方法,来获取我们想要的数据。
3、实现原理
为啥这么写SpringApplicationUtil工具类,就能满足我们以上场景的需要了呢?这就得结合spring bean的生命周期和spring BeanPostProcessor的特性来说了。
首先我们要理解,如果我们想编码获取spring容器中的bean对象和环境配置信息,就需要拿到application对象和environment对象。我在之前写的“浅谈spring bean的生命周期”的文章中的2.3部分,提到了“初始化bean”的这一操作。在这一步有如下的描述:
(2)调用所有的BeanPostProcessor对象的postProcessBeforeInitialization方法
获取spring容器中所有的BeanPostProcessor对象,循环执行各个beanPostProcessor的postProcessBeforeInitialization方法,如果出现postProcessBeforeInitialization返回的对象为null,则终止循环,返回上一个postProcessBeforeInitialization方法返回的对象,然后进入第(3)步。
(3)调用bean的初始化方法
另外Spring容器中有一个很重要的BeanPostProcessor对象,它就是ApplicationContextAwareProcessor对象,它专门用来为那些实现了EnvironmentAware接口和ApplicationContextAware接口的bean服务,能往这些bean中注入application对象和environment对象。它的实现如下(以spring 5.3.20版本为例):
class ApplicationContextAwareProcessor implements BeanPostProcessor {
private final ConfigurableApplicationContext applicationContext;
private final StringValueResolver embeddedValueResolver;
/**
* Create a new ApplicationContextAwareProcessor for the given context.
*/
public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicationContext) {
this.applicationContext = applicationContext;
this.embeddedValueResolver = new EmbeddedValueResolver(applicationContext.getBeanFactory());
}
@Override
@Nullable
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware ||
bean instanceof ApplicationStartupAware)) {
return bean;
}
AccessControlContext acc = null;
if (System.getSecurityManager() != null) {
acc = this.applicationContext.getBeanFactory().getAccessControlContext();
}
if (acc != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareInterfaces(bean);
return null;
}, acc);
}
else {
invokeAwareInterfaces(bean);
}
return bean;
}
private void invokeAwareInterfaces(Object bean) {
if (bean instanceof EnvironmentAware) {
((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
}
if (bean instanceof EmbeddedValueResolverAware) {
((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
}
if (bean instanceof ResourceLoaderAware) {
((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
}
if (bean instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
}
if (bean instanceof MessageSourceAware) {
((MessageSourceAware) bean).setMessageSource(this.applicationContext);
}
if (bean instanceof ApplicationStartupAware) {
((ApplicationStartupAware) bean).setApplicationStartup(this.applicationContext.getApplicationStartup());
}
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}
}
}
可以看到,当初始化bean的时候,ApplicationContextAwareProcessor的postProcessBeforeInitialization方法就会被调用。
如果bean实现了EnvironmentAware接口,就会调用bean的setEnvironment方法,那么就会执行下面这行:
((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
bean对象中的Environment类型的成员变量就有值了。
而如果bean实现了ApplicationContextAware接口,就会调用bean的setApplicationContext方法,也就会执行下面这行,
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}
此时bean对象中的ApplicationContext类型的成员变量就有值了。
当然,前提是我们的bean对象(所属的类)中定义了ApplicationContext类型的成员变量和Environment类型的成员变量,而且要分别在setApplicationContext和setEnvironment方法中对它们赋值才行。另外,最重要的一点是:我们讲的是spring bean的初始化,那我们的SpringApplicationUtil工具类就要被托管(注册)到spring容器中才行,这也就是为什么在SpringApplicationUtil类上面加上@Component注解的原因,就是为了将工具类标记为spring的一个组件(记得要确保该组件能被spring识别)。当然啦,我们在配置类中使用@Bean的方式将工具类托管(注册)到spring容器中也是可以的。
4、知识补充
前面我们提到了ApplicationContextAwareProcessor这个后置处理器,spring容器初始化bean的时候,会从容器中拿到它来执行它内部的方法,那么它是什么时候被注入到spring容器中的呢?
我们再来看下spring容器启动过程中会调用到的AbstractApplicationContext.refresh方法,实现如下:
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
//省略后面的代码
....
}
}
里面会调用prepareBeanFactory方法,我们来看下它内部实现:
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// Tell the internal bean factory to use the context's class loader etc.
beanFactory.setBeanClassLoader(getClassLoader());
if (!shouldIgnoreSpel) {
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
}
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));
//就是这里,ApplicationContextAwareProcessor对象被加到了bean工厂,后面初始化bean时会使用
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
//省略后面的代码
。。。。。
}
另外,refresh方法内部还调用了一个registerBeanPostProcessors(beanFactory);方法,其实这个方法就是对容器中的各种BeanPostProcessor进行一个优先级排序。
结束语
觉得有收获的朋友,麻烦点击下“关注”,或者进行分享或收藏,多谢支持~