Spring 代理模式應用場景
1. 前言
大家好,我們學習了代理模式的概念,也知道了代理模式可以在程序的運行過程中,實現(xiàn)對某個方法的增強。那么,在我們程序的編寫過程中,
什么樣的場景,能使用代理模式呢?
本節(jié),我們模擬一個實際應用場景,目的是觀察日常程序中可能發(fā)生的問題,以及代理模式如何解決問題,這樣可以更加深刻地理解代理模式的意義。
2. 案例實戰(zhàn)
2.1 轉賬工程的搭建
我們模擬一個實際生活中常見的情景,就是賬號的轉賬。 假設有兩個用戶 A 和 用戶 B,我們通過程序,從 A 賬號中轉成指定的 money 到 B 賬號中。
那么,針對正常和異常的程序執(zhí)行,我們來分析下問題以及它的解決方案。
2.1.1 工程準備
創(chuàng)建 maven 工程
引入 pom 文件的依賴 jar 包坐標信息
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
Spring 框架的配置文件編寫
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 配置Service -->
<bean id="accountService" class="com.offcn.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"></property>
</bean>
<!--配置Dao對象-->
<bean id="accountDao" class="com.offcn.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="queryRunner"></property>
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置QueryRunner-->
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
<!-- 配置數(shù)據(jù)源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--連接數(shù)據(jù)庫的必備信息-->
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/transmoney"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!-- 配置Connection的工具類 ConnectionUtils -->
<bean id="connectionUtils" class="com.offcn.utils.ConnectionUtils">
<!-- 注入數(shù)據(jù)源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
配置文件說明:
- connectionUtils 是獲取數(shù)據(jù)庫連接的工具類;
- dataSource 采用 c3p0 數(shù)據(jù)源,大家一定要注意數(shù)據(jù)庫的名稱與賬號名和密碼;
- queryRunner 是 dbutils 第三方框架提供用于執(zhí)行 SQL 語句,操作數(shù)據(jù)庫的一個工具類;
- accountDao 和 accountService 是我們自定義的業(yè)務層實現(xiàn)類和持久層實現(xiàn)類。
項目使用數(shù)據(jù)庫環(huán)境
CREATE TABLE account
(
id
int(11) NOT NULL auto_increment,
accountNum
varchar(20) default NULL,
money
int(8) default NULL,
PRIMARY KEY (id
)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8
2.1.2 代碼編寫
實體類代碼
public class Account implements Serializable {
//數(shù)據(jù)id
private Integer id;
//賬號編碼
private String accountNum;
//賬號金額
private Float money;
//省略 get 和 set 的方法
}
持久層接口
//接口代碼
public interface IAccountDao {
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 根據(jù)編號查詢賬戶
* @param accountNum
* @return 如果有唯一的一個結果就返回,如果沒有結果就返回null
* 如果結果集超過一個就拋異常
*/
Account findAccountByNum(String accountNum);
}
持久層實現(xiàn)類
public class AccountDaoImpl implements IAccountDao {
//數(shù)據(jù)庫查詢工具類
private QueryRunner runner;
//數(shù)據(jù)庫連接工具類
private ConnectionUtils connectionUtils;
//省略 get 和 set 的方法
//修改賬號的方法
public void updateAccount(Account account) {
try{
runner.update(connectionUtils.getThreadConnection(),
"update account set accountNum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId());
}catch (Exception e) {
throw new RuntimeException(e);
}
}
//根據(jù)賬號查詢 Account 對象的方法
public Account findAccountByNum(String accountNum) {
try{
List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),
"select * from account where accountNum = ? ",new BeanListHandler<Account>(Account.class),accountNum);
if(accounts == null || accounts.size() == 0){
return null;
}
if(accounts.size() > 1){
throw new RuntimeException("結果集不唯一,數(shù)據(jù)有問題");
}
return accounts.get(0);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}
業(yè)務層接口
public interface IAccountService {
/**
* 轉賬
* @param sourceAccount 轉出賬戶名稱
* @param targetAccount 轉入賬戶名稱
* @param money 轉賬金額
*/
void transfer(String sourceAccount, String targetAccount, Integer money);
}
業(yè)務層實現(xiàn)類
public class AccountServiceImpl implements IAccountService {
//持久層對象
private IAccountDao accountDao;
//省略 set 和 get 方法
//轉賬的方法
public void transfer(String sourceAccount, String targetAccount, Integer money) {
//查詢原始賬戶
Account source = accountDao.findAccountByNum(sourceAccount);
//查詢目標賬戶
Account target = accountDao.findAccountByNum(targetAccount);
//原始賬號減錢
source.setMoney(source.getMoney()-money);
//目標賬號加錢
target.setMoney(target.getMoney()+money);
//更新原始賬號
accountDao.updateAccount(source);
//更新目標賬號
accountDao.updateAccount(target);
System.out.println("轉賬完畢");
}
}
測試運行類代碼
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AccountServiceTest {
@Autowired
@Qualifier("proxyAccountService")
private IAccountService as;
@Test
public void testTransfer(){
as.transfer("622200009999","622200001111",100);
}
}
測試結果
代碼執(zhí)行完畢,可以看到輸出打印轉賬 ok 了。那么數(shù)據(jù)庫的數(shù)據(jù)有沒有改變呢?我們再看一眼:
可以看到:兩個賬號的數(shù)據(jù)已經(jīng)發(fā)生了改變,證明轉賬的動作,確實完成了。那這樣看來,我們的代碼也沒有問題啊,代理模式有什么用呢?
接下來我們改造下工程,模擬程序發(fā)生異常時候,執(zhí)行以后的結果如何。
2.1.3 改造業(yè)務類代碼
在業(yè)務層的代碼加入一行異常代碼,看看結果是否還會轉賬成功呢?
執(zhí)行結果:
當然了,其實提前也能想得到,肯定會執(zhí)行失敗的啦,哈哈哈哈,我們手動加了運算會出現(xiàn)異常的代碼嘛!但是轉賬的動作是不是也失敗了呢?我們再來看一下數(shù)據(jù)庫:
問題來了: id 為 1 的賬號 money 的列值由原來的 900 變成了 800,說明存款確實減少了 100,但是由于在代碼執(zhí)行的過程中,出現(xiàn)了異常,導致原始賬號減少 100 的金錢后保存成功, 而 id 為 2 的賬號并沒有增加 100。這就出現(xiàn)了數(shù)據(jù)的事務問題,破壞了數(shù)據(jù)的原子性和一致性。
那么如何解決呢? 思路就是將我們的數(shù)據(jù)操作代碼,使用事務控制起來。由于本小節(jié)篇幅有限,我們留待下一小節(jié)解決。
3. 小結
本小節(jié)模擬了一個現(xiàn)實生活中轉賬的業(yè)務場景,其目的是為了引出我們的程序在執(zhí)行過程中可能會產(chǎn)生的問題。而如何解決,并且對于原始代碼侵入性更小,耦合性更低,是我們需要思考的事情。
使用知識點:
- JDBC 的基礎,本案例用到了 JDBC 連接數(shù)據(jù)庫的工具類 dbutils 知識;
- 數(shù)據(jù)庫基礎,本案例的數(shù)據(jù)操作,涉及到了事務的四個特性:原子性,一致性,隔離性,持久性。