spring:实现初始化动态bean|获取对象型数组配置文件

0. 引言

近期因为要完成实现中间件的工具包组件,其中涉及要读取对象型的数组配置文件,并且还要将其加载为bean,因为使用了spring 4.3.25.RELEASE版本,很多springboot的相关特性无法支持,因此特此记录,以方便后续同学有相同情况可以参考

1. 获取对象型数组配置文件

首先对象型数组配置文件如下所示:

minio.clients[0].name=xxx
minio.clients[0].endpoint=http://ip1:9000
minio.clients[0].access.key=admin
minio.clients[0].secret.key=asd
minio.clients[0].default.bucket=wu
minio.clients[1].name=yyy
minio.clients[1].endpoint=http://ip2:9000
minio.clients[1].access.key=admin
minio.clients[1].secret.key=asd
minio.clients[1].default.bucket=wu

转换成yml的格式如下:

minio:
  clients:
    - name: xxx
      endpoint: http://ip1:9000
      access:
        key: admin
      secret:
        key: asd
      default:
        bucket: wu
    - name: yyy
      endpoint: http://ip2:9000
        access:
          key: admin
        secret:
          key: asd
        default:
          bucket: wu

如果是springboot项目,我们直接用一个@ConfigurationProperties(prefix="minio.clients"),然后配置一个实体类就可以实现了,但这里因为遇到的是较老版本的spring项目,不支持该注解。于是尝试用其他方式实现。

@Value形式
首先来看@Value的确能够帮助我们读取到配置项,但是只能针对基础类型或者基础类型数组的配置项,对于我们对象项的数组配置文件,就不支持了,而spring中,除了这种方式,还有可以直接通过操作Environment对象来实现

Environment形式
可以看到通过environment.getProperty方法,是可以获取到我们想要的配置项的,于是这种方式明显是可行的。

同时Environment读取配置项时,要指定配置文件,于是需要借助@PropertySource来声明,同时因为这是一个工具包,也就是说配置文件可能会没有,没有则不用初始化bean,有对应配置文件再自动初始化bean,这是我们想要实现的

点开@PropertySource注解,我们可以看到一个ignoreResourceNotFound属性,从属性名已经告诉我们它的作用了,将其值设置为true,就可以实现配置文件存在时读取配置项,不存在时也不会报错

spring:实现初始化动态bean|获取对象型数组配置文件_第1张图片

完整的示例如下:

@Configuration
@PropertySource(value = {"classpath:applicaiton.properties","classpath:minio.properties"}, ignoreResourceNotFound=true)
public class MinioMultiConfiguration {
 
    @Resource
    private Environment environment;
 	
 	...   
}    

如何动态获取非固定长度数组配置项?
其次我们要观察这里的需求,因为要获取的minio.clients配置实际上是多个,这里我们只是列举了两个,因为实现的是工具包,后续可能还会配置很多个,所以长度是非预期的。

那么我们就需要获取到这个数组的长度,如果是springboot,可以直接通过List<配置实体类> list定义的集合,获取集合长度即可,但是这里spring中,无法直接获取到数组长度,于是为了满足需求,只能采取了一个笨方法,直接定义一个长度配置minio.clients.length=2,后续大家这里有更好的办法可以留言讨论

然后我们就在代码中通过for循环获取配置项即可

2. 如何将bean注册到spring容器

同时因为我这里的需求还需要初始化对应的MinioClient,那就需要将创建的bean,注册到spring容器中,而注册到容器除了@Bean注解的方式,如下所示

@Configuration
public class MinioConfiguration {
    /**
     * 对象存储服务的url
     */
    @Value("${minio.endpoint:null}")
    private String endpoint;

    /**
     * 用户ID
     */
    @Value("${minio.access.key:null}")
    private String accessKey;

    /**
     * 账户密码
     */
    @Value("${minio.secret.key:null}")
    private String secretKey;

    /**
     * 默认桶名
     */
    @Value("${minio.default.bucket:null}")
    private String defaultBucketName = "wu";

