Spring 的 AOP 简介

什么是 AOP

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

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

AOP 的作用及其优势

作用:在程序运行期间,在不修改源码的情况下对方法进行功能增强

优势:减少重复代码,提高开发效率,并且便于维护

AOP 的底层实现

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

AOP 相关术语

在正式讲解 AOP 的操作之前,我们必须理解 AOP 的相关术语,常用的术语如下:

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

其中,重要的几个概念:

**目标对象target:**计划被增强的对象

切点pointcut:计划被增强的对象中的方法

通知advice:附加功能

切面类:通知(增强功能)所在的类,称为切面类

切面aspect:切点(要增强的方法)+通知(增强的功能) = 切面。

1592704730869

基于AOP的开发步骤

1.编写要增强的类,该类称为目标类

2.编写附加功能,附加功能要编写到一个类中。附加功能称为通知,附加功能所在的类被称为切面类

3.将目标类和切面类中的通知配置到一块

基于 XML 的 AOP 开发

快速入门

导入maven库

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
<dependencies>
<!--导入spring的context坐标,context依赖aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<!-- aspectj的织入 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>

<!--Spring集成Junit测试-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

创建目标接口和目标类

在cn.itcast.aop包中新建

1
2
3
4
5
6
7
8
9
10
11
public interface TargetInterface {
public void save();
}

public class Target implements TargetInterface {
@Override
//save方法就是切点,即计划被增强的方法
public void save() {
System.out.println("save()方法正在执行,保存数据中");
}
}

创建切面类以及通知

在cn.itcast.aop包中新建

1
2
3
4
5
6
7
8
9
10
11
package cn.itcast.aop;

public class MyAspect {
//附加功能,被称为通知,通知所在的类称为切面类
public void startTransactional(){
System.out.println("开启事物");
}
public void submitTransactional(){
System.out.println("提交事物");
}
}

将目标类和切面类的对象创建权交给spring

1
2
3
4
<!--    要增强的对象-->
<bean id="target" class="cn.itcast.aop.Target"></bean>
<!-- 附加功能-->
<bean id="myAspect" class="cn.itcast.aop.MyAspect"></bean>

在 applicationContext.xml 中配置切面

需要先导入aop命名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
">

<!--要被增强的对象,目标对象-->
<bean class="cn.itcast.aop.Target"></bean>
<!--切面类,里面封装了附加功能-->
<bean id="myAspect" class="cn.itcast.aop.MyAspect"></bean>

<!--配置AOP-->
<aop:config >
<aop:aspect ref="myAspect">
<!--配置切面 切面 = 通知(附加功能)+切入点(被增强的方法)-->
<aop:before method="startTransactional" pointcut="execution(public void cn.itcast.aop.Target.save())"></aop:before>
<aop:after method="submitTransactional" pointcut="execution(public void cn.itcast.aop.Target.save())"></aop:after>
</aop:aspect>
</aop:config>
</beans>

上面代码切入点表达式写了两次,可以抽取出公共的

1
2
3
4
5
6
7
8
9
10
<!--配置AOP-->
<aop:config >
<aop:aspect ref="myAspect">
<aop:pointcut id="pc" expression="execution(public void cn.itcast.aop.Target.save())"/>

<!--配置切面 切面 = 通知(附加功能)+切入点(被增强的方法)-->
<aop:before method="startTransactional" pointcut-ref="pc"></aop:before>
<aop:after method="submitTransactional" pointcut-ref="pc" ></aop:after>
</aop:aspect>
</aop:config>

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import cn.itcast.target.TargetInterface;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class T {

@Autowired
private TargetInterface target;

@Test
public void 入门案例测试(){
target.save();
}
}

测试结果

1
2
3
开启事物
save()方法正在执行,保存数据中
提交事物

可能出现的异常

1
Unsatisfied dependency expressed through field 'target'; nested exception is org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'cn.itcast.aop.Target#0' is expected to be of type 'cn.itcast.aop.Target' but was actually of type 'com.sun.proxy.$Proxy14'

原因:默认spring使用JDK代理,基于接口, @Autowired private TargetInterface target; 这里要用接口接受代理对象

XML 配置 AOP 详解

切点表达式的写法

表达式语法:

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

例如:

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(* *..*.*(..))

1592710544340

切入点的三种配置方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<aop:config>
<!--方式1:配置公共切入点-->
<aop:pointcut id="pt1" expression="execution(* *(..))"/>
<aop:aspect ref="myAdvice">
<!--方式2:配置局部切入点-->
<aop:pointcut id="pt2" expression="execution(* *(..))"/>
<!--引用公共切入点-->
<aop:before method="logAdvice" pointcut-ref="pt1"/>
<!--引用局部切入点-->
<aop:before method="logAdvice" pointcut-ref="pt2"/>
<!--方式3:直接配置切入点-->
<aop:before method="logAdvice" pointcut="execution(* *(..))"/>
</aop:aspect>
</aop:config>

