当前位置:   article > 正文

聊聊Spring Boot 如何实现热插拔 AOP

聊聊Spring Boot 如何实现热插拔 AOP
现在有这么一个需求:就是我们日志的开与关是交给使用人员来控制的,而不是由我们开发人员固定写死的。大家都知道可以用aop来实现日志管理,但是如何动态的来实现日志管理呢?aop源码中的实现逻辑中有这么一个步骤,就是会依次扫描Advice的实现类,然后执行。我们要做的就是自定义一个advice的实现类然后,在用户想要开启日志的时候就把advice加到项目中来,关闭日志的时候就把advice剔除就行了。


59f22609805c9b2ffbf41901c17146fa.png

前置知识

13b9f3f3d36e88a48cf803eb9415b21e.png

  • Advice:

org.aopalliance.aop.Advice

“通知”,表示 Aspect 在特定的 Join point 采取的操作。包括 “around”, “before” and “after 等 Advice,大体上分为了三类:BeforeAdvice、MethodInterceptor、AfterAdvice

  • Advisor:

org.springframework.aop.Advisor

“通知者”,它持有 Advice,是 Spring AOP 的一个基础接口。它的子接口 PointcutAdvisor 是一个功能完善接口,它涵盖了绝大部分的 Advisor。

  • Advised:

org.springframework.aop.framework.Advised

AOP 代理工厂配置类接口。提供了操作和管理 Advice 和 Advisor 的能力。它的实现类 ProxyFactory 是 Spring AOP 主要用于创建 AOP 代理类的核心类。


9193e91b8c210e39600489471e9c9179.png

热插拔AOP执行核心逻辑

1b91bccc0721da6ee4d9a729865a0f03.png

6f037615a02d4e66c9ce7ab2f06c19bc.jpeg

图片


95f4c8d74dab5163cbd075b667ee69b8.png

核心实现代码

bcdee1500a4a0661d5a9d7c8e8d4d884.png

1、动态管理advice端点实现

  1. @RestControllerEndpoint(id = "proxy")
  2. @RequiredArgsConstructor
  3. public class ProxyMetaDefinitionControllerEndPoint {
  4.     private final ProxyMetaDefinitionRepository proxyMetaDefinitionRepository;
  5.     @GetMapping("listMeta")
  6.     public List<ProxyMetaDefinition> getProxyMetaDefinitions(){
  7.        return proxyMetaDefinitionRepository.getProxyMetaDefinitions();
  8.     }
  9.     @GetMapping("{id}")
  10.     public ProxyMetaDefinition getProxyMetaDefinition(@PathVariable("id") String proxyMetaDefinitionId){
  11.         return proxyMetaDefinitionRepository.getProxyMetaDefinition(proxyMetaDefinitionId);
  12.     }
  13.     @PostMapping("save")
  14.     public String save(@RequestBody ProxyMetaDefinition definition){
  15.         try {
  16.             proxyMetaDefinitionRepository.save(definition);
  17.             return "success";
  18.         } catch (Exception e) {
  19.         }
  20.         return "fail";
  21.     }
  22.     @PostMapping("delete/{id}")
  23.     public String delete(@PathVariable("id")String proxyMetaDefinitionId){
  24.         try {
  25.             proxyMetaDefinitionRepository.delete(proxyMetaDefinitionId);
  26.             return "success";
  27.         } catch (Exception e) {
  28.         }
  29.         return "fail";
  30.     }
  31. }

2、利用事件监听机制捕获安装或者卸载插件

  1. @RequiredArgsConstructor
  2. public class ProxyMetaDefinitionChangeListener {
  3.     private final AopPluginFactory aopPluginFactory;
  4.     @EventListener
  5.     public void listener(ProxyMetaDefinitionChangeEvent proxyMetaDefinitionChangeEvent){
  6.         ProxyMetaInfo proxyMetaInfo = aopPluginFactory.getProxyMetaInfo(proxyMetaDefinitionChangeEvent.getProxyMetaDefinition());
  7.         switch (proxyMetaDefinitionChangeEvent.getOperateEventEnum()){
  8.             case ADD:
  9.                 aopPluginFactory.installPlugin(proxyMetaInfo);
  10.                 break;
  11.             case DEL:
  12.                 aopPluginFactory.uninstallPlugin(proxyMetaInfo.getId());
  13.                 break;
  14.         }
  15.     }
  16. }

3、安装插件

  1. public void installPlugin(ProxyMetaInfo proxyMetaInfo){
  2.         if(StringUtils.isEmpty(proxyMetaInfo.getId())){
  3.             proxyMetaInfo.setId(proxyMetaInfo.getProxyUrl() + SPIILT + proxyMetaInfo.getProxyClassName());
  4.         }
  5.         AopUtil.registerProxy(defaultListableBeanFactory,proxyMetaInfo);
  6.     }

