前言

本文主要探讨基于 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<Class<?>> 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<Object, Boolean> advisedBeans = (Map<Object, Boolean>) f.get(annotationAwareAspectJAutoProxyCreator);
Object cacheKey = getCacheKey(bean.getClass(), beanName);
advisedBeans.remove(cacheKey);
}
} private Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
if (StringUtils.hasLength(beanName)) {
return (FactoryBean.class.isAssignableFrom(beanClass) ?
BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
} else {
return beanClass;
}
}

到此可以测试以下新生成的代理类:

public class PluginTest {

    @Autowired
private BEngine bEngine; @Autowired
private DslListener dslListener; @Test
public void test() throws NoSuchFieldException, IllegalAccessException {
System.out.println("--------proxy before-----------");
System.out.println("BEngine.class:" + bEngine.getClass());
bEngine.launch(); DslContext dslContext = new DslContext();
// 初始值为 execution( void com.youclk.et.car.a.AEngine.launch() ),BEngine 并未被代理
dslContext.setExpression("execution( void com.youclk.et.car.b.BEngine.launch() )");
dslContext.setRefreshTypes(Collections.singletonList(BEngine.class));
dslListener.refresh(dslContext);
} }

结果如下:

通过这种方式更新可以不用担心多次刷新代理对象产生的副作用,因为最终变化的只是代理类所匹配切面通知而已。

3.4 更新 Spring Context

开码之前我一直认为这一步是难点,刷了一遍源码后发觉这一步异常简单(看源码还是很重要...)。DefaultListableBeanFactory 其实有提供 remove 和 register 方法用于更新 Bean,但是这两步的操作我认为太重了,而且在 remove 和 register 之间用到了这个 Bean 怎么办,因此存在极大风险。且看我们上一步做了什么,从 BeanDefinition 这个维度看我们只更新了 classType,其他的都没变,因此我考虑只要更新下 BeanDefinition,并清除对应的缓存即可,示例如下:

private void refreshTypes(DslContext dslContext) throws NoSuchFieldException, IllegalAccessException {
List<Class<?>> 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);
}
refreshBeanDefinition(beanName, bean.getClass());
}
}
} private void refreshBeanDefinition(String beanName, Class<?> classType) throws NoSuchFieldException, IllegalAccessException {
RootBeanDefinition rootBeanDefinition = (RootBeanDefinition) defaultListableBeanFactory.getMergedBeanDefinition(beanName);
rootBeanDefinition.setBeanClass(classType); ScannedGenericBeanDefinition scannedGenericBeanDefinition = (ScannedGenericBeanDefinition) defaultListableBeanFactory.getBeanDefinition(beanName);
scannedGenericBeanDefinition.setBeanClass(classType); removeBeanDefinitionCache(beanName);
} private void removeBeanDefinitionCache(String beanName) throws NoSuchFieldException, IllegalAccessException {
Field factoryBeanObjectCache_f = DefaultListableBeanFactory.class
.getSuperclass()
.getSuperclass()
.getSuperclass()
.getDeclaredField("factoryBeanObjectCache");
factoryBeanObjectCache_f.setAccessible(true);
Map<String, Object> factoryBeanObjectCache = (Map<String, Object>) factoryBeanObjectCache_f.get(defaultListableBeanFactory);
factoryBeanObjectCache.remove(beanName); Field singletonObjects_f = DefaultListableBeanFactory.class
.getSuperclass()
.getSuperclass()
.getSuperclass()
.getSuperclass()
.getDeclaredField("singletonObjects");
singletonObjects_f.setAccessible(true);
Map<String, Object> singletonObjects = (Map<String, Object>) singletonObjects_f.get(defaultListableBeanFactory);
singletonObjects.remove(beanName); Field singletonFactories_f = DefaultListableBeanFactory.class
.getSuperclass()
.getSuperclass()
.getSuperclass()
.getSuperclass()
.getDeclaredField("singletonFactories");
singletonFactories_f.setAccessible(true);
Map<String, Object> singletonFactories = (Map<String, Object>) singletonFactories_f.get(defaultListableBeanFactory);
singletonFactories.remove(beanName); Field earlySingletonObjects_f = DefaultListableBeanFactory.class
.getSuperclass()
.getSuperclass()
.getSuperclass()
.getSuperclass()
.getDeclaredField("earlySingletonObjects");
earlySingletonObjects_f.setAccessible(true);
Map<String, Object> earlySingletonObjects = (Map<String, Object>) earlySingletonObjects_f.get(defaultListableBeanFactory);
earlySingletonObjects.remove(beanName);
}

