spring中的多线程aop方法拦截

日常开发中,常用spring的aop机制来拦截方法,记点日志、执行结果、方法执行时间啥的,很是方便,比如下面这样:(以spring-boot项目为例)

一、先定义一个Aspect

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component("logAspect")
public class LogAspect {


    @Pointcut("execution(* com.cnblogs.yjmyzz..service..*(..))")
    private void logPointCut() {
    }

    @Around("logPointCut()")
    public Object doAround(ProceedingJoinPoint pjp) {
        Object result = null;
        StringBuilder sb = new StringBuilder();
        long start = 0;
        try {
            //记录线程id、方法签名
            sb.append("thread:" + Thread.currentThread().getId() + ", method:" + pjp.getSignature() + ",");
            //记录参数
            if (pjp.getArgs() != null) {
                sb.append("args:");
                for (int i = 0; i < pjp.getArgs().length; i++) {
                    sb.append("[" + i + "]" + pjp.getArgs()[i] + ",");
                }
            }
            start = System.currentTimeMillis();
            result = pjp.proceed();
            //记录返回结果
            sb.append("result:" + result);
        } catch (Throwable e) {
            sb.append(",error:" + e.getMessage());
            throw e;
        } finally {
            long elapsedTime = System.currentTimeMillis() - start;
            //记录执行时间
            sb.append(",elapsedTime:" + elapsedTime + "ms");
            System.out.println(sb.toString());
            return result;
        }
    }

}

二、定义一个service

import org.springframework.stereotype.Service;

@Service("sampleService")
public class SampleService {

    public String hello(String name) {
        return "你好," + name;
    }

}

三、跑一把

@SpringBootApplication
@EnableAspectJAutoProxy
@ComponentScan(basePackages = {"com.cnblogs.yjmyzz"})
public class AopThreadApplication {

    public static void main(String[] args) throws InterruptedException {
        ApplicationContext context = SpringApplication.run(AopThreadApplication.class, args);
        SampleService sampleService = context.getBean(SampleService.class);

        System.out.println("main thread:" + Thread.currentThread().getId());

        System.out.println(sampleService.hello("菩提树下的杨过"));
        System.out.println();

    }
}

输出:

main thread:1
thread:1, method:String com.cnblogs.yjmyzz.aop.thread.service.SampleService.hello(String),args:[0]菩提树下的杨过,result:你好,菩提树下的杨过,elapsedTime:6ms
你好,菩提树下的杨过

第2行即aop拦截后输出的内容。但有些时候,我们会使用多线程来调用服务,这时候aop还能不能拦到呢?

四、多线程

4.1 场景1:Runnable中传入了Spring上下文

public class RunnableA implements Runnable {

    private ApplicationContext context;

    public RunnableA(ApplicationContext context) {
        this.context = context;
    }

    @Override
    public void run() {
        SampleService sampleService = context.getBean(SampleService.class);
        System.out.println("thread:" + Thread.currentThread().getId() + "," + sampleService.hello("菩提树下的杨过-2"));
    }
}

把刚才的main方法,改成用线程池调用(即:多线程)

    public static void main(String[] args) throws InterruptedException {
        ApplicationContext context = SpringApplication.run(AopThreadApplication.class, args);

        System.out.println("main thread:" + Thread.currentThread().getId());
        System.out.println();

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(new RunnableA(context));
    }

输出如下:

main thread:1
thread:23, method:String com.cnblogs.yjmyzz.aop.thread.service.SampleService.hello(String),args:[0]菩提树下的杨过-2,result:你好,菩提树下的杨过-2,elapsedTime:4ms
thread:23,你好,菩提树下的杨过-2

很明显,仍然正常拦截到了,而且从线程id上看,确实是一个新线程。

4.2 场景2:Runnable中没传入Spring上下文

public class RunnableB implements Runnable {

    public RunnableB() {
    }

    @Override
    public void run() {
        SampleService sampleService = new SampleService();
        System.out.println("thread:" + Thread.currentThread().getId() + "," + sampleService.hello("菩提树下的杨过-2"));
    }
}

与RunnableA的区别在于,完全与spring上下文没有任何关系,服务实例是手动new出来的。

修改main方法:

    public static void main(String[] args) throws InterruptedException {
        ApplicationContext context = SpringApplication.run(AopThreadApplication.class, args);

        System.out.println("main thread:" + Thread.currentThread().getId());
        System.out.println();

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(new RunnableB());
    }

输出:

main thread:1
thread:22,你好,菩提树下的杨过-2

全都是手动new出来的对象,与spring没半毛钱关系,aop不起作用也符合预期。这种情况下该怎么破?

轮到CGLib出场了,其实spring的aop机制,跟它就有密切关系,大致原理:CGLib会从被代理的类,派生出一个子类,然后在子类中覆写所有非final的public方法,从而达到"方法增强"的效果。为此,我们需要写一个代理类:

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.apache.commons.lang3.ArrayUtils;

import java.lang.reflect.Method;

public class AopProxy implements MethodInterceptor {

    private final static int MAX_LEVEL = 3;
    private final static String DOT = ".";

