以下内容为本人学习Spring Boot的AOP
与ChatGpt提问后对其回答进行部分修改
(有的错误实在是离谱 = =)、格式调整等操作后的答案,可能对于其中部分细节(是错是对,能力有限有的看不出来 = =),并未做深入探究
,大家感兴趣的话可以自行验证。
Spring AOP(面向切面编程)
是Spring框架提供的一个功能模块
,用于实现横切关注点
(cross-cutting concerns
)的处理。横切关注点指的是在一个应用程序中多个模块中共享的功能
,例如日志记录、事务管理、安全性检查
等,这些功能通常存在于应用程序的多个部分中
,而不仅仅属于某个特定的模块。
Spring AOP利用了面向切面编程
的思想,将这些横切关注点从核心业务逻辑中分离出来,形成独立的模块
。通过使用AOP,我们可以将这些横切关注点定义为切面(Aspect)
,然后将它们应用于应用程序中的不同模块,从而实现模块之间的解耦和重用
。
在Spring AOP中,切面是一个类
,它定义了在哪些连接点(Join Point)
上执行特定的行为
。连接点可以是方法调用、方法执行、异常抛出等程序执行过程中的特定点
。切面可以通过定义通知(Advice)
来描述在连接点上执行的具体行为
,例如在方法调用前后执行的操作。
通知可以分为以下几种类型:
前置通知(Before Advice)
:在连接点之前
执行的通知。后置通知(After Advice)
:在连接点之后
执行的通知,无论连接点是否正常完成。返回通知(After Returning Advice)
:在连接点正常完成后
执行的通知。异常通知(After Throwing Advice)
:在连接点抛出异常后
执行的通知。环绕通知(Around Advice)
:包围连接点的通知,在连接点之前和之后
执行自定义的行为。Spring AOP通过代理机制
实现切面的应用。当应用程序使用Spring容器管理的bean时,Spring会为这些bean创建代理对象
,并将切面逻辑织入到代理对象中
。在运行时,当应用程序调用代理对象
的方法时,代理对象会按照切面
的定义执行相应的通知
。
总之,Spring AOP提供了一种方便的方式来处理横切关注点
,实现了模块之间的解耦和重用
。通过将通用功能与核心业务逻辑分离
,我们可以更好地组织和维护
应用程序的代码。
Spring AOP
由以下几个组成部分构成,每个部分都有不同的作用:
切面(Aspect)
:
切面是一个普通的Java类
,它定义了横切关注点的行为和逻辑
。切面包含了通知和切点的定义。通知描述了在连接点上执行的具体操作,而切点定义了在哪些连接点上应用通知。
连接点(Join Point)
:
连接点是应用程序执行过程中的特定点
,例如方法调用、方法执行、异常抛出等
。切面可以定义一个或多个连接点,并在这些连接点上应用通知。
通知(Advice)
:
通知是切面在连接点上执行的具体行为
。Spring AOP定义了几种类型的通知:前置通知(Before Advice)、后置通知(After Advice)、返回通知(After Returning Advice)、异常通知(After Throwing Advice)和环绕通知(Around Advice)
。通知定义了在连接点的何时执行以及执行什么样的操作
。
切点(Pointcut)
:
切点定义了在哪些连接点上应用通知
。切点通过使用表达式或模式匹配来描述连接点的选择规则
。它允许我们指定应该在哪些类、哪些方法上应用通知。
引入(Introduction)
:
引入允许我们在现有的类中添加新的接口和方法
。通过引入,我们可以使现有的类具备新的功能,而无需修改原始类的代码
。
目标对象(Target Object)
:
目标对象是应用程序中被代理的真正对象
。它是包含核心业务逻辑的对象。
代理(Proxy)
:
代理是Spring AOP框架生成的对象
,它包装了目标对象
,并拦截对目标对象方法的调用
。代理对象根据切面的定义,在调用目标对象的方法之前、之后或环绕时应用相应的通知。
织入(Weaving)
:
织入是将切面应用到目标对象上的过程
。织入可以在编译时、类加载时或运行时
进行。Spring AOP采用运行时织入的方式
,即在应用程序运行时将切面逻辑织入到目标对象的方法调用中。
这些组成部分共同协作,使得我们能够将横切关注点的逻辑从核心业务逻辑中分离出来
,并在需要的连接点上应用相应的通知。通过使用Spring AOP,我们可以实现横切关注点的重用、解耦和集中管理
,从而提高代码的可维护性和可扩展性
。
在AOP中,连接点(Join Point)和切点(Pointcut)
是两个相关
的概念。
连接点是在应用程序执行过程中的特定位置
,例如方法调用、方法执行前后等。它是切面可以插入的特定执行点
。连接点代表了应用程序中的一个执行点,可以被拦截和修改
。在AOP中,连接点是被切面拦截的目标,也是切面可以应用的地方。
切点是在AOP中用于匹配连接点的表达式
。它定义了切面在哪些连接点上被触发
。切点是用于选择特定连接点的一种方式
。切点使用表达式
来描述连接点的匹配规则
,例如指定特定的类、方法或包等。
因此,切点和连接点是相关的,切点定义了哪些连接点是切面感兴趣的,即匹配的连接点
。切面通过与切点关联,选择在匹配的连接点上应用相应的增强逻辑(通知)。
切点通过切入点表达式来描述连接点的匹配规则,而连接点则是实际的应用程序执行点
。切点与连接点的关联使得切面可以选择性地在特定的连接点上应用增强逻辑,从而实现横切关注点的处理。
一个切面在目标对象创建到执行目标对象方法执行的
过程可以分为以下几个阶段:
目标对象的创建
:
在应用程序启动时,目标对象会被创建。这可以通过Spring的依赖注入或其他方式来实现。目标对象是切面将要拦截和增强的实际业务对象
。
切面对象的创建
:
切面对象也会在应用程序启动时被创建
。它是一个普通的Java类,使用@Aspect
注解进行标记。切面对象定义了切面的行为,包括切入点和通知等。
切面与目标对象的关联
:
在目标对象创建时
,Spring框架会自动识别和创建与之相关的切面对象
。切面与目标对象之间的关联由Spring的AOP框架完成
,它会检查目标对象是否与切面匹配,并将切面与目标对象进行关联
。
方法拦截
:
当目标对象的方法被调用时,AOP框架会检查是否有与该方法匹配的切入点和通知
。如果有匹配的切入点
,AOP框架将调用与之关联的通知方法
。
执行通知
:
根据切面定义的通知类型,在特定的执行时机执行相应的通知方法
。通知方法可以是目标对象方法执行前
的@Before
等注解修饰的方法,这会比目标对象先执行。而@After
、@AfterReturning
、@AfterThrowing
等注解修饰的方法,要在目标对象方法执行后
才会执行。
通知方法可以访问连接点信息并执行特定的逻辑,如记录日志、处理异常等。
目标对象方法的执行
:
在切面的通知方法执行完毕后
,AOP框架将继续执行目标对象的方法
。目标对象的方法会按照正常的流程继续执行,并返回相应的结果。
通过以上过程,切面可以在目标对象创建时与之关联
,并在目标对象的方法执行前、执行后、执行成功完成或抛出异常时执行相应的通知逻辑。切面的存在使得我们能够在不修改目标对象代码的情况下,通过切入和增强的方式
来实现对目标对象行为的控制和扩展
。
在Spring AOP中,各组成部分的相关注解有以下几种,它们的作用如下:
@Aspect
:
该注解标识一个类为切面类
。切面类中包含了通知和切点的定义。
@Component
(或其他注解,如@Service
、@Repository
等):
该注解用于将切面类作为Spring的组件进行管理
,使其成为Spring容器中的一个Bean。
@Pointcut
:
该注解定义切点
,用于描述在哪些连接点上应用通知。通过指定切点表达式或方法来选择连接点。
@Before
:
该注解定义前置通知
,表示在连接点之前执行的通知。可以用于执行一些预处理操作。
@After
:
该注解定义后置通知
,表示在连接点之后执行的通知,无论连接点是否正常完成。通常用于执行一些清理操作。
@AfterReturning
:
该注解定义返回通知
,表示在连接点正常返回结果后执行的通知。可以获取方法的返回值并进行处理。
@AfterThrowing
:
该注解定义异常通知
,表示在连接点抛出异常后执行的通知。可以捕获方法抛出的异常并进行相应的处理。
@Around
:
该注解定义环绕通知,表示包围连接点的通知,可以在连接点之前和之后执行
自定义的行为。可以控制是否执行连接点、修改连接点的参数和返回值。
@DeclareParents
:
该注解用于引入,允许为现有的类添加新的接口和方法
。通过引入,我们可以在不修改原始类代码的情况下为其添加额外的功能
。
@EnableAspectJAutoProxy
:
该注解用于启用Spring的自动代理功能
,使得切面能够生效(非必须)。
这些注解配合使用,能够帮助我们定义切面的行为和逻辑,并将其应用到目标对象的连接点上。通过切面和通知的组合,我们可以实现横切关注点的处理,例如日志记录、事务管理、安全性检查等,从而实现代码的解耦和重用。
以下是一个示例的AOP配置,用于在com.demo.controller
包中的所有方法执行前、执行后、执行成功完成和执行抛出异常时进行日志记录
:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
//标识为一个切面
@Aspect
//标识为一个组件 由Spring管理
@Component
public class LoggingAspect {
private final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
//这里指定了切点的匹配规则 通过该方法匹配找到所有切点
@Pointcut("execution(* com.demo.controller.*.*(..))")
public void controllerMethods() {
}
//注解的参数方法就是上面的方法 用于找到切点 切点规则由方法上的@Pointcut决定
@Before("controllerMethods()")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
logger.info("Before executing method: {}", methodName);
}
@AfterReturning("controllerMethods()")
public void logAfterReturning(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
logger.info("Method executed successfully: {}", methodName);
}
@AfterThrowing(pointcut = "controllerMethods()", throwing = "exception")
public void logAfterThrowing(JoinPoint joinPoint, Throwable exception) {
String methodName = joinPoint.getSignature().getName();
logger.error("Exception occurred in method: {}", methodName);
logger.error("Exception details: ", exception);
}
@After("controllerMethods()")
public void logAfter(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
logger.info("After executing method: {}", methodName);
}
}
在上述示例中,我们使用了@Aspect
注解将类声明为切面类,并使用@Component
注解将其作为一个组件交由Spring进行管理。
使用@Pointcut
注解定义了一个切点controllerMethods()
,用于匹配com.demo.controller
包中的所有方法。
然后,我们定义了各个通知方法:
@Before
注解的logBefore()
方法,在方法执行前记录日志。@AfterReturning
注解的logAfterReturning()
方法,在方法成功完成后记录日志。@AfterThrowing
注解的logAfterThrowing()
方法,在方法抛出异常时记录日志。@After
注解的logAfter()
方法,在方法执行后记录日志。在每个通知方法中,我们使用JoinPoint
参数来获取连接点的信息,如方法名、参数等,并使用Logger
来进行日志记录。
请注意,您需要确保在Spring配置中启用了对AOP的支持(例如,使用@EnableAspectJAutoProxy
注解 ,Spring Boot中不需要)以及将该切面类包含在组件扫描范围内,以便Spring能够自动识别和应用该切面。
这只是一个简单的示例,您可以根据实际需求进行进一步的定制和扩展,例如添加日志的级别、记录更详细的方法参数等。
以下是上述注解内常用参数的说明:
@Pointcut
注解参数说明:
value
:用于定义切点表达式,描述在哪些连接点上应用通知。argNames
:可选参数,用于为切点表达式中的参数命名,方便在通知方法中引用。@Before
、@After
、@AfterReturning
、@AfterThrowing
、@Around
注解参数说明:
value
:用于指定切点表达式,描述在哪些连接点上应用通知。argNames
:可选参数,用于为通知方法的参数命名,方便在通知方法中引用连接点的参数。returning
:仅适用于@AfterReturning
注解,用于指定一个参数名,表示连接点方法的返回值。throwing
:仅适用于@AfterThrowing
注解,用于指定一个参数名,表示连接点方法抛出的异常。除了上述常用参数外,还可以根据需要使用其他参数,例如:
@Aspect
注解还支持使用@Order
注解,用于指定切面的优先级顺序。@Around
注解还可以使用@ProceedingJoinPoint
参数类型的参数,用于控制连接点的执行。@Before
、@After
、@AfterReturning
、@AfterThrowing
注解还可以使用@JoinPoint
参数类型的参数,用于获取连接点的信息。请注意,这里列举的是常用的参数说明,而实际上每个注解还可能有其他可选参数,具体使用时可参考相关文档或API文档以获得更详细的参数说明。
在@Before
、@After
、@AfterReturning
、@AfterThrowing
、@Around
等注解中,value
参数用于指定切点表达式
,描述在哪些连接点上应用通知
。切点表达式由切点指示器和切点描述符组成
,用于匹配满足条件的连接点。
常用的切点指示器包括:
execution
:匹配方法执行连接点。within
:匹配指定类型内的方法执行连接点。this
:匹配当前AOP代理对象的类型。target
:匹配当前目标对象的类型。args
:匹配方法参数的类型。@target
:匹配当前目标对象类型的类级别注解。@args
:匹配当前方法参数类型的运行时注解。@within
:匹配包含指定注解的类或接口。@annotation
:匹配特定注解的方法。根据具体的需求,您可以根据切点表达式的语法和上述切点指示器的使用方式,结合类名、方法名、参数类型、注解等条件来编写合适的切点表达式。例如:
execution(* com.example.controller.*.*(..))
:匹配com.example.controller
包下的所有方法。(这个常用的,会详细讲)within(com.example.service.*)
:匹配com.example.service
包及其子包下的所有方法。args(String)
:匹配接受一个String
类型参数的方法。@annotation(org.springframework.web.bind.annotation.GetMapping)
:匹配使用@GetMapping
注解的方法。(这个常用的,会详细讲)需要注意的是,切点表达式的编写需要遵循一定的语法规则
,并且可以根据实际需求进行灵活的组合和扩展。切点表达式的具体写法会根据应用的结构和需要而有所差异。
在execution
括号内的表达式是切点表达式的核心部分
,用于描述方法执行连接点的匹配条件。它由多个元素组成,每个元素代表一个匹配条件
。下面是切点表达式中各个元素的详细说明:
访问修饰符(Access Modifier)
:
可选项,用于匹配方法的访问修饰符,例如public
、private
、protected
等。
返回类型(Return Type)
:
可选项,用于匹配方法的返回类型
,可以是具体的类名或通配符*
表示任意类型。
包名(Package Name)
:
包名用于匹配方法所在的包名,可以使用通配符*
表示任意包,使用点.
分隔包名的各个部分。
类名(Class Name)
:
类名用于匹配方法所在的类名,可以使用通配符*
表示任意类名,可以使用全限定类名或简单类名。
方法名(Method Name)
:
方法名用于匹配方法的名称,可以使用通配符*
表示任意方法名。
参数列表(Parameter List)
:
参数列表用于匹配方法的参数类型和数量,可以使用具体的类型或通配符*
表示任意类型,多个参数类型使用逗号,
分隔。
异常类型(Exception Type)
:
异常类型用于匹配方法可能抛出的异常类型,可以使用具体的异常类名或通配符*
表示任意异常类型。
切点表达式中各个元素可以根据需要组合使用,并使用点.
和星号*
等符号进行通配或精确匹配。例如,以下是一些切点表达式的示例:
execution(public * com.example.service.*.*(..))
:匹配com.example.service
包下所有公共方法。execution(* com.example.service.UserService.*(..))
:匹配com.example.service.UserService
类中的所有方法。execution(* com.example.service.UserService.save*(..))
:匹配com.example.service.UserService
类中以"save"开头的方法。execution(* com.example.service.*.*(String))
:匹配com.example.service
包下所有接受一个String
参数的方法。execution(* com.example.service.*.*(..) throws com.example.exception.CustomException)
:匹配com.example.service
包下所有可能抛出CustomException
的方法。请注意,切点表达式的编写需要根据具体的应用结构和需求进行调整,并且需要遵循一定的语法规则。可以根据实际需求使用各种元素组合来创建精确的切点表达式,以匹配特定的方法执行连接点。
在@annotation
括号内的表达式是切点表达式中的一种,用于匹配特定注解的方法执行连接点
。它用于在切面中选择那些标记了特定注解
的方法。下面是@annotation
括号内表达式的详细组成:
注解类型
:com.example.annotations.MyAnnotation
,也可以是简单注解名,例如MyAnnotation
。如果是简单注解名,会根据当前切面所在的包和导入的包进行匹配。切点表达式示例:
@annotation(org.springframework.web.bind.annotation.GetMapping)
:匹配标记了@GetMapping
注解的方法。@annotation(com.example.annotations.CustomAnnotation)
:匹配标记了@CustomAnnotation
注解的方法。通过使用@annotation
表达式,您可以根据特定的自定义注解或框架提供的注解来选择连接点。这使得您可以针对具有特定注解的方法应用特定的切面逻辑。
如果您的自定义注解与切面在同一个包
下,您可以使用简单注解名
来匹配注解,而无需使用完全限定的注解类名。这是因为在同一个包下的注解会自动被导入
,因此可以直接使用注解名
。
例如,如果自定义注解@MyAnnotation
与切面
在同一个包com.example.aspect
下,那么可以使用以下方式来匹配注解:
@annotation(MyAnnotation)
在这种情况下,切点表达式中的@annotation
括号内的参数直接使用注解名即可。不需要使用完全限定的注解类名。
请注意,这种简化写法只适用于注解与切面在同一个包下
的情况。如果注解与切面不在同一个包下,仍然需要使用完全限定的注解类名来匹配注解。