尚硅谷JavaWeb笔记——书城项目(第九阶段:添加过滤器)

「爱情、让人受尽委屈。」 2022-11-01 10:57 293阅读 0赞

文章目录

  • 第九阶段-使用Filter过滤器
    • 使用Filter过滤器拦截
    • ThreadLocal的使用
    • 使用Filter和ThreadLocal组合管理事务
      • 使用ThreadLocal来获取数据库连接
        • 修改JDBC工具类
          • **获取数据库连接**
          • **数据库提交与关闭**
          • **数据库事物回滚与关闭**
      • 使用FIlter过滤器统一处理数据库事物
      • 将所有异常都统一交给Tomcat

第九阶段-使用Filter过滤器

使用Filter过滤器拦截

需求:如果想查看后台数据,需要用户登陆后才能访问

需要拦截/pages/manager/所有内容,实现权限检查

  1. public class ManagerFilter implements Filter {
  2. public void destroy() { }
  3. public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
  4. HttpServletRequest httpServletRequest = (HttpServletRequest) req;
  5. HttpSession session = httpServletRequest.getSession();
  6. Object user = session.getAttribute("user");
  7. if (user == null) {
  8. req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
  9. }else {
  10. chain.doFilter(req, resp);
  11. }
  12. chain.doFilter(req, resp);
  13. }
  14. public void init(FilterConfig config) throws ServletException { }
  15. }

