AOP面向切面编程

用于解决系统层面上的问题,比如:日志、事务、权限等待。它是一种编程范式,不是编程语言。

优点以及一些概念

1、AOP的优点

  • 降低模块之间的耦合度
  • 使系统更容易扩展
  • 更好的代码复用
  • 非业务代码更加集中,不分散,便于统一管理
  • 业务代码更加简洁纯粹,不掺杂其他的代码的影响

2、AOP中出现的一些概念

  • 横切关注点

对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

  • Aspect 切面

通常是一个类,里面可以定义切入点和通知。

  • JointPoint 连接点

连接点是在应用执行中能够插入切面的一个点。即程序执行过程中能够应用通知的所有点。

程序执行过程中明确的点,一般是方法的调用。

被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器。

  • Advice 通知

AOP在特定的切入点上执行的增强处理

  • Pointcut 切入点

就是带有通知的连接点,在程序中主要体现为书写切入点表达式

将切面应用到目标对象,并创建新的代理对象的过程。

切面在指定的连接点被织入到目标对象中。

在目标对象的生命周期里有多个点可以进行织入:

  1. 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。Aspect的织入编译器就是以这种方式织入切面的。
  2. 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader) ,它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入(load-time weaving, LTW)就支持以这种方式织入切面。
  3. 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时, AOP容器会为目标对象动态地创建一个代理对象(动态代理)。Spring AOP就是以这种方式织入切面的。
  • AOP Proxy 代理

AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类

常见通知

5种Advice

  • 前置通知(Before):在目标方法被调用之前调用通知功能。

  • 后置通知(After-returning):在目标方法成功执行之后调用通知。目标方法异常不执行。

  • 异常通知(After-throwing)):在目标方法抛出异常后调用通知。

  • 环绕通知(Around) :通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

    • 环绕(后)通知,目标方法异常不执行。
  • 最终通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。

AOP实践——基于xml的Spring

添加依赖

1
2
3
4
5
6
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
<scope>compile</scope>
</dependency>

配置命名空间

关于aop的三行

1
2
3
4
5
6
7
8
9
10
11
12
<?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:context="http://www.springframework.org/schema/context"
context:xsi="schemaLocation=http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd"
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">

AOP的两种配置方式

第一种,使用<aspect>配置。

这种方式更加灵活。Advice方法的配置用标签实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
<aop:config>
<!-- 指定切入点 -->
<!-- 也可以直接将execution表达式写在pointcut-ref属性中 -->
<aop:pointcut id="myPointCut" expression="execution(void com.service.impl.UserServiceImpl.show())" />
<!-- 指定通知 以及类型 -->
<aop:aspect ref="myAdvice">
<aop:before method="beforeAdvice" pointcut-ref="myPointCut"/>
<aop:after-returning method="afterReturnAdvice" pointcut-ref="myPointCut"/>
<aop:around method="aroundAdvice" pointcut-ref="myPointCut"/>
<aop:after-throwing method="afterThrowingAdvice" pointcut-ref="myPointCut"/>
<aop:after method="afterAdvice" pointcut-ref="myPointCut"/>
</aop:aspect>
</aop:config>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.advice;

import org.aspectj.lang.ProceedingJoinPoint;

public class MyAdvice {
public void beforeAdvice() {
System.out.println("执行了前置增强...");
}
public void afterReturnAdvice() {
System.out.println("执行了后置增强...");
}
public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("执行了环绕(前)增强...");
Object proceed = proceedingJoinPoint.proceed();//执行目标方法(目标方法有可能有返回值所以返回Object)
System.out.println("执行了环绕(后)增强...");
return proceed;
}
public void afterThrowingAdvice() {
System.out.println("执行了异常后增强...");
}
public void afterAdvice() {
System.out.println("执行了最终增强...");
}
}

第二种,使用<advisor>配置。(开发中基本不用)

需要增强类实现相应的Advice接口或其子接口

相比于前者,其Advice方法在advice类中实现,标签只需要引用advice类和切入点即可。

1
2
3
4
<aop:config>
<aop:pointcut id="myPointCut" expression="execution(void com.service.impl.UserServiceImpl.show())" />
<aop:advisor pointcut-ref="myPointCut" advice-ref="myAdvice2"></aop:advisor>
</aop:config>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.advice;

import java.lang.reflect.Method;

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.lang.Nullable;

public class MyAdvice2 implements MethodBeforeAdvice, AfterReturningAdvice {

@Override
public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
System.out.println("执行了前置增强...");
}

@Override
public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable {
System.out.println("执行了后置增强...");
}

}

两种方法的比较

语法形式不同:

  • advisor是通过实现接口来确认通知的类型
  • aspect是通过配置确认通知的类型,更加灵活

可配置的切面数量不同:

  • 一个advisor只能配置一个固定通知和一个切点表达式
  • 一个aspect可以配置多个通知和多个切点表达式任意组合

使用场景不同:

  • 允许随意搭配情况下可以使用aspect进行配置
  • 如果通知类型单一、切面单一的情况下可以使用advisor进行配置
  • 在通知类型已经固定,不用人为指定通知类型时,可以使用advisor进行配置,例如后面要学习的 Spring事务控制的配置

AOP原理刨析

Bilibili精准空降网友文字版

AOP实践——基于注解的Spring

启用AOP的注解

如果你选择使用配置文件来使用注解

1
<aop:aspectj-autoproxy />

使用配置类

