AOP原理解析(一)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: AOP原理解析

AOP 底层实现方式之一是代理,由代理结合通知和目标,提供增强功能

除此以外,aspectj 提供了两种另外的 AOP 底层实现:

  • 第一种是通过 ajc 编译器在编译 class 类文件时,就把通知的增强功能,织入到目标类的字节码中
  • 第二种是通过 agent 在加载目标类时,修改目标类的字节码,织入增强功能
  • 作为对比,之前学习的代理是运行时生成新的字节码

简单比较的话:

  • aspectj 在编译和加载时,修改目标字节码,性能较高
  • aspectj 因为不用代理,能突破一些技术上的限制,例如对构造、对静态方法、对 final 也能增强
  • 但 aspectj 侵入性较强,且需要学习新的 aspectj 特有语法,因此没有广泛流行

ajc 编译器

  1. 编译器也能修改 class 实现增强
  2. 编译器增强能突破代理仅能通过方法重写增强的限制:可以对构造方法、静态方法等实现增强

注意


  • 版本选择了 java 8, 因为目前的 aspectj-maven-plugin 1.14.0 最高只支持到 java 16
  • 一定要用 maven 的 compile 来编译, idea 不会调用 ajc 编译器

agent 类加载

  1. 类加载时可以通过 agent 修改 class 实现增强

AOP 实现之 proxy

jdk 动态代理

public class JdkProxyDemo {
    interface Foo {
        void foo();
    }
    static final class Target implements Foo {
        public void foo() {
            System.out.println("target foo");
        }
    }
    // jdk 只能针对接口代理
    public static void main(String[] param) throws IOException {
        // 目标对象
        Target target = new Target();
        ClassLoader loader = JdkProxyDemo.class.getClassLoader(); // 用来加载在运行期间动态生成的字节码
        Foo proxy = (Foo) Proxy.newProxyInstance(loader, new Class[]{Foo.class}, (p, method, args) -> {
            System.out.println("before...");
            // 目标.方法(参数)
            // 方法.invoke(目标, 参数);
            Object result = method.invoke(target, args);
            System.out.println("after....");
            return result; // 让代理也返回目标方法执行的结果
        });
        System.out.println(proxy.getClass());
        proxy.foo();
        System.in.read();
    }
}

运行结果

proxy before...
target foo
proxy after...

注意:jdk 动态代理要求目标必须实现接口,生成的代理类实现相同接口,因此代理与目标之间是平级兄弟关系

模拟 jdk 动态代理

public class A12 {
    interface Foo {
        void foo();
        int bar();
    }
    static class Target implements Foo {
        public void foo() {
            System.out.println("target foo");
        }
        public int bar() {
            System.out.println("target bar");
            return 100;
        }
    }
    public static void main(String[] param) {
        // ⬇️1. 创建代理,这时传入 InvocationHandler
        Foo proxy = new $Proxy0(new InvocationHandler() {    
            // ⬇️5. 进入 InvocationHandler
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
                // ⬇️6. 功能增强
                System.out.println("before...");
                // ⬇️7. 反射调用目标方法
                return method.invoke(new Target(), args);
            }
        });
        // ⬇️2. 调用代理方法
        proxy.foo();
        proxy.bar();
    }
}

