2023.10.30
本章将在web应用中使用MyBatis,实现一个银行转账的功能。整体架构采用MVC架构模式。
数据库表的初始化
环境的初始化配置
web.xml文件的配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="false">
<!-- <servlet>-->
<!-- <servlet-name>test</servlet-name>-->
<!-- <servlet-class>bank.web.AccountServlet</servlet-class>-->
<!-- </servlet>-->
<!-- <servlet-mapping>-->
<!-- <servlet-name>test</servlet-name>-->
<!-- <url-pattern>/transfer</url-pattern>-->
<!-- </servlet-mapping>-->
</web-app>
ps:metadata-complete如果填true,会导致注解使用不了,路径就必须在xml文件里配置了,所以这里填false,就可以使用注解了。
pom.xml文件的配置:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jay</groupId>
<artifactId>mybatis-004-web</artifactId>
<packaging>war</packaging>
<version>1.0</version>
<name>mybatis-004-web Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>10.0.12</version>
</dependency>
</dependencies>
<build>
<finalName>mybatis-004-web</finalName>
</build>
</project>
引入相关配置文件,放到resources目录下:
AccountMapper.xml文件:配置相关sql语句。
logback-test.xml文件
mybatis-config.xml文件:
前端页面的准备
index.html : 启动服务器会自动跳转到这个页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行账户转账</title>
</head>
<body>
<!--/bank是应用的根,部署web应用到tomcat的时候一定要注意这个名字-->
<form action="/bank/transfer" method="post">
转出账户:<input type="text" name="fromActno"/><br>
转入账户:<input type="text" name="toActno"/><br>
转账金额:<input type="text" name="money"/><br>
<input type="submit" value="转账"/>
</form>
</body>
</html>
error1.html:当因“余额不足”导致转账失败,会跳到这个页面上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>余额不足!!!!</h1>
</body>
</html>
error.html:当未知原因导致转账失败,会跳到这个页面上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>转账失败,未知原因!!!</h1>
</body>
</html>
success.html:转账成功跳到这个页面上
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>转账成功!!!</h1>
</body>
</html>
在bank包下创建pojo包、service包、dao包、web包、utils、exceptions包:
dao为数据访问层,也称为持久层,位于三层中的最下层,用于对数据进行处理。
service为业务逻辑层,用于对业务逻辑的封装。
web为表示层,用于显示数据和接收用户输入的数据,为用户提供一种交互式操作的界面。
pojo为实体类,用于对数据的封装。
utils为工具类。
exceptions为异常处理。
定义pojo类:Account
package bank.pojo;
public class Account {
private Long id;
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public Account() {
}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
编写AccountDao接口,以及AccountDaoImpl实现类
package bank.dao;
import bank.pojo.Account;
public interface AccountDao {
/**
* 根据账号获取账户信息
* @param actno 账号
* @return 账户信息
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act 账户信息
* @return 1表示更新成功,其他值表示失败
*/
int update(Account act);
}
package bank.dao.impl;
import bank.dao.AccountDao;
import org.apache.ibatis.session.SqlSession;
import bank.pojo.Account;
import bank.utils.SqlSessionUtil;
import javax.swing.plaf.IconUIResource;
public class AccountDaoImpl implements AccountDao {
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account act = (Account) sqlSession.selectOne("selectByActno",actno);
return act;
}
public int update(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("update",act);
return count;
}
}
编写AccountService接口以及AccountServiceImpl实现类
package bank.service;
import bank.exceptions.MoneyNotEnoughException;
import bank.exceptions.TransferException;
public interface AccountService {
void transfer(String fromActno,String toActno,double money) throws MoneyNotEnoughException, TransferException;
}
package bank.service.impl;
import bank.dao.AccountDao;
import bank.dao.impl.AccountDaoImpl;
import bank.exceptions.MoneyNotEnoughException;
import bank.exceptions.TransferException;
import bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
import bank.pojo.Account;
import bank.service.AccountService;
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
//添加事务控制代码
SqlSession sqlSession = SqlSessionUtil.openSession();
//查询账户余额,判断转出账户的余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if(fromAct.getBalance() < money){
throw new MoneyNotEnoughException("对不起,余额不足!");
}
//执行到这说明余额充足了
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
//更新数据库
int count = accountDao.update(fromAct);
count += accountDao.update(toAct);
if(count != 2){
throw new TransferException("转账异常,未知原因");
}
sqlSession.commit();
SqlSessionUtil.close(sqlSession);
}
}
异常类
package bank.exceptions;
public class MoneyNotEnoughException extends Exception{
public MoneyNotEnoughException(){}
public MoneyNotEnoughException(String msg){
super(msg);
}
}
package bank.exceptions;
public class TransferException extends Exception{
public TransferException(){}
public TransferException(String msg){
}
}
工具类SqlSessionUtil:
package bank.utils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
public class SqlSessionUtil {
private SqlSessionUtil(){};
private static SqlSessionFactory sqlSessionFactory;
//静态代码块:类加载时运行
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//全局的,服务器级别的,一个服务器当中定义一个即可。
private static ThreadLocal<SqlSession> local = new ThreadLocal<SqlSession>();
//获取会话对象
public static SqlSession openSession(){
SqlSession sqlSession = local.get();
if(sqlSession == null){
sqlSession = sqlSessionFactory.openSession();
//将sqlSession对象绑定到当前线程上
local.set(sqlSession);
}
return sqlSession;
}
//从当前线程中移除SqlSession对象
public static void close(SqlSession sqlSession){
if(sqlSession != null){
sqlSession.close();
//注意移除SqlSession对象和当前线程的绑定关系
//因为Tomcat服务器支持线程池,也就是说用过的线程对象,可能下一次还会再用。
local.remove();
}
}
}
表示层AccountServlet类:
package bank.web;
import bank.exceptions.MoneyNotEnoughException;
import bank.exceptions.TransferException;
import bank.service.AccountService;
import bank.service.impl.AccountServiceImpl;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
private AccountService accountService = new AccountServiceImpl();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取表单数据
String fromActno = request.getParameter("fromActno");
String toActno = request.getParameter("toActno");
double money = Double.parseDouble(request.getParameter("money"));
//调用service的转账方法完成转账,(调业务层)
try {
accountService.transfer(fromActno,toActno,money);
response.sendRedirect(request.getContextPath() + "/success.html");
} catch (MoneyNotEnoughException e) {
response.sendRedirect(request.getContextPath() + "/error1.html");
} catch (TransferException e) {
response.sendRedirect(request.getContextPath() + "/error2.html");
}
}
}
实验结果:
启动服务器,出现页面:
填入金额:
跳转页面:
数据库中的数据:
事务问题
在AccountServiceImpl实现类中,收尾分别添加了事务语句,以确保事务安全。为了保证service和dao中使用的SqlSession对象是同一个,可以将SqlSession对象存放到ThreadLocal当中。 所以在工具类SqlSessionUtil中,使用了一个全局的ThreadLocal,它的使用场合主要是为了解决多线程中因为数据并发产生不一致的问题。 获取会话对象的代码为:
public static SqlSession openSession(){
SqlSession sqlSession = local.get();
if(sqlSession == null){
sqlSession = sqlSessionFactory.openSession();
//将sqlSession对象绑定到当前线程上
local.set(sqlSession);
}
return sqlSession;
}
即一个线程对应一个sqlSession ,这样子做 dao 和 service中的sqlsession就都是同一个的了。
这里注意 sqlsession的close方法中需要对local进行移除:
public static void close(SqlSession sqlSession){
if(sqlSession != null){
sqlSession.close();
//注意移除SqlSession对象和当前线程的绑定关系
//因为Tomcat服务器支持线程池,也就是说用过的线程对象,可能下一次还会再用。
local.remove();
}
}
MyBatis的三个对象作用域
SqlSessionFactoryBuilder:
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory:
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是在WEB应用使用MyBatis应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession:
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。