通知的类型

通知的配置语法:

1
<aop:通知类型 method=“切面类中方法名” pointcut=“切点表达式"></aop:通知类型>

环绕通知相当于将要写到配置文件里的通知(前置、后置、异常、最终)整合到一个方法里了
如果在XML中不配置其他四种通知,那么可以使用java代码来配置其他四种通知。

img

环绕通知

1
2
3
4
5
6
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//TODO 附加功能
Object ret = pjp.proceed(pjp.getArgs); //调用被增强的方法
//TODO 附加功能
return ret;
}

在通知中获取目标方法的实参

格式

1
2
3
public void before(JoinPoint jp) throws Throwable {
Object[] args = jp.getArgs();
}

测试步骤

1.创建实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.itcast.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String name;
private Integer age;
}

2.创建目标类

1
2
3
4
5
6
7
package cn.itcast.aop;

import cn.itcast.domain.User;

public interface TargetInterface {
public void save(User user);
}
1
2
3
4
5
6
7
8
9
10
11
package cn.itcast.aop;

import cn.itcast.domain.User;

public class Target implements TargetInterface {
//save方法就是切点,即计划被增强的方法
@Override
public void save(User user) {
System.out.println("save()方法正在执行,保存数据中,姓名:"+user.getName()+"年龄"+user.getAge());
}
}

3.创建切面类

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.itcast.aop;

import org.aspectj.lang.JoinPoint;

import java.util.Arrays;

public class MyAspect {
//附加功能,被称为通知,通知所在的类称为切面类
public void log(JoinPoint jp){//获取目标方法save中的参数
Object[] args = jp.getArgs();
System.out.println("目标方法的实参是:"+ Arrays.toString(args));
}
}

4.修改切入点

因为save方法多了参数所以参数要写..

1
<aop:pointcut id="pc" expression="execution(public void cn.itcast.aop.Target.save(..))"/>

5.测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import cn.itcast.aop.TargetInterface;
import cn.itcast.domain.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class T {

@Autowired
private TargetInterface target;

@Test
public void 入门案例测试(){
target.save(new User("tom",3));
}
}

6.输出结果

1
2
save()方法正在执行,保存数据中,姓名:tom年龄3
目标方法的实参是:[User(name=tom, age=3)]

在通知中获取目标方法的返回值

1592721296767

格式,只能用在afterReturning通知中

1
2
3
public void afterReturning(Object result) {
System.out.println(result);
}

代码

1.修改TargetInterafce中的save方法添加返回值类型为int

1
2
3
4
5
6
7
package cn.itcast.aop;

import cn.itcast.domain.User;

public interface TargetInterface {
public int save(User user);
}
1
2
3
4
5
6
7
8
9
10
11
12
package cn.itcast.aop;

import cn.itcast.domain.User;

public class Target implements TargetInterface {
//save方法就是切点,即计划被增强的方法
@Override
public int save(User user) {
System.out.println("save()方法正在执行,保存数据中,姓名:"+user.getName()+"年龄"+user.getAge());
return 1;
}
}

2.修改配置文件

配置文件中的切点表达式返回值现在为void,改成int,并且修改通知类型为after-returning

1
2
3
4
<!--配置切入点-->
<aop:pointcut id="pc" expression="execution(public int cn.itcast.aop.Target.save(..))"/>
<!--<aop:before method="startTrasaction" pointcut-ref="pc"></aop:before>-->
<aop:after-returning method="log" pointcut-ref="pc" returning="result"></aop:after-returning>

3.修改切面类,获取返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.itcast.aop;

import org.aspectj.lang.JoinPoint;

import java.util.Arrays;

public class MyAspect {
public void log(JoinPoint jp,Object result){
Object[] args = jp.getArgs();
System.out.println("目标方法的实参是:"+ Arrays.toString(args));
System.out.println("目标方法的返回值是:"+ result);
}
}

在通知中获取目标方法抛出的异常信息

方式1,在通知类中使用Throwable中接收异常信息

aop配置

1
2
3
4
<aop:aspect ref="myAdvice">
<aop:pointcut id="pt4" expression="execution(* *(..)) "/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pt4" throwing="t"/>
</aop:aspect>

通知类

1
2
3
public void afterThrowing(Throwable t){
System.out.println(t.getMessage());
}

方式2,使用环绕通知,直接使用trycatch捕获

