目录
一:在WEB中应⽤MyBatis(使⽤MVC架构模式)
1. 前期准备
2. 核心代码实现
3. 事务控制
4. 三大对象的作用域
目标:
①掌握mybatis在web应⽤中怎么⽤
②mybatis三⼤对象的作⽤域和⽣命周期
③ThreadLocal原理及使⽤
④巩固MVC架构模式
⑤为学习MyBatis的接⼝代理机制做准备
实现功能:银⾏账户转账
使⽤技术: HTML + Servlet + MyBatis
WEB应⽤的名称: bank
(1)数据库表准备
设计表结构
插入数据
(2)在Web应用中使用Mybatis搭建环境,创建一个Web应用,并添加本地的Tomcat服务器,然后在进行资源的配置
①在pom.xml文件中引入依赖
4.0.0
mybatis-004-web
com.bjpowernode
mybatis-004-web
1.0-SNAPSHOT
war
UTF-8
1.8
1.8
org.mybatis
mybatis
3.5.10
mysql
mysql-connector-java
5.1.23
ch.qos.logback
logback-classic
1.2.11
javax.servlet
javax.servlet-api
3.1.0
②一些配置文件的引入
③编写前端转账页面index.html
银行账户转账
④使用MVC架构模式创建包
解释:dao是连接数据库、pojo是普通的java类、service是编写业务、utils是工具类、web是servlet
(1)按照三层架构分析:web是表示层、service是业务层、dao是持久化层。
(2)按照MVC架构模式分析:pojo、dao、service属于M、所有的.jsp属于V、web属于C。(3)MVC架构模式和三层的关系:M是包括业务层和持久层,V和C属于表示层。
pojo类,封装账户信息
package com.bjpowernode.bank.pojo;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.pojo
* @Project:mybatis
* @name:Account
* @Date:2023/1/1 17:53
*/
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;
}
// setter and getter
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;
}
// 重写toString
@Override
public String toString() {
return "Account{" +
"id=" + id +
", actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
}
工具类,连接数据库的类
package com.bjpowernode.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 SqlSessionUtils {
public SqlSessionUtils() {
}
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static SqlSession openSession(){
return sqlSessionFactory.openSession();
}
}
细节:在web.xml文件中有一个配置,metadata-complete="true">表示只支持配置文件的形式,不支持注解的形式开发;等于false表示两个都支持!
主要框架形式和所需要的展示页面
①bao包:连接数据库的操作,面向接口编程,接口和对应的额实现类
②exception包:抛出异常,一个是余额不足抛出的异常,一个是转账过程出现的异常
③pojo包:封装数据的,普通的java类
④service包:编写业务逻辑代码的,面向接口编程,接口和对应的额实现类
⑤utils包:连接数据库的代码封装
⑥web包:编写servlet,是司令官,进行调度的
①index.html是前端提交数据的页面,输入转账信息的
②money-not-enough.html是转出余额不足,所展示结果的页面
③success.html是转账成功的页面
④transfer-error.html是转账出现问题的页面
①AccountMapper.xml:专门编写sql语句的
②jdbc.properties:连接数据库所需要的配置信息
③logbaxk.xml:日志配置信息
④mybatis-config.xml:引入日志、读取连接数据库配置文件、连接数据库、关联AccountMapper.xml配置文件等信息的核心配置文件
(1)编写servlet,根据前端发送的请求,编写AccountServlet,这个Servlet相当于司令官C,调度两个秘书M和V
package com.bjpowernode.bank.web;
import com.bjpowernode.bank.exception.MoenyNotEnoughException;
import com.bjpowernode.bank.exception.TransferException;
import com.bjpowernode.bank.service.AccountService;
import com.bjpowernode.bank.service.impl.AccountServiceImpl;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.web
* @Project:mybatis
* @name:AccountServlet
* @Date:2023/1/1 19:23
*/
@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"));
// AccountService accountService = new AccountServiceImpl(); // 面向接口编程
try {
// 调用service的转账方法完成转账(调度业务层-M)
accountService.transfer(fromActno,toActno,money);
// 调用View完成展示结果
// 转账成功,跳转页面
response.sendRedirect(request.getContextPath()+"/success.html");
} catch (MoenyNotEnoughException e) {
// 钱不够,跳转页面
response.sendRedirect(request.getContextPath()+"/money-not-enough.html");
} catch (TransferException e) {
// 转账失败,跳转页面
response.sendRedirect(request.getContextPath()+"/transfer-error.html");
}
}
}
余额不足money-not-enough.html页面
余额不足
余额不足
转账成功success.html页面
转账成功
转账成功
转账失败transfer-error.html页面
转账失败
转账失败
(2)上面逻辑发现是司令官Servlet调用业务逻辑代码(M)完成转账操作,所以需要编写AccountService接口和AccountServiceImpl实现类
AccountService接口
package com.bjpowernode.bank.service;
import com.bjpowernode.bank.exception.MoenyNotEnoughException;
import com.bjpowernode.bank.exception.TransferException;
public interface AccountService {
/**
* 账户转账
* @param fromActno 转账账号
* @param toActno 转入账号
* @param money 转账金额
*/
void transfer(String fromActno,String toActno,double money) throws MoenyNotEnoughException, TransferException;
}
AccountServiceImpl实现类
package com.bjpowernode.bank.service.impl;
import com.bjpowernode.bank.bao.AccountDao;
import com.bjpowernode.bank.bao.impl.AccountDaoImpl;
import com.bjpowernode.bank.exception.MoenyNotEnoughException;
import com.bjpowernode.bank.exception.TransferException;
import com.bjpowernode.bank.pojo.Account;
import com.bjpowernode.bank.service.AccountService;
/** 编写业务逻辑代码
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.service.impl
* @Project:mybatis
* @name:AccountServiceImpl
* @Date:2023/1/1 19:40
*/
public class AccountServiceImpl implements AccountService {---
// 修改数据的类
private AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromActno, String toActno, double money) throws MoenyNotEnoughException, TransferException {
// 判断转出账户的余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
// 如果转出账户余额不足,抛出异常提示用户
throw new MoenyNotEnoughException("对不起,余额不足");
}
// 如果余额充足,更新转出和转入账户余额
Account toAct = accountDao.selectByActno(toActno);
// 修改内存中的余额
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
// 修改数据库当中余额
int count = accountDao.updateByActno(fromAct);
count += accountDao.updateByActno(toAct);
if (count != 2) {
// 转账异常
throw new TransferException("转账异常");
}
}
}
余额不足抛出异常MoenyNotEnoughException
package com.bjpowernode.bank.exception;
public class MoenyNotEnoughException extends Exception{
// 两个构造方法
public MoenyNotEnoughException() {
}
public MoenyNotEnoughException(String message) {
super(message);
}
}
转账失败抛出异常TransferException
package com.bjpowernode.bank.exception;
public class TransferException extends Exception{
public TransferException() {
}
public TransferException(String message) {
super(message);
}
}
(3)在编写业务逻辑发现又需要数据库的操作,这就需要一个类专门连接数据库,专门处理数据的操作;面向接口编程,编写AccountDao接口和AccountDaoImpl类
AccountDao接口
package com.bjpowernode.bank.bao;
import com.bjpowernode.bank.pojo.Account;
/**
* 负责对表中数据进行CRUD
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.bao
* @Project:mybatis
* @name:AccountDao
* @Date:2023/1/1 19:48
*/
public interface AccountDao {
/**
* 根据账号查询账户信息
* @param actno
* @return 返回的是账户信息
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act
* @return 1表示更新成功
*/
int updateByActno(Account act);
}
AccountDaoImpl类
package com.bjpowernode.bank.bao.impl;
import com.bjpowernode.bank.bao.AccountDao;
import com.bjpowernode.bank.pojo.Account;
import com.bjpowernode.bank.utils.SqlSessionUtils;
import org.apache.ibatis.session.SqlSession;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.bao.impl
* @Project:mybatis
* @name:AccountDaoImpl
* @Date:2023/1/1 19:57
*/
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtils.openSession();
Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
sqlSession.close();
return account;
}
@Override
public int updateByActno(Account act) {
SqlSession sqlSession = SqlSessionUtils.openSession();
int count = sqlSession.update("account.updateByActno", act);
sqlSession.commit();
sqlSession.close();
return count;
}
}
编写sql语句的AccountMapper.xml
update t_act set balance = #{balance} where actno = #{actno};
连接数据库的配置文件jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mybatis
jdbc.username=root
jdbc.password=123
核心配置文件mybatis-config.xml
注:当前的代码没有事务控制,如果在第一条更新语句之后,第二条更新语句之前增加一个空指针异常,那么就会出现钱丢失的情况,这就需要事务的控制!
(1)实际上事务的控制不应该放在Dao层,不能更新一条就提交一次;应该发到Service层,所有逻辑走完了,在提交。
(2)我们在Service层直接增加控制事务的代码,也是不行的;因为在这里获得的sqlSession对象和执行update语句获得的sqlSession对象不是同一个sqlSession对象。
package com.bjpowernode.bank.service.impl;
import com.bjpowernode.bank.bao.AccountDao;
import com.bjpowernode.bank.bao.impl.AccountDaoImpl;
import com.bjpowernode.bank.exception.MoenyNotEnoughException;
import com.bjpowernode.bank.exception.TransferException;
import com.bjpowernode.bank.pojo.Account;
import com.bjpowernode.bank.service.AccountService;
import com.bjpowernode.bank.utils.SqlSessionUtils;
import org.apache.ibatis.session.SqlSession;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.service.impl
* @Project:mybatis
* @name:AccountServiceImpl
* @Date:2023/1/1 19:40
*/
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromActno, String toActno, double money) throws MoenyNotEnoughException, TransferException {
// 添加事务控制代码
SqlSession sqlSession = SqlSessionUtils.openSession();
// 判断转出账户的余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
// 如果转出账户余额不足,抛出异常提示用户
throw new MoenyNotEnoughException("对不起,余额不足");
}
// 如果余额充足,更新转出和转入账户余额
Account toAct = accountDao.selectByActno(toActno);
// 修改内存中的余额
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
// 修改数据库当中余额
int count = accountDao.updateByActno(fromAct);
count += accountDao.updateByActno(toAct);
if (count != 2) {
// 转账异常
throw new TransferException("转账异常");
}
// 提交事务
sqlSession.commit();
sqlSession.close();
}
}
(3)使用ThreadLocal控制,本质上是一个Map集合,它的key存储的是线程对象,value存储的是sqlSession对象;我们把sqlSession对象存到这个Map集合里面,对于同一个线程,以后取出来的都是同一个sqlSession对象。
在工具类SqlSessionUtils中引入ThreadLocal
package com.bjpowernode.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:朗朗乾坤
* @Package:com.bjpowernode.mybatis.utils
* @Project:mybatis
* @name:SqlSessionUtils
* @Date:2022/12/29 14:11
*/
public class SqlSessionUtils {
public SqlSessionUtils() {
}
private static SqlSessionFactory sqlSessionFactory;
static {
try {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config.xml"));
} catch (IOException e) {
e.printStackTrace();
}
}
// 全局的,服务器级别的,一个服务器当中定义一个
private static ThreadLocal local = new ThreadLocal<>();
// 获取会话对象
public static SqlSession openSession(){
// 先从ThreadLocal当中获取
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服务器是支持多线程的,被当前用户用到的t1线程,有可能继续被其他用户使用
local.remove();
}
}
}
修改数据库AccountDaoImpl类,不应该在这里面commit和close
package com.bjpowernode.bank.bao.impl;
import com.bjpowernode.bank.bao.AccountDao;
import com.bjpowernode.bank.pojo.Account;
import com.bjpowernode.bank.utils.SqlSessionUtils;
import org.apache.ibatis.session.SqlSession;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.bao.impl
* @Project:mybatis
* @name:AccountDaoImpl
* @Date:2023/1/1 19:57
*/
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtils.openSession();
Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
return account;
}
@Override
public int updateByActno(Account act) {
SqlSession sqlSession = SqlSessionUtils.openSession();
int count = sqlSession.update("account.updateByActno", act);
return count;
}
}
在Service层AccountServiceImpl类中增加事务的控制
package com.bjpowernode.bank.service.impl;
import com.bjpowernode.bank.bao.AccountDao;
import com.bjpowernode.bank.bao.impl.AccountDaoImpl;
import com.bjpowernode.bank.exception.MoenyNotEnoughException;
import com.bjpowernode.bank.exception.TransferException;
import com.bjpowernode.bank.pojo.Account;
import com.bjpowernode.bank.service.AccountService;
import com.bjpowernode.bank.utils.SqlSessionUtils;
import org.apache.ibatis.session.SqlSession;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.service.impl
* @Project:mybatis
* @name:AccountServiceImpl
* @Date:2023/1/1 19:40
*/
public class AccountServiceImpl implements AccountService {
private AccountDao accountDao = new AccountDaoImpl();
@Override
public void transfer(String fromActno, String toActno, double money) throws MoenyNotEnoughException, TransferException {
// 添加事务控制代码
SqlSession sqlSession = SqlSessionUtils.openSession();
// 判断转出账户的余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
// 如果转出账户余额不足,抛出异常提示用户
throw new MoenyNotEnoughException("对不起,余额不足");
}
// 如果余额充足,更新转出和转入账户余额
Account toAct = accountDao.selectByActno(toActno);
// 修改内存中的余额
fromAct.setBalance(fromAct.getBalance()-money);
toAct.setBalance(toAct.getBalance()+money);
// 修改数据库当中余额
int count = accountDao.updateByActno(fromAct);
count += accountDao.updateByActno(toAct);
if (count != 2) {
// 转账异常
throw new TransferException("转账异常");
}
// 提交事务
sqlSession.commit();
// 调用方法关闭
SqlSessionUtils.close(sqlSession);
}
}
(1)SqlSessionFactoryBuilder:这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。
(2)SqlSessionFactory:SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例,除非连接另一个数据库,一个数据库就对应一个SqlSessionFactory对象。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,所以封装的时候是放到静态代码块里。因此 SqlSessionFactory 的最佳作用域是应用作用域。
(3)SqlSession:每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域(一次请求)。 一个线程对应一个SqlSession对象。
补充:我们发现Dao实现类AccountDaoImp的每个方法代码就两行,而且还有一行是重复的;那么这个类能不能不写了,在内存中生成这个接口的实现类?使用javassist技术,可以只给接口,根据这个接口在内存中自动生成这个实现类,这就是接下来要学的技术!
package com.bjpowernode.bank.bao.impl;
import com.bjpowernode.bank.bao.AccountDao;
import com.bjpowernode.bank.pojo.Account;
import com.bjpowernode.bank.utils.SqlSessionUtils;
import org.apache.ibatis.session.SqlSession;
/**
* @Author:朗朗乾坤
* @Package:com.bjpowernode.bank.bao.impl
* @Project:mybatis
* @name:AccountDaoImpl
* @Date:2023/1/1 19:57
*/
public class AccountDaoImpl implements AccountDao {
@Override
public Account selectByActno(String actno) {
SqlSession sqlSession = SqlSessionUtils.openSession();
Account account = (Account) sqlSession.selectOne("account.selectByActno", actno);
return account;
}
@Override
public int updateByActno(Account act) {
SqlSession sqlSession = SqlSessionUtils.openSession();
int count = sqlSession.update("account.updateByActno", act);
return count;
}
}