- 目标:
- 掌握 mybatis 在 web 应用中怎么用。
- mybatis 三大对象的作用域和生命周期。
- ThreadLocal 原理及使用。
- 巩固 MVC 架构模式。
- 为学习 MyBatis 的接口代理机制做准备。
- 实现功能:
- 银行账户转账
- 使用技术:
- HTML + Servlet + Mybatis
- Web应用名称:
- bank
六、在WEB中使用Mybatis
- 目标:
- 掌握 mybatis 在 web 应用中怎么用。
- mybatis 三大对象的作用域和生命周期。
- ThreadLocal 原理及使用。
- 巩固 MVC 架构模式。
- 为学习 MyBatis 的接口代理机制做准备。
- 实现功能:
- 银行账户转账
- 使用技术:
- HTML + Servlet + Mybatis
- Web应用名称:
- bank
6.1 银行账户转账实现
实现步骤,可以从前端往后端写,根据请求一步步往后写。也可以从后端往前端写。
这里是从前端往后端写。
6.1.1 环境搭建
在 IDEA 中创建 Maven WEB 应用(
mybatis-004-web
):
IDEA 配置 Tomcat,这里 Tomcat 使用 10+ 版本。并部署应用到 tomcat。


这个 web 模板的
web.xml
文件的版本较低,可以从 tomcat10 的样例文件中复制,然后修改。1
2
3
4
5
6
7
8
9
<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_6_0.xsd"
version="6.0"
metadata-complete="false">
</web-app>注意:
metadata-complete="false"
是指支持@WebServle("")
注解式开发,如果为 true 则表示不支持注解式开发。删除
index.jsp
文件,因为我们这个项目不使用 JSP,只使用 html。在
pom.xml
文件中引入相关依赖,包括mybatis
,mysql
驱动,logback
,servlet
,jsp
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.f</groupId>
<artifactId>mybatis-004-web</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>mybatis-004-web Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
</dependency>
<!--logback依赖-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.12</version>
</dependency>
<!--servlet依赖-->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
</dependency>
<!--jsp依赖-->
<dependency>
<groupId>jakarta.servlet.jsp</groupId>
<artifactId>jakarta.servlet.jsp-api</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>mybatis-004-web</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>引入相关配置文件,放到
resources
目录下(全部放到类的根路径下)mybatis-config.xml
AccountMapper.xml
logback.xml
jdbc.properties
6.1.2 前端页面index.html
1 |
|
6.1.3 创建web包、service包、pojo包、dao包、utils包、exceptions包
web
包 -> 表示层service
包 -> 业务逻辑层dao
包 -> 持久层(数据访问层)
6.1.4 定义pojo类:Account
1 | package com.f.bank.pojo; |
6.1.5 编写AccountService接口以及AccountServiceImpl
★面向接口编程
在 javaweb 中说过,可以将业务代码分为表示层、业务逻辑层和数据访问层,层与层之间通过接口进行调用,是面向接口编程。
所以在
service
包中编写接口AccountService
,以及在后面的dao
包中编写接口AccountDao
。再编写它们的实现类,通过调用实现类具体的方法来实现功能。- 注意:将接口写在
service
包和dao
包下,然后在这两个包下分别再建impl
包,用于存放创建的实现类。
- 注意:将接口写在
1 | package com.f.bank.service; |
1 | package com.f.bank.service.impl; |
6.1.6 编写AccountDao接口,以及AccountDaoImpl实现类
1 | package com.f.bank.dao; |
1 | package com.f.bank.dao.impl; |
6.1.7 编写AccountServlet
AccountServlet
即为controller
,由它来调度model
和view
,以完成用户的请求。逻辑为:
- 获取用户提交的表单数据。
- 调用
AccountService
的转账方法完成转账(调业务层)。- 在
AccountService
中需要有AccountDao
,才能完成对数据库的操作。 - 在
AccountDao
的实现类中,使用 mybatis。
- 在
- 调用
view
完成展示结果。
1 | package com.f.bank.web; |
6.1.8 其他…
- 还有转账成功后的页面、转账失败后的页面、自定义的异常、
SqlSessionUtil
等代码,就不贴在这里了,可以去看mybatis-004-web
的项目源代码。
6.2 Mybatis事务问题
在 6.1 小节中,还有一个问题,就是在
AccountServiceImpl
代码中,还有一个事务的问题:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, TransferException {
// 1.判断转出账户的余额是否充足(select)
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
// 2.如果转出账户的余额不足,提示用户
throw new MoneyNotEnoughException("您的账户余额不足!");
}
// 3.如果转出账户余额充足,更新转出账户余额(update)
// 先更新内存中java对象的信息,再更新数据库
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.updateAct(fromAct);
// 4.更新转入账户余额(update)
count += accountDao.updateAct(toAct);
if (count != 2) {
// count不等于2说明转账有异常
throw new TransferException("转账失败, 原因未知");
}
}在更新账户对象,即下面的代码时:
1
2
3int count = accountDao.updateAct(fromAct);
// 如果有其他代码,且这些代码中出现异常
count += accountDao.updateAct(toAct);如果上一行代码执行成功,下一行代码执行失败,虽然会抛出异常,但在数据库中,上一行代码是实际地改变了数据库的数据,所以就会造成数据错误。因此,我们需要在
AccountServiceImpl
中进行事务的提交,而非在AccountDaoImpl
中进行事务的提交。
★ThreadLocal
另外还有一个问题,在 6.1 节中,
AccountDaoImpl
中两个方法的SqlSession
对象并非是同一个,这也会影响事务的控制。为了让AccountServiceImpl
、AccountDaoImpl
的两个方法中的SqlSession
对象是同一个,我们需要使用ThreadLocal
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29package com.f.bank.dao.impl;
import com.f.bank.dao.AccountDao;
import com.f.bank.pojo.Account;
import com.f.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
/**
* @author fzy
* @date 2024/1/6 11:51
*/
public class AccountDaoImpl implements AccountDao {
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession(); //一个SqlSession对象
Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
sqlSession.close();
return account;
}
public int updateAct(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession(); //另一个SqlSession对象
int count = sqlSession.update("account.updateAct", act);
sqlSession.commit();
sqlSession.close();
return count;
}
}修改后的代码如下:
SqlSessionUtil
中的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63package com.f.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;
/**
* @author fzy
* @date 2023/12/31 20:59
*/
public class SqlSessionUtil {
private static SqlSessionFactory sqlSessionFactory = null;
// 增加ThreadLocal对象
private static ThreadLocal<SqlSession> local = new ThreadLocal<>();
static {
try {
// SqlSessionFactory对象:一个SqlSessionFactory对应一个environment,一个environment通常是一个数据库。
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));
} catch (Exception e) {
e.printStackTrace();
}
}
// 工具类的构造方法一般都是私有化的。
// 工具类中所有的方法都是静态的,直接采用类名即可调用。不需要new对象。
// 为了防止new对象,构造方法私有化。
private SqlSessionUtil() {
}
/**
* 获取 SqlSession 对象
*/
//public static SqlSession openSession() {
// return sqlSessionFactory.openSession();
//}
// 将上面的代码改为下面的代码
public static SqlSession openSession() {
SqlSession sqlSession = local.get();
if (sqlSession == null) {
sqlSession = sqlSessionFactory.openSession();
// 将SqlSession对象绑定到当前线程上
local.set(sqlSession);
}
return sqlSession;
}
/**
* 关闭SqlSession对象(从当前线程中移除SqlSession对象)
*
* @param sqlSession
*/
public static void close(SqlSession sqlSession) {
if (sqlSession != null) {
sqlSession.close();
// 解绑SqlSession对象。
// 因为Tomcat服务器支持线程池,也就是说,用过的线程对象t1,可能下一次还会使用这个t1线程,
// 为了避免错误使用之前绑定的SqlSession对象,所以要解绑
local.remove();
}
}
}AccountServiceImpl
中的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46package com.f.bank.service.impl;
import com.f.bank.dao.AccountDao;
import com.f.bank.dao.impl.AccountDaoImpl;
import com.f.bank.exceptions.MoneyNotEnoughException;
import com.f.bank.exceptions.TransferException;
import com.f.bank.pojo.Account;
import com.f.bank.service.AccountService;
import com.f.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
/**
* @author fzy
* @date 2024/1/6 11:36
*/
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();
// 1.判断转出账户的余额是否充足(select)
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
// 2.如果转出账户的余额不足,提示用户
throw new MoneyNotEnoughException("您的账户余额不足!");
}
try {
// 3.如果转出账户余额充足,更新转出账户余额(update)
// 先更新内存中java对象的信息,再更新数据库
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.updateAct(fromAct);
// 4.更新转入账户余额(update)
count += accountDao.updateAct(toAct);
// 提交事务
sqlSession.commit();
} catch (Exception e) {
throw new TransferException("转账失败, 原因未知");
} finally {
// 释放资源
SqlSessionUtil.close(sqlSession);
}
}
}AccountDaoImpl
中的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28package com.f.bank.dao.impl;
import com.f.bank.dao.AccountDao;
import com.f.bank.pojo.Account;
import com.f.bank.utils.SqlSessionUtil;
import org.apache.ibatis.session.SqlSession;
/**
* @author fzy
* @date 2024/1/6 11:51
*/
public class AccountDaoImpl implements AccountDao {
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtil.openSession();
Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
// 不进行commit和close了
return account;
}
public int updateAct(Account act) {
SqlSession sqlSession = SqlSessionUtil.openSession();
int count = sqlSession.update("account.updateAct", act);
// 不进行commit和close了
return count;
}
}
6.3 ★Mybatis对象作用域
SqlSessionFactoryBuilder
这个类可以被实例化、使用和丢弃,一旦创建了
SqlSessionFactory
,就不再需要它了。因此
SqlSessionFactoryBuilder
实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用SqlSessionFactoryBuilder
来创建多个SqlSessionFactory
实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。
SqlSessionFactory
SqlSessionFactory
一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用SqlSessionFactory
的最佳实践是在应用运行期间不要重复创建多次,多次重建SqlSessionFactory
被视为一种代码“坏习惯”。因此SqlSessionFactory
的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。
SqlSession
- 每个线程都应该有它自己的
SqlSession
实例。SqlSession
的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将SqlSession
实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将SqlSession
实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的HttpSession
。 如果你现在正在使用一种 Web 框架,考虑将SqlSession
放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个SqlSession
,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到finally
块中。