解決日誌開關需求:SpringBoot 實現熱插拔 AOP

現在有這麼一個需求:就是我們日誌的開與關是交給使用人員來控制的,而不是由我們開發人員固定寫死的。

大家都知道可以用 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")
@RequiredArgsConstructor
public 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、利用事件監聽機制捕獲安裝或者卸載插件

@RequiredArgsConstructor
public 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
@Slf4j
public 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")
@RequiredArgsConstructor
public class HelloController {
    private final HelloService helloService;
    @GetMapping("{message}")
    public String sayHello(@PathVariable("message")String message){
        return helloService.sayHello(message);
    }
}

3、準備一個日誌切面 jar

切面內容爲:

@Slf4j
public 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 的路由定位器端點源碼,一直沒找到機會實現一下,就把他搬來這個示例實現一把,加深一下印象。

來源:程序員小富

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/WVSi5GpLa5W2X1Un9YNkYA