4、安装插件核心实现

  1. public static void registerProxy(DefaultListableBeanFactory beanFactory,ProxyMetaInfo proxyMetaInfo){
  2.         AspectJExpressionPointcutAdvisor advisor = getAspectJExpressionPointcutAdvisor(beanFactory, proxyMetaInfo);
  3.         addOrDelAdvice(beanFactory,OperateEventEnum.ADD,advisor);
  4.     }
  5.     private static AspectJExpressionPointcutAdvisor getAspectJExpressionPointcutAdvisor(DefaultListableBeanFactory beanFactory, ProxyMetaInfo proxyMetaInfo) {
  6.         BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
  7.         GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();
  8.         beanDefinition.setBeanClass(AspectJExpressionPointcutAdvisor.class);
  9.         AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
  10.         advisor.setExpression(proxyMetaInfo.getPointcut());
  11.         advisor.setAdvice(Objects.requireNonNull(getMethodInterceptor(proxyMetaInfo.getProxyUrl(), proxyMetaInfo.getProxyClassName())));
  12.         beanDefinition.setInstanceSupplier((Supplier<AspectJExpressionPointcutAdvisor>) () -> advisor);
  13.         beanFactory.registerBeanDefinition(PROXY_PLUGIN_PREFIX + proxyMetaInfo.getId(),beanDefinition);
  14.         return advisor;
  15.     }

5、卸载插件

  1. public void uninstallPlugin(String id){
  2.         String beanName = PROXY_PLUGIN_PREFIX + id;
  3.         if(defaultListableBeanFactory.containsBean(beanName)){
  4.            AopUtil.destoryProxy(defaultListableBeanFactory,id);
  5.         }else{
  6.             throw new NoSuchElementException("Plugin not found: " + id);
  7.         }
  8.     }

6、卸载插件核心实现

  1. public static void destoryProxy(DefaultListableBeanFactory beanFactory,String id){
  2.         String beanName = PROXY_PLUGIN_PREFIX + id;
  3.         if(beanFactory.containsBean(beanName)){
  4.             AspectJExpressionPointcutAdvisor advisor = beanFactory.getBean(beanName,AspectJExpressionPointcutAdvisor.class);
  5.             addOrDelAdvice(beanFactory,OperateEventEnum.DEL,advisor);
  6.             beanFactory.destroyBean(beanFactory.getBean(beanName));
  7.         }
  8.     }

7、操作advice实现

  1. public static void addOrDelAdvice(DefaultListableBeanFactory beanFactory, OperateEventEnum operateEventEnum,AspectJExpressionPointcutAdvisor advisor){
  2.         AspectJExpressionPointcut pointcut = (AspectJExpressionPointcut) advisor.getPointcut();
  3.         for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
  4.             Object bean = beanFactory.getBean(beanDefinitionName);
  5.             if(!(bean instanceof Advised)){
  6.                 if(operateEventEnum == OperateEventEnum.ADD){
  7.                     buildCandidateAdvised(beanFactory,advisor,bean,beanDefinitionName);
  8.                 }
  9.                 continue;
  10.             }
  11.             Advised advisedBean = (Advised) bean;
  12.             boolean isFindMatchAdvised = findMatchAdvised(advisedBean.getClass(),pointcut);
  13.             if(operateEventEnum == OperateEventEnum.DEL){
  14.                 if(isFindMatchAdvised){
  15.                     advisedBean.removeAdvice(advisor.getAdvice());
  16.                     log.info("########################################## Remove Advice -->【{}】 For Bean -->【{}】 SUCCESS !",advisor.getAdvice().getClass().getName(),bean.getClass().getName());
  17.                 }
  18.             }else if(operateEventEnum == OperateEventEnum.ADD){
  19.                 if(isFindMatchAdvised){
  20.                     advisedBean.addAdvice(advisor.getAdvice());
  21.                     log.info("########################################## Add Advice -->【{}】 For Bean -->【{}】 SUCCESS !",advisor.getAdvice().getClass().getName(),bean.getClass().getName());
  22.                 }
  23.             }
  24.         }
  25.     }


594df85474dccbfef8d3bd6e4e4995d6.png

热插拔AOP演示示例

71b7c64b1d5e9e39c204cf84f321c3a9.png

