写点什么

SpringBoot 实现动态插拔的 AOP,就变得有趣多了

作者:采菊东篱下
  • 2025-01-04
    湖南
  • 本文字数:5649 字

    阅读完需:约 19 分钟

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

前置知识

  • 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 代理类的核心类。

热插拔 AOP 执行核心逻辑



核心实现代码

1、动态管理 advice 端点实现

@RestControllerEndpoint(id = "proxy")@RequiredArgsConstructorpublic class ProxyMetaDefinitionControllerEndPoint {
    private final ProxyMetaDefinitionRepository proxyMetaDefinitionRepository;

    @GetMapping("listMeta")    public List<ProxyMetaDefinition> getProxyMetaDefinitions(){       return proxyMetaDefinitionRepository.getProxyMetaDefinitions();    }
    @GetMapping("{id}")    public ProxyMetaDefinition getProxyMetaDefinition(@PathVariable("id") String proxyMetaDefinitionId){        return proxyMetaDefinitionRepository.getProxyMetaDefinition(proxyMetaDefinitionId);    }
    @PostMapping("save")    public String save(@RequestBody ProxyMetaDefinition definition){
        try {            proxyMetaDefinitionRepository.save(definition);            return "success";        } catch (Exception e) {
        }        return "fail";
    }
    @PostMapping("delete/{id}")    public String delete(@PathVariable("id")String proxyMetaDefinitionId){        try {            proxyMetaDefinitionRepository.delete(proxyMetaDefinitionId);            return "success";        } catch (Exception e) {
        }        return "fail";    }
}
复制代码

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

@RequiredArgsConstructorpublic class ProxyMetaDefinitionChangeListener {
    private final AopPluginFactory aopPluginFactory;
    @EventListener    public void listener(ProxyMetaDefinitionChangeEvent proxyMetaDefinitionChangeEvent){        ProxyMetaInfo proxyMetaInfo = aopPluginFactory.getProxyMetaInfo(proxyMetaDefinitionChangeEvent.getProxyMetaDefinition());        switch (proxyMetaDefinitionChangeEvent.getOperateEventEnum()){            case ADD:                aopPluginFactory.installPlugin(proxyMetaInfo);                break;            case DEL:                aopPluginFactory.uninstallPlugin(proxyMetaInfo.getId());                break;        }
    }}
复制代码

3、安装插件

public void installPlugin(ProxyMetaInfo proxyMetaInfo){        if(StringUtils.isEmpty(proxyMetaInfo.getId())){            proxyMetaInfo.setId(proxyMetaInfo.getProxyUrl() + SPIILT + proxyMetaInfo.getProxyClassName());        }        AopUtil.registerProxy(defaultListableBeanFactory,proxyMetaInfo);    }
复制代码

4、安装插件核心实现

public static void registerProxy(DefaultListableBeanFactory beanFactory,ProxyMetaInfo proxyMetaInfo){        AspectJExpressionPointcutAdvisor advisor = getAspectJExpressionPointcutAdvisor(beanFactory, proxyMetaInfo);        addOrDelAdvice(beanFactory,OperateEventEnum.ADD,advisor);
    }
    private static AspectJExpressionPointcutAdvisor getAspectJExpressionPointcutAdvisor(DefaultListableBeanFactory beanFactory, ProxyMetaInfo proxyMetaInfo) {        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();        GenericBeanDefinition beanDefinition = (GenericBeanDefinition) builder.getBeanDefinition();        beanDefinition.setBeanClass(AspectJExpressionPointcutAdvisor.class);        AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();        advisor.setExpression(proxyMetaInfo.getPointcut());        advisor.setAdvice(Objects.requireNonNull(getMethodInterceptor(proxyMetaInfo.getProxyUrl(), proxyMetaInfo.getProxyClassName())));        beanDefinition.setInstanceSupplier((Supplier<AspectJExpressionPointcutAdvisor>) () -> advisor);        beanFactory.registerBeanDefinition(PROXY_PLUGIN_PREFIX + proxyMetaInfo.getId(),beanDefinition);
        return advisor;    }
复制代码

5、卸载插件

