JavaWeb书城项目(四)
9、使用 Filter 过滤器
9.1、使用 Filter 过滤器拦截/pages/manager/所有内容,实现权限检查
自定义类 ManagerFilter ,实现 javax.servlet.Filter 接口,重写方法
在 doFilter() 方法中,将参数 ServletRequest 转化为 HttpServletRequest
package com.atguigu.filter;
import com.atguigu.bean.User;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* @author shkstart
* @create 2022/1/4-11:48
*/
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 req = (HttpServletRequest) request;
User user = (User) req.getSession().getAttribute("user");
if(user == null){
//跳回登录页面
request.getRequestDispatcher("/pages/user/login.jsp").forward(request,response);
}else {
//如果已经登陆,就放行
chain.doFilter(request,response);
}
}
@Override
public void destroy() { }
}
web.xml 配置文件中的配置
<filter>
<filter-name>ManagerFilter</filter-name>
<filter-class>com.atguigu.filter.ManagerFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ManagerFilter</filter-name>
<url-pattern>/pages/manager/*</url-pattern>
<!--不仅要拦截具体的页面,还要拦截后台的 Servlet 资源内容-->
<url-pattern>/manager/bookServlet</url-pattern>
</filter-mapping>
9.2、使用 ThreadLocal 来确保所有 dao 操作都在同一个 Connection 连接对象中完成
以下仅对 生成订单 的部分进行演示
原理分析图:
在 orderServiceImpl.createOrder() 方法里面,调用到了 orderDAOImpl.saveOrder()、orderItemDAOImpl.saveOrderItem()、bookServiceImpl.updateBook() 三个方法
如果在生成订单之后,生成订单详情和销量库存变更的 for 循环之前出现了异常,就会导致数据库里面只有订单,但是没有订单商品内容
【如图】
数据库里面有订单
但是没有订单商品内容
所以要保证 createOrder() 方法在一个事务中,要么都执行,要么都不执行
回忆 JDBC 里的事务部分,代码如下:
【代码如下】
建立连接——开启事务——一系列操作——如没有异常就提交事务——如有异常就回滚事务——最后关闭连接
那么在 OrderServlet.createOrder() 方法里面,首先要保证该方法里面所有调用到的操作都是一个连接里面,就使用 ThreadLocal 来存放连接,那么对 JdbcUtils 工具类进行修改,代码如下:
【代码如下】
存放连接的同时可以开启事务,这样非首次获取连接的时候,就已经开启了事务
最后关闭连接之后还需要释放连接,因为 Tomcat 底层也是使用数据库连接池,如果不及时释放,那么到一定数量时,就再也拿不到连接了
public class JdbcUtils {
private static ThreadLocal<Connection> conns = new ThreadLocal<Connection>();
/**
* 获取数据库连接池中的连接
* @return 如果返回 null,说明获取连接失败<br/>有值就是获取连接成功
*/
public static Connection getConnection(){
Connection conn = conns.get();
if (conn == null) {
try {
conn = dataSource.getConnection();//从数据库连接池中获取连接
conns.set(conn); // 保存到 ThreadLocal 对象中,供后面的 jdbc 操作使用
conn.setAutoCommit(false); // 设置为手动管理事务
} catch (SQLException e) {
e.printStackTrace();
}
}
return conn;
}
/**
* 提交事务,并关闭释放连接
*/
public static void commitAndClose(){
Connection connection = conns.get();
if (connection != null) { // 如果不等于 null,说明 之前使用过连接,操作过数据库
try {
connection.commit(); // 提交 事务
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
connection.close(); // 关闭连接,资源资源
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// 一定要执行 remove 操作,否则就会出错。(因为 Tomcat 服务器底层使用了线程池技术)
conns.remove();
}
/**
* 回滚事务,并关闭释放连接
*/
public static void rollbackAndClose(){
Connection connection = conns.get();
if (connection != null) { // 如果不等于 null,说明 之前使用过连接,操作过数据库
try {
connection.rollback();//回滚事务
} catch (SQLException e) {
e.printStackTrace();
} finally {
try {
connection.close(); // 关闭连接,资源资源
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// 一定要执行 remove 操作,否则就会出错。(因为 Tomcat 服务器底层使用了线程池技术)
conns.remove();
}
}
在 orderServiceImpl.createOrder() 方法里面,调用到了 orderDAOImpl.saveOrder()、orderItemDAOImpl.saveOrderItem()、bookServiceImpl.updateBook() 三个方法
那么这些方法传入的连接就都使用 ThreadLocal 里面存放的那个连接,并且不可以有关闭操作,也不可以捕获异常,必须抛出异常,否则回滚事务的地方就无法捕获异常,代码如下:
【代码如下】
修改 BaseDao
编写代码的时候都对异常进行捕获了,这里就需要抛出异常,可以将 try-catch 改为 throws ,这里为了方便,直接加一句 throw new RuntimeException(e);
因为抛出了异常,最下面的 return -1; 就没作用了
修改 BaseServlet
不可以捕获异常
修改 OrderItemDAOImpl
不可以有关闭操作
修改 OrderDAOImpl
不可以有关闭操作
修改 OrderServiceImpl
修改 OrderServlet
如此在 Servlet 程序里面,对业务语句进行 try-catch 操作,即可实现生成订单、生产订单详情位于同一个事务中
做到发生异常的时候实现回滚
9.3、使用 Filter 过滤器对上面进行优化
其实就是使用 Filter 过滤器统一给所有的 Service 方法都加上 try-catch,来进行实现的管理
原理分析图:
上面说到要对业务 Servlet 程序进行 try-catch 操作,由于业务较多,会比较麻烦
不妨将所有 Servlet 程序放入 Filter 过滤器中
因为在 Filter 过滤器中,执行到 filterChain.doFilter()
的时候才会执行所有 Servlet 程序
所以对 filterChain.doFilter()
方法进行 try-catch 操作,就相当于对所有 Servlet 程序进行 try-catch 操作
Filter 类代码:
在 web.xml 中的配置:
<filter>
<filter-name>TransactionFilter</filter-name>
<filter-class>com.atguigu.filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TransactionFilter</filter-name>
<!-- /* 表示当前工程下所有请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
一定要记得把 把 BaseServlet 中的异常往外抛给 Filter 过滤器
public abstract class BaseServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
doPost(req, resp);
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
// 解决 post 请求中文乱码问题
// 一定要在获取请求参数之前调用才有效
req.setCharacterEncoding("UTF-8");
String action = req.getParameter("action");
try {
// 获取 action 业务鉴别字符串,获取相应的业务 方法反射对象
Method method = this.getClass().getDeclaredMethod(action, HttpServletRequest.class,HttpServletResponse.class);
// System.out.println(method);
// 调用目标业务 方法
method.invoke(this, req, resp);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);// 把异常抛给 Filter 过滤器
}
}
}
9.4、将所有异常都统一交给 Tomcat,让 Tomcat 展示友好的错误信息页面
因为对于用户的角度,只知道点击后显示空白页面,不知道出了什么问题
在 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-page>
<!--error-code 是错误类型-->
<error-code>404</error-code>
<!--location 标签表示。要跳转去的页面路径-->
<location>/pages/error/error404.jsp</location>
</error-page>
10、使用 AJAX
10.1、使用 AJAX 验证注册用户名是否可用
实现前后端的分离
因为 JSON 对象就是键值对的方式存储的,所以要将客户端需要的结果封装为 Map 对象
UserServlet 程序中 ajaxExistsUsername 方法:
public class UserServlet extends BaseServlet {
protected void ajaxExistsUsername(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
UesrServiceImpl uesrService = new UesrServiceImpl();
// 获取请求的参数 username
String username = request.getParameter("username");
// 调用 userService.existsUsername();
boolean b = uesrService.existsUsername(username);
// 将客户端需要的结果封装为 Map 对象
Map<String,Object> resultMap = new HashMap<>();
resultMap.put("existsUsername",b);
// 将 Map 对象转化为 JSON 字符串
Gson gson = new Gson();
String json = gson.toJson(resultMap);
// 响应到浏览器
response.getWriter().write(json);
}
}
regist.jsp 页面中的代码:
<script type="text/javascript">
$(function () {
//用户名的失去焦点事件,验证用户名是否合法
$("#username").blur(function () {
//1 获取用户名参数
var username = this.value;
//2 将参数传递给 Servlet 程序
$.getJSON("http://localhost:8080/book02/userServlet","methodName=ajaxExistsUsername&username=" + username,function (abc) {
//3 Servlet 程序回传的数据(JSON对象),获取对象key为existsUsername的值,进行判断
if (abc.existsUsername) {
$("span.errorMsg").text("用户名已存在!");
} else {
$("span.errorMsg").text("用户名可用!");
}
});
})
})
</script>
10.2、使用 AJAX 修改把商品添加到购物车
修改之前,是将添加商品的名称存入 session 域中
CartServlet 程序:
public class CartServlet extends BaseServlet {
private BookServiceImpl bookService = new BookServiceImpl();
//加入购物车
public void addItem(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1、 获取请求的参数:商品编号,本项目中也就是图书编号
int id = WebUtils.parseInt(request.getParameter("id"), 0);
//2、 调用 BookServiceImpl.queryBookById() ,得到图书信息
Book book = bookService.queryBookById(id);
//3、根据图书信息,生成购物车商品项对象 CartItem
CartItem cartItem = new CartItem(book.getId(), book.getName(), 1, book.getPrice(), book.getPrice());
//4、 获取 session 域中的购物车对象
//注意这里,每次都创建新的购物车的话,里面的内容就没法都保存下来
// Cart cart = new Cart();
//本项目讲解的是将购物车信息保存到 session 域中
//保证每次使用的都是一个购物车,不是每次请求添加就都是新的购物车
Cart cart = (Cart) request.getSession().getAttribute("cart");
if(cart == null){
cart = new Cart();
request.getSession().setAttribute("cart",cart);
}
//5、 调用 Cart.addItem(cartItem) ,添加商品项
cart.addItem(cartItem);
//6、返回购物车总的商品数量和最后一个添加的商品名称
// 将要回传的数据封装进 Map 对象
Map<String,Object> resultMap = new HashMap<String,Object>();
// 购物车总数量
resultMap.put("totalCount", cart.getTotalCount());
// 每次点击添加的商品名称
resultMap.put("lastName",cartItem.getName());
Gson gson = new Gson();
// 将 Map 对象转化为 JSON 字符串
String resultMapJsonString = gson.toJson(resultMap);
// 回传到浏览器
response.getWriter().write(resultMapJsonString);
// // 每次点击添加的商品名称
// request.getSession().setAttribute("cartItemName",cartItem.getName());
// //请求重定向回到商品列表中按钮点击发起请求的页面
// response.sendRedirect(request.getHeader("Referer"));
}
}
修改之前,是将 “ 加入购物车 ” 的按钮绑定单击事件,传递请求参数:商品id
pages/client/index.jsp 页面
html 代码:
添加几个 id 属性
javaScript 代码:
<script type="text/javascript">
$(function () {
//加入购物车按钮,绑定单击事件
$("button.addToCart").click(function () {
//获取商品的编号
//在事件响应的 function 函数 中,有一个 this 对象,这个 this 对象,是当前正在响应事件的 dom 对象
var bid = $(this).attr("bbid");
发 ajax 请求,添加商品到购物车
$.getJSON("${basePath}cartServlet","methodName=addItem&id="+bid,function (abc) {
$("#cartTotalCount").text("您的购物车中有"+ abc.totalCount +"件商品");
$("#cartLastName").text(abc.lastName);
})
})
})
</script>