1. 什么是AOP
全称是 Aspect Oriented Programming 即:面向切面编程。是OOP的延续,也是Spring框架中的一个重要内容,是函数式编程的一种衍生泛型。简单的说他就是把我们程序重复的代码抽取出来,在需要执行的时候使用动态代理技术在不修改源码的基础上,对我们的已有方法进行增强。(AOP用于简化动态代理的使用)
2. AOP的作用
- 作用:不修改源码的情况下,进行功能增强,通过动态代理实现的
- 优势:减少重复代码,提高开发效率,降低耦合度方便维护
3. AOP底层
实际上,Sping的AOP,底层是通过动态代理实现的。在运行期间,通过代理技术动态生成代理对象,代理对象方法执行时进行功能的增强介入,再去调用目标方法,从而完成功能增强。
- 常用的动态代理技术有
- JDK的动态代理:基于接口实现的
- cglib的动态代理:基于子类实现的
- Spring的AOP采用了那种代理方式?
- 如果目标对象有接口,就采用JDK的动态代理技术
- 如果目标对象没有接口,就采用cglib技术
4. AOP相关概念
- 目标对象(Target):要代理的/要增强的目标对象
- 代理对象(Proxy):目标对象被AOP织入增强后,就得到了一个代理对象
- 连接点(JoinPoint):能够被拦截的点,在Spring里指的是方法。(能增强的方法)
- 切入点(PointCut):要对哪些连接点进行拦截的定义。(要增强的方法)
- 通知/增强(Advice):拦截到连接点之后要做的事情。(要如何增强,额外添加上去的功能和能力)
- 切面(Aspect):是切入点和通知的结合。(告诉Sping的AOP:要对那个方法,做什么样的增强)
- 织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程
5. AOP使用步骤:
-
添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> -
编写切面类
- 类上加@Aspect注解:声明一下这个类是切面类
- 类上加@Component注解:标注一个类为Spring容器的Bean
- 类里编写通知方法:要额外增加上去的功能
- 方法上加注解:@通知类型("切入点表达式") 如:@Around("切入点表达式")
6. 通知类型
| 通知类型 | 注解 | 作用 |
|---|---|---|
| 前置通知 | @Before | 通知方法将在目标方法之前 先执行 |
| 后置通知 | @AfterReturning | 通知方法将在目标方法正常结束之后 再执行 |
| 异常通知 | @AfterThrowing | 通知方法将在目标方法抛出异常之后 再执行 |
| 最终通知 | @After | 通知方法将在目标方法之后 必定执行,无论有没有抛出异常 |
| 环绕通知 | @Around | 有最大自主性的通知方法,如何调用目标方法及如何增强,完全由我们自己决定 |
7. 切入点表达式
7.1 execution
用于根据方法的名称进行模糊匹配
语法:execution(权限修饰符 返回值类型 全限定类名.方法名(形参列表))
*:通配符来表示模糊匹配..:用于形参列表里,表示任意个任意形参;用于其他地方,表示任意层级的package包- 权限修饰符写
public,通常省略不写
示例:
public void com.sdf.service.XxService.delete(Integer)void com.sdf.service.XxService.delete(Integer)* com.sdf.service.XxService.delete(Integer)* com.sdf.service.*.delete(Integer)* com.sdf..*.delete(Integer)* com.sdf..*.*(Integer)* com.sdf..*.*(Integer, String)* com.sdf..*.*(Integer, *)* com.sdf..*.*(..)
7.2 @annotation
根据注解选择要增强的方法
语法:@annotation(注解的全限定类名),会选择所有加了指定注解的方法,进行增强
使用步骤:
- 自定义注解
- 创建切面配置类
- 给要增强的方法上加自定义的注解
示例:
-
自定义注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnno { } -
创建切面配置类
@Aspect @Component public class Demo03Aspect { @Before("@annotation(com.sdf.aop.MyLog)") public void saveLogs(){ System.out.println("========保存日志信息======"); } } -
给要增强的方法上加自定义的注解
@MyAnno @Override public List<Dept> queryAllDepts() { return deptMapper.selectAll(); }
8. 抽取公用切入点表达式
语法:
- 定义公用的切入点表达式:在切面类里创建一个方法,方法上加注解
@Pointcut("公用的切入点表达式") - 引用公用的切入点表达式:
- 完整用法:
Before("com.sdf.aop.Demo02Aspect.pc()") - 简写形式:
Before("pc()"),仅适合于 切入点方法 和 通知方法在同一个类里
- 完整用法:
@Aspect
@Component
public class Demo02Aspect {
/**这个方法仅仅作为切入点表达式的载体*/
@Pointcut("execution(* com.sdf.service.*.*(..))")
public void pc(){}
@Before("pc()")
public void before(){
System.out.println("=====Demo02Aspect.before执行了======");
}
@After("pc()")
public void after(){
System.out.println("=====Demo02Aspect.after执行了======");
}
}
9. 通知执行顺序
10. 示例练习
需求:
将案例中增、删、改相关接口的操作日志文件记录到数据库表中。
- 就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存在数据表中,便于后期数据追踪。
操作日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
分析:
使用AOP技术实现:
- 如果不使用AOP,就需要修改源码,给大量的方法增加保存日志的代码。代码重复,日志和业务功能耦合到了一起
- 使用AOP,可在不修改源码的情况下,给Service层方法进行增强。
首先要明确切入点和通知
-
切入点:对那些方法增强
哪个方法需要保存日志,就在哪个方法上加一个自定义注解
使用注解切入点表达式
@annotation(自定义注解) -
通知:
通知方法:获取当前用户、当前时间、当前方法的类名和方法名、方法的实参、返回值,运行耗时,把这些信息保存到日志表。
通知类型: 使用@Around环绕通知
实现:
1.自定义注解MyLog
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyLog {
}
2.给增删改查方法添加注解
3.创建切面类
@Aspect
@Component
public class LogAspect {
@Autowired
private HttpServletRequest request;
@Autowired
private OperateLogMapper operateLogMapper;
@Around("@annotation(com.sdf.anno.MyLog)")
public Object logAspect(ProceedingJoinPoint pjp) throws Throwable {
long begin = System.currentTimeMillis();
Object res = pjp.proceed();
long end = System.currentTimeMillis();
OperateLog log = new OperateLog();
//获取当前用户
//通过request对象后去token,解析token得到用户id
String token = request.getHeader("token");
Claims claims = JwtUtils.parseJWT(token);
Integer userId = claims.get("id", Integer.class);
log.setOperateUser(userId);
// 当前时间、
LocalDateTime now = LocalDateTime.now();
log.setOperateTime(now);
// 当前方法的类名
String clazzName = pjp.getSignature().getDeclaringType().toString();
log.setClassName(clazzName);
//当前方法方法名、
String methodName = pjp.getSignature().getName();
log.setMethodName(methodName);
// 方法的实参、
String methodParams = pjp.getArgs().toString();
log.setMethodParams(methodParams);
// 返回值,
String returnValue = res.toString();
log.setReturnValue(returnValue);
// 运行耗时,
log.setCostTime(begin-end);
operateLogMapper.insert(log);
return res;
}
}
通知方法里获取目标方法信息
不同通知方法里,获取被调用的目标方法:
- 可以直接给通知方法,直接加形参JoinPoint
- 如果是环绕通知方法,则要加形参ProceedingJoinPoint,继承了JoinPoint
JoinPoint常用方法:
getArgs():获取调用方法时的实参值getTarget():获取被调用的目标对象(原始目标对象)getThis():获取当前代理对象getSignature():获取被调用的方法信息。Signature对象的方法:-
getName():获取被调用的方法名。比如:queryAllDepts -
getDeclaringType():获取被调用方法所属的类Class -
getDeclaringTypeName():获取被调用方法所属的类的全限定类名 -
toLongString():获取方法的完整名称。 -
toShortString:获取方法的简短名称。 -
toString:获取方法信息。
-