超实用的Spring bean工具类

您所在的位置:网站首页 帕斯卡对绘画的指责 超实用的Spring bean工具类

超实用的Spring bean工具类

#超实用的Spring bean工具类| 来源: 网络整理| 查看: 265

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       * @return 指定类型的bean实例      */     public static  T getBean(Class clazz) {         return applicationContext.getBean(clazz);     }     /**      * 根据bean名称和bean的类型从spring容器中获取bean实例      * @param name bean的名称      * @param clazz bean的类型      * @param       * @return 指定类型的bean实例      */     public static  T getBean(String name, Class 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       * @return 指定类型的配置项的值      */     public static  T getProperty(String key,Class 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) () -> {                 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进行一个优先级排序。

结束语

觉得有收获的朋友,麻烦点击下“关注”,或者进行分享或收藏,多谢支持~



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3