• 动态代理与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)

  • 相关阅读:
    CentOS下网卡启动、配置等ifcfg-eth0教程(转)
    device eth0 does not seem to be present, delaying initialization(转)
    MicroPython TPYBoard v201 简易家庭气象站的实现过程
    micropython TPYBoard v201 简易的web服务器的实现过程
    使用Visual Studio Code进行MicroPython编程
    PyCharm安装MicroPython插件
    基于MicroPython:TPYBoard心率监测器
    CentOS7+CDH5.14.0安装全流程记录,图文详解全程实测-7主节点CM安装子节点Agent配置
    CentOS7+CDH5.14.0安装全流程记录,图文详解全程实测-6CM安装前环境检查
    CentOS7+CDH5.14.0安装全流程记录,图文详解全程实测-5安装JDK及安装mysql数据库
  • 原文地址:https://www.cnblogs.com/lyhero11/p/15553458.html
Copyright © 2020-2023  润新知