【Spring】Spring5 AOP

img

AOP 基本概念

AOP(Aspect Oriented Programming)意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

Spring AOP的源码分析见【Spring】Spring5 AOP源码分析

img

  • 面向切面编程(方面),利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率;

  • 通俗描述:不通过修改源代码方式,在主干功能里面添加新功能。

  • 使用登录例子说明 AOP:

img

AOP 底层原理

AOP 底层使用动态代理 ,动态代理有两种情况:

第一种 有接口情况,使用JDK 动态代理 ;创建接口实现类代理对象,增强类的方法
在这里插入图片描述

第二种 没有接口情况,使用CGLIB 动态代理创建子类的代理对象,增强类的方法(该方法不需要实现接口,由CGLIB创建代理对象)
在这里插入图片描述

AOP JDK 动态代理

1)使用 JDK 动态代理,使用 Proxy类里面的方法创建代理对象:

调用 newProxyInstance 方法,方法有三个参数:

1
2
3
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
  • 参数一:类加载器
  • 参数二:增强方法所在的类,这个类实现的接口,支持多个接口
  • 参数三:实现这个接口 InvocationHandler,创建代理对象,写增强的部分

2)编写 JDK 动态代理代码

1
2
3
4
5
//(1)创建接口,定义方法
public interface UserDao {
public int add(int a,int b);
public String update(String id);
}
1
2
3
4
5
6
7
8
9
10
11
//(2)创建接口实现类,实现方法
public class UserDaoImpl implements UserDao {
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public String update(String id) {
return id;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//(3)使用 Proxy 类创建接口代理对象
public class JDKProxy {
public static void main(String[] args) {
//创建接口实现类代理对象
Class[] interfaces = {UserDao.class};
UserDaoImpl userDao = new UserDaoImpl();
/** 第一参数,类加载器
第二参数,增强方法所在的类,这个类实现的接口,(支持多个接口)
第三参数,实现这个接口 InvocationHandler,创建代理对象,写增强的部分 */
UserDao dao =(UserDao)Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces,
new UserDaoProxy(userDao));
int result = dao.add(1, 2);
System.out.println("result:"+result);
}
}

//创建代理对象代码
class UserDaoProxy implements InvocationHandler {
//1 把创建的是谁的代理对象,把谁传递过来
//有参数构造传递
private Object obj;
public UserDaoProxy(Object obj) {
this.obj = obj;
}
//增强的逻辑
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法之前
System.out.println("方法之前执行...."+method.getName()+" :传递的参数..."+ Arrays.toString(args));
//被增强的方法执行
Object res = method.invoke(obj, args);
//方法之后
System.out.println("方法之后执行...."+obj);
return res;
}
}

AOP 术语

  • 连接点(JointPoint):类里面哪些方法可以被增强,这些方法称为连接点,每一个方法的每一个位置(开始位置,返回位置,异常位置等)都是一个连接点。
  • 切入点(PointCut):切面通知执行的“地点”的定义,实际被真正增强的方法称为切入点。
  • 横切关注点:跨越应用程序多个模块的方法或功能。即是,与我们业务逻辑无关的,但是我们需要关注的部分,就是横切关注点。如日志 , 安全 , 缓存 , 事务等等 ….
  • 切面(Aspect):横切关注点被模块化的特殊对象。即,它是一个类。
  • 通知方法(Advice):切面必须要完成的工作。即,它是类中的一个方法。包含前置通知,后置通知,环绕通知 ,异常通知和最终通知。
  • 目标(Target):被通知的对象。
  • 代理(Proxy):向目标对象应用通知之后创建的对象。

image-20210713170239284

img

AOP CGLIB 动态代理

CGLIB是一个功能强大,高性能的代码生成库(第三方库,可以由Maven导入)。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。CGLIB作为一个开源项目,其代码托管在github,地址为:https://github.com/cglib/cglib

CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。

CGLIB 底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

CGLIB缺点:对于final方法,无法进行代理。

Spring 框架一般都是基于 AspectJ 实现 AOP 操作,AspectJ 不是 Spring 组成部分,独立 AOP 框架,一般把 AspectJ 和 Spirng 框架一起使用,进行 AOP 操作。基于 AspectJ 实现 AOP 操作的两种方式:

  • 基于注解方式实现
  • 基于 xml 配置文件实现

