超实用的Spring bean工具类

908 阅读8分钟

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 ApplicationContextAwareEnvironmentAware {

    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(thisgetEnvironment()));

        //就是这里,ApplicationContextAwareProcessor对象被加到了bean工厂,后面初始化bean时会使用
        beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
        beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
        //省略后面的代码
        。。。。。
    }

另外,refresh方法内部还调用了一个registerBeanPostProcessors(beanFactory);方法,其实这个方法就是对容器中的各种BeanPostProcessor进行一个优先级排序。

结束语

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