1.AOP概述
AOP就是面向切面编程,其实就是面向特定的方法编程。基于动态代理的技术,给特定的方法进行功能增强。
举一个常见的例子,比如想要统计系统中每个方法的运行耗时,如果不使用AOP,就必须给每个方法就加上统计运行时长的代码逻辑,这样会很繁琐,且容易漏。如果使用AOP的话只需要在切面类中增加这个逻辑,使用切入点表达式去指定目标方法,就可以给目标方法都加上这一逻辑,实现我们的统计时长的功能。
2.AOP的核心概念
(1)连接点 :JoinPoint 可以被AOP控制的方法 (暗含方法执行时的相关信息)
(2)通知:Advice,指那些重复的逻辑,及公共的功能,都体现在通知方法中。
(3)切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
(4)切面:Aspect 描述通知与切入点的对应关系(切入点+通知)
(5)目标对象:target,通知所应用的对象
3.AOP的实现原理
Spring
的AOP
实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring
的某个bean
配置了切面,那么Spring
在创建这个bean
的时候,实际上创建的是这个bean
的一个代理对象,我们后续对bean
中方法的调用,实际上调用的是代理类重写的代理方法。而Spring
的AOP
使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
(一)JDK动态代理
Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java
语言的应该会对JDK
动态代理有所了解。JDK
实现动态代理需要两个组件,首先第一个就是InvocationHandler
接口。我们在使用JDK
的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke
方法,这个方法其实就是我们提供的代理方法。然后JDK
动态代理需要使用的第二个组件就是Proxy
这个类,我们可以通过这个类的newProxyInstance
方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke
方法。
(二)CGLib动态代理
JDK
的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK
的动态代理将没有办法使用,于是Spring
会使用CGLib
的动态代理来生成代理对象。CGLib
直接操作字节码,生成类的子类,重写类的方法完成代理。
以上就是Spring
实现动态的两种方式,下面我们具体来谈一谈这两种生成动态代理的方式。
3.1 JDK的动态代理
(一)实现原理
JDK
的动态代理是基于反射实现。JDK
通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler
接口的invoke
方法。并且这个代理类是Proxy类的子类(记住这个结论,后面测试要用)。这就是JDK
动态代理大致的实现方式。
(二)优点
JDK
动态代理是JDK
原生的,不需要任何依赖即可使用;- 通过反射机制生成代理类的速度要比
CGLib
操作字节码生成代理类的速度更快;
(三)缺点
- 如果要使用
JDK
动态代理,被代理的类必须实现了接口,否则无法代理; JDK
动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring
仍然会使用JDK
的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。JDK
动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;
3.2 CGLib动态代理
(一)实现原理
CGLib
实现动态代理的原理是,底层采用了ASM
字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring
中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。
(二)优点
- 使用
CGLib
代理的类,不需要实现接口,因为CGLib
生成的代理类是直接继承自需要被代理的类; CGLib
生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理;CGLib
生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib
执行代理方法的效率要高于JDK
的动态代理;
(三)缺点
- 由于
CGLib
的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final
类,则无法使用CGLib
代理; - 由于
CGLib
实现代理方法的方式是重写父类的方法,所以无法对final
方法,或者private
方法进行代理,因为子类无法重写这些方法; CGLib
生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK
通过反射生成代理类的速度更慢;
4.AOP的通知类型
1.@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
2.@Before:前置通知,此注解标注的通知方法在目标方法前被执行
3.@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行4.@AfterReturning,返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行5.@AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行
注意事项:
1.@Around 环绕通知需要自己调用 Proceeding]oinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
2.@Around 环绕通知方法的返回值,必须指定为0bject,来接收原始方法的返回值。
5. 切入点表达式
Spring AOP 支持以下几种切点表达式类型。
1.execution:
匹配方法切入点。根据表达式描述匹配方法,是最通用的表达式类型,可以匹配方法、类、包。
表达式模式:
execution(modifier? ret-type declaring-type?name-pattern(param-pattern) throws-pattern?)
modifier:匹配修饰符,public, private 等,省略时匹配任意修饰符
ret-type:匹配返回类型,使用 * 匹配任意类型
declaring-type:匹配目标类,省略时匹配任意类型
.. 匹配包及其子包的所有类
name-pattern:匹配方法名称,使用 * 表示通配符
* 匹配任意方法
set* 匹配名称以 set 开头的方法
param-pattern:匹配参数类型和数量
() 匹配没有参数的方法
(..) 匹配有任意数量参数的方法
(*) 匹配有一个任意类型参数的方法
(*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
throws-pattern:匹配抛出异常类型,省略时匹配任意类型
使用示例:
// 匹配public方法
execution(public * *(..))
// 匹配名称以set开头的方法
execution(* set*(..))
// 匹配AccountService接口或类的方法
execution(* com.xyz.service.AccountService.*(..))
// 匹配service包及其子包的类或接口
execution(* com.xyz.service..*(..))
2.within
匹配指定类型。匹配指定类的任意方法,不能匹配接口。
表达式模式:
within(declaring-type)
使用示例
// 匹配service包的类
within(com.xyz.service.*)
// 匹配service包及其子包的类
within(com.xyz.service..*)
// 匹配AccountServiceImpl类
within(com.xyz.service.AccountServiceImpl)
3.this
匹配代理对象实例的类型,匹配在运行时对象的类型。
注意:基于 JDK 动态代理实现的 AOP,
this
不能匹配接口的实现类,因为代理类和实现类并不是同一种类型,详情参阅《Spring中的AOP和动态代理》
4.target
匹配目标对象实例的类型,匹配 AOP 被代理对象的类型。
表达式模式:
target(declaring-type)
使用示例
// 匹配目标对象类型为service包下的类
target(com.xyz.service.*)
// 匹配目标对象类型为service包及其子包下的类
target(com.xyz.service..*)
// 匹配目标对象类型为AccountServiceImpl的类
target(com.xyz.service.AccountServiceImpl)
三种表达式匹配范围如下:
表达式匹配范围 | within | this | target |
---|---|---|---|
接口 | ✘ | ✔ | ✔ |
实现接口的类 | ✔ | 〇 | ✔ |
不实现接口的类 | ✔ | ✔ | ✔ |
5.args
匹配方法参数类型和数量,参数类型可以为指定类型及其子类。
使用
execution
表达式匹配参数时,不能匹配参数类型为子类的方法。
表达式模式:
args(param-pattern)
使用示例
// 匹配参数只有一个且为Serializable类型(或实现Serializable接口的类)
args(java.io.Serializable)
// 匹配参数个数至少有一个且为第一个为Example类型(或实现Example接口的类)
args(cn.codeartist.spring.aop.pointcut.Example,..)
6.bean
通过 bean 的 id 或名称匹配,支持 *
通配符。
bean(bean-name)
使用示例
// 匹配名称以Service结尾的bean
bean(*Service)
// 匹配名称为demoServiceImpl的bean
bean(demoServiceImpl)
7.@within
匹配指定类型是否含有注解。当定义类时使用了注解,该类的方法会被匹配,但在接口上使用注解不匹配。
使用示例:
// 匹配使用了Demo注解的类
@within(cn.codeartist.spring.aop.pointcut.Demo)
8.@target
匹配目标对象实例的类型是否含有注解。当运行时对象实例的类型使用了注解,该类的方法会被匹配,在接口上使用注解不匹配。
使用示例:
// 匹配对象实例使用了Demo注解的类
@target(cn.codeartist.spring.aop.pointcut.Demo)
9.@annotation
匹配方法是否含有注解。当方法上使用了注解,该方法会被匹配,在接口方法上使用注解不匹配。
使用示例:
// 匹配使用了Demo注解的方法
@annotation(cn.codeartist.spring.aop.pointcut.Demo)
10.@args
匹配方法参数类型是否含有注解。当方法的参数类型上使用了注解,该方法会被匹配。
使用示例:
// 匹配参数只有一个且参数类使用了Demo注解
@args(cn.codeartist.spring.aop.pointcut.Demo)
// 匹配参数个数至少有一个且为第一个参数类使用了Demo注解
@args(cn.codeartist.spring.aop.pointcut.Demo,..)
切点表达式的参数匹配
切点表达式中的参数类型,可以和通知方法的参数通过名称绑定,表达式中不需要写类或注解的全路径,而且能直接获取到切面拦截的参数或注解信息。
@Before("pointcut() && args(name,..)")
public void doBefore(String name) {
// 切点表达式增加参数匹配,可以获取到name的信息
}
@Before("@annotation(demo)")
public void doBefore(Demo demo) {
// 这里可以直接获取到Demo注解的信息
}
切点表达式的参数匹配同样适用于
@within, @target, @args
怎样编写一个好的切点表达式?
要使切点的匹配性能达到最佳,编写表达式时,应该尽可能缩小匹配范围,切点表达式分为三大类:
类型表达式:匹配某个特定切入点,如 execution
作用域表达式:匹配某组切入点,如 within
上下文表达式:基于上下文匹配某些切入点,如 this、target 和 @annotation
一个好的切点表达式应该至少包含前两种(类型和作用域)类型。
作用域表达式匹配的性能非常快,所以表达式中尽可能使用作用域类型。
上下文表达式可以基于切入点上下文匹配或在通知中绑定上下文。
单独使用类型表达式或上下文表达式比较消耗性能(时间或内存使用)。
三、切点表达式组合
使用 &&
、||
和 !
来组合多个切点表达式,表示多个表达式“与”、“或”和“非”的逻辑关系。
这可以用来组合多种类型的表达式,来提升匹配效率。
// 匹配doExecution()切点表达式并且参数第一个为Account类型的方法
@Before("doExecution() && args(account,..)")
public void validateAccount(Account account) {
// 自定义逻辑
}
1. 常用注解
@Pointcut 指定切点表达式
2. 切点表达式类型
execution 匹配方法切入点
within 匹配指定类型
this 匹配代理对象实例的类型
target 匹配目标对象实例的类型
args 匹配方法参数
bean 匹配 bean 的 id 或名称
@within 匹配类型是否含有注解
@target 匹配目标对象实例的类型是否含有注解
@annotation 匹配方法是否含有注解
@args 匹配方法参数类型是否含有注解
原文链接:https://blog.youkuaiyun.com/weixin_43793874/article/details/124753521
6.AOP的通知顺序
1.默认按照切面类的字母排序执行,字母排序越小的前置通知先执行,后置通知后执行,反之易然。
2.若需要自定以执行顺序,可以如下图使用@order()注解控制切面类的执行顺序,@order()注解的值越小,前置通知越先执行,后置通知越后执行
7.AOP的使用案例(使用@annotation的切点表达式)
1.导入AOP依赖
<!-- 切面依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.首先需要新建一个注解用于标记目标的方法
package com.test.aspect.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Details 自定义日志记录
*/
@Target(ElementType.METHOD)//目标对象:方法类型
@Retention(RetentionPolicy.RUNTIME)//执行时间,目标方法运行时执行
public @interface WebLog {
//日志级别
String desc() default "info";
//日志信息
String message();
}
3.编写切面类
package com.test.aspect;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.hangyi.aspect.model.WebLog;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
/**
* @Details: @MyLog 注解切面类
*/
@Component
@Aspect
@Slf4j
public class WebLogAspect {
@Pointcut("@annotation(com.hangyi.aspect.annotation.WebLog)")
private void webLog(){}
@Around("webLog()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//开始时间
Long starTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
//结束时间
long endTime= System.currentTimeMillis();
System.out.print("方法运行耗时:"+endTime-starTime);
}
4.在需要记录方法耗时的方法中加入步骤一新增的注解
package com.test.controller;
import com.hangyi.aspect.annotation.WebLog;
import com.hangyi.test.CommonResult;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@WebLog(message = "健康测试")
@GetMapping("/healthTest")
@ApiOperation(value="健康测试")
public CommonResult<String> getTest(){
return CommonResult.success("healthTest");
}
}
此时该运行该方法时会运行对应的切面类,记录方法运行时间。