AOP 基于注解开发

采用动态代理的设计模式,在程序运行期间动态地将某段代码切入到指定方法(切入点)指定位置进行运行的编程方式。

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>

定义一个业务逻辑类(MathCalculator)。试图在业务逻辑运行的时候将日志进行打印(方法之前,方法运行结束,方法出现异常等)

1
2
3
4
5
public class MathCalculator {
public int div(int i, int j){
return i/j;
}
}

定义一个日志切面类(LogAspects),在切面类里需要动态感知MathCalculator.div()方法运行到什么阶段并执行相应通知方法。通知方法:

  • 前置通知(@Before):在切入点(PointCut)运行之前运行
  • 后置通知(@After):在切入点运行结束之后运行(无论方法是否正常结束)
  • 返回通知(@AfterReturning):在切入点正常返回之后运行(异常不执行)
  • 异常通知(@AfterThrowing):在切入点出现异常之后运行
  • 环绕通知(@Around):动态代理的方式手动推进切入点运行(joinPoint.procced()),是最底层的通知,其可以实现上述四个通知效果

通知方法的执行顺序

  • 环绕通知(@AroundjoinPoint.procced()方法之前的代码
  • 前置通知(@Before
  • 业务代码
  • 返回通知(@AfterReturning)/ 若有异常,此时执行异常通知(@AfterThrowing
  • 后置通知(@After
  • 环绕通知(@AroundjoinPoint.procced()方法以及其之后的代码

多个切面的情况下,先执行前置通知的后执行返回通知和后置通知,后执行前置通知的先执行返回通知和后置通知。类似方法栈先进后出。执行顺序由切面类的字母顺序排序,也可以通过@Order(1)设置优先级

切入点表达式写法:

1
2
3
4
5
6
7
8
9
1)切入点表达式作用:知道对哪个类里面的哪个方法进行增强 
2)语法结构: execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]) )
3)例子如下:
1:对 com.zhao.dao.BookDao 类里面的 add 进行增强
execution(* com.zhao.dao.BookDao.add(..))
2:对 com.zhao.dao.BookDao 类里面的所有的方法进行增强
execution(* com.zhao.dao.BookDao.* (..))
3:对 com.zhao.dao 包里面所有类,类里面所有方法进行增强
execution(* com.zhao.dao.*.* (..))

配置类需要添加@EnableAspectJAutoProxy以开启注解版的AOP自动代理。整个AOP就是从@EnableAspectJAutoProxy注解开始执行的。(Spring中有很多的@EnableXXX注解,其作用是代替xml文件中的一些配置开启某些功能)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@EnableAspectJAutoProxy
@Configuration
public class SpringConfigAOP {
// 将业务逻辑类加入到容器中
@Bean
public MathCalculator calculator(){
return new MathCalculator();
}

// 切面类加入到容器中
@Bean
public LogsAspects logsAspects(){
return new LogsAspects();
}
}

