# day13 书城项目第五阶段
# 功能二 结账
# 1. 创建订单模型
# 1.1 物理建模
# 1.1.1 t_order表
CREATE TABLE t_order( order_id INT PRIMARY KEY AUTO_INCREMENT, order_sequence VARCHAR(200), create_time VARCHAR(100), total_count INT, total_amount DOUBLE, order_status INT, user_id INT );
Copied!
字段名 | 字段作用 |
---|---|
order_id | 主键 |
order_sequence | 订单号 |
create_time | 订单创建时间 |
total_count | 订单的总数量 |
total_amount | 订单的总金额 |
order_status | 订单的状态 |
user_id | 下单的用户的id |
- 虽然order_sequence也是一个不重复的数值,但是不使用它作为主键。数据库表的主键要使用没有业务功能的字段来担任。
- 订单的状态
- 待支付(书城项目中暂不考虑)
- 已支付,待发货:0
- 已发货:1
- 确认收货:2
- 发起退款或退货(书城项目中暂不考虑)
- 用户id
- 从逻辑和表结构的角度来说,这其实是一个外键。
- 但是开发过程中建议先不要加外键约束:因为开发过程中数据尚不完整,加了外键约束开发过程中使用测试数据非常不方便,建议项目预发布时添加外键约束测试。
# 1.1.2 t_order_item表
CREATE TABLE t_order_item( item_id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(20), price DOUBLE, img_path VARCHAR(50), item_count INT, item_amount DOUBLE, order_id VARCHAR(20) );
Copied!
字段名称 | 字段作用 |
---|---|
item_id | 主键 |
book_name | 书名 |
price | 单价 |
item_count | 当前订单项的数量 |
item_amount | 当前订单项的金额 |
order_id | 当前订单项关联的订单表的主键 |
说明:book_name、author、price这三个字段其实属于t_book表,我们把它们加入到t_order_item表中,其实并不符合数据库设计三大范式。这里做不符合规范的操作的原因是:将这几个字段加入当前表就不必在显示数据时和t_book表做关联查询,提高查询的效率,这是一种变通的做法。
# 1.2 逻辑模型
# 1.2.1 Order类
public class Order { private Integer orderId; private String orderSequence; private String createTime; private Integer totalCount; private Double totalAmount; private Integer orderStatus = 0; private Integer userId;
Copied!
# 1.2.2 OrdrItem类
public class OrderItem { private Integer itemId; private String bookName; private Double price; private String imgPath; private Integer itemCount; private Double itemAmount; private Integer orderId;
Copied!
# 2. 创建组件
# 2.1 持久化层
# 2.2 业务逻辑层
# 2.3 表述层
# 3. 功能步骤
- 创建订单对象
- 给订单对象填充数据
- 生成订单号
- 生成订单的时间
- 从购物车迁移总数量和总金额
- 从已登录的User对象中获取userId并设置到订单对象中
- 将订单对象保存到数据库中
- 获取订单对象在数据库中自增主键的值
- 根据购物车中的CartItem集合逐个创建OrderItem对象
- 每个OrderItem对象对应的orderId属性都使用前面获取的订单数据的自增主键的值
- 把OrderItem对象的集合保存到数据库
- 每一个item对应的图书增加销量
- 每一个item对应的图书减少库存
- 清空购物车
# 4. 案例思路
# 5. 代码实现
# 5.1 购物车页面结账超链接
cart.html
<a class="pay" href="protected/OrderClientServlet?method=checkout">去结账</a>
Copied!
# 5.2 OrderClientServlet.checkout()
protected void checkout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1.获取HttpSession对象 HttpSession session = request.getSession(); // 2.获取购物车对象 Cart cart = (Cart) session.getAttribute("cart"); if (cart == null) { String viewName = "cart/cart"; processTemplate(viewName, request, response); return ; } // 3.获取已登录的用户对象 User user = (User) session.getAttribute("user"); // 4.调用Service方法执行结账的业务逻辑 String orderSequence = orderService.checkout(cart, user); // 5.清空购物车 session.removeAttribute("cart"); // 6.将订单号存入请求域 request.setAttribute("orderSequence", orderSequence); // 7.将页面跳转到下单成功页面 String viewName = "cart/checkout"; processTemplate(viewName, request, response); }
Copied!
# 5.3 BaseDao.batchUpdate()
/** * 通用的批量增删改方法 * @param sql * @param params 执行批量操作的二维数组 * 每一条SQL语句的参数是一维数组 * 多条SQL语句的参数就是二维数组 * @return 每一条SQL语句返回的受影响的行数 */ public int[] batchUpdate(String sql, Object[][] params) { Connection connection = JDBCUtils.getConnection(); int[] rowCountArr = null; try { rowCountArr = queryRunner.batch(connection, sql, params); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } finally { JDBCUtils.releaseConnection(connection); } return rowCountArr; }
Copied!
# 5.4 OrderService.checkout()
@Override public String checkout(Cart cart, User user) { // 从User对象中获取userId Integer userId = user.getUserId(); // 创建订单对象 Order order = new Order(); // 给订单对象填充数据 // 生成订单号=系统时间戳 String orderSequence = System.currentTimeMillis() + "_" + userId; // 生成订单的时间 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); String createTime = simpleDateFormat.format(new Date()); // 从购物车迁移总数量和总金额 Integer totalCount = cart.getTotalCount(); Double totalAmount = cart.getTotalAmount(); order.setOrderSequence(orderSequence); order.setCreateTime(createTime); order.setTotalCount(totalCount); order.setTotalAmount(totalAmount); // 将订单对象保存到数据库中 // ※说明:这里对insertOrder()方法的要求是获取自增的主键并将自增主键的值设置到Order对象的orderId属性中 orderDao.insertOrder(order); // 获取订单对象在数据库中自增主键的值 Integer orderId = order.getOrderId(); // 根据购物车中的CartItem集合逐个创建OrderItem对象 Map<String, CartItem> cartItemMap = cart.getCartItemMap(); Collection<CartItem> cartItems = cartItemMap.values(); List<CartItem> cartItemList = new ArrayList<>(cartItems); // 为了便于批量保存OrderItem,创建Object[][] // 二维数组第一维:SQL语句的数量 // 二维数组第二维:SQL语句中参数的数量 Object[][] saveOrderItemParamArr = new Object[cartItems.size()][6]; // 为了便于批量更新Book,创建Object[][] Object[][] updateBookParamArr = new Object[cartItems.size()][3]; for (int i = 0;i < cartItemList.size(); i++) { CartItem cartItem = cartItemList.get(i); // 为保存OrderItem创建Object[] Object[] orderItemParam = new Object[6]; // book_name,price,img_path,item_count,item_amount,order_id orderItemParam[0] = cartItem.getBookName(); orderItemParam[1] = cartItem.getPrice(); orderItemParam[2] = cartItem.getImgPath(); orderItemParam[3] = cartItem.getCount(); orderItemParam[4] = cartItem.getAmount(); orderItemParam[5] = orderId; // 将一维数组存入二维数组中 saveOrderItemParamArr[i] = orderItemParam; // 创建数组用于保存更新Book数据的信息 String[] bookUpdateInfoArr = new String[3]; // 增加的销量 bookUpdateInfoArr[0] = cartItem.getCount() + ""; // 减少的库存 bookUpdateInfoArr[1] = cartItem.getCount() + ""; // bookId bookUpdateInfoArr[2] = cartItem.getBookId(); // 将数组存入List集合 updateBookParamArr[i] = bookUpdateInfoArr; } // 把OrderItem对象的集合保存到数据库:批量操作 orderItemDao.insertOrderItemArr(saveOrderItemParamArr); // 使用bookUpdateInfoList对图书数据的表执行批量更新操作 bookDao.updateBookByParamArr(updateBookParamArr); // 返回订单号 return orderSequence; }
Copied!
# 5.5 orderDao.insertOrder(order)
@Override public void insertOrder(Order order) { // ※DBUtils没有封装获取自增主键的方法,需要我们使用原生的JDBC来完成 // 1.获取数据库连接 Connection connection = JDBCUtils.getConnection(); // 2.创建PreparedStatement对象 String sql = "INSERT INTO t_order(order_sequence,create_time,total_count,total_amount,order_status,user_id) VALUES(?,?,?,?,?,?)"; try { // ①创建PreparedStatement对象,指明需要自增的主键 PreparedStatement preparedStatement = connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); // ②给PreparedStatement对象设置SQL语句的参数 preparedStatement.setString(1, order.getOrderSequence()); preparedStatement.setString(2, order.getCreateTime()); preparedStatement.setInt(3, order.getTotalCount()); preparedStatement.setDouble(4, order.getTotalAmount()); preparedStatement.setInt(5, order.getOrderStatus()); preparedStatement.setInt(6, order.getUserId()); // ③执行更新 preparedStatement.executeUpdate(); // ④获取封装了自增主键的结果集 ResultSet generatedKeysResultSet = preparedStatement.getGeneratedKeys(); // ⑤解析结果集 if (generatedKeysResultSet.next()) { int orderId = generatedKeysResultSet.getInt(1); order.setOrderId(orderId); } } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } finally { JDBCUtils.releaseConnection(connection); } }
Copied!
# 5.6 orderItemDao.insertOrderItemArr(saveOrderItemParamArr)
@Override public void insertOrderItemArr(Object[][] saveOrderItemParamArr) { String sql = "INSERT INTO t_order_item(book_name,price,img_path,item_count,item_amount,order_id) VALUES(?,?,?,?,?,?)"; super.batchUpdate(sql, saveOrderItemParamArr); }
Copied!
# 5.7 bookDao.updateBookByParamArr(updateBookParamArr)
@Override public void updateBookByParamArr(Object[][] updateBookParamArr) { String sql = "update t_book set sales=sales+?,stock=stock-? where book_id=?"; super.batchUpdate(sql, updateBookParamArr); }
Copied!
# 功能三 结账过程中使用事务
# 1. 事务回顾
# 1.1 ACID属性
A:原子性 事务中包含的数据库操作缺一不可,整个事务是不可再分的。
C:一致性 事务执行之前,数据库中的数据整体是正确的;事务执行之后,数据库中的数据整体仍然是正确的。
- 事务执行成功:提交(commit)
- 事务执行失败:回滚(rollback)
I:隔离性 数据库系统同时执行很多事务时,各个事务之间基于不同隔离级别能够在一定程度上做到互不干扰。简单说就是:事务在并发执行过程中彼此隔离。
D:持久性 事务一旦提交,就永久保存到数据库中,不可撤销。
# 1.2 隔离级别
# 1.2.1 并发问题
并发问题 | 问题描述 |
---|---|
脏读 | 当前事务读取了其他事务尚未提交的修改 如果那个事务回滚,那么当前事务读取到的修改就是错误的数据 |
不可重复读 | 当前事务中多次读取到的数据不一致(数据行数一致,但是行中的具体内容不一致) |
幻读 | 当前事务中多次读取到的数据行数不一致 |
# 1.2.2 隔离级别
隔离级别 | 描述 | 能解决的并发问题 |
---|---|---|
读未提交 | 允许当前事务读取其他事务尚未提交的修改 | 啥问题也解决不了 |
读已提交 | 允许当前事务读取其他事务已经提交的修改 | 脏读 |
可重复读 | 当前事务执行时锁定当前记录,不允许其他事务操作 | 脏读、不可重复读 |
串行化 | 当前事务执行时锁定当前表,不允许其他事务操作 | 脏读、不可重复读、幻读 |
# 2. JDBC事务控制
# 2.1 同一个数据库连接
# 2.2 关闭事务的自动提交
connection.setAutoCommit(false);
Copied!
# 2.3 提交事务
connection.commit();
Copied!
# 2.4 回滚事务
connection.rollBack();
Copied!
# 2.5 事务整体的代码块
try{ // 关闭事务的自动提交 connection.setAutoCommit(false); // 事务中包含的所有数据库操作 // 提交事务 connection.commit(); }catch(Excetion e){ // 回滚事务 connection.rollBack(); }finally{ // 释放数据库连接 connection.close(); }
Copied!
# 3. 将事务对接到书城项目中
# 3.1 三层架构中事务要对接的位置
从逻辑上来说,一个事务对应一个业务方法(Service层的一个方法)。
# 3.2 假想
每一个Service方法内部,都套用了事务操作所需要的try...catch...finally块。
# 3.3 假想代码的缺陷
- 会出现大量的冗余代码:我们希望能够抽取出来,只写一次
- 对核心业务功能是一种干扰:我们希望能够在编写业务逻辑代码时专注于业务本身,而不必为辅助性质的套路代码分心
- 将持久化层对数据库的操作写入业务逻辑层,是对业务逻辑层的一种污染,导致持久化层和业务逻辑层耦合在一起
# 3.4 事务代码抽取
- 只要是Filter拦截到的请求都会从doFilter()方法经过
- chain.doFilter(req, resp);可以包裹住将来要执行的所有方法
- 事务操作的try...catch...finally块只要把chain.doFilter(req, resp)包住,就能够包住将来要执行的所有方法
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { try{ // 关闭事务的自动提交 connection.setAutoCommit(false); // 『事务中包含的所有数据库操作』就在chain.doFilter(req, resp);将来要调用的方法中 // 所以用事务的try...catch...finally块包住chain.doFilter(req, resp); // 就能让所有事务方法都『享受』到事务功能的『服务』。 // 所谓框架其实就是把常用的『套路代码』抽取出来,为大家服务,我们享受框架服务提高开发效率。 chain.doFilter(req, resp); // 提交事务 connection.commit(); }catch(Excetion e){ // 回滚事务 connection.rollBack(); }finally{ // 释放数据库连接 connection.close(); } }
Copied!
# 3.5 在Filter中获取数据库连接
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { try{ // 获取数据库连接 Connection connection = JDBCUtils.getConnection(); // 关闭事务的自动提交 connection.setAutoCommit(false); // 『事务中包含的所有数据库操作』就在chain.doFilter(req, resp);将来要调用的方法中 // 所以用事务的try...catch...finally块包住chain.doFilter(req, resp); // 就能让所有事务方法都『享受』到事务功能的『服务』。 // 所谓框架其实就是把常用的『套路代码』抽取出来,为大家服务,我们享受框架服务提高开发效率。 chain.doFilter(req, resp); // 提交事务 connection.commit(); }catch(Excetion e){ // 回滚事务 connection.rollBack(); }finally{ // 释放数据库连接 connection.close(); } }
Copied!
# 3.6 保证所有数据库操作使用同一个连接
重构JDBCUtils.getConnection()方法实现:所有数据库操作使用同一个连接。
# 3.6.1 从数据源中只拿出一个
为了保证各个需要Connection对象的地方使用的都是同一个对象,我们从数据源中只获取一个Connection。不是说整个项目只用一个Connection,而是说调用JDBCUtils.getConnection()方法时,只使用一个。所以落实到代码上就是:每次调用getConnection()方法时先检查是否已经拿过了,拿过就给旧的,没拿过给新的。
# 3.6.2 公共区域
为了保证各个方法中需要Connection对象时都能拿到同一个对象,需要做到:将唯一的对象存入一个大家都能接触到的地方。
线程本地化技术实现Connection对象从上到下传递。
# 3.7 线程本地化
# 3.7.1 确认同一个线程
在从Filter、Servlet、Service一直到Dao运行的过程中,我们始终都没有做类似new Thread().start()这样开启新线程的操作,所以整个过程在同一个线程中。
# 3.7.2 一条小河
# 3.7.3 一个线程
# 3.7.4 代码
java.lang.ThreadLocal的set()方法:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
Copied!
java.lang.TheadLocal的get()方法:
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
Copied!
所以TheadLocal的基本原理是:它在内部维护了一个Map,需要存入数据时,就以this为键,要存入的数据为值,存入Map。需要取出数据时,就以this为键,从Map中取出数据。
# 3.7.5 结论
如果我们需要将数据在整个项目中按照没法通过方法的参数来实现,这时使用线程本地化技术是一个非常好的选择。
# 3.8 异常向上抛出的线路
上图中标记颜色的位置都是有try...catch块的代码,需要逐个检查一下,catch块捕获的异常是否转换为运行时异常又再次抛出。
如果没有抛出,异常就不会传递到Filter中,TransactionFilter就会认为代码执行过程中没有发生问题,从而提交事务,但是实际上应该回滚。下面是一个例子:
/** * 通用的批量增删改方法 * @param sql * @param params 执行批量操作的二维数组 * 每一条SQL语句的参数是一维数组 * 多条SQL语句的参数就是二维数组 * @return 每一条SQL语句返回的受影响的行数 */ public int[] batchUpdate(String sql, Object[][] params) { Connection connection = JDBCUtils.getConnection(); int[] rowCountArr = null; try { rowCountArr = queryRunner.batch(connection, sql, params); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } finally { JDBCUtils.releaseConnection(connection); } return rowCountArr; }
Copied!
# 4. 代码实现
# 4.1 重构JDBCUtils类
- 要点1:将ThreadLocal对象声明为静态成员变量
- 要点2:重构获取数据库连接的方法
- 要点3:重构释放数据库连接的方法
/** * 功能1:创建数据源对象 * 功能2:获取数据库连接并绑定到当前线程上 * 功能3:释放数据库连接并从当前线程移除 */ public class JDBCUtils { // 将数据源对象设置为静态属性,保证大对象的单一实例 private static DataSource dataSource; // 将ThreadLocal对象设置为静态成员变量,保证以此为键时从Map中取值能够取到同一个值 private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); static { // 1.创建一个用于存储外部属性文件信息的Properties对象 Properties properties = new Properties(); // 2.使用当前类的类加载器加载外部属性文件:jdbc.properties InputStream inputStream = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties"); try { // 3.将外部属性文件jdbc.properties中的数据加载到properties对象中 properties.load(inputStream); // 4.创建数据源对象 dataSource = DruidDataSourceFactory.createDataSource(properties); } catch (Exception e) { e.printStackTrace(); } } /** * 从数据源中获取数据库连接 * @return 数据库连接对象 */ public static Connection getConnection() { // 1.尝试从当前线程获取Connection对象 Connection connection = threadLocal.get(); if (connection == null) { try { // 2.如果从当前线程上没有获取到Connection对象那么从数据源获取 connection = dataSource.getConnection(); // 3.将Connection对象绑定到当前线程 threadLocal.set(connection); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } // 4.返回Connection对象 return connection; } /** * 释放数据库连接 * @param connection 要执行释放操作的连接对象 */ public static void releaseConnection(Connection connection) { if (connection != null) { try { connection.close(); // 将Connection对象从当前线程移除 threadLocal.remove(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } } }
Copied!
# 4.2 重构BaseDao
- 要点:去除释放数据库连接的操作(转移到TransactionFilter中)
/** * 各个具体Dao类的基类,泛型T对应具体实体类类型 * @param <T> */ public class BaseDao<T> { private QueryRunner queryRunner = new QueryRunner(); /** * 通用的批量增删改方法 * @param sql * @param params 执行批量操作的二维数组 * 每一条SQL语句的参数是一维数组 * 多条SQL语句的参数就是二维数组 * @return 每一条SQL语句返回的受影响的行数 */ public int[] batchUpdate(String sql, Object[][] params) { Connection connection = JDBCUtils.getConnection(); int[] rowCountArr = null; try { rowCountArr = queryRunner.batch(connection, sql, params); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); }/* finally { JDBCUtils.releaseConnection(connection); }*/ return rowCountArr; } /** * 通用的增删改方法 * @param sql 要执行的SQL语句 * @param param 为SQL语句准备好的参数 * @return 受影响的行数 */ public int update(String sql, Object ... param) { int updatedRowCount = 0; Connection connection = JDBCUtils.getConnection(); try { updatedRowCount = queryRunner.update(connection, sql, param); } // 为了让上层方法调用方便,将编译时异常捕获 catch (SQLException e) { e.printStackTrace(); // 为了不掩盖问题,将编译时异常封装为运行时异常抛出 throw new RuntimeException(e); }/* finally { // 关闭数据库连接 JDBCUtils.releaseConnection(connection); }*/ return updatedRowCount; } /** * 查询单个对象 * @param clazz 单个对象所对应的实体类类型 * @param sql 查询单个对象所需要的SQL语句 * @param param SQL语句的参数 * @return 查询到的单个对象 */ public T getBean(Class<T> clazz, String sql, Object ... param) { Connection connection = JDBCUtils.getConnection(); T t = null; try { t = queryRunner.query(connection, sql, new BeanHandler<>(clazz), param); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); }/* finally { // 关闭数据库连接 JDBCUtils.releaseConnection(connection); }*/ return t; } /** * 查询集合对象 * @param clazz 集合中单个对象所对应的实体类类型 * @param sql 查询集合所需要的SQL语句 * @param param SQL语句的参数 * @return 查询到的集合对象 */ public List<T> getBeanList(Class<T> clazz, String sql, Object ... param) { Connection connection = JDBCUtils.getConnection(); List<T> list = null; try { list = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), param); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); }/* finally { // 关闭数据库连接 JDBCUtils.releaseConnection(connection); }*/ return list; } }
Copied!
注意:OrderDao中insertOrder()方法也要去掉关闭数据库连接的操作。
@Override public void insertOrder(Order order) { // ※DBUtils没有封装获取自增主键的方法,需要我们使用原生的JDBC来完成 // 1.获取数据库连接 Connection connection = JDBCUtils.getConnection(); // 2.创建PreparedStatement对象 String sql = "INS222ERT INTO t_order(order_sequence,create_time,total_count,total_amount,order_status,user_id) VALUES(?,?,?,?,?,?)"; try { // ①创建PreparedStatement对象,指明需要自增的主键 PreparedStatement preparedStatement = connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS); // ②给PreparedStatement对象设置SQL语句的参数 preparedStatement.setString(1, order.getOrderSequence()); preparedStatement.setString(2, order.getCreateTime()); preparedStatement.setInt(3, order.getTotalCount()); preparedStatement.setDouble(4, order.getTotalAmount()); preparedStatement.setInt(5, order.getOrderStatus()); preparedStatement.setInt(6, order.getUserId()); // ③执行更新 preparedStatement.executeUpdate(); // ④获取封装了自增主键的结果集 ResultSet generatedKeysResultSet = preparedStatement.getGeneratedKeys(); // ⑤解析结果集 if (generatedKeysResultSet.next()) { int orderId = generatedKeysResultSet.getInt(1); order.setOrderId(orderId); } } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } /*finally { JDBCUtils.releaseConnection(connection); }*/ }
Copied!
# 4.3 创建一个用于显示通用错误信息的页面
# 4.3.1 创建页面
这个页面可以从login_success.html复制过来
# 4.3.2 创建Servlet跳转到页面
protected void showSystemError(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String viewName = "error"; processTemplate(viewName, request, response); }
Copied!
# 4.4 创建TransactionFilter
<filter> <filter-name>TransactionFilter</filter-name> <filter-class>com.atguigu.bookstore.filter.TransactionFilter</filter-class> </filter> <filter-mapping> <filter-name>TransactionFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Copied!
Java代码如下:
public class TransactionFilter implements Filter { private static final Set<String> PUBLIC_STATIC_RESOURCE_EXT_NAME_SET = new HashSet<>(); static { PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".png"); PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".css"); PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".js"); PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".jpg"); PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.add(".gif"); } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException { // 排除掉静态资源,它们和数据库操作没有关系 // 1.给请求和响应对象转换类型 HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 2.获取当前请求的ServletPath String servletPath = request.getServletPath(); // 3.检查servletPath中是否包含“.” if (servletPath.contains(".")) { int index = servletPath.lastIndexOf("."); String extensionName = servletPath.substring(index); if (PUBLIC_STATIC_RESOURCE_EXT_NAME_SET.contains(extensionName)) { chain.doFilter(request, response); return ; } } // 执行事务操作 // 1.获取数据库连接 Connection connection = JDBCUtils.getConnection(); // 2.使用try...catch...finally块管理事务 try{ // 3.关闭事务的自动提交 connection.setAutoCommit(false); // 4.尝试执行目标代码 chain.doFilter(request, response); // 5.如果上面的操作没有抛出异常 connection.commit(); }catch (Exception e){ // 6.如果上面的操作抛出了异常 try { connection.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } // 7.捕获到异常后,跳转到专门的页面显示提示消息 String message = e.getMessage(); request.setAttribute("error", message); request.getRequestDispatcher("/ErrorServlet?method=showSystemError").forward(request, response); }finally { // 8.不管前面操作是成功还是失败,到这里都要释放数据库连接 JDBCUtils.releaseConnection(connection); } } public void init(FilterConfig config) throws ServletException {} public void destroy() {} }
Copied!