热插拔实现之AOP


一、需求背景

最近项目(OSGI框架,目前在做与Spring的兼容)在运行过程中发现日志过大,撑爆硬盘,在随后的分析中,发现是由于之前在项目的接口性能分析中,日志埋点过多,所以在某个业务并发量过大时,出现日志过多,所以考虑是否控制仅在需要进行性能分析时,才进行耗时的打印,而平常正常业务执行时,不记录日志信息。

二、实践代码

本实践主要原理是利用Spring提供的AOP能力,实现所谓热插拔的能力,高手绕行!

1.插件工程

该工程为简单的一个maven工程,由于使用Spring的AOP切面功能,所以需要引入相关的依赖。

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.3</version>
            <scope>compile</scope>
            <optional>true</optional>
        </dependency>

功能实现是利用环绕通知:

public class MethodCostAdvise implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        long current = System.currentTimeMillis();
        Object proceed = null;
        try {
            proceed = methodInvocation.proceed();
        } catch (Throwable e) {
            throw e;
        } finally {
            String costString = String.format("[%s] invoke cost:%d ms.", methodInvocation.getMethod().getName(), System.currentTimeMillis() - current);
            System.out.println(costString);
        }
        return proceed;
    }
}

2.业务工程

业务工程中,提供了四个接口,分别是:

  1. http://localhost:8080/active/plugin/1 激活指定插件
  2. http://localhost:8080/deActive/plugin/1 去激活指定插件
  3. http://localhost:8080/dog/shut-up 模拟业务接口
  4. http://localhost:8080/list/plugin 插件明细查询

该工程是一个简单的SpringBoot的web应用,首先是插件信息加载:

plugins.list[0].id=1 编号
plugins.list[0].cls=com.zte.sdn.plugin.method.MethodCostAdvise 插件类名
plugins.list[0].name=Time Cost by Method 插件名
plugins.list[0].enable=false 插件状态
plugins.list[0].jar=method-time-cost-plugin-1.0.0-SNAPSHOT.jar 插件JAR包

@Data
@Configuration
@ConfigurationProperties(prefix = "plugins")
public class PluginConfig {
    private List<Plugin> list;
}
@Data
@ToString
public class Plugin {
    private int id;
    private String cls;
    private String name;
    private boolean enable;
    private String jar;
}

基于AOP进行热插拔进行进行,其前提是Bean是可以插拔的,即是Advise,所以需要定义切面和切入点

@Aspect
@Component
public class DogAspect {
    @Pointcut("execution(public * com.zte.sdn.dog.web.contorl..*.*(..))")
    public void as() {
    }
    @Before("as()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
    }
}

Rest实现,具体参考注释

package com.zte.sdn.dog.web.rest;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import lombok.extern.slf4j.Slf4j;
import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.zte.sdn.dog.web.config.Plugin;
import com.zte.sdn.dog.web.config.PluginConfig;
import com.zte.sdn.dog.web.contorl.DogShutUp;

@Slf4j
@RestController
public class PluginControlRest {

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private PluginConfig pluginConfig;

    @Autowired
    private DogShutUp dogShutUp;

    private String pluginDir = "file:E:/00code/";

    private Map<Integer, Advice> adviceMap = new HashMap<>();

    @GetMapping("/list/plugin")
    public String getAllPlugins() {
        return pluginConfig.getList().toString();
    }

    @GetMapping("/active/plugin/{id}")
    public boolean activePlugin(@PathVariable(value = "id") int id) {
        Optional<Plugin> pluginOpt = pluginConfig.getList().stream().filter(p -> p.getId() == id).findFirst();
        if (!pluginOpt.isPresent()) {
            throw new RuntimeException("not find [" + id + "] plugin config");
        }
        Plugin plugin = pluginOpt.get();

        //查询所有Bean实例,并判断是否已被切面过
        for (String bdn : applicationContext.getBeanDefinitionNames()) {
            Object bean = applicationContext.getBean(bdn);
            if (bean == this || !(bean instanceof Advised)) {
                continue;
            }
            //可增强 且未增加过当前指定的advice
            if (!hasAdviced((Advised) bean, plugin.getId())) {
            //借助JAR包装配并构建Advice
                Advice advice = wrapAdviseById(plugin);
                if (advice != null) {
                    ((Advised) bean).addAdvice(advice);
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }
        return true;

    }

    @GetMapping("/deActive/plugin/{id}")
    public boolean deActivePlugin(@PathVariable(value = "id") int id) {
        for (String bdn : applicationContext.getBeanDefinitionNames()) {
            Object bean = applicationContext.getBean(bdn);
            if (bean == this || !(bean instanceof Advised)) {
                continue;
            }
            if (adviceMap.containsKey(id)) {
                ((Advised) bean).removeAdvice(adviceMap.get(id));
            }
        }
        return true;
    }

    @GetMapping("/dog/shut-up")
    public String dogShutUp() {
        return dogShutUp.shutUp();
    }


    private boolean hasAdviced(Advised bean, int id) {
        Advice advice = adviceMap.get(id);
        if (advice != null) {
            for (Advisor advisor : ((Advised) bean).getAdvisors()) {
                if (advisor.getAdvice() == advice) {
                    return true;
                }
            }
        }
        return false;
    }

	//根据插件信息查找构建advice
    private Advice wrapAdviseById(Plugin plugin) {
        int id = plugin.getId();
        if (adviceMap.containsKey(id)) {
            return adviceMap.get(id);
        }
        try {
            boolean loadJar = false;
            //定位到JAR文件位置
            URL url = new URL(pluginDir + plugin.getJar());
            URLClassLoader loader = (URLClassLoader) getClass().getClassLoader();
            for (URL loaderURL : loader.getURLs()) {
                if (loaderURL.equals(url)) {
                    loadJar = true;
                    break;
                }
            }
            if (!loadJar) {
                Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
                addURL.setAccessible(true);
                addURL.invoke(loader, url);
            }
            //加载MethodInterceptor类
            Class<?> aClass = loader.loadClass(plugin.getCls());
            adviceMap.put(id, (Advice) aClass.newInstance());
            return adviceMap.get(id);
        } catch (Exception e) {
            log.error("wrap advise error.", e);
        }
        return null;
    }
}

三、演示效果

执行正常调用:
在这里插入图片描述
执行加载插件:
在这里插入图片描述
再次执行接口调用,控制台会记录并打印接口调用的时间戳
在这里插入图片描述

去激活插件后,接口调用耗时打印随即消失
在这里插入图片描述

通过上述的思路与实现,可以定义N多个支持热插拔的插件,在运行期启用关闭相关业务功能。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值