AOP入门

  1. 1. 1. Spring 的 AOP 简介
    1. 1.1. 1.1 什么是 AOP
    2. 1.2. 1.2 AOP 的作用及其优势
    3. 1.3. 1.3 AOP 的底层实现
    4. 1.4. 1.4 AOP 的动态代理技术
    5. 1.5. 1.5 AOP 相关概念
    6. 1.6. 1.6 AOP 开发明确的事项
      1. 1.6.1. 1.6.1 需要编写的内容
      2. 1.6.2. 1.6.2 AOP 技术实现的内容
      3. 1.6.3. 1.6.3 AOP 底层使用哪种代理方式
    7. 1.7. 1.7 知识要点
  2. 2. 2. 导入依赖
  3. 3. 3. 切点表达式的写法
    1. 3.1. 3.1 表达式语法:
    2. 3.2. 3.2 Spring AOP支持的切入点指示符:
  4. 4. 4. 基于注解的 AOP 开发
    1. 4.1. 4.1 基于注解的aop开发步骤
    2. 4.2. 4.2 使用步骤
    3. 4.3. 4.3 AOP底层实现
    4. 4.4. 4.4 快速入门
  5. 5. 5. 为什么用aop
  6. 6. 6. aop原理

1. Spring 的 AOP 简介

1.1 什么是 AOP

​ AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

​ AOP 是 OOP 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

1.2 AOP 的作用及其优势

  1. 作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强
  2. 优势:减少重复代码,提高开发效率,并且便于维护

1.3 AOP 的底层实现

​ 实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。

1.4 AOP 的动态代理技术

动态代理是一种常用的设计模式,广泛应用于框架中,Spring框架的AOP特性就是应用动态代理实现的。

常用的动态代理技术

  1. 实现动态代理有两种形式:

    1. jdk动态代理:根据目标类接口获取代理类实现规则,生成代理对象。这个代理对象,也是目标类接口的一个实现类。
    2. cglib动态代理:根据目标类本身获取代理类实现规则,生成代理对象。这个代理对象,也是目标类的一个子类。 (如果目标类为final,则不能使用CGLib实现动态代理)

    SpringAOP可以自动在jdk动态代理和CGLib动态代理之间进行切换,规则如下:

    1. 如果目标对象实现了接口,采用jdk动态代理实现aop。

    2. 如果目标对象没有实现接口,采用CGLib动态代理实现aop。

    3. 如果目标对象实现了接口,但仍然想要使用CGLIB实现aop,可以手动进行配置。

      cglib动态代理:

      注意:

      1. dao层不能再实现接口。
      2. service层不能再实现接口。

1.5 AOP 相关概念

  • Target(目标对象):代理的目标对象。
  • Proxy (代理):一个类被 AOP 织入增强后,就产生一个结果代理类。
  • Joinpoint(连接点):所谓连接点是指那些被拦截到的点。在spring中,这些点指的是方法,因为spring只支持方法类型的连接点。
  • Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
  • Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
  • Aspect(切面):是切入点和通知(引介)的结合。
  • Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织入, 而 AspectJ 采用编译期织入和类装载期织入。

1.6 AOP 开发明确的事项

1.6.1 需要编写的内容

​ 1. 编写核心业务代码(目标类的目标方法)

​ 2. 编写切面类,切面类中有通知(增强功能方法)

​ 3. 在配置文件中,配置织入关系,即将哪些通知与哪些连接点进行结合

1.6.2 AOP 技术实现的内容

​ Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

1.6.3 AOP 底层使用哪种代理方式

​ 在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。

1.7 知识要点

  1. aop:面向切面编程
  2. aop底层实现:基于JDK的动态代理 和 基于Cglib的动态代理
  3. aop的重点概念:
    * Pointcut(切入点):被增强的方法
    * Advice(通知/ 增强):封装增强业务逻辑的方法
    * Aspect(切面):切点+通知
    * Weaving(织入):将切点与通知结合的过程
  4. 开发明确事项:
    1. 谁是切点(切点表达式配置)
    2. 谁是通知(切面类中的增强方法)
    3. 将切点和通知进行织入配置

2. 导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--导入spring的context坐标,context依赖aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.14</version>
</dependency>

<!-- 添加aspectj依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>