1、创建一个service

  1. @Service
  2. @Slf4j
  3. public class HelloService implements BeanNameAware, BeanFactoryAware {
  4.     private BeanFactory beanFactory;
  5.     private String beanName;
  6.     @SneakyThrows
  7.     public String sayHello(String message) {
  8.         Object bean = beanFactory.getBean(beanName);
  9.         log.info("============================ {} is Advised : {}",bean, bean instanceof Advised);
  10.         TimeUnit.SECONDS.sleep(new Random().nextInt(3));
  11.         log.info("============================ hello:{}",message);
  12.         return "hello:" + message;
  13.     }
  14.     @Override
  15.     public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
  16.         this.beanFactory = beanFactory;
  17.     }
  18.     @Override
  19.     public void setBeanName(String name) {
  20.         this.beanName = name;
  21.     }
  22. }

2、创建一个controller

  1. @RestController
  2. @RequestMapping("hello")
  3. @RequiredArgsConstructor
  4. public class HelloController {
  5.     private final HelloService helloService;
  6.     @GetMapping("{message}")
  7.     public String sayHello(@PathVariable("message")String message){
  8.         return helloService.sayHello(message);
  9.     }
  10. }

3、准备一个日志切面jar

eab78043217b3376cbe814daba0cff7b.jpeg

图片

切面内容为

  1. @Slf4j
  2. public class LogMethodInterceptor implements MethodInterceptor {
  3.     @Override
  4.     public Object invoke(MethodInvocation invocation) throws Throwable {
  5.         Object result;
  6.         try {
  7.             result = invocation.proceed();
  8.         } finally {
  9.            log.info(">>>>>>>>>>>>>>>>>>>>>>>>TargetClass:【{}】,method:【{}】,args:【{}】",invocation.getThis().getClass().getName(),invocation.getMethod().getName(), Arrays.toString(invocation.getArguments()));
  10.         }
  11.         return result;
  12.     }
  13. }

4、测试

场景一:未添加切面时 浏览器访问:http://localhost:8080/hello/zhangsan 观察控制台

5d563d65adc2daea88e86e5aea9ab076.jpeg

图片

场景二:通过postman动态操作代理

1、新增代理

09d3a598efe19cbe310ad133bebcfb46.jpeg观察控制台

########################################## BuildCandidateAdvised -->【com.github.lybgeek.aop.test.hello.service.HelloService】 With Advice -->【com.github.lybgeek.interceptor.LogMethodInterceptor】 SUCCESS !

此时浏览器访问:http://localhost:8080/hello/zhangsan

再次观察控制台

cb94fb5331960d0e86564003e25aaaa7.jpeg出现了切面日志信息,说明代理生效

2、删除代理

15211206b75199ccf15e8eed7e2891e8.jpeg观察控制台

########################################## Remove Advice -->【com.github.lybgeek.interceptor.LogMethodInterceptor】 For Bean -->【com.github.lybgeek.aop.test.hello.service.HelloService$$EnhancerBySpringCGLIB$$7bc75aa3】 SUCCESS !

此时浏览器访问:http://localhost:8080/hello/zhangsan

再次观察控制台

fb90e7b21b850b452a4e30a2a1b170f3.jpeg此时没有出现切面日志信息,说明代理删除成功


24c58eb91b2c38e1f4cb49092ca7b8f8.png

总结

43eb6bffbeacd34562e65408ccd958b3.png

本文实现热插拔AOP就在于对advice、advised、advisor、pointcut概念的理解,这是实现热插拔AOP的前提,其次就是对自定义classloader也需要有一定的了解,因为我们jar不一定从classpath底下加载,也有可能来源其他地方,比如远程链接啥的,最后就是把原先spring自动帮我们实现aop,我们利用相关的api,自己手动实现一遍,示例代码的api只是利用spring api其中一种实现方式,它还有多种实现方式,比如可以利用TargetSource,感兴趣的朋友,也可以自己实现一把。

至于那个代理增删改查端点contoller,是我之前看springcloud gateway的路由定位器端点源码,一直没找到机会实现一下,就把他搬来这个示例实现一把,加深一下印象!

  1. 推荐阅读:
  2. 被 GPT-4 Plus 账号价格劝退了!
  3. 世界的真实格局分析,地球人类社会底层运行原理
  4. 不是你需要中台,而是一名合格的架构师(附各大厂中台建设PPT)
  5. 企业IT技术架构规划方案
  6. 论数字化转型——转什么,如何转?
  7. 华为干部与人才发展手册(附PPT)
  8. 【中台实践】华为大数据中台架构分享.pdf
  9. 华为的数字化转型方法论
  10. 华为如何实施数字化转型(附PPT)
  11. 华为大数据解决方案(PPT)
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/86680
推荐阅读
相关标签
  

闽ICP备14008679号