Spring AOP


Spring AOP

基本概念

什么是 AOP ?

  • AOP 是 Aspect Oriented Programming(面向切面编程) 的简称,和 OOP(面向对象编程)一样是一种编程思想,是对 OOP 的一种补充。

  • AOP 旨在将横切关注点(crosscutting concern)从业务主体逻辑中进行剥离,实现关注点分离,以提高程序的模块化程度(及业务模块只需关注业务逻辑,无需关注日志、安全、事务等通用逻辑)。

常见术语

(1) 切面(Aspect)

切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。切面由切入点和通知组成,它既包含了横切逻辑的定义,也包括了切入点的定义。 Spring AOP 就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。

@Component 
@Aspect 
public class LogAspect { }

可以简单地认为,使用 @Aspect 注解的类就是切面。

(2) 目标对象(Target)

目标对象指将要被增强的对象,即包含主业务逻辑的类对象。或者说是被一个或者多个切面所通知的对象。

(3) 连接点(JoinPoint)

程序执行过程中明确的点,如方法的调用或特定的异常被抛出。连接点由两个信息确定:

  • 方法(表示程序执行点,即在哪个目标方法)
  • 相对点(表示方位,即目标方法的什么位置,比如调用前,后等)

简单来说,连接点就是被拦截到的程序执行点,因为 Spring 只支持方法类型的连接点,所以在 Spring 中连接点就是被拦截到的方法。

@Before("pointcut()") 
public void log(JoinPoint joinPoint) {
  //这个JoinPoint参数就是连接点 
}

(4) 切入点(PointCut)

切入点是对连接点进行拦截的条件定义。切入点表达式如何和连接点匹配是 AOP 的核心,Spring 缺省使用 AspectJ 切入点语法。 一般认为,所有的方法都可以认为是连接点,但是我们并不希望在所有的方法上都添加通知,而切入点的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述)来匹配连接点,给满足规则的连接点添加通知。

@Pointcut("execution(* com.remcarpediem.test.aop.service..*(..))") 
public void pointcut() { }

上边切入点的匹配规则是 com.remcarpediem.test.aop.service 包下的所有类的所有函数。

(5) 通知(Advice)

通知是指拦截到连接点之后要执行的代码,包括了 “around” 、“before” 和 “after” 等不同类型的通知。Spring AOP 框架以拦截器来实现通知模型,并维护一个以连接点为中心的拦截器链。

// @Before说明这是一个前置通知,log函数中是要前置执行的代码,JoinPoint是连接点。
@Before("pointcut()") 
public void log(JoinPoint joinPoint) { }

(6) 织入(Weaving)

织入是将切面和业务逻辑对象连接起来, 并创建通知代理的过程。织入可以在编译时,类加载时和运行时完成。在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理。

(7) 增强器(Adviser)

Advisor 是切面的另外一种实现,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。Advisor 由切入点和 Advice 组成。 Advisor 这个概念来自于 Spring 对 AOP 的支撑,在 AspectJ 中是没有等价的概念的。Advisor 就像是一个小的自包含的切面,这个切面只有一个通知。切面自身通过一个 Bean 表示,并且必须实现一个默认接口。

// AbstractPointcutAdvisor是默认接口 
public class LogAdvisor extends AbstractPointcutAdvisor { 
  private Advice advice; // Advice 
  private Pointcut pointcut; // 切入点 
  @PostConstruct public void init() { 
    // AnnotationMatchingPointcut是依据修饰类和方法的注解进行拦截的切入点。 
    this.pointcut = new AnnotationMatchingPointcut((Class) null, Log.class); 
    // 通知 
    this.advice = new LogMethodInterceptor(); 
  } 
}

Spring AOP 与 AspectJ AOP

Spring AOP 是 Spring 实现的 AOP 框架,采用动态代理的模式进行切面的织入,目前 Spring 同时支持原生的 Spring AOP 与 AspectJ 的 AOP 。

AspectJ 是一个基于 Java 语言的 AOP 框架,提供了强大的 AOP 功能,AspectJ 中的很多语法结构基本上已成为 AOP 领域的标准。