模拟代理实现

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
// ⬇️这就是 jdk 代理类的源码, 秘密都在里面
public class $Proxy0 extends Proxy implements A12.Foo {
    public $Proxy0(InvocationHandler h) {
        super(h);
    }
    // ⬇️3. 进入代理方法
    public void foo() {
        try {
            // ⬇️4. 回调 InvocationHandler
            h.invoke(this, foo, new Object[0]);
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
    @Override
    public int bar() {
        try {
            Object result = h.invoke(this, bar, new Object[0]);
            return (int) result;
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
    static Method foo;
    static Method bar;
    static {
        try {
            foo = A12.Foo.class.getMethod("foo");
            bar = A12.Foo.class.getMethod("bar");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }
}

代理一点都不难,无非就是利用了多态、反射的知识

  1. 方法重写可以增强逻辑,只不过这【增强逻辑】千变万化,不能写死在代理内部
  2. 通过接口回调将【增强逻辑】置于代理类之外
  3. 配合接口方法反射(是多态调用),就可以再联动调用目标方法
  4. 会用 arthas 的 jad 工具反编译代理类

限制

由于JDK动态代理是基于接口实现的,所以它只能代理接口中的方法,而不能代理类中的成员变量、静态方法以及final方法。

成员变量是对象的属性,与方法不同,成员变量并不是接口中定义的一部分,所以不能通过代理实现。静态方法属于类而不是对象,因此即使代理了对象,也无法代理类的静态方法。final方法在编译期就已经绑定到方法调用点,因此不能被代理。

需要注意的是,虽然JDK动态代理不能代理类中的成员变量、静态方法以及final方法,但是它可以代理接口中的默认方法,因为默认方法是接口中的一种特殊方法,可以在接口中定义和实现。

如果需要代理类中的成员变量、静态方法以及final方法,可以考虑使用其他类型的代理,例如CGLIB代理或者字节码操作库ASM。

方法反射优化

  1. 前 16 次反射性能较低
  2. 第 17 次调用会生成代理类,优化为非反射调用

cglib 代理

CGLIB是第三方提供的包,所以需要引入jar包的坐标:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>

代码演示:

public class CglibProxyDemo {
    static class Target {
        public void foo() {
            System.out.println("target foo");
        }
    }
    // 代理是子类型, 目标是父类型
    public static void main(String[] param) {
        Target target = new Target();
        Target proxy = (Target) Enhancer.create(Target.class, (MethodInterceptor) (p, method, args, methodProxy) -> {
            System.out.println("before...");
//            Object result = method.invoke(target, args); // 用方法反射调用目标
            // methodProxy 它可以避免反射调用
//            Object result = methodProxy.invoke(target, args); // 内部没有用反射, 需要目标 (spring)
            Object result = methodProxy.invokeSuper(p, args); // 内部没有用反射, 需要代理
            System.out.println("after...");
            return result;
        });
        proxy.foo();
    }
}

注意:调用目标时有所改进,见下面代码片段

  1. method.invoke 是反射调用,必须调用到足够次数才会进行优化
  2. methodProxy.invoke 是不反射调用,它会正常(间接)调用目标对象的方法(Spring 采用)
  3. methodProxy.invokeSuper 也是不反射调用,它会正常(间接)调用代理对象的方法,可以省略目标对象
  • cglib 不要求目标实现接口,它生成的代理类是目标的子类,因此代理与目标之间是子父关系
  • 限制:根据上述分析 final 类无法被 cglib 增强

jdk 和 cglib 在 Spring 中的统一

Spring 中对切点、通知、切面的抽象如下

  • 切点:接口 Pointcut,典型实现 AspectJExpressionPointcut
  • 通知:典型接口为 MethodInterceptor 代表环绕通知
  • 切面:Advisor,包含一个 Advice 通知,PointcutAdvisor 包含一个 Advice 通知和一个 Pointcut

相关术语

两个切面概念
aspect =
    通知1(advice) +  切点1(pointcut)
    通知2(advice) +  切点2(pointcut)
    通知3(advice) +  切点3(pointcut)
    ...
advisor = 更细粒度的切面,包含一个通知和切点

代理相关类图

  • AopProxyFactory 根据 proxyTargetClass 等设置选择 AopProxy 实现
  • AopProxy 通过 getProxy 创建代理对象
  • 图中 Proxy 都实现了 Advised 接口,能够获得关联的切面集合与目标(其实是从 ProxyFactory 取得)
  • 调用代理方法时,会借助 ProxyFactory 将通知统一转为环绕通知:MethodInterceptor

public class ProxyTest {
    public static void main(String[] args) {
        // 1. 备好切点
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("execution(* foo())");
        // 2. 备好通知
        MethodInterceptor advice = invocation -> {
            System.out.println("before...");
            Object result = invocation.proceed(); // 调用目标
            System.out.println("after...");
            return result;
        };
        // 3. 备好切面
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
        /*
           4. 创建代理
                a. proxyTargetClass = false, 目标实现了接口, 用 jdk 实现
                b. proxyTargetClass = false,  目标没有实现接口, 用 cglib 实现
                c. proxyTargetClass = true, 总是使用 cglib 实现
         */
        Target2 target = new Target2();
        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(target);
        factory.addAdvisor(advisor);
        factory.setInterfaces(target.getClass().getInterfaces());
        factory.setProxyTargetClass(false);
        Target2 proxy = (Target2) factory.getProxy();
        System.out.println(proxy.getClass());
        proxy.foo();
        proxy.bar();
        
    }
    interface I1 {
        void foo();
        void bar();
    }
    static class Target1 implements I1 {
        public void foo() {
            System.out.println("target1 foo");
        }
        public void bar() {
            System.out.println("target1 bar");
        }
    }
    static class Target2 {
        public void foo() {
            System.out.println("target2 foo");
        }
        public void bar() {
            System.out.println("target2 bar");
        }
    }

收获:

  1. 底层的切点实现
  2. 底层的通知实现
  3. 底层的切面实现
  4. ProxyFactory 用来创建代理
  • 如果指定了接口,且 proxyTargetClass = false,使用 JdkDynamicAopProxy
  • 如果没有指定接口,或者 proxyTargetClass = true,使用 ObjenesisCglibAopProxy
  • 例外:如果目标是接口类型或已经是 Jdk 代理,使用 JdkDynamicAopProxy

注意

  • 要区分本章节提到的 MethodInterceptor,它与之前 cglib 中用的的 MethodInterceptor 是不同的接口

切点匹配

收获

a. 底层切点实现是如何匹配的: 调用了 aspectj 的匹配方法

b. 比较关键的是它实现了 MethodMatcher 接口, 用来执行方法的匹配

public class ProxyTest {
    public static void main(String[] args) throws NoSuchMethodException {
//        AspectJExpressionPointcut pt1 = new AspectJExpressionPointcut();
//        pt1.setExpression("execution(* bar())");
//        System.out.println(pt1.matches(T1.class.getMethod("foo"), T1.class));
//        System.out.println(pt1.matches(T1.class.getMethod("bar"), T1.class));
//
//        AspectJExpressionPointcut pt2 = new AspectJExpressionPointcut();
//        pt2.setExpression("@annotation(org.springframework.transaction.annotation.Transactional)");
//        System.out.println(pt2.matches(T1.class.getMethod("foo"), T1.class));
//        System.out.println(pt2.matches(T1.class.getMethod("bar"), T1.class));
        StaticMethodMatcherPointcut pt3 = new StaticMethodMatcherPointcut() {
            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                // 检查方法上是否加了 Transactional 注解
                MergedAnnotations annotations = MergedAnnotations.from(method);
                if (annotations.isPresent(Transactional.class)) {
                    return true;
                }
                // 查看类上是否加了 Transactional 注解
                annotations = MergedAnnotations.from(targetClass, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY);
                if (annotations.isPresent(Transactional.class)) {
                    return true;
                }
                return false;
            }
        };
        System.out.println(pt3.matches(T1.class.getMethod("foo"), T1.class));
        System.out.println(pt3.matches(T1.class.getMethod("bar"), T1.class));
        System.out.println(pt3.matches(T2.class.getMethod("foo"), T2.class));
        System.out.println(pt3.matches(T3.class.getMethod("foo"), T3.class));
    }
    static class T1 {
        @Transactional
        public void foo() {
        }
        public void bar() {
        }
    }
    @Transactional
    static class T2 {
        public void foo() {
        }
    }
    @Transactional
    interface I3 {
        void foo();
    }
    static class T3 implements I3 {
        public void foo() {
        }
    }
}


AOP原理解析(二)https://developer.aliyun.com/article/1469474

目录
相关文章
|
14天前
|
vr&ar
简单易懂的 全景图高清下载方法以及原理简要解析(支持下载建E、720yun、酷雷曼、景站、酷家乐、百度街景原图)
这篇文章介绍了一种简单易懂的全景图高清下载方法,使用在线网站全景管家,支持下载包括建E、720yun、酷雷曼等多个平台的全景图原图,并简要解析了全景图的原理和制作方法。
简单易懂的 全景图高清下载方法以及原理简要解析(支持下载建E、720yun、酷雷曼、景站、酷家乐、百度街景原图)
|
9天前
|
域名解析 网络协议
DNS服务工作原理
文章详细介绍了DNS服务的工作原理,包括FQDN的概念、名称解析过程、DNS域名分级策略、根服务器的作用、DNS解析流程中的递归查询和迭代查询,以及为何有时基于IP能访问而基于域名不能访问的原因。
21 2
|
11天前
|
缓存 Java 开发者
Spring高手之路22——AOP切面类的封装与解析
本篇文章深入解析了Spring AOP的工作机制,包括Advisor和TargetSource的构建与作用。通过详尽的源码分析和实际案例,帮助开发者全面理解AOP的核心技术,提升在实际项目中的应用能力。
10 0
Spring高手之路22——AOP切面类的封装与解析
|
17天前
|
JavaScript 前端开发 安全
JS 混淆解析:JS 压缩混淆原理、OB 混淆特性、OB 混淆JS、混淆突破实战
JS 混淆解析:JS 压缩混淆原理、OB 混淆特性、OB 混淆JS、混淆突破实战
25 2
|
17天前
|
缓存 前端开发 JavaScript
Webpack 模块解析:打包原理、构造形式、扣代码补参数和全局导出
Webpack 模块解析:打包原理、构造形式、扣代码补参数和全局导出
21 1
|
5天前
|
负载均衡 网络协议 安全
DNS解析中的Anycast技术:原理与优势
【9月更文挑战第7天】在互联网体系中,域名系统(DNS)将域名转换为IP地址,但网络规模的扩张使DNS解析面临高效、稳定与安全挑战。Anycast技术应运而生,通过将同一IP地址分配给多个地理分布的服务器,并依据网络状况自动选择最近且负载低的服务器响应查询请求,提升了DNS解析速度与效率,实现负载均衡,缓解DDoS攻击,增强系统高可用性。此技术利用动态路由协议如BGP实现,未来在网络发展中将扮演重要角色。
25 0
|
11天前
|
前端开发 Java UED
瞬间变身高手!JSF 与 Ajax 强强联手,打造极致用户体验的富客户端应用,让你的应用焕然一新!
【8月更文挑战第31天】JavaServer Faces (JSF) 是 Java EE 标准的一部分,常用于构建企业级 Web 应用。传统 JSF 应用采用全页面刷新方式,可能影响用户体验。通过集成 Ajax 技术,可以显著提升应用的响应速度和交互性。本文详细介绍如何在 JSF 应用中使用 Ajax 构建富客户端应用,并通过具体示例展示 Ajax 在 JSF 中的应用。首先,确保安装 JDK 和支持 Java EE 的应用服务器(如 Apache Tomcat 或 WildFly)。
23 0
|
11天前
|
Java Spring
🔥JSF 与 Spring 强强联手:打造高效、灵活的 Web 应用新标杆!💪 你还不知道吗?
【8月更文挑战第31天】JavaServer Faces(JSF)与 Spring 框架是常用的 Java Web 技术。本文介绍如何整合两者,发挥各自优势,构建高效灵活的 Web 应用。首先通过 `web.xml` 和 `ContextLoaderListener` 配置 Spring 上下文,在 `applicationContext.xml` 定义 Bean。接着使用 `@Autowired` 将 Spring 管理的 Bean 注入到 JSF 管理的 Bean 中。
25 0
|
11天前
|
监控 数据库 开发者
云端飞跃:Play Framework应用的惊心动魄部署之旅,从本地到云的华丽转身
【8月更文挑战第31天】Play Framework是一款高效Java和Scala Web应用框架,支持快速开发与灵活部署。本文详细介绍从本地环境到云平台(如Heroku和AWS Elastic Beanstalk)的部署策略,涵盖配置文件设置、依赖管理和环境变量配置等关键步骤,并提供示例代码,帮助开发者顺利完成部署。此外,还介绍了如何进行日志和性能监控,确保应用稳定运行。通过本文,开发者可充分利用云计算的优势,实现高效部署与维护。
19 0
|
11天前
|
SQL 监控 数据库
深度解析Entity Framework Core中的变更跟踪与并发控制:从原理到实践的全方位指南,助你构建稳健高效的数据访问层
【8月更文挑战第31天】本文通过问答形式深入探讨了 Entity Framework Core 中的变更跟踪与并发控制。变更跟踪帮助我们监控实体状态变化,默认适用于所有实体,但可通过特定配置关闭。并发控制确保多用户环境下数据的一致性,包括乐观和悲观两种方式。文章提供了具体代码示例,展示了如何配置和处理相关问题,帮助读者在实际项目中更高效地应用这些技术。
21 0

热门文章

最新文章

推荐镜像

更多