​ Aspectj是一个基于java的、面向切面的AOP框架。Spring2.0之后增加了对Aspectj切点表达式的支持。而实际开发中一般都使用Aspectj方式来实现AOP。所以还要导入Aspectj相关jar包。

3. 切点表达式的写法

3.1 表达式语法:

execution([修饰符] 返回值类型 包名.类名.方法名(参数))

  1. 访问修饰符可以省略
  2. 返回值类型、包名、类名、方法名可以使用星号* 代表任意
  3. 包名与类名之间一个点 . 代表当前包下的类,两个点 .. 表示当前包及其子包下的类
  4. 参数列表可以使用两个点 .. 表示任意个数,任意类型的参数列表

例如:

1
2
3
4
5
execution(public void com.itheima.aop.Target.method())
execution(void com.itheima.aop.Target.*(..))
execution(* com.itheima.aop.*.*(..))
execution(* com.itheima.aop..*.*(..))
execution(* *..*.*(..))
1
2
3
4
5
6
7
/*
* 例如:execution(* com.xxxx.service.impl.*.*(..))
* 第一个 *:所有的返回值类型
* 第二个 *:所有的类
* 第三个 *:所有的方法
* 第四个 .. :所有的参数
*/

3.2 Spring AOP支持的切入点指示符:

(1)execution:用来匹配执行方法的连接点

A:@Pointcut(“execution(* com.aijava.springcode.service..*.*(..))”)

1
第一个*表示匹配任意的方法返回值,..(两个点)表示零个或多个,上面的第一个..表示service包及其子包,第二个*表示所有类,第三个*表示所有方法,第二个..表示

B:@Pointcut(“within(com.aijava.springcode.service.*)”)

within限定匹配方法的连接点,上面的就是表示匹配service包下的任意连接点

C:@Pointcut(“this(com.aijava.springcode.service.UserService)”)

this用来限定AOP代理必须是指定类型的实例,如上,指定了一个特定的实例,就是UserService

D:@Pointcut(“bean(userService)”)

bean也是非常常用的,bean可以指定IOC容器中的bean的名称

4. 基于注解的 AOP 开发

4.1 基于注解的aop开发步骤

​ ① 创建目标接口和目标类(内部有切点)
​ ② 创建切面类(内部有增强方法)
​ ③ 将目标类和切面类的对象创建权交给 spring
​ ④ 在切面类中使用注解配置织入关系
​ ⑤ 在配置文件中开启组件扫描和 AOP 的自动代理
​ ⑥ 测试

4.2 使用步骤

  1. @EnableAspectJAutoProxy(proxyTargetClass = false):声明自动为spring容器中那些配置@Aspect切面的bean创建代理,织入切面。开启基于注解的aop模式
  2. @Aspect:定义切面类,切面类里定义通知
  3. @PointCut 切入点,可以写切入点表达式,指定在哪个方法切入
  4. JoinPoint:连接点,是一个类,配合通知使用,用于获取切入的点的信息
  5. 通知的类型:
名称 注解 说明
前置通知 @Before 用于配置前置通知。指定增强的方法在切入点方法之前执行
后置通知 @AfterReturning 用于配置后置通知。指定增强的方法在切入点方法之后执行
环绕通知 @Around 用于配置环绕通知。指定增强的方法在切入点方法之前和之后都执行
异常抛出通知 @AfterThrowing 用 用于配置异常抛出通知。指定增强的方法在出现异常时执行
最终通知 @After 用于配置最终通知。无论增强方式执行是否有异常都会执行

​ 注: 连接点(JoinPoint 就是要织入的业务方法)

​ 在实际开发中,除了业务逻辑这个主要功能之外,还需要处理许多辅助功能。 比如:日志、异常处理、事务、输入验证、安全等等,我们将这些代码称为:方面代码。而方面代码,就是我们要抽取出来的。

下面抽取日志方面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.xxxx.advice;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect //@Aspect定义此类为方面代码,即是一个通知。
public class MyAdvice {
@Before("execution(* com.neusoft.service.impl.*.*(..))")
public void beforeMethod(JoinPoint joinpoint){
System.out.println("【前置通知日志】" + joinpoint.toString());
}
}
  1. @Aspect注解:定义此类为方面代码,即是一个通知。
  2. @Before注解:定义一个前置通知。即在目标方法执行前切入此注解标注的方法。
  3. execution() 是一个Aspect表达式,语法为:execution(返回值类型 包名.类名.方法名 (参数) 异常)

