动态代理与AOP(三)

书接上文,https://www.cnblogs.com/lyhero11/p/10370750.html , 又是1年多,天性自由散漫,佛系求知,但总归是有求知意愿,所以脚步不能停。这次尝试把动态代理和AOP理解更深入些。

这次结合AOP来看看动态代理的玩法。先看看怎么开发AOP切面。再引出spring实现AOP所用的Cglib动态代理,并且对jdk动态代理和cglib动态代理进行比较。

使用@Aspect开发AOP切面

比如要在某个TestServiceImpl的eatCarrot()方法的业务逻辑around前后注入切面逻辑

@Slf4j
@Service
public class TestServiceImpl {
    public void eatCarrot(){
        log.info("吃胡萝卜"); //业务逻辑
    }

    public void eatLettuce(){
        log.info("吃生菜");
    }
}

定义切面:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class TestAdvice {

    @Pointcut("execution(* com.wangan.springbootone.aop.TestServiceImpl.eatCarrot())")
    private void eatCarrot_p(){};

    @Around("eatCarrot_p()")
    public void handleRpcResult(ProceedingJoinPoint point) throws Throwable {
        log.info("吃萝卜前洗手");
        point.proceed();	//原业务逻辑
        log.info("吃萝卜后买单");
    }
}

切面的几个概念:

  • JoinPoint 切面可以被注入的时间点,上面例子是执行eatCarrot()方法时
  • Pointcut 指定JoinPoint切面注入时的规则,通过AspectJ pointcut el表达式来描述
  • Advice 指定切面增强(织入)的方式(Before/After/After returning/After throwing/Around),以及切面具体的代码逻辑,比如上面例子的handleRpcResult方法

应用场景:统一异常捕获

考虑一个场景,如果对service层的业务逻辑方法都做统一异常捕获,应该如何用AOP来解决:

上面例子里pointcut用的筛选规则是写死指定某一个类的某一个方法,不具有通用性,虽然可以用正则指定,但更多是用annotation注解的方式:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GlobalErrorCatch {
}

复习下@Retention和@Target两个元注解:

  • @Retention是标识注解的保存级别,RetentionPolicy.SOURCE是编译的时候就么了被编译器丢弃、只在原代码里存在;CLASS是在class文件里有、但是jvm无视、默认是这个;RUNTIME是运行时也保留、可以通过反射机制读取注解信息,最常用的也是这个。
  • @Target是注解用在的地方,ElementType.TYPE是说注解是用在类上的,ElementType.FIELD是域也就是类成员变量,其他还有CONSTRUCTOR,LOCAL_VARIABLE局部变量,METHOD方法, PACKAGE等等。

然后我们就可以用上面的注解来标识pointcut了:

切面类增加pointcut和advice:

    @Pointcut("@annotation(com.wangan.springbootone.aop.GlobalErrorCatch)")
    private void globalCatch(){}

    @Around("globalCatch()")
    public Object handleRpcResult(ProceedingJoinPoint point) throws Throwable {
        try{
            return point.proceed();
        }catch (Exception e){
            log.error("切面捕获异常了,返回失败" + e.getMessage(), e);
            return "系统错误error";
        }
    }

在需要做统一异常捕获的service方法加上我们的注解:

@GlobalErrorCatch
public Object callRpcService(){
    log.info("执行rpc服务业务逻辑并返回结果");
    int i = 1/0; //模拟rpc业务逻辑出现异常
    return "success";
}

controller

@Autowired
private TestServiceImpl testService;

@RequestMapping(value = "aop", method = RequestMethod.GET)
public String aop(){
    return (String) testService.callRpcService();
}

输出

2021-11-14 18:48:20.081  INFO 15812 --- [nio-8080-exec-4] c.w.springbootone.aop.TestServiceImpl    : 执行rpc服务业务逻辑并返回结果
2021-11-14 18:48:20.085 ERROR 15812 --- [nio-8080-exec-4] com.wangan.springbootone.aop.TestAdvice  : 切面捕获异常了,返回失败/ by zero

