简单介绍:
AOP:Aspect Oriented Programming (面向切面编程、面向方面编程),其实就是面向特定方法编程。
场景:
比如现在有一个需求,我要统计每一个业务方法的耗时时长,
我们只需在业务方法的前面获取一个开始时间,在方法的后面再获取一个结束时间,然后将两时间相减就能得出耗时时长。
这样做当然没问题,但是在一个项目中,业务方法是很多的,如果每次都这样操作,那工作量是非常大的,我们需要换一种方式实现。
我们就可以用到springAop技术
核心:在不惊动原始代码的基础上增强功能。
我们需要统计业务方法的耗时,可以创建一个模板方法,将记录耗时时长的公共的代码放入模板方法。
原始方法就是业务方法,面向这种特定方法的编程就是面向切面编程。
这个模板方法中定义的逻辑就是创建出来的代理对象方法的逻辑。
实现:
动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
编写AOP程序:
针对特定方法根据业务需要进行编程
我们需要创建一个切面类,在类中定义模板方法,并把类交给spring管理
@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {
@Around("execution(* com.itheima.service.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();
//执行原始方法
Object result = pjp.proceed();
//记录方法执行结束时间
long end = System.currentTimeMillis();
//计算方法执行耗时
log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);
return result;
}
}
pjp.proceed() 执行原始方法,原始方法可能有返回值,所以最后要把返回值retrun。通过pjp.getSignature() 可以获取原始方法的名称,这样就可以具体知道是哪个方法耗时的时间。
在@Around注解中有个excution表达式,表示对哪些方法进行增强,* com.itheima.service.*.*(..),
* 表示方法的返回值 ,后面的是包名,在任意接口的任意方法上执行。(..)表示方法的形参也是任意的。
AOP核心概念:
①连接点:JointPoint ,可以被AOP控制的方法(暗含方法执行时的相关信息)。
②通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)。
③切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用。
④切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
⑤目标对象:Target,通知所应用的对象
AOP执行流程:
定义好切入点表达式后,在程序运行时,SpringAOP会自动地基于动态代理技术为目标对象生成一个对应的代理对象,也就是上图的DeptServiceProxy对象, 在对象中已经做了对业务方法的增强。此时,再进行service注入的时候,spring就不会注入原始的目标对象,而是注入生成出来的代理对象,此代理对象已经对业务方法做了增强的处理。
AOP进阶:
Spring中AOP的通知类型:
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行
注意:
① :@Around环绕通知需要自己调用ProceedingJoinPoint.proceed() 来让原始方法执行,其它通知不需要考虑目标方法执行。
②:@Aound环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。
五种通知类型代码演示:
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* com.itheima.service.*.*(..))")
private void pt(){
}
//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常
//后续代码不在执行
log.info("around after ...");
return result;
}
//后置通知
@After("pt()")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
通知顺序:
默认按照切面类的类名字母排序:
-
目标方法前的通知方法:字母排名靠前的先执行
-
目标方法后的通知方法:字母排名靠前的后执行
一般我们通过@Order注解可以控制通知的执行顺序
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
切入点表达式:
作用:主要用来决定项目中的哪些方法需要加入通知。
它的常见形式有两种:
①execution(……):
根据方法的签名来匹配
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
其中带
?
的表示可以省略的部分
访问修饰符:可省略(比如: public、protected)
包名.类名: 可省略
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
示例:
@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
②@annotation(……) :
根据注解匹配
自定义注解:Log 一般我们记录操作日志需要用到 自定义注解 + 环绕通知
//自定义注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
public String title() ; // 模块名称
public OperatorType operatorType() default OperatorType.MANAGE; // 操作人类别
public int businessType() ; // 业务类型(0其它 1新增 2修改 3删除)
public boolean isSaveRequestData() default true; // 是否保存请求的参数
public boolean isSaveResponseData() default true; // 是否保存响应的参数
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(value = LogAspect.class) // 通过Import注解导入日志切面类到Spring容器中
public @interface EnableLogAspect { //这个注解可以添加到启动类上,程序启动时可以扫描到LogAspect这个切面类
}
@Aspect
@Component
@Slf4j
public class LogAspect { //环绕通知切面类定义
@Autowired
private AsyncOperLogService asyncOperLogService;
@Around("@annotation(sysLog)")
public Object doAroundAdvice(ProceedingJoinPoint joinPoint, Log sysLog){
// 构建前置参数
SysOperLog sysOperLog = new SysOperLog() ;
LogUtil.beforeHandleLog(sysLog, joinPoint, sysOperLog); //封装日志信息
Object proceed = null;
try {
proceed = joinPoint.proceed(); // 执行业务方法
LogUtil.afterHandlLog(sysLog,proceed, sysOperLog, 0, null); //封装日志信息
} catch (Throwable e) { // 代码执行进入到catch中,业务方法执行产生异常
e.printStackTrace();
LogUtil.afterHandlLog(sysLog,proceed, sysOperLog, 1, e.getMessage()); //封装日志信息
throw new RuntimeException();
}
// 保存日志数据
asyncOperLogService.saveSysOperLog(sysOperLog);
return proceed ;
}
}
连接点:
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
@Slf4j
@Component
@Aspect
public class MyAspect7 {
@Pointcut("@annotation(com.itheima.anno.MyLog)")
private void pt(){}
//前置通知
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ...");
}
//后置通知
@Before("pt()")
public void after(JoinPoint joinPoint){
log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//获取目标类名
String name = pjp.getTarget().getClass().getName();
log.info("目标类名:{}",name);
//目标方法名
String methodName = pjp.getSignature().getName();
log.info("目标方法名:{}",methodName);
//获取方法执行时需要的参数
Object[] args = pjp.getArgs();
log.info("目标方法参数:{}", Arrays.toString(args));
//执行原始方法
Object returnValue = pjp.proceed();
return returnValue;
}
}