需要在web.xml中追加过滤器的配置信息

  1. <filter>
  2. <filter-name>ManagerFilter</filter-name>
  3. <filter-class>filter.ManagerFilter</filter-class>
  4. </filter>
  5. <filter-mapping>
  6. <filter-name>ManagerFilter</filter-name>
  7. <url-pattern>/pages/manager/*</url-pattern>
  8. <url-pattern>/manager/bookServlet</url-pattern>
  9. </filter-mapping>

这里为了防止用户使用Servlet程序进入后台,还需要追加一个拦截地址。

ThreadLocal的使用

ThreadLocal的作用:解决多线程数据安全问题

ThradLocal可以给当前线程关联一个数据(普通变量,对象,数组或集合等)

ThreadLocal的特点

  1. ThreadLocal可以为当前线程关联一个数据可以像Map一样存取数据,key为当前线程)Map关联线程名方法如下:

    1. public static Map<String,Object> data = new Hashtable<String,Object>();
    2. public static class Task implements Runnable {
    3. @Override
    4. public void run() {
    5. String name = Thread.currentThread().getName();
    6. System.out.println("在线程[" +name + "]保存的值是:bbj" );
    7. threadLocal.set("bbj");
    8. // 在Run方法中,随机生成一个变量(线程要关联的数据),然后以当前线程名为key保存到map中
    9. Integer i = random.nextInt(1000);
    10. // 获取当前线程名
    11. String name = Thread.currentThread().getName();
    12. System.out.println("线程["+name+"]生成的随机数是:" + i);
    13. data.put(name,i); //将当前线程名绑定上一个随机数
    14. threadLocal.set(i);
    15. try {
    16. Thread.sleep(3000);
    17. } catch (InterruptedException e) {
    18. e.printStackTrace();
    19. }
    20. new OrderService().createOrder(); // 在该线程下访问OrderService,模拟多线程
    21. // 在Run方法结束之前,以当前线程名获取出数据并打印。查看是否可以取出操作
    22. Object o = data.get(name);
    23. Object o = threadLocal.get();
    24. System.out.println("在线程["+name+"]快结束时取出关联的数据是:" + o);
    25. }
    26. }
    27. public static void main(String[] args) {
    28. for (int i = 0; i < 3; i++){
    29. new Thread(new Task()).start();
    30. }
    31. Map<String,Object> map = new HashMap<>();
    32. System.out.println(map.get("key"));
    33. System.out.println( threadLocal.get() ); // 不保存,也想取一个有效的值。怎么办,需要你事务准备这个值。
    34. }
    • 上述代码相当于模拟多线程操作,在主函数中创建了3个线程每个线程都绑定了一个随机数。效果如图所示

      \[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-My5FowTA-1614147980336)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210222153641604.png)\]

    • 每一个线程对应了一个随机数值,在该线程流运行的过程中,该线程途径的任何操作都有唯一的随机数值
  2. 每一个ThreadLocal对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个ThreadLocal对象实例。

    • 使用ThreadLocal来替换上述代码

      1. public static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(){
      2. // 默认值
      3. @Override
      4. protected Object initialValue() {
      5. return "童伟的默认值";
      6. }
      7. };
      8. // 这里只有一个泛型——value值的类型
      9. threadLocal.set("1");//向当前线程中设置唯一的的数据
      10. Object o = threadLocal.get(); // 获取关联的唯一数据
      • **小结:**刚刚使用了一个线程安全的静态变量的hashtable对象在多线程情况下存取数据,没有发生线程安全问题。使用共享的静态ThreadLocal对象在多线程情况下存取数据也不存在线程安全问题
  3. 每个ThreadLocal对象实例定义时,一般都是static类型
  4. ThreadLocal中保存的数据,在线程销毁后,会由JVM虚拟机释放

使用该方法最主要的原因是为了线程安全考虑。由于在分布式多线程环境下,如果希望处理数据库事务时能有回滚操作,就必须保证操作前后是针对于同一个数据库连接进程。因此,考虑到ThreadLocal的特性,这里可以在/utils/中的的JDBCUtils定义一个静态变量

  1. private static ThreadLocal<Connection> conns = new ThreadLocal<Connection>();//value值类型时一个连接

使用Filter和ThreadLocal组合管理事务

回顾数据库事务:确保所有操作要么都成功,要么都失败。

  1. Connection conn = JDBCUtils.getConnection();
  2. try{
  3. conn.setAutoCommit(false); //设置为手懂管理实务
  4. 执行一些列jdbc操作
  5. conn.commit();//手动提交事务
  6. }catch(Exception e){
  7. conn.rollback(); // 如果有异常就会滚事务
  8. }

⚠️ 如果要确保所有操作都在一个事务哪,就必须要确保,所有操作都在同一个Connection连接对象

问:如何确保所有操作都使用同一个Connection连接对象?

答:ThreadLocal线程绑定连接

使用ThreadLocal来获取数据库连接

可以使用ThreadLocal对象,来确保所有操作都适用同一个Connection对象。但所有操作都必须在同一个线程中完成,这个条件是满足的。由此可以得到如下分析示意图

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CLVQ7EVD-1614147980339)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210222155831881.png)\]

说明:当创建了一个数据库连接后,就将其存储在当前线程的线程域中,之后设置事务,执行操作以及关闭时都从该线程域中获取对应的数据库连接,从而保证了前后都是同一个数据库连接(事务处理要求)

修改JDBC工具类

获取数据库连接

如果是第一次获取,则需要先从数据库连接池中获取,并设置提交方式。如果不是,则直接从线程域中得到连接

  1. private static final ThreadLocal<Connection> conns = new ThreadLocal<Connection>();//设置线程域对象
  2. public static Connection getConnection3Druid() throws SQLException{
  3. Connection conn = conns.get(); //先从当前线程域中获取,如果获取不了(说明是第一次获取)再创建
  4. if (conn == null) {
  5. // 将从数据库连接池中获取到连接保存到ThreadLocal对象中,供后面的jdbc操作使用 相对最后一次保存有效!
  6. conns.set(source.getConnection());
  7. conn = conns.get(); //以后所有的连接都获取唯一的
  8. conn.setAutoCommit(false);// 设置为手动管理事务
  9. }
  10. return conn;
  11. }
数据库提交与关闭

直接获取当前数据库连接,如果连戒值非空,则说明之前有过连接操作,直接从线程域中获取,然后再执行事务提交操作,并在最后关闭数据库连接(这里一定要把异常抛出去,让tomcat知道出现了异常)

  1. /** * 提交事务,并关闭释放连接 */
  2. public static void commitAndClose() {
  3. Connection conn = conns.get();
  4. if (conn != null) { // 如果不等于null,说明 之前使用过连接,操作过数据库
  5. try {
  6. conn.commit();// 提交 事务
  7. } catch (SQLException throwables) {
  8. throwables.printStackTrace();
  9. }finally {
  10. try {
  11. conn.close();// 关闭连接,资源资源
  12. } catch (SQLException throwables) {
  13. throwables.printStackTrace();
  14. }
  15. }
  16. }
  17. // 一定要执行remove操作,否则就会出错。(因为Tomcat服务器底层使用了线程池技术)
  18. conns.remove();
  19. }
数据库事物回滚与关闭

