Service业务逻辑对象层
什么是业务?
代表用户完成的一个业务功能,这个功能可以由一个或者多个DAO层的调用组成。
需要将这种业务功能也单独的抽取出来一层Service,将一个和多个业务功能在Service层中实现,而对于入口main方法,只需要调用Service层的某个业务方法,即可实现具体内容,main方法只需要提供对应的参数即可,具体实现都在Service和DAO层中完成。
准备关系表
准备一张Account表:
- id:int类型,主键自增
- name:varchar类型,非空
- card_no:varchar类型,非空
- pwd:varchar类型,非空
- money:decimal(10,2)类型,非空不允许为负数
create table account( id int primary key auto_increment comment '主键', name varchar(16) not null comment '名称', card_no varchar(32) not null comment '卡号', pwd varchar(32) not null comment '密码', money decimal(10,2) UNSIGNED not null comment '余额' );
准备测试数据
准备实体类
/**
* 映射account表
*/
public class Account {
private int id;
private String name;
private String cardNo;
private String pwd;
private BigDecimal money;
// 省略 无参,有参,toString,get/set
}
准备DAO接口和实现类
根据咱们转账的业务,可以看出,需要提供两个方法
- 根据cardNo查询当前account的信息。
- 根据卡号修改金额money。
AccountDao接口:
package com.jimihua.dao;
import com.jimihua.entity.Account;
/**
* account表的Dao层操作
*/
public interface AccountDao {
/**
* 根据卡号查询具体的account信息
* @param cardNo
* @return
*/
Account findByCardNo(String cardNo);
/**
* 根据卡号修改account的余额
* @param account
* @return
*/
int updateMoneyByCardNo(Account account);
}
准备AccountDao的实现类:
package com.jimihua.dao.impl;
import com.jimihua.dao.AccountDao;
import com.jimihua.entity.Account;
import com.jimihua.entity.Person;
import com.jimihua.utils.DatabaseUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class AccountDaoImpl implements AccountDao {
@Override
public Account findByCardNo(String cardNo) throws SQLException {
//1、获取连接
Connection conn = DatabaseUtils.getConnection();
//2、编写SQL语句
String sql = "select * from account where card_no = ?";
//3、获取PreparedStatement,传入SQL
PreparedStatement ps = conn.prepareStatement(sql);
//4、给占位符赋值
ps.setObject(1,cardNo);
//5、执行SQL,获取结果集ResultSet
ResultSet rs = ps.executeQuery();
//6、处理结果集封装数据
Account account = null;
if(rs.next()){
account = new Account();
account.setId(rs.getInt("id"));
account.setName(rs.getString("name"));
account.setCardNo(rs.getString("card_no"));
account.setPwd(rs.getString("pwd"));
account.setMoney(rs.getBigDecimal("money"));
}
//7、释放资源
DatabaseUtils.closeAll(conn,ps,rs);
//8、返回结果
return account;
}
@Override
public int updateMoneyByCardNo(Account account) throws SQLException {
//1、获取连接
Connection conn = DatabaseUtils.getConnection();
//2、编写SQL语句
String sql = "update account set money = ? where card_no = ?";
//3、获取PreparedStatement,传入SQL
PreparedStatement ps = conn.prepareStatement(sql);
//4、给占位符赋值
ps.setObject(1,account.getMoney());
ps.setObject(2,account.getCardNo());
//5、执行SQL,获取结果
int count = ps.executeUpdate();
//6、释放资源
DatabaseUtils.closeAll(conn,ps);
//7、返回结果
return count;
}
}
准备Service接口和实现类
经过需要做的转账操作,大致分析,需要几个参数
- fromCardNo,转账人的卡号。
- toCardNo,被转账人的卡号。
- fromPwd,转账人的密码。
- amount,转账金额
提供Service层的接口:
package com.jimihua.service;
import com.jimihua.entity.Account;
import java.math.BigDecimal;
/**
* 在这个AccountService中提供转账的功能~
*/
public interface AccountService {
/**
* 转账操作
* @param fromCardNo 转账人的卡号。
* @param toCardNo 被转账人的卡号。
* @param fromPwd 转账人的密码。
* @param amount 转账金额
* @return
*/
String transfer(String fromCardNo, String toCardNo, String fromPwd, BigDecimal amount);
}
实现AccountServiceImpl中的具体转账逻辑
package com.jimihua.service.impl;
import com.jimihua.dao.AccountDao;
import com.jimihua.dao.impl.AccountDaoImpl;
import com.jimihua.entity.Account;
import com.jimihua.service.AccountService;
import java.math.BigDecimal;
import java.sql.SQLException;
public class AccountServiceImpl implements AccountService {
// 与account的Dao层的交互,需要用的对象
private AccountDao accountDao = new AccountDaoImpl();
@Override
public String transfer(String fromCardNo, String toCardNo, String fromPwd, BigDecimal amount) throws SQLException {
// 1、查看卡号是否存在?
Account fromAccount = accountDao.findByCardNo(fromCardNo);
if(fromAccount == null){
throw new RuntimeException("--------转账人卡号不存在---------");
}
// 2、匹配你输入的密码是否正确?
if (!fromAccount.getPwd().equals(fromPwd)) {
throw new RuntimeException("--------转账人密码错误---------");
}
// 3、查看余额是否充足?
if (fromAccount.getMoney().compareTo(amount) < 0) {
throw new RuntimeException("--------转账人余额不足---------");
}
// 4、查看被转账方卡号是否存在?
Account toAccount = accountDao.findByCardNo(toCardNo);
if (toAccount == null) {
throw new RuntimeException("--------被转账人卡号不存在---------");
}
// 5、给转账人卡里扣钱。
fromAccount.setMoney(fromAccount.getMoney().subtract(amount));
int fromCount = accountDao.updateMoneyByCardNo(fromAccount);
if (fromCount != 1) {
throw new RuntimeException("--------转账人扣钱失败!---------");
}
// 6、给被转账人卡里加钱。
toAccount.setMoney(toAccount.getMoney().add(amount));
int toCount = accountDao.updateMoneyByCardNo(toAccount);
if (toCount != 1){
throw new RuntimeException("--------被转账人加钱失败!---------");
}
return "转账成功!";
}
}
在main方法中测试
可以根据Service中的业务逻辑,测试各个判断是否正常的生效。
package com.jimihua;
import com.jimihua.service.AccountService;
import com.jimihua.service.impl.AccountServiceImpl;
import java.math.BigDecimal;
import java.sql.SQLException;
/**
* 完成转账操作的main方法
*/
public class Demo9 {
public static void main(String[] args) throws SQLException {
AccountService accountService = new AccountServiceImpl();
String result = accountService.transfer("10086", "10010", "123456", new BigDecimal(1000));
System.out.println(result);
}
}
事务的处理
JDBC中事务如何操作
在Java中控制事务操作。
在Java中需要使用Connection对象来控制事务。Connection提供了三个方法:
- conn.setAutoCommit(false);
- conn.commit();
- conn.rollback();
转账事务如何控制
现在需要做的事情很简单,只需要在5和6的操作位置追加上事务控制即可。
Connection conn1 = DatabaseUtils.getConnection(); try { // 开启事务 conn1.setAutoCommit(false); // 5、给转账人卡里扣钱。 // 6、给被转账人卡里加钱。 // 提交事务 conn1.commit(); }catch(Exception e){ // 回滚事务 conn1.rollback(); }
但是这样一来就存在了一个问题,Service层基于DatabaseUtils拿到的Connection对象和Dao层中基于DatabaseUtils获取到的Connection不是同一个。
Service和Dao不是一个Connection
解决方案:
- 可以直接将Service层获取到的Connection对象作为参数传递到Dao层,Dao层直接使用Service中传递过来的Connection和数据库交互即可。
Ps:首先上述方式可以解决,但是会污染到Dao层接口和实现类的方法声明的信息……
- 可以采用ThreadLocal的方式,ThreadLocal可以将Connection对象在Service层获取,并且存储到ThreadLocal中,当Dao层需要和数据库交互时,优先从ThreadLocal中尝试获取Connection对象。
基于ThreadLocal的方案,直接去修改DatabaseUtils中提供的getConnection的静态方法
private static final ThreadLocal<Connection> tl = new ThreadLocal<>();
/**
* 获取Connection对象方法
* @return
*/
public static Connection getConnection() {
//1、先尝试从ThreadLocal中获取。
Connection conn = tl.get();
if (conn == null) {
//2、没获取到,基于DriverManager获取Connection,然后添加到ThreadLocal中
try {
conn = DriverManager.getConnection(PROP.getProperty("jdbc.url"),PROP.getProperty("jdbc.username"),PROP.getProperty("jdbc.password"));
} catch (SQLException e) {
throw new RuntimeException("获取Connection出错!");
}
tl.set(conn);
}
//3、获取到了,直接返回
return conn;
}
Dao层释放Connection资源需要前置到Service
在前面的ThreadLocal封装好之后,会出现一个诡异的错误
经过ThreadLocal的整改后,让整个Service和Dao中使用的Connection对象只有一个。
但是咱们将释放资源的操作放在了Dao层中完成,可是Service一次业务可能会涉及到多次和Dao层交互,如果第一个Dao层操作完,直接释放掉了Connection,就会导致后续的Dao操作无法完成,事务也无法正常开启。
问题的解决方案很简单,将Dao层释放Connection资源的操作前置到Service层。
但是Dao依然要正常的释放Statement,ResultSet。
所以要再次改造DatabaseUtils中的closeAll方法。
- 每个资源的close和非空判断要单独走。
- 当释放connection资源时,记得要将ThreadLocal中存储的Connection给remove掉。
/**
* 针对查询操作的释放资源
* @param connection
* @param statement
* @param resultSet
*/
public static void closeAll(Connection connection, Statement statement, ResultSet resultSet) {
try {
if(resultSet != null)
resultSet.close();
if(statement != null)
statement.close();
if (connection != null) {
connection.close();
tl.remove();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 针对写操作的释放资源
* @param connection
* @param statement
*/
public static void closeAll(Connection connection, Statement statement) {
try {
if(statement != null)
statement.close();
if(connection != null) {
connection.close();
tl.remove();
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
首先将Dao层中的connection资源的释放逻辑去掉,closeAll时,不传递connection对象
可以在Service层中提交事务或者回滚事务之后,将Connection资源释放掉
package com.jimihua.service.impl;
import com.jimihua.dao.AccountDao;
import com.jimihua.dao.impl.AccountDaoImpl;
import com.jimihua.entity.Account;
import com.jimihua.service.AccountService;
import com.jimihua.utils.DatabaseUtils;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.SQLException;
public class AccountServiceImpl implements AccountService {
// 与account的Dao层的交互,需要用的对象
private AccountDao accountDao = new AccountDaoImpl();
@Override
public String transfer(String fromCardNo, String toCardNo, String fromPwd, BigDecimal amount) throws SQLException {
// 1、查看卡号是否存在?
Account fromAccount = accountDao.findByCardNo(fromCardNo);
if (fromAccount == null) {
throw new RuntimeException("--------转账人卡号不存在---------");
}
// 2、匹配你输入的密码是否正确?
if (!fromAccount.getPwd().equals(fromPwd)) {
throw new RuntimeException("--------转账人密码错误---------");
}
// 3、查看余额是否充足?
if (fromAccount.getMoney().compareTo(amount) < 0) {
throw new RuntimeException("--------转账人余额不足---------");
}
// 4、查看被转账方卡号是否存在?
Account toAccount = accountDao.findByCardNo(toCardNo);
if (toAccount == null) {
throw new RuntimeException("--------被转账人卡号不存在---------");
}
Connection conn = DatabaseUtils.getConnection();
try {
// 开启事务
conn.setAutoCommit(false);
// 5、给转账人卡里扣钱。
fromAccount.setMoney(fromAccount.getMoney().subtract(amount));
int fromCount = accountDao.updateMoneyByCardNo(fromAccount);
if (fromCount != 1) {
throw new RuntimeException("--------转账人扣钱失败!---------");
}
// 6、给被转账人卡里加钱。
toAccount.setMoney(toAccount.getMoney().add(amount));
int toCount = accountDao.updateMoneyByCardNo(toAccount);
if (toCount != 1) {
throw new RuntimeException("--------被转账人加钱失败!---------");
}
// 提交事务
conn.commit();
DatabaseUtils.closeAll(conn,null);
} catch (Exception e) {
e.printStackTrace();
// 回滚事务
conn.rollback();
DatabaseUtils.closeAll(conn,null);
return "转账失败!";
}
return "转账成功!";
}
}
事务的控制封装到DatabaseUtils中
在Service层中的这个操作有点恶心,获取Connection,同时还要是提交或者回滚之后,去释放Connection资源,不如将事务的控制再封装到DatabaseUtils中。
/**
* 开启事务
*/
public static void begin(){
Connection conn = getConnection();
try {
conn.setAutoCommit(false);
} catch (SQLException e) {
throw new RuntimeException("开启事务失败!");
}
}
/**
* 提交事务
*/
public static void commit(){
Connection conn = getConnection();
try {
conn.commit();
} catch (SQLException e) {
throw new RuntimeException("提交事务失败!");
}finally {
closeAll(conn,null);
}
}
/**
* 回滚事务
*/
public static void rollback(){
Connection conn = getConnection();
try {
conn.rollback();
} catch (SQLException e) {
throw new RuntimeException("回滚事务失败!");
}finally {
closeAll(conn,null);
}
}
再次改造Service层代码
package com.jimihua.service.impl;
import com.jimihua.dao.AccountDao;
import com.jimihua.dao.impl.AccountDaoImpl;
import com.jimihua.entity.Account;
import com.jimihua.service.AccountService;
import com.jimihua.utils.DatabaseUtils;
import java.math.BigDecimal;
import java.sql.Connection;
import java.sql.SQLException;
public class AccountServiceImpl implements AccountService {
// 与account的Dao层的交互,需要用的对象
private AccountDao accountDao = new AccountDaoImpl();
@Override
public String transfer(String fromCardNo, String toCardNo, String fromPwd, BigDecimal amount) throws SQLException {
// 1、查看卡号是否存在?
Account fromAccount = accountDao.findByCardNo(fromCardNo);
if (fromAccount == null) {
throw new RuntimeException("--------转账人卡号不存在---------");
}
// 2、匹配你输入的密码是否正确?
if (!fromAccount.getPwd().equals(fromPwd)) {
throw new RuntimeException("--------转账人密码错误---------");
}
// 3、查看余额是否充足?
if (fromAccount.getMoney().compareTo(amount) < 0) {
throw new RuntimeException("--------转账人余额不足---------");
}
// 4、查看被转账方卡号是否存在?
Account toAccount = accountDao.findByCardNo(toCardNo);
if (toAccount == null) {
throw new RuntimeException("--------被转账人卡号不存在---------");
}
try {
// 开启事务
DatabaseUtils.begin();
// 5、给转账人卡里扣钱。
fromAccount.setMoney(fromAccount.getMoney().subtract(amount));
int fromCount = accountDao.updateMoneyByCardNo(fromAccount);
if (fromCount != 1) {
throw new RuntimeException("--------转账人扣钱失败!---------");
}
// 6、给被转账人卡里加钱。
toAccount.setMoney(toAccount.getMoney().add(amount));
int toCount = accountDao.updateMoneyByCardNo(toAccount);
if (toCount != 1) {
throw new RuntimeException("--------被转账人加钱失败!---------");
}
// 提交事务
DatabaseUtils.commit();
} catch (Exception e) {
e.printStackTrace();
// 回滚事务
DatabaseUtils.rollback();
return "转账失败!";
}
return "转账成功!";
}
}