public void uninstallPlugin(String id){        String beanName = PROXY_PLUGIN_PREFIX + id;        if(defaultListableBeanFactory.containsBean(beanName)){           AopUtil.destoryProxy(defaultListableBeanFactory,id);        }else{            throw new NoSuchElementException("Plugin not found: " + id);        }    }
复制代码

6、卸载插件核心实现

public static void destoryProxy(DefaultListableBeanFactory beanFactory,String id){        String beanName = PROXY_PLUGIN_PREFIX + id;        if(beanFactory.containsBean(beanName)){            AspectJExpressionPointcutAdvisor advisor = beanFactory.getBean(beanName,AspectJExpressionPointcutAdvisor.class);            addOrDelAdvice(beanFactory,OperateEventEnum.DEL,advisor);            beanFactory.destroyBean(beanFactory.getBean(beanName));        }    }
复制代码

7、操作 advice 实现

public static void addOrDelAdvice(DefaultListableBeanFactory beanFactory, OperateEventEnum operateEventEnum,AspectJExpressionPointcutAdvisor advisor){        AspectJExpressionPointcut pointcut = (AspectJExpressionPointcut) advisor.getPointcut();        for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {            Object bean = beanFactory.getBean(beanDefinitionName);            if(!(bean instanceof Advised)){                if(operateEventEnum == OperateEventEnum.ADD){                    buildCandidateAdvised(beanFactory,advisor,bean,beanDefinitionName);                }                continue;            }            Advised advisedBean = (Advised) bean;            boolean isFindMatchAdvised = findMatchAdvised(advisedBean.getClass(),pointcut);            if(operateEventEnum == OperateEventEnum.DEL){                if(isFindMatchAdvised){                    advisedBean.removeAdvice(advisor.getAdvice());                    log.info("########################################## Remove Advice -->【{}】 For Bean -->【{}】 SUCCESS !",advisor.getAdvice().getClass().getName(),bean.getClass().getName());                }            }else if(operateEventEnum == OperateEventEnum.ADD){                if(isFindMatchAdvised){                    advisedBean.addAdvice(advisor.getAdvice());                    log.info("########################################## Add Advice -->【{}】 For Bean -->【{}】 SUCCESS !",advisor.getAdvice().getClass().getName(),bean.getClass().getName());                }            }

        }    }
复制代码

热插拔 AOP 演示示例

1、创建一个 service

@Service@Slf4jpublic class HelloService implements BeanNameAware, BeanFactoryAware {    private BeanFactory beanFactory;
    private String beanName;
    @SneakyThrows    public String sayHello(String message) {        Object bean = beanFactory.getBean(beanName);        log.info("============================ {} is Advised : {}",bean, bean instanceof Advised);        TimeUnit.SECONDS.sleep(new Random().nextInt(3));        log.info("============================ hello:{}",message);
        return "hello:" + message;
    }
    @Override    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {        this.beanFactory = beanFactory;    }
    @Override    public void setBeanName(String name) {        this.beanName = name;    }}
复制代码

2、创建一个 controller

@RestController@RequestMapping("hello")@RequiredArgsConstructorpublic class HelloController {
    private final HelloService helloService;
    @GetMapping("{message}")    public String sayHello(@PathVariable("message")String message){        return helloService.sayHello(message);    }}
复制代码

3、准备一个日志切面 jar



切面内容为

@Slf4jpublic class LogMethodInterceptor implements MethodInterceptor {    @Override    public Object invoke(MethodInvocation invocation) throws Throwable {        Object result;        try {            result = invocation.proceed();        } finally {           log.info(">>>>>>>>>>>>>>>>>>>>>>>>TargetClass:【{}】,method:【{}】,args:【{}】",invocation.getThis().getClass().getName(),invocation.getMethod().getName(), Arrays.toString(invocation.getArguments()));        }
        return result;    }}
复制代码

4、测试

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



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

1、新增代理



观察控制台

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

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

再次观察控制台


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

2、删除代理



观察控制台

########################################## 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

再次观察控制台


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

总结

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

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

用户头像

还未添加个人签名 2023-02-14 加入

还未添加个人简介

评论

发布
暂无评论
SpringBoot 实现动态插拔的 AOP,就变得有趣多了_编程_采菊东篱下_InfoQ写作社区