一、概述与准备工作
- 我们通过一个银行转账的案例来实现在Web应用中应用mybatis
- 了解 mybatis 中三大对象的作用域
- 采用 MVC 的架构模式,应用了 ThreadLocal 的线程绑定机制
- 准备工作如下:
🌔1、我们需要提前设计一个简单的表 t_act,包含三个属性
直接在Navicat 中手动添加两条记录
🌔2、在我们的项目中添加一个新的模块 mybatis-004-web
(1)与之前有所不同,此次选择 Maven Archetype,然后在Archetype处指定webapp【快速生成web项目】
(2)手动升级 web.xml 版本,我直接找到我 tomcat 安装文件夹中的 web.xml 复制上面的内容:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0"
metadata-complete="true">
</web-app>
-
此处有一个需要注释的地方:
-
如果
metadata-complete="true"
,那么仅支持在web.xml文件中去配置 -
如果
metadata-complete="false"
,那么也可以通过注解去配置【面向注解开发】
(3)配置 pom.xml 文件,确定打包方式和配置依赖
- 因为是web项目,所以打包方式为 war 包,
- 采用MVC的架构模式,所以我们需要 mybatis、mysql、junit、logback、servlet 的依赖
<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>com.powernode</groupId>
<artifactId>mybatis-004-web</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>mybatis-004-web Maven Webapp</name>
<url>http://localhost:8080/bank</url>
<dependencies>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!--mysql驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--junit依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!--logback依赖-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
</dependency>
<!--servlet依赖-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>mybatis-004-web</finalName>
</build>
</project>
-
这里面也有个需要我们注意的地方:【版本为】
-
如果我们 tomcat 的版本是10或以上版本,就不能使用下面的依赖导入 servlet
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
- 需要替换为以下的依赖:
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
(4)配置 tomcat 服务器
- 第一步如图所示找到 tomcat 服务器
- 去配置我们的 tomcat
(5)引入我们的 xml 文件
- mybatis 的核心配置文件 mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<setting name="logImpl" value="SLF4J"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/powernode"/>
<property name="username" value="root"/>
<property name="password" value="111111"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="AccountMapper.xml"/>
</mappers>
</configuration>
- 数据库操作的映射文件 AccountMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="account">
<select id="selectByActno" resultType="com.powernode.bank.pojo.Account">
select * from t_act where actno ={
actno}
</select>
<update id="updateByActno">
update t_act set balance ={
balance} where actno ={
actno}
</update>
</mapper>
- 日志的核心文件 logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{
yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{
50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${
LOG_HOME}/TestWeb.log.%d{
yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{
yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{
50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>100MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!--mybatis log configure-->
<logger name="com.apache.ibatis" level="TRACE"/>
<logger name="java.sql.Connection" level="DEBUG"/>
<logger name="java.sql.Statement" level="DEBUG"/>
<logger name="java.sql.PreparedStatement" level="DEBUG"/>
<!-- 日志输出级别,logback日志级别包括五个:TRACE < DEBUG < INFO < WARN < ERROR -->
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
二、构建我们的Bank项目
- 对于web的页面没有使用 jsp,全部使用的都是html类型的文件
🌔 1、编写我们的web页面
(1)** index.html** 前端表单页面【数据收集】
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>银行账户转账</title>
</head>
<body>
<!--form表单,用于接收信息-->
<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>
(2)** success.html** 转账成功页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>转账成功!</h1>
</body>
</html>
(3)** error1.html** 余额不足异常
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>余额不足!!!</h1>>
</body>
</html>
(4)** error.html** 其他异常
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>转账报告</title>
</head>
<body>
<h1>转账失败,未知原因!!!</h1>
</body>
</html>
🌔2、创建各种包和文件
- com.powernode.bank.pojo >> 普通Java类,用于封装数据库表中的字段
- com.powernode.bank.service >> 业务处理层
- com.powernode.bank.dao >> 数据库的增删改查
- com.powernode.bank.web >> 表示层
- com.powernode.bank.exception >> 异常层
- com.powernode.bank.utils >> 工具类
(1)在 pojo 包下我们创建一个 Account 账户类,包含id、账户号和余额
- 普通Java类的含义大抵是:私有属性、构造方法、get和set方法、toString方法【私有属性对应表中的字段】
- 代码如下:
package com.powernode.bank.pojo;
/**
* @author Bonbons
* @version 1.0
*/
public class Account {
private Long id;
private String actno;
private Double balance;
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;
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
}
(2)在 dao 包下,我们创建对数据库操作的接口AccountDao,并提供一个实现类AccountDaoImpl
- 数据库操作的接口:
package com.powernode.bank.dao;
import com.powernode.bank.pojo.Account;
/**
* 账户的Dao对象,负责对 t_act 表进行增删改查
* Dao 中的方法与业务逻辑不存在联系
* @author Bonbons
* @version 1.0
*/
public interface AccountDao {
/**
* 根据账号查询账户信息
* @param actno 账户id
* @return 返回账户的信息
*/
Account selectByActno(String actno);
/**
* 更新账户的信息
* @param act 被更新的账户
* @return 返回更新结果(成功/失败)
*/
int updateActno(Account act);
}
- 我们在dao包下创建一个impl包,在这个包下写接口的实现类 AccountDaoImpl
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
/**
* @author Bonbons
* @version 1.0
*/
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
//开启会话,根据actno查询账户
SqlSession sqlSession = SqlSessionUtil.openSession();
Account account = (Account) sqlSession.selectOne("account.selectByActno",actno);
// 关闭会话,返回账户信息
// sqlSession.close();
return account;
}
@Override
public int updateActno(Account act) {
//开启会话
SqlSession sqlSession = SqlSessionUtil.openSession();
//修改余额
int count = sqlSession.update("account.updateByActno", act);
// 提交事务,关闭会话
// sqlSession.commit();
// sqlSession.close();
//返回影响数据库表中记录的条数
return count;
}
}
- 此处可以发现,我将事务提交、连接关闭的代码块注释掉了,为什么?
如果是查询账户还好说,没啥太大的影响,但是如果是完成转账操作,我们调用方法之后就直接提交事务是不严谨的,如果在此期间出现了异常,那么转账操作也会成功执行。
所以我们把事务提交、连接关闭放到了具体事务处理的部分。【通过ThreadLocal将线程与连接对象绑定起来】
- 还有一个注意的点,此处调用的方法要与我们的 AccountMapepr.xml 映射文件对应上
(3)在service包下,我们来写具体的业务,同样也需要写一个接口和对应的实现类【转账业务】
- AccountService接口:【就是对业务的约束】
package com.powernode.bank.service;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.exception.TransferException;
/**
* 负责处理账户相关的业务
* 在不同层之间需要提供接口
* @author Bonbons
* @version 1.0
*/
public interface AccountService {
/**
* 账户转账业务[见名知意]
* @param fromActno 转出账户
* @param toActno 转入账户
* @param money 转账金额
*/
void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException;
}
- 同样创建一个 impl 子包,下面创建一个业务的实现类 AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.dao.impl.AccountDaoImpl;
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.exception.TransferException;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
/**
* @author Bonbons
* @version 1.0
*/
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();
//获取转出账户
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);
//更新数据库中的余额,我们可以通过count查看是否更新成功
int count = 0;
count += accountDao.updateActno(fromAct);
count += accountDao.updateActno(toAct);
//转账失败
if(count != 2){
throw new TransferException("转账异常,未知原因");
}
//能执行到这里说明没问题,我们提交事务关闭会话
sqlSession.commit();
//特殊的关闭
SqlSessionUtil.close(sqlSession);
}
}
-
几个注意的点:
-
尽管此处给出的是抛出异常的方式,但是我们在表示层是通过 html 页面来显示异常信息的
-
也可看到并不是直接调用
sqlSession.close();
关闭连接的,而是调用了我们工具类中内置的关闭方法【会在工具类的位置说明】 -
我们获取银行账户的对象,对内存中对象的信息进行修改,最后再通过 update 方法写回数据库表中
(4)在我们的 exception 包下写我们的自定义异常类,此处只包含两个 余额不足和其他转账异常
- 其实内部方法十分简单,就是调用构造方法传递我们想要输出的异常信息
- 余额不足异常类 MoneyNotEnoughException
package com.powernode.bank.exception;
/**
* 余额不足异常
* 继承了运行时异常
* @author Bonbons
* @version 1.0
*/
public class MoneyNotEnoughException extends Exception{
//提供无参和有参的异常构造方法
public MoneyNotEnoughException(){
}
public MoneyNotEnoughException(String msg){
super(msg);
}
}
- 其他转账异常类 TransferException
package com.powernode.bank.exception;
/**
* 转账异常
* @author Bonbons
* @version 1.0
*/
public class TransferException extends Exception{
public TransferException(){
}
public TransferException(String msg){
super(msg);
}
}
(5)在 utils 包下,是我们的工具类 SqlSessionUtil 【创建会话和关闭会话】
package com.powernode.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;
/**
* @author Bonbons
* @version 1.0
*/
public class SqlSessionUtil {
private SqlSessionUtil(){
}
//定义一个SqlSession
private static final SqlSessionFactory sqlSessionFactory;
//在类加载的时候初始化SqlSessionFactory
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
//定义一个全局的ThreadLocal,可以保证一个SqlSession对应一个线程
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
//通过一个公有的方法为外部提供会话的对象 >> 确保同一个线程操作的是同一个连接对象
public static SqlSession openSession(){
//我们用local去获取会话
SqlSession sqlSession = local.get();
//如果当前没有开启的会话就去创建一个,如果get到了就用这个[确保我们操作的是同一个连接对象]
if(sqlSession == null){
sqlSession = sqlSessionFactory.openSession();
//将SqlSession对象绑定到当前线程上
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭SqlSession对象并从当前线程中解绑
* @param sqlSession 会话对象
*/
public static void close(SqlSession sqlSession){
if(sqlSession != null){
sqlSession.close();
local.remove();
}
}
}
-
对于SqlSessionFactoryBuilder我们仅仅使用它的build方法,所以创建完SqlSessionFactory对象后就没用了
-
对于SqlSessionFactory对应一个数据库,所以在类初始化的时候创建一个就够了,我们采用静态代码块的方式完成
-
对于SqlSession我们希望一个线程对应一个,也可以理解为一个线程对应一个业务操作
-
为了避免调用openSqlSession()就创建一个连接对象,我们引入 ThreadLocal 作为全局变量
-
在调用openSession的时候,如果已经存在连接对象了,我们就将这个连接对象返回
-
如果此时没有会话,我们就利用会话工厂去创建一个会话,并将这个连接对象绑定到当前的线程上
-
这也就是为什么在关闭会话的时候,需要设置一个单独的方法,关闭会话后立即解除绑定
(6)在 web 包下,我们创建一个 AccountServlet 类作为表示层
package com.powernode.bank.web; /**
* @author Bonbons
* @version 1.0
*/
import com.powernode.bank.exception.MoneyNotEnoughException;
import com.powernode.bank.exception.TransferException;
import com.powernode.bank.service.AccountService;
import com.powernode.bank.service.impl.AccountServiceImpl;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
//@WebServlet("/transfer") 未找到资源,可能是注解没有生效,我们去web.xml中配置一下
//在web.xml中将 metadata-complete="false" 代表支持web.xml配置也支持注解配置
//@WebServlet("/transfer")
public class AccountServlet extends HttpServlet {
//因为其他方法也可能用到业务类的对象,所以设置为全局变量,父类引用执行子类对象[多态]
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类型
double money = Double.parseDouble(request.getParameter("money"));
//并不处理业务,调用service的转账方法完成转账,在这块属于一个控制器
try {
//转账操作可能出现异常,所以需要我们对异常进行捕获
accountService.transfer(fromActno, toActno, money);
//调用视图层(View)查看结果
response.sendRedirect(request.getContextPath() + "/success.html");
} catch (MoneyNotEnoughException e) {
//利用页面来实现异常 >> 牛
response.sendRedirect(request.getContextPath() + "/error1.html");
} catch (Exception e) {
response.sendRedirect(request.getContextPath() + "/error2.html");
}
}
}
三、测试我们的项目
🌔 1、运行tomcat,在网页填写表单
🌔 2、查看数据库表在执行操作前后的变换
(1)执行操作前
(2)执行操作后
四、MyBatis三大对象的作用域
- 这个与Web项目没啥关系,不过理解这部分也很重要,其实我在前面的SqlSessionUtil中就有提及
- 引用 MyBatis 的中文官方文档,对此展开详细的论述
1、SqlSessionFactoryBuilder
- 这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。【以保证所有的 XML 解析资源】
- 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
2、SqlSessionFactory
- SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。【不要创建多次】
- 因此 SqlSessionFactory 的最佳作用域是应用作用域。【最简单的就是使用单例模式或者静态单例模式】
3、SqlSession
- 每个线程都应该有它自己的 SqlSession 实例。
- SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。