1
2
3
4
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed(); //对此处调用进行try……catch……捕获异常,或抛出异常
return ret;
}
1
2
3
4
<aop:aspect ref="myAdvice">
<aop:pointcut id="pt4" expression="execution(* *(..)) "/>
<aop:around method="around" pointcut-ref="pt4" />
</aop:aspect>

基于注解的 AOP 开发

快速入门

创建目标接口和目标类(内部有切点)

1
2
3
4
5
6
7
8
9
10
public interface TargetInterface {
public void method();
}

public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}

创建切面类

1
2
3
4
5
6
public class MyAspect {
//前置增强方法
public void before(){
System.out.println("前置代码增强.....");
}
}

将目标类和切面类的对象创建权交给 spring

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component("target")
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}
@Component("myAspect")
public class MyAspect {
public void before(){
System.out.println("前置代码增强.....");
}
}

在切面类中使用注解配置织入关系

1
2
3
4
5
6
7
8
@Component("myAspect")
@Aspect
public class MyAspect {
@Before("execution(* com.itheima.aop.*.*(..))")
public void before(){
System.out.println("前置代码增强.....");
}
}

或者把切点表达式抽取出来

1
2
3
4
5
6
7
8
9
10
11
@Component("myAspect")
@Aspect
public class MyAspect {
@Pointcut(""execution(* com.itheima.aop.*.*(..))"")
public void pt(){}

@Before("pt()")
public void before(){
System.out.println("前置代码增强.....");
}
}

在配置文类中开启组件扫描和 AOP 的自动代理

1
2
3
4
5
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy
public class SpringConfig {
}

测试代码

1
2
3
4
5
6
7
8
9
10
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=SpringConfig.class)
public class AopTest {
@Autowired
private TargetInterface target;
@Test
public void test1(){
target.method();
}
}

测试结果

img

注解通知的类型

通知的配置语法:@通知注解(“切点表达式”)

img

AOP使用XML配置情况下,通知的执行顺序由配置顺序决定,在注解情况下由于不存在配置顺序的概念的概念,参照通知所配置的方法名字符串对应的编码值顺序,可以简单理解为字母排序

  • 同一个通知类中,相同通知类型以方法名排序为准
  • 不同通知类中,以类名排序为准
  • 使用@Order注解通过变更bean的加载顺序改变通知的加载顺序

企业开发经验

  • 通知方法名由3部分组成,分别是前缀、顺序编码、功能描述
  • 前缀为固定字符串,例如baidu、itheima等,无实际意义
  • 顺序编码为6位以内的整数,通常3位即可,不足位补0
  • 功能描述为该方法对应的实际通知功能,例如exception、strLenCheck

综合案例

案例介绍

对项目进行业务层接口执行监控,测量业务层接口的执行效率

1
2
3
4
5
6
7
public interface AccountService {
void save(Account account);
void delete(Integer id);
void update(Account account);
List<Account> findAll();
Account findById(Integer id);
}

案例分析

  • 测量接口执行效率:接口方法执行前后获取执行时间,求出执行时长
    • System.currentTimeMillis( )
  • 对项目进行监控:项目中所有接口方法,AOP思想,执行期动态织入代码
    • 环绕通知
    • proceed()方法执行前后获取系统时间

案例制作步骤

  • 定义切入点(务必要绑定到接口上,而不是接口实现类上)
  • 制作AOP环绕通知,完成测量功能
  • 注解配置AOP
  • 开启注解驱动支持

案例制作核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RunTimeMonitorAdvice {
//拦截所有的业务层接口中查询操作的执行
@Pointcut("execution(* com.itheima.service.*Service.find*(..))")
public void pt(){}
@Around("pt()")
public Object runtimeMonitor(ProceedingJoinPoint pjp) throws Throwable {
//获取执行签名信息
Signature signature = pjp.getSignature();
//通过签名获取执行类型(接口名)
String targetClass = signature.getDeclaringTypeName();
//通过签名获取执行操作名称(方法名)
String targetMethod = signature.getName();
//获取操作前系统时间beginTime
long beginTime = System.currentTimeMillis();
Object ret = pjp.proceed(pjp.getArgs());
//获取操作后系统时间endTime
long endTime = System.currentTimeMillis();
System.out.println(targetClass+" 中 "+targetMethod+" 运行时长 "+(endTime-beginTime)+"ms");
return ret;
}
}

