Spring 代理模式应用场景
1. 前言
大家好,我们学习了代理模式的概念,也知道了代理模式可以在程序的运行过程中,实现对某个方法的增强。那么,在我们程序的编写过程中,
什么样的场景,能使用代理模式呢?
本节,我们模拟一个实际应用场景,目的是观察日常程序中可能发生的问题,以及代理模式如何解决问题,这样可以更加深刻地理解代理模式的意义。
2. 案例实战
2.1 转账工程的搭建
我们模拟一个实际生活中常见的情景,就是账号的转账。 假设有两个用户 A 和 用户 B,我们通过程序,从 A 账号中转成指定的 money 到 B 账号中。
那么,针对正常和异常的程序执行,我们来分析下问题以及它的解决方案。
2.1.1 工程准备
创建 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 class="com.offcn.service.impl.AccountServiceImpl">
<!-- 注入dao -->
<property name="accountDao" ref="accountDao"></property>
</bean>
<!--配置Dao对象-->
<bean class="com.offcn.dao.impl.AccountDaoImpl">
<!-- 注入QueryRunner -->
<property name="runner" ref="queryRunner"></property>
<!-- 注入ConnectionUtils -->
<property name="connectionUtils" ref="connectionUtils"></property>
</bean>
<!--配置QueryRunner-->
<bean class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean>
<!-- 配置数据源 -->
<bean class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--连接数据库的必备信息-->
<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 class="com.offcn.utils.ConnectionUtils">
<!-- 注入数据源-->
<property name="dataSource" ref="dataSource"></property>
</bean>
</beans>
配置文件说明:
- connectionUtils 是获取数据库连接的工具类;
- dataSource 采用 c3p0 数据源,大家一定要注意数据库的名称与账号名和密码;
- queryRunner 是 dbutils 第三方框架提供用于执行 SQL 语句,操作数据库的一个工具类;
- accountDao 和 accountService 是我们自定义的业务层实现类和持久层实现类。
项目使用数据库环境
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 {
//数据id
private Integer id;
//账号编码
private String accountNum;
//账号金额
private Float money;
//省略 get 和 set 的方法
}
持久层接口
//接口代码
public interface IAccountDao {
/**
* 更新
* @param account
*/
void updateAccount(Account account);
/**
* 根据编号查询账户
* @param accountNum
* @return 如果有唯一的一个结果就返回,如果没有结果就返回null
* 如果结果集超过一个就抛异常
*/
Account findAccountByNum(String accountNum);
}
持久层实现类
public class AccountDaoImpl implements IAccountDao {
//数据库查询工具类
private QueryRunner runner;
//数据库连接工具类
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);
}
}
//根据账号查询 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("结果集不唯一,数据有问题");
}
return accounts.get(0);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}
业务层接口
public interface IAccountService {
/**
* 转账
* @param sourceAccount 转出账户名称
* @param targetAccount 转入账户名称
* @param money 转账金额
*/
void transfer(String sourceAccount, String targetAccount, Integer money);
}
业务层实现类
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);
}
}
测试结果
代码执行完毕,可以看到输出打印转账 ok 了。那么数据库的数据有没有改变呢?我们再看一眼:
可以看到:两个账号的数据已经发生了改变,证明转账的动作,确实完成了。那这样看来,我们的代码也没有问题啊,代理模式有什么用呢?
接下来我们改造下工程,模拟程序发生异常时候,执行以后的结果如何。
2.1.3 改造业务类代码
在业务层的代码加入一行异常代码,看看结果是否还会转账成功呢?
执行结果:
当然了,其实提前也能想得到,肯定会执行失败的啦,哈哈哈哈,我们手动加了运算会出现异常的代码嘛!但是转账的动作是不是也失败了呢?我们再来看一下数据库:
问题来了: id 为 1 的账号 money 的列值由原来的 900 变成了 800,说明存款确实减少了 100,但是由于在代码执行的过程中,出现了异常,导致原始账号减少 100 的金钱后保存成功, 而 id 为 2 的账号并没有增加 100。这就出现了数据的事务问题,破坏了数据的原子性和一致性。
那么如何解决呢? 思路就是将我们的数据操作代码,使用事务控制起来。由于本小节篇幅有限,我们留待下一小节解决。
3. 小结
本小节模拟了一个现实生活中转账的业务场景,其目的是为了引出我们的程序在执行过程中可能会产生的问题。而如何解决,并且对于原始代码侵入性更小,耦合性更低,是我们需要思考的事情。
使用知识点:
- JDBC 的基础,本案例用到了 JDBC 连接数据库的工具类 dbutils 知识;
- 数据库基础,本案例的数据操作,涉及到了事务的四个特性:原子性,一致性,隔离性,持久性。