测试下是否完成了我的预期:

@Autowired
private ApplicationContext applicationContext; @Test
public void testRefreshBeanDefinition() throws NoSuchFieldException, IllegalAccessException {
System.out.println("--------refresh before-----------");
System.out.println("BEngine.class:" + applicationContext.getBean(bEngine.getClass()).getClass());
refresh();
System.out.println("--------refresh after-----------");
System.out.println("BEngine.class:" + applicationContext.getBean(bEngine.getClass()).getClass());
} private void refresh() throws NoSuchFieldException, IllegalAccessException {
DslContext dslContext = new DslContext();
//初始值为 execution( void com.youclk.et.car.a.AEngine.launch() ),BEngine 并未被代理
dslContext.setExpression("execution( void com.youclk.et.car.b.BEngine.launch() )");
dslContext.setRefreshTypes(Collections.singletonList(BEngine.class));
dslListener.refresh(dslContext);
}

结果如下:

两次获取到的 classType 不同,说明更新成功。

3.5 更新 IOC 容器

这是最关键的一步,从操作数量上来看也是最重的一步,我们来回顾下,到此我们已经刷新了代理、刷新了切面通知、并将变更提交到了 Spring Context 中,我们还缺最后一步:更新目标对象所有的依赖注入。

因为我们需要将修改后的 Bean 重新注入所有依赖他的 Bean 中,这其中可能涉及到众多的修改操作,因此第一步我们要获取所有的依赖注入关系,他们维护在:AutowiredAnnotationBeanPostProcessor.injectionMetadataCache 中;由于一次提交可能涉及到多个目标对象的更新,他们之间又有存在依赖的可能性,因此第二步先把那一堆新的 bean 刷到 metadataCache,最后筛选出所有与更新相关的依赖,重新注入一遍,示例如下:

private AutowiredAnnotationBeanPostProcessor autowiredAnnotationBeanPostProcessor;

