文章目录
什么是AOP切面编程
所谓AOP就是一种编程范例,我们一般做一个项目,会分为很多个模块,这些模块中有很多不同的功能,功能中有很多不同的切入点,举个例子,现在我们有一个保存用户业务逻辑功能,我们需要在保存用户之前输入一句话,这句话必须在保存用户之前做到,一般对我们springboot项目中,针对这样的情况,我们就会在Service层的代码中加入一行,如下图所示:
===================保存用户业务逻辑=================
用户业务
UserService
void save(User user)
void delete(Integer id);
User queryById(Integer id);
....
UserServiceImpl implement UserService
void save(User user){
sout("=============");//业务功能-1
...
userDao.save(user);
}
但是你想过没有,假设我们又要在删除用户的逻辑中也输出这样的一行内容呢?
我们是不是又要在业务逻辑层中又增加这样的一行呢?
void delete(Integer id){
sout("=============");//业务功能-1
......
userDao.delete(id);
}
那如果我们的其他模块的功能结构也需要这样这样的功能,那岂不是我们都要去加入这一行,这岂不是很麻烦吗?所以我们引入了AOP,我们能把这个要加入的功能,作为一个关注点切入进去,就不用交织在每个模块的代码中,而是作为一个单独的模块存在。
从这里我们就可以发现AOP解决了两个问题:一个是代码混乱,核心的业务逻辑代码还必须兼顾其他功能,这就导致不同功能的代码交织到一起,可读性很差。
第二,是代码分散问题。同一个功能代码分散在多个模块中,不易维护。
AOP中重要概念
切面
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
连接点
应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
总的来说,就是程序执行的某一刻的,在这个点可以做的额外操作。
通知
切面的工作被称之为通知。
切入点
切入点是用来描述连接点的,它决定了当前代码与连接点匹配
我们可以简单的把 AOP 理解为贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。
springboot的切面编程的步骤
引入切面编程依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
开发附加操作(在springboot项目新建config配置)
/**
* 自定义切面配置类
*/
@Configuration //代表这个类是spring的配置类
@Aspect //代表这个类是切面配置类
public class MyAspectConfig {
//切面Aspect=Advice附加操作+Pointcut切入点
@Before("execution(* com.demo.springbootaoptest.service.*.*(..))")//代表这是一个核心业务逻辑执行之前的附加操作,value属性表明切书店
public void before(JoinPoint joinPoint){
System.out.println("=========前置附加操作=========");
System.out.println("当前执行目标类: "+ joinPoint.getTarget());
System.out.println("当前执行目标类中方法: "+ joinPoint.getSignature().getName());
}
@After("execution(* com.demo.springbootaoptest.service.*.*(..))")
public void after(){
System.out.println("后置通知");
}
@Around("execution(* com.demo.springbootaoptest.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("==========进入环绕的前置操作===========");
System.out.println("当前执行类: "+proceedingJoinPoint.getTarget());
System.out.println("方法名: "+proceedingJoinPoint.getSignature().getName());
//放心目标方法执行
Object proceed = proceedingJoinPoint.proceed();//继续处理 业务逻辑方法执行
System.out.println("==========进入环绕的后置操作===========");
return null;
}
}
我对这上面的注解一一进行解释
- @Aspect 用来类上,代表这个类是一个切面
- @Before 用在方法上代表这个方法是一个前置通知方法
- @After 用在方法上代表这个方法是一个后置通知方法
- @Around 用在方法上代表这个方法是一个环绕的方法
另外这个方法是 上述 的 execution() 方法,用于匹配需要被 AOP 切入的方法。
execution() 方法的参数有以下几个:
"*" 表示匹配任意类型的方法。
com.demo.springbootaoptest.service 表示匹配 com.demo.springbootaoptest.service 包下的所有类。
.* 表示匹配所有类的所有方法。
(..) 表示匹配方法的任意参数。
service类
public class UserService {
public void save(String name){
// System.out.println("===========================");//=》额外操作
System.out.println("处理save核心业务逻辑,调用mapper");
}
public void delete(Integer id) {
System.out.println("===========================");
System.out.println("处理delete核心业务逻辑,调用mapper");
}
public void update(String name){
System.out.println("处理update核心业务逻辑,调用mapper");
}
public String find(String name){
System.out.println("处理find核心业务逻辑,调用mapper");
return name;
}
}
前置结果
后置结果
环绕结果
这里要特别解释一下环绕通知的流程
在方法执行前,打印一条日志,表示进入环绕的前置操作。
获取目标方法的执行类和方法名。
调用 proceed() 方法,让目标方法执行。
在方法执行后,打印一条日志,表示进入环绕的后置操作。
SpringBoot Aop 常用注解
常用注解
- @Aspect:用于定义切面
- @Before:通知方法会在目标方法调用之前执行
- @After:通知方法会在目标方法返回或抛出异常后执行
- @AfterReturning:通知方法会在目标方法返回后执行
- @AfterThrowing:通知方法会在目标方法抛出异常后执行
- @Around:通知方法会将目标方法封装起来
- @Pointcut:定义切点表达式
切点表达式
execution(方法修饰符 返回类型 方法所属的包.类名.方法名称(方法参数))
例子
在AspectJ中,execution
是一个常用的切入点表达式,用于匹配特定的方法执行。它可以根据方法的修饰符、返回类型、所属的包、类名以及方法名称等信息来定义切入点。以下是几个使用execution
表达式的例子,以及对它们的解释:
-
匹配特定类中的方法:
execution(* com.example.MyClass.myMethod(..))
这个表达式匹配
com.example
包中MyClass
类里的myMethod
方法。*
表示任何修饰符,..
表示任何参数。 -
匹配任何返回类型的方法:
execution(* *.methodName(..))
这里第一个
*
代表任何返回类型,第二个*
代表任何类,methodName
是你想要匹配的方法名。 -
匹配特定包及子包下所有类的方法:
execution(* com.example.*.*(..))
这个表达式匹配
com.example
包及其所有子包中所有类的方法。 -
匹配特定修饰符的方法:
execution(public * com.example.MyClass.myMethod(..))
这个表达式仅匹配公开(
public
)的myMethod
方法。 -
匹配特定参数类型的方法:
execution(* com.example.MyClass.myMethod(java.lang.String, int))
这个表达式匹配
MyClass
中接受String
和int
作为参数的myMethod
方法。 -
匹配继承自特定类的类中的方法:
execution(* com.example.*+.myMethod(..))
+
表示匹配继承自com.example
包中任何类的myMethod
方法。 -
匹配注解了特定注解的方法:
execution(@com.example.MyAnnotation * *(..))
这个表达式匹配任何被
@com.example.MyAnnotation
注解的方法。 -
匹配特定异常类型的抛出:
execution(* *.*(..) throws java.lang.Exception)
这个表达式匹配任何可能抛出
java.lang.Exception
或其子类异常的方法。 -
组合使用多个条件:
execution(public * com.example.service.*.*(..))
这个表达式匹配
com.example.service
包中公开的任何类的方法。
在AspectJ中,execution
表达式提供了强大的灵活性,允许你定义非常具体或非常广泛的切入点,以实现面向切面编程(AOP)的各种需求。
引入AOP切面编程的步骤
1、添加日志信息封装类WebLog
public class WebLog {
private final Long userId; // 用户ID
private final String username; // 用户名
private final String operation; // 操作
private final Long time; // 时间
// 构造函数
public WebLog(Long userId, String username, String operation, Long time) {
this.userId = userId;
this.username = username;
this.operation = operation;
this.time = time;
}
// Getter方法
public Long getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public String getOperation() {
return operation;
}
public Long getTime() {
return time;
}
// toString方法,用于日志输出
@Override
public String toString() {
return "WebLog{" +
"userId=" + userId +
", username='" + username + '\'' +
", operation='" + operation + '\'' +
", time=" + time +
'}';
}
}
2、添加切面类WebLogAspect
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect // 声明这是一个切面
@Component // 将切面类作为组件加入到Spring容器中
public class WebLogAspect {
@Pointcut("execution(* com.example.controller.*.*(..))") // 定义一个切入点,匹配com.example.controller包下所有类的任何方法
public void logForControllerPackage() {}
@Before("logForControllerPackage()") // 定义前置通知,在切入点执行前执行
public void beforeAdvice(JoinPoint joinPoint) {
// 从连接点中提取信息
String username = "user"; // 这里通常是从安全上下文中获取用户名
Long userId = 1L; // 这里通常是从安全上下文中获取用户ID
String operation = joinPoint.getSignature().getName(); // 获取方法名作为操作名称
Long time = System.currentTimeMillis(); // 获取当前时间
// 创建WebLog实例
WebLog webLog = new WebLog(userId, username, operation, time);
// 打印日志信息
System.out.println(webLog.toString());
}
}
3、测试类
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration // 声明这是一个配置类
@EnableAspectJAutoProxy // 启用对AspectJ自动代理的支持
public class AppConfig {
@Bean // 声明一个Bean
public WebLogAspect webLogAspect() {
return new WebLogAspect();
}
@Bean
public SomeController someController() {
return new SomeController();
}
public static void main(String[] args) {
// 创建Spring应用程序上下文
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(AppConfig.class);
context.refresh();
// 获取控制器Bean并执行一个动作
SomeController controller = context.getBean(SomeController.class);
controller.someMethod();
// 关闭上下文
context.close();
}
}
// 一个示例控制器,将被WebLogAspect记录日志
class SomeController {
public void someMethod() {
// 方法逻辑
}
}