4.3 AOP底层实现

Jdk代理:基于接口的代理,一定是基于接口,会生成目标对象的接口的子对象。

Cglib代理:基于类的代理,不需要基于接口,会生成目标对象的子对象。

  1. 注解@EnableAspectJAutoProxy开启代理;

  2. 如果属性proxyTargetClass默认为false, 表示使用jdk动态代理织入增强;

  3. 如果属性proxyTargetClass设置为true,表示使用Cglib动态代理技术织入增强;

  4. 如果属性proxyTargetClass设置为false,但是目标类没有声明接口,

  5. Spring aop还是会使用Cglib动态代理,也就是说非接口的类要生成代理都用Cglib。

4.4 快速入门

​ 通过注解**@Pointcut*定义切点,pointCut*()只是一个标识,无所谓是什么,

​ 方法中内容本身也是空的,使用该切点的地方直接通过标识 pointCut()引用切点表达式。

Operator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.xxxx.test;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class Operator {
@Pointcut("execution(* com.xxxx.service.*.*(..))")
public void pointCut(){}

@Before("pointCut()")
public void doBefore(JoinPoint joinPoint){
System.out.println("AOP Before Advice...");
}


@AfterReturning(pointcut="pointCut()",returning="returnVal")
public void afterReturn(JoinPoint joinPoint,Object returnVal){
System.out.println("AOP AfterReturning Advice:" + returnVal);
}

@After("pointCut()")
public void doAfter(JoinPoint joinPoint){
System.out.println("AOP After Advice...");
}


@AfterThrowing(pointcut="pointCut()",throwing="error")
public void afterThrowing(JoinPoint joinPoint,Throwable error){
System.out.println("AOP AfterThrowing Advice..." + error);
System.out.println("AfterThrowing...");
}

// @Around("pointCut()")
public void around(ProceedingJoinPoint pjp){
System.out.println("AOP Aronud before...");
try {
pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("AOP Aronud after...");
}
}

注意:

  1. 如果配置了环绕通知,那么业务方法的执行将在环绕通知中的obj = pjp.proceed();这段代码时开始执行。此时要注意:如果环绕通知方法不写返回值,或者obj = pjp.proceed()这段代码如果不取得返回值,那么返回通知也不能取得返回值。
  2. 有了环绕通知,异常通知也将失去作用。
  3. 实际上,如果要配置环绕通知,那么其他通知就失去意义了。因为在环绕通知中,也可以在方法执行前、执行后做方面代码,包括获取返回值、做异常处理等。
UserService.java
1
2
3
4
5
6
7
8
9
10
11
package com.xxxx.service;

import org.springframework.stereotype.Service;

@Service("userService")
public class UserService {
public void add(){
System.out.println("UserService add()");
}
}

测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.xxxx.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.xxxx.service.UserService;

public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
UserService userService = (UserService) ctx.getBean("userService");
userService.add();
}
}

注解版的配置文件,相当于xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.xxxx.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;

/**
* 指定该类是一个spring配置类,相当于一个配置文件
* ComponentScan:组件扫描
* Import:引入JdbcConfig.class文件
*/
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false) //少了这个aop功能失效
@ComponentScan(basePackages = "com.xxxx")
@Import(JdbcConfig.class)
public class SpringConfig {

}

运行结果:

1
2
3
4
5
6
AOP Before Advice...
UserService add()
AOP AfterReturning Advice:null
AOP After Advice...

Process finished with exit code 0

5. 为什么用aop