    @Bean
    public MinioClient minioClient() throws Exception{
        MinioProperties minioProperties = new MinioProperties(endpoint, accessKey, secretKey, defaultBucketName);
        if(StringUtils.isEmpty(minioProperties.getEndpoint())){
            return null;
        }
        return new MinioClient(minioProperties.getEndpoint(), minioProperties.getAccessKey(), minioProperties.getSecretKey());
    }
}

还可以通过BeanFactory来进行注册,如下所示

DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
beanFactory.registerSingleton("beanName", new MinioClient());

可以看到我们这里因为要循环读取配置项,bean的个数是不定的,所以固定使用@Bean的形式肯定行不通,只能通过beanFactory进行注册

bean的注册时机
我们知道bean肯定是要在项目启动时就注册的,但是启动时也分了很多阶段,比如我们初始化好的bean实际上是要通过@Autowired@Resource引用的,所以我们肯定需要在这两个引用之前就注册好,否则就会报错bean找不到了。

spring中项目启动时执行方法,有几种方式,比较常用的有@PostConstruct注解的方式,但这种方式的执行顺序是@Bean > @Autowired > @PostConstruct,因此它肯定是不行了

于是我们尝试另一种方式通过申明BeanFactoryPostProcessor接口,实现postProcessBeanFactory方法,这可以对beanFactory进行一些自定义的修改,而我们就可以在这些修改中将bean注册进去

同时因为要通过Environment获取配置项,于是我们还需要申明下EnvironmentAware,通过setEnvironment方法把Environment注册进来,当然你也可以选择通过beanFactory.getBean("environment")获取

完整的示例代码如下:

public class MinioMultiBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
    private final static Logger log = LogManager.getLogger(MinioMultiBeanFactoryPostProcessor.class);

    private Environment environment;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        if(!environment.containsProperty("minio.clients.length")){
            log.error("未识别到minio.clients.length,取消配置多个minioClient");
            return;
        }
        Integer length = 0;
        try {
            length = environment.getProperty("minio.clients.length", Integer.class);
        }catch (Exception e){
            throw new RuntimeException("minioClient初始化失败,minio.clients.length数据类型为Int");
        }
        for (int i = 0; length != null && i < length; i++) {
            String name = environment.getProperty("minio.clients["+i+"].name");
            String endpoint = environment.getProperty("minio.clients["+i+"].endpoint");
            String access = environment.getProperty("minio.clients["+i+"].access.key");
            String secret = environment.getProperty("minio.clients["+i+"].secret.key");
            String bucket = environment.getProperty("minio.clients["+i+"].default.bucket");
            try{
                // 自定义对象
                MinioProperties minioProperties = new MinioProperties(endpoint, access, secret, bucket);
                // 创建client
                MinioClient minioClient = new MinioClient(minioProperties.getEndpoint(), minioProperties.getAccessKey(), minioProperties.getSecretKey());
                beanFactory.registerSingleton(name+"MinioClient", minioClient);
            }catch (Exception e){
                log.error(String.format("minioClient初始化失败:%s", ExceptionUtil.getErrorInfo(e)));
            }
        }
    }
}

然后还需要结合上述说明的@Bean > @Autowired的顺序,因为我们自定义的BeanFactoryPostProcessor现在还只是个单纯的类,我们也需要将其声明为bean才能实现修改BeanFactory的目的,于是通过@Bean来初始化

@Configuration
@PropertySource(value = {"classpath:application.properties","classpath:minio.properties"}, ignoreResourceNotFound=true)
public class MinioMultiConfiguration {

    @Bean
    public MinioMultiBeanFactoryPostProcessor minioMultiBeanFactoryPostProcessor(){
        return new MinioMultiBeanFactoryPostProcessor();
    }

}

至此,我们初始化动态bean的操作就完成了,以上方式适用于任何spring项目,对需要搭建中间包的项目更加适用,大家可以选择性参考

你可能感兴趣的:(java进阶之路,技术分享,java,spring)