面向切面跟自定义注解的结合
# 面向切面跟自定义注解的结合
# 面向切面的使用
关于面向切面是什么,这里不过多赘述了,直接上操作。不过,演示之前,还是想问一个问题,面相切面的原理是什么——动态代理,那动态代理的基础是什么——反射。
在 Spring Boot 应用中使用面向切面编程(AOP,Aspect-Oriented Programming)可以帮助你分离横切关注点(如日志记录、事务管理等),从而使业务逻辑代码更加简洁和清晰。下面通过一个简单的例子来演示如何在 Spring Boot 中配置和使用 AOP。
# 添加依赖
<dependencies>
<!-- 其他依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
2
3
4
5
6
7
# 目标类和定义切面
- 目标类
package com.example.demo.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
public void findUser() {
System.out.println("Finding user...");
// 模拟业务逻辑
}
}
2
3
4
5
6
7
8
9
10
11
12
- 定义切面
记得加入IOC容器,并标明这是一个切面类,就是加上@Aspect和@Component。
package com.example.demo.aspect;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.demo.service.UserService.findUser(..))")
public void beforeFindUser() {
System.out.println("Before finding user.");
}
@After("execution(* com.example.demo.service.UserService.findUser(..))")
public void afterFindUser() {
System.out.println("After finding user.");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 参数的意思
上述切面类中,@Before
注解是 Spring AOP 中用于定义前置通知(Advice)的注解,它允许你在目标方法执行之前插入额外的处理逻辑。至于括号中的内容, @Before("execution(* com.example.demo.service.UserService.findUser(..))")
则是一个切入点表达式(Pointcut Expression),这个表达式定义了哪些方法会被该前置通知拦截。
- 切入点表达式
在你的例子中,切入点表达式为 "execution(* com.example.demo.service.UserService.findUser(..))"
,下面是对这个表达式的详细解释:
- execution:这是最常用的切入点指示符之一,用于匹配方法执行连接点。换句话说,它帮助我们选择特定的方法调用作为切入点。
- (*):表示返回类型。星号(
*
)意味着任何返回类型都将被匹配。如果你只想针对特定返回类型的方法进行拦截,则可以在这里指定具体的类型,例如java.lang.String
。 - com.example.demo.service.UserService:指定了包含目标方法的类路径。在这个例子中,表示
UserService
类位于com.example.demo.service
包下。 - findUser:这是要匹配的方法名。如果你想对多个方法应用相同的切面逻辑,你可以使用通配符(如
find*
来匹配所有以find
开头的方法)。 - (..):圆括号内的双点号(
..
)表示任意数量和类型的参数。如果你想要精确匹配参数的数量和类型,可以直接在这里列出参数类型,比如(int, String)
表示只匹配有两个参数且第一个参数为int
,第二个为String
的方法。
# 测试
这个简单的例子配置好后,下面就开始简单测试啦
private final UserService userService;
@Test
public void run(String... args) throws Exception {
userService.findUser();
}
2
3
4
5
6
最后,你会发现它的输出有三行
Before finding user.
Finding user...
After finding user.
2
3
# 有参数的使用
# JoinPoint的使用
上述的例子很简单,面向切面一般用作参数补充或者业务逻辑补充,不过实际开发中,目标类中的某个方法基本都有参数,遇到这种情况,该怎么处理呢?如果想使用其中的参数该怎么办?或者说切面方法,怎么获得目标类的参数?
- 使用参数-JoinPoint joinPoint
在 Spring AOP 中,JoinPoint
是一个非常重要的接口,它提供了访问连接点(Join Point)信息的能力。连接点是指程序执行过程中明确的点,比如方法的调用或者异常的抛出等。当你使用 @Before
、@After
等注解定义通知(Advice)时,可以通过参数传递 JoinPoint
对象来获取关于当前拦截的方法的信息。
它的作用:
- 提供对当前拦截方法的元数据访问:包括方法签名、方法参数、目标对象等。
- 允许你动态地获取或修改某些行为:例如,可以检查传入的参数值,甚至可以在某些情况下改变它们(尽管直接修改参数的情况较少见)。
# 一个例子
以下是一个简单的用户实体类 User
和一个服务类 UserService
。
package com.example.demo.model;
public class User {
private Long id;
private String name;
// Constructors, getters and setters
public User(Long id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
以下是服务类
package com.example.demo.service;
import com.example.demo.model.User;
public class UserService {
public User findUserById(Long id) {
// 模拟从数据库查询用户
return new User(id, "John Doe");
}
}
2
3
4
5
6
7
8
9
10
11
最后是一个切面类 LoggingAspect
,它会在 UserService.findUserById
方法被调用之前记录相关信息。
package com.example.demo.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Before("execution(* com.example.demo.service.UserService.findUserById(..))")
public void logMethodCallDetails(JoinPoint joinPoint) {
// 获取方法签名
String methodName = joinPoint.getSignature().getName();
// 获取方法参数
Object[] args = joinPoint.getArgs();
StringBuilder params = new StringBuilder();
for (Object arg : args) {
params.append(arg).append(", ");
}
if (params.length() > 0) {
params.setLength(params.length() - 2); // 移除最后多余的逗号和空格
}
// 日志输出
logger.info("Calling method: {} with arguments: {}", methodName, params.toString());
}
}
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
在这个示例中,logMethodCallDetails
方法是一个前置通知,它会在 UserService.findUserById
方法执行前运行。通过 JoinPoint
对象,我们可以获取到当前被拦截的方法的名字 (methodName
) 及其参数 (args
)。然后,这些信息被格式化并记录下来。
# 参数介绍
下面对参数做进一步介绍:
Object[] args = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
2
3
4
joinPoint.getArgs()
:- 获取被拦截方法的所有参数(以数组形式返回)。
- 比如某个方法是:
public void saveUser(User user, String name)
,那么args[0]
是user
,args[1]
是"name"
。
joinPoint.getSignature()
:- 获取当前连接点的方法签名信息(如方法名、参数类型等)。
MethodSignature
:- 这是一个接口,扩展自
Signature
,专门用于表示方法级别的签名信息。 - 需要向下转型才能调用
.getMethod()
方法。
- 这是一个接口,扩展自
methodSignature.getMethod()
:- 获取当前被拦截的 Java 方法对象(
java.lang.Method
),可用于反射操作,比如获取注解、参数类型等。
- 获取当前被拦截的 Java 方法对象(
# 更广泛的定位
@Before("execution(* com.example.demo.service.UserService.findUserById(..))"),上述的例子中,是拦截具体的某个方法,不管其中传入的参数,只要名字符合即可。
那如何想一次性拦截多个方法呢?
可以使用以下的写法
@Pointcut("execution(* com.sky.mapper.*.*(..))")
public void autoFillPointCut(){
// *********
}
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
/////////////////////重要////////////////////////////////////
//可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解
log.info("开始进行公共字段自动填充...");
}
2
3
4
5
6
7
8
9
10
11
12
这个切入点表达式的意思是:“匹配 com.sky.mapper
包下的所有类
中的所有方法
”。
那如果你想拦截不同包下的不同方法呢?上述的这种方法似乎不太行。
要想实现上述目的,则可以考虑结合注解来实现。
# 结合注解使用切面
配合注解,将某个方法,加上注解后,比如加上某个自定义的注解,最后再在切面类,定义Pointcut时,加上注解定位即可,达到精准定位的目的,这样就实现了不同包,不同类,不同方法的
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知,在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
/////////////////////重要////////////////////////////////////
//可先进行调试,是否能进入该方法 提前在mapper方法添加AutoFill注解
log.info("开始进行公共字段自动填充...");
}
2
3
4
5
6
7
8
9
10
11
12
13
上述例子表示,,@annotation(com.example.annotation.AutoFill)
表示这个前置通知只会应用到那些被 @AutoFill
注解标记的方法上。同时,它又加上了&&
与——操作,则表示只有在,com.sky.mapper
包下且方法上有 自定义注解@AutoFill
的方法才行。
那方法中的属性怎么获取呢?
也很简单。
# 注解实例
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
2
3
4
5
6
- 获取注解属性
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充...");
//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
// 获取 注解
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
// 获取 注解的属性值
OperationType operationType = autoFill.value();//获得数据库操作类型
//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}
// *** 更多后续操作
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 多个属性的自定义注解
上述的方法是通过,JoinPoint,获得注解,但在 AOP 切面中更常用的方式是直接在通知(Advice)的方法签名中声明注解作为参数,这样 Spring AOP 会自动将匹配的方法上的注解实例传递给你的通知方法。
下面将展示如何做到这一点。
# 多属性例子
假设我们有一个带有多个属性的自定义注解 @OperationLog
,以及相应的业务逻辑和切面代码。
- 自定义多属性注解
package com.example.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 OperationLog {
String operationType(); // 操作类型
String description() default "No description"; // 描述,默认值为"No description"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
- 业务逻辑及接口实现
package com.example.service;
public interface UserService {
void createUser(String username);
}
package com.example.service.impl;
import com.example.annotation.OperationLog;
import com.example.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
@OperationLog(operationType = "CREATE", description = "Creating a new user")
public void createUser(String username) {
System.out.println("Creating user: " + username);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 使用 JoinPoint
和直接参数注入
在这个例子中,我们将演示两种方式来获取注解及其属性:
- 直接参数注入:这是推荐的方式。
- 通过
JoinPoint
获取
package com.example.aspect;
import com.example.annotation.OperationLog;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Component
public class OperationLogAspect {
// 直接参数注入的方式
@AfterReturning("@annotation(operationLog)")
public void logOperationWithDirectParameter(OperationLog operationLog) {
System.out.println("操作类型(直接参数): " + operationLog.operationType());
System.out.println("描述(直接参数): " + operationLog.description());
}
// 使用 JoinPoint 获取注解的方式
@AfterReturning("execution(* com.example.service..*(..))") // 更通用的切入点表达式
public void logOperationWithJoinPoint(JoinPoint joinPoint) throws NoSuchMethodException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 从方法中获取注解
OperationLog operationLog = method.getAnnotation(OperationLog.class);
if (operationLog != null) {
System.out.println("操作类型(JoinPoint): " + operationLog.operationType());
System.out.println("描述(JoinPoint): " + operationLog.description());
}
}
}
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
在上述代码片段中,@annotation(operationLog)
并没有显式地指定包名。这是因为我们在通知方法的参数列表中已经声明了 OperationLog operationLog
参数。Spring AOP 能够根据这个参数类型自动推断出对应的注解类型,因此不需要再在 @annotation
表达式中添加包名。
但是,如果你希望在切入点表达式中明确指定注解的全限定名,也可以这样做。例如:
@Before("@annotation(com.example.annotation.OperationLog)")
public void logOperation(OperationLog operationLog) {
// ...
}
2
3
4
# 注意-注解-全类名路径
有时候你发现不使用注解的全类名路径时,可能会报错,而有时候不会。
报错与否,完全取决你——是否要将,注解绑定到方法参数上。
如果你不绑定注解到方法参数上,可以在切点表达式中使用全类名路径(如 @annotation(com.sky.annotations.TestAnnotation))。 但如果你希望将注解实例绑定到方法参数上供后续使用,则不能直接在表达式中写全类名,而必须使用一个变量名(如 @annotation(testAnnotation)),Spring AOP 才能将其绑定到方法参数。
- 只做匹配,不绑定参数
@Before("execution(* com.sky.service.impl.*.*(..)) && @annotation(com.sky.annotations.TestAnnotation)")
public void autoFill(JoinPoint joinPoint) {
...
}
2
3
4
5
- 要做参数绑定,就必须用变量名
@Before("execution(* com.sky.service.impl.*.*(..)) && @annotation(testAnnotation)")
public void autoFill(JoinPoint joinPoint, TestAnnotation testAnnotation) {
...
}
2
3
4
5
最后还要提醒你是,为了避免命名冲突,还是建议你使用全类名路径。
- 如果项目中存在多个同名的自定义注解(例如
com.package1.TestAnnotation
和com.package2.TestAnnotation
),使用简写会导致 Spring 无法确定具体引用的是哪个注解,从而引发错误。 - 全限定类名可以唯一标识注解类型,避免歧义。