[Spring]-AOP(面向切面编程)
[Spring]-AOP(面向切面编程)
掌握内容
(1)什么是AOP
(2)JDK动态代理和CGLIB动态代理区别
(3)Spring AOP的术语(切点、切面、连接点等)
(4)如果使用Spring AOP构建业务场景(结合业务场景进行说明):XML配置方式和注解方式
基本概念
AOP:Aspect Oriented Programming 面向切面编程;OOP:Object Oriented Programming 面向对象编程
面向切面编程:基于OOP基础之上新的编程思想,OOP面向的主要对象是类,而AOP面向的主要对象是切面,在处理日志、安全管理、事务管理等方面有非常重要的作用。AOP是Spring中重要的核心点,虽然IOC容器没有依赖AOP,但是AOP提供了非常强大的功能,用来对IOC做补充。通俗点说的话就是在程序运行期间在不修改原有代码的情况下增强跟主要业务没有关系的公共功能代码到之前写好的方法中的指定位置这种编程的方式叫AOP
AOP底层:代理模式
不要死记硬背,结合概念、应用场景、项目引用去描述技术栈
AOP应用场景
日志管理、权限认证、接口鉴权、安全检查、事务控制等
1.什么是AOP?
AOP Aspect Oriented Programing 面向切面编程:采取横向抽取机制,取代了传统纵向继承体系重复性代码(性能监视、事务管理、安全检查、缓存)
Spring中的AOP默认使用JDK代理模式实现(运行期通过代理方式向目标类织入增强代码)
场景分析
如果现在有一个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();
}
}
2.AOP应用场景
AOP(面向切面编程)的应用场景主要包括以下几个方面:
日志记录:通过在方法调用前后插入切面逻辑,可以实现日志记录功能,例如记录方法名和参数、方法的返回值等
事务管理:在方法调用前开启事务,在方法调用后提交事务或回滚事务,可以简化事务管理的代码,提高代码的可读性和可维护性
安全性检查:通过在方法调用前插入切面逻辑,可以实现安全性检查功能,例如检查用户的权限,只允许有特定权限的用户访问某些方法(参考Shiro框架)
性能监控:在方法调用前后分别记录方法的开始时间和结束时间,以此计算方法的执行时间,可以方便地监控方法的性能,找出性能瓶颈
异常处理:例如在方法调用后捕获异常并进行处理,这样可以统一处理异常,避免在每个方法中都进行异常处理的重复代码
缓存管理:在方法调用前检查缓存中是否存在结果,在方法调用后将结果存入缓存,这样可以提高系统的性能,避免重复计算
参数校验和转换:在方法调用前对方法的参数进行校验和转换,以确保参数的有效性和符合业务要求
日志审计:在方法调用前记录用户的操作,包括操作的类型、时间和操作的对象等,这样可以方便地进行日志审计和追踪
综上所述,AOP的应用场景非常广泛,可以在不改变业务逻辑的情况下,通过切面编程的方式,对系统的横切关注点进行统一管理和增强
核心内容:日志记录、接口鉴权、参数校验、缓存管理等
3.项目中如何使用AOP
思路构建
关键思路
【1】理解AOP的原理、关键术语和应用场景
【2】掌握AOP核心概念和相关的注解(@Component组件注入、@Aspect切面、@Pointcut切入点、5种通知@Before/@After/@AfterReturning/@AfterThrowing/@Around)
【3】掌握AOP的使用流程
注解参数使用解析
表达式类型 | 功能 |
---|---|
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)
参考案例
(1)权限验证(接口鉴权)
步骤说明(此构建思路可结合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;
}
}
XML配置AOP参考(基于 XML配置的AspectJ编程)
定义被代理对象
<bean id="productDAO" class="com.spring.b_aspectj_xml.ProductDAO"></bean>
定义切面
<bean id="myAspectXML" class="com.spring.b_aspectj_xml.MyAspectXML"></bean>
使用xml详细配置aspectj的配置
<aop:config>
<!-- 定义切面 -->
<aop:aspect ref="myAspectXML">
<!-- 定义切点 -->
<aop:pointcut expression="切点(要拦截的方法)" id="pointcut1"/>
<!-- 定义增强 -->
<aop:before method="before" pointcut-ref="pointcut1"/>
</aop:aspect>
</aop:config>
4.Spring中的AOP
AOP的代理使用 JDK 动态代理和 CGLIB 代理来实现,默认如果目标对象是接口,则使用 JDK 动态代理,否则使用 CGLIB 来生成代理类。
动态代理:程序执行过程中,使用JDK的反射机制,创建代理类对象,并动态的指定要代理目标类。动态代理涉及到的三个类:
InvocationHandler接口:处理器,负责完调用目标方法(就是被代理类中的方法),并增强功能;通过代理类对象执行目标接口中的方法,会把方法的调用分派给调用处理器(InvocationHandler)的实现类,执行实现类中的invoke()方法,需要把在该invoke()方法中实现调用目标类的目标方法;
Proxy 类:通过 JDK 的 java.lang.reflect.Proxy 类实现动态代理 ,使用其静态方法 newProxyInstance(),依据目标对象(被代理类的对象)、业务接口及调用处理器三者,自动生成一个动态代理对象
Method 类:Method 是实例化的对象,有一个方法叫 invoke(),该方法在反射中就是用来执行反射对象的方法的