摘要

AOP是Spring提供的关键特性之一。AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了将不同的关注点分离出来的效果。本文深入剖析Spring的AOP的原理。

一、spring AOP相关的概念

1.1 PointCut

即在哪个地方进行切入,它可以指定某一个点,也可以指定多个点。比如类A的methord函数,当然一般的AOP与语言(AOL)会采用多用方式来定义PointCut,比如说利用正则表达式,可以同时指定多个类的多个函数。

Spring——spring AOP源码分析_动态代理

我们来看一下PointCut的类图,以PointCut接口为核心进行扩展。PointCut 依赖了ClassFilter和MethodMatcher,ClassFilter用来指定特定的类,MethodMatcher 指定特定的函数,正是由于PointCut仅有的两个依赖,它只能实现函数级别的AOP。对于属性、for语句等是无法实现该切点的。MethodMatcher 有两个实现类StaticMethodMatcher和DynamicMethodMatcher,它们两个实现的唯一区别是isRuntime(参考下面的源码)。StaticMethodMatcher不在运行时检测,DynamicMethodMatcher要在运行时实时检测参数,这也会导致DynamicMethodMatcher的性能相对较差。

public abstract class StaticMethodMatcher implements MethodMatcher {

@Override
public final boolean isRuntime() {
return false;
}

@Override
public final boolean matches(Method method, Class<?> targetClass, Object[] args) {
// should never be invoked because isRuntime() returns false
throw new UnsupportedOperationException("Illegal MethodMatcher usage");
}
}
public abstract class DynamicMethodMatcher implements MethodMatcher {

@Override
public final boolean isRuntime() {
return true;
}

/**
* Can override to add preconditions for dynamic matching. This implementation
* always returns true.
*/
@Override
public boolean matches(Method method, Class<?> targetClass) {
return true;
}

}

类似继承于StaticMethodMatcher和DynamicMethodMatcher也有两个分支StaticMethodMatcherPointcut和DynamicMethodMatcherPointcut,StaticMethodMatcherPointcut是我们最常用,其具体实现有两个NameMatchMethodPointcut和JdkRegexpMethodPointcut,一个通过name进行匹配,一个通过正则表达式匹配。有必要对另外一个分支说一下ExpressionPointcut,它的出现是了对AspectJ的支持,所以其具体实现也有AspectJExpressionPointcut。最左边的三个给我们提供了三个更强功能的PointCut,AnnotationMatchingPointcut:可以指定某种类型的注解,ComposiblePointcut:进行与或操作,ControlFlowPointcut:这个有些特殊,它是一种控制流,例如类A 调用B.method(),它可以指定当被A调用时才进行拦截。

1.2 Advice

在切入点干什么,指定在PointCut地方做什么事情(增强),打日志、执行缓存、处理异常等等。

Spring——spring AOP源码分析_Spring_02

AfterAdvice是指函数调用结束之后增强,它又包括两种情况:异常退出和正常退出;BeforeAdvice指函数调用之前增强;Inteceptor有点特殊,它是由AOP联盟定义的标准,也是为了方便Spring AOP 扩展,以便对其它AOL支持。Interceptor有很多扩展,比如Around Advice的功能实现(具体实现是Advisor的内容了,接下来再看)

1.3 Advisor/Aspect

PointCut + Advice 形成了切面Aspect,这个概念本身即代表切面的所有元素。但到这一地步并不是完整的,因为还不知道如何将切面植入到代码中,解决此问题的技术就是PROXY。同样Advisor按照Advice去分也可以分成两条线路,一个是来源于Spring AOP 的类型,一种是来自AOP联盟的Interceptoor, IntroductionAdvisor就是对MethodInterceptor的继承和实现。

Spring——spring AOP源码分析_AOP_03

所以接下类我们还是分成两类来研究其具体实现:Spring AOP的PointcutAdvisor

Spring——spring AOP源码分析_AOP_04

AbstractPointcutAdvisor 实现了Ordered,为多个Advice指定顺序,顺序为Int类型,越小优先级越高,AbstractGenericPointcutAdvisor 指定了Advice,除了Introduction之外的类型
下面具体的Advisor实现则对应于PointCut 的类型,具体指定哪个pointCut。

1.4 Proxy

Proxy 即代理,其不能算做AOP的家庭成员,更相当于一个管理部门,它管理 了AOP的如何融入OOP。之所以将其放在这里,是因为Aspect虽然是面向切面核心思想的重要组成部分,但其思想的践行者却是Proxy,也是实现AOP的难点与核心据在。

Spring——spring AOP源码分析_Spring_05

ProxyConfig设置了几个参数:

