Spring学习笔记03-AOP

AOP 概念

AOP:全称是 Aspect Oriented Programming 即:面向切面编程。简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的基础上,对我们的已有方法进行增强。

image-20200204211913152

AOP 的作用及优势

作用:
     在程序运行期间,不修改源码对已有方法进行增强。
优势:
     减少重复代码
     提高开发效率
     维护方便

AOP 的实现方式

使用动态代理技术   动态代理:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
客户的业务层实现类
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {

private IAccountDao accountDao;

public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}

@Override
public void saveAccount(Account account) throws SQLException {
accountDao.save(account);
}

@Override
public void updateAccount(Account account) throws SQLException{
accountDao.update(account);
}

@Override
public void deleteAccount(Integer accountId) throws SQLException{
accountDao.delete(accountId);
}

@Override
public Account findAccountById(Integer accountId) throws SQLException {
return accountDao.findById(accountId);
}

@Override
public List<Account> findAllAccount() throws SQLException{
return accountDao.findAll();
}
}


问题就是:
事务被自动控制了。换言之,我们使用了 connection 对象的 setAutoCommit(true)
此方式控制事务,如果我们每次都执行一条 sql 语句,没有问题,但是如果业务方法一次要执行多条 sql语句,这种方式就无法实现功能了。

问题的解决

解决办法:
     让业务层来控制事务的提交和回滚。(这个我们之前已经在 web 阶段讲过了)