1
@EnableAspectJAutoProxy

切面的配置

1
2
3
4
5
6
@Aspect 				//声明切面
@Before() //前置增强
@AfterReturning() //后置增强
@Around() //环绕增强
@AfterThrowing(value或pointcut="",throwing="",...) //抛出异常后增强
@After() //最终增强

切点表达式抽取

  1. 写一个空方法,添加注解@Pointcut("execution(* com.package.*.*(..))")
1
2
3
4
5
public class MyAdvice{
@Pointcut("execution(* com.package.*.*(..))")
public void myPointcut();
...
}
  1. 使用类名.方法名()的语法引用切点表达式

“ ”

注意这种格式只是一种语法规范,而不是Java代码!

1
@Before("MyAdvice.myPointcut()") // <=> @Before("execution(* com.package.*.*(..))")

SpringBoot AOP实践

添加依赖

1
2
3
4
<dependency>
<groupld>org.springframework.boot</groupld>
<artifactld>spring-boot-starter-aop</artifactld>
</dependency>

拦截方法

方法一 声明自定义注解进行拦截

新建Annotation包,新建adminOnly接口

  • interface前添加@表明定义的是注解
  • 添加@annotation注解
  • 添加参数@Retention@Target
1
2
3
4
5
6
7
8
9
10
11
12
package com.mysqlapi.Annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) //指定了注解的生命周期在运行时
@Target(ElementType.METHOD)//指定了注解应用在方法上
public @interface adminOnly {

}

方法二 使用execution表达式拦截

语法格式

1
execution(修饰符pattern 返回值pattern 描述包名方法名(参数) 方法抛出异常pattern)

示例

1
2
3
4
5
6
7
8
9
@Pointcut("execution(public * com.example.controller.*Controller.*(..))")
public void match(){

}

@Before("match()")
public void before(){
// 前置通知...
}

切面的管理

新建Aspect包对切面进行统一管理,并新建CheckUserAscpect.java

  • @Aspect注解声明这个类是一个切面
  • @Component注解,将这个类标记为Spring容器中的一个Bean,这样就可以使用其它注解了(@Autowired
  • 使用CheckUserService.java这个类,实现逻辑处理
  • 编写切入点,@Pointcut("@annotation(com.mysqlapi.Annotation.adminOnly)"),其中参数用来指定在何处插入,此处表示在使用了(自定义)注解@adminOnly处切入,也可以使用execution表达式进行拦截
    • 切入点方法为checkAdmin()
  • 编写通知,决定了何时执行,以@Before通知为例,即在执行操作前,检查用户是否为管理员。@Before("checkAdmin()")其中的参数为切入点的方法
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
package com.mysqlapi.Aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.mysqlapi.service.CheckUserService;

@Aspect
@Component
public class CheckUserAscpect {
@Autowired
private CheckUserService checkUserService;

@Pointcut("@annotation(com.mysqlapi.Annotation.adminOnly)")
public void checkAdmin() {

}

@Before("checkAdmin()")
public void check() throws Exception {
checkUserService.check();
}
}

修改之前的权限验证

UserService.java

  • addUser()方法前,添加注解@adminOnly
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
package com.mysqlapi.Aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.mysqlapi.service.CheckUserService;

@Aspect
@Component
public class CheckUserAscpect {
@Autowired
private CheckUserService checkUserService;

@Pointcut("@annotation(com.mysqlapi.Annotation.adminOnly)")
// @Pointcut("execution(public * com.mysqlapi.service.*.*(..))")
public void checkAdmin() {

}

@Before("checkAdmin()")
public void check() throws Exception {
checkUserService.check();
}

// @Before("checkAdmin()")
// public void before(JoinPoint joinPoint){
// System.out.println("[前置通知]"+joinPoint);
// }

}

进行单元测试

UserServiceTest.java

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
package com.mysqlapi.service;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.mysqlapi.holder.CurrentUserHolder;

@RunWith(SpringRunner.class)
@SpringBootTest
class UserServiceTest2 {

@Autowired
private UserService userService;

@Test
void testFindAll() throws Exception {
CurrentUserHolder.set("sds");
userService.findAll();
}

@Test
void testAddUser() throws Exception {
CurrentUserHolder.set("sds");
userService.addUser();
}

}

同样可以实现,权限控制。这种添加自定义注解的方式,比在每有一个方法都要添加check()throws Exception要更加简洁,但还是需要对每个需要权限的方法添加注解。

image-20230313172948144

用execution表达式的单元测试

AspectTestUserService1.java <– 这个是service包中的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.mysqlapi.service;

import org.springframework.stereotype.Service;


@Service
public class AspectTestUserService1 {
public void findAll() {
System.out.println("AspectTestUserService1 查找成功!");
}
public void addUser() {
System.out.println("AspectTestUserService1 插入成功!");
}
}

AspectTestUserService1Test.java <– 单元测试文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.mysqlapi.service;

import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
class AspectTestUserService1Test {

@Autowired
private AspectTestUserService1 aspectTestUserService1;
@Test
void testFindAll() {
aspectTestUserService1.findAll();
}

@Test
void testAddUser() {
aspectTestUserService1.addUser();
}

}

excution表达式

语法格式

1
execution (* com.sample.service.impl..*.*(..))

整个表达式可以分为五个部分:

1、execution(): 表达式主体。

2、第一个号:表示返回类型,号表示所有的类型。

3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。

4、第二个号:表示类名,号表示所有的类。

5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。