private void refreshTypes(DslContext dslContext) throws Exception {
List<Class<?>> refreshTypes = dslContext.getRefreshTypes();
HashMap<String, String> refreshBeans = new HashMap<>();
for (Class<?> refreshType : refreshTypes) {
String[] beanNames = defaultListableBeanFactory.getBeanNamesForType(refreshType);
for (String beanName : beanNames) {
Object bean = defaultListableBeanFactory.getBean(beanName);
for (BeanPostProcessor processor : defaultListableBeanFactory.getBeanPostProcessors()) {
if (processor instanceof AutowiredAnnotationBeanPostProcessor) {
autowiredAnnotationBeanPostProcessor = (AutowiredAnnotationBeanPostProcessor) processor;
continue;
}
bean = getProxyBean(bean, beanName, processor);
}
refreshBeanDefinition(beanName, bean.getClass());
refreshBeans.put(beanName, getRealName(bean.getClass().getName()));
}
}
refreshIoc(refreshBeans);
} private void refreshIoc(HashMap<String, String> refreshBeans) throws Exception {
for (String refreshBeanName : refreshBeans.keySet()) {
resetInjectionMetadataCache(refreshBeanName);
}
Set<Object> beans = getReInjectionBeans(refreshBeans);
for (Object bean : beans) {
defaultListableBeanFactory.autowireBeanProperties(bean, 0, false);
}
} private void resetInjectionMetadataCache(String refreshBeanName) {
autowiredAnnotationBeanPostProcessor.resetBeanDefinition(refreshBeanName);
autowiredAnnotationBeanPostProcessor.determineCandidateConstructors(refreshBeanName.getClass(), refreshBeanName); RootBeanDefinition rootBeanDefinition = (RootBeanDefinition) defaultListableBeanFactory.getMergedBeanDefinition(refreshBeanName);
Object bean = defaultListableBeanFactory.getBean(refreshBeanName);
autowiredAnnotationBeanPostProcessor.postProcessMergedBeanDefinition(rootBeanDefinition, bean.getClass(), refreshBeanName);
} private Set<Object> getReInjectionBeans(HashMap<String, String> refreshBeans) throws Exception {
Field injectionMetadataCache_f = AutowiredAnnotationBeanPostProcessor.class.getDeclaredField("injectionMetadataCache");
injectionMetadataCache_f.setAccessible(true);
Map<String, InjectionMetadata> factoryBeanObjectCache = (Map<String, InjectionMetadata>) injectionMetadataCache_f.get(autowiredAnnotationBeanPostProcessor); Set<Object> injectedBeanNames = new HashSet<>(); for (String beanName : factoryBeanObjectCache.keySet()) {
Collection<InjectionMetadata.InjectedElement> injectedElements = getInjectedElements(factoryBeanObjectCache.get(beanName));
if (injectedElements == null) {
continue;
}
for (InjectionMetadata.InjectedElement injectedElement : injectedElements) {
if (refreshBeans.values().contains(getRealName(getResourceType(injectedElement).getName()))) {
injectedBeanNames.add(defaultListableBeanFactory.getBean(beanName));
}
} } return injectedBeanNames;
} private Collection<InjectionMetadata.InjectedElement> getInjectedElements(InjectionMetadata injectionMetadata) throws Exception {
Field injectedElements_f = InjectionMetadata.class.getDeclaredField("injectedElements");
injectedElements_f.setAccessible(true);
Collection<InjectionMetadata.InjectedElement> injectedElements = (Collection<InjectionMetadata.InjectedElement>) injectedElements_f.get(injectionMetadata);
return injectedElements; } private Class<?> getResourceType(InjectionMetadata.InjectedElement injectedElement) throws Exception {
Method getResourceType_m = InjectionMetadata.InjectedElement.class.getDeclaredMethod("getResourceType");
getResourceType_m.setAccessible(true);
return (Class<?>) getResourceType_m.invoke(injectedElement);
} private String getRealName(String instanceName) {
int index = instanceName.indexOf("$");
if (index > 0) {
instanceName = instanceName.substring(0, index);
}
return instanceName;
}

最后再来测试一波:

@Test
public void test() throws Exception {
bEngine.launch();
refresh();
bEngine.launch();
}

正如预期效果:

结语

灵明无著,物来顺应,未来不迎,当下不杂,既过不恋~ 请关注公众号:

