迁移到SpringBoot 01 - 插件加载

首先简单说一下云平台的业务功能:云平台上提供了多种多样的服务,这些服务可能来源于不同的供应商。也就是说对于某一种服务来说,可能会有多个供应商提供相同的服务;而对于某一个供应商来说,它可能提供多种服务。

之前的云平台中有一个插件机制,使用插件机制解决了供应商与系统的耦合度问题。系统抽象了服务层,服务层在合适的时机加载插件以调用供应商的服务。

加载相关的代码大概类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
...
ClassLoader localClassLoader = Thread.currentThread().getContextClassLoader();
for (String str : providerClassNames) {
try {
Class<?> localClass = localClassLoader.loadClass(str);
XXXServiceProvider localProvider = (XXXServiceProvider) localClass.newInstance();
localProvider.initial();// 初始化插件
} catch (Exception e) {
...
}
}
...

providerClassNames是一个数组,其中并所有插件的主类名字。通过获取到的ClassLoader,对插件主类进行加载,从而获得插件对象。

但是上面这个加载机制没有解决Spring诸如的问题,使得插件代码中无法通过@Value这种形式注入配置,所以每个插件中必须自行读取配置文件,类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void initProperties() {
InputStream inputStream = getClass().getResourceAsStream("/config.properties");
try {
Properties prop = new Properties();
prop.load(inputStream);
reqUrl = prop.getProperty("xxx.reqUrl").trim();
userId = prop.getProperty("xxx.userId").trim();
...
} catch (IOException e) {
logger.error("init haoduo auth provider error! ", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
logger.warn("close config.properties inputStream error! ", e);
}
}
}
}

这种方式用起来还是挺繁琐的,所以很希望改造成能够和普通Spring类一样使用注入方式。

1. 改造方案

要实现上述目标,合理的方案是将插件定义成Spring的Bean。但是考虑到插件的与系统的耦合度问题,所以也不太考虑直接将插件定义成Bean,写在Spring的配置文件中。那剩余的解决方案只能是将插件动态定义成Bean。通过在网上搜索解决方案,找到了两种方案。

1.1 BeanFactoryPostProcessor

最初找到的是BeanFactoryPostProcessor方案:BeanFactoryPostProcessor在BeanFactory创建成功、初始化之后、Bean真正创建之前执行。一般通过该回调函数中修改Bean定义或者定义新的Bean。

理论上讲可以在此节点创建新的Bean定义,例如根据插件列表创建对应的插件Bean。但是问题在于这个节点@Value是无效的(@Value也是由BeanFactory处理的),在这个节点Bean还没有创建,更加不可能注入配置值,因此也就无法从配置文件中拿到插件列表这一配置项信息。

方案失败。在此不做过多描述,具体可以参考网上的资料。

1.2 直接在动态加载的地方注册Bean

最后找到的方案是先动态注册Bean然后加载,代码大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
AutowireCapableBeanFactory bf = applicationContext.getAutowireCapableBeanFactory();
for (String str : this.providers) {
try {
// 定义BeanDefinition,根据providers列表动态添加Bean定义
String beanName = getSimpleClassName(str);
GenericBeanDefinition bd = new GenericBeanDefinition();
bd.setBeanClassName(str);
((DefaultListableBeanFactory)bf).registerBeanDefinition(beanName, bd);
// 使用applicationContext加载刚刚定义的Bean,此时应该完成了@Autowired的注入工作
XXXServiceProvider localProvider = (XXXServiceProvider) applicationContext.getBean(beanName);
localProvider.initial();// 初始化插件
logger.info("XXXServiceProvider {} is added to list.", localProvider.getProviderName());
} catch (Exception e) {
...
}
}

上面的代码使用applicationContext获取的autowireCapableBeanFactory,先创建Bean定义(registerBeanDefinition),然后获取该Bean(applicationContext.getBean)。这样处理之后,除了Bean定义是动态创建的意外,就和一个普通的Spring Bean表现一致了。

该方案工作正常。

附录A、参考资料

热评文章