近期因为要完成实现中间件的工具包组件,其中涉及要读取对象型的数组配置文件,并且还要将其加载为bean,因为使用了spring 4.3.25.RELEASE
版本,很多springboot的相关特性无法支持,因此特此记录,以方便后续同学有相同情况可以参考
首先对象型数组配置文件如下所示:
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,就可以实现配置文件存在时读取配置项,不存在时也不会报错
完整的示例如下:
@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
循环获取配置项即可
同时因为我这里的需求还需要初始化对应的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项目,对需要搭建中间包的项目更加适用,大家可以选择性参考