摘要
AOP即面向切面编程,是OOP编程的有效补充。使用AOP技术,可以将一些系统性相关的编程工作,独立提取出来,独立实现,然后通过切面切入进系统。从而避免了在业务逻辑的代码中混入很多的系统相关的逻辑——比如权限管理,事物管理,日志记录等等。这些系统性的编程工作都可以独立编码实现,然后通过AOP技术切入进系统即可。从而达到了将不同的关注点分离出来的效果。AOP是Spring提供的关键特性之一,本文深入剖析Spring AOP的原理。
一、Spring AOP概念
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程.AOP最早是AOP联盟的组织提出的,指定的一套规范,spring将AOP的思想引入框架之中,通过预编译方式和运行期间动态代理实现程序的统一维护的一种技术,先来看一个例子, 如何给如下UserServiceImpl中所有方法添加进入方法的日志。
public class UserServiceImpl implements IUserService {
/**
* find user list.
*
* @return user list
*/
@Override
public List<User> findUserList() {
System.out.println("execute method: findUserList");
return Collections.singletonList(new User("zhuangxiaoyan", 18));
}
/**
* add user
*/
@Override
public void addUser() {
System.out.println("execute method: addUser");
// do something
}
}
我们将记录日志功能解耦为日志切面,它的目标是解耦。进而引出AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中!
OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。这两种设计思想在目标上有着本质的差异。
二、Spring AOP的术语
-
连接点(Jointpoint):表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点,在AOP中表示为在哪里干;
-
切入点(Pointcut): 选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为在哪里干的集合;
-
通知(Advice):在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知;在AOP中表示为干什么;
-
方面/切面(Aspect):横切关注点的模块化,比如上边提到的日志组件。可以认为是通知、引入和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为在哪干和干什么集合;
-
引入(inter-type declaration):也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象), 在AOP中表示为干什么(引入什么);
-
目标对象(Target Object):需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为对谁干;
-
织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。在AOP中表示为怎么实现的;
-
AOP代理(AOP Proxy):AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。在AOP中表示为怎么实现的一种典型方式;
通知类型:
-
前置通知(Before advice):在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
-
后置通知(After returning advice):在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
-
异常通知(After throwing advice):在方法抛出异常退出时执行的通知。
-
最终通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
-
环绕通知(Around Advice):包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。
环绕通知是最常用的通知类型。和AspectJ一样,Spring提供所有类型的通知,我们推荐你使用尽可能简单的通知类型来实现需要的功能。例如,如果你只是需要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知,尽管环绕通知也能完成同样的事情。用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。比如,你不需要在JoinPoint上调用用于环绕通知的proceed()方法,就不会有调用的问题。
三、Spring AOP 实现方式
AOP分为静态AOP和动态AOP。静态AOP是指AspectJ实现的AOP,他是将切面代码直接编译到Java类文件中。动态AOP是指将切面代码进行动态织入实现的AOP。Spring的AOP为动态AOP,实现的技术为:JDK提供的动态代理技术 和 CGLIB(动态字节码增强技术)。尽管实现技术不一样,但都是基于代理模式,都是生成一个代理对象。
3.1 JDK动态代理
主要使用到 InvocationHandler 接口和 Proxy.newProxyInstance() 方法。JDK动态代理要求被代理实现一个接口,只有接口中的方法才能够被代理。其方法是将被代理对象注入到一个中间对象,而中间对象实现InvocationHandler接口,在实现该接口时,可以在 被代理对象调用它的方法时,在调用的前后插入一些代码。而 Proxy.newProxyInstance() 能够利用中间对象来生产代理对象。插入的代码就是切面代码。所以使用JDK动态代理可以实现AOP。
JDK动态代理:必须是面向接口的,只有实现了具体接口的类才能生成代理对象
3.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。
3.3 Cglib 和 jdk 动态代理的区别
- Jdk动态代理:利用拦截器(必须实现 InvocationHandler )加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理
- Cglib动态代理:利用
ASM
框架,对代理对象类生成的 class 文件加载进来,通过修改其字节码生成子类来处理
什么时候用 cglib 什么时候用 JDK 动态代理?
- 目标对象生成了接口 默认用 JDK 动态代理
- 如果目标对象使用了接口,可以强制使用 cglib
- 如果目标对象没有实现接口,必须采用 cglib 库, Spring 会自动在 JDK 动态代理和 cglib 之间转换
JDK 动态代理和 cglib 字节码生成的区别?
- JDK 动态代理只能对实现了接口的类生成代理,而不能针对类。
- Cglib 是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,并覆盖其中方法的增强,但是因为采用的是继承,所以该类或方法最好不要设置为 final ,对于 final 类或方法,是无法继承的
Cglib 比 JDK 快?
- cglib 底层是 ASM 字节码生成框架,但是字节码技术生成代理类,在 JDK1.6 之前比使用 java 反射的效率要高
- 在 JDK1.6 之后逐步对 JDK 动态代理进行了优化,在调用次数比较少时效率高于 cglib 代理效率
- 只有在大量调用的时候 cglib 的效率高,但是在 JDK8 的时候JDK的效率已高于 cglib
- Cglib 不能对声明 final 的方法进行代理,因为 cglib 是动态生成代理对象,final 关键字修饰的类不可变只能被引用不能被修改
Spring 如何选择是用 JDK 还是 cglib?
- 当 bean 实现接口时,会用 JDK 代理模式
- 当 bean 没有实现接口,用 cglib实现
- 可以强制使用cglib(在spring配置中加入<aop:aspectj-autoproxy proxyt-target-class="true"/>)
3.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 AOP的配置
AOP是一个标准规范,而为了实现这个标准规范,有几种方式:
- 基于代理的AOP
- @AspectJ注解驱动的切面
- 纯POJO切面
- 注入式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();
}
}
直接运行测试类,可以看到对方法进行了增强
更多实战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 事务