代理有两种方式:一种是接口代理(上文提到过的动态代理),一种是CGLIB。默认有接口的类采用接口代理,否则使用CGLIB。如果设置成true,则直接使用CGLIB;

private boolean proxyTargetClass = false;


是否进行优化,不同代理的优化一般是不同的。如代理对象生成之后,就会忽略Advised的变动。

private boolean optimize = false;

opaque 是否强制转化为advised

boolean opaque = false;

exposeProxy:AOP生成对象时,绑定到ThreadLocal, 可以通过AopContext获取

boolean exposeProxy = false;

frozen:代理信息一旦设置,是否允许改变

private boolean frozen = false;

二、spring AOP 的实现方式

AOP分为静态AOP和动态AOP。静态AOP是指AspectJ实现的AOP,他是将切面代码直接编译到Java类文件中。动态AOP是指将切面代码进行动态织入实现的AOP。Spring的AOP为动态AOP,实现的技术为:JDK提供的动态代理技术 和 CGLIB(动态字节码增强技术)。尽管实现技术不一样,但都是基于代理模式,都是生成一个代理对象。

2.1 JDK动态代理

主要使用到 InvocationHandler 接口和 Proxy.newProxyInstance() 方法。JDK动态代理要求被代理实现一个接口,只有接口中的方法才能够被代理。其方法是将被代理对象注入到一个中间对象,而中间对象实现InvocationHandler接口,在实现该接口时,可以在 被代理对象调用它的方法时,在调用的前后插入一些代码。而 Proxy.newProxyInstance() 能够利用中间对象来生产代理对象。插入的代码就是切面代码。所以使用JDK动态代理可以实现AOP。

JDK动态代理:必须是面向接口的,只有实现了具体接口的类才能生成代理对象

2.2 CGLIB动态代理

CGLIB动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。CGLIB动态代理和jdk代理一样,使用反射完成代理,不同的是他可以直接代理类(jdk动态代理不行,他必须目标业务类必须实现接口),CGLIB动态代理底层使用字节码技术,CGLIB动态代理不能对 final类进行继承。(CGLIB动态代理需要导入jar包)。

最底层的是字节码 Bytecode ,字节码是 java 为了保证依次运行,可以跨平台使用的一种虚拟指令格式,在字节码文件之上的是ASM,只是一种直接操作字节码的框架,应用 ASM 需要对 Java 字节码、 class 结构比较熟悉位于 ASM 上面的是 Cglib、groovy、beanshell,后来那个种并不是 Java 体系中的内容是脚本语言,他们通过 ASM 框架生成字节码变相执行 Java 代码,在JVM中程序执行不一定非要写 java 代码,只要能生成 java 字节码, ​​JVM​​ 并不关系字节码的来源位于 cglib、groovy、beanshell之上的就是 hibernate 和 Spring AOP,最上面的是applications。

2.3 Cglib 和 jdk 动态代理的区别

  1. Jdk动态代理:利用拦截器(必须实现 InvocationHandler )加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
  2. Cglib动态代理:利用 ​​ASM​​ 框架,对代理对象类生成的 class 文件加载进来,通过修改其字节码生成子类来处理

什么时候用 cglib 什么时候用 JDK 动态代理?

  1. 目标对象生成了接口 默认用 JDK 动态代理
  2. 如果目标对象使用了接口,可以强制使用 cglib
  3. 如果目标对象没有实现接口,必须采用 cglib 库, Spring 会自动在 JDK 动态代理和 cglib 之间转换

JDK 动态代理和 cglib 字节码生成的区别?

  1. JDK 动态代理只能对实现了接口的类生成代理,而不能针对类。
  2. Cglib 是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法的增强,但是因为采用的是继承,所以该类或方法最好不要设置为 final ,对于 final 类或方法,是无法继承的

Cglib 比 JDK 快?

  1. cglib 底层是 ASM 字节码生成框架,但是字节码技术生成代理类,在 JDK1.6 之前比使用 java 反射的效率要高
  2. 在 JDK1.6 之后逐步对 JDK 动态代理进行了优化,在调用次数比较少时效率高于 cglib 代理效率
  3. 只有在大量调用的时候 cglib 的效率高,但是在 JDK8 的时候JDK的效率已高于 cglib
  4. Cglib 不能对声明 final 的方法进行代理,因为 cglib 是动态生成代理对象,final 关键字修饰的类不可变只能被引用不能被修改

Spring 如何选择是用 JDK 还是 cglib?

  1. 当 bean 实现接口时,会用 JDK 代理模式
  2. 当 bean 没有实现接口,用 cglib实现
  3. 可以强制使用cglib(在spring配置中加入<aop:aspectj-autoproxy proxyt-target-class="true"/>)