@ControllerAdvice 、ResponseBodyAdvice 这些controller增强和response增强就是AOP的思想,之前我们封装的框架里用来做统一异常处理和统一response返回,底下的原理现在似乎可以搞清楚了。

如果我们把TestServiceImpl这个类对应的spring的bean打出来,会发现它是一个代理对象,而不是TestServiceImpl对象。

@Slf4j
@SpringBootApplication
public class SpringbootoneApplication {

    public static void main(String[] args) {

        ConfigurableApplicationContext context = SpringApplication.run(SpringbootoneApplication.class, args);
        TestServiceImpl testService = context.getBean(TestServiceImpl.class);
        log.info("start up , testServiceImpl = " + testService.getClass());
    }

}

start up , testServiceImpl = class com.wangan.springbootone.aop.TestServiceImpl$$EnhancerBySpringCGLIB$$ef173d93

这是spring用cglib生成的代理对象。再进入cglib动态代理之前,先复习一下jdk动态代理。

JDK动态代理

先复习一下JDK动态代理:

AOP面向切面的基石——动态代理(一) - 肥兔子爱豆畜子 - 博客园 (cnblogs.com)里边实现动态代理的步骤,首先定义代理类,实现InvocationHandler接口,代理类要做两个事情:

1、对原对象LancerEvolutionVI(实现Car接口)使用Proxy.newProxyInstance(classloader, Class<?>[] interfaces, InvocationHandler)方法生成代理对象。

3个参数,第1个我们是使用跟原对象一致的classloader,加载代理类的字节码到方法区作为类定义,同时在堆生成一个Class对象指向方法区的类定义、所以反射里边用的Class对象实际上就是方法区类的元数据或者说类定义的入口。第2个参数是原对象所实现的接口,这个也是JDK动态代理的特点:要求被代理的对象必须实现interface,反射才能根据interface定义知道要去代理哪些方法。第3个参数就是InvocationHandler了,里边的invoke方法确保调用原对象的interface定义的方法都从这里进入。

2、Override必须实现的invoke方法

public Object invoke(Object proxy, Method method, Object[] args)

参数,proxy代理对象,method原对象的方法,args是method传入的参数。

相关代码:

import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@Slf4j
public class CarTestModelProxy implements InvocationHandler {

    private Car testCar;

    public CarTestModelProxy(Car car){
        this.testCar = car;
    }

    public Object newInstance(){
        return Proxy.newProxyInstance(testCar.getClass().getClassLoader(),
                                      testCar.getClass().getInterfaces(),
                                    this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("proxy = " + proxy.getClass());
        if(method.getName().equals("speed")){
            log.info("速度测试开始...");
            method.invoke(this.testCar, args);
            log.info("速度测试结束!");
        }else{
            log.info("扭矩测试开始...");
            method.invoke(this.testCar, args);
            log.info("扭矩测试结束!");
        }
        return null;
    }

    public static void main(String[] args){
        CarTestModelProxy proxy = new CarTestModelProxy(new LancerEvolutionVI());
        Car pcar = (Car)proxy.newInstance();
        pcar.speed();
        pcar.torque();
    }
}

输出:

20:07:59.862 [main] INFO com.wangan.springbootone.aop.CarTestModelProxy - proxy = class com.sun.proxy.$Proxy0
20:07:59.862 [main] INFO com.wangan.springbootone.aop.CarTestModelProxy - 速度测试开始...
LancerEvolutionVI speed is 280
20:07:59.862 [main] INFO com.wangan.springbootone.aop.CarTestModelProxy - 速度测试结束!
20:07:59.862 [main] INFO com.wangan.springbootone.aop.CarTestModelProxy - proxy = class com.sun.proxy.$Proxy0
20:07:59.862 [main] INFO com.wangan.springbootone.aop.CarTestModelProxy - 扭矩测试开始...
LancerEvolutionVI torque is 450
20:07:59.862 [main] INFO com.wangan.springbootone.aop.CarTestModelProxy - 扭矩测试结束!

上面提到过,JDK的动态代理是利用的反射机制,需要被代理类先实现interface才能据此获知要wrap哪些方法。

如果我们想对某一个没有实现任何接口的类做动态代理该怎么办?我们知道,除了implements接口之外,还可以extends父类,接口规范了实现类的方法,反过来子类也是可以获知父类的方法的。这就是Cglib动态代理的思路。

Cglib动态代理

Code Generation Lib,代码生成库,看名字就知道是字节码生成技术,底层是用的ASM库(开源的高效 java 字节码编辑类库)直接操作字节码实现的,性能比JDK的动态代理强。

用Cglib实现我们上面的小例子,大致思路是,Enhancer增强类,设置委托类也就是LancerEvolutionVI为其父类,然后设置一个MethodInterceptor接口实现为其方法拦截类。Enhancer.create()生成一个委托类的子类实例。这样委托类的所有非final方法执行时就都先进入覆写的子类方法里了。

import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

@Slf4j
public class CarMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        log.info("o = " + o.getClass());
        log.info("methodProxy = " + methodProxy.getClass());
        if (method.getName().equals("speed"))
            log.info("速度测试开始...");
        if (method.getName().equals("torque"))
            log.info("扭矩测试开始...");