Spring AOP AspectJ AOP
简介 Spring AOP 旨在通过 Spring IoC 提供一个简单的 AOP 实现,以解决编码人员面临的最常出现的问题。这并不是完整的 AOP 解决方案,它只能用于 Spring 容器管理的 beans 。 AspectJ 是最原始的 AOP 实现技术,提供了完整的 AOP 解决方案。AspectJ 更为健壮,相对于 Spring AOP 也显得更为复杂。
织入(Weaving)方式 基于动态代理的 AOP 框架,要实现目标对象的切面就需要创建目标对象的代理类。Spring AOP 的代理模式有 2 种实现:
JDK 动态代理:Spring AOP 的首选方法。 每当目标对象实现一个接口时,就会使用 JDK 动态代理。
CGLIB 代理:如果目标对象没有实现接口,则可以使用 CGLIB 代理。
AspectJ 使用了三种不同类型的织入:
编译期织入:把切面类和目标类放在一起用 AJC 编译。
编译后织入:目标类或切面类已经打成一个 jar 包,这时也可以用 AJC 命令对 jar 包再进行一次编织。
类加载时织入(LTW) :在 JVM 加载目标类的时候,做字节码的替换。
支持的连接点(JoinPoint) ❌Method Call
✅Method Execution
❌Constructor Call
❌Constructor Execution
❌Static initializer execution
❌Object initialization
❌Field reference
❌Field assignment
❌Handler execution
❌Advice execution
✅Method Call
✅Method Execution
✅Constructor Call
✅Constructor Execution
✅Static initializer execution
✅Object initialization
✅Field reference
✅Field assignment
✅Handler execution
✅Advice execution
接入便利性 由 Spring 集成,无需引入额外的配置。 需要使用 AJC 编译器进行编译(Maven 下 引入 aspectj-maven-plugin 即可)。
性能(Performance) AspectJ 一般采用编译期织入,而 Spring AOP 一般采用类加载期使用动态代理织入,由于切面代码执行的逻辑是一样的,因此少量切面的时间开销会很接近,但考虑到动态代理产生的开销,一般认为 AspectJ 的性能要优于 Spring AOP 。有人做过相关的性能分析,大家可以参考一下 AOP 底层技术性能测试与比较

Spring AOP

Spring AOP 是通过动态代理的形式实现切面的织入的,调用方通过 Proxy 代理对象间接地与目标对象进行交互,如下图所示:

Spring AOP 的动态代理模式

Spring AOP 的动态代理有 2 种实现方式:

  1. JDK 动态代理:基于接口实现,底层基于反射

  2. CGLIB 动态代理:基于继承实现。

JDK 动态代理

特点:代理对象不需要实现接口,利用 JDK 的 API ,动态地在内存中构建代理对象(需要我们指定创建代理对象/目标对象实现的接口的类型)。代理类在程序运行时创建。

使用场景:只能基于接口实现代理,目标对象必须实现接口。

  1. 定义接口和多个目标对象实现类,与静态代理一致。
  2. 定义一个动态代理类,实现 InvocationHandler 接口。
public class CoderDynamicProxy implements InvocationHandler{
     //被代理的实例
    private ICoder coder;

    public CoderDynamicProxy(ICoder _coder){
        this.coder = _coder;
    }

    //调用被代理的方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(System.currentTimeMillis());
        Object result = method.invoke(coder, args);
        System.out.println(System.currentTimeMillis());
        return result;
    }
}
  1. 通过场景类,动态生成代理类。
public class DynamicClient {

    public static void main(String args[]){
        //要代理的真实对象
        ICoder coder = new JavaCoder("Jack");
        //创建中介类实例
        InvocationHandler  handler = new CoderDynamicProxy(coder);
        //获取类加载器
        ClassLoader cl = coder.getClass().getClassLoader();
        //动态产生一个代理类,或者ICoder proxy = (ICoder) new ProxyFactory(coder).getProxyInstance();
        ICoder proxy = (ICoder) Proxy.newProxyInstance(cl, coder.getClass().getInterfaces(), handler);
        //通过代理类,执行doSomething方法;
        proxy.implDemands("Modify user management");
    }
}

其实就是在运行时通过反射找到一个接口的目标实现类,然后执行对应的方法。

查看 Proxy.newProxyInstance 方法:

//生成代理类class,并加载到jvm中
Class<?> cl = getProxyClass0(loader, interfaces);
//获取代理类参数为InvocationHandler的构造函数
final Constructor<?> cons = cl.getConstructor(constructorParams);
//生成代理类,并返回
return newInstance(cons, ih);

参数:

ClassLoader loader :指定当前目标对象使用类加载器,获取加载器的方法是固定的。

Class[] interfaces :目标对象实现的接口的类型,使用泛型方式确认类型。

InvocationHandler h :事件处理,执行目标对象的方法时,会触发事件处理器的方法,会把当前执行目标对象的方法作为参数传入。

CGLIB 动态代理

【底层通过继承实现】

