springboot动态添加aop切面

需求:在不停止服务的情况下,通过上传一个jar包然后捕获某方法的异常进行处理

思路:

使用springaop实现

  • 定义一个切入点为service包下面的所以方法

  • 将jar文件加载到classLoader

  • 动态添加切入点到指定的方法

至于为什么要定义一个切入点到service包下面的所以方法,感兴趣的可以研究一下springAop的源码,里面有个postProcessBeforeInstantiation方法,会返回代理对象,如果没有则不会返回代理对象。
当然还有一种思路,就是在动态添加切入点的时候把spring容器中的对象替换成自己的代理对象(没有实验过,在非单例模式的时候有问题,这里不深入研究)。

引入aop的starter:


<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

第一步:


@Aspect
@Configuration
public class TestAop {
    /*
     * 定义一个大的切入点
     */
    @Pointcut("execution(* com.cdinit.spring.demo..*(..))")
    public void initAllAop(){}
    @Before("initAllAop()")
    public void initAllAop1(){
    }
}

第二步:


//核心逻辑 实例化jar包里面的类
private Advice buildAdvice(PluginConfig config) throws Exception {

        if (adviceCache.containsKey(config.getClassName())) {
            return adviceCache.get(config.getClassName());
        }

        File jarFile = new File(config.getLocalUrl());

        // 将本地jar 文件加载至 classLoader
        URLClassLoader loader = (URLClassLoader) getClass().getClassLoader();
        URL targetUrl = jarFile.toURI().toURL();
        boolean isLoader = false;
        for (URL url : loader.getURLs()) {
            if (url.equals(targetUrl)) {
                isLoader = true;
                break;
            }
        }
        if (!isLoader) {
            Method add = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class });
            add.setAccessible(true);
            add.invoke(loader, targetUrl);
        }
        // Advice 实例化
        Class<?> adviceClass = loader.loadClass(config.getClassName()); //上传的jar文件的类
        if (!Advice.class.isAssignableFrom(adviceClass)) {
            throw new RuntimeException("无法实例化非" + Advice.class.getName() + "的实例");
        }
        adviceCache.put(adviceClass.getName(), (Advice) adviceClass.newInstance());
        return adviceCache.get(adviceClass.getName());
    }

@Service
public class AopPluginTest implements ApplicationContextAware, InitializingBean {
//核心逻辑 根据切入点动态切入
    private ApplicationContext applicationContext; // 应用上下文
    private Map<String, Advice> adviceCache = new HashMap<>();

    private PluginConfig pluginConfig = new PluginConfig()
            .setId("1")
            .setName("test")
            .setClassName("CountingBeforeAdvice")
            .setLocalUrl("E:\\aop-fix-zero\\target\\aop-fix-zero-1.0-SNAPSHOT.jar")
            .setActive("true")
//            .setExp("execution(* *.test(..))") // 加入切入点到切面
            .setExp("execution(* test(..))")
            .setVersion("1.0");

    public void activePlugin(String pluginId) {

        PluginConfig config = pluginConfig; // TODO 这里应该从数据库里面查询config的配置

        for (String name : applicationContext.getBeanDefinitionNames()) {
            Object bean = applicationContext.getBean(name);
            if (bean == this)
                continue;
            if (!(bean instanceof Advised)) // 如果bean不是Advised类型则跳过
                continue;
            if (findAdvice(config.getClassName(), (Advised) bean) != null) // 如果bean已经注册了Advised则跳过
                continue;

            Advice advice = null;
            try {
                advice = buildAdvice(config); //初始化 Plugin Advice 实例化
                // 包一层 advisor
                AspectJExpressionPointcutAdvisor advisor = new AspectJExpressionPointcutAdvisor();
                advisor.setExpression(config.getExp());
                advisor.setAdvice(advice);
                ((Advised) bean).addAdvisor(advisor);
            } catch (Exception e) {
                throw new RuntimeException("安装失败", e);
            }
        }
    }

    private Advice findAdvice(String className, Advised advised) {
        for (Advisor a : advised.getAdvisors()) {
            if (a.getAdvice().getClass().getName().equals(className)) {
                return a.getAdvice();
            }
        }
        return null;
    }
}

jar包怎么写?只需要实现对应的切面方法就行了


public class ServerLogPlugin implements MethodBeforeAdvice {

    public void before(Method method, Object[] args, Object target) throws Throwable {
        String result = String.format("%s.%s() 参数:%s", method.getDeclaringClass().getName(), method.getName(),
                Arrays.toString(args));
        System.out.println(result);
    }

}


通常有方法前拦截,方法后拦截,以及异常拦截。通过在这些拦截中编写自己的业务处理,可以达到特定的需求。

  • MethodBeforeAdvice

  • MethodBeforeAdvice

  • ThrowsAdvice

execution表达式


匹配所有类public方法  execution(public * *(..))
匹配指定包下所有类方法 execution(* com.baidu.dao.*(..)) 不包含子包
execution(* com.baidu.dao..*(..))  ..*表示包、子孙包下所有类
匹配指定类所有方法 execution(* com.baidu.service.UserService.*(..))
匹配实现特定接口所有类方法
    execution(* com.baidu.dao.GenericDAO+.*(..))
匹配所有save开头的方法 execution(* save*(..))

20200401:添加注入applicationContext到jar里面

public Advice buildAdvice(PluginConfig config) throws Exception {

        if (adviceCache.containsKey(config.getClassName())) {
            return adviceCache.get(config.getClassName());
        }

        File jarFile = new File(config.getLocalUrl());

        // 将本地jar 文件加载至 classLoader
        URLClassLoader loader = (URLClassLoader) getClass().getClassLoader();
        URL targetUrl = jarFile.toURI().toURL();
        boolean isLoader = false;
        for (URL url : loader.getURLs()) {
            if (url.equals(targetUrl)) {
                isLoader = true;
                break;
            }
        }
        if (!isLoader) {
            Method add = URLClassLoader.class.getDeclaredMethod("addURL", new Class[] { URL.class });
            add.setAccessible(true);
            add.invoke(loader, targetUrl);
        }
        // 初始化 Plugin Advice 实例化
        Class<?> adviceClass = loader.loadClass(config.getClassName());
        if (!Advice.class.isAssignableFrom(adviceClass)) {
            throw new RuntimeException(
                    String.format("plugin 配置错误 %s非 %s的实现类 ", config.getClassName(), Advice.class.getName()));
        }

//        Advice advice = (Advice) adviceClass.newInstance();

        DefaultListableBeanFactory factory = (DefaultListableBeanFactory)applicationContext.getAutowireCapableBeanFactory();
        // 通过BeanDefinitionBuilder创建bean定义
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(adviceClass);
        beanDefinitionBuilder.addPropertyValue("applicationContext",applicationContext);
        // 注册bean
        factory.registerBeanDefinition("adviceClass", beanDefinitionBuilder.getRawBeanDefinition());
        Advice bean = (Advice) this.applicationContext.getBean("adviceClass");
        adviceCache.put(adviceClass.getName(), (Advice)bean);
        return adviceCache.get(adviceClass.getName());
    }

https://github.com/cdInit/aopHotPlugin

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351