Spring笔记10 AOP
coconutnut

https://www.bilibili.com/video/av47952931
p56~65


AOP概述

Aspect Oriented Programming 面向切面编程

  • 扩展功能不通过修改源代码实现

  • AOP采取横向抽取机制,取代了传统的纵向继承体系重复性代码

  • 底层使用动态代理方法实现

    • 有接口时,使用jdk动态代理,创建和接口实现平级的代理对象
    • 无接口时,使用cglib动态代理,创建类的子类的代理对象

作用及优势

作用:

  • 在程序运行期间,不修改源码对已有方法进行增强

优势:

  • 减少重复代码
  • 提高开发效率
  • 维护方便

Spring中的AOP

相关术语

Joinpoint 连接点

类中可以被增强的方法

Pointcut 切入点

类中实际增强的方法

Advice 通知/增强

切入点上扩展的功能

  • 前置增强:在方法之前执行
  • 后置增强:在方法正常执行之后执行
  • 最终增强:在最后执行,无论是否有异常
  • 环绕增强:在之前和之后执行
  • 异常增强:方法出现异常时执行

Introduction 引介

一种特殊的增强,在不修改类代码的前提下,可以在运行期间为类动态地添加一些方法或Field

Target 目标对象

要增强的类

Weaving 织入

把增强应用到目标对象的过程

Proxy 代理

一个类被AOP织入增强后,就产生一个结果代理类

Aspect 切面

切入点 + 通知/引介

Spring基于XML的AOP配置

配置步骤

  1. 配置增强Bean

  2. 使用aop:config标签表明开始AOP的配置

  3. 使用aop:aspect标签表明配置切面

    id属性:是给切面提供一个唯一标识
    ref属性:是指定通知类bean的Id

  4. 在aop:aspect标签的内部使用对应标签来配置通知的类型

    aop:before:表示配置前置通知(实例中让printLog方法在切入点方法前执行)
    method属性:用于指定类中哪个方法是前置通知
    pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强

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
切入点表达式的写法:
关键字:execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)

标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用..表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用*来实现通配
* *..*.*()

参数列表:
可以直接写数据类型:
基本类型直接写名称 int
引用类型写包名.类名的方式 java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用..表示有无参数均可,有参数可以是任意类型

全通配写法:
* *..*.*(..)

实际开发中切入点表达式的通常写法:
切到业务层实现类下的所有方法
* com.itheima.service.impl.*.*(..)

示例

xml配置

在Spring Framework Documentation的Core中搜索xmlns: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
25
<?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: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/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">

<!-- 配置srping的Ioc,把service对象配置进来-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>

<!-- 配置Logger类 -->
<bean id="logger" class="com.itheima.utils.Logger"></bean>

<!--配置AOP-->
<aop:config>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>

</beans>

业务层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 账户的业务层接口
*/
public interface IAccountService {

// 模拟保存账户 无返回值无参
void saveAccount();

// 模拟更新账户 无返回值有参
void updateAccount(int i);

// 删除账户 有返回值无参
int deleteAccount();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService{

public void saveAccount() {
System.out.println("执行了保存");
}

public void updateAccount(int i) {
System.out.println("执行了更新"+i);

}

public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}

通知

1
2
3
4
5
6
7
8
9
10
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
public class Logger {

// 用于打印日志:计划让其在切入点方法执行之前执行
public void printLog(){
System.out.println("Logger类中的pringLog方法开始记录日志了。。。");
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 测试AOP的配置
*/
public class AOPTest {

public static void main(String[] args) {
//1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IAccountService as = (IAccountService)ac.getBean("accountService");
//3.执行方法
as.saveAccount();
as.updateAccount(1);
as.deleteAccount();
}
}

配置切入点表达式

id属性用于指定表达式的唯一标识

expression属性用于指定表达式内容

该标签写在aop:aspect标签内部只能当前切面使用

还可以写在aop:aspect外面,此时就变成了所有切面可用

注意:必须放在切面之前,否则会报错(还不提醒怎么错的)

4种常用通知类型

继续在刚才的实例上加其它通知类型

使用切入点表达式简化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--配置AOP-->
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知:在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut-ref="pt1" ></aop:before>

<!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>

<!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>

<!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行-->
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>

切入点方法无异常时输出

1
2
3
4
前置通知Logger类中的beforePrintLog方法开始记录日志了...
执行了保存
后置通知Logger类中的afterReturningPrintLog方法开始记录日志了...
最终通知Logger类中的afterPrintLog方法开始记录日志了...

切入点方法有异常时输出

1
2
3
4
5
6
前置通知Logger类中的beforePrintLog方法开始记录日志了...
执行了保存
异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了...
最终通知Logger类中的afterPrintLog方法开始记录日志了...
Exception in thread "main" java.lang.ArithmeticException: / by zero
...

后置和异常只会执行其中一个

环绕通知

下面这样配置环绕通知是不行的

1
2
3
public  void aroundPringLog(){
System.out.println("Logger类中的afterThrowingPrintLog方法开始记录日志了...");
}

这样配置后,切入点方法不执行了,只执行通知方法

分析:对比动态代理中,发现其中的环绕通知有明确的切入点调用,而这样没有

解决:使用Spring的ProceedingJoinPoint接口。该接口有一个proceed()方法,在程序执行时,Spring会提供该接口的实现类供我们使用

这样可以控制增强方法何时执行,前置、后置、异常、最终都可以实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Logger {
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();// 得到方法执行所需的参数

System.out.println("Logger类中的aroundPringLog方法开始记录日志了...前置");
rtValue = pjp.proceed(args);// 明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了...后置");

return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了...异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了...最终");
}
}
}
1
2
<!-- 配置环绕通知-->
<aop:around method="aroundPringLog" pointcut-ref="pt1"></aop:around>

Spring基于注解的AOP配置

示例

xml配置

约束比基于xml的多了context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>

<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

Logger配置(4种常用类型)![建议别用,有bug]

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
@Component("logger")
@Aspect // 表示当前类是一个切面类
public class Logger {

// 配置切入点表达式
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}

// 注意要加()
@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了...");
}

@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了...");
}

@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了...");
}

@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了...");
}

}

运行输出:

1
2
3
4
前置通知Logger类中的beforePrintLog方法开始记录日志了...
执行了保存
最终通知Logger类中的afterPrintLog方法开始记录日志了...
后置通知Logger类中的afterReturningPrintLog方法开始记录日志了...

发现最终在后置之前执行了(异常也一样)

没有办法,因为Spring基于注解的AOP中,调用顺序确实有问题,实际开发中应该慎重

Logger配置(环绕通知)

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
@Component("logger")
@Aspect // 表示当前类是一个切面类
public class Logger {

// 配置切入点表达式
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}

@Around("pt1()")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();// 得到方法执行所需的参数

System.out.println("Logger类中的aroundPringLog方法开始记录日志了...前置");

rtValue = pjp.proceed(args);// 明确调用业务层方法(切入点方法)

System.out.println("Logger类中的aroundPringLog方法开始记录日志了...后置");

return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了...异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了...最终");
}
}
}

此时就没有顺序问题了

1
2
3
4
Logger类中的aroundPringLog方法开始记录日志了...前置
执行了保存
Logger类中的aroundPringLog方法开始记录日志了...后置
Logger类中的aroundPringLog方法开始记录日志了...最终