@Aspect的使用
配置
要启用 Spring AOP 和 @Aspect 注解,需要在 Spring 配置中启用 AspectJ 自动代理,但是在 Spring Boot 中,通常不需要显式地添加 @EnableAspectJAutoProxy,因为 Spring Boot 的自动配置特性已经包含了这一设置。:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// ...
}
@Aspect 注解也要写@Component,或通过其他方式注册为 Spring Bean,以确保 Spring 容器能够识别并管理这个切面。
@Aspect
@Component
public class LoggingAspect {
}
作用
定义切面(Aspect):
切面是跨多个类或对象的横切关注点的模块化。在 Spring AOP 中,切面是通过使用 @Aspect 注解的类来表示的。
切入点(Pointcut):
使用 @Pointcut 注解来定义切入点表达式,指定切面应用的位置。切入点定义了切面应该在何处插入其横切逻辑,即切面应该应用的连接点(如方法执行)的集合。
通知(Advice):
前置通知(Before advice):在某连接点(方法执行等)之前执行,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
后置通知(After advice):在某连接点之后执行,无论该连接点是正常完成还是抛出异常。
返回后通知(After-returning advice):在某连接点正常完成后执行。
抛出异常后通知(After-throwing advice):如果方法抛出异常退出,则执行通知。
环绕通知(Around advice):在方法调用之前和之后执行,它将决定是否继续执行连接点或直接返回自己的返回值或抛出异常。
通知相关的注解
@Around:环绕增强: 就是既可以前置增强,也可以后置增强。环绕通知会影响到AfterThrowing通知的运行,不要同时使用。
@Before:标识一个前置增强方法,
@AfterReturning:后置增强,如无返回结果,此注解不会执行
@After:final增强,不管是抛出异常或者正常退出都会执行
@AfterThrowing: 异常抛出增强
例子
假设我们有一个服务类 SampleService,我们想在其方法执行的不同阶段添加日志。
public class SampleService {
public void performAction() {
System.out.println("Performing action in SampleService");
}
}
现在,我们定义一个切面 LoggingAspect 来添加日志:
@Aspect
@Component
public class LoggingAspect {
// 前置通知:在方法执行之前执行
@Before("execution(* SampleService.performAction(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before executing: " + joinPoint.getSignature().getName());
}
// 后置通知:在方法执行之后执行(无论是否发生异常)
@After("execution(* SampleService.performAction(..))")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After executing: " + joinPoint.getSignature().getName());
}
// 返回后通知:在方法成功执行之后执行
@AfterReturning("execution(* SampleService.performAction(..))")
public void logAfterReturning(JoinPoint joinPoint) {
System.out.println("After returning from: " + joinPoint.getSignature().getName());
}
// 异常后通知:在方法抛出异常后执行
@AfterThrowing(pointcut = "execution(* SampleService.performAction(..))", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
System.out.println("After throwing in: " + joinPoint.getSignature().getName() + ", Exception: " + ex);
}
// 环绕通知:在方法执行之前和之后执行
@Around("execution(* SampleService.performAction(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Around before: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed(); // 继续执行方法
System.out.println("Around after: " + joinPoint.getSignature().getName());
return result;
}
}
结果:
就@Before来说,只要performAction方法被调用,那么第一步就是先执行这段代码:System.out.println("Before executing: " + joinPoint.getSignature().getName());执行完后在执行performAction方法。其他方法也是如此只是触发时机不同罢了。
@Aspect作用和Spring Aop关系
@Aspect 是 AspectJ 框架中的一个注解,它是面向切面编程(AOP)的一个关键部分。AspectJ 是一个独立的 AOP 框架,而 Spring 框架(包括 Spring Boot)则集成了 AspectJ 的一部分功能,使得开发者可以在 Spring 应用中方便地使用 AOP。
AOP 是一种编程范式(软件工程中的一类典型的编程风格。),用于增加程序的模块化,通过将横切关注点(如日志、安全、事务管理等)从业务逻辑中分离出来,提高代码的可重用性和可维护性。说白了其实我感觉就是动态代理,可以为相关方法前后等统一进行一些处理。
Spring Boot 作为 Spring 框架的一个扩展,继承了 Spring 的 AOP 功能。在 Spring Boot 应用中,AOP 使用代理模式来实现 AOP,其中 @Aspect 注解的类被当作一个带有通知(Advice)和切入点(Pointcut)的切面。
Spring AOP 使用代理模式来拦截对对象的方法调用。当在 Spring 应用中定义了 AOP 切面时,Spring 容器会为匹配切面指定的切入点的 Bean 创建一个代理对象。这个代理对象在调用原始方法之前或之后执行切面中定义的逻辑。
动态代理:如果一个 Bean 至少实现了一个接口,Spring AOP 默认会使用 JDK 的动态代理来创建这个 Bean 的代理。
CGLIB 代理:如果一个 Bean 没有实现任何接口,Spring AOP 会使用 CGLIB 库来创建代理。
虽然使用 @Aspect 注解和 @Before、@After 等注解定义了切面的行为,但这些注解本身并不负责拦截方法调用。真正的方法拦截是由 Spring AOP 在背后通过动态代理机制来实现的。当方法被调用时,实际上是先调用代理对象的对应方法,代理对象再根据 AOP 配置来决定是否调用原始对象的方法,以及在调用前后执行哪些额外的逻辑。
示例
假设有一个简单的服务类 MyService,我们想在其 performAction 方法执行前后添加日志:
public class MyService {
public void performAction() {
System.out.println("Performing action");
}
}
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* MyService.performAction(..))")
public void logBefore() {
System.out.println("Before action");
}
@After("execution(* MyService.performAction(..))")
public void logAfter() {
System.out.println("After action");
}
}
在这里,LoggingAspect 定义了在 MyService 的 performAction 方法执行前后要执行的日志操作。但实际上,当 performAction 方法被调用时,它是通过 MyService 的代理来调用的,代理负责根据 LoggingAspect 的配置执行相应的 AOP 逻辑。
发现通知参数中的joinPoint不知道做什么用,请看我的下一章 Springboot注解@Aspect(二)JoinPoint 使用详解
标签表达式
上述例子中你会好奇 @Before("execution(* SampleService.performAction(..))"),这中间的execution是什么意思后面括号内是什么意思。这其实是注解使用的标签表达式,有如下这些:
within:用于匹配指定类型内的方法执行
this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也* 类型匹配
target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配
args:用于匹配当前执行的方法传入的参数为指定类型的执行方法
@within:用于匹配所以持有指定注解类型内的方法
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解,和@annotation区别是不用全路径
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行
@annotation:用于匹配当前执行方法持有指定注解的方法
主要记住execution和@annotation用的比较多,一个是用在方法上一个是用在相关注解上
如何编写切入点表达式:
匹配特定方法:execution([权限加返回类型] [类路径].[方法名称]([参数]))
匹配带有特定注解的方法:@annotation(com.example.MyAnnotation)
匹配所有被 @MyAnnotation 注解的方法。
其中:
(..)代表所有参数
(*,String)代表第一个参数为任何值,第二个为String类型,
(..,String)代表最后一个参数是String类型
————————————————
JoinPoint 的作用
在 Spring AOP 中,JoinPoint 接口代表了一个程序执行的点,比如方法执行或异常处理。当使用 AOP 通知(Advice)时,你可以将 JoinPoint 作为参数传递到通知方法中,以便获取有关当前执行点的详细信息。
JoinPoint 提供了一种方式来访问当前被通知方法的详细信息,如方法签名、参数等。这在编写通知逻辑时非常有用,因为你可以根据当前执行的方法来修改通知的行为。
JoinPoint:
用于所有类型的通知(@Before、@After、@AfterReturning、@AfterThrowing),但不包括环绕通知。
JoinPoint 常用方法
getArgs():返回一个对象数组,包含了被通知方法的参数。
getThis():返回代理对象。
getTarget():返回目标对象。
getSignature():返回被通知方法的签名信息。
toString():打印出正在执行的被通知方法的详细信息。
toShortString():提供正在执行的被通知方法的简短描述。
toLongString():提供正在执行的被通知方法的完整描述
示例
假设你有一个前置通知(Before),你想在方法执行之前打印方法名称和参数:
ProceedingJoinPoint对象:ProceedingJoinPoint对象是JoinPoint的子接口,该对象只用在@Around的切面方法中。
@Aspect
@Component
public class aopAspect {
/**
* 定义一个切入点表达式,用来确定哪些类需要代理
* execution(* aopdemo.*.*(..))代表aopdemo包下所有类的所有方法都会被代理
*/
@Pointcut("execution(* aopdemo.*.*(..))")
public void declareJoinPointerExpression() {}
/**
* 前置方法,在目标方法执行前执行
* @param joinPoint 封装了代理方法信息的对象,若用不到则可以忽略不写
*/
@Before("declareJoinPointerExpression()")
public void beforeMethod(JoinPoint joinPoint){
System.out.println("目标方法名为:" + joinPoint.getSignature().getName());
System.out.println("目标方法所属类的简单类名:" + joinPoint.getSignature().getDeclaringType().getSimpleName());
System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));
//获取传入目标方法的参数
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
System.out.println("第" + (i+1) + "个参数为:" + args[i]);
}
System.out.println("被代理的对象:" + joinPoint.getTarget());
System.out.println("代理对象自己:" + joinPoint.getThis());
}
/**
* 环绕方法,可自定义目标方法执行的时机
* @param pjd JoinPoint的子接口,添加了
* Object proceed() throws Throwable 执行目标方法
* Object proceed(Object[] var1) throws Throwable 传入的新的参数去执行目标方法
* 两个方法
* @return 此方法需要返回值,返回值视为目标方法的返回值
*/
@Around("declareJoinPointerExpression()")
public Object aroundMethod(ProceedingJoinPoint pjd){
Object result = null;
try {
//前置通知
System.out.println("目标方法执行前...");
//执行目标方法
//result = pjd.proeed();
//用新的参数值执行目标方法
result = pjd.proceed(new Object[]{"newSpring","newAop"});
//返回通知
System.out.println("目标方法返回结果后...");
} catch (Throwable e) {
//异常通知
System.out.println("执行目标方法异常后...");
throw new RuntimeException(e);
}
//后置通知
System.out.println("目标方法执行后...");
return result;
}
}
被代理类
/**
* 被代理对象
*/
@Component
public class TargetClass {
/**
* 拼接两个字符串
*/
public String joint(String str1, String str2) {
return str1 + "+" + str2;
}
}
测试类
public class TestAop {
@Test
public void testAOP() {
//1、创建Spring的IOC的容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:bean.xml");
//2、从IOC容器中获取bean的实例
TargetClass targetClass = (TargetClass) ctx.getBean("targetClass");
//3、使用bean
String result = targetClass.joint("spring","aop");
System.out.println("result:" + result);
}
}
结果:
目标方法执行前...
目标方法名为:joint
目标方法所属类的简单类名:TargetClass
目标方法所属类的类名:aopdemo.TargetClass
目标方法声明类型:public
第1个参数为:newSpring
第2个参数为:newAop
被代理的对象:aopdemo.TargetClass@4efc180e
代理对象自己:aopdemo.TargetClass@4efc180e
目标方法返回结果后...
目标方法执行后...
result:newSpring+newAop
JoinPoint 的子类和关联类
MethodSignature:
MethodSignature 是 Signature 接口的子接口,专门用于方法调用。它提供了访问被拦截方法的详细信息,如方法名称、返回类型和参数类型。
在通知方法中,通常通过将 JoinPoint.getSignature() 的返回值强制转换为 MethodSignature 来获取更多关于方法的信息。
ProceedingJoinPoint:
ProceedingJoinPoint 是 JoinPoint 的子接口,专门用于环绕通知(@Around)。它添加了 proceed() 方法,允许控制何时继续执行拦截的方法。
proceed() 方法是环绕通知中的关键,它决定了是否继续执行原方法或者提前返回自定义结果。
————————————————
Spring Boot 条件注解:@ConditionalOnProperty 完全解析
在 Spring Boot 项目中,有时候我们希望根据配置文件中的某个属性值来决定是否启用某个功能或加载某个组件。此时,@ConditionalOnProperty 注解就可以发挥作用。它通过配置文件的属性值控制 Bean 或配置类的加载,使得我们的程序更具灵活性。
本文将详细介绍 @ConditionalOnProperty 的用法,并通过 功能开关 和 环境配置 两个实际场景来展示它的强大之处。
1. @ConditionalOnProperty 基本用法
语法
@ConditionalOnProperty(
prefix = "前缀",
name = "属性名",
havingValue = "指定值",
matchIfMissing = false
)
1
2
3
4
5
6
参数说明:
prefix:属性的前缀部分。
name:属性名称。
havingValue:属性的值与 havingValue 相等时条件成立(默认不指定)。
matchIfMissing:如果属性未定义,是否加载配置(默认 false,即未定义时不加载)。
2. 实战场景
场景一:功能开关
在实际项目中,我们可能需要通过配置文件中的某个属性来控制某个功能的启用或禁用。比如,是否开启定时任务、是否启用某个服务等。
示例:通过功能开关启用日志增强功能
Step 1:配置文件定义开关
在 application.properties 文件中添加一个开关属性:
feature.logging-enhancement.enabled=true
1
Step 2:实现日志增强功能
使用 @ConditionalOnProperty 来决定是否加载日志增强的 Bean:
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnProperty(prefix = "feature.logging-enhancement", name = "enabled", havingValue = "true", matchIfMissing = false)
public class LoggingEnhancementConfig {
@Bean
public String loggingEnhancement() {
System.out.println("日志增强功能已启用!");
return "Logging Enhancement Activated";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Step 3:测试
当 feature.logging-enhancement.enabled=true 时,LoggingEnhancementConfig 类会被加载,控制台会输出:
日志增强功能已启用!
1
当 feature.logging-enhancement.enabled=false 或未配置时,LoggingEnhancementConfig 类不会被加载。
场景二:环境配置
在不同的环境(开发、测试、生产)中,我们可能需要加载不同的配置。例如,开发环境下使用内存数据库,生产环境下使用 MySQL 数据库。
示例:不同环境下选择数据源
Step 1:配置文件
在 application.properties 中配置环境标识:
# 开发环境
spring.datasource.env=dev
# 生产环境
# spring.datasource.env=prod
1
2
3
4
5
Step 2:开发环境数据源配置
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource", name = "env", havingValue = "dev")
public class DevDataSourceConfig {
@Bean
public DataSource devDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:mem:testdb");
dataSource.setUsername("sa");
dataSource.setPassword("");
System.out.println("开发环境:加载内存数据库");
return dataSource;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Step 3:生产环境数据源配置
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource", name = "env", havingValue = "prod")
public class ProdDataSourceConfig {
@Bean
public DataSource prodDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/proddb");
dataSource.setUsername("root");
dataSource.setPassword("password");
System.out.println("生产环境:加载 MySQL 数据库");
return dataSource;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Step 4:测试
开发环境:spring.datasource.env=dev
控制台输出:
开发环境:加载内存数据库
1
生产环境:spring.datasource.env=prod
控制台输出:
生产环境:加载 MySQL 数据库
1
3. 常见应用场景总结
功能开关:动态启用或禁用某个功能模块(如定时任务、监控服务等)。
环境配置:根据不同环境加载不同的配置(如数据源、日志级别等)。
组件选择:根据配置加载特定的第三方组件(如不同的缓存实现 Redis/ehcache)。
服务切换:实现备用服务或降级服务的自动切换。
4. 小结
@ConditionalOnProperty 是 Spring Boot 中非常实用的条件注解,可以通过配置文件灵活地控制 Bean 和配置类的加载,避免不必要的资源浪费,并提高系统的可维护性。
通过功能开关和环境配置的示例,我们可以看到 @ConditionalOnProperty 如何让代码更清晰、配置更灵活,极大地满足了开发者在不同场景下的需求。
最佳实践
配置文件中使用统一的前缀管理属性,避免冲突。
开关属性的命名要清晰直观,比如 feature.xxx.enabled。
对于重要的功能开关,可以结合文档明确其作用和默认值。
-------------------------------------------------------------