DSL 系列(2) - 插件的论述与实现的更多相关文章

  1. ThreeJS系列1_CinematicCameraJS插件详解

    ThreeJS系列1_CinematicCameraJS插件详解 接着上篇 ThreeJS系列1_CinematicCameraJS插件介绍 看属性的来龙去脉 看方法作用 通过调整属性查看效果 总结 ...

  2. ThreeJS系列2_effect插件集简介( 3d, vr等 )

    ThreeJS系列2_effect插件集简介( 3d, vr等 ) ThreeJS 官方案例中有一些 js库 可以替代 render 将场景中的物质变换为其他效果的物质 目录 ThreeJS系列2_e ...

  3. DSL 系列(1) - 扩展点的论述与实现

    前言 DSL 全称为 domain-specific language(领域特定语言),本系列应当会很长,其中包含些许不成熟的想法,欢迎私信指正. 1. DSL 简述 我理解的 DSL 的主要职能是 ...

  4. jenkins系列之插件配置(二)

    第一步:下面来安装nodejs插件 第二步:可以看到,Jenkins提供了丰富的插件供开发者使用,找到需要的[NodeJS Plugin],勾选后点击安装即可 我的是已经安装了 第三步: 安装完毕后, ...

  5. mybatis学习系列五--插件及类型处理器

    2 插件编写(80-81) 单个插件编写 2.1实现interceptor接口(ibatis) invocation.proceed()方法执行必须要有,否则不会无法实现拦截作用 2.2 使用@int ...

  6. 码云平台IDEA系列的插件使用

    一.IDEA插件安装 file -- setting --  Plugins -- 搜索gitee --  Search in repositories 安装后重启编译器 二.登录并拉取项目 file ...

  7. WorldWind源码剖析系列:插件列表视图类PluginListView和插件列表视图项类PluginListItem

    WorldWind中的插件类是个庞大的类,可以说从软件设计层面上统筹可扩展的插件体系的设计思想是WorldWind中的精华,值得学习和借鉴.插件体系中的所用到的类可以分为两大类,一类是插件类Plugi ...

  8. WorldWind源码剖析系列:插件类Plugin、插件信息类PluginInfo和插件编译器类PluginCompiler

    插件类Plugin是所有由插件编译器加载的插件子类的抽象父类,提供对插件的轻量级的访问控制功能. 插件信息类PluginInfo用来存储关于某个插件的信息的类,可以理解为对插件类Plugin类的进一步 ...

  9. mybatis generator插件系列--分页插件

    1.首先定义分页插件 MysqlPagePlugin.java package com.demo.mybatis.plugin; import org.mybatis.generator.api.Co ...

随机推荐

  1. java后台打开浏览器代码

    import java.awt.Desktop; import java.io.IOException; import java.net.URI; import java.net.URISyntaxE ...

  2. 基于Python3的漏洞检测工具 ( Python3 插件式框架 )

    目录 Python3 漏洞检测工具 -- lance screenshot requirements 关键代码 usage documents Any advice or sugggestions P ...

  3. Azure SQL Virtual Machine报Login failed for user 'NT Service\SqlIaaSExtension'. Reason: Could not find a login matching the name provided

    在一台位于HK的Azure SQL Virtual Machine上修改排序规则,重建系统数据库后,监控发现大量的登录失败告警生成,如下所示: DESCRIPTION:  Login failed f ...

  4. 单用户实例添加DB账号

    停止实例 net stop mssqlserver 以单用户启动实例,指定以sqlcmd连接 net start mssqlserver /m"SQLCMD" 以单用户启动实例,指 ...

  5. 自动化测试的Selenium的python版安装与使用

    Selenium是专做网页自动化测试的,即web drive,通过百度Selenium就能找到Selenium的官网 由图可见,selenium支持相当多的编程语言进行网页自动化测试,这里我们使用py ...

  6. Windows四大傻X功能——那些拖慢系统性能的罪魁祸首

    最近新装了一个PC,配置还算蛮高,i7的CPU,8G内存,2T的硬盘,于是小心翼翼地装了一个干净的正版Win7,但是发现居然开机明显卡?所以做了些研究,发现即使全新安装的正版windows,居然也有些 ...

  7. xp,windows7,windows8,windows10那个系统好用些

    Windows XP:这曾经是微软史上最好的.最受欢迎.最受好评的可以说空前绝后的系统,虽然,XP系统对电脑配置的要求很低,基本现在所有的电脑都支持安装该系统,可它太老旧了,到2014年4-5月份微软 ...

  8. LeetCode算法题-Balanced Binary Tree(Java实现)

    这是悦乐书的第167次更新,第169篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第26题(顺位题号是110).给定二叉树,判断它是否是高度平衡的.对于此问题,高度平衡二 ...

  9. March 10th, 2018 Week 10th Saturday

    All good things must come to an end. 好景无常. Love is when the other person's happiness is more importa ...

  10. February 28th, 2018 Week 9th Wednesday

    Knowledge makes humble, ignorance makes proud. 博学使人谦逊,无知使人骄傲. Humility is not equal with being passi ...