类似于事务提交,区别在于

  1. /** * 回滚事务,并关闭释放连接 */
  2. public static void rollbackAndClose() {
  3. Connection conn = conns.get();
  4. if (conn != null) { // 如果不等于null,说明 之前使用过连接,操作过数据库
  5. try {
  6. conn.rollback();// 回滚事务
  7. } catch (SQLException throwables) {
  8. throwables.printStackTrace();
  9. }finally {
  10. try {
  11. conn.close();// 关闭连接,资源资源
  12. } catch (SQLException throwables) {
  13. throwables.printStackTrace();
  14. }
  15. }
  16. }

⚠️由于上述代码中包含有关闭数据库连接的操作,因此需要在baseDao中删除数据库连接关闭的代码段,同时还要将捕获的异常外抛Dao层的异常一定要外抛,这样外层就知道当前存在异常好执行回滚操作。

update方法为例

  1. public int update(String sql, Object ... args) {
  2. System.out.println(" BaseDao 程序在[" +Thread.currentThread().getName() + "]中");
  3. Connection conn = null;
  4. try {
  5. conn = JDBCUtils.getConnection3Druid();
  6. System.out.println(conn);
  7. int i = queryRunner.update(conn, sql, args);
  8. System.out.println("影响了" + i +"行");
  9. return i;
  10. } catch (SQLException throwables) { //外抛异常
  11. throwables.printStackTrace();
  12. throw new RuntimeException(throwables);
  13. } // 删除数据库连接关闭操作
  14. // finally{
  15. // JdbcUtils.close(conn);
  16. // }
  17. }

使用FIlter过滤器统一处理数据库事物

\[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tRiZnlF0-1614147980340)(/Users/gaojunsong/Library/Application Support/typora-user-images/Java学习/JAVAWeb/image-20210222134124639.png)\]

由于每一个Servlet程序都需要添加try-catch来进行提交与事务回滚,因此考虑使用Filter来对所有的Service服务进行事务管理。这是因为在Filter过滤器中,如果希望请求某个目标页面的资源,就会执行doFilter()方法,一旦doFilter()方法出现错误,就可以捕获该异常,从而实现事务回滚。

基于上述讨论,由于doFilter()方法能够简介调用Servlet中的任何方法(对任意的资源进行过滤),因此可以捕获任意的servlet程序,故可以构造一个TransactionFilter过滤器,在其中增加try-catch代码块,且该过滤器的过滤范围为工程内的全部资源,代码如下:

  1. public class TransactionFilter implements Filter {
  2. public void destroy() { }
  3. public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
  4. try {
  5. chain.doFilter(req, resp); //由于该处是最有一个filter过滤器,这里的功能是调用目标资源
  6. JDBCUtils.commitAndClose();//提交事物
  7. } catch (Exception e) {
  8. JDBCUtils.rollbackAndClose();//回滚事物
  9. e.printStackTrace();
  10. throw new RuntimeException(e);//把异常抛给Tomcat管理展示友好的错误页面
  11. }
  12. }
  13. public void init(FilterConfig config) throws ServletException { }
  14. }

对应的xml配置信息如下:

  1. <filter>
  2. <filter-name>TransactionFilter</filter-name>
  3. <filter-class>filter.TransactionFilter</filter-class>
  4. </filter>
  5. <filter-mapping>
  6. <filter-name>TransactionFilter</filter-name>
  7. <!-- /* 表示当前工程下所有请求 -->
  8. <url-pattern>/*</url-pattern>
  9. </filter-mapping>

此外,还需要注意:对于Service层出现的问题,抛出给上一级是是在BaseServlet中,因此在对应位置还需要将异常抛给过滤器

将所有异常都统一交给Tomcat

让Tomcat展示友好的错误页面,在web.xml配置错误页面信息

  1. <!--error-page标签配置,服务器出错之后,自动跳转的页面-->
  2. <error-page>
  3. <!--error-code是错误类型-->
  4. <error-code>500</error-code>
  5. <!--location标签表示。要跳转去的页面路径-->
  6. <location>/pages/error/error500.jsp</location>
  7. </error-page>
  8. <!--error-page标签配置,服务器出错之后,自动跳转的页面-->
  9. <error-page>
  10. <!--error-code是错误类型-->
  11. <error-code>404</error-code>
  12. <!--location标签表示。要跳转去的页面路径-->
  13. <location>/pages/error/error404.jsp</location>
  14. </error-page>

发表评论

表情:
评论列表 (有 0 条评论,293人围观)

还没有评论,来说两句吧...

相关阅读