Spring AOP
1. AOP概述
1.1 AOP是什么?
AOP是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善。通过预编译的方式和运行期动态代理方式,实现在不修改源代码的情况下给程序统一添加额外的功能的一种技术。
1.2 AOP应用场景
实际项目中通常将系统为两大部分,一部分是核心业务,一部分是非核心业务。
首先要完成核心业务的实现,非核心业务一般是通过特定方式切入到系统中,一般就是借助AOP实现
AOP要基于OCP(开闭原则),在不改变原有系统核心业务代码的基础上动态添加一些扩展功能
1.3 AOP 应用原理分析
(1)假如目标对象(被代理对象)实现接口,则底层可以采用JDK动态代理记之为目标对象创建代理对象(目标类和代理类会共同实现接口)
(2)假如目标对象(被代理对象)没有实现接口,则底层可以采用CGLIB代理机制为目标对象创建代理对象(默认创建的代理类会继承目标对象类型)
1.4 AOP 相应术语分析
- 切面(asptct):横向切面对象,一般为一个具体类对象(可以借助#Aspect声明)
- 通知(Advice):在切面的某个特定连接点上执行的动作(扩展功能),例如around,before,after等
- 连接点(joinpoint):程序执行过程中某个特定的点,一般指被拦截到的方法
- 切入点(pointcut):多个连接点(Joinpoint)一种定义,一般可以理解为多个连接点的集合
2. Spring AOP快速实践
2.1 业务描述
基于项目中的核心业务,添加简单的日志操作,借助SLF4日志API输出目标方法的执行时长(不修改目标方法的代码-遵循OCP原则)
2.2 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.3 扩展业务分析及实现
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @Aspect 注解描述的类为Spring aop中的一个切面类型此类型可以定义为
* (1)切入点(PointCat)方法(可以是多个):要进行功能扩展的一些点
* (2)通知(Advice)方法(也可以是多个)封装了扩展功能的一些方法(再切入点方法之前或之后要执行的方法)
*/
@Aspect
@Slf4j
@Component
public class SysLogAspect {
/**
* @Pointcut 这个注解描述的方法为切入点方法注解中定义的内容为切入点表达式(可以有多种形式)
* 1)bean(bean名称)切入点表达式,这个表达式中的名称为Spring容器中管理的一个bean的名字
* 2)bean表达式是一种粗粒度的切入点表达式,这种表达式定义的切入点表示bean中的所有方法都是将来
* 要切入功能扩展功能的一些方法(目标方法)
* 在当前应用中sysUserServiceImpl这个名字对应的bean中所有的方法的集合为切入点
*/
@Pointcut("bean(sysUserServiceImpl)")//切入点
public void logPointCut(){}//方法中不写任何内容,只是切入点表达式的载体
/**
* @Around 注解描述的方法为一个通知方法,这个通知我们称为环绕通知,可以在目标方法
* 执行之前或之后做服务增益、在环绕通知方法我们可以自己控制目标方法的调用
* @param jp 连接点对象,此对象封装了要执行的目标方法信息
* @return 目标方法的执行结果
* @throws Throwable 执行目标方法过程中出现的异常
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint jp) throws Throwable {
try {
long t1 = System.currentTimeMillis();//开始执行时间
Object result = jp.proceed();//最终可能要执行目标方法
long t2 = System.currentTimeMillis();//结束执行时间
log.info("目标方法执行时长:{}",t2-t1);
return result;//目标方法执行结果
}catch (Throwable e){
log.error("目标方法执行过程中出现了问题,具体为:",e.getMessage());
throw e;
}
}
}
说明:
@Aspect 注解用于标识描述AOP中的切面类型,基于切面类型构建的对象用于目标对象进行功能扩展或控制目标对象的执行
@Pointcut 注解描述切面中的方法,并定义切面中的切入点(基于特定表达式的方式进行描述)
@Around 注解描述用于切面方法,这样的方法会被认为是一个环绕通知(核心业务方法之前和之后要执行的一个动作)
@Around 注解内部value属性的值为一个切入点表达式或者是切入点表达式的一个引用(这个引用为一个@PointCut注解描述的方法的方法名)
ProceedingJoinPoint类为一个连接点类型,此类型的对象用于封装要执行的目标方法相关的一些信息。只能用于@Around注解描述的方法参数
2.4 扩展业务织入增强分析
2.4.1 基于JDK代理方式实现
假如对象有实现接口,则可以基于JDK为目标对象那个创建代理对象,然后为目标对象进行功能扩展
2.4.2 基于CGLIB代理方式实现
假如对象没有实现接口(当然实现了接口也是可以的),可以基于CGLIB代理方式为目标对象织入功能扩展
说明:目标对象实现了接口也可以基于CGLIB为目标对象创建代理对象
3. Spring AOP编程增强
3.1 切面通知应用增强
3.1.1 通知类型
基于Spring AOP编程中,基于AspectJ框架标准,Spring中定义了五种类型的通知(通知描述的是一种扩展业务),他们分表是:
-
@Before(前置) 通知在目标方法之前执行
-
@AfterReturning 在After之后执行程序正常返回则执行此通知
-
@AfterThrowing 在After出现异常之后执行
-
@After(最终、后置) 在目标方法结束(return或者throw之前)之前结果无论是异常还是非异常结果都要执行
-
@Around 重点掌握 环绕通会先执行
说明:在切面类中使用了什么通知,由业务决定,并不是说,在切面中要把所有通知都写上
3.1.2 通知执行顺序
3.1.3 通知实践过程分析
package com.cy.pj.common.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import sun.java2d.pipe.SpanIterator;
@Component
@Aspect
public class SysTimeAspect {
@Pointcut("bean(sysUserServiceImpl)")
public void doTime(){}
@Before("doTime()")
public void doBefore(){
System.out.println("time doBefore()执行了");
}
@After("doTime()")
public void dpAfter(){
System.out.println("time dpAfter()执行了");
}
/**
* 核心业务正常执行 说明:假如有after,先执行after,再执行Returning
*/
@AfterReturning("doTime()")
public void doAfterReturning(){
System.out.println("time doAfterReturning()执行了");
}
/**
* 核心业务出现异常时执行 说明:假如有after,先执行after,再执行Throwing
*/
@AfterThrowing("doTime()")
public void doAfterThrowing(){
System.out.println("time ddoAfterThrowing()执行了");
}
@Around("doTime()")
public Object doAround(ProceedingJoinPoint jp) throws Throwable {
System.out.println("doAround.before");
try{
Object obj = jp.proceed();
System.out.println("doAround.after");
return obj;
}catch (Throwable e){
System.out.println(e.getMessage());
throw e;
}
}
}
说明:对于@AfterThrowing通知只会在出现异常时才会执行,所以当做一些异常监控时可在此方法中进行代码实现
定义一个异常监控切面,对目标方法进行异常监控,并以日志信息的形式输出异常
package com.cy.pj.common.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
@Component
@Aspect
@Slf4j
public class SysExceptionAspect {
@AfterThrowing(value = "bean(*ServiceImpl)",throwing = "e")
public void handleException(JoinPoint jp,Throwable e){
Class<?> targetClass = jp.getTarget().getClass();//获取对象类型
String className = targetClass.getName();
System.out.println("className="+className);
//获取方法签名对象
MethodSignature s = (MethodSignature)jp.getSignature();
String methodName = s.getName();//获取方法名
System.out.println("name="+methodName);
String targetClassMethod = className+"."+methodName;
log.error("{} exception {}", targetClassMethod,e.getMessage());
}
}
3.2 切入点表达式增强
Spring中通过切入点表达式定义具体切入点,其常用的AOP切入点表达式定义及说明:
指示符 | 作用 |
---|---|
bean | 用于匹配指定bean对象的所有方法 |
within | 用于匹配指定包下所有类的所有方法 |
execution | 用于匹配按指定语法规则匹配到具体方法 |
@annotation | 用于匹配指定注解修饰的方法 |
3.2.1 bean表达式(重点)
bean表达式一般应用于类型级别,实现粗粒度的切入点定义
-
bean(“userServiceImpl”)指定一个userServiceImol类中的所有方法
-
bean("*ServiceImpl")指定所有后缀为ServiceImpl的类中所有方法
说明:bean表达式内部对象是由Spring容器管理的一个bean对象,表达式内部的名字是Spring容器中某个bean的name
3.2.2 within表达式(了解)
within表达式应用于类级别,实现粗粒度的切入点表达式定义,案例分析:
- within(“aop.service.UserServiceImpl”)指定当前包中这个类内部的所有方法。
- within(“aop.service.*”) 指定当前目录下的所有类的所有方法。
- within(“aop.service…*”) 指定当前目录以及子目录中类的所有方法。
within表达式应用场景分析:
1)对所有业务bean都要进行功能增强,但是bean名字又没有规则。
2)按业务模块(不同包下的业务)对bean对象进行业务功能增强。
3.2.3 execution表达式(了解)
execution表达式应用于方法级别,实现细粒度的切入点表达式定义,案例分析:
语法:execution(返回值类型 包名.类名.方法名(参数列表))。
- execution(void aop.service.UserServiceImpl.addUser())匹配addUser方法。
- execution(void aop.service.PersonServiceImpl.addUser(String)) 方法参数必须为String的addUser方法。
- execution(* aop.service….(…)) 万能配置。
3.2.4 @annotation表达式(重点)
-
@annotation(anno.RequiredLog) 匹配有此注解描述的方法。
-
@annotation(anno.RequiredCache) 匹配有此注解描述的方法。
其中:RequiredLog为我们自己定义的注解,当我们使用@RequiredLog注解修饰业务层方法时,系统底层会执行此方法时进行日志扩展操作
练习:定义一个Cache相关切面,使用注解表达式定义切入点,并使用此注解对需要使用cache的业务方法进行描述
第一步:定义注解RequiredCache
package com.cy.pj.common.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 自定义注解,一个特殊的类,所有注解都默认继承Annotation接口 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RequiredCache { }
第二步:定义SysCacheAspect切面对象
package com.cy.pj.common.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Aspect @Component public class SysCacheAspect { //注解方式切入点表达式的定义(细粒度的表达式) @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)") public void doCache(){} @Around("doCache()") public Object around(ProceedingJoinPoint jp) throws Throwable { System.out.println("Get data from cache"); Object obj = jp.proceed(); System.out.println("Put data to cache"); return obj; } }
第三步:使用@RequiredCache注解对特定业务目标对象中的查询方法进行描述。
@RequiredCache @Override public List<Map<String, Object>> findObjects() { List<Map<String, Object>> list= sysDeptDao.findObjects(); if(list==null||list.size()==0) throw new ServiceException("没有部门信息"); return list; }
定义Cache练习,第一次查询会存到Cache中,之后查询都是在Cache中取数据,但是一旦执行更新操作会导致数据更新成功页面不会更新数据的脏读现象!
package com.cy.pj.common.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Aspect @Component public class SysCacheAspect { //假设此对象为存储数据的一个缓存对象 private Map<String,Object> cache = new ConcurrentHashMap<>();//线程安全的map //注解方式切入点表达式的定义(细粒度的表达式) @Pointcut("@annotation(com.cy.pj.common.annotation.RequiredCache)") public void doCache(){} @Pointcut("@annotation(com.cy.pj.common.annotation.ClearCache)") public void doClearCache(){} @AfterReturning("doClearCache()") public void afterReturning (JoinPoint jp){ if (cache!=null){ cache.clear(); } } @Around("doCache()") public Object around(ProceedingJoinPoint jp) throws Throwable { System.out.println("Get data from cache"); Object result = cache.get("deptKey"); if(result!=null)return result; result = jp.proceed(); System.out.println("Put data to cache"); cache.put("deptKey",result); return result; } }
所有再定义一个注解用来如果更新成功先清理Cache,然后再执行查询操作,使用@AfterReturning注解描述在通知方法正常执行之后执行
3.3 切面优先级设置实现
切面的优先级需要借助@Order注解进行描述,数字越小优先级越高,默认优先级比较低
定义日志切面并指定优先级
@Order(1)
@Aspect
@Component
public class SysLogAspect {
…
}
定义缓存切面并指定优先级:
@Order(2)
@Aspect
@Component
public class SysCacheAspect {
…
}
说明:当多个切面作用于同一个目标对象方法时,这些切面狗简称一个切面链,类似过滤器链、拦截器链