2.4 Lombok原理

Lombok 属于 Java 的一个热门代码生成工具类,使用它可以自动生成 Setter、Getter、toString、equals 和 hashCode 等等方法。

package com.zhuangxiaoyan.spring.srcode.entity;

import java.util.Objects;

/**
* @Classname User2
* @Description TODO
* @Date 2022/5/4 11:15
* @Created by xjl
*/
public class User2 {
private static final long serialVersionUID = -8054600833969507380L;

private Integer id;

private String username;

private Integer age;

public User2() {
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", age=" + age +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User2 user2 = (User2) o;
return Objects.equals(id, user2.id) &&
Objects.equals(username, user2.username) &&
Objects.equals(age, user2.age);
}

@Override
public int hashCode() {
return Objects.hash(id, username, age);
}
}
package com.zhuangxiaoyan.spring.srcode.entity;

import lombok.Data;

import java.util.Objects;

/**
* @Classname User2
* @Description TODO
* @Date 2022/5/4 11:15
* @Created by xjl
*/
@Data
public class User3 {
private static final long serialVersionUID = -8054600833969507380L;

private Integer id;

private String username;

private Integer age;
}

 编译源文件,然后反编译class文件,反编译结果如下图。说明@Data注解在类上,会为类的所有属性自动生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。可以看出 Lombok 是在编译期就为我们生成了对应的字节码。Lombok 是基于 Java 1.6 实现的 JSR 269: Pluggable Annotation Processing API 来实现的,也就是通过编译期自定义注解处理器来实现的,它的执行步骤如下:

Spring——spring AOP源码分析_Spring_06

三、spring AOP的配置使用

AOP是一个标准规范,而为了实现这个标准规范,有几种方式:

  1. 基于代理的AOP
  2. @AspectJ注解驱动的切面
  3. 纯POJO切面
  4. 注入式AspectJ切面

这四种方式都是实现aop的方法,这里讲一下通过AspectJ提供的注解实现AOP,但在spring官网中,有AspectJ 的概念,主要是因为在spring2.x的时候,spring aop的语法过于复杂,spring想进行改进,而改进的时候就借助了AspectJ 的语法、编程风格来完场aop的配置功能,这里使用AspectJ 注解方式来实现。

package com.zhuangxiaoyan.helloworld.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
* @Classname AppConfig
* @Description TODO
* @Date 2022/4/24 18:14
* @Created by xjl
*/
@Configuration
@ComponentScan("com.zhuangxiaoyan.helloworld")
public class AppConfig {

}

增加切面类

使用@Aspect注解声明一个切面,并使用@Before、@After等注解表明连接点

package com.zhuangxiaoyan.helloworld.proxy;

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

/**
* @Classname LogAspect
* @Description TODO
* @Date 2022/5/1 17:32
* @Created by xjl
*/
@Aspect
@Component
public class LogAspect {

@Pointcut("execution(public * com.zhuangxiaoyan.helloworld.proxy..*.*(..))*")
public void pointCut(){};

@Before("pointCut()")
public void logStart(){
System.out.println("查询之前打印日志....");
}

@After("pointCut()")
public void logEnd(){
System.out.println("查询之后打印日志....");
}

@AfterReturning("pointCut()")
public void logReturn(){
System.out.println("查询之后正常返回....");
}

@AfterThrowing("pointCut()")
public void logException(){
System.out.println("查询之后返回异常....");
}
}

测试运行类

package com.zhuangxiaoyan.helloworld.test;

import com.zhuangxiaoyan.helloworld.config.AppConfig;
import com.zhuangxiaoyan.helloworld.proxy.UserService2;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
* @Classname AOPTest
* @Description TODO
* @Date 2022/5/1 17:33
* @Created by xjl
*/
public class AOPTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
UserService2 userService = ac.getBean(UserService2.class);
userService.queryAll();
}
}

直接运行测试类,可以看到对方法进行了增强

Spring——spring AOP源码分析_Spring_07

更多实战code请参考: ​​SpringPrinciple: SpringPrinciple - Gitee.com​

三、spring中AOP实现

  • Authentication 权限
  • Caching 缓存
  • Context passing 内容传递
  • Error handling 错误处理
  • Lazy loading 懒加载
  • Debugging  调试
  • logging, tracing, profiling and monitoring 记录跟踪 优化 校准
  • Performance optimization 性能优化
  • Persistence  持久化
  • Resource pooling 资源池
  • Synchronization 同步
  • Transactions 事务

3.1 Spring事务实现aop思想


博文参考