浅谈 ThreadLocal 在持久化的作用
补充知识可以先看看这一篇: MVC开发规范的常见问题
ThreadLocal 是什么?
ThreadLocal 底层就是用 Map 来实现的 只不过他的 key 是
Thread类型,value 是Object以便存放元素
而在持久化的作用,是让事务跨 Dao、service 层获取连接对象
持久化的什么场景需要用到 ThreadLocal ?
当在 JavaWeb 的 Servlet 开发时,使用 MVC 规范的三层架构时,在需要有事务控制的业务需要使用到 ThreadLocal 的帮助
我们知道,事务一般是在 Service层中开启 / 关闭的。然而,Connection 对象并不在 Service层开启,而是在 Dao 中开启 但这就存在一个问题了:那就是如果有多条 SQL 语句执行的话,就不能确保原子性了,事务就不能保证
持久化时容易出现的问题
未使用 ThreadLocal 的 DBUtils
public class DBUtils {
private static Connection conn = null;
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取连接
*/
public static Connection getconnection() {
try {
conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
} catch (SQLException throwables) { }
return conn;
}
/**
* 释放资源
*/
public static void close(){
if (conn != null) {
try {
conn.close();
} catch (SQLException throwables) { }
}
}
}
Dao 是这样子定义的
public Integer insert(Enterprise enterprise, String[] invregnums, String[] regcaps, String[] scales) throws SQLException {
String sql = "insert into t_enterprise values(?,?,?,?,?,?,?,?,?,?,?)";
ps = DBUtil.createPrepareStatement(sql); // 注意一下这里
}
Service 是这样的
在获取 Connection 连接的时候还没出现问题,问题出在了 Service 的事务控制上
/**
* 在这里,query,insert 是两条独立的语句了,并没有原子性(即使已经开启了事务,但Connection不是同一个,所以不起作用!)
*/
public Integer insert(Enterprise enterprise, String[] invregnums, String[] regcaps, String[] scales) throws SQLException {
try {
DBUtils.beginTransaction(conn); // 在这里有问题
count = enterpriseDao.query(); // 查询
result = enterpriseDao.insert(enterprise,invregnums,regcaps,scales); // insert
DBUtils.commitTransaction(conn);
} catch (SQLException throwables) {
DBUtils.rollbackTransaction(conn);
}finally {
DBUtils.endTransaction(conn);
DBUtils.close();
}
return count;
}
问题所在
于是我们发现了问题:service 中的
Connection和 Dao 中的Connection不是同一个对象 而都是分别开启了不同的连接对象,所以不能保证事务的成成功执行!!! Connection 在一条 SQL 语句执行完毕之后就已经关闭了,但我们要在事务完成之后才能将 Connection 关闭才是正确的做法
解决办法一(无 ThreadLocal)
将 Connection 作为参数从 Service 传入到 Dao,然后在 DBUtils 上写好方法重载(带 Connection 的形参)
// Service中
enterpriseDao.query("sql", connection);
// Dao中
public Integer insert(String sql, Connection conn) {
ps = DBUtil.createPrepareStatement(sql, conn);
}
// DBUtils中
public static PreparedStatement createPrepareStatement(String sql, Connection conn) {
this.conn = conn;
PreparedStatement ps = conn.prepareStatement(sql);
return ps;
}
缺点
虽然解决了事务控制的问题,保证了使用同一个 Connection 对象 但方法的形参变多了,导致Service传参、Dao接口、DaoImpl、DBUtils 的方法上都要多出来一个 Connection 这样子代码就很累赘,而且耦合度增加了
解决方法二(使用 ThreadLocal)
DBUtils 定义
public class DBUtils {
private static ThreadLocal threadLocal = new ThreadLocal();
private static Connection conn = null;
static {
Class.forName("com.mysql.cj.jdbc.Driver");
}
/**
* 获取连接
*/
public static Connection getConnection() {
conn = (Connection) threadLocal.get(); // 首先看 ThreadLocal 中有没有 conn
if (conn == null){ // 如果没有就新建
try {
conn = DriverManager.getConnection(URL,USERNAME,PASSWORD);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
threadLocal.set(conn);
}
return conn; // 如果有就直接返回
}
/**
*获取 PreparedStatement
*/
public static PreparedStatement createPrepareStatement(String sql) throws SQLException {
conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
return ps;
}
/**
* 释放资源
*/
public static void close(){
if (ps != null) {
try {
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (conn != null) {
try {
threadLocal.remove(); // 当前线程一定要和连接对象解除绑定关系!!!
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
public static void close(ResultSet rs){
if (rs != null) {
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
close();
}
}
这样子就保证了一个线程对应一个
Connection对象
Dao 方法
ps = DBUtil.createPrepareStatement(sql);
Service 层调用
DBUtils.beginTransaction(conn); // 开启事务
count = enterpriseDao.query();
result = enterpriseDao.insert(enterprise,invregnums,regcaps,scales);
DBUtils.commitTransaction(conn); // 提交事务
// 提交、回滚、释放Connection 代码略...
这样子就显得干净很多了,代码结构没那么混乱
ThreadLocal 实现原理
import java.util.HashMap;
import java.util.Map;
/**
* ThreadLocal 作用是让事务跨 Dao、service 层获取连接对象
* 因为在 service 层中控制事务,必须保证 service 方法执行的连接对象和 dao 方法中的 Connection 是同一个
* Author: chenjunjia
* Date: 2021/11/13 18:37
* WeChat: China_JoJo_
* Blog: https://juejin.cn/user/1856417285289304/posts
* Github: https://github.com/chenjjiaa
*/
public class ThreadLocal<T> {
Map<Thread,T> threadLocalMap = new HashMap();
public void set(T obj){
threadLocalMap.put(Thread.currentThread(),obj);
}
public T get(){
return threadLocalMap.get(Thread.currentThread());
}
public void remove(){
threadLocalMap.remove(Thread.currentThread());
}
}
end……