改造后的业务层实现类:
     注:此处没有使用 spring 的 IoC.
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService {
private IAccountDao accountDao = new AccountDaoImpl();


@Override
public void saveAccount(Account account) {
try {
TransactionManager.beginTransaction();
accountDao.save(account);
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}

@Override
public void updateAccount(Account account) {
try {
TransactionManager.beginTransaction();
accountDao.update(account);
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}

@Override
public void deleteAccount(Integer accountId) {
try {
TransactionManager.beginTransaction();
accountDao.delete(accountId);
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}

@Override
public Account findAccountById(Integer accountId) {
Account account = null;
try {
TransactionManager.beginTransaction();
account = accountDao.findById(accountId);
TransactionManager.commit();
return account;
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
return null;
}

@Override
public List<Account> findAllAccount() {
List<Account> accounts = null;
try {
TransactionManager.beginTransaction();
accounts = accountDao.findAll();
TransactionManager.commit();
return accounts;
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
return null;
}

@Override
public void transfer(String sourceName, String targetName, Float money) {
try {
TransactionManager.beginTransaction();
Account source = accountDao.findByName(sourceName);
Account target = accountDao.findByName(targetName);
source.setMoney(source.getMoney()-money);
target.setMoney(target.getMoney()+money);
accountDao.update(source);
int i=1/0;
accountDao.update(target);
TransactionManager.commit();
} catch (Exception e) {
TransactionManager.rollback();
e.printStackTrace();
}finally {
TransactionManager.release();
}
}
}

TransactionManager 类的代码:
/**
* 事务控制类
*/
public class TransactionManager {

//定义一个 DBAssit
private static DBAssit dbAssit = new DBAssit(C3P0Utils.getDataSource(),true);


//开启事务
public static void beginTransaction() {
try {
dbAssit.getCurrentConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}

//提交事务
public static void commit() {
try {
dbAssit.getCurrentConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}

//回滚事务

public static void rollback() {
try {
dbAssit.getCurrentConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}

//释放资源
public static void release() {
try {
dbAssit.releaseConnection();
} catch (Exception e) {
e.printStackTrace();
}
}
}

新的问题

上一小节的代码,通过对业务层改造,已经可以实现事务控制了,但是由于我们添加了事务控制,也产生了一个新的问题:

​ 业务层方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦合了。
试想一下,如果我们此时提交,回滚,释放资源中任何一个方法名变更,都需要修改业务层的代码,况且这还只是一个业务层实现类,而实际的项目中这种业务层实现类可能有十几个甚至几十个。

思考:
     这个问题能不能解决呢?
     答案是肯定的,使用下一小节中提到的技术。

动态代理

解决案例中的问题

1
等吧这个代码写了再码上来~

Spring 中 AOP 的细节

说明

我们学习 spring 的 aop,就是通过配置的方式,实现上一章节的功能。

AOP 相关术语

Joinpoint(连接点):
        所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
1
2
Pointcut(切入点):
所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义。
1
2
3
Advice(通知/增强):
所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知。
通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
1
2
Introduction(引介):
引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法或 Field。
1
2
3
4
5
6
7
8
9
Target(目标对象):
代理的目标对象。
Weaving(织入):
是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
Proxy(代理):
一个类被 AOP 织入增强后,就产生一个结果代理类。
Aspect(切面):
是切入点和通知(引介)的结合。

学习 spring 中的 AOP 要明确的事

a、开发阶段(我们做的)
     编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
     把公用代码抽取出来,制作成通知。(开发阶段最后再做):AOP 编程人员来做。
     在配置文件中,声明切入点与通知间的关系,即切面。:AOP 编程人员来做。
b、运行阶段(Spring 框架完成的)
     Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。

## 关于代理的选择

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

基于 XML 的 AOP 配置

示例:
我们在学习 spring 的 aop 时,采用账户转账作为示例。
并且把 spring 的 ioc 也一起应用进来。

环境搭建

待写代码~

第一步:准备必要的代码

此处包含了实体类,业务层和持久层代码。我们沿用上一章节中的代码即可。

第二步:拷贝必备的 jar 包到工程的 lib 目录

image-20200204220400522

第三步:创建 spring 的配置文件并导入约束

image-20200204220631930

第四步:配置 spring 的 ioc

image-20200204220700310
image-20200204220720630

第五步:抽取公共代码制作成通知

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
/**
* 事务控制类
*/
public class TransactionManager {

//定义一个 DBAssit
private DBAssit dbAssit ;

public void setDbAssit(DBAssit dbAssit) {
this.dbAssit = dbAssit;
}

//开启事务
public void beginTransaction() {
try {
dbAssit.getCurrentConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}

//提交事务
public void commit() {
try {
dbAssit.getCurrentConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}

//回滚事务
public void rollback() {
try {
dbAssit.getCurrentConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}

//释放资源
public void release() {
try {
dbAssit.releaseConnection();
} catch (Exception e) {
e.printStackTrace();
}
}
}

配置步骤

第一步:把通知类用 bean 标签配置起来

1
2
3
4
<!-- 配置通知 -->
<bean id="txManager" class="com.itheima.utils.TransactionManager">
<property name="dbAssit" ref="dbAssit"></property>
</bean>

第二步:使用 aop:config 声明 aop 配置

aop:config:
     作用:用于声明开始 aop 的配置
<aop:config>

     <!-- 配置的代码都写在此处 -->
</aop:config>

第三步:使用 aop:aspect 配置切面

aop:aspect:
     作用:
         用于配置切面。
     属性:
         id:给切面提供一个唯一标识。
         ref:引用配置好的通知类 bean 的 id。
<aop:aspect id="txAdvice" ref="txManager">
         <!--配置通知的类型要写在此处-->
</aop:aspect>

第四步:使用 aop:pointcut 配置切入点表达式

aop:pointcut:
     作用:
         用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
     属性:
         expression:用于定义切入点表达式。
         id:用于给切入点表达式提供一个唯一标识
1
2
3
4
<aop:pointcut expression="execution(
public void com.itheima.service.impl.AccountServiceImpl.transfer(
java.lang.String, java.lang.String, java.lang.Float)
)" id="pt1"/>

第五步:使用 aop:xxx 配置对应的通知类型

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"/>

## 切入点表达式说明

image-20200204221658313
image-20200204221708460

环绕通知

image-20200204221741749
image-20200204221802374

基于注解的 AOP 配置

环境搭建

第一步:准备必要的代码和 jar包

拷贝上一小节的工程即可

第二步:在配置文件中导入 context 的名称空间

image-20200204222018293

第三步:把资源使用注解配置

image-20200204222036847
image-20200204222052181

第四步:在配置文件中指定 spring 要扫描的包

image-20200204222121354

配置步骤

第一步:把通知类也使用注解配置

1
2
3
4
5
6
7
8
9
/**
* 事务控制类
*/
@Component("txManager")
public class TransactionManager {
//定义一个 DBAssit
@Autowired
private DBAssit dbAssit ;
}

第二步:在通知类上使用@Aspect 注解声明为切面

作用:
把当前类声明为切面类。

1
2
3
4
5
6
7
8
9
10
11
/**
* 事务控制类
*/
@Component("txManager")
@Aspect//表明当前类是一个切面类
public class TransactionManager {

//定义一个 DBAssit
@Autowired
private DBAssit dbAssit ;
}

第三步:在增强的方法上使用注解配置通知

@Before
作用:
把当前方法看成是前置通知。
属性:
value:用于指定切入点表达式,还可以指定切入点表达式的引用。

1
2
3
4
5
6
7
8
9
//开启事务
@Before("execution(* com.itheima.service.impl.*.*(..))")
public void beginTransaction() {
try {
dbAssit.getCurrentConnection().setAutoCommit(false);
} catch (SQLException e) {
e.printStackTrace();
}
}

@AfterReturning
作用:
把当前方法看成是后置通知。
属性:
value:用于指定切入点表达式,还可以指定切入点表达式的引用

1
2
3
4
5
6
7
8
9
10
//提交事务
@AfterReturning("execution(* com.itheima.service.impl.*.*(..))")
public void commit() {

try {
dbAssit.getCurrentConnection().commit();
} catch (SQLException e) {
e.printStackTrace();
}
}

@AfterThrowing
作用:
把当前方法看成是异常通知。
属性:
value:用于指定切入点表达式,还可以指定切入点表达式的引用```

1
2
3
4
5
6
7
8
9
//回滚事务
@AfterThrowing("execution(* com.itheima.service.impl.*.*(..))")
public void rollback() {
try {
dbAssit.getCurrentConnection().rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}

@After
作用:
把当前方法看成是最终通知。
属性:
value:用于指定切入点表达式,还可以指定切入点表达式的引用

1
2
3
4
5
6
7
8
9
//释放资源
@After("execution(* com.itheima.service.impl.*.*(..))")
public void release() {
try {
dbAssit.releaseConnection();
} catch (Exception e) {
e.printStackTrace();
}
}

第四步:在 spring 配置文件中开启 spring 对注解 AOP 的支持

1
2
<!-- 开启 spring 对注解 AOP 的支持 -->
<aop:aspectj-autoproxy/>

环绕通知注解配置

@Around
作用:
把当前方法看成是环绕通知。
属性:
value:用于指定切入点表达式,还可以指定切入点表达式的引用。

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
/**
* 环绕通知
* @param pjp
* @return
*/
@Around("execution(* com.itheima.service.impl.*.*(..))")
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;
}

切入点表达式注解

@Pointcut
作用:
指定切入点表达式
属性:
value:指定表达式的内容

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
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1() {}

引用方式:
/**
* 环绕通知
* @param pjp
* @return
*/
@Around("pt1()")//注意:千万别忘了写括号
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;
}

不使用 XML 的配置方式

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