实现JDBC事务
依赖
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-dbutils/commons-dbutils -->
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
配置文件jdbc.properties
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql:///db_demo
username=root
password=root
initialSize=10
maxActive=20
maxWait=10000
JDBC工具类
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* 功能1:从数据源获取数据库连接
* 功能2:从数据库获取到数据库连接后,绑定到本地线程(借助 ThreadLocal)
* 功能3:释放线程时和本地线程解除绑定
*/
public class JDBCUtils {
// 数据源成员变量设置为静态资源,保证大对象的单例性;同时保证静态方法中可以访问
private static DataSource dataSource;
// 由于 ThreadLocal 对象需要作为绑定数据时 k-v 对中的 key,所以要保证唯一性
// 加 static 声明为静态资源即可保证唯一性
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
// 在静态代码块中初始化数据源
static {
try {
// 操作思路分析:
// 从 jdbc.properties 文件中读取连接数据库的信息
// 为了保证程序代码的可移植性,需要基于一个确定的基准来读取这个文件
// 确定的基准:类路径的根目录。resources 目录下的内容经过构建操作中的打包操作后会确定放在 WEB-INF/classes 目录下。
// WEB-INF/classes 目录存放编译好的 *.class 字节码文件,所以这个目录我们就称之为类路径。
// 类路径无论在本地运行还是在服务器端运行都是一个确定的基准。
// 操作具体代码:
// 1、获取当前类的类加载器
ClassLoader classLoader = JDBCUtils.class.getClassLoader();
// 2、通过类加载器对象从类路径根目录下读取文件
InputStream stream = classLoader.getResourceAsStream("jdbc.properties");
// 3、使用 Properties 类封装属性文件中的数据
Properties properties = new Properties();
properties.load(stream);
// 4、根据 Properties 对象(已封装了数据库连接信息)来创建数据源对象
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
e.printStackTrace();
// 为了避免在真正抛出异常后,catch 块捕获到异常从而掩盖问题,
// 这里将所捕获到的异常封装为运行时异常继续抛出
throw new RuntimeException(e);
}
}
/**
* 工具方法:获取数据库连接并返回
* @return
*/
public static Connection getConnection() {
Connection connection = null;
try {
// 1、尝试从当前线程检查是否存在已经绑定的 Connection 对象
connection = threadLocal.get();
// 2、检查 Connection 对象是否为 null
if (connection == null) {
// 3、如果为 null,则从数据源获取数据库连接
connection = dataSource.getConnection();
// 4、获取到数据库连接后绑定到当前线程
threadLocal.set(connection);
}
} catch (SQLException e) {
e.printStackTrace();
// 为了调用工具方法方便,编译时异常不往外抛
// 为了不掩盖问题,捕获到的编译时异常封装为运行时异常抛出
throw new RuntimeException(e);
}
return connection;
}
/**
* 释放数据库连接
*/
public static void releaseConnection(Connection connection) {
if (connection != null) {
try {
// 在数据库连接池中将当前连接对象标记为空闲
connection.close();
// 将当前数据库连接从当前线程上移除
threadLocal.remove();
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
}
过滤器Filter
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Set;
public class TransactionFilter implements Filter {
// 声明集合保存静态资源扩展名
private static Set<String> staticResourceExtNameSet;
static {
staticResourceExtNameSet = new HashSet<>();
staticResourceExtNameSet.add(".png");
staticResourceExtNameSet.add(".jpg");
staticResourceExtNameSet.add(".css");
staticResourceExtNameSet.add(".js");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 前置操作:排除静态资源
HttpServletRequest request = (HttpServletRequest) servletRequest;
String servletPath = request.getServletPath();
if (servletPath.contains(".")) {
String extName = servletPath.substring(servletPath.lastIndexOf("."));
if (staticResourceExtNameSet.contains(extName)) {
// 如果检测到当前请求确实是静态资源,则直接放行,不做事务操作
filterChain.doFilter(servletRequest, servletResponse);
// 当前方法立即返回
return;
}
}
Connection connection = null;
try {
// 1、获取数据库连接
connection = JDBCUtils.getConnection();
// 重要操作:关闭自动提交功能
connection.setAutoCommit(false);
// 2、核心操作
filterChain.doFilter(servletRequest, servletResponse);
// 3、提交事务
connection.commit();
} catch (Exception e) {
try {
// 4、回滚事务
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
// 页面显示:将这里捕获到的异常发送到指定页面显示
// 获取异常信息
String message = e.getMessage();
// 将异常信息存入请求域
request.setAttribute("systemMessage", message);
// 将请求转发到指定页面
request.getRequestDispatcher("/").forward(request, servletResponse);
} finally {
// 5、释放数据库连接
JDBCUtils.releaseConnection(connection);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
配置 web.xml
<filter>
<filter-name>txFilter</filter-name>
<filter-class>com.wuxian.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>txFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
基类Dao
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import java.sql.Connection;
import java.sql.SQLException;
/**
* BaseDao 类:所有 Dao 实现类的基类
* @param <T> 实体类的类型
*/
public class BaseDao<T> {
// DBUtils 工具包提供的数据库操作对象
private QueryRunner runner = new QueryRunner();
/**
* 查询返回多个对象的方法
* @param sql 执行查询操作的 SQL 语句
* @param entityClass 实体类的 Class 对象
* @param parameters SQL 语句的参数
* @return 查询结果
*/
public List<T> getBeanList(String sql, Class<T> entityClass, Object ... parameters) {
try {
// 获取数据库连接
Connection connection = JDBCUtils.getConnection();
return runner.query(connection, sql, new BeanListHandler<>(entityClass), parameters);
} catch (SQLException e) {
e.printStackTrace();
// 如果真的抛出异常,则将编译时异常封装为运行时异常抛出
new RuntimeException(e);
}
return null;
}
/**
* 查询单个对象
* @param sql 执行查询的 SQL 语句
* @param entityClass 实体类对应的 Class 对象
* @param parameters 传给 SQL 语句的参数
* @return 查询到的实体类对象
*/
public T getSingleBean(String sql, Class<T> entityClass, Object ... parameters) {
try {
// 获取数据库连接
Connection connection = JDBCUtils.getConnection();
return runner.query(connection, sql, new BeanHandler<>(entityClass), parameters);
} catch (SQLException e) {
e.printStackTrace();
// 如果真的抛出异常,则将编译时异常封装为运行时异常抛出
new RuntimeException(e);
}
return null;
}
/**
* 通用的增删改方法,insert、delete、update 操作都可以用这个方法
* @param sql 执行操作的 SQL 语句
* @param parameters SQL 语句的参数
* @return 受影响的行数
*/
public int update(String sql, Object ... parameters) {
try {
Connection connection = JDBCUtils.getConnection();
int affectedRowNumbers = runner.update(connection, sql, parameters);
return affectedRowNumbers;
} catch (SQLException e) {
e.printStackTrace();
// 如果真的抛出异常,则将编译时异常封装为运行时异常抛出
new RuntimeException(e);
return 0;
}
}
}
DemoDao
public interface DemoDao {
Demo getById(Integer id);
}
DemoDaoimpl
public class DemoDaoImpl extends BaseDao<Demo> implements DemoDao {
@Override
public Demo getById(Integer id) {
// 1、编写 SQL 语句
String sql = "select id," +
"name," +
"type," +
"code " +
"from t_demo where id=?";
// 2、调用父类方法查询单个对象
return super.getSingleBean(sql, Demo.class, id);
}
}
注意事项
-
确保异常回滚
在程序执行的过程中,必须让所有 catch 块都把编译时异常转换为运行时异常抛出;如果不这么做,在 TransactionFilter 中 catch 就无法捕获到底层抛出的异常,那么该回滚的时候就无法回滚。 -
谨防数据库连接提前释放
由于诸多操作都是在使用同一个数据库连接,那么中间任何一个环节释放数据库连接都会导致后续操作无法正常完成。