JavaWeb 扩展4

254 阅读11分钟

书城项目第六、七阶段

项目第六阶段:购物车

购物车模块分析:

项目中常用的购物车实现技术:

  • Session 版本(将购物车信息存放到 Session 域中)
  • 数据库版本(将购物车信息存放到数据库中)
  • redis + sql + Cookie(使用 Cookie + redis 缓存,和数据库 )

购物车的功能:

  • 加入购物车 addItem
  • 删除商品项 deleteItem
  • 清空购物车 clear
  • 修改商品数量 updateCount

购物车模块实现:

加入购物车 addItem

  • CartServlet.java
  • req.getHeader("Referer") 获取请求发起时的浏览器地址
  • location.href = "http..."; 浏览器发起请求
    private BookService bs = new BookServiceImpl();

    /**
     * 加入购物车
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    protected void addItem (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // super.doPost(req, resp);
        /**
         * 1.获取请求参数,商品编号
         * 2.调用 bs.queryBookById(id)获取图书信息
         * 3.将图书信息转换为 cartItem商品项
         * 4.调用 cart.addItem(); 添加商品项
         * 5.重定向回商品列表页面
         */
        int id = WebUtils.parseInt(req.getParameter("id"), 0);
        Book book = bs.queryBookById(id);

        CartItem cartItem = new CartItem(book.getId(), book.getName(), 1, book.getPrice(), book.getPrice());

        Cart cart = (Cart) req.getSession().getAttribute("cart");
        if (null == cart) {
            cart = new Cart();
        }
        cart.addItem(cartItem);
        req.getSession().setAttribute("cart", cart);
        System.out.println(cart);
        System.out.println("Referer " + req.getHeader("Referer"));
        // req.getContextPath(): 项目根目录
        // resp.sendRedirect(req.getContextPath() + "/pages/client/index.jsp");

        /**
         * 在 http协议中有一个请求头,叫 Referer,
         * 它保存了请求发起时的浏览器地址,
         * 可以使用它作为 重定向的地址
          */
        resp.sendRedirect(req.getHeader("Referer"));
    }
// index.jsp
<div class="book_add">
   <button class="add_cart" bookId="${item.id}">加入购物车</button>
</div>

<script type="text/javascript">
   $(function () {
      $("button.add_cart").click(function () {
          var bookId = $(this).attr("bookId");
         location.href = "${pageContext.getAttribute("basePath")}" + "cartServlet?action=addItem&id=" + bookId;
           })
       })
</script>>

删除商品项 deleteItem

  • 使用 sessionScope 获取保存在 Session 中的数据
  • Map 在 jsp页面中的遍历方法 <c:forEach var="entry" items="${sessionScope.cart.items}">
  • 空值的判断 <c:if test="${not empty sessionScope.cart.items}">
// CartServlet.java
/**
 * 删除购物车指定商品
 * @param req
 * @param resp
 * @throws ServletException
 * @throws IOException
 */
protected void deleteItem (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // super.doPost(req, resp);
    int id = WebUtils.parseInt(req.getParameter("id"), 0);
    Cart cart = (Cart) req.getSession().getAttribute("cart");

    if (null != cart) {
        cart.deleteItem(id);
    }
    req.getSession().setAttribute("cart", cart);
    resp.sendRedirect(req.getHeader("Referer"));
}
// index.jsp
<div id="main">

   <table>
      <tr>
         <td>商品名称</td>
         <td>数量</td>
         <td>单价</td>
         <td>金额</td>
         <td>操作</td>
      </tr>
      <c:choose>
         <c:when test="${empty sessionScope.cart.items}">
            <tr>
               <td colspan="5">亲,当前购物车为空!<a href="index.jsp">返回首页</a> </td>
            </tr>
         </c:when>
         <c:otherwise>
            <c:forEach var="entry" items="${sessionScope.cart.items}">
               <tr>
                  <td>${entry.value.name}</td>
                  <td>${entry.value.count}</td>
                  <td>${entry.value.price}</td>
                  <td>${entry.value.totalPrice}</td>
                  <td><a class="del_item" href="cartServlet?action=deleteItem&id=${entry.value.id}">删除</a></td>
               </tr>
            </c:forEach>
         </c:otherwise>
      </c:choose>

   </table>
   <c:if test="${not empty sessionScope.cart.items}">
      <div class="cart_info">
         <span class="cart_span">购物车中共有<span class="b_count">${sessionScope.cart.totalCount}</span>件商品</span>
         <span class="cart_span">总金额<span class="b_price">${sessionScope.cart.totalPrice}</span>元</span>
         <span class="cart_span"><a href="#">清空购物车</a></span>
         <span class="cart_span"><a href="pages/cart/checkout.jsp">去结账</a></span>
      </div>
   </c:if>
</div>

<script type="text/javascript">
   $(function () {
       $("a.del_item").click(function () {
               return confirm("你确认删除《 " + $(this).parent().parent().find("td:first").text() + " 》图书吗?");
           })
       })
</script>

清空购物车 clear