        Object ret = methodProxy.invokeSuper(o,args);
        log.info("ret = " + ret);
        if (method.getName().equals("speed"))
            log.info("速度测试结束!");
        if (method.getName().equals("torque"))
            log.info("扭矩测试结束!");
        return ret;
    }

    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer(); //增强类
        enhancer.setSuperclass(LancerEvolutionVI.class);    //增强类实际上是原类的子类
        enhancer.setCallback(new CarMethodInterceptor());   //拦截原方法,即子类override重写父类的方法
        LancerEvolutionVI lancer = (LancerEvolutionVI) enhancer.create();   //生成代理对象、即子类对象
        log.info("lancer = " + lancer.getClass());
        lancer.speed();
        lancer.torque();
    }
}

输出:

20:45:01.105 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - lancer = class com.wangan.springbootone.aop.LancerEvolutionVI$$EnhancerByCGLIB$$9332002c
20:45:01.105 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - o = class com.wangan.springbootone.aop.LancerEvolutionVI$$EnhancerByCGLIB$$9332002c
20:45:01.105 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - methodProxy = class org.springframework.cglib.proxy.MethodProxy
20:45:01.105 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - 速度测试开始...
LancerEvolutionVI speed is 280
20:45:01.121 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - ret = null
20:45:01.121 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - 速度测试结束!
20:45:01.121 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - o = class com.wangan.springbootone.aop.LancerEvolutionVI$$EnhancerByCGLIB$$9332002c
20:45:01.121 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - methodProxy = class org.springframework.cglib.proxy.MethodProxy
20:45:01.121 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - 扭矩测试开始...
LancerEvolutionVI torque is 450
20:45:01.121 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - ret = null
20:45:01.121 [main] INFO com.wangan.springbootone.aop.CarMethodInterceptor - 扭矩测试结束!

从前面的例子和分析我们可以看出,JDK是利用反射机制对委托类的方法进行调用的,鉴于反射的效率问题,cglib采用所谓FastClass机制来实现方法调用,提升了调用效率。

好了,又扯到反射的原理了,又是JVM的底层机制。另外cglib生成的代理类的生成规则是怎样的。这些是接下来要去研究的。

钻进去一个点,会扯出来一条线,一条线上许多个点又会延展开许多不会的面。知道的越多,就会知道不知道的越多。

参考:

漫画:AOP 面试造火箭事件始末 (qq.com)

cglib源码分析(四):cglib 动态代理原理分析 - cruze_lee - 博客园 (cnblogs.com)

Spring AOP 是怎么运行的?彻底搞定这道面试必考题 - 云+社区 - 腾讯云 (tencent.com)

原文地址:https://www.cnblogs.com/lyhero11/p/15553458.html