​ 1. 就是为了方便,看一个国外很有名的大师说,编程的人都是“懒人”,因为他把自己做的事情都让程序做了。用了aop能让你少写很多代码,这点就够充分了吧

  2. 就是为了更清晰的逻辑,可以让你的业务逻辑去关注自己本身的业务,而不去想一些其他的事情,这些其他的事情包括:安全,事物,日志等。

  3. 那些AOP的术语

  初看这么多术语,一下子都不好接受,慢慢来,很快就会搞懂。

  1.通知(Advice)

  就是你想要的功能,也就是上面说的 安全,事物,日志等。你给先定义好把,然后在想用的地方用一下。

  2.连接点(JoinPoint)

  这个更好解释了,就是spring允许你使用通知的地方,那可真就多了,基本每个方法的前,后(两者都有也行),或抛出异常时都可以是连接点,spring只支持方法连接点.其他如aspectJ还可以让你在构造器或属性注入时都行,不过那不是咱关注的,只要记住,和方法有关的前前后后(抛出异常),都是连接点。

  3.切入点(Pointcut)

  上面说的连接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有几十个连接点了对把,但是你并不想在所有方法附近都使用通知(使用叫织入,以后再说),你只想让其中的几个,在调用这几个方法之前,之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法。

  4.切面(Aspect)

  切面是通知和切入点的结合。现在发现了吧,没连接点什么事情,连接点就是为了让你好理解切点,搞出来的,明白这个概念就行了。通知说明了干什么和什么时候干(什么时候通过方法名中的before,after,around等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。

  5.引入(introduction)

  允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗

  6.目标(target)

  引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

  7.代理(proxy)

  怎么实现整套aop机制的,都是通过代理,这个一会给细说。

  8.织入(weaving)

  把切面应用到目标对象来创建新的代理对象的过程。有3种方式,spring采用的是运行时,为什么是运行时,后面解释。

  关键就是:切点定义了哪些连接点会得到通知

6. aop原理

  spring用代理类包裹切面,把他们织入到Spring管理的bean中。也就是说代理类伪装成目标类,它会截取对目标类中方法的调用,让调用者对目标类的调用都先变成调用伪装类,伪装类中就先执行了切面,再把调用转发给真正的目标bean。

  现在可以自己想一想,怎么搞出来这个伪装类,才不会被调用者发现(过JVM的检查,JAVA是强类型检查,哪里都要检查类型)。

  1.实现和目标类相同的接口,我也实现和你一样的接口,反正上层都是接口级别的调用,这样我就伪装成了和目标类一样的类(实现了同一接口,咱是兄弟了),也就逃过了类型检查,到java运行期的时候,利用多态的后期绑定(所以spring采用运行时),伪装类(代理类)就变成了接口的真正实现,而他里面包裹了真实的那个目标类,最后实现具体功能的还是目标类,只不过伪装类在之前干了点事情(写日志,安全检查,事物等)。

  这就好比,一个人让你办件事,每次这个时候,你弟弟就会先出来,当然他分不出来了,以为是你,你这个弟弟虽然办不了这事,但是他知道你能办,所以就答应下来了,并且收了点礼物(写日志),收完礼物了,给把事给人家办了啊,所以你弟弟又找你这个哥哥来了,最后把这是办了的还是你自己。但是你自己并不知道你弟弟已经收礼物了,你只是专心把这件事情做好。

  顺着这个思路想,要是本身这个类就没实现一个接口呢,你怎么伪装我,我就压根没有机会让你搞出这个双胞胎的弟弟,那么就用第2种代理方式,创建一个目标类的子类,生个儿子,让儿子伪装我

  2.生成子类调用,这次用子类来做为伪装类,当然这样也能逃过JVM的强类型检查,我继承的吗,当然查不出来了,子类重写了目标类的所有方法,当然在这些重写的方法中,不仅实现了目标类的功能,还在这些功能之前,实现了一些其他的(写日志,安全检查,事物等)。

  这次的对比就是,儿子先从爸爸那把本事都学会了,所有人都找儿子办事情,但是儿子每次办和爸爸同样的事之前,都要收点小礼物(写日志),然后才去办真正的事。当然爸爸是不知道儿子这么干的了。这里就有件事情要说,某些本事是爸爸独有的(final的),儿子学不了,学不了就办不了这件事,办不了这个事情,自然就不能收人家礼了。

  前一种兄弟模式,spring会使用JDK的java.lang.reflect.Proxy类,它允许Spring动态生成一个新类来实现必要的接口,织入通知,并且把对这些接口的任何调用都转发到目标类。

  后一种父子模式,spring使用CGLIB库生成目标类的一个子类,在创建这个子类的时候,spring织入通知,并且把对这个子类的调用委托到目标类。

  相比之下,还是兄弟模式好些,他能更好的实现松耦合,尤其在今天都高喊着面向接口编程的情况下,父子模式只是在没有实现接口的时候,也能织入通知,应当做一种例外。

————————————————
AOP部分原文链接:https://blog.csdn.net/zzpitheilang/article/details/83634727