<c:if test="${not empty sessionScope.cart.items}">
   <div class="cart_info">
      <span class="cart_span">购物车中共有<span class="b_count">${sessionScope.cart.totalCount}</span>件商品</span>
      <span class="cart_span">总金额<span class="b_price">${sessionScope.cart.totalPrice}</span>元</span>
      // clear
      <span class="cart_span"><a class="clear_cart" href="cartServlet?action=clear">清空购物车</a></span>
      
      <span class="cart_span"><a href="pages/cart/checkout.jsp">去结账</a></span>
   </div>
</c:if>

// ..............................

<script type="text/javascript">
   $(function () {

       // 清空购物车
       $("a.clear_cart").click(function () {
               return confirm("你确认要清空购物车吗?");
           });

</script>
protected void clear (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // super.doPost(req, resp);
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    if (null != cart) {
        cart.clear();
    }
    req.getSession().setAttribute("cart", cart);
    resp.sendRedirect(req.getHeader("Referer"));
}

修改商品数量 updateCount

  • DOM 的 defaultValue属性
  • attr() 方法,获取自定义属性
<c:otherwise>
   <c:forEach var="entry" items="${sessionScope.cart.items}">
      <tr>
         <td>${entry.value.name}</td>
         <td>
                            <input oldCount="${entry.value.count}" bookId="${entry.value.id}" style="width: 60px;" type="text" class="cart_count" value="${entry.value.count}" />
                        </td>
         <td>${entry.value.price}</td>
         <td>${entry.value.totalPrice}</td>
         <td><a class="del_item" href="cartServlet?action=deleteItem&id=${entry.value.id}">删除</a></td>
      </tr>
   </c:forEach>
</c:otherwise>

// ..............................

<script type="text/javascript">
   $(function () {

       // 改变数量:change 改变数据并失去焦点后触发此事件
       $("input.cart_count").change(function () {

               var bookName = $(this).parent().parent().find("td:first").text();

               // var count = $(this).val();
               var count = this.value;
               var id = $(this).attr("bookId");
               // var oldCount = $(this).attr("oldCount");
               var isAdd = confirm("你确定将《 " + bookName + " 》商品数量修改为:" + count + " 吗?");
               if (isAdd) {
                   // updateCount (Integer id, Integer count)
                   location.href = "${pageContext.getAttribute("basePath")}" + "cartServlet?action=updateCount&id=" + id + "&count=" + count;
               } else {
                   // $(this).val(oldCount);
                   // this.value = oldCount;
                   // defaultValue 表示 DOM 默认的 value 属性值
                   this.value = this.defaultValue;
               }
           })
       })
</script>
protected void updateCount (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // super.doPost(req, resp);
    int id = WebUtils.parseInt(req.getParameter("id"), 0);
    int count = WebUtils.parseInt(req.getParameter("count"), 0);
    Cart cart = (Cart) req.getSession().getAttribute("cart");
    if (null != cart) {
        cart.updateCount(id, count);
    }
    req.getSession().setAttribute("cart", cart);
    resp.sendRedirect(req.getHeader("Referer"));
}

首页,购物车信息回显

  • 在 Session 中存放最后一次添加的图书名称
  • req.getSession().setAttribute("lastName", cartItem.getName());
/**
 * 加入购物车
 * @param req
 * @param resp
 * @throws ServletException
 * @throws IOException
 */
protected void addItem (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    // super.doPost(req, resp);
    /**
     * 1.获取请求参数,商品编号
     * 2.调用 bs.queryBookById(id)获取图书信息
     * 3.将图书信息转换为 cartItem商品项
     * 4.调用 cart.addItem(); 添加商品项
     * 5.重定向回商品列表页面
     */
    int id = WebUtils.parseInt(req.getParameter("id"), 0);
    Book book = bs.queryBookById(id);

    CartItem cartItem = new CartItem(book.getId(), book.getName(), 1, book.getPrice(), book.getPrice());

    Cart cart = (Cart) req.getSession().getAttribute("cart");
    if (null == cart) {
        cart = new Cart();
    }
    cart.addItem(cartItem);
    req.getSession().setAttribute("cart", cart);
    // System.out.println(cart);
    // System.out.println("Referer " + req.getHeader("Referer"));
    // req.getContextPath(): 项目根目录
    // resp.sendRedirect(req.getContextPath() + "/pages/client/index.jsp");

    /**
     * 在 http协议中有一个请求头,叫 Referer,
     * 它保存了请求发起时的浏览器地址,
     * 可以使用它作为 重定向的地址
      */
    req.getSession().setAttribute("lastName", cartItem.getName());
    
    resp.sendRedirect(req.getHeader("Referer"));
}
<div style="text-align: center">
   <c:choose>
      <c:when test="${not empty sessionScope.cart.items}">
         <span>您的购物车中有 ${sessionScope.cart.totalCount} 件商品</span>
         <div>
            您刚刚将<span class="book_cart" style="color: red">《 ${sessionScope.lastName} 》</span>加入到了购物车中
         </div>
      </c:when>
      <c:otherwise>
         <div class="book_cart">当前购物车暂无商品...</div>
      </c:otherwise>
   </c:choose>
</div>

项目第六阶段:订单

订单功能分析:

OrderServlet 程序:

  • 生成订单:createOrder()

  • 查询所有订单(管理员):showAllOrders()

  • 发货(管理员):sendOrder()

  • 查看订单详情(管理员、用户):showOrderDetail()

  • 查看我的订单(用户):showMyOrders()

  • 签收订单(用户):receiveOrder()

OrderService 程序:

  • 生成订单:createOrder(Cart, userId)

  • 查询所有订单(管理员):showAllOrders()

  • 发货(管理员):sendOrder(orderId)

  • 查看订单详情(管理员、用户):showOrderDetail(orderId)

  • 查看我的订单(用户):showMyOrders(userId)

  • 签收订单(用户):receiveOrder(orderId)

创建表单数据库:

USE book_project;

CREATE TABLE t_order(
	order_id VARCHAR(50) PRIMARY KEY,
	create_time datetime,
	price DECIMAL(11, 2),
	`status` INT,
	user_id INT,
	FOREIGN KEY(`user_id`) REFERENCES t_user(`id`)
);

CREATE TABLE t_order_item(
	`id` INT PRIMARY KEY auto_increment,
	`name` VARCHAR(100),
	`count` INT,
	price DECIMAL(11,2),
	total_price DECIMAL(11,2),
	order_id VARCHAR(50),
	FOREIGN KEY(order_id) REFERENCES t_order(order_id)
);

编写订单模块的 Dao 程序

  • OrderDao.java
package com.atjava.dao;

import com.atjava.pojo.Order;

/**
 * @author lv
 * @create 2021-10-21 22:26
 */
public interface OrderDao {
    public int saveOrder (Order order);
}
  • OrderDaoImpl.java
package com.atjava.dao.impl;

import com.atjava.dao.BaseDao;
import com.atjava.dao.OrderDao;
import com.atjava.pojo.Order;

/**
 * @author lv
 * @create 2021-10-21 22:31
 */
public class OrderDaoImpl extends BaseDao implements OrderDao {
    @Override
    public int saveOrder(Order order) {
        String sql = "insert into t_order(order_id, create_time, price, status, user_id) values(?,?,?,?,?)";
        int updates = updates(sql, order.getOrderId(), order.getCreateTime(), order.getPrice(), order.getStatus(), order.getUserId());

        return updates;
    }
}
  • OrderItemDao.java
package com.atjava.dao;

import com.atjava.pojo.OrderItem;

/**
 * @author lv
 * @create 2021-10-21 22:29
 */
public interface OrderItemDao {
    public int saveOrderItem (OrderItem orderItem);
}
  • OrderItemDaoImpl.java
package com.atjava.dao.impl;

import com.atjava.dao.BaseDao;
import com.atjava.dao.OrderItemDao;
import com.atjava.pojo.OrderItem;

/**
 * @author lv
 * @create 2021-10-21 22:32
 */
public class OrderItemDaoImpl extends BaseDao implements OrderItemDao {
    @Override
    public int saveOrderItem(OrderItem orderItem) {
        String sql = "insert into t_order_item(name, count, price, total_price, order_id) values(?,?,?,?,?)";

        return updates(sql, orderItem.getName(), orderItem.getCount(), orderItem.getPrice(), orderItem.getTotalPrice(), orderItem.getOrderId());
    }
}

编写订单模块的 Service 和测试

  • OrderService.java
package com.atjava.service;

import com.atjava.pojo.Cart;

/**
 * @author lv
 * @create 2021-10-22 20:53
 */
public interface OrderService {
    public String createOrder(Cart cart, Integer userId);
}
  • OrderServiceImpl.java
package com.atjava.service.impl;

import com.atjava.dao.OrderDao;
import com.atjava.dao.OrderItemDao;
import com.atjava.dao.impl.OrderDaoImpl;
import com.atjava.dao.impl.OrderItemDaoImpl;
import com.atjava.pojo.*;
import com.atjava.service.OrderService;

import java.util.Date;
import java.util.Map;

/**
 * @author lv
 * @create 2021-10-22 20:56
 */
public class OrderServiceImpl implements OrderService {

    private OrderDao od = new OrderDaoImpl();
    private OrderItemDao oid = new OrderItemDaoImpl();

    @Override
    public String createOrder(Cart cart, Integer userId) {
        /**
         * 1.先保存订单
         * 2.再保存订单项
         */
        // 订单唯一:时间戳 + userId
        String orderId = System.currentTimeMillis() + "" + userId;
        Order order = new Order(orderId, new Date(), cart.getTotalPrice(), 0, userId);
        od.saveOrder(order);

        for (Map.Entry<Integer, CartItem> entry : cart.getItems().entrySet()) {
            /**
             * 1.获取购物车中的商品项并转化为订单项
             * 2.将订单项保存到数据库中
             */
            CartItem cartItem = entry.getValue();
            oid.saveOrderItem(new OrderItem(null, cartItem.getName(), cartItem.getCount(), cartItem.getPrice(), cartItem.getTotalPrice(), orderId));

        }

        /**
         * 结账后清空购物车
         */
        cart.clear();
        return orderId;
    }
}

web层,编写 OrderServlet 程序

  • OrderServlet.java
package com.atjava.web;

import com.atjava.pojo.Cart;
import com.atjava.pojo.User;
import com.atjava.service.OrderService;
import com.atjava.service.impl.OrderServiceImpl;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author lv
 * @create 2021-10-22 22:05
 */
public class OrderServlet extends BaseServlet {

    OrderService os = new OrderServiceImpl();

    /**
     * 生成订单
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    protected void createOrder (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // super.doPost(req, resp);
        /**
         * 1.获取 Session中的购物车
         * 2.调用 OrderServletImpl 生成订单
         * 3.获取订单号保存在 域中并跳转到订单页面
         */
        Cart cart = (Cart) req.getSession().getAttribute("cart");
        User currentUser = (User) req.getSession().getAttribute("currentUser");
        if (null == currentUser) {
            req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
            // 停止执行下边的代码
            return;
        }
        Integer userId = currentUser.getId();
        String osOrder = os.createOrder(cart, userId);

        // 订单号保存到 Session 域中
        req.getSession().setAttribute("orderId", osOrder);
//        req.setAttribute("orderId", osOrder);

        resp.sendRedirect(req.getContextPath() + "/pages/cart/checkout.jsp");

    }
}
  • cart.jsp
<div class="cart_info">
   <span class="cart_span">购物车中共有<span class="b_count">${sessionScope.cart.totalCount}</span>件商品</span>
   <span class="cart_span">总金额<span class="b_price">${sessionScope.cart.totalPrice}</span>元</span>
   <span class="cart_span"><a class="clear_cart" href="cartServlet?action=clear">清空购物车</a></span>
   <span class="cart_span"><a href="orderServlet?action=createOrder">去结账</a></span>
</div>

Filter 过滤器

Filter 过滤器介绍

  • Filter 过滤器它是 JavaWeb的三大组件之一;三大组件是:Servlet 程序、Listener 监听器、Filter 过滤器
  • Filter 过滤器是 JavaEE的规范,也就是接口
  • Filter 过滤器的作用:拦截请求、过滤响应

拦截请求 - 常见应用场景:

  1. 权限检查
  2. 日记操作
  3. 事务管理
  4. ... 等等

Filter 过滤器的初体验

要求:在项目 web工程下,有一个 admin目录,此目录下的所有资源(html、jpg、jsp 等)都必须是用户登录后才能访问。

思考:根据之前所学内容,我们知道,用户登录后一般会将用户登录信息保存到 Session 域中;所以检查用户是否登录,可以判断 Session 域中是否包含用户的登录信息。

  • AdminFilter.java
package com.atjava.filter;


import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author lv
 * @create 2021-10-26 22:40
 */
public class AdminFilter implements Filter {


    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    /**
     * 此方法很重要,专门用于拦截请求。
     * 1.可以做权限检查
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest hsr = (HttpServletRequest) request;
        HttpSession session = hsr.getSession();
        Object user = session.getAttribute("user");
        if (null == user) {
            request.getRequestDispatcher("/login.jsp").forward(request, response);
            return;
        } else {
            // 让程序继续访问用户的目标资源
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {

    }
}
  • web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--filter标签,用于配置一个 filter过滤器-->
    <filter>
        <!--给 filter起一个别名-->
        <filter-name>AdminFilter</filter-name>
        <!--配置 filter的全类名-->
        <filter-class>com.atjava.filter.AdminFilter</filter-class>
    </filter>
    <!--filter-mapping 配置 filter过滤器的拦截路径-->
    <filter-mapping>
        <!--filter-name 表示当前的拦截路径给那个 filter使用-->
        <filter-name>AdminFilter</filter-name>
        <!--
            url-pattern 配置拦截路径
            "/" 表示请求地址为 http://ip:port/工程路径/ 映射到 IDEA 的 web目录
            "*" 表示全部资源
        -->
        <url-pattern>/admin/*</url-pattern>
    </filter-mapping>
</web-app>
  • admin
  1. a.jsp
  2. a.html
<%--
  Created by IntelliJ IDEA.
  User: Weili
  Date: 2021/10/26
  Time: 22:17
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>JSP</title>
</head>
<body>
    <%
        /**
         * 权限控制:
         * 方式一:
         */
        Object user = session.getAttribute("user");
        /**
         * 如果 user 为 null,跳转到登录页面
         */
//        if (null == user) {
//            request.getRequestDispatcher("/login.jsp").forward(request, response);
//            return;
//        }

        // 方式二:filter 过滤器控制
    %>
    我是 a.jsp页面
</body>
</html>

Filter 的生命周期

Filter 的生命周期包含的几个方法

  1. 构造器方法
  2. init 初始化方法
    • 第 1、2 步,在 web工程启动时执行(已创建 Filter 过滤器)
  3. doFilter 过滤方法
    • 每次拦截到请求,就会执行
  4. destory 销毁
    • 停止 web 工程时,就会执行(并销毁 Filter 过滤器)
package com.atjava.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author lv
 * @create 2021-10-26 22:40
 */
public class AdminFilter implements Filter {

    public AdminFilter () {
        System.out.println("1.AdminFilter构造器方法");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("2.init初始化方法");
    }

    /**
     * 此方法很重要,专门用于拦截请求。
     * 1.可以做权限检查
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("3.doFilter过滤器方法");

        HttpServletRequest hsr = (HttpServletRequest) request;
        HttpSession session = hsr.getSession();
        Object user = session.getAttribute("user");
        if (null == user) {
            request.getRequestDispatcher("/login.jsp").forward(request, response);
            return;
        } else {
            // 让程序继续访问用户的目标资源
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
        System.out.println("4.destroy销毁方法");
    }
}

FilterConfig 类

FilterConfig 类见名知意,它是 Filter 过滤器的配置文件类。

Tomcat 每次创建 Filter 时,也会同时创建一个 FilterConfig 类,这个包含了 Filter 配置文件的配置信息。

FilterConfig 类的作用是获取 filter 过滤器的配置内容

  1. 获取 Filter 的名称 filter-name 的内容
  2. 获取在 web.xml中配置的 Filter 的 init-param 初始化参数
  3. 获取 ServletContext 对象
  • AdminFilter.java
package com.atjava.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author lv
 * @create 2021-10-26 22:40
 */
public class AdminFilter implements Filter {

    public AdminFilter () {
        System.out.println("1.AdminFilter构造器方法");
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("2.init初始化方法");

        // 1. 获取 Filter 的名称 filter-name 的内容
        System.out.println("filter-name:" + filterConfig.getFilterName());
        // 2. 获取在 Filter 中配置的 init-param 初始化参数
        System.out.println("init-param:username - " + filterConfig.getInitParameter("username"));
        // 3. 获取 ServletContext 对象
        System.out.println("ServletContext:" + filterConfig.getServletContext());
    }

    /**
     * 此方法很重要,专门用于拦截请求。
     * 1.可以做权限检查
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("3.doFilter过滤器方法");

        HttpServletRequest hsr = (HttpServletRequest) request;
        HttpSession session = hsr.getSession();
        Object user = session.getAttribute("user");
        if (null == user) {
            request.getRequestDispatcher("/login.jsp").forward(request, response);
            return;
        } else {
            // 让程序继续访问用户的目标资源
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {
        System.out.println("4.destroy销毁方法");
    }
}
  • web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--filter标签,用于配置一个 filter过滤器-->
    <filter>
        <!--给 filter起一个别名-->
        <filter-name>AdminFilter</filter-name>
        <!--配置 filter的全类名-->
        <filter-class>com.atjava.filter.AdminFilter</filter-class>

        <!-- filter 的初始化参数-->
        <init-param>
            <param-name>username</param-name>
            <param-value>root</param-value>
        </init-param>
    </filter>
    <!--filter-mapping 配置 filter过滤器的拦截路径-->
    <filter-mapping>
        <!--filter-name 表示当前的拦截路径给那个 filter使用-->
        <filter-name>AdminFilter</filter-name>
        <!--
            url-pattern 配置拦截路径
            "/" 表示请求地址为 http://ip:port/工程路径/ 映射到 IDEA 的 web目录
            "*" 表示全部资源
        -->
        <url-pattern>/admin/*</url-pattern>
    </filter-mapping>
    
    <!--... ...-->
</web-app>

FilterChain 过滤器链

  • 多个过滤器如何一起工作

FilterChain 的作用:

  • 执行下一个 Filter 过滤器(如果有)
  • 执行目标资源(没有 Filter 或 有关的 Filter 都执行完毕后)

多个 FilterChain 的执行顺序:

  • 对于同一个 目标资源 或 文件夹 的过滤,在 web.xml 中的 Filter的书写顺序,就是程序执行过滤的顺序

  • web.xml

<filter>
    <filter-name>Filter1</filter-name>
    <filter-class>com.atjava.filter.Filter1</filter-class>
</filter>
<filter-mapping>
    <filter-name>Filter1</filter-name>
    <url-pattern>/target.jsp</url-pattern>
</filter-mapping>
<filter>
    <filter-name>Filter2</filter-name>
    <filter-class>com.atjava.filter.Filter2</filter-class>
</filter>
<filter-mapping>
    <filter-name>Filter2</filter-name>
    <url-pattern>/target.jsp</url-pattern>
</filter-mapping>

多个 Filter 过滤器执行的特点:

  • 所有 Filter 和 目标资源 默认都执行在同一个线程中
  • 多个 Filter 和 目标资源 链式执行的时候,它们都使用同一个 Request 对象(request中的数据是共享的)
// Filter1 前置代码
// 线程1:http-apr-8080-exec-9
// Filter2 前置代码
// 线程2:http-apr-8080-exec-9
// target.jsp 页面执行了
// filter1:filter1
// Filter2 后置代码
// Filter1 后置代码
  • target.jsp
<%--
  Created by IntelliJ IDEA.
  User: Weili
  Date: 2021/11/1
  Time: 21:33
  To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Target</title>
</head>
<body>
    <%
        System.out.println("target.jsp 页面执行了");
        System.out.println("filter1:" + request.getAttribute("filter1"));
    %>
</body>
</html>
  • Filter1.java
package com.atjava.filter;

import javax.servlet.*;
import java.io.IOException;

/**
 * @author lv
 * @create 2021-11-01 21:25
 */
public class Filter1 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        System.out.println("Filter1 前置代码");
        System.out.println("线程1:" + Thread.currentThread().getName());
        chain.doFilter(request, response);
        System.out.println("Filter1 后置代码");

    }

    @Override
    public void destroy() {

    }
}

Filter 的拦截路径

Filter 过滤器,它只关心请求地址是否匹配,不关心请求的资源是否存在

精确匹配

  • <url-pattern>/target.jsp</url-pattern>
  • 以上配置的路径,表示请求地址必须为:http://ip:port/工程路径/target.jsp

目录匹配

  • <url-pattern>/admin/*</url-pattern>
  • 以上配置的路径,表示请求地址必须为:http://ip:port/工程路径/admin/*

后缀名匹配

  • <url-pattern>*.html</url-pattern>
  • 以上配置的路径,表示请求地址必须以 .html 为结尾的资源,才会进行过滤
  • 后缀名可以自己定义:*.abc,*.xxx 等
  • 注意:后缀名匹配,路径不能以 斜杠 "/" 开头

书城第八阶段

使用 Filter 过滤器拦截 /pages/manager/ 内的所有内容,实现权限检查

  • ManagerFilter.java
package com.atjava.filter;

import com.atjava.pojo.User;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * @author lv
 * @create 2021-11-02 21:16
 */
public class ManagerFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 类型转换
        HttpServletRequest hsr = (HttpServletRequest) request;
        HttpServletResponse hsrr = (HttpServletResponse) response;
        HttpSession session = hsr.getSession();
        User user = (User) session.getAttribute("currentUser");
        if (null == user) {
            hsrr.sendRedirect(hsr.getContextPath() + "/pages/user/login.jsp");
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void destroy() {

    }
}
  • web.xml
<filter>
    <filter-name>ManagerFilter</filter-name>
    <filter-class>com.atjava.filter.ManagerFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>ManagerFilter</filter-name>
    <url-pattern>/pages/manager/*</url-pattern>
    <url-pattern>/manager/bookServlet</url-pattern>
</filter-mapping>

ThreadLocal 的使用

在JDK 中,用于解决数据安全问题的工具类。

  • ThreadLocal 的作用,它可以解决多线程的数据安全问题
  • ThreadLocal 可以给当前线程关联一个数据(可以是 普通变量、数组、对象、集合)

ThreadLocal 的特点:

  1. ThreadLocal 可以为当前线程关联一个数据(可以像 Map 一样存取数据,key 为当前线程)
  2. 每个 ThreadLocal 对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个 ThreadLocal 对象实例。
  3. 每个 ThreadLocal 对象实例定义时,一般都是 static 类型
  4. ThreadLocal 中保存的数据,在线程销毁后,会由 JVM 虚拟机自动释放
  • ThreadLocal.java
package com.atjava.threadlocal;

import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author lv
 * @create 2021-11-02 22:21
 */
public class ThreadLocal {

    // ConcurrentHashMap 线程安全的 Map,在高并发下使用
    public static Map<String,Object> data = new ConcurrentHashMap<>();
    private static Random rd = new Random();
    public static java.lang.ThreadLocal<Object> threadLocal = new java.lang.ThreadLocal<>();

    public static class Task implements Runnable {

        @Override
        public void run() {
            /**
             * 1.在 Run 方法中,随机生成一个变量(线程要关联的数据),
             * 然后 以当前线程名为 key 保存到 Map 中
             *
             * 2.在 Run 方法结束之前,以当前线程名获取数据并打印,
             * 查看是否可以取出操作
             */

            // 获取随机数
            Integer i = rd.nextInt(1000);
            // 获取当前线程名
            String name = Thread.currentThread().getName();
            System.out.println("线程 " + name + " 的随机数:" + i);
            data.put(name, i);
            // 使用 java.lang.ThreadLocal 保存当前变量
            threadLocal.set(i);

            try {
                // 等待 5 秒钟
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            new OrderService().createOrder();

            Object o = data.get(name);
            Object o1 = threadLocal.get();
            System.out.println("在本线程 " + name + " 销毁前取出的随机数:" + o.toString() + " - " + o1.toString());
        }
    }
    
    public static void main (String[] args) {

        for (int i = 0; i < 3; ++i) {
            new Thread(new Task()).start();
        }
    }

}
  • OrderService.java
package com.atjava.threadlocal;

/**
 * @author lv
 * @create 2021-11-02 22:51
 */
public class OrderService {

    public void createOrder () {
        String name = Thread.currentThread().getName();

        System.out.println("OrderService 当前线程 " + name + " 中保存的数据是:" + ThreadLocal.data.get(name) + " - " + ThreadLocal.threadLocal.get());
        new OrderDao().saveOrder();
    }
}
  • OrderDao.java
package com.atjava.threadlocal;

/**
 * @author lv
 * @create 2021-11-02 22:58
 */
public class OrderDao {

    public void saveOrder () {
        String name = Thread.currentThread().getName();
        System.out.println("OrderDao 当前线程 " + name + " 中保存的数据是:" + ThreadLocal.data.get(name) + " - " + ThreadLocal.threadLocal.get());
    }
}
  • 程序执行的结果
线程 Thread-0 的随机数:121
线程 Thread-1 的随机数:414
线程 Thread-2 的随机数:709
OrderService 当前线程 Thread-1 中保存的数据是:414 - 414
OrderService 当前线程 Thread-0 中保存的数据是:121 - 121
OrderService 当前线程 Thread-2 中保存的数据是:709 - 709
OrderDao 当前线程 Thread-0 中保存的数据是:121 - 121
OrderDao 当前线程 Thread-2 中保存的数据是:709 - 709
OrderDao 当前线程 Thread-1 中保存的数据是:414 - 414
在本线程 Thread-2 销毁前取出的随机数:709 - 709
在本线程 Thread-0 销毁前取出的随机数:121 - 121
在本线程 Thread-1 销毁前取出的随机数:414 - 414

使用 Filter 和 ThreadLocal 组合管理事务

  • 问题:谁能确保所有操作都使用同一个 Connection 连接对象? - ThreadLocal对象可以
  • 前提条件:ThreadLocal 要确保所有操作都使用同一个 Connection 连接对象的前提条件是,所有操作都必须在同一个线程中完成!!!

回顾 jdbc 的数据库事务管理

  • 要确保所有操作 要么都成功、要么都失败,就必须要使用数据库的事务
  • 要确保所有操作都在一个事务内,就必须确保,所有操作都使用同一个 Connection 连接对象
// 回顾 jdbc 的数据库事务管理
Connection conn = JdbcUtils.getConnection();
try {
    conn.setAutoCommit(false); // 设置为手动管理事务
    // 执行一系列的 jdbc 操作
    conn.commit(); // 手动提交事务
} catch (Exception err) {
    conn.rollback(); // 回滚事务
} finally {
    JdbcUtils.close(conn);
}

使用 Filter 和 ThreadLocal 组合管理事务

  • JDBCUtils.java
    • 添加 ThreadLocal 对象 conns
package com.atjava.utils;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

/**
 * @author lv
 * @create 2021-08-02 22:33
 */
public class JDBCUtils {

    private static DruidDataSource dataSource;
    private static ThreadLocal<Connection> conns = new ThreadLocal<>();

    static {
        try {
            Properties properties = new Properties();
            // 读取 jabc.properties属性配置文件
            InputStream inputStream = JDBCUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
            // 加载流数据
            properties.load(inputStream);
            // 创建 数据库连接池
            dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取数据库连接池中的连接
     * @return
     */
    public static Connection getConnection () {
        Connection conn = conns.get();
        if (null == conn) {
            try {
                // 从数据库连接池中获取连接
                conn = dataSource.getConnection();

                // 设置为手动管理事务
                conn.setAutoCommit(false);

                // 将数据源中的连接保存到 ThreadLocal 中,供后面的 jdbc 操作使用
                conns.set(conn);

            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        return conn;
    }

    /**
     * 提交事务,并关闭释放连接
     */
    public static void commitAndClose () {
        Connection connect = conns.get();
        if (null != connect) {
            try {
                connect.commit(); // 提交事务
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                try {
                    connect.close(); // 关闭连接
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        // 一定要执行 remove操作,否则会出错(因为 Tomcat服务器底层使用了线程池技术)
        conns.remove();
    }

    /**
     * 回滚事务,并关闭释放连接
     */
    public static void rollbackAndClose () {
        Connection connect = conns.get();
        if (null != connect) {
            try {
                connect.rollback(); // 回滚事务
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                try {
                    connect.close();
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        // 一定要执行 remove操作,否则会出错(因为 Tomcat服务器底层使用了线程池技术)
        conns.remove();
    }
}
  • BaseDao.java
    • 去除自动关闭 连接
    • 抛出所有异常,供使用者判断是 提交或回滚
package com.atjava.dao;

import com.atjava.utils.JDBCUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;
import org.apache.commons.dbutils.handlers.ScalarHandler;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

/**
 * @author lv
 * @create 2021-08-04 21:56
 */
public abstract class BaseDao {

    // 使用 DbUtils操作数据
    private QueryRunner qr = new QueryRunner();

    /**
     * 用来操作 insert、update、delete方法
     * @return
     */
//    public int updates (String sql, Object ...args) {
//        Connection conn = JDBCUtils.getConnection();
//        try {
//            return qr.update(conn, sql, args);
//        } catch (SQLException e) {
//            e.printStackTrace();
//        } finally {
//            JDBCUtils.close(conn);
//        }
//        return -1;
//    }
    public int updates (String sql, Object ...args) {
        Connection conn = JDBCUtils.getConnection();
        try {
            return qr.update(conn, sql, args);
        } catch (SQLException e) {
            e.printStackTrace();
            /**
             * 抛出异常,使外面捕获到可以进行事务回滚
             */
            throw new RuntimeException(e);
        }
    }

    /**
     * 返回结果为 单个数据
     * @param type
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
//    public <T> T queryForOne (Class<T> type, String sql, Object ...args) {
//        Connection conn = JDBCUtils.getConnection();
//        try {
//            return qr.query(conn, sql, new BeanHandler<T>(type), args);
//        } catch (SQLException e) {
//            e.printStackTrace();
//        } finally {
//            // JDBCUtils.close(conn);
//        }
//        return null;
//    }
    public <T> T queryForOne (Class<T> type, String sql, Object ...args) {
        Connection conn = JDBCUtils.getConnection();
        try {
            return qr.query(conn, sql, new BeanHandler<T>(type), args);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * 返回结果为 list的数据
     * @param type
     * @param sql
     * @param args
     * @param <T>
     * @return
     */
//    public <T> List<T> queryForList (Class<T> type, String sql, Object ...args) {
//        Connection conn = JDBCUtils.getConnection();
//        try {
//            return qr.query(conn, sql, new BeanListHandler<T>(type), args);
//        } catch (SQLException e) {
//            e.printStackTrace();
//        } finally {
//            // JDBCUtils.close(conn);
//        }
//        return null;
//    }
    public <T> List<T> queryForList (Class<T> type, String sql, Object ...args) {
        Connection conn = JDBCUtils.getConnection();
        try {
            return qr.query(conn, sql, new BeanListHandler<T>(type), args);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * 返回一行一列的 sql数据
     * @param sql
     * @param args
     * @return
     */
//    public Object queryForSingleValue (String sql, Object ...args) {
//        Connection conn = JDBCUtils.getConnection();
//        try {
//            return qr.query(conn, sql, new ScalarHandler(), args);
//        } catch (Exception e) {
//            e.printStackTrace();
//        } finally {
//            // JDBCUtils.close(conn);
//        }
//        return null;
//    }
    public Object queryForSingleValue (String sql, Object ...args) {
        Connection conn = JDBCUtils.getConnection();
        try {
            return qr.query(conn, sql, new ScalarHandler(), args);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }
}
  • OrderServlet.java
    • 使用 ThreadLocal
package com.atjava.web;

import com.atjava.pojo.Cart;
import com.atjava.pojo.User;
import com.atjava.service.OrderService;
import com.atjava.service.impl.OrderServiceImpl;
import com.atjava.utils.JDBCUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author lv
 * @create 2021-10-22 22:05
 */
public class OrderServlet extends BaseServlet {

    OrderService os = new OrderServiceImpl();

    /**
     * 生成订单
     * @param req
     * @param resp
     * @throws ServletException
     * @throws IOException
     */
    protected void createOrder (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // super.doPost(req, resp);
        /**
         * 1.获取 Session中的购物车
         * 2.调用 OrderServletImpl 生成订单
         * 3.获取订单号保存在 域中并跳转到订单页面
         */
        Cart cart = (Cart) req.getSession().getAttribute("cart");
        User currentUser = (User) req.getSession().getAttribute("currentUser");
        if (null == currentUser) {
            req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
            // 停止执行下边的代码
            return;
        }
        Integer userId = currentUser.getId();
        String osOrder = null;

        /**
         * ThreadLocal 的管理事务
         */
        try {
            osOrder = os.createOrder(cart, userId);
            JDBCUtils.commitAndClose(); // 提交事务
        } catch (Exception e) {
            JDBCUtils.rollbackAndClose(); // 回滚事务
            e.printStackTrace();
        }

        // 订单号保存到 Session 域中
        req.getSession().setAttribute("orderId", osOrder);
//        req.setAttribute("orderId", osOrder);

        resp.sendRedirect(req.getContextPath() + "/pages/cart/checkout.jsp");

    }
}

使用 Filter 过滤器统一给所有的 Service 方法都加上 try-catch,来进行事务的管理

  • web.xml
    • "/*" 表示过滤当前工程下的所有请求
<!--
    filter 与 sql事务 组合,统一进行 try-catch
-->
<filter>
    <filter-name>TransactionFilter</filter-name>
    <filter-class>com.atjava.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>TransactionFilter</filter-name>
    <!--
        "/*":表示当前工程下所有请求
    -->
    <url-pattern>/*</url-pattern>
</filter-mapping>
  • TransactionFilter.java
    • 使用 try-catch 包裹 chain.doFilter()方法
    • 从而利用是否报错来触发 sql事务的 提交或回滚
package com.atjava.filter;

import com.atjava.utils.JDBCUtils;

import javax.servlet.*;
import java.io.IOException;

/**
 * @author lv
 * @create 2021-11-04 21:43
 */
public class TransactionFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            chain.doFilter(request, response);
            JDBCUtils.commitAndClose(); // 提交事务
        } catch (Exception e) {
            JDBCUtils.rollbackAndClose(); // 回滚事务
            e.printStackTrace();
        }
    }

    @Override
    public void destroy() {

    }
}

将所有的异常都统一交给 Tomcat,让 Tomcat 展示友好的错误信息页面

  • 在 web.xml 中可以通过 错误页面配置来管理工程中出现的错误

  • web.xml

<!--
    error-page:工程中出现错误的配置
    注意:工程中出现的错误需要抛出后,此配置才能生效
-->
<error-page>
    <!-- error-code 错误类型 -->
    <error-code>500</error-code>
    <!-- location 标签表示,要跳转去的页面路径 -->
    <location>/pages/error/error500.jsp</location>
</error-page>

<error-page>
    <!-- error-code 错误类型 -->
    <error-code>404</error-code>
    <!-- location 标签表示,要跳转去的页面路径 -->
    <location>/pages/error/error404.jsp</location>
</error-page>
  • error500.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>我的订单</title>
   <%-- 使用静态包含替换,包含 base标签、css样式、jQuery文件 --%>
   <%@ include file="/pages/common/head.jsp" %>
<%--<link type="text/css" rel="stylesheet" href="../../static/css/style.css" >--%>
<style type="text/css">
   h1 {
      text-align: center;
      margin-top: 200px;
   }
</style>
</head>
<body>
   抱歉,后台服务错误,程序管理人员正在修复!!!<a href="index.jsp">返回首页</a>
</body>
</html>