前言
本文主要探讨基于 DSL(domain specific language) 之上的插件设计,他们是领域的附属,为领域提供额外的服务,但领域不依赖于他们。
1. 论述
领域应当尽可能地去专注他的核心业务规则,应当尽可能地与其他辅助性的代码解耦,一些通用的功能可以耦合进框架或者设计为中间件;但还存在有一些是与核心功能无关的,且又与业务逻辑密不可分,譬如特定的监控、特定的埋点、为领域定制的稳定性保障等,把他们定义为插件再合适不过,其依赖关系如前言所述。
2. 设计方案
暂不讨论特定的插件要实现哪些特定的能力,后续系列中将逐步展开构建一个完整的 DSL 具体需要哪些插件及其实现方案,这里我想展开思考的是怎样设计一个比较通用的 DSL 插件方案。
论述中对插件的定义与 AOP 的思想相当吻合,也当首选使用 AOP 来实现,但这其中还存在一个问题,我希望插件只专注其自身职责的表达,至于哪些节点需要接入哪些插件应当在 DSL 中配置(即我期望插件与 DSL 之间只存在配置关系),而配置应当支持动态更新,因此这就导致了 AOP 的代理对象事先是不确定的,需要去动态生成。
最后落到实现上,插件这块我需要去攻克两个核心技术点:
1、怎样去更新 AOP 的代理及切点表达式?
2、怎样去更新 IOC 容器?
3. 实现方案
3.1 配置入口
若不考虑动态更新,那么入口要实现的基本功能有两个:1、按需引入,这很简单,用一个 Conditional 即可;2、加载一个表达式类型的通知器,示例如下:
@Configuration
@ConditionalOnBean(DSL.class)
public class PluginConfig {
@Bean
public AspectJExpressionPointcutAdvisor pluginAdvisor() {
AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
advisor.setExpression(DSL.getExpression());
advisor.setAdvice(new PluginAdvice());
return advisor;
}
}
public class PluginAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("do plugin_work start...");
Object resObj = invocation.proceed();
System.out.println("do plugin_work end...");
return resObj;
}
}
测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DefaultTest {
@ExtensionNode
private Engine engine;
@Test
public void test() {
DslUtils.setDslA();
engine.launch();
}
}
3.2 监听 DLS 变更
怎么监听配置的更新是所有的配置中心都需要去深入设计的(后续系列中探讨),此处暂用伪代码代替:
@Configuration
public class PluginListenerImpl implements DslListener {
@Override
public void refresh(DslContext dslContext) {
// do something...
}
}
3.3 更新切点表达式
3.1 中我们已经注入了一个表达式通知器的 Bean:AspectJExpressionPointcutAdvisor,因此仅仅更新表达式的字符串非常简单,但查看查看源码会发现起匹配作用的是他的内部对象 AspectJExpressionPointcut,而他在首次执行匹配时会构建一个 PointcutExpression 并保存起来:
private PointcutExpression obtainPointcutExpression() {
if (getExpression() == null) {
throw new IllegalStateException("Must set property 'expression' before attempting to match");
}
if (this.pointcutExpression == null) {
this.pointcutClassLoader = determinePointcutClassLoader();
this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
}
return this.pointcutExpression;
}
因此我们还需要通过反射将这个私有字段置空,让 ClassFilter 重新执行构建,示例如下:
@Configuration
public class PluginListenerImpl implements DslListener {
@Autowired
private AspectJExpressionPointcutAdvisor aspectJExpressionPointcutAdvisor;
@Override
public void refresh(DslContext dslContext) throws NoSuchFieldException, IllegalAccessException {
refreshExpression(dslContext);
// next...
}
private void refreshExpression(DslContext dslContext) throws NoSuchFieldException, IllegalAccessException {
aspectJExpressionPointcutAdvisor.setExpression(dslContext.getExpression());
AspectJExpressionPointcut pointcut = (AspectJExpressionPointcut) aspectJExpressionPointcutAdvisor.getPointcut().getClassFilter();
Field f = AspectJExpressionPointcut.class
.getDeclaredField("pointcutExpression");
f.setAccessible(true);
f.set(pointcut, null);
}
}
3.3 更新动态代理
通过翻阅源码可得出 Spring AOP 主要通过:AbstractAdvisingBeanPostProcessor 、AbstractAutoProxyCreator 这两个 processor 来实现动态代理,其对应的实例为:MethodValidationPostProcessor、AnnotationAwareAspectJAutoProxyCreator,前者用于创建代理对象,后者用于标记切面(即织入代理)。由此,若我们需要去更新动态代理,我想到的最简单的方法就是对指定的节点重新执行以下这两个 processor(原理简单,就是一点点扣源码,麻烦...),其中还有一个小问题,和 3.2 中的一致,代理结果被缓存了,清空再执行即可,示例如下:
@Autowired
private DefaultListableBeanFactory defaultListableBeanFactory;
private void refreshTypes(DslContext dslContext) throws NoSuchFieldException, IllegalAccessException {
List> refreshTypes = dslContext.getRefreshTypes();
for (Class> refreshType : refreshTypes) {
String[] beanNames = defaultListableBeanFactory.getBeanNamesForType(refreshType);
for (String beanName : beanNames) {
Object bean = defaultListableBeanFactory.getBean(beanName);
for (BeanPostProcessor processor : defaultListableBeanFactory.getBeanPostProcessors()) {
bean = getProxyBean(bean, beanName, processor);
}
}
}
}
private Object getProxyBean(Object bean, String beanName, BeanPostProcessor processor) throws NoSuchFieldException, IllegalAccessException {
if (processor instanceof MethodValidationPostProcessor
|| processor instanceof AnnotationAwareAspectJAutoProxyCreator) {
removeAdvisedBeanCache(processor, bean, beanName);
Object current = processor.postProcessAfterInitialization(bean, beanName);
return current == null ? bean : current;
}
return bean;
}
private void removeAdvisedBeanCache(BeanPostProcessor processor, Object bean, String beanName) throws NoSuchFieldException, IllegalAccessException {
if (processor instanceof AnnotationAwareAspectJAutoProxyCreator) {
AnnotationAwareAspectJAutoProxyCreator annotationAwareAspectJAutoProxyCreator = (AnnotationAwareAspectJAutoProxyCreator) processor;
Field f = AnnotationAwareAspectJAutoProxyCreator.class
.getSuperclass()
.getSuperclass()
.getSuperclass()
.getDeclaredField("advisedBeans");
f.setAccessible(true);
Map