DSL 系列(2) - 插件的论述与实现
前言
本文主要探讨基于 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) - 插件的论述与实现的更多相关文章
- ThreeJS系列1_CinematicCameraJS插件详解
ThreeJS系列1_CinematicCameraJS插件详解 接着上篇 ThreeJS系列1_CinematicCameraJS插件介绍 看属性的来龙去脉 看方法作用 通过调整属性查看效果 总结 ...
- ThreeJS系列2_effect插件集简介( 3d, vr等 )
ThreeJS系列2_effect插件集简介( 3d, vr等 ) ThreeJS 官方案例中有一些 js库 可以替代 render 将场景中的物质变换为其他效果的物质 目录 ThreeJS系列2_e ...
- DSL 系列(1) - 扩展点的论述与实现
前言 DSL 全称为 domain-specific language(领域特定语言),本系列应当会很长,其中包含些许不成熟的想法,欢迎私信指正. 1. DSL 简述 我理解的 DSL 的主要职能是 ...
- jenkins系列之插件配置(二)
第一步:下面来安装nodejs插件 第二步:可以看到,Jenkins提供了丰富的插件供开发者使用,找到需要的[NodeJS Plugin],勾选后点击安装即可 我的是已经安装了 第三步: 安装完毕后, ...
- mybatis学习系列五--插件及类型处理器
2 插件编写(80-81) 单个插件编写 2.1实现interceptor接口(ibatis) invocation.proceed()方法执行必须要有,否则不会无法实现拦截作用 2.2 使用@int ...
- 码云平台IDEA系列的插件使用
一.IDEA插件安装 file -- setting -- Plugins -- 搜索gitee -- Search in repositories 安装后重启编译器 二.登录并拉取项目 file ...
- WorldWind源码剖析系列:插件列表视图类PluginListView和插件列表视图项类PluginListItem
WorldWind中的插件类是个庞大的类,可以说从软件设计层面上统筹可扩展的插件体系的设计思想是WorldWind中的精华,值得学习和借鉴.插件体系中的所用到的类可以分为两大类,一类是插件类Plugi ...
- WorldWind源码剖析系列:插件类Plugin、插件信息类PluginInfo和插件编译器类PluginCompiler
插件类Plugin是所有由插件编译器加载的插件子类的抽象父类,提供对插件的轻量级的访问控制功能. 插件信息类PluginInfo用来存储关于某个插件的信息的类,可以理解为对插件类Plugin类的进一步 ...
- mybatis generator插件系列--分页插件
1.首先定义分页插件 MysqlPagePlugin.java package com.demo.mybatis.plugin; import org.mybatis.generator.api.Co ...
随机推荐
- java后台打开浏览器代码
import java.awt.Desktop; import java.io.IOException; import java.net.URI; import java.net.URISyntaxE ...
- 基于Python3的漏洞检测工具 ( Python3 插件式框架 )
目录 Python3 漏洞检测工具 -- lance screenshot requirements 关键代码 usage documents Any advice or sugggestions P ...
- 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 ...
- 单用户实例添加DB账号
停止实例 net stop mssqlserver 以单用户启动实例,指定以sqlcmd连接 net start mssqlserver /m"SQLCMD" 以单用户启动实例,指 ...
- 自动化测试的Selenium的python版安装与使用
Selenium是专做网页自动化测试的,即web drive,通过百度Selenium就能找到Selenium的官网 由图可见,selenium支持相当多的编程语言进行网页自动化测试,这里我们使用py ...
- Windows四大傻X功能——那些拖慢系统性能的罪魁祸首
最近新装了一个PC,配置还算蛮高,i7的CPU,8G内存,2T的硬盘,于是小心翼翼地装了一个干净的正版Win7,但是发现居然开机明显卡?所以做了些研究,发现即使全新安装的正版windows,居然也有些 ...
- xp,windows7,windows8,windows10那个系统好用些
Windows XP:这曾经是微软史上最好的.最受欢迎.最受好评的可以说空前绝后的系统,虽然,XP系统对电脑配置的要求很低,基本现在所有的电脑都支持安装该系统,可它太老旧了,到2014年4-5月份微软 ...
- LeetCode算法题-Balanced Binary Tree(Java实现)
这是悦乐书的第167次更新,第169篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第26题(顺位题号是110).给定二叉树,判断它是否是高度平衡的.对于此问题,高度平衡二 ...
- 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 ...
- February 28th, 2018 Week 9th Wednesday
Knowledge makes humble, ignorance makes proud. 博学使人谦逊,无知使人骄傲. Humility is not equal with being passi ...