我们提供安全,免费的手游软件下载!
@
在 Web 中应用 MyBatis ,同时使用 MVC 架构模式,以及对应的 ThreadLocal 事务控制。
实现功能,银行账户的转账功能,同时进行事务上的处理。
需求描述:
实际简单的转账操作:
数据库表的设计和准备数据 :
这里说明一下,开发可以
二者没有太大区别,你认为哪个方向更好编写,便按照哪个方向即可,我个人比较习惯从前往后,所以这里我就从前往后了。
IDEA中创建Maven WEB应用
注意:这里的 Archetype : 选择
org.apache.maven.archetypes:maven-archetype-webapp
不要选错了。
IDEA配置Tomcat,这里Tomcat使用10+版本。并部署应用到tomcat。
默认创建的maven web应用没有
java
和
resources
目录。
一般会自动添加上,如果没有的话,有两种手动添加上的方式:
这里自动生成的:
web.xml
文件的版本较低,内容有点不太合适,我们可以从 tomcat10 的样例文件中复制,然后修改
如下是:tomcat 10 当中的样例:
删除
index.jsp
文件,因为我们这个项目不使用JSP。只使用 html。
确定
pom.xml
文件中的打包方式是
war
包。
引入相关依赖
4.0.0
com.rainbowsea
mybatis-004-web
1.0-SNAPSHOT
war
mybatis-004-web Maven Webapp
http://www.example.com
UTF-8
1.7
1.7
junit
junit
4.11
test
org.mybatis
mybatis
3.5.10
mysql
mysql-connector-java
8.0.30
ch.qos.logback
logback-classic
1.2.11
jakarta.servlet
jakarta.servlet-api
5.0.0
provided
mybatis-004-web
maven-clean-plugin
3.1.0
maven-resources-plugin
3.0.2
maven-compiler-plugin
3.8.0
maven-surefire-plugin
2.22.1
maven-war-plugin
3.2.2
maven-install-plugin
2.5.2
maven-deploy-plugin
2.8.2
引入相关配置文件,放到resources目录下(全部放到类的根路径下)
logback.xml logbak 日志框架信息
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
AccountMapper.xml SQl语句的映射文件
update t_act set balance = #{balance} where actno = #{actno}
jdb.properties 数据库连接信息的配置文件
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=MySQL123
mybatis-config.xml MyBatis 的核心配置文件
在Tomcat当中 ,index.html 默认就是开始页面,主页的。
银行账户转账
package com.rianbowsea.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 {
// 工具类的构造方法一般都是私有话化的
// 工具类中所有的方法都是静态的,直接类名即可调用,不需要 new 对象
// 为了防止new对象,构造方法私有化。
private SqlSessionUtil() {
}
private static SqlSessionFactory sessionFactory = null;
// 静态代码块,类加载时执行
// SqlSessionUtil 工具类在进行第一次加载的时候,解析mybatis-config.xml 文件,创建SqlSessionFactory对象。
static {
// 获取到 SqlSessionFactoryBuilder 对象
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 获取到SqlSessionFactory 对象
// SQlsessionFactory对象,一个SqlSessionFactory对应一个 environment, 一个environment通常是一个数据库
try {
sessionFactory = sqlSessionFactoryBuilder.build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 全局的,服务器级别的,一个服务器当中定义一个即可
private static ThreadLocal local = new ThreadLocal<>();
/**
* 获取会话对象
*
* @return SqlSession
*/
public static SqlSession openSession() {
// 先从 ThreadLocal 当中获取,获取到 SqlSession 对象
SqlSession sqlSession = local.get();
if (null == sqlSession) {
// ThreadLocat 没有就, 创建一个
sqlSession = sessionFactory.openSession();
// 同时将其设置到 ThreadLocal容器当中,将SqlSession对象绑定到当前线程上
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭SqlSession 对象(从当前线程中移除SqlSession 对象)
* @param sqlSession
*/
public static void close(SqlSession sqlSession) {
if(sqlSession != null) {
// 1.先将其关闭
sqlSession.close();
// 2. 再将其当前线程移除ThreadLocal 当前线程外面,防止被其他线程拿到整个没用的线程
local.remove();
/*
注意:移除SqlSession 对象和当前线程的绑定关系
因为Tomcat 服务器是支持线程池的,也就是说,用过的先吃对象t1,可能下一I此还会使用整个t1(已经关闭,没用的)线程。
*/
}
}
}
对于 pojo 当中的类,一定要有 set 和 get 方法,以及无参数构造方法,不然,大部分的框架是无法通过反射机制,进行操作的,从而出现错误的。
package com.rianbowsea.bank.pojo;
/**
* 账户类,封装账户数据
*/
public class Account {
private Long id;
private String actno;
private Double balance;
public Account() {
}
public Account(Long id, String actno, Double balance) {
this.id = id;
this.actno = actno;
this.balance = balance;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", 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;
}
}
分析dao中至少要提供几个方法,才能完成转账:
AccountDao
package com.rianbowsea.bank.dao;
import com.rianbowsea.bank.pojo.Account;
/**
* 账户的DAO对象,负责t_act 表中数据的CRUD,一般一个表对应一个 DAO
* 强调以下,DAO对象中的任何一个方法和业务不挂钩,没有任何业务逻辑在里头
* DAo中的方法就是CRUD的,所以方法名大部分是:insertXxx,deletexxx,updatexxx,selectxxx
*/
public interface AccountDao {
/**
* 根据账号查询账户信息
* @param actno 账号
* @return 账户信息
*/
Account selectActno(String actno);
/**
* 更新账户信息
* @param account 被更新的账户信息
* @return 1表示更新成功,其他表示更新失败
*/
int updateByActno(Account account);
}
AccountDaoImpl
package com.rianbowsea.bank.dao.impl;
import com.rianbowsea.bank.dao.AccountDao;
import com.rianbowsea.bank.pojo.Account;
import com.rianbowsea.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountDaoImpl implements AccountDao {
private SqlSession sqlSession = SqlSessionUtil.openSession();
@Override
public Account selectActno(String actno) {
Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
// 注意:事务的控制,都是放在业务层的,不是放在持久层DAo,更不放在utils工具层
//sqlSession.close();
return account;
}
@Override
public int updateByActno(Account account) {
int count = sqlSession.update("account.updateByActno", account);
// 注意:事务的控制,都是放在业务层的,不是放在持久层DAo,更不放在utils工具层
//sqlSession.commit(); // 提交数据
//sqlSession.close();
return count;
}
}
AccountMapper.xml
update t_act set balance = #{balance} where actno = #{actno}
AccountService
package com.rianbowsea.bank.service;
import com.rianbowsea.bank.exceptions.MoneyNotEnoughException;
import com.rianbowsea.bank.exceptions.TransferException;
/**
* 注意: 业务类当中的业务方法的名字在起名字的时候,最好见名知意,能够体现出具体的业务是做什么的
* 账户业务类
*/
public interface AccountService {
/**
* 账户转账业务
*
* @param fromActno 转出账户
* @param toActno 转入账户
* @param money 转账金额
*/
void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException;
}
AccountServiceImpl
package com.rianbowsea.bank.service.impl;
import com.rianbowsea.bank.dao.AccountDao;
import com.rianbowsea.bank.dao.impl.AccountDaoImpl;
import com.rianbowsea.bank.exceptions.MoneyNotEnoughException;
import com.rianbowsea.bank.exceptions.TransferException;
import com.rianbowsea.bank.pojo.Account;
import com.rianbowsea.bank.service.AccountService;
import com.rianbowsea.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
// 添加事务控制代码
SqlSession sqlSession = SqlSessionUtil.openSession();
// 1.判断转出账户的金额是否充足(select)
Account fromAct = accountDao.selectActno(fromActno);
if (fromAct.getBalance() < money) {
// 2.如果转出账户余额不足,提示用户
throw new MoneyNotEnoughException("对不起,余额不足");
}
// 3. 如果转出账户余额充足,更新转出账户的余额(update)
// 先在内存当中修改
Account toACt = accountDao.selectActno(toActno);
toACt.setBalance(toACt.getBalance() + money);
fromAct.setBalance(fromAct.getBalance() - money);
// 4. 更新转入账户的余额(update)
int count = accountDao.updateByActno(toACt);
count += accountDao.updateByActno(fromAct);
// 模拟异常
//String s = null;
//s.toString();
if(count !=2) {
throw new TransferException("转账异常,未知原因");
}
// 注意:事务的控制,都是放在业务层的,不是放在持久层DAo,更不放在utils工具层
sqlSession.commit(); // 提交数据
sqlSession.close();
}
}
MoneyNotEnoughException
package com.rianbowsea.bank.exceptions;
/**
* 余额不足异常
*/
public class MoneyNotEnoughException extends Exception{
public MoneyNotEnoughException() {
}
public MoneyNotEnoughException(String message) {
super(message);
}
}
TransferException
package com.rianbowsea.bank.exceptions;
/**
* 转账异常
*/
public class TransferException extends Exception{
public TransferException() {
}
public TransferException(String message) {
super(message);
}
}
AccountController
package com.rianbowsea.bank.web;
import com.rianbowsea.bank.exceptions.MoneyNotEnoughException;
import com.rianbowsea.bank.exceptions.TransferException;
import com.rianbowsea.bank.service.AccountService;
import com.rianbowsea.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"));
// 转账业务
try {
// 调用service的转账方法完成转账,(调用业务层)
accountService.transfer(fromActno,toActno,money);
// 程序能够走到这里,表示转账一定成功了
// 调用View完成展示结果
response.sendRedirect(request.getContextPath()+"/success.html");
} catch (MoneyNotEnoughException e) {
response.sendRedirect(request.getContextPath()+"/error.html");
throw new RuntimeException(e);
} catch (TransferException e) {
response.sendRedirect(request.getContextPath()+"/error2.html");
throw new RuntimeException(e);
} catch (NullPointerException e) {
response.sendRedirect(request.getContextPath()+"/error2.html");
throw new RuntimeException(e);
}
}
}
首先测试,没有模拟异常,看是否可以转账成功。
模拟异常,看事务上的处理,是否成功,是否能成功回滚,是否会丢失数据 。
在之前的转账业务中,更新了两个账户,我们需要保证它们的同时成功或同时失败,这个时候就需要使用事务机制,在 transfer 方法开始执行时开启事务,直到两个更新都成功之后 ,为了保证service和dao中使用的SqlSession对象是同一个,可以将SqlSession对象存放到ThreadLocal当中。
注意:移除SqlSession 对象和当前线程的绑定关系
因为Tomcat 服务器是支持线程池的,也就是说,用过的先吃对象t1,可能下一I此还会使用整个t1(已经关闭,没用的)线程。
注意:事务的控制,都是放在业务层的,不是放在持久层DAo,更不放在utils工具层 .
这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的, 所以它的最佳的作用域是请求或方法作用域 。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行 。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中 。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:
try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}
- 为了保证 service 和 dao 中使用的SqlSession对象是同一个,可以将SqlSession对象存放到ThreadLocal当中。
- 注意:移除SqlSession 对象和当前线程的绑定关系
因为Tomcat 服务器是支持线程池的,也就是说,用过的先吃对象t1,可能下一I此还会使用整个t1(已经关闭,没用的)线程。
注意:事务的控制,都是放在业务层的,不是放在持久层DAo,更不放在utils工具层 .
SqlSessionFactoryBuilder: 这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。
SqlSession 它的最佳的作用域是请求或方法作用域**。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行 。 **也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。
“在这个最后的篇章中,我要表达我对每一位读者的感激之情。你们的关注和回复是我创作的动力源泉,我从你们身上吸取了无尽的灵感与勇气。我会将你们的鼓励留在心底,继续在其他的领域奋斗。感谢你们,我们总会在某个时刻再次相遇。”
热门资讯