简介
什么是AOP
AOP(Aspect Oriented Programming)面向切面编程,它是一种思想,它是对某一类事情的集中处理。比如用户登录权限的效验,没学 AOP 之前,我们所有需要判断用户登录的页面 (中的方法),都要各自实现或调用用户验证的方法,然而有了 AOP 之后,我们只需要在某一处配置一下,所有需要判断用户登录页面 (中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了而AOP 是一种思想,而 Spring AOP 是一个框架,提供了一种对 AOP 思想的实现,它们的关系和loC 与 DI 类似。
AOP是一个思想, SpringAOP是AOP的一种实现, AOP是OOP的补充与完善
OOP: 面向对象编程
AOP: 面相切面编程, 对某一类事情的集中处理
应用
想象一个场景,我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面调用的前端控制器(Controller) 都需要先验证用户登录的状态,那这个时候我们要怎么处理呢?
我们之前的处理方式是每个 Controller 都要写一遍用户登录验证,然而当你的功能越来越多,那么你要写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会代码修改和维护的成本。那有没有简单的处理方案呢?答案是有的,对于这种功能统一,且使用的地方较多的功能,就可以考虑 AOP来统一处理了。
除了统一的用户登录判断之外,AOP 还可以实现:
- 统一日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事务的开启和提交等
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OP (Object Oriented Programming,面向对象编程) 的补充和完善
AOP的组成
切面
切⾯(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包 括了连接点的定义。
_切⾯是包含了:通知、切点和切⾯的类,相当于 AOP 实现的某个功能的集合 _
连接点
应⽤执⾏过程中能够插⼊切⾯的⼀个点,这个点可以是⽅法调⽤时,抛出异常时,甚⾄修改字段 时。切⾯代码可以利⽤这些点插⼊到应⽤的正常流程之中,并添加新的⾏为。
_连接点相当于需要被增强的某个 AOP 功能的所有⽅法。 _
切点
Pointcut 是匹配 Join Point 的谓词。 Pointcut 的作⽤就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述)来匹配 Join Point,给满⾜规则的 Join Point 添加 Advice。
_切点相当于保存了众多连接点的⼀个集合(如果把切点看成⼀个表,⽽连接点就是表中⼀条⼀条的数据) _
通知
切⾯也是有⽬标的 ------它必须完成的⼯作。在 AOP 术语中,切⾯的⼯作被称之为通知。
SpringAOP的使用
引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
写切面
新建包专门放切面
java
@Slf4j
@Component
@Aspect
public class LoginAspect {
@Pointcut("execution(* springaop.controller.UserController.*(..))")
public void pointcut(){}
//前置通知: 通知方法会在目标方法调用之前执行
@Before("pointcut()")
public void doBefore(){
log.info("do before...");
}
//后置通知:通知方法会在目标方法返回或者抛出异常后调用
@After("pointcut()")
public void doAfter(){
log.info("do after...");
}
//返回之后通知:通知方法会在目标方法返回后调用
@AfterReturning("pointcut()")
public void doAfterReturning(){
log.info("do AfterReturning...");
}
//环绕通知:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object oj;
log.info("环绕通知开始之前");
try {
oj = joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
log.info("环绕通知开始之后");
return oj;
}
}
各种通知
各种通知:
- @Before前置通知: 通知方法会在目标方法调用之前执行
- @After后置通知: 通知方法会在目标方法返回或者抛出异常后调用
- @AfterReturning返回之后通知: 知方法会在目标方法返回后调用
- @AfterThrowing抛异常后通知: 通知方法会在目标方法抛出异常后调用
- @Around环绕通知: 通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为
执行顺序:
ProceedingJoinPoint常用方法:
在环绕通知里log.info(joinPoint.getSignature().toLongString());
注意点:
- 当出现异常的时候, @AfterReturning就不执行了
- @Around注意写法, 注意返回值的接收
- 一个切面里可以有多个切点
- 可以直接在注解里写
**execution(.....)**
来匹配切点方法
切点方法注解说明
例子(获取方法执行时间)
java
//环绕通知实现统计方法执行时间
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint){
Object oj;
long start = System.currentTimeMillis();
try {
oj = joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
long end = System.currentTimeMillis();
System.out.println(joinPoint.getSignature().toLongString()+"耗时: "+(end-start));
return oj;
}
SpringAOP的实现原理
实现原理
Spring AOP 是构建在动态代理基础上,因此 Spring 对AOP 的支持局限于方法级别的拦截.
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用AOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类
织入(Weaving):代理的生成时机
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中.
在目标对象的生命周期里有多个点可以进行织入:
**编译期:**
切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。**类加载期:**
切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5的加载时织入 (load-time weaving.LTW) 就支持以这种方式织入切面。**运行期:**
切面在应用运行的某一时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。SpringAOP就是以这种方式织入切面的。
代理
为其他对象提供⼀种代理以控制对这个对象的访问。在某些情况下,⼀个对 象不适合或者不能直接引用另⼀个对象,而代理对象可以在客户端和⽬标对象之间 起到中介的作用。 代理模式分为静态代理和动态代理
静态代理
静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改) 且麻烦(需要对每个目标类都单独写一个代理类)。实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景
上面我们是从实现和应用角度来说的静态代理,从JVM 层面来说,静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件
静态代理实现步骤:
- 定义一个接口及其实现类
- 创建一个代理类同样实现这个接口
- 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法
这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情
举例
①定义接口
②实现接口
③创建代理类, 丙同样实现支付接口
④实际使用
动态代理
相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB动态代理机制)。
从JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
说到动态代理,不得不提的是Spring AOP,它的实现依赖了动态代理
动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助
就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理、CGLIB 动态代理等等。
JDK动态代理
步骤:
- 定义一个接口及其实现类;
- 自定义InvocationHandler 并重写invoke方法,在invoke 方法中我们会调用原生方法(被代理类的方法) 并自定义一些处理逻辑;
- 通过 Proxy.newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h) 方法创建代理对象
示例:
①定义接口及其实现类
java
//接口
public interface PayService {
void pay();
}
//实现类
public class AliPayService implements PayService{
@Override
public void pay() {
System.out.println("Alipay...");
}
}
② 定义JDK动态代理类
java
public class JDKInvocationHandler implements InvocationHandler {
//目标对象即是被代理对象
private final Object target;
//代理对象
public JDKInvocationHandler(Object target){
this.target=target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//安全检查
System.out.println("安全检查");
//记录日志
System.out.println("记录日志");
//时间统计开始
System.out.println("时间统计开始");
//通过反射调用被代理类的方法
Object retVal = method.invoke(target,args);
//时间统计结束
System.out.println("时间统计结束");
return retVal;
}
}
③创建代理对象
java
public static void main(String[] args) {
//动态代理
PayService target = new AliPayService();
//创建一个代理类
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(), //类加载器
new Class[]{PayService.class}, //被代理类实现的一些接口
new JDKInvocationHandler(target) //实现了InvocationHandler接口的对象
);
proxy.pay();
}
缺点:
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。
CGLIB动态代理
JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免
步骤:
- 定义一个类
- 自定义 Methodinterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和JDK 动态代理中的 invoke 方法类似;
- 通过 Enhancer 类的 create()创建代理类
示例:
〇添加依赖(一般Spring集成了不用添加)
xml
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
①定义一个类
②定义MethodInterceptor(方法拦截器)
java
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class CGLIBInterceptor implements MethodInterceptor {
//被代理对象
private final Object target;
public CGLIBInterceptor(Object target){
this.target = target;
}
@Override
public Object intercept(Object o,
Method method,
Object[] args,
MethodProxy methodProxy) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过cglib的代理⽅法调⽤
Object retVal = methodProxy.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
}
③创建代理对象
java
//动态代理CGLIB
PayService target = new AliPayService();
//创建代理对象
PayService proxy = (PayService) Enhancer.create(target.getClass(),new CGLIBInterceptor(target));
proxy.pay();
JDK和CGLIB区别
- JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。
- CGLIB动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final
- 大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更明显
总结
proxyTargetClass默认为false, 表示使用JDK动态代理织入增强. true就用CGLIB, false如果未实现接口用CGLIB,实现了接口用JDK.