Spring-AOP
AOP:Aspect Oriented Programming(面向切面编程)
OOP:Object Oriented Programming(面向对象编程)
在 Spring 框架中,AOP(面向切面编程,Aspect-Oriented Programming)是一种编程范式,它通过将关注点(如事务管理、日志记录、安全等)从业务逻辑中分离出来,使得代码更加模块化和可维护。AOP 允许你将横切关注点(cross-cutting concerns)封装在单独的单元中,这样可以减少代码重复并增强系统的可重用性。
静态代理
静态代理是在编译时就确定了代理类和被代理类的关系。代理类实现与被代理类相同的接口,并在代理类中持有被代理类的引用。静态代理的优点是简单易懂,但缺点是每增加一个被代理类,就需要增加一个代理类,导致代码重复。
// 被代理的接口
interface Subject {
void request();
}
// 被代理的类
class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: Handling request.");
}
}
// 代理类
class ProxySubject implements Subject {
private RealSubject realSubject;
public ProxySubject() {
this.realSubject = new RealSubject();
}
@Override
public void request() {
System.out.println("ProxySubject: Pre-processing request.");
realSubject.request();
System.out.println("ProxySubject: Post-processing request.");
}
}
// 测试
public class StaticProxyTest {
public static void main(String[] args) {
Subject proxy = new ProxySubject();
proxy.request();
}
}
动态代理
动态代理是在运行时创建代理类,Java提供了
java.lang.reflect.Proxy
类和InvocationHandler
接口来实现动态代理。动态代理的优点是可以在运行时决定代理的对象,减少了代码的重复。强制要求,目标对象必有接口。代理的也只是接口规定的方法。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 被代理的接口
interface Subject {
void request();
}
// 被代理的类
class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: Handling request.");
}
}
// 动态代理处理器
class DynamicProxyHandler implements InvocationHandler {
private Object target;
public DynamicProxyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("DynamicProxy: Pre-processing request.");
Object result = method.invoke(target, args);
System.out.println("DynamicProxy: Post-processing request.");
return result;
}
}
// 测试
public class DynamicProxyTest {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
Subject proxy = (Subject) Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
new Class[]{Subject.class},
new DynamicProxyHandler(realSubject)
);
proxy.request();
}
}
AOP
AOP(面向切面编程)是一种编程范式,它允许将横切关注点(如日志记录、事务管理、安全等)从业务逻辑中分离出来。
AOP思想主要的应用场景
AOP(面向切面编程)是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,使得代码更加清晰、简洁、易于维护。AOP可以应用于各种场景,以下是一些常见的AOP应用场景:
- 日志记录:在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,可以在方法执行前、执行后或异常抛出时记录日志。
- 事务处理:在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。
- 安全控制:在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
- 性能监控:在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。
- 异常处理:系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。
- 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
- 动态代理:AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。
综上所述,AOP可以应用于各种场景,它的作用是将通用的横切关注点与业务逻辑分离,使得代码更加清晰、简洁、易于维护。
Spring AOP底层技术组成
动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
AspectJ:早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解。
AOP的基本概念
切面(Aspect):
切面是横切关注点的模块化,它可以被看作是一个类,该类包含相关的切点和通知(Advice)。
切点(Pointcut):
切点是一种表达式,用于定义在哪些连接点执行通知。连接点是程序执行期间的某个点,例如方法调用。
通知(Advice):
通知是在切点处执行的动作,AOP有几种类型的通知:
- 前置通知(@Before):目标方法执行前执行。
- 后置通知(@After):目标方法执行后执行,且无论方法是否抛出异常。
- 返回通知(@AfterReturning):目标方法成功返回后执行。
- 异常通知(@AfterThrowing):目标方法抛出异常后执行。
- 环绕通知(@Around):可以在方法执行前后自定义逻辑,能够控制方法是否被调用。
连接点(Joinpoint):
连接点是程序执行中的一个点,这个点可以是方法调用、对象实例化、异常处理等。
Spring AOP 示例
Maven 依赖
首先,确保你的 pom.xml
中包含 Spring AOP 和 AspectJ 的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
创建一个切面
接下来,创建一个切面(Aspect),用于定义横切关注点。
/*
通知方法
@Before:方法执行之前运行。
@AfterReturning:方法执行正常返回结果运行。
@AfterThrowing:方法抛出异常运行。
@After:方法执行之后运行
切入点表达式:【指定要进行横切的类】
execution(方法的全签名):
全写法:[public] int [com.atguigu.spring.aop.calculator.MathCalculator.] add(int,int) [throwsArithmeticException]
省略写法:int add(int,int)
通配符:
* :表示任意字符
.. :代表多个参数
.. :代表多层包
*/
@Before("execution(int *(int,int))")
public void logStart(JoinPoint joinPoint){
//joinPoint=>这个点可以是方法调用、对象实例化、异常处理等
System.out.println("【切面 - 日志】开始...");
}
@After("execution(int *(int,int))")
public void logEnd(JoinPoint joinPoint){
//joinPoint=>这个点可以是方法调用、对象实例化、异常处理等
System.out.println("【切面 - 日志】结束...");
}
@AfterReturning("execution(int *(int,int))")
public void logReturn(JoinPoint joinPoint){
//joinPoint=>这个点可以是方法调用、对象实例化、异常处理等
System.out.println("【切面 - 日志】返回...");
}
@AfterThrowing("execution(int *(int,int))")
public void logException(JoinPoint joinPoint){
//joinPoint=>这个点可以是方法调用、对象实例化、异常处理等
System.out.println("【切面 - 日志】异常...");
}
创建业务类
创建一个业务类,用于测试 AOP 功能。
//接口
public interface ArithmeticTest {
int add(int i ,int j);
int sub(int i ,int j);
int mul(int i ,int j);
int div(int i ,int j);
}
//实现类
@Component
public class ArithmeticTestImpl implements ArithmeticTest {
@Override
public int add(int i, int j) {
return i+j;
}
@Override
public int sub(int i, int j) {
return i-j;
}
@Override
public int mul(int i, int j) {
return i*j;
}
@Override
public int div(int i, int j) {
return i/j;
}
}
@SpringBootTest测试
@SpringBootTest
public class AopTest {
@Autowired
private ArithmeticTest arithmeticTest;
@Test
public void test01(){
arithmeticTest.add(1,3);
}
}
通知方法的执行顺序
正常:前置通知 ==》目标方法 ==》返回通知 ==》后置通知
异常:前置通知 ==》目标方法 ==》异常通知 ==》后置通知
切面表达式
execution 最常用;匹配方法执行连接点,可以匹配方法、类、包。
表达式模式:
execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)
表达式解释:
modifier:匹配修饰符,public, private 等,省略时匹配任意修饰符
ret-type:匹配返回类型,使用 * 匹配任意类型
declaring-type:匹配目标类,省略时匹配任意类型
.. 匹配包及其子包的所有类
name-pattern:匹配方法名称,使用 * 表示通配符
* 匹配任意方法
set* 匹配名称以 set 开头的方法
param-pattern:匹配参数类型和数量
() 匹配没有参数的方法
(..) 匹配有任意数量参数的方法
(*) 匹配有一个任意类型参数的方法
(*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
throws-pattern:匹配抛出异常类型,省略时匹配任意类型
// 匹配public方法
execution(public * *(..))
// 匹配名称以set开头的方法
execution(* set*(..))
// 匹配AccountService接口或类的方法
execution(* com.xyz.service.AccountService.*(..))
// 匹配service包及其子包的类或接口
execution(* com.xyz.service..*(..))
within匹配指定类型。匹配指定类的任意方法,不能匹配接口。
// 匹配service包的类
within(com.xyz.service.*)
// 匹配service包及其子包的类
within(com.xyz.service..*)
// 匹配AccountServiceImpl类
within(com.xyz.service.AccountServiceImpl)
args匹配方法参数类型和数量,参数类型可以为指定类型及其子类。
使用
execution
表达式匹配参数时,不能匹配参数类型为子类的方法。
// 匹配参数只有一个且为Serializable类型(或实现Serializable接口的类)
args(java.io.Serializable)
// 匹配参数个数至少有一个且为第一个为Example类型(或实现Example接口的类)
args(cn.codeartist.spring.aop.pointcut.Example,..)
@annotation匹配方法是否含有注解。当方法上使用了注解,该方法会被匹配,在接口方法上使用注解不匹配。
// 匹配使用了Demo注解的方法
@annotation(cn.codeartist.spring.aop.pointcut.Demo)
要使切点的匹配性能达到最佳,编写表达式时,应该尽可能缩小匹配范围,切点表达式分为三大类:
-
类型表达式:匹配某个特定切入点,如 execution
-
作用域表达式:匹配某组切入点,如 within
-
上下文表达式:基于上下文匹配某些切入点,如 this、target 和 @annotation
切点表达式组合
使用
&&
、||
和!
来组合多个切点表达式,表示多个表达式“与”、“或”和“非”的逻辑关系。
这可以用来组合多种类型的表达式,来提升匹配效率。
// 匹配doExecution()切点表达式并且参数第一个为Account类型的方法
@Before("doExecution() && args(account,..)")
public void validateAccount(Account account) {
// 自定义逻辑
}
类型 | 语法 | 解释&案例 |
---|---|---|
类型 | 语法**** | 解释&案例 |
execution | execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?) | 最常用;匹配方法执行连接点 如:execution(* com.example.service..(..)) 匹配com.example.service包及其子包中所有类的所有方法。 |
within | within(type-pattern) | 匹配指定类型内(包括子类型)的所有连接点 与execution表达式类似,但更侧重于类型而非具体的方法签名 |
this | this(type-pattern) | 匹配代理对象是指定类型或其子类型的任何连接点 |
target | target(type-pattern) | 匹配目标对象是指定类型或其子类型的任何连接点 如:target(com.example.MyBean) 匹配所有目标对象是MyBean类型或其子类型的连接点。 |
args | args(param-pattern) | 匹配方法参数是指定类型或其子类型的任何连接点 如:args(java.io.Serializable);匹配所有参数是序列化接口的方法 |
bean | bean(bean-id-or-name-pattern) | 匹配特定Spring bean的所有连接点。这依赖于Spring bean的名称或ID。 |
@target | @target(annotation-type) | 匹配标注了指定注解的所有目标对象的方法。 如@target(org.springframework.transaction.annotation.Transactional) |
@args | @args(annotation-type) | 匹配方法参数标注指定注解。如:@args(com.atguigu.Hello); 匹配参数上标注@Hello注解的 |
@within | @within(annotation-type) | 匹配目标对象类型上拥有指定注解的所有方法。如: @within(org.springframework.transaction.annotation.Transactional) 匹配所有目标对象类上被@Transactional注解标注 |
@annotation | @annotation(annotation-type) | 匹配任何被指定注解标注的方法。如,@annotation(org.springframework.transaction.annotation.Transactional) 匹配所有被@Transactional注解标注的方法。 |
组合表达式 | &&、||、! | &&代表同时成立、||代表某个成立、!代表非(某个不成立) |
公共配置@Pointcut
@Aspect
@Component
public class DemoAspect {
//配置公共的切面表单时
@Pointcut("execution(切面表达式)")
private void pointcut() {}
//调用公共的切面表达式
@Before("pointcut()")
public void doBefore(JoinPoint joinPoint) {
// 自定义逻辑
}
}
环绕通知
环绕通知对应整个 try...catch...finally 结构,包括前面四种通知的所有功能。
// 使用@Around注解标明环绕通知方法
@Around(value = "com.atguigu.aop.aspect.AtguiguPointCut.transactionPointCut()")
public Object manageTransaction(
// 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,
// Spring会将这个类型的对象传给我们
ProceedingJoinPoint joinPoint) {
// 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
Object[] args = joinPoint.getArgs();
// 通过ProceedingJoinPoint对象获取目标方法的签名对象
Signature signature = joinPoint.getSignature();
// 通过签名对象获取目标方法的方法名
String methodName = signature.getName();
// 声明变量用来存储目标方法的返回值
Object targetMethodReturnValue = null;
try {
// 在目标方法执行前:开启事务(模拟)
log.debug("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));
// 过ProceedingJoinPoint对象调用目标方法
// 目标方法的返回值一定要返回给外界调用者
targetMethodReturnValue = joinPoint.proceed(args);
// 在目标方法成功返回后:提交事务(模拟)
log.debug("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);
}catch (Throwable e){
// 在目标方法抛异常后:回滚事务(模拟)
log.debug("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
}finally {
// 在目标方法最终结束后:释放数据库连接
log.debug("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
}
return targetMethodReturnValue;
}
注意:目标方法抛异常后,处理完一定要向上抛出异常。保证链路完整。
ProceedingJoinPoint
说明:使用
org.aspectj.lang.ProceedingJoinPoint
允许你在环绕通知中控制方法的执行。用法:你可以使用此参数在通知中进行方法的执行和参数传递。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimingAspect {
//任意返回值类型 在com.example.service包下的 任意类名 任意方法名 (任意类型一个或多个形参)
@Around("execution(* com.example.service.*.*(..))")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object proceed = joinPoint.proceed(); // 执行目标方法
long executionTime = System.currentTimeMillis() - start;
System.out.println("Method executed in: " + executionTime + "ms");
return proceed;
}
}
连接点(Joinpoint)
连接点(Joinpoint)是面向切面编程(AOP)中的一个重要概念,指的是程序执行过程中可以插入横切关注点(如通知)的特定点。连接点可以是多个不同的事件,例如方法调用、对象实例化、字段的访问、异常处理等。
getSignature():返回连接点的签名信息(即方法名、参数类型等)。
JoinPoint joinPoint;
Signature signature = joinPoint.getSignature();
System.out.println("Method Name: " + signature.getName());
getTarget():返回连接点所对应的目标对象,通常是正在被代理的对象。
Object target = joinPoint.getTarget();
getArgs():返回连接点方法的参数,返回类型为
Object[]
。
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
getThis():返回当前代理对象,通常是进行切面处理的对象。
Object proxy = joinPoint.getThis();
getStaticPart(): 返回连接点的静态部分,通常用于获取与该连接点的相关信息。
Joinpoint staticPart = joinPoint.getStaticPart();
方法参数
说明:你还可以直接在切面方法中定义与切点方法参数类型相匹配的参数,以获取原始参数值。
用法:在切点表达式中,匹配的方法参数会对应到切面方法的参数。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class UserServiceAspect {
@Before("execution(* com.example.service.UserService.createUser(..)) && args(user)")
public void beforeCreateUser(User user) {
System.out.println("Creating user: " + user.getName());
}
}
其他类型
Signature
:你可以通过参数获得连接点的方法签名。
import org.aspectj.lang.Signature;
@Before("execution(* com.example.service.*.*(..))")
public void logMethodName(Signature signature) {
System.out.println("Method called: " + signature.getName());
}
多切面优先级设置
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用 @Order 注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1) // 高优先级的切面
public class FirstAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeFirst() {
System.out.println("FirstAspect: Before method execution");
}
}
@Aspect
@Component
@Order(2) // 低优先级的切面
public class SecondAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeSecond() {
System.out.println("SecondAspect: Before method execution");
}
}
扩展(@EnableAspectJAutoProxy)
@EnableAspectJAutoProxy
是 Spring Framework 中的一个注解,用于启用 AspectJ 风格的切面编程(AOP)功能。通过该注解,Spring 可以自动识别和支持用@Aspect
注解标记的切面,并创建所需的代理对象来管理它们。
主要功能:
- 启用 AOP 的支持:使用
@EnableAspectJAutoProxy
后,Spring 将能够处理切面和对应的切入点,支持用@Before
、@After
、@Around
等注解定义的通知方法。 - 代理创建:当使用该注解后,Spring 会为被标注为切面的 beans 创建代理对象。这些代理对象能够在方法执行前后执行切面逻辑。
- 配置方式:通常将此注解放在一个配置类上(该类使用
@Configuration
注解标识),告知 Spring 应该启用 AOP 功能并扫描切面。 - 代理类型配置:你可以通过设置
proxyTargetClass
属性为true
或false
,来选择使用 JDK 动态代理还是 CGLIB 代理。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
@Bean
public MyAspect myAspect() {
return new MyAspect();
}
}
@Aspect
class MyAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
System.out.println("方法执行之前");
}
}
AopContext
AopContext
是 Spring AOP 的一个接口,它提供了一种机制来访问当前的 AOP 代理对象。通过AopContext
,您可以在需要时获取到当前执行上下文中的代理对象。这在某些情况下非常有用,例如当您希望在同一个类的不同方法之间进行方法调用,并且希望能够应用切面逻辑时。
例子
启用 AopContext: 在配置类中设置
exposeProxy
属性为true
,这样可以将代理对象暴露到AopContext
中。
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig {
// ...
}
获取代理对象: 在需要使用 AOP 功能的地方,调用
AopContext.currentProxy()
来获取当前的 AOP 代理对象。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.aop.framework.AopContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
@Bean
public MyAspect myAspect() {
return new MyAspect();
}
}
@Aspect
class MyAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeAdvice() {
System.out.println("方法执行之前");
}
}
interface MyService {
void doSomething();
}
class MyServiceImpl implements MyService {
public void doSomething() {
System.out.println("执行服务逻辑");
// 可以通过 AopContext 获取代理对象
MyService proxy = (MyService) AopContext.currentProxy();
proxy.doSomething(); // 这次调用会触发切面
}
}
当您希望在同一个类的不同方法之间进行方法调用,并且希望能够应用切面逻辑时。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.aop.framework.AopContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
@Bean
public MyAspect myAspect() {
return new MyAspect();
}
}
@Aspect
class MyAspect {
@Before("execution(* com.example.service.MyService.*(..))")
public void beforeAdvice() {
System.out.println("方法执行之前");
}
}
interface MyService {
void someMethod();
}
class MyServiceImpl implements MyService {
@Override
public void someMethod() {
System.out.println("执行服务逻辑");
// 调用另一个方法
this.anotherMethod(); // 这里不会触发切面
// 使用 AopContext 获取代理对象并调用
MyService proxy = (MyService) AopContext.currentProxy();
proxy.anotherMethod(); // 这里会触发切面
}
public void anotherMethod() {
System.out.println("执行另一个方法");
}
}
标签:匹配,int,Spring,切面,AOP,方法,public From: https://www.cnblogs.com/21CHS/p/18430634代码分析
- @Aspect 注解的切面:
MyAspect
中定义了一个前置通知beforeAdvice
,它会在MyService
接口中的任何方法执行之前被调用。- 服务实现类:
MyServiceImpl
实现了MyService
接口。在someMethod
方法中,首先是直接调用anotherMethod()
,这不会触发切面逻辑,因为调用的是同一对象的方法。- 获取代理对象:调用
AopContext.currentProxy()
获取代理对象,然后通过代理对象调用anotherMethod()
,这样可以确保切面逻辑能够正确执行。