特点:

  • 以目标对象子类的方式类实现代理,直接基于类实现代理。在内存中构建一个子类对象从而实现对目标对象功能的扩展。
  • CGLIB 是一个强大的高性能的代码生成包,它可以在运行期扩展 Java 类与实现 Java 接口。它广泛的被许多 AOP 的框架使用,例如 Spring AOP 和 synaop ,为他们提供方法的 interception(拦截)。
  • CGLIB 包的底层是通过使用一个小而块的字节码处理框架 ASM 来转换字节码并生成新的类。不鼓励直接使用 ASM ,因为它要求你必须对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。
  1. 目标对象类
/**
 * 目标对象,没有实现任何接口
 */
public class UserDao {

    public void save() {
        System.out.println("----已经保存数据!----");
    }
}
  1. CGLIB 代理工厂
/**
 * Cglib子类代理工厂
 * 对UserDao在内存中动态构建一个子类对象
 */
public class ProxyFactory implements MethodInterceptor{
    //维护目标对象
    private Object target;

    public ProxyFactory(Object target) {
        this.target = target;
    }

    //给目标对象创建一个代理对象
    public Object getProxyInstance(){
        //1.工具类
        Enhancer en = new Enhancer();
        //2.设置父类
        en.setSuperclass(target.getClass());
        //3.设置回调函数
        en.setCallback(this);
        //4.创建子类(代理对象)
        return en.create();

    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("开始事务...");

        //执行目标对象的方法
        Object returnValue = method.invoke(target, args);

        System.out.println("提交事务...");

        return returnValue;
    }
}
  1. 测试类
public class App {

    @Test
    public void test(){
        //目标对象
        UserDao target = new UserDao();

        //代理对象
        UserDao proxy = (UserDao)new ProxyFactory(target).getProxyInstance();

        //执行代理对象的方法
        proxy.save();
    }
}

两种代理的对比

JDK 代理是不需要第三方库支持,只需要 JDK 环境就可以进行代理,使用条件:

1)实现 InvocationHandler ;

2)使用 Proxy.newProxyInstance 产生代理对象;

3)被代理的对象必须要实现接口。

CGLIB 必须依赖于 CGLIB 的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承但是针对接口编程的环境下推荐使用 JDK 的代理。

何时使用 JDK 还是 CGLIB ?

1)如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理实现 AOP 。

2)如果目标对象实现了接口,可以强制使用 CGLIB 实现 AOP 。

3)如果目标对象没有实现了接口,必须采用 CGLIB 库,Spring 会自动在 JDK 动态代理和 CGLIB 之间转换。

Spring 如何选择用 JDK 还是 CGLIB ?

1)当 Bean 实现接口时,Spring 就会用 JDK 的动态代理。

2)当 Bean 没有实现接口时,Spring 使用 CGLIB 实现。

3)可以强制使用 CGLIB(在 Spring 配置中加入 <aop:aspectj-autoproxy proxy-target-class="true"/> )。

CGLIB 比 JDK 快?

1)使用 CGLIB 实现动态代理,CGLIB 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类,在 jdk 6 之前比使用 Java 反射效率要高。唯一需要注意的是,CGLIB 不能对声明为 final 的方法进行代理,因为 CGLIB 原理是动态生成被代理类的子类。

2)在 jdk 6 、jdk 7 、jdk 8 逐步对 JDK 动态代理优化之后,在调用次数较少的情况下,JDK 代理效率高于 CGLIB 代理效率,只有当进行大量调用的时候,jdk 6 和 jdk 7 比 CGLIB 代理效率低一点,但是到 jdk 8 的时候,jdk 代理效率高于 CGLIB 代理。

总之,每一次 jdk 版本升级,jdk 代理效率都得到提升,而 CGLIB 代理效率却有点跟不上步伐。

Spring AOP 获取代理对象时序图

Spring AspectJ AOP

AspectJ 的织入过程

AspectJ 利用了特殊的编译器 AJC(AspectJ Compiler),在编译时编译后进行织入(除了 LTW )。

参考


文章作者: wenjun
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wenjun !
评论
 上一篇
Spring IoC Spring IoC
Spring IoC概念梳理什么是 IoC 和 DI ?IoC :控制反转(Inversion of Control),这不是什么技术,而是一种设计思想。在 Java 开发中,IoC 意味着将你设计好的对象交给容器控制,而不是传统的在你的对
2020-05-26
下一篇 
约瑟夫问题 约瑟夫问题
约瑟夫问题前言约瑟夫问题是个著名的问题:N 个人围成一圈,第一个人从 1 开始报数,报 M 的将被杀掉,下一个人接着从 1 开始报。如此反复,最后剩下一个,求最后的胜利者。例如只有三个人,把他们叫做 A 、B 、C ,他们围成一圈,从 A
2020-05-20
  目录