切面类LogsAspects

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Aspect
@Order(1)
public class LogsAspects {

// 抽取公共的切入点表达式
// 1. 本类可以引用
// 2. 其他的切面类也可以引用(需要全类名)
@Pointcut("execution(* com.zhao.aop.MathCalculator.*(..))")
public void pointCut(){
}

@Before("execution(int com.zhao.aop.MathCalculator.div(int, int))")
public void logStart(JoinPoint joinPoint){
Object[] args = joinPoint.getArgs(); // 方法参数
System.out.println("前置通知@Before.... ");
}

@After("execution(* com.zhao.aop.MathCalculator.*(..))")
public void logEnd(){
System.out.println("后置通知@After....");
}

@AfterReturning(value = "pointCut()", returning = "result")
public void logReturn(Object result){
// result: 方法返回值
System.out.println("返回通知@AfterReturning.... ");
}

@AfterThrowing(value = "pointCut()", throwing = "exception")
public void logException(Exception exception){
System.out.println("异常通知@AfterThrowing.... ");
}

@Around("pointCut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("签名: " + proceedingJoinPoint.getSignature());
Object[] args = proceedingJoinPoint.getArgs();

try {
System.out.println("【环绕前置通知】.... ");
//执行目标方法proceed
Object result = proceedingJoinPoint.proceed(args);
System.out.println("【环绕返回通知】.... ");
} catch (Exception exception){
System.out.println("【环绕异常通知】.... ");
} finally {
System.out.println("【环绕后置通知】.... ");
}

return result;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AOPTest {
AnnotationConfigApplicationContext context;

@Test
public void testOfAop() {
context = new AnnotationConfigApplicationContext(SpringConfigAOP.class);
System.out.println("容器创建完成....");

// 必须从容器中获得bean才能启动AOP
MathCalculator bean = context.getBean(MathCalculator.class);
bean.div(1, 1);

context.close();
}
}

控制台打印:

1
2
3
4
5
6
7
8
9
10
签名: int com.zhao.aop.MathCalculator.div(int,int)
【环绕前置通知】....
前置通知@Before....
div方法执行...
返回通知@AfterReturning....
后置通知@After....
【环绕返回通知】....
【环绕后置通知】...

Process finished with exit code 0

AOP 基于xml开发

使用AOP织入,需要导入一个依赖包(和上面的区别?)

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

方式一:通过 Spring API 实现AOP

第一步:首先编写业务接口和实现类

1
2
3
4
5
6
public interface UserService {
public void add();
public void delete();
public void update();
public void search();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserServiceImpl implements UserService{
@Override
public void add() {
System.out.println("增加用户");
}
@Override
public void delete() {
System.out.println("删除用户");
}
@Override
public void update() {
System.out.println("更新用户");
}
@Override
public void search() {
System.out.println("查询用户");
}
}

第二步:然后编写增强类 :一个前置增强,一个后置增强

1
2
3
4
5
6
7
8
9
public class Log implements MethodBeforeAdvice {
//method : 要执行的目标对象的方法
//objects : 被调用的方法的参数
//Object : 目标对象
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println( o.getClass().getName() + "的" + method.getName() + "方法被执行了");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
public class AfterLog implements AfterReturningAdvice {
//returnValue 返回值
//method被调用的方法
//args 被调用的方法的对象的参数
//target 被调用的目标对象
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了" + target.getClass().getName()
+"的"+method.getName()+"方法,"
+"返回值:"+returnValue);
}
}

第三步:最后去Spring的文件中注册 , 并实现AOP切入实现 , 注意导入约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--注册bean-->
<bean id="userService" class="com.zhao.service.UserServiceImpl"/>
<bean id="log" class="com.zhao.log.Log"/>
<bean id="afterLog" class="com.zhao.log.AfterLog"/>
<!--aop的配置-->
<aop:config>
<!--切入点 expression:表达式匹配要执行的方法 ..代表该包下的子包也能被扫描到 -->
<aop:pointcut id="pointcut" expression="execution(* com.zhao.service.UserServiceImpl..*(..))"/>
<!--执行环绕; advice-ref执行方法 . pointcut-ref切入点-->
<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
<aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
</aop:config>
</beans>

第四步:测试

1
2
3
4
5
6
7
8
public class MyTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.search();
}
}

Spring的AOP就是将公共的业务 (日志 , 安全等) 和领域业务结合起来 ,当执行领域业务时,将会把公共业务加进来,实现公共业务的重复利用。

方式二:自定义类来实现AOP

目标业务类不变依旧是userServiceImpl

第一步:编写一个切入类

1
2
3
4
5
6
7
8
public class DiyPointcut {
public void before(){
System.out.println("---------方法执行前---------");
}
public void after(){
System.out.println("---------方法执行后---------");
}
}

第二步:在Spring中配置

1
2
3
4
5
6
7
8
9
10
11
12
<!--第二种方式自定义实现-->
<!--注册bean-->
<bean id="diy" class="com.zhao.config.DiyPointcut"/>
<!--aop的配置-->
<aop:config>
<!--第二种方式:使用AOP的标签实现-->
<aop:aspect ref="diy">
<aop:pointcut id="diyPonitcut" expression="execution(* com.zhao.service.UserServiceImpl..*(..))"/>
<aop:before pointcut-ref="diyPonitcut" method="before"/>
<aop:after pointcut-ref="diyPonitcut" method="after"/>
</aop:aspect>
</aop:config>

第三步:测试

1
2
3
4
5
6
7
8
public class MyTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.add();
}
}

Spring AOP 源码分析

Spring AOP的源码分析见【Spring】Spring5 AOP源码分析