Spring之——AOP基础、回顾动态代理
什么是AOP
AOP:全称是 Aspect Oriented Programming 即:面向切面编程。
简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。
AOP 的作用及优势
- 作用:在程序运行期间,不修改源码对已有方法进行增强。
- 优势:
- 减少重复代码
- 提高开发效率
- 维护方便
AOP 的实现方式:使用动态代理技术
AOP的具体作用
发现问题
在我们进行数据库的增删改查中,业务层中单条SQL语句是没问题的,因为执行一条SQL语句会创建一个Connection控制事务,但如果是多条语句,就会创建是多个Connection,这就产生了一个问题:不符合事务的一致性,如果SQL语句执行过程中出现异常,那么数据就会出问题。
举个例子:银行转账
1、转账账户 - 转账金额
2、转账账户数据操作成功
3、收钱账户 + 转账金额转账需要经过1和3这两个步骤才能实现,但如果步骤2位置出现异常,那么就会出现这么一个情况
转账账户的钱少了,但是收钱账户的钱却没增加
解决问题
既然已经发现问题的出现是因为事务的不一致,那么我们就可以想办法让同一个线程里面统一使用一个Connection来控制事务
解决方法:
1、创建一个Connection工具类,它可以绑定一个Connection在某个线程上,如果当前线程没有绑定Connection,那么从连接池中获取一个再绑定
2、创建一个事务管理类,它通过Connection工具类获取当前线程绑定的Connection对象,然后进行相应的事务管理
ConnectionUtil连接的工具类
1 | package com.itheima.utils; |
TransactionManager事务控制类
1 | package com.itheima.utils; |
DAO层实现类
1 | package com.itheima.dao.impl; |
Service业务层实现类
1 | package com.itheima.service.impl; |
Spring IOC配置
1 |
|
Maven坐标
1 | <dependencies> |
引申出新的问题
现在已经可以实现事务控制了,但是由于我们添加了事务控制,也产生了一个新的问题:
业务层方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦合了。
试想一下,如果我们此时提交,回滚,释放资源中任何一个方法名变更,都需要修改业务层的代码
况且这还只是一个业务层实现类,而实际的项目中这种业务层实现类可能有十几个甚至几十个。这时,我们就可以使用动态代理技术来解决这个问题
回顾动态代理:动态代理的特点
字节码随用随创建,随用随加载。
它与静态代理的区别也在于此。因为静态代理是字节码一上来就创建好,并完成加载。
装饰者模式就是静态代理的一种体现。
动态代理常用的有两种方式
- 基于接口的动态代理
- 提供者:JDK 官方的 Proxy 类。
- 要求:被代理类最少实现一个接口。
- 基于子类的动态代理
- 提供者:第三方的 CGLib,如果报 asmxxxx 异常,需要导入 asm.jar。
- 要求:被代理类不能用 final 修饰的类(最终类)。
1、基于接口的动态代理
接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package com.itheima.proxy;
/**
* 对生产厂家要求的接口
*/
public interface IProducer {
/**
* 销售
* @param money
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterService(float money);
}接口实现类(生产者)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.itheima.proxy;
/**
* 一个生产者
*/
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}创建动态代理
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67package com.itheima.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类最少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
* Class[]:字节码数组
* 它是用于让代理对象和被代理对象有相同方法。固定写法。
* InvocationHandler:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法有相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
2、基于子类的动态代理
生产者(被代理的类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24package com.itheima.cglib;
/**
* 一个生产者
*/
public class Producer {
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}消费者(创建动态代理)
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68package com.itheima.cglib;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 模拟一个消费者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于子类的动态代理:
* 涉及的类:Enhancer
* 提供者:第三方cglib库
* 如何创建代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类不能是最终类
* create方法的参数:
* Class:字节码
* 它是用于指定被代理对象的字节码。
*
* Callback:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
* 我们一般写的都是该接口的子接口实现类:MethodInterceptor
*/
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行北地阿里对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy :当前执行方法的代理对象
* @return
* @throws Throwable
*/
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float)args[0];
//2.判断当前方法是不是销售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}
使用动态代理技术解决刚刚发现的新问题:业务层事务管理耦合
在上面代码的基础上修改
创建Service的代理对象的工厂(通过动态代理创建Service这个生产者的增强对象)
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75package com.itheima.factory;
import com.itheima.service.IAccountService;
import com.itheima.utils.TransactionManager;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* 用于创建Service的代理对象的工厂
*/
public class BeanFactory {
private IAccountService accountService;
private TransactionManager txManager;
public void setTxManager(TransactionManager txManager) {
this.txManager = txManager;
}
public final void setAccountService(IAccountService accountService) {
this.accountService = accountService;
}
/**
* 获取Service代理对象
* @return
*/
public IAccountService getAccountService() {
return (IAccountService)Proxy.newProxyInstance(accountService.getClass().getClassLoader(),
accountService.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 添加事务的支持
*
* @param proxy
* @param method
* @param args
* @return
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if("test".equals(method.getName())){
return method.invoke(accountService,args);
}
Object rtValue = null;
try {
//1.开启事务
txManager.beginTransaction();
//2.执行操作
rtValue = method.invoke(accountService, args);
//3.提交事务
txManager.commit();
//4.返回结果
return rtValue;
} catch (Exception e) {
//5.回滚操作
txManager.rollback();
throw new RuntimeException(e);
} finally {
//6.释放连接
txManager.release();
}
}
});
}
}测试
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
28package com.itheima.test;
import com.itheima.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* 使用Junit单元测试:测试我们的配置
*/
public class AccountServiceTest {
private IAccountService as;
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}
Spring中的AOP
Spring中的AOP其实就是通过配置方式,实现上面的内容
AOP相关术语
在开发中可能用不到什么,但是资料上会出现这些术语,为了以后的学习,需要慢慢消化它
- Joinpoint(连接点):
所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
- Pointcut(切入点):
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
- Advice(通知/增强):
所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
通知的类型:
- 前置通知
- 后置通知
- 异常通知
- 最终通知
- 环绕通知。
- Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。
- Target(目标对象):
代理的目标对象。
- Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。 - Proxy(代理):
一个类被 AOP 织入增强后,就产生一个结果代理类。
- Aspect(切面):
是切入点和通知(引介)的结合。
学习 spring 中的 AOP 要明确的事
开发阶段(我们做的)
- 1、编写核心业务代码(开发主线)
- 2、把公用代码抽取出来,制作成通知。(开发阶段最后再做)
- 3、在配置文件中,声明切入点与通知间的关系,即切面。
运行阶段(Spring 框架完成的)
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对
象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
关于代理的选择
在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
实现XML的AOP配置
1、导入Maven坐标
1 | <dependencies> |
2、创建创建Spring配置文件,注意约束的不同
1 |
|
logger日志对象(通用的通知对象)
1 | package com.itheima.utils; |
讲解XML的AOP配置
aop:config:
- 作用:用于声明开始 aop 的配置
1
2
3<aop:config>
<!-- 配置的代码都写在此处 -->
</aop:config>
aop:aspect:
- 作用:用于配置切面。
- 属性:
- id:给切面提供一个唯一标识。
- ref:引用配置好的通知类 bean 的 id。
1
2
3<aop:aspect id="txAdvice" ref="txManager">
<!--配置通知的类型要写在此处-->
</aop:aspect>
aop:pointcut:
- 作用:用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
- 属性:
- expression:用于定义切入点表达式。
- id:用于给切入点表达式提供一个唯一标识
1
<aop:pointcut expression="表达式" id="pt1"/>
aop:before
- 作用:用于配置前置通知。指定增强的方法在切入点方法之前执行
- 属性:
- method:用于指定通知类中的增强方法名称
- ponitcut-ref:用于指定切入点的表达式的引用
- poinitcut:用于指定切入点表达式
- 执行时间点:切入点方法执行之前执行
1
<aop:before method="beginTransaction" pointcut-ref="pt1"/>
aop:after-returning:
- 作用:用于配置后置通知
- 属性:
- method:指定通知中方法的名称。
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
- 执行时间点:切入点方法正常执行之后。它和异常通知只能有一个执行
1
<aop:after-returning method="commit" pointcut-ref="pt1"/>
aop:after-throwing
- 作用:用于配置异常通知
- 属性:
- method:指定通知中方法的名称。
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
- 执行时间点:切入点方法执行产生异常后执行。它和后置通知只能执行一个
1
<aop:after-throwing method="rollback" pointcut-ref="pt1"/>
aop:after
- 作用:用于配置最终通知
- 属性:
- method:指定通知中方法的名称。
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
- 执行时间点:无论切入点方法执行时是否有异常,它都会在其后面执行。
1
<aop:after method="release" pointcut-ref="pt1"/>
切入点表达式说明
- execution:匹配方法的执行(常用)
- execution(表达式)
- 表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
- 写法说明:
完整写法
public void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
返回值可以使用
*
号,表示任意返回值* com.itheima.service.impl.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
包名可以使用
*
号,表示任意包,但是有几级包,需要写几个*
* *.*.*.*.AccountServiceImpl.saveAccount(com.itheima.domain.Account)
使用
..
来表示当前包,及其子包* com..AccountServiceImpl.saveAccount(com.itheima.domain.Account)
类名可以使用
*
号,表示任意类* com..*.saveAccount(com.itheima.domain.Account)
方法名可以使用
*
号,表示任意方法* com..*.*( com.itheima.domain.Account)
参数列表可以使用
*
,表示参数可以是任意数据类型,但是必须有参数* com..*.*(*)
参数列表可以使用
..
表示有无参数均可,有参数可以是任意类型* com..*.*(..)
全通配方式:
* *..*.*(..)
注:通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。
execution(* com.itheima.service.impl.*.*(..))
环绕通知
配置方式:
1
2
3
4
5
6
7<aop:config>
<aop:pointcut expression="execution(* com.itheima.service.impl.*.*(..))" id="pt1"/>
<aop:aspect id="txAdvice" ref="txManager">
<!-- 配置环绕通知 -->
<aop:around method="transactionAround" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>aop:around:
- 作用:用于配置环绕通知
- 属性:
- method:指定通知中方法的名称。
- pointct:定义切入点表达式
- pointcut-ref:指定切入点表达式的引用
- 说明:它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
- 注意:通常情况下,环绕通知都是独立使用的
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/**
* 环绕通知
* @param pjp
* spring 框架为我们提供了一个接口:ProceedingJoinPoint,它可以作为环绕通知的方法参数。
* 在环绕通知执行时,spring 框架会为我们提供该接口的实现类对象,我们直接使用就行。
* @return
*/
public Object transactionAround(ProceedingJoinPoint pjp) {
//定义返回值
Object rtValue = null;
try {
//获取方法执行所需的参数
Object[] args = pjp.getArgs();
//前置通知:开启事务
beginTransaction();
//执行方法
rtValue = pjp.proceed(args);
//后置通知:提交事务
commit();
}catch(Throwable e) {
//异常通知:回滚事务
rollback();
e.printStackTrace();
}finally {
//最终通知:释放资源
release();
}
return rtValue;
}
基于注解的AOP配置
Spring配置文件
1 |
|
logger通知对象
1 | package com.itheima.utils; |
Service业务层实现类
1 | package com.itheima.service.impl; |
不使用 XML 的配置方式
1 | @Configuration//用于IOC |