案例后续思考与设计

  • 测量真实性
    • 开发测量是隔离性反复执行某个操作,是理想情况,上线测量差异过大
    • 上线测量服务器性能略低于单机开发测量
    • 上线测量基于缓存的性能查询要优于数据库查询测量
    • 上线测量接口的性能与最终对外提供的服务性能差异过大
    • 当外部条件发生变化(硬件),需要进行回归测试,例如数据库迁移
  • 测量结果展示
    • 测量结果无需每一个都展示,需要设定检测阈值
    • 阈值设定要根据业务进行区分,一个复杂的查询与简单的查询差异化很大
    • 阈值设定需要做独立的配置文件或通过图形工具配置(工具级别的开发)
    • 配合图形界面展示测量结果

代理设计模式介绍

静态代理和装饰者设计模式的区别

相同点

1)都要实现与目标类相同的业务接口

2)在俩个类中都要声明目标对象

3)都可以在不修改目标类的前提下增强目标方法

不同点

1)目的不同,装饰者,简单说,就是为了增强目标对象

静态代理的使用目的是为了保护和隐藏目标对象

2)对于目标对象的获取方式不同

装饰者中目标对象的获取,通过代参构造器传入,静态代理类中,是在无参构造器中直接创建。

静态代理

静态代理的思想:将被代理类作为代理类的成员,通过代理类调用被代理类的函数,并添加新的控制。包装类与被包装类实现同一接口,使得使用时的代码一致。

应用:已经有一个日志记录器LoggerSubject,需要对writeLog()函数的前后进行某些操作(如初始化、异常处理等),使用Proxy类间接调用LoggerSubject.writeLog()实现新控制操作的添加。

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
interface Logger {
void writeLog();
}

// 被代理类
class LoggerSubject implements Logger{
@Override
public void writeLog(){
System.out.println("writeLog by LoggerSubject");
}
}

// 代理类
class Proxy implements Logger{
Logger logger;
// 与装饰者模式的主要区别位置
// 代理模式一般要求和原来的类行为一致,因此构造函数不传入对象
Proxy(){
this.logger = new LoggerSubject();
}
@Override
public void writeLog(){
System.out.println("logger write before");
logger.writeLog();
System.out.println("logger write after");
}
}

public class StaticProxy {
public static void main(String []argvs){
Logger logger = new Proxy();
logger.writeLog();
}
}

动态代理技术

常用的动态代理技术

JDK 代理 : 基于接口的动态代理技术

cglib 代理:基于父类的动态代理技术

img

JDK 的动态代理

①目标类接口

1
2
3
public interface TargetInterface {
public void method();
}

②目标类

1
2
3
4
5
6
public class Target implements TargetInterface {
@Override
public void method() {
System.out.println("Target running....");
}
}

③动态代理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Target target = new Target(); //创建目标对象
//创建代理对象
TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance(target.getClass()
.getClassLoader(),target.getClass().getInterfaces(),new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("前置增强代码...");
Object invoke = method.invoke(target, args);
System.out.println("后置增强代码...");
return invoke;
}
}
);

④ 调用代理对象的方法测试

1
2
// 测试,当调用接口的任何方法时,代理对象的代码都无序修改
proxy.method();

img

cglib 的动态代理

①目标类

1
2
3
4
5
public class Target {
public void method() {
System.out.println("Target running....");
}
}

②动态代理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
Target target = new Target(); //创建目标对象
Enhancer enhancer = new Enhancer(); //创建增强器
enhancer.setSuperclass(Target.class); //设置父类
enhancer.setCallback(new MethodInterceptor() { //设置回调
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("前置代码增强....");
Object invoke = method.invoke(target, objects);
System.out.println("后置代码增强....");
return invoke;
}
});
Target proxy = (Target) enhancer.create(); //创建代理对象

③调用代理对象的方法测试

1
2
//测试,当调用接口的任何方法时,代理对象的代码都无序修改
proxy.method();

img

代理模式的选择

Spirng可以通过配置的形式控制使用的代理形式,默认使用jdkproxy,通过配置可以修改为使用cglib

false表示使用默认JDK动态代理,true表示使用cglib动态代理

1
2
<!--XMP配置AOP-->
<aop:config proxy-target-class="false"> </aop:config>

注解驱动

1
2
//注解驱动
@EnableAspectJAutoProxy(proxyTargetClass = true)

面试

AOP是什么

面向切面编程,在不修改源代码的情况下对原有功能进行增强,底层是动态代理

动态代理的分类

分为jdk和cglib,jdk动态代理只能增强某个接口的子类对象,cglib接口和类的子类对象都可以增强

在开发中经常的通知有哪些

前置通知Before,后置通知After,环绕通知Around 这三个用的比较多

要想对某个方法进行增强有几种方式

1.继承

2.装饰者

3.动态代理

Spring和SpringBoot默认的动态代理

spring默认的动态代理是JDK,springboot是cglib