👀概念
当谈论AOP(面向切面编程)时,我们在软件设计中引入了一种编程范式,用于解决关注点分离的问题。关注点分离是将一个应用程序的不同关注点(例如日志记录、事务管理、安全性等)从业务逻辑中分离出来,以便提高代码的模块化和可维护性。
✌以下是AOP的主要概念和特性:
✍1. 切面(Aspect):
切面是一个包含横切关注点逻辑的模块。它定义了在何处(切点)以及何时(通知)执行关注点的逻辑。在实际场景中,我们可以创建一个日志记录切面来记录方法的调用和执行时间。
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.myapp.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
System.out.println("Method " + methodName + " executed in " + (endTime - startTime) + "ms");
return result;
}
}
在这个示例中,@Aspect
注解标记了该类为切面,@Around
通知定义了在哪个切点上执行日志记录逻辑。切点表达式execution(* com.example.myapp.service.*.*(..))
匹配了com.example.myapp.service
包下的所有方法。
✍2. 切点(Pointcut):
-
切点定义了哪些方法将会被影响,即在哪些方法上应用切面的通知。在实际应用中,我们可以定义一个切点,用于匹配所有
UserService
类的方法。 -
在这个示例中,我们定义了三个不同的切点并应用了不同类型的通知。serviceMethods()切点匹配com.example.myapp.service包中的所有方法。afterRepositoryMethods()切点匹配com.example.myapp.repository包中的所有方法。loggableMethods()切点匹配带有@Loggable注解的方法。
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.myapp.service.*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void beforeAdvice() {
System.out.println("Before method execution");
}
@After("execution(* com.example.myapp.repository.*.*(..))")
public void afterRepositoryMethods() {
System.out.println("After repository method execution");
}
@Around("@annotation(com.example.myapp.annotation.Loggable)")
public Object loggableMethods(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method execution");
Object result = joinPoint.proceed();
System.out.println("After method execution");
return result;
}
}
在这个示例中,@Pointcut
注解定义了一个切点,其切点表达式指定了匹配UserService
类的所有方法。你可以在通知中引用这个切点来应用切面逻辑。
✍3. 通知(Advice):
通知是切面在特定切点处执行的代码。在实际场景中,我们可以创建一个记录日志的切面,其中包括在目标方法执行前后记录日志的通知。
@Aspect
@Component
public class LoggingAspect {
@Before("userServiceMethods()")
public void beforeAdvice() {
System.out.println("Before method execution");
}
@After("userServiceMethods()")
public void afterAdvice() {
System.out.println("After method execution");
}
}
在这个示例中,@Before
和@After
注解定义了通知,它们分别在切点匹配的方法执行前和执行后执行。userServiceMethods()
是之前定义的切点。
✍4. 引入(Introduction):
引入是一种增强方式,允许在现有的类中添加新的方法和属性。在实际应用中,我们可以为现有的类引入一个新的接口,以添加新的功能,而无需修改现有代码。
@Aspect
@Component
public class IntroductionAspect {
@DeclareParents(value = "com.example.myapp.service.*+", defaultImpl = AuditableServiceImpl.class)
private AuditableService auditableService;
}
在这个示例中,我们通过@DeclareParents
注解将AuditableService
接口引入到com.example.myapp.service
包下的所有类中。这使得这些类都具备了AuditableService
接口的功能。
AOP的引入(Introduction)和依赖注入
依赖注入(Dependency Injection):
- 依赖注入是一种设计模式,旨在通过将依赖关系从一个类转移到另一个类,以实现解耦和可测试性。
- 在依赖注入中,我们将某个类所需的依赖通过构造函数、方法参数或属性注入到该类中,而不是在类内部直接创建它们。
- 依赖注入的目的是将组件的依赖关系集中管理,以提高模块的灵活性和可替换性。
AOP的引入(Introduction):
- AOP的引入是一种通过在现有类中添加新接口、方法或属性的方式来增加类的功能,而无需修改现有代码。
- 引入功能使我们能够在不破坏现有类的情况下,向类中引入新的行为或功能。
- AOP的引入与依赖注入的目的不同,主要是为了在不修改现有代码的情况下添加新的功能,从而实现关注点的分离。
尽管AOP的引入和依赖注入都涉及在类中添加新功能,但它们的重点和用途是不同的。依赖注入更关注解耦和和可测试性,而AOP的引入更关注在不破坏现有代码的情况下添加新功能。
✍5. 织入(Weaving):
织入是将切面与应用程序代码结合的过程。在实际应用中,织入将切面的通知插入到切点处,从而实现关注点分离。
@Aspect
@Component
public class LoggingAspect {
@Around("userServiceMethods()")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method execution");
Object result = joinPoint.proceed();
System.out.println("After method execution");
return result;
}
}
✍6. 连接点(Joinpoint):
-
连接点是程序执行的某个位置,如方法的调用或异常的抛出。
示例:
@AfterReturning(pointcut = "serviceMethods()", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { // ... }
在这个示例中,
logAfterReturning
方法的joinPoint
参数是一个连接点。
在这个示例中,@Around
注解定义了织入的通知。它在目标方法执行前后执行,并将日志记录的逻辑织入到目标方法中。
✍7. 目标对象(Target Object):
-
目标对象是被一个或多个切面通知的对象。
示例:
@Service public class ExampleService { public String doSomething(String name) { return "Hello, " + name; } }
在这个示例中,
ExampleService
是一个目标对象,它被LoggingAspect
切面通知。
✍8. AopProxyUtils:
AopProxyUtils
类是Spring AOP框架的一个工具类,它提供了一些静态方法,用于处理代理对象和目标对象。
-
getSingletonTarget(Object candidate):
这个方法用于获取单例bean的目标对象。示例:
ExampleService targetObject = (ExampleService) AopProxyUtils.getSingletonTarget(exampleService);
在这个示例中,
exampleService
是ExampleService
的代理对象。我们使用AopProxyUtils.getSingletonTarget
方法获取exampleService
的目标对象。注意:这个方法只适用于单例bean。如果bean的作用域不是单例,这个方法将返回
null
。 -
getTargetClass(Object candidate):
这个方法用于获取代理对象的目标类。示例:
Class<?> targetClass = AopProxyUtils.getTargetClass(exampleService);
在这个示例中,
exampleService
是ExampleService
的代理对象。我们使用AopProxyUtils.getTargetClass
方法获取exampleService
的目标类。注意:这个方法返回的是目标类,而不是目标对象。
-
ultimateTargetClass(Object candidate):
这个方法用于获取代理对象的最终目标类。示例:
Class<?> ultimateTargetClass = AopProxyUtils.ultimateTargetClass(exampleService);
在这个示例中,
exampleService
是ExampleService
的代理对象。我们使用AopProxyUtils.ultimateTargetClass
方法获取exampleService
的最终目标类。注意:这个方法返回的是最终目标类,而不是目标对象。如果代理对象有多层代理,这个方法将返回最终的目标类。
这些是AopProxyUtils
类的一些常用方法。这个类还有一些其他方法,但它们通常不需要在应用程序代码中直接使用。
注意:通常我们不需要直接访问目标对象。代理对象会将调用转发到目标对象,并在调用之前或之后执行通知。所以,通常我们应该使用代理对象,而不是目标对象。直接访问目标对象会绕过代理,这意味着切面的通知将不会被执行。
这个类还包含一些其他的方法,但是它们主要用于内部使用,通常不需要在应用程序代码中直接使用。例如,AopProxyUtils.completeProxiedInterfaces
方法用于确定给定的代理配置的完整代理接口集,包括从目标类继承的接口。这个方法通常用于在创建代理对象时确定代理接口。
✌AOP在实际开发中的应用场景包括:
- 日志记录:记录方法的调用、参数和返回值,以便进行调试和性能监控。
- 事务管理:在方法调用前后控制事务的开始和提交、回滚。
- 安全性:实现访问控制、认证和授权等安全相关的功能。
- 性能优化:在关键方法中添加性能监控和优化逻辑。
- 缓存管理:在方法中添加缓存逻辑,提高应用程序的响应速度。
当涉及到AOP的实际应用场景时,让我们从五个不同的方面来示例化讲解:
✍1. 日志记录:
示例:在一个Web应用中,记录每个请求的处理时间和请求参数。
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.myapp.controller.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
System.out.println("Method " + methodName + " executed in " + (endTime - startTime) + "ms");
return result;
}
}
✍2. 事务管理:
示例:确保在调用服务方法时,事务在适当的时候启动、提交或回滚。
@Aspect
@Component
public class TransactionAspect {
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
TransactionStatus transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
Object result = joinPoint.proceed();
transactionManager.commit(transactionStatus);
return result;
} catch (Exception ex) {
transactionManager.rollback(transactionStatus);
throw ex;
}
}
}
✍3. 安全性:
示例:在敏感操作前,检查用户是否有足够的权限执行操作。
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(com.example.myapp.annotation.RequiresAdminRole)")
public void checkAdminRole() {
if (!currentUserHasAdminRole()) {
throw new SecurityException("Admin role required");
}
}
}
✍4. 性能优化:
示例:在关键方法中添加性能监控和优化逻辑,如数据库查询。
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.myapp.repository.*.*(..))")
public Object measureQueryPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
if (endTime - startTime > 1000) {
System.out.println("Query execution took more than 1 second");
}
return result;
}
}
✍5. 缓存管理:
示例:在方法中添加缓存逻辑,提高应用程序的响应速度。
@Aspect
@Component
public class CachingAspect {
private Map<String, Object> cache = new HashMap<>();
@Around("execution(* com.example.myapp.service.*.*(..))")
public Object applyCaching(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
if (cache.containsKey(methodName)) {
return cache.get(methodName);
} else {
Object result = joinPoint.proceed();
cache.put(methodName, result);
return result;
}
}
}
👀导入依赖
在Spring Boot应用中使用AOP,你需要导入以下核心依赖以支持AOP功能:
- spring-boot-starter-aop: 这个starter提供了Spring AOP的支持,使你可以在应用程序中使用AOP功能。
在Maven项目中,你可以将这些依赖添加到pom.xml
文件中:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
👀启用AOP
当涉及到在Spring Boot应用程序中启用AOP时,你可以将@EnableAspectJAutoProxy
注解放置在两个位置:配置类上或入口函数所在的主类上。以下是这两种方式的合并输出:
✌方式1:将@EnableAspectJAutoProxy
注解放置在配置类上:
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// 配置类的其他内容
}
✌方式2:将@EnableAspectJAutoProxy
注解放置在入口函数所在的主类上:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@SpringBootApplication
@EnableAspectJAutoProxy
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
无论你选择哪种方式,它们都会启用AspectJ自动代理,使AOP切面能够正常应用于Spring Boot应用程序中的Bean。根据你的偏好和项目结构,选择合适的位置来放置@EnableAspectJAutoProxy
注解。
👀切点表达式
切点表达式(Pointcut Expression)是AOP中一个重要的概念,用于指定在哪些方法上应用通知。切点表达式使用AspectJ的语法,它允许你定义一组匹配方法的规则。这些规则可以基于方法的名称、参数、返回类型等来进行匹配。以下是切点表达式的详细说明和示例:
✌切点表达式的语法:
切点表达式的基本语法是使用关键字execution
,后面跟着方法的返回类型、类名、方法名和参数列表。通配符可以用来匹配不同的部分。
execution([可见性] 返回类型 [类全名.]方法名(参数列表) [异常模式])
其中,方括号内的部分是可选的,具体的匹配规则如下:
可见性
:方法的可见性,如public
、private
等。返回类型
:方法的返回类型,使用*
通配符表示任意类型。类全名
:类的全名,使用*
通配符表示任意包名,省略则表示当前包下的所有类。方法名
:方法的名称,使用*
通配符表示任意方法名。参数列表
:方法的参数列表,使用..
表示任意数量和类型的参数。异常模式
:方法可能抛出的异常。
✌切点表达式示例:
假设我们有以下服务类:
package com.example.myapp.service;
@Service
public class UserService {
public void createUser(String username) {
System.out.println("User created: " + username);
}
public void deleteUser(String username) {
System.out.println("User deleted: " + username);
}
}
我们来定义一些切点表达式示例:
✍1. 匹配所有UserService
类的方法:
@Pointcut("execution(* com.example.myapp.service.UserService.*(..))")
public void userServiceMethods() {}
✍2. 匹配所有公共方法:
@Pointcut("execution(public * *(..))")
public void publicMethods() {}
✍3. 匹配任何以create
开头的方法:
@Pointcut("execution(* create*(..))")
public void createMethods() {}
✍4. 匹配任何返回类型是void
的方法:
@Pointcut("execution(void *(..))")
public void voidReturnMethods() {}
✍5. 匹配参数列表包含一个String
类型参数的方法:
@Pointcut("execution(* *(String))")
public void stringParameterMethods() {}
✍6. 匹配参数列表包含两个参数的方法,第一个参数是int
类型,第二个参数是任意类型:
@Pointcut("execution(* *(int, ..))")
public void intAndAnyParameterMethods() {}
👀切面注解
✌1. @Aspect:
- 标记一个类为切面类,其中包含切点和通知。
- 在类级别上使用。
- 表示这个类将包含切点和通知。
示例:定义一个用于日志记录的切面类。
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.myapp.service.*.*(..))")
public void beforeAdvice() {
System.out.println("Before method execution");
}
@After("execution(* com.example.myapp.service.*.*(..))")
public void afterAdvice() {
System.out.println("After method execution");
}
}
✌2. @Pointcut:
- 定义切点,可以在通知中引用。
- 在切面类中定义一个无返回值的方法,并使用
@Pointcut
注解。 - 切点表达式被写入
@Pointcut
注解的方法体中。
示例:定义一个切点来匹配服务类中的所有方法。
@Pointcut("execution(* com.example.myapp.service.*.*(..))")
public void serviceMethods() {}
✌3. @Before:
- 在目标方法执行之前执行通知。
- 通知方法会在切点方法之前被调用。
示例:在调用服务方法之前打印日志。
@Before("serviceMethods()")
public void beforeAdvice() {
System.out.println("Before method execution");
}
✌4. @After:
- 在目标方法执行之后(不论是否发生异常)执行通知。
- 通知方法会在切点方法之后被调用。
示例:在调用服务方法之后打印日志。
@After("serviceMethods()")
public void afterAdvice() {
System.out.println("After method execution");
}
✌5. @AfterReturning:
- 在目标方法成功执行之后执行通知。
- 通知方法会在切点方法成功执行后被调用。
示例:在调用服务方法成功执行后打印日志。
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void afterReturningAdvice(Object result) {
System.out.println("Method returned: " + result);
}
✌6. @AfterThrowing:
- 在目标方法抛出异常后执行通知。
- 通知方法会在切点方法抛出异常后被调用。
示例:在调用服务方法抛出异常后打印日志。
@AfterThrowing(pointcut = "serviceMethods()", throwing = "exception")
public void afterThrowingAdvice(Exception exception) {
System.out.println("Exception thrown: " + exception.getMessage());
}
✌7. @Around:
- 包围通知,可以在目标方法执行前后执行自定义逻辑。
- 通知方法需要接受一个
ProceedingJoinPoint
参数,通过调用其proceed()
方法来执行目标方法。
示例:在调用服务方法前后记录执行时间。
@Around("serviceMethods()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("Method execution time: " + (endTime - startTime) + "ms");
return result;
}
✌8. @Order:
- 控制多个切面的执行顺序。
- 值越小,优先级越高。
示例:定义切面的执行顺序。
@Aspect
@Component
@Order(1)
public class FirstAspect {
// ...
}
@Aspect
@Component
@Order(2)
public class SecondAspect {
// ...
}
👀使用注解做切点
当谈论AOP中的切面注解时,除了基于切点表达式匹配方法外,还可以使用注解作为切点。这种方式允许你在特定注解被使用时触发通知,从而实现一种基于注解的AOP。以下是涉及切面注解和监听注解的解释和示例:
✌ 示例:
假设我们有一个使用了自定义注解@Loggable
的服务类:
package com.example.myapp.service;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Loggable
public void createUser(String username) {
System.out.println("User created: " + username);
}
@Loggable
public void deleteUser(String username) {
System.out.println("User deleted: " + username);
}
}
我们可以创建一个切面来监听使用了@Loggable
注解的方法,并在这些方法执行前后记录日志:
package com.example.myapp.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(com.example.myapp.annotation.Loggable)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Before method execution");
Object result = joinPoint.proceed();
System.out.println("After method execution");
return result;
}
}
在上述示例中,我们使用了@Around
注解并指定切点表达式为@annotation(com.example.myapp.annotation.Loggable)
,这将匹配使用了@Loggable
注解的方法。在切面的logAround
方法中,我们在方法执行前后记录了日志。