如果将OOP(面向对象编程)看作是一层一层的模块,则AOP(面向切面编程)则是贯穿每一层的不同功能模块(每一层都涉及了这些功能模块),所以才叫作面向切面编程。
1.为什么需要 AOP?
日志记录、事务管理、性能监控等功能通常分散在多个类中,导致代码重复、难以维护。这些功能模块也被叫做横切关注点,而 AOP 允许我们将这些关注点集中到一个地方(切面),就相当于包装在一起,并自动织入到相关的业务逻辑中,使得代码更加简洁、可维护。
2.AOP 的核心概念
2.1 横切关注点
所谓“横切关注点”,就是那些跟核心业务逻辑没直接关系,但又贯穿多个模块的功能。(将这些功能被称作横切关注点)。
常见的横切关注点有如下这些:
- 日志记录:可以自动在每个方法调用前后记录日志,节省手动添加日志的工作。
- 事务管理:可以在数据库操作时,自动开启、提交或回滚事务。
- 性能监控:可以在方法执行前后记录时间,监控方法的执行性能。
2.2 切面
切面是横切关注点的模块化实现。它可以包含多个“通知”(Advice)和“切入点”(Pointcut),定义了在哪里以及如何实现横切逻辑。
具体例子
假设你有一个 UserService
类,专门处理用户注册、更新等操作。每次用户注册前,你想记录日志,表示“正在注册用户”。
不使用 AOP 的写法:
public class UserService {
public void registerUser(User user) {
// 日志记录
System.out.println("正在注册用户...");
// 核心业务逻辑
// 注册用户
}
}
使用 AOP 的写法:
@Service
public class UserService {
public void registerUser(User user) {
// 核心业务逻辑
// 注册用户
}
}
@Aspect
@Component
public class LoggingAspect {
// 定义一个切入点,匹配 UserService 的所有方法
@Pointcut("execution(* com.example.UserService.*(..))")
public void userServiceMethods() {}
// 前置通知:在方法执行前记录日志
@Before("userServiceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("正在注册用户...");
}
}
在这个例子里,LoggingAspect
就是一个切面,它的前置通知在 UserService
的方法执行前自动插入日志记录逻辑。这样,你的业务代码就干净了,不再需要手动写日志逻辑。
2.3 通知
通知定义了切面的具体行为,也就是在何时、何地、以何种方式对目标方法进行增强。根据增强发生的时机,通知分为:
- 前置通知(Before Advice):在方法调用前执行。
- 后置通知(After Advice):在方法调用后执行。
- 返回通知(After Returning Advice):在方法成功返回后执行。
- 异常通知(After Throwing Advice):在方法抛出异常后执行。
- 环绕通知(Around Advice):在方法执行的前后都执行,可以完全控制方法的执行过程。
2.4 切入点
在 Spring AOP 中,切入点(Pointcut)是用来定义你想要拦截哪些方法。
- 切入点决定了拦截点,即哪些方法会被增强(例如,哪些方法会被日志记录或事务管理等功能拦截)。
- 配合“通知”(Advice)一起工作,通知决定了在方法执行前、执行后或抛出异常时执行什么逻辑。
- 切入点:选择哪些方法需要“被拦截”。
- 通知:指定拦截后要做的事情。
2.5 连接点
连接点就是你业务逻辑层中可以插入横切关注点的位置(简单来说就是业务逻辑层中的方法),因为横切关注点的这些功能是需要在你调用业务逻辑层的方法伴随着进行。程序执行当中的一个方法调用、一个异常抛出都可以作为连接点。
2.6 织入
织入是将切面与目标对象(业务代码)结合的过程。在 Spring AOP 中,织入通常发生在运行时,通过动态代理来完成。代理对象在调用目标方法之前,会先执行切面中定义的通知逻辑(如记录日志),然后再执行实际的业务逻辑 。
3.AOP的具体实现
3.1 切面的定义
切面(Aspect) 是包含横切关注点逻辑的模块。它主要用于定义在哪些地方(切入点)应用哪些操作(通知)。在 Spring AOP 中,切面使用 @Aspect
注解来标记,并作为 Spring 管理的 Bean。
Spring AOP的自动代理:Spring AOP通过切面(Aspect)配置,会自动为目标Bean创建代理对象。在这种常见的场景下,开发者不需要手动操作,Spring会自动检测到切面和目标Bean之间的关系,并生成代理对象。
- 你只需定义一个切面(Aspect),比如
@Aspect
注解的类,Spring AOP会自动创建代理对象,拦截目标Bean的方法调用,并在合适的位置执行切面逻辑(例如@Before
、@After
等增强)。
示例:
@Aspect
@Component
public class LoggingAspect {
// 这是切入点和通知的具体实现
}
@Aspect
:用来定义一个切面类。@Component
:将这个切面类交给 Spring 容器管理。
关键点:
- 切面类一般使用
@Component
或者通过 XML 方式配置成 Spring Bean,这样 Spring 才能将其织入到业务逻辑中。 @Aspect
注解表明这是一个切面类,包含增强逻辑的通知(Advice)和定义切入点(Pointcut)。
3.2 切入点的定义
通过@Pointcut注解定义在哪些方法、类或特定条件下,AOP 的增强逻辑(即通知)应该被应用。
举例:
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
"execution(* com.example.service.UserService.*(..))" 是切入点表达式,用这个表达式规定了通知的使用范围(某些方法或某些类)
后面所跟着的public void userServiceMethods() {},则是这个你用切入点表达式规定的范围的标识符,可以把它看作是对切入点表达式的“命名”,从而在多个地方复用这个切入点表达式。
举例:
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceMethods() {}
// 复用切入点
@Before("userServiceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("执行方法之前:" + joinPoint.getSignature().getName());
}
在以上例子中,将"execution(* com.example.service.UserService.*(..))"这个范围命名为userServiceMethods(),则你在通知复用切入点的时候就可以直接用"userServiceMethods()"来代表 "execution(* com.example.service.UserService.*(..))"这复杂的一串了
3.3 切入点表达式的定义规则
关于注解以外的部分进行标记
3.3.1 execution
表达式
execution()
是 Spring AOP 中最常用的切入点表达式,用来匹配方法的执行。它可以精确地定义目标方法的签名,包括方法的访问修饰符、返回类型、类名、方法名和参数。
语法格式:
execution( [修饰符模式] 返回类型 [类全路径] . 方法名(参数列表) [异常模式] )
- 修饰符模式(可选):匹配方法的访问修饰符,如
public
、private
。如果不指定,则匹配所有修饰符。 - 返回类型:匹配方法的返回类型,可以是具体的类型(如
String
)或通配符*
(表示任意返回类型)。 - 类全路径(可选):可以是类的全路径(如
com.example.UserService
),也可以是通配符(如com.example..*
表示com.example
包及其子包的所有类)。 - 方法名:匹配具体的方法名或使用通配符
*
(表示任意方法)。 - 参数列表:匹配方法的参数,可以是具体的类型(如
(int, String)
),也可以是通配符..
(表示任意类型和数量的参数)。 - 异常模式(可选):匹配抛出的异常类型。
示例:
-
匹配
com.example.UserService
类中的所有方法,不论返回类型和参数是什么:execution(* com.example.UserService.*(..))
-
匹配
com.example.UserService
中public
修饰的所有方法:execution(public * com.example.UserService.*(..))
-
匹配
com.example.UserService
中所有返回类型为String
的方法:execution(String com.example.UserService.*(..))
-
匹配
com.example.UserService
中所有带有两个参数的方法(参数类型为int
和String
):execution(* com.example.UserService.*(int, String))
-
匹配
com.example.UserService
中所有无参数的方法:execution(* com.example.UserService.*())
-
匹配
com.example
包及其子包中的所有类的所有方法:execution(* com.example..*.*(..))
3.3.2 within
表达式
within()
用来匹配特定类或包中的所有方法,常用于指定切入点的作用范围(类或包)。
语法格式:
within(类全路径)
- 类全路径:可以是类的全路径(如
com.example.UserService
),也可以是通配符(如com.example..*
表示com.example
包及其子包的所有类)。
示例:
-
匹配
com.example.UserService
类中的所有方法:within(com.example.UserService)
-
匹配
com.example.service
包中的所有类的所有方法:within(com.example.service..*)
within()
和 execution()
都可以匹配类中的方法,不同之处在于:
execution()
可以精确匹配方法签名;within()
只能匹配类或包中的所有方法,不能细粒度地匹配特定方法。
3.3.3 args
表达式
args()
用来匹配方法的参数类型。它与 execution()
的参数部分类似,但 args()
可以在运行时获取参数的实际类型,而不是编译时确定的类型。
语法格式:
args(参数类型列表)
- 参数类型列表:匹配方法的参数,可以是具体的类型(如
(int, String)
),也可以是通配符..
(表示任意类型和数量的参数)。
示例:
-
匹配方法的第一个参数为
String
,且可以有任意数量的其他参数:args(String, ..)
-
匹配所有参数为
String
的方法:args(String)
-
匹配所有方法中至少有一个参数是
String
类型的情况:args(.., String, ..)
3.3.4 this
表达式
this()
用来匹配当前代理对象的类型。这个表达式常用于匹配代理类,而不是目标类的类型。
语法格式:
this(类型)
- 类型:代理对象的类型,可以是类或接口。
示例:
- 匹配所有代理对象实现了
UserService
接口的方法:this(com.example.service.UserService)
this()
使用的类型是代理对象的类型,而 target()
表达式使用的是目标对象的类型。
3.3.5 代理对象
关于this()中所用到的代理对象,这里需要作出说明:
1.当某个类没有实现任何接口就是个普通类,或者该类就是接口类的时候,这个类的代理对象就是自身。
2.当某个类实现了某个接口的时候,这个类的代理对象就是该接口类
举例:
假设我们有以下类和接口:
public interface UserService {
void registerUser(User user);
}
@Service
public class UserServiceImpl implements UserService {
public void registerUser(User user) {
System.out.println("用户注册:" + user.getName());
}
}
并且我们有一个切面:
@Aspect
@Component
public class LoggingAspect {
@Pointcut("this(com.example.service.UserService)")
public void proxyBasedMethods() {}
@Before("proxyBasedMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("调用方法前:" + joinPoint.getSignature().getName());
}
}
调用过程:
1.通过接口 UserService
调用:
@Autowired
private UserService userService;
userService.registerUser(new User("Alice"));
因为this()中的是com.example.service.UserService这个接口类,所以如果我们想使用增强逻辑(通知),就需要让某个依赖注入对象的代理对象为UserService这个接口类才可以。
这里进行依赖注入的时候,因为UserService本身就是个接口类,所以其代理对象还是其本身UserService,故这个地方在切入点的范围内。
2.直接通过 UserServiceImpl
调用
@Autowired
private UserServiceImpl userServiceImpl;
userServiceImpl.registerUser(new User("Alice"));
UserServiceImpl实现了UserService接口类,所以 UserServiceImpl的代理对象还是UserService接口类,故也在切入点定义的范围内。
3.3.6 target
表达式
target()
用来匹配目标对象的类型。它和 this()
类似,但 target()
匹配的是目标对象的类型,而不是代理对象。
当你通过接口类型(例如
UserService
)进行依赖注入时,Spring 会根据接口的实现类自动找到并注入相应的目标对象。目标对象的匹配刚好和代理对象反过来,代理对象的匹配是遇到接口的实现类,回头去找接口类,而目标对象的匹配则是遇到接口类,去找该接口类的实现类。
语法格式:
target(类型)
- 类型:目标对象的类型,可以是类或接口。
示例:
-
匹配所有目标对象实现了
UserService
接口的方法:target(com.example.service.UserService)
-
匹配目标对象是某个具体类的所有方法:
target(com.example.service.MyConcreteClass)
this()
通常填写接口类型,因为代理对象的类型是接口类型(尤其是在使用 JDK 动态代理时)。target()
通常填写实现类类型,因为目标对象的实际类型是实现类,而不管代理对象是什么类型。- 而如果是普通类的话,目标对象和代理对象都可以看作自身。
关于注解进行标记
3.3.7 @annotation
表达式
@annotation()
用来匹配标注了特定注解的方法。常用于筛选被某些注解标记的方法。
语法格式:
@annotation(注解类型)
- 注解类型:指定注解的全路径。
示例:
-
匹配所有标注了
@Transactional
注解的方法:@annotation(org.springframework.transaction.annotation.Transactional)
-
匹配标注了自定义注解
@MyCustomAnnotation
的方法:@annotation(com.example.annotation.MyCustomAnnotation)
3.3.8 @within
表达式
@within()
用来匹配标注了特定注解的类中的所有方法。
语法格式:
@within(注解类型)
- 注解类型:指定注解的全路径。
示例:
- 匹配所有被
@Service
注解标记的类中的方法:@within(org.springframework.stereotype.Service)
3.3.9 @target
表达式
@target()
用来匹配目标对象标注了特定注解的情况,作用与 @within()
类似,但它只针对目标对象而不是类本身。
语法格式:
@target(注解类型)
- 注解类型:指定注解的全路径。
示例:
- 匹配所有目标对象被
@Transactional
注解标记的类中的方法:@target(org.springframework.transaction.annotation.Transactional)
3.3.10 @args
表达式
@args()
用来匹配方法参数标注了特定注解的情况。
语法格式:
@args(注解类型)
- 注解类型:指定注解的全路径。
示例:
- 匹配方法参数被
@Valid
注解标记的方法:@args(javax.validation.Valid)
3.3.11 @within
与 within
的区别
-
@within
是用来匹配类级别的注解,而within
是基于包或类层级进行匹配。 -
例如:
within(com.example..*)
匹配com.example
包及其子包下的所有类。@within(org.springframework.stereotype.Service)
匹配被@Service
注解标记的所有类中的方法。
3.3.12 bean
表达式
bean()
用于匹配特定 Bean 名称的类中的方法。
语法格式:
bean(beanName)
- beanName:Bean 的名称。
示例:
- 匹配名称为
userService
的 Bean 的所有方法:bean(userService)
总结
- execution():最常用的表达式,匹配方法执行,精细化控制。
- within():匹配类或包中的所有方法。
- args():匹配方法参数的类型。
- this() 和 target():匹配代理对象或目标对象的类型。
- @annotation():匹配方法上带有特定注解。
- @within():匹配类上带有特定注解的情况。
- bean():匹配特定名称的 Spring Bean。
3.4 通知的定义与实现
切入点表达式后跟的函数可以看作是对切入点表达式的“命名”,但是通知后跟的函数是要在拦截调用后要执行的操作。
3.4.1 前置通知的实现
@Before("execution(* com.example.service.UserService.*(..))")
public void logBeforeMethod(JoinPoint joinPoint) {
System.out.println("执行方法之前:" + joinPoint.getSignature().getName());
}
@Before
:这是一个前置通知,表示在目标方法执行之前运行。JoinPoint
:提供对当前连接点的访问,例如获取方法名、参数等。
3.4.2 后置通知的实现
@After("execution(* com.example.service.UserService.*(..))")
public void logAfterMethod(JoinPoint joinPoint) {
System.out.println("方法执行结束:" + joinPoint.getSignature().getName());
}
@After
:无论方法是否正常结束,都会在方法执行后调用。
3.4.3 返回通知的实现
@AfterReturning(pointcut = "execution(* com.example.service.UserService.*(..))", returning = "result")
public void logAfterReturningMethod(JoinPoint joinPoint, Object result) {
System.out.println("方法返回值:" + result);
}
@AfterReturning
:仅在目标方法成功返回后才调用,并且可以获取返回值。returning = "result"
:指定返回值可以作为参数传递给通知。
3.4.4异常通知的实现
@AfterThrowing(pointcut = "execution(* com.example.service.UserService.*(..))", throwing = "error")
public void logAfterThrowingMethod(JoinPoint joinPoint, Throwable error) {
System.out.println("方法抛出异常:" + error);
}
@AfterThrowing
:在方法抛出异常后调用。throwing = "error"
:将异常对象作为参数传递给通知。
3.4.5 环绕通知的实现
@Around("execution(* com.example.service.UserService.*(..))")
public Object logAroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("方法执行之前:" + joinPoint.getSignature().getName());
Object result = joinPoint.proceed(); // 继续执行目标方法
System.out.println("方法执行之后:" + joinPoint.getSignature().getName());
return result;
}
@Around
:环绕通知既能控制方法执行前后,也可以决定是否执行目标方法。ProceedingJoinPoint
:它是JoinPoint
的子类,可以通过proceed()
方法调用目标方法。
- 以上代码中的
joinPoint是ProceedingJoinPoint joinPoint中的,故还是ProceedingJoinPoint类型,而并非是JoinPoint
类型,故具有proceed()
方法控制目标方法的执行。joinPoint.proceed();
调用实际的目标方法,执行业务逻辑。proceed()
方法返回的类型是Object
,它是目标方法的返回值。Object result = joinPoint.proceed();
的写法用于捕获目标方法的返回值,这样可以对返回值进行处理、修改或记录。- 直接使用
joinPoint.proceed();
适合不需要处理返回值的场景,但在大多数情况下,捕获返回值会更灵活和实用。
3.5 通知中方法使用的参数JoinPoint
和 ProceedingJoinPoint
3.5.1 JoinPoint
的常用方法
方法 | 说明 |
---|---|
getArgs() | 获取目标方法的参数,返回一个 Object[] 数组。 |
getSignature() | 获取目标方法的签名(包括方法名、返回类型、参数类型等信息)。 |
getTarget() | 获取目标对象,即实际被代理的对象(目标类的实例)。 |
getThis() | 获取代理对象(代理类实例)。 |
toString() | 返回当前连接点的字符串表示。 |
Method method = ((MethodSignature) point.getSignature()).getMethod();
MethodSignature
是Signature
的子接口(父类可以强制转换为子类,父接口可以强制转换为子接口),专门描述方法级别的信息,扩展了与方法相关的细节功能。它提供了以下核心方法:
getMethod()
:
- 返回
java.lang.reflect.Method
对象,这是反射机制的核心类。- 通过
Method
对象,可以操作目标类中的方法,例如:
- 获取方法的注解。
- 调用目标方法。
- 查看方法的名称、参数类型、返回值类型等。
getParameterTypes()
:
- 返回方法的参数类型数组。
getReturnType()
:
- 返回方法的返回值类型。
3.5.2 ProceedingJoinPoint
的常用方法(继承自 JoinPoint
,用于 @Around
通知)
方法 | 说明 |
---|---|
proceed() | 执行目标方法。 |
proceed(Object[] args) | 执行目标方法,并传入新的参数。 |
getArgs() | 获取目标方法的参数,返回一个 Object[] 数组(继承自 JoinPoint )。 |
getSignature() | 获取目标方法的签名(继承自 JoinPoint )。 |
getTarget() | 获取目标对象(继承自 JoinPoint )。 |
getThis() | 获取代理对象(继承自 JoinPoint )。 |
JoinPoint
提供了获取目标方法、参数、目标对象和代理对象的功能。ProceedingJoinPoint
继承自JoinPoint
,除了JoinPoint
的功能外,ProceedingJoinPoint
还可以通过proceed()
控制目标方法的执行,允许修改方法的参数和返回值,通常用于环绕通知(@Around
)。
3.5.3 具体使用例子
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.UserService.*(..))")
public void logBeforeMethod(JoinPoint joinPoint) {
System.out.println("开始执行方法: " + joinPoint.getSignature().getName());
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println("方法参数:" + arg);
}
System.out.println("目标对象:" + joinPoint.getTarget());
System.out.println("代理对象:" + joinPoint.getThis());
}
}
JoinPoint
在上面的通知方法中提供了以下信息:
joinPoint.getSignature().getName()
:获取被调用方法的名称。joinPoint.getArgs()
:获取被调用方法的参数,返回一个Object[]
数组。joinPoint.getTarget()
:获取被代理的目标对象,即执行目标方法的实际对象(目标类的实例)。joinPoint.getThis()
:获取当前的代理对象(即 Spring AOP 生成的代理类)。
输出示例:
如果我们有一个 UserService
类如下:
@Service
public class UserService {
public void registerUser(String userName) {
System.out.println("用户注册: " + userName);
}
}
然后调用:
userService.registerUser("Alice");
输出结果可能是:
开始执行方法: registerUser
方法参数:Alice
目标对象:com.example.service.UserService@123abc
代理对象:com.example.service.UserService$$EnhancerBySpringCGLIB$$123abc
4.五个横切关注点的实现
4.1 日志记录
在 Spring AOP 中实现日志记录功能,可以让开发者通过切面(Aspect)来记录应用程序中的方法调用信息,例如方法名、参数、返回值、异常等,而不需要手动在每个方法中编写日志代码。
4.1.1 引入依赖
Spring Boot 默认包含日志框架,不需要额外配置。如果你是传统项目,可以添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
4.1.2 实现日志切面
创建一个 AOP 切面类,用于记录方法执行前后的日志。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
// 定义切入点:匹配 service 包下的所有方法
@Before("execution(* com.example.service.*.*(..))")
public void logBeforeMethod(JoinPoint joinPoint) {
logger.info("开始执行方法: {}", joinPoint.getSignature().getName());
}
@After("execution(* com.example.service.*.*(..))")
public void logAfterMethod(JoinPoint joinPoint) {
logger.info("方法执行结束: {}", joinPoint.getSignature().getName());
}
}
详细解释:
Logger logger
:创建一个日志记录器,用来记录日志。LoggerFactory.getLogger(LoggingAspect.class)
返回一个与LoggingAspect
类绑定的日志记录器。
logger.info("开始执行方法: {}", joinPoint.getSignature().getName());
:使用日志记录器记录日志,输出目标方法的名称。joinPoint.getSignature().getName()
返回被调用方法的名称。
4.1.3 配置日志存储
在 application.properties
中,配置日志输出到控制台或文件。
# 输出日志到文件
logging.file.name=logs/application.log
logging.level.root=INFO
logging.file.name=logs/application.log
:将日志输出到文件,文件路径为logs/application.log
。日志文件存储在项目的logs
目录下。logging.level.root=INFO
:设置全局日志级别为INFO
。这意味着,日志级别低于INFO
的日志(如DEBUG
、TRACE
)将不会被记录,而INFO
、WARN
、ERROR
级别的日志会被记录。
4.2 事务管理
Spring AOP 提供了声明式事务管理,只需使用 @Transactional
注解,Spring AOP 会自动处理事务的开启、提交和回滚。
实现示例:
@Service
public class UserService {
@Transactional
public void createUser(User user) {
// 业务逻辑:插入用户数据到数据库
}
}
工作原理:
@Transactional
:Spring AOP 在方法调用时,自动开启事务,方法执行完毕后提交事务。如果方法抛出异常,事务将回滚。
4.3 性能监控
4.3.1 实现性能监控的基本步骤
- 创建一个 AOP 切面类:通过
@Aspect
和@Around
注解,围绕目标方法计算方法的执行时间。 - 记录方法的执行时间:在方法执行前记录开始时间,方法执行后记录结束时间,计算差值就是方法的执行时间。
- 将执行时间输出到日志:使用日志记录器将方法的执行时间记录到日志中,方便后续分析和调试。
4.3.2 具体实现步骤
4.3.2.1 配置日志框架
为了将监控的性能数据记录到日志中,我们首先需要确保已经配置了日志框架,例如 Logback 或 Log4j。在 Spring Boot 中,默认会使用 Logback,因此我们不需要手动添加额外的依赖。
4.3.2.2 创建 AOP 切面类
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class PerformanceMonitorAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitorAspect.class);
// 定义切入点,匹配 com.example.service 包下所有类的所有方法
@Around("execution(* com.example.service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis(); // 记录方法开始时间
// 执行目标方法
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start; // 计算方法执行时间
logger.info("方法 {} 执行时间: {} 毫秒", joinPoint.getSignature(), executionTime);
return result; // 返回方法的执行结果
}
}
代码解释
@Aspect
:标记该类为切面类,表明该类中包含横切逻辑(性能监控)。@Component
:将切面类注册为 Spring 管理的 Bean,使其可以参与 AOP 切面。@Around("execution(* com.example.service.*.*(..))")
:定义切入点,匹配com.example.service
包下所有类中的所有方法。@Around
表示环绕通知,允许在目标方法执行的前后加入逻辑。ProceedingJoinPoint joinPoint
:表示当前的目标方法,通过它可以获取方法签名、参数等信息,并执行目标方法。long start = System.currentTimeMillis()
:记录方法开始的时间(以毫秒为单位)。joinPoint.proceed()
:执行目标方法,返回目标方法的执行结果。long executionTime = System.currentTimeMillis() - start
:计算方法执行的总耗时。logger.info
:将方法的执行时间记录到日志中。
4.3.2.3 如何应用到具体的方法
假设我们有一个 UserService
类,其中包含方法 getUserDetails
。AOP 切面将自动拦截并监控其性能:
@Service
public class UserService {
public String getUserDetails(String userId) {
// 模拟业务逻辑
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
e.printStackTrace();
}
return "用户详情: " + userId;
}
}
当调用 UserService.getUserDetails
时,PerformanceMonitorAspect
切面将自动记录该方法的执行时间。
4.3.2.4 测试结果输出
当 getUserDetails
方法被调用时,日志将输出类似的信息:
INFO - 方法 public java.lang.String com.example.service.UserService.getUserDetails(java.lang.String) 执行时间: 102 毫秒
4.3.3 自定义性能阈值与报警
你可以进一步优化切面,设置一个性能阈值,当方法执行时间超过某个值时输出警告日志,提醒开发人员关注性能问题。
实现自定义性能阈值
@Aspect
@Component
public class PerformanceMonitorAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitorAspect.class);
// 设置一个阈值,超过这个时间就记录警告日志(以毫秒为单位)
private static final long THRESHOLD = 500;
@Around("execution(* com.example.service.*.*(..))")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 执行目标方法
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
if (executionTime > THRESHOLD) {
logger.warn("方法 {} 执行时间: {} 毫秒,超过阈值 {} 毫秒", joinPoint.getSignature(), executionTime, THRESHOLD);
} else {
logger.info("方法 {} 执行时间: {} 毫秒", joinPoint.getSignature(), executionTime);
}
return result;
}
}
解释:
THRESHOLD
:设置性能阈值为 500 毫秒。如果方法执行时间超过这个阈值,将记录警告日志(logger.warn
),否则记录普通信息日志。- 当某个方法执行时间超过设定的阈值时,会输出类似以下的警告信息:
WARN - 方法 public java.lang.String com.example.service.UserService.getUserDetails(java.lang.String) 执行时间: 800 毫秒,超过阈值 500 毫秒
4.3.4 日志配置
可以通过 application.properties
或 application.yml
文件配置日志的存储位置、格式等信息。
在 application.properties
中配置日志文件和日志级别:
# 将日志输出到 logs/application.log 文件中
logging.file.name=logs/application.log
# 设置日志级别,INFO 级别及以上的日志会被记录
logging.level.root=INFO
logging.level.com.example=DEBUG
4.3.5 扩展性能监控的功能
使用注解实现更精细的性能监控
你可以通过自定义注解来标注需要监控的方法,避免对所有方法都进行性能监控。例如:
4.3.5.1 定义一个注解 @MonitorPerformance
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MonitorPerformance {
}
public @interface MonitorPerformance {}
:这定义了一个名为MonitorPerformance
的注解。注解的定义类似于接口声明,使用@interface
关键字创建。- 这个注解目前是空的,没有任何属性或方法,它的主要作用是作为标记,可以通过反射或 AOP 等机制识别并应用特定的逻辑(如性能监控)。
-
@Retention
是元注解(Meta-Annotation),它定义了注解的保留策略。注解保留策略决定了注解在生命周期的哪个阶段对其进行保留。 -
RetentionPolicy.RUNTIME
:表示注解会保留到运行时,因此可以通过反射机制在运行时读取注解。大多数与 AOP 或反射相关的注解都需要RUNTIME
级别的保留策略,因为它们通常用于运行时动态处理逻辑。
-
@Target
也是元注解,它定义了注解可以应用的程序元素类型。 -
ElementType.METHOD
:表示这个注解只能应用在方法上。它限制了MonitorPerformance
注解的使用范围,确保它只能标注方法,不能标注类、字段、构造器等其他程序元素。
4.3.5.2 在切面中使用注解作为切入点
@Aspect
@Component
public class PerformanceMonitorAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitorAspect.class);
@Around("@annotation(com.example.annotations.MonitorPerformance)")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - start;
logger.info("方法 {} 执行时间: {} 毫秒", joinPoint.getSignature(), executionTime);
return result;
}
}
上面所定义的注解在这段代码里只出现在了@Around("@annotation(com.example.annotations.MonitorPerformance)") 这堆里,而后面的函数public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {}则跟你所定义的注解无关,只是名字起的很像而已。
4.3.5.3 在具体方法上使用 @MonitorPerformance
注解
@Service
public class UserService {
@MonitorPerformance
public String getUserDetails(String userId) {
// 模拟耗时业务逻辑
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "用户详情: " + userId;
}
}
这样,只有带有 @MonitorPerformance
注解的方法会进行性能监控。
- 使用 AOP 进行性能监控:通过
@Around
注解和ProceedingJoinPoint
,可以在不修改业务逻辑的情况下,监控方法的执行时间。- 日志记录执行时间:将方法执行的时间记录到日志中,帮助开发者发现性能瓶颈。
- 阈值报警:可以通过设置执行时间的阈值,在执行时间过长时发出警告。
- 扩展性:通过自定义注解,可以灵活地对特定的方法进行性能监控,而不必对所有方法进行监控。
5.通过自定义权限注解与AOP切面实现权限校验
5.1.自定义注解 @HasPermission
@HasPermission
注解用于标识哪些方法需要进行权限校验。在注解中通过value()
属性定义了所需的权限标识(如user:edit
、user:list
),这样可以在AOP切面中根据该权限标识来进行校验。这个注解本身并不提供权限校验的逻辑,它只是一个标记,表明某个方法需要进行权限检查。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HasPermission {
String value(); // 权限标识
}
@Target
:表示该注解可以应用在类和方法上。@Retention
:表示注解会在运行时保留,便于反射获取。value
:权限标识,如"user:edit"
。
5.2.AOP切面类(PermissionAspect
)
AOP(面向切面编程)通过@Aspect
和@Around
注解定义切面。PermissionAspect
切面类会拦截所有带有@HasPermission
注解的方法,提取权限标识,然后根据当前用户是否拥有该权限来决定是否放行方法的执行。
@Aspect
@Component
@Slf4j
public class PermissionAspect {
@Autowired
private RemotePermissionService permissionClient;
@Around("@annotation(com.example.auth.annotation.HasPermission)")
public Object around(ProceedingJoinPoint point) throws Throwable {
// 提取方法上的注解和权限标识
Signature signature = point.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
HasPermission annotation = method.getAnnotation(HasPermission.class);
String requiredPermission = annotation.value();
// 校验权限
if (hasPermission(requiredPermission)) {
return point.proceed(); // 放行方法执行
} else {
throw new ForbiddenException(); // 权限不足,抛出异常
}
}
private boolean hasPermission(String requiredPermission) {
HttpServletRequest request = ServletUtils.getRequest();
String tmpUserKey = request.getHeader(Constants.CURRENT_USER_ID);
if (Optional.ofNullable(tmpUserKey).isPresent()) {
Long userId = Long.valueOf(tmpUserKey);
log.debug("userid:{}", userId);
// 通过远程服务获取用户权限并校验
return permissionClient.getPermissionsByUserId(userId).stream()
.anyMatch(requiredPermission::equals);
}
return false; // 无用户信息,拒绝访问
}
}
@Around
:表示在目标方法执行之前和之后进行拦截和处理。方法中的point.proceed()
会执行目标方法,hasPermission
方法负责校验用户权限。hasPermission
方法:从请求头获取当前用户的ID,然后通过RemotePermissionService
远程调用获取该用户的权限集合,再根据requiredPermission
(注解中指定的权限标识)校验是否拥有权限。如果用户没有权限,则抛出ForbiddenException
。- 远程服务:
permissionClient.getPermissionsByUserId(userId)
调用了一个远程服务,假设该服务会返回当前用户的所有权限,并通过stream().anyMatch()
检查当前用户是否有足够的权限。
5.3.远程权限服务接口(Feign)
RemotePermissionService
通过Feign与远程权限服务通信,获取指定用户的权限集。Feign是Spring Cloud提供的一种声明式HTTP客户端,它简化了远程调用的过程。
@FeignClient(name = ServiceNameConstants.PERMISSION_SERVICE, fallbackFactory = RemotePermissionFallbackFactory.class)
public interface RemotePermissionService {
@GetMapping("permission/perms/{userId}")
public Set<String> getPermissionsByUserId(@PathVariable("userId") Long userId);
}
- Feign客户端:声明了一个接口与
permissionService
进行交互,getPermissionsByUserId
方法返回指定用户的权限集。 - 回退工厂:使用
fallbackFactory
来处理权限服务不可用时的容错逻辑。
5.4.业务方法中的注解应用
在实际的业务方法中,使用@HasPermission
注解标注需要进行权限校验的方法,Spring会通过AOP拦截这些方法,执行权限校验逻辑。
@HasPermission("user:list")
@GetMapping("/users")
public String listUsers() {
return "用户列表信息";
}
@HasPermission("user:edit")
@GetMapping("/user/edit")
public String editUser() {
return "用户编辑操作执行成功";
}
@HasPermission("user:list")
:表示listUsers
方法需要检查用户是否具备"user:list"
权限。@HasPermission("user:edit")
:表示editUser
方法需要检查用户是否具备"user:edit"
权限。
5.5.切面中获取控制层方法的参数并校验
@Target(ElementType.METHOD) // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface CheckDownload {
String resourceIdParam() default "resourceId"; // 方法参数中资料ID的参数名
}
package com.aqian.niubi.aspect;
import com.aqian.niubi.annotation.CheckDownload;
import com.aqian.niubi.service.ResourceDownloadService;
import com.aqian.niubi.util.SecurityContextUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.lang.reflect.Parameter;
@Aspect
@Component
public class DownloadCheckAspect {
@Autowired
private ResourceDownloadService resourceDownloadService;
/**
* 拦截被 @CheckDownload 注解标记的方法
*/
@Around("@annotation(checkDownload)")
public Object checkDownloadPermission(ProceedingJoinPoint joinPoint, CheckDownload checkDownload) throws Throwable {
// 1. 获取当前用户ID(需结合项目的认证机制)
Integer userId = SecurityContextUtil.getUserId();
if (userId == null) {
throw new RuntimeException("用户未登录");
}
// 2. 从方法参数中提取资料ID
Long resourceId = Long.valueOf(extractResourceId(joinPoint, checkDownload.resourceIdParam()));
if (resourceId == null) {
throw new IllegalArgumentException("未找到资料ID参数");
}
// 3. 检查用户是否下载过该资料
if (!resourceDownloadService.query()
.eq("user_id", userId)
.eq("resource_id", resourceId)
.exists()
) {
throw new RuntimeException("用户未下载该资料,无法操作");
}
// 4. 执行原方法(校验通过)
return joinPoint.proceed();
}
/**
* 从方法参数中提取资料ID
*/
private String extractResourceId(ProceedingJoinPoint joinPoint, String paramName) {
// 获取被拦截方法的签名信息(方法名、参数类型等)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取方法的所有参数定义(Parameter 对象数组)
Parameter[] parameters = signature.getMethod().getParameters();
// 获取方法调用时的实际参数值(Object 数组)
Object[] args = joinPoint.getArgs();
// 遍历所有参数,找到名称匹配的参数的索引
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].getName().equals(paramName)) {
// 返回对应参数的值(转换为字符串)
return args[i].toString();
}
}
return null;
}
}
参数名获取的限制
Java 默认编译时不会保留方法参数名称(如 resourceId
),而是使用 arg0
、arg1
这样的占位符。例如,以下方法:
public void addComment(String resourceId, String content) { }
编译后的参数名可能是 arg0
和 arg1
,而非 resourceId
和 content
。
因此,parameters[i].getName()
可能无法正确获取参数名,导致 extractResourceId
方法失效。
解决方案
-
编译时保留参数名
在 Maven 或 Gradle 中启用-parameters
编译选项,强制保留参数名。
Maven 配置示例:<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <compilerArgs> <arg>-parameters</arg> </compilerArgs> </configuration> </plugin>
-
使用注解指定参数名
通过@Param
或 Spring 的@RequestParam
显式指定参数名:public void addComment(@RequestParam("resourceId") String resourceId, String content) { ... }
控制层方法
@CheckDownload(resourceIdParam = "resourceId")
public void addComment(String resourceId, String content) {
System.out.println("用户成功评论资料:" + resourceId);
}
@RequireDownload(resourceIdParam = "resourceId")
→ 这个注解的resourceIdParam()
方法会返回 "resourceId" (字符串 "resourceId")。- Long resourceId = Long.valueOf(extractResourceId(joinPoint, checkDownload.resourceIdParam()));的checkDownload.resourceIdParam()就会是resourceId。
工作流程
(1)标记方法
@CheckDownload(resourceIdParam = "resourceId")
表示该方法需要校验用户是否下载过资料。
resourceIdParam
指定了方法参数中资料ID的参数名为 resourceId
。
(2)AOP 拦截
当调用 addComment
方法时,AOP 切面 DownloadCheckAspect
会拦截该方法,并执行以下操作:
-
步骤 1:获取当前用户ID(需结合项目认证机制,如 Spring Security)。
-
步骤 2:调用
extractResourceId
方法,从addComment
的参数中提取resourceId
的值。 -
步骤 3:检查用户是否下载过该资料(调用
downloadRecordService.hasDownloaded
)。 -
步骤 4:如果未下载,抛出异常;如果已下载,继续执行原方法。
6. JDK动态代理 和 CGLIB动态代理
6.1 JDK动态代理
通过创建一个实现了InvocationHandler
接口的处理器(如MyHandler
),在invoke
方法中定义代理对象在调用目标方法前后的增强逻辑。
public class MyHandler implements InvocationHandler {
private Object target; // 被代理的目标对象
public MyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法调用前:准备执行 " + method.getName());
Object result = method.invoke(target, args); // 调用真正的方法
System.out.println("方法调用后:执行完成 " + method.getName());
return result;
}
}
你在MyHandler
的invoke
方法里,可以写任何逻辑。
-
方法调用前做日志打印。
-
方法调用后做性能统计。
-
方法出异常时做统一处理。
然后由JDK在运行时动态生成一个新的代理类,这个代理类实现目标对象的接口,并在接口方法中统一调用MyHandler
的invoke
方法,从而实现方法拦截与增强。
public class $Proxy0 implements UserService {
private InvocationHandler handler;
public $Proxy0(InvocationHandler handler) {
this.handler = handler;
}
@Override
public void addUser() {
// 每个接口方法内部,都是调用handler.invoke()
handler.invoke(this, UserService.class.getMethod("addUser"), null);
}
}
JDK运行时自动帮你生成一个新类,这个类:
-
实现了你定义的接口(比如UserService)
-
内部持有MyHandler(也就是你自定义的InvocationHandler)
-
接口里的每个方法都被自动代理到MyHandler的invoke方法
6.2 CGLIB动态代理
通过生成目标类的子类,在子类中重写目标类的非final方法,在方法执行前后插入自定义的增强逻辑。CGLIB通过继承方式完成代理,因此不依赖接口,即使目标类没有实现接口也可以进行代理。
public class OrderService {
public void createOrder() {
System.out.println("创建订单");
}
}
CGLIB生成的子类大致像这样(伪代码):
public class OrderService$$EnhancerByCGLIB$$xxx extends OrderService {
@Override
public void createOrder() {
System.out.println("方法执行前打印日志");
super.createOrder(); // 调用原来的父类方法
System.out.println("方法执行后打印日志");
}
}
-
CGLIB代理是在运行时,动态生成一个子类。
-
这个子类会继承目标类,然后重写目标类的所有非final方法。
-
在重写的方法里,会:
-
方法调用前加增强逻辑
-
调用
super.原方法()
执行原来的业务逻辑 -
方法调用后继续加增强逻辑
-
6.3 注意事项
AOP的底层是通过生成代理对象来实现拦截增强的。
调用路径要经过代理对象,AOP增强逻辑才能生效!
如果直接在自己类内部调用另一个方法,就不会走代理对象,而是直接调用自己本身的方法,所以不会触发AOP拦截。因为Spring创建的代理对象,本质上是:
只有外部对象调用代理对象时,才会拦截,动态织入增强逻辑。
但是对象自己内部的方法调用,实际上是直接
this.xxx()
,根本绕过了代理对象。所以,Spring无法感知到内部调用,也就没办法织入AOP逻辑。
如果想拦截接口的实现类中独立存在的方法(接口中没有定义),则JDK动态代理就会出现问题,因为JDK动态代理只能拦截接口中存在的方法,虽然CGLIB动态代理可以解决这个问题但是只要类实现了接口,Spring优先使用JDK动态代理,所以这里要强制使用CGLIB动态代理。
注意点 说明 final类不能被代理 CGLIB代理是通过继承,final类不能被继承 final方法不能被拦截 CGLIB只能拦截可以被重写的方法,final方法不会被拦截 在你的启动类或者AOP配置类上,加一句
@EnableAspectJAutoProxy(proxyTargetClass = true)
,
就可以让Spring强制所有代理都用CGLIB,不再看接口有没有!
目标类情况 | Spring默认选择 |
---|---|
只要实现了接口 | 默认用 JDK动态代理 |
没有实现任何接口 | 才用 CGLIB代理 |
注意
-
即使类中还有额外的方法(即接口里没有声明的方法),
-
只要你实现了接口,Spring默认依然选择JDK动态代理!
如果你用默认JDK代理:
-
只能拦截接口定义的方法。
-
类自己额外的方法,不会被AOP拦截!(即使加了@Around @Before也无效)
如果你设置了强制CGLIB代理:
-
拦截所有非final方法。
-
包括接口方法+自己扩展的方法,都可以AOP增强。
比如有一个接口和实现:
public interface UserService {
void addUser();
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser() {
System.out.println("新增用户");
}
public void deleteUser() {
System.out.println("删除用户");
}
}
然后你有一个切面:
@Aspect
@Component
public class LogAspect {
@Before("execution(* com.example.UserService.addUser(..))")
public void beforeAddUser() {
System.out.println("before addUser");
}
@Before("execution(* com.example.UserServiceImpl.deleteUser(..))")
public void beforeDeleteUser() {
System.out.println("before deleteUser");
}
}
如果是默认JDK代理情况(proxyTargetClass=false):
-
addUser()
能被拦截(因为接口有) -
deleteUser()
拦截不到(因为JDK代理类里压根没有deleteUser方法)
如果是强制CGLIB代理(proxyTargetClass=true):
-
addUser()
能拦截 -
deleteUser()
也能拦截!