    public static String getMethodName(Method method) {
        if (method == null) {
            return null;
        }
        String[] arr = method.toString().split(" ");
        String methodName = arr[2].split("\\(")[0] + "()";
        String[] arr2 = methodName.split("\\.");
        if (arr2.length > MAX_LEVEL) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < arr2.length; i++) {
                if (i <= MAX_LEVEL) {
                    sb.append(arr2[i].substring(0, 1) + DOT);
                } else {
                    sb.append(arr2[i] + DOT);
                }
            }
            String temp = sb.toString();
            if (temp.endsWith(DOT)) {
                temp = temp.substring(0, temp.length() - 1);
            }
            return temp;
        }
        return methodName;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        StringBuilder sb = new StringBuilder();
        Object result = null;
        long start = System.currentTimeMillis();
        boolean hasError = false;
        try {
            sb.append("thread[" + Thread.currentThread().getId() + "] " + getMethodName(method) + " =>args:");
            if (ArrayUtils.isNotEmpty(objects)) {
                for (int i = 0; i < objects.length; i++) {
                    sb.append("[" + i + "]" + objects[i].toString() + ",");
                }
            } else {
                sb.append("null,");
            }
            result = methodProxy.invokeSuper(o, objects);
            sb.append(" result:" + result);
        } catch (Exception e) {
            sb.append(", error:" + e.getMessage());
            hasError = true;
        } finally {
            long execTime = System.currentTimeMillis() - start;
            sb.append(", execTime:" + execTime + " ms");
        }
        System.out.println(sb.toString());
        return result;
    }
}

关键点都在intercept方法里,被代理的类有方法调用时,在intercept中处理拦截逻辑,为了方便使用这个代理类,再写一个小工具:

import net.sf.cglib.proxy.Enhancer;

public class ProxyUtils {

    /**
     * 创建代理对象实例
     *
     * @param type
     * @param <T>
     * @return
     */
    public static <T> T createProxyObject(Class<T> type) {
        AopProxy factory = new AopProxy();
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(type);
        enhancer.setCallback(factory);
        //注意:被代理的类,必须有默认无参的空构造函数
        T instance = (T) enhancer.create();
        return instance;
    }
}

有了它就好办了:

public class RunnableB implements Runnable {

    public RunnableB() {
    }

    @Override
    public void run() {
        //注:这里改成用CGLib来创建目标的代理类实例
        SampleService sampleService = ProxyUtils.createProxyObject(SampleService.class);
        System.out.println("thread:" + Thread.currentThread().getId() + "," + sampleService.hello("菩提树下的杨过-2"));
    }
}

手动new的地方,改成用ProxyUtils生成代理类实例,还是跑刚才的main方法:

main thread:1
thread[24] c.c.y.a.thread.service.SampleService.hello() =>args:[0]菩提树下的杨过-2, result:你好,菩提树下的杨过-2, execTime:9 ms
thread:24,你好,菩提树下的杨过-2

第2行的输出,便是AopProxy类拦截的输出,成功拦截,皆大欢喜! 

注意事项:

1. 被代理的类,不能是内部类(即嵌套在类中的类),更不能是final类

2.要拦截的方法,不能是private方法或final方法

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一枝花算不算浪漫

[Java Collection]List分组之简单应用.

34350
来自专栏钟绍威的专栏

SpringMVC类型转换器、属性编辑器PropertiesEditor源码分析CustomDateEditor源码分析TypeConverterDelegate源码分析

对于MVC框架,参数绑定一直觉得是很神奇很方便的一个东西,在参数绑定的过程中利用了属性编辑器、类型转换器 参数绑定流程 参数绑定:把请求中的数据,转化成指定类型...

22280
来自专栏我是攻城师

Apache Pig学习笔记之内置函数(三)

45140
来自专栏iOS技术杂谈

iOS runtime探究(四): 从runtiem开始实践Category添加属性与黑魔法method swizzling你要知道的runtime都在这里

你要知道的runtime都在这里 转载请注明出处 https://cloud.tencent.com/developer/user/1605429 本文主要讲解...

34260
来自专栏技术专栏

彻底搞懂jdk动态代理并自己动手写一个动态代理

我们都知道牛逼轰轰的Spring AOP的实现的一种方式是使用JDK的动态代理(另一种是cglib,后面会介绍),大部分人也会用jdk的动态代理,不过没有研究过...

26020
来自专栏微信公众号:Java团长

关于Java中枚举Enum的深入剖析

在编程语言中我们,都会接触到枚举类型,通常我们进行有穷的列举来实现一些限定。Java也不例外。Java中的枚举类型为Enum,本文将对枚举进行一些比较深入的剖析...

23130
来自专栏开发与安全

《linux c 编程一站式学习》课后部分习题解答

1、假设变量x和n是两个正整数,我们知道x/n这个表达式的结果要取Floor,例如x是17,n是4,则结果是4。如果希望结果取Ceiling应该怎么写表达式呢?...

46660
来自专栏HTML5学堂

2015.12.03 HTML5真题练习

HTML5学堂:每天一道题,强壮程序员!今日主要涉及昨日题目的解答,以及一道涉及计时器、时间对象的题目。 HTML5真题【2015.12.02】答案解析 昨日真...

33450
来自专栏everhad

android输入限制

前言2 使用EditText让用户输入文字时,需要对输入验证。除过验证是否有效的逻辑不同,EditText的基本交互是一样的: 考虑到可能的copy,past...

20260
来自专栏前端儿

JS 的 call apply bind 方法

js的call apply bind 方法都很常见,目的都是为了改变某个方法的执行环境(context)

14730

扫码关注云+社区

领取腾讯云代金券