Spring-AOP
Spring-AOP
学习核心
- 面向切面编程AOP的引入
- AOP概念理解,底层实现原理
- AOP的应用场景
- AOP的实现方式
- 了解 AspectJ AOP(Spring AOP VS AspectJ AOP)
学习资料
AOP概念核心
AOP(Aspect-Oriented Programming,即 面向切面编程)与 OOP( Object-Oriented Programming,面向对象编程) 相辅相成,提供了与 OOP 不同的抽象软件结构的视角
AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、安全检查、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。OOP 的目的是将业务逻辑按照对象的属性和行为进行封装,通过类、对象、继承、多态等概念,实现代码的模块化和层次化(也能实现代码的复用),提高代码的可读性和可维护性。
AOP核心:将横切关注点从核心业务逻辑中分离出来,形成一个个切面
1.场景分析
OOP 不能很好地处理一些分散在多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等),这些行为通常被称为 横切关注点(cross-cutting concerns) 。如果在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。
AOP 可以将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从 核心业务逻辑(core concerns,核心关注点) 中分离出来,实现关注点的分离。
【场景说明】如果现在有一个UserDao,需要对其中的add方法做增强(即在目标方法add的前后做扩展,例如日志记录等)
传统模式:通过继承实现方法扩展(可以参考装饰类模式调调)
public class UserDAO {
public void add(){
System.out.println("执行add操作");
}
}
// 通过继承实现方法扩展
public class UserDAOExtend extends UserDAO{
public void moreAdd(){
System.out.println("before....");
// 调用父类方法
add();
System.out.println("after....");
}
public static void main(String[] args) {
UserDAOExtend userDAOExtend = new UserDAOExtend();
userDAOExtend.moreAdd();
}
}
// 缺点:JAVA是单继承机制,如果采用继承容易导致该类后续可扩展性弱
AOP:采用横向抽取的方式实现,借助代理向目标方法织入增强代码
实现步骤参考:(AOP的实现有很多种,选择一种简单的去理解AOP核心点,其他配置复杂(例如原生Spring通过XML配置的方式)的扩展了解),此处结合Springboot的aop进行说明
- pom.xml中引入springboot的aop依赖
- 编写目标类(被代理的对象)、定义切面(@Aspect):需注意定义的组件要借助spring注解注入
- 测试AOP功能(如果是Run模式,则需在启动类中添加@EnableAspectJAutoProxy启动AOP支持)
# 1.引入aspect依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<scope>test</scope>
</dependency>
# 2.编写被代理类(目标类)、定义切面
此处目标类为UserService、UserServiceImpl
public interface UserService {
void add();
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void add() {
System.out.println("执行add操作");
}
}
切面定义
@Component // 注入bean
@Aspect // 定义切面
public class MyAspect {
/** 切入点表达式(如果多处需要引用,则可定义一个切点表达式供切面引入) */
@Pointcut("execution(* com.aop.UserService.add(..))")
public void logPointCut(){}
// 方式1:通过切点切入
@Before("logPointCut()")
public void writeLog() {
System.out.println("前置增强......记录日志");
}
// 方式2:直接织入
@AfterReturning("execution(* com.aop.UserService.add(..))")
public void doSth() {
System.out.println("后置增强......执行操作后处理某些事物......");
}
}
# 3.编写测试类
@SpringBootTest
class SpringbootDemoAopApplicationTests {
@Autowired
private UserService userService;
@Test
void testAOP(){
userService.add();
}
}
理解AOP的核心术语
切点 | 说明 | 目的 |
---|---|---|
横切关注点 | cross-cutting concerns | 多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等) |
切面 | Aspect | 切面=切点+通知(即在什么地方、什么时机、做什么) 对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能 |
连接点 | JoinPoint | 连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等) |
通知 | Advice | 在方法前、方法后、方法前后要做什么 通知就是切面在某个连接点要执行的操作。通知有五种类型: 前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around) 前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程 |
切点 | Pointcut | 哪些类、哪些方法上切入 一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义 比如 execution(* com.xyz.service..*(..)) 匹配 com.xyz.service 包及其子包下的类或接口 |
织入 | Weaving | 将切面加入对象并创建代理对象 织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上 常见的织入时机有两种,分别是编译期织入(AspectJ)和运行期织入(AspectJ) |
👻项目中如何使用AOP(构建思路)
1.思路构建
关键思路(此处基于AspectJ提供的注解方式构建AOP案例)
【1】理解AOP的原理、关键术语和应用场景
【2】掌握AOP核心概念和相关的注解(@Component组件注入、@Aspect切面、@Pointcut切入点、5种通知@Before/@After/@AfterReturning/@AfterThrowing/@Around)
【3】掌握AOP的使用流程
2.注解参数使用解析
表达式类型 | 功能 |
---|---|
execution() | 匹配方法,最全的一个 |
args() | 匹配入参类型 |
@args() | 匹配入参类型上的注解 |
@annotation() | 匹配方法上的注解 |
within() | 匹配类路径 |
@within() | 匹配类上的注解 |
this() | 匹配类路径,实际上AOP代理的类 |
target() | 匹配类路径,目标类 |
@target() | 匹配类上的注解 |
比较常用的是execution()和@annotation,前者指定匹配的方法,后者通过注解方式匹配
execution()
execution(修饰符 返回值类型 方法名(参数)异常)
// 参考示例
@Pointcut("execution(public * com.noob.aop.controller..*.*(..) throws Exception)")
public void pointcut(){}
语法参数 | 描述 |
---|---|
修饰符 | 可选,如public,protected,写在返回值前,任意修饰符填* 号就可以 |
返回值类型 | 必选 ,可以使用* 来代表任意返回值 |
方法名 | 必选 ,可以用* 来代表任意方法 |
参数 | ():代表是没有参数, (…)代表是匹配任意数量,任意类型的参数,当然也可以指定类型的参数进行匹配 如要接受一个String类型的参数,则(java.lang.String),任意数量的String类型参数:(java.lang.String…) |
异常 | 可选,语法:throws 异常 ,异常是完整带包名,可以是多个,用逗号分隔 |
@annotation
匹配方法上的注解,括号内写注解定义的全路径,所有加了此注解的方法都会被增强
// 增强被指定注解修饰的方法(所有加了@TestAspect注解的都会被)
@annotation(com.noob.test.annotation.MyAspect)
// 指定前缀的注解修饰的方法
@annotation(com.noob.test.annotation.Prefix*)
// 指定后缀的注解修饰的方法
@annotation(com.noob.test.annotation.*Suffix)
3.AspectJ 方式实现AOP
(1)基于 XML配置的方式实现AOP
AOP相关依赖引入(注意Spring AOP版本和AspectJ版本兼容性)
<!-- Spring AOP相关依赖引入 -->
<!-- Spring AOP依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- AspectJ依赖 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.8</version>
</dependency>
定义目标类(目标对象,确认切入点)
public class UserServiceImpl {
public void method(){
System.out.println("hello spring aop");
}
public void methodExc() throws Exception{
System.out.println("sth wrong throw Exception");
throw new Exception();
}
}
定义切面类:切点+通知
public class LogAspectJ {
/**
* 环绕通知
*/
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("-----------------------");
System.out.println("环绕通知: 进入方法");
Object o = pjp.proceed();
System.out.println("环绕通知: 退出方法");
return o;
}
/**
* 前置通知
*/
public void doBefore() {
System.out.println("前置通知");
}
/**
* 后置通知
*/
public void doAfterReturning(String result) {
System.out.println("后置通知, 返回值: " + result);
}
/**
* 异常通知
*/
public void doAfterThrowing(Exception e) {
System.out.println("异常通知, 异常: " + e.getMessage());
}
/**
* 最终通知
*/
public void doAfter() {
System.out.println("最终通知");
}
}
xml配置(applicationContext-aop.xml )
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.noob.framework.aop.aspectj" />
<aop:aspectj-autoproxy/>
<!-- 目标类 -->
<bean id="demoService" class="com.noob.framework.aop.aspectj.service.UserServiceImpl">
<!-- configure properties of bean here as normal -->
</bean>
<!-- 切面 -->
<bean id="logAspect" class="com.noob.framework.aop.aspectj.LogAspectJ">
<!-- configure properties of aspect here as normal -->
</bean>
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="logAspect">
<!-- 配置切入点 -->
<aop:pointcut id="pointCutMethod" expression="execution(* com.noob.framework.aop.aspectj.service.*.*(..))"/>
<!-- 环绕通知 -->
<aop:around method="doAround" pointcut-ref="pointCutMethod"/>
<!-- 前置通知 -->
<aop:before method="doBefore" pointcut-ref="pointCutMethod"/>
<!-- 后置通知;returning属性:用于设置后置通知的第二个参数的名称,类型是Object -->
<aop:after-returning method="doAfterReturning" pointcut-ref="pointCutMethod" returning="result"/>
<!-- 异常通知:如果没有异常,将不会执行增强;throwing属性:用于设置通知第二个参数的的名称、类型-->
<aop:after-throwing method="doAfterThrowing" pointcut-ref="pointCutMethod" throwing="e"/>
<!-- 最终通知 -->
<aop:after method="doAfter" pointcut-ref="pointCutMethod"/>
</aop:aspect>
</aop:config>
</beans>
定义测试类
// 日志AOP测试
public class LogAOPTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext-aop.xml");
UserServiceImpl userService = context.getBean("demoService", UserServiceImpl.class);
// 执行普通方法方法
userService.method();
// 异常方法测试
try {
userService.methodExc();
} catch (Exception e) {
System.out.println("异常捕获处理");
}
}
}
// output
-----------------------
环绕通知: 进入方法
前置通知
hello spring aop
环绕通知: 退出方法
最终通知
-----------------------
环绕通知: 进入方法
前置通知
sth wrong throw Exception
异常通知, 异常: null
最终通知
异常捕获处理
(2)基于注解的AspectJ方式实现AOP:权限验证(接口鉴权)
基于XML的声明式AspectJ存在一些不足,需要在Spring配置文件配置大量的代码信息,为了解决这个问题,Spring 使用了@AspectJ框架为AOP的实现提供了一套注解。
注解名称 | 解释 |
---|---|
@Aspect | 用来定义一个切面 |
@pointcut | 用于定义切入点表达式。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法 |
@Before | 用于定义前置通知,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式) |
@AfterReturning | 用于定义后置通知,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式 |
@Around | 用于定义环绕通知,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 |
@After-Throwing | 用于定义异常通知来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定-一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常 |
@After | 用于定义最终final 通知,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点 |
@DeclareParents | 用于定义引介通知,相当于IntroductionInterceptor (了解) |
步骤说明(此构建思路可结合Shiro权限校验框架理解)
【1】引入aop依赖
【2】自定义注解(@Auth)
【3】定义切面
【4】构建连接点
# 1.引入aspect依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<scope>test</scope>
</dependency>
# 2.自定义注解(@Auth)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auth {}
# 3.定义切面
@Aspect
@Component
public class AuthAspect {
// 定义了一个切点:指定自定义注解的全路径
@Pointcut("@annotation(com.auth.Auth)")
public void authCut() {}
@Before("authCut()")
public void cutProcess(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("注解方式AOP开始拦截, 当前拦截的方法名: " + method.getName());
}
@After("authCut()")
public void after(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("注解方式AOP执行的方法 :" + method.getName() + " 执行完了");
}
@Around("authCut()")
public Object testCutAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("注解方式AOP拦截开始进入环绕通知.......");
Object proceed = joinPoint.proceed();
System.out.println("准备退出环绕......");
return proceed;
}
/**
* returning属性指定连接点方法返回的结果放置在result变量中
* @param joinPoint 连接点
* @param result 返回结果
*/
@AfterReturning(value = "authCut()", returning = "result")
public void afterReturn(JoinPoint joinPoint, Object result) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("注解方式AOP拦截的方法执行成功, 进入返回通知拦截, 方法名为: " + method.getName() + ", 返回结果为: " + result.toString());
}
@AfterThrowing(value = "authCut()", throwing = "e")
public void afterThrow(JoinPoint joinPoint, Exception e) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("注解方式AOP进入方法异常拦截, 方法名为: " + method.getName() + ", 异常信息为: " + e.getMessage());
}
}
# 4.构建连接点(例如在controller层通过注解设定拦截方法)
@RestController
@RequestMapping("/auth")
public class AuthController {
// http://localhost:9082/demo/auth/aopTest?name=hhh
@GetMapping("/aopTest")
@Auth
public String aopTest(@RequestParam String name) {
System.out.println("正在执行接口name" + name);
return "执行成功" + name;
}
}
概念扩展
1.Spring中的AOP
AOP的代理使用 JDK 动态代理和 CGLIB 代理来实现,默认如果目标对象是接口,则使用 JDK 动态代理,否则使用 CGLIB 来生成代理类。
动态代理:程序执行过程中,使用JDK的反射机制,创建代理类对象,并动态的指定要代理目标类。动态代理涉及到的三个类:
InvocationHandler接口:处理器,负责完调用目标方法(就是被代理类中的方法),并增强功能;通过代理类对象执行目标接口中的方法,会把方法的调用分派给调用处理器(InvocationHandler)的实现类,执行实现类中的invoke()方法,需要把在该invoke()方法中实现调用目标类的目标方法;
Proxy 类:通过 JDK 的 java.lang.reflect.Proxy 类实现动态代理 ,使用其静态方法 newProxyInstance(),依据目标对象(被代理类的对象)、业务接口及调用处理器三者,自动生成一个动态代理对象
Method 类:Method 是实例化的对象,有一个方法叫 invoke(),该方法在反射中就是用来执行反射对象的方法的
2.Spring AOP VS AspectJ
AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)
- AspectJ是更强的AOP框架,是实际意义的AOP标准;
- Spring为何不写类似AspectJ的框架? Spring AOP使用纯Java实现, 它不需要专门的编译过程, 它一个重要的原则就是无侵入性(non-invasiveness); Spring 小组完全有能力写类似的框架,只是Spring AOP从来没有打算通过提供一种全面的AOP解决方案来与AspectJ竞争。Spring的开发小组相信无论是基于代理(proxy-based)的框架如Spring AOP或者是成熟的框架如AspectJ都是很有价值的,他们之间应该是互补而不是竞争的关系
- Spring小组喜欢@AspectJ注解风格更胜于Spring XML配置; 所以在Spring 2.0使用了和AspectJ 5一样的注解,并使用AspectJ来做切入点解析和匹配。但是,AOP在运行时仍旧是纯的Spring AOP,并不依赖于AspectJ的编译器或者织入器(weaver)
- Spring 2.5对AspectJ的支持:在一些环境下,增加了对AspectJ的装载时编织支持,同时提供了一个新的bean切入点