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

第九阶段-使用Filter过滤器

使用Filter过滤器拦截

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

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

public class ManagerFilter implements Filter {
  public void destroy() {}

  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) req;
    HttpSession session = httpServletRequest.getSession();
    Object user = session.getAttribute("user");

    if (user == null) {
      req.getRequestDispatcher("/pages/user/login.jsp").forward(req, resp);
    }else {
      chain.doFilter(req, resp);
    }
    
    chain.doFilter(req, resp);
  }
  
  public void init(FilterConfig config) throws ServletException {}

}

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

<filter>
  <filter-name>ManagerFilter</filter-name>
  <filter-class>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>

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

ThreadLocal的使用

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

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

ThreadLocal的特点

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

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

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

    • 每一个线程对应了一个随机数值,在该线程流运行的过程中,该线程途径的任何操作都有唯一的随机数值

  2. 每一个ThreadLocal对象,只能为当前线程关联一个数据,如果要为当前线程关联多个数据,就需要使用多个ThreadLocal对象实例。

    • 使用ThreadLocal来替换上述代码

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

  4. ThreadLocal中保存的数据,在线程销毁后,会由JVM虚拟机释放

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

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

使用Filter和ThreadLocal组合管理事务

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

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

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

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

答:ThreadLocal线程绑定连接

使用ThreadLocal来获取数据库连接

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

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

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

修改JDBC工具类
获取数据库连接

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

private static final ThreadLocal<Connection> conns = new ThreadLocal<Connection>();//设置线程域对象

public static Connection getConnection3Druid() throws SQLException{

  Connection conn = conns.get();	//先从当前线程域中获取,如果获取不了(说明是第一次获取)再创建

  if (conn == null) {		
    // 将从数据库连接池中获取到连接保存到ThreadLocal对象中,供后面的jdbc操作使用 相对最后一次保存有效!
    conns.set(source.getConnection());
    conn = conns.get();				//以后所有的连接都获取唯一的
    conn.setAutoCommit(false);// 设置为手动管理事务
  }

  return conn;
}
数据库提交与关闭

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

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

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

/**
	 * 回滚事务,并关闭释放连接
	 */
public static void rollbackAndClose() {
  Connection conn = conns.get();
  if (conn != null) {// 如果不等于null,说明 之前使用过连接,操作过数据库
    try {
      conn.rollback();// 回滚事务
    } catch (SQLException throwables) {
      throwables.printStackTrace();
    }finally {
      try {
        conn.close();// 关闭连接,资源资源
      } catch (SQLException throwables) {
        throwables.printStackTrace();
      }
    }
  }

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

update方法为例

    public int update(String sql, Object ... args) {
        System.out.println(" BaseDao 程序在[" +Thread.currentThread().getName() + "]中");

        Connection conn = null;
        try {
            conn = JDBCUtils.getConnection3Druid();
            System.out.println(conn);
            int i = queryRunner.update(conn, sql, args);
            System.out.println("影响了" + i +"行");
            return i;
        } catch (SQLException throwables) {		//外抛异常
            throwables.printStackTrace();
            throw new RuntimeException(throwables);
        }	// 删除数据库连接关闭操作
//        finally{
//            JdbcUtils.close(conn);
//        }
    }

使用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代码块,且该过滤器的过滤范围为工程内的全部资源,代码如下:

public class TransactionFilter implements Filter {
  public void destroy() {}

  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {

    try {
      chain.doFilter(req, resp);		//由于该处是最有一个filter过滤器,这里的功能是调用目标资源

      JDBCUtils.commitAndClose();//提交事物
    } catch (Exception e) {
      JDBCUtils.rollbackAndClose();//回滚事物
      e.printStackTrace();
      throw new RuntimeException(e);//把异常抛给Tomcat管理展示友好的错误页面
    }
  }

  public void init(FilterConfig config) throws ServletException {}
}

对应的xml配置信息如下:

<filter>
  <filter-name>TransactionFilter</filter-name>
  <filter-class>filter.TransactionFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>TransactionFilter</filter-name>
  <!-- /* 表示当前工程下所有请求 -->
  <url-pattern>/*</url-pattern>
</filter-mapping>

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

将所有异常都统一交给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>
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值