zoukankan      html  css  js  c++  java
  • 【Java EE 学习 19】【使用过滤器实现全站压缩】【使用ThreadLocal模式解决跨DAO事务回滚问题】

    一、使用过滤器实现全站压缩

    1.目标对网站的所有JSP页面进行页面压缩,减少用户流量的使用。但是对图片和视频不进行压缩,因为图片和视频的压缩率很小,而且处理所需要的服务器资源很大。

    2.实现原理

      (1)使用GZIPOutputStream工具对数据进行压缩,中间借助了ByteArrayOutputStream类进行结果的存储。

      (2)使用过滤器对浏览器请求进行拦截,通过自定义HttpServletResponse类(使用包装模式),重写getWriter方法,使得写出的目的地转变成ByteArrayOutputStream对象,即内存;通过查看JSP引擎解析之后的源代码,可以发现Servlet使用out对象向浏览器输出html代码,out对象是PageContext对象的一个成员变量,为JspWriter类的实例;而JspWriter实际上是对Writer类的包装,其本身和PrintWriter是兄弟关系;

      (3)JSP页面中使用的out对象和浏览器请求时产生的response对象通过方法getPrintWriter得到的对象是同一个对象。具体验证方法可以通过在自定义Response对象中的getWriter方法中进行打印输出验证。JspWriter类中有关键代码:  

     if (this.out == null) {
          this.out = this.response.getWriter();
    }

    而out对象有定义:

    public class JspWriterImpl
      extends JspWriter
    {
      private Writer out;
    ......
    }

      (5)具体过程:过滤器拦截浏览器向JSP页面发出的请求,并将请求的response对象替换成自定义的response对象,该对象重写了getWriter方法,使得JSP页面中的代码在经过JSP解析引擎解析之后的Servlet代码写出的目的地由浏览器变成了内存(ByteArrayOutputStream);过滤器拦截服务器向浏览器发出的响应信息,并将内存中的数据拿出来(ByteArrayOutputStream.toByteArray()),使用GZIPOutputStream进行压缩(压缩到ByteArrayOutputStream),最后通过getOutputSteam方法获得原生输出流对象,将压缩之后的数据写出到浏览器。

    3.问题升级

      (1)现在又需求使用Servlet,而且要对Servlet进行压缩

      (2)Servlet使用getOutputStream方法获得输出流。

      (3)问:如果只是2.(5)的操作流程能解决这个需求吗?

    答:不能,将会产生空指针异常,原因是ByteArrayOutputStream在重写的getWriter中被赋初值,如果没有调用该方法,则该变量一直为NULL,几乎不能对该变量进行任何操作,否则一定会报空指针异常。

      (4)解决方法:重写getOutputStream方法,通过和getWriter相同的目的将JSP页面的代码写到ByteArrayOutputStream(实现方式不同)

      (5)疑问:这样会不会对之前的JSP页面的压缩产生影响?之前的页面调用了response的getOutputStream方法~

        答:不会。最后输出的时候使用的response是原生的response对象,该对象的getOutputStream方法写到的目的地永远都是浏览器,而重写的getOutputStream方法只在自定义的HttpServletResponse,所以二者不会相互影响。

    4.代码。

     1 package com.kdyzm.filter;
     2 /*
     3  * 使用过滤器拦截所有JSP页面并压缩文字,而且要实现低耦合性,一旦删掉过滤器
     4  * 原来的jsp页面将不会被压缩。
     5  * 全站压缩使用的模板代码!!!!!!!!!!!!!!!!!
     6  */
     7 import java.io.ByteArrayOutputStream;
     8 import java.io.IOException;
     9 import java.io.OutputStream;
    10 import java.io.OutputStreamWriter;
    11 import java.io.PrintWriter;
    12 import java.util.zip.GZIPOutputStream;
    13 
    14 import javax.servlet.Filter;
    15 import javax.servlet.FilterChain;
    16 import javax.servlet.FilterConfig;
    17 import javax.servlet.ServletException;
    18 import javax.servlet.ServletOutputStream;
    19 import javax.servlet.ServletRequest;
    20 import javax.servlet.ServletResponse;
    21 import javax.servlet.http.HttpServletResponse;
    22 import javax.servlet.http.HttpServletResponseWrapper;
    23 
    24 public class GzipFilter implements Filter {
    25 
    26     @Override
    27     public void init(FilterConfig filterConfig) throws ServletException {
    28     }
    29 
    30     @Override
    31     public void doFilter(ServletRequest request, ServletResponse response,
    32             FilterChain chain) throws IOException, ServletException {
    33         HttpServletResponse hsr=(HttpServletResponse) response;
    34         MyResponse myResponse=new MyResponse(hsr);
    35         chain.doFilter(request, myResponse);
    36         ByteArrayOutputStream baso=myResponse.getArrayOutputStream();
    37         
    38         //1.准备数据
    39         byte []arr=baso.toByteArray();
    40         ByteArrayOutputStream dest=new ByteArrayOutputStream();
    41         //2.准备压缩工具
    42         GZIPOutputStream gzip=new GZIPOutputStream(dest);
    43         gzip.write(arr);
    44         gzip.close();
    45         //3.设置相关文件头
    46         hsr.setContentType("text/html;charset=utf-8");
    47         hsr.setHeader("Content-encoding", "gzip");
    48         hsr.setContentLength(dest.toByteArray().length);
    49         //3.1验证压缩结果
    50         System.out.println("压缩之前:"+arr.length);
    51         System.out.println("压缩之后:"+dest.toByteArray().length);
    52         System.out.println("压缩比为:"+(1-dest.toByteArray().length*1.0/arr.length));
    53         //4.发送到浏览器
    54         OutputStream os=response.getOutputStream();
    55         os.write(dest.toByteArray());
    56     }
    57     
    58     @Override
    59     public void destroy() {
    60     }
    61 }
    62 class MyResponse extends HttpServletResponseWrapper
    63 {
    64     private ByteArrayOutputStream baos=null;
    65     PrintWriter pw=null;
    66     public MyResponse(HttpServletResponse response) {
    67         super(response);
    68     }
    69     //重写Writer方法
    70     @Override
    71     public PrintWriter getWriter() throws IOException {
    72 //        System.out.println("拿到输出流!");//验证拿到的结果Writer就是在这里拿到的。
    73         baos=new ByteArrayOutputStream();
    74         pw=new PrintWriter(new OutputStreamWriter(baos,"utf-8"),true);
    75         //因为调用的方法是write(String str)方法,所以就算写了true也不会自动刷新,所以必须关流才行
    76         return pw;
    77     }
    78     //重写该类的getOutputStream方法,重写该方法的目的是为了防止某个Servlet使用了OutputStream输出了数据。
    79     //这样即使对Servlet进行了过滤也不会产生任何错误!
    80     @Override
    81     public ServletOutputStream getOutputStream() throws IOException {
    82         baos=new ByteArrayOutputStream();
    83         ServletOutputStream sos=new ServletOutputStream() {
    84             @Override
    85             public void write(int b) throws IOException {
    86                 baos.write(b);//这一步是最关键的步骤。
    87             }
    88         };
    89         return sos;
    90     }
    91     public ByteArrayOutputStream getArrayOutputStream() {
    92         if(pw!=null){
    93             //关闭输出流的时机要充分把握好
    94             pw.close();
    95         }
    96         return baos;
    97     }
    98 }
    GzipFilter过滤器源代码

      注意:

      (1)自定义的PrintWriter对象一定要关闭,否则不会将内容输出缓冲区,因为JSP解析引擎调用的方法是out.write(String value);而不是out.println(String value),这样即使使用了自动刷新的构造方法创建PrintWriter对象也不会起作用;

      (2)重写getOutputStream方法的时候一定是将字节写到ByteArrayOutputStream对象中。

      (3)随时注意编码方式,有编码方式的地方一定要带上参数,否则极易发生乱码问题。


    二、使用ThreadLocal模式解决跨DAO事务回滚问题。

    1.目标解决跨DAO的事务回滚问题从而实现使用ThreadLocal管理事务。

    2.问题产生:由于DAO层的每个实例使用的都不是同一个Connection对象,因此如果想要实现事务回滚,就必须实现让DAO层使用的Connection是同一个对象,但是使用数据库连接池的时候分配的Connection对象是随机的,这样就很难办到让跨DAO的事务具有回滚功能。

    3.解决方法:使用ThreadLocal管理事务,这样就能保证在DAO层涉及的Connection对象具有唯一性。

    4.ThreadLocal简介

      (1)ThreadLocal内部维护了一个Map对象,该对象的键值是当前线程,即Thread.currentThread(),而值是希望与当前线程绑定的某个状态(一个对象),这样只要在同一个线程中所有代码均能获得该线程的绑定对象。再本例题中使用的对象是Connection。

      (2)ThreadLocal提供了set和get方法,表示设置当前线程的绑定对象和获取当前线程的绑定对象。

      (3)在java当前对象中有四种引用方式:强引用、软引用、弱引用、虚引用,而ThreadLocal使用内部使用的是若引用,因此可以不调用方法remove而删除和当前线程绑定的对象。

      (4)弱引用的表现:

     1 package com.kdyzm.test;
     2 
     3 import java.lang.ref.WeakReference;
     4 
     5 public class Test2 {
     6     public static void main(String[] args) {
     7         WeakReference<A>wr=new WeakReference<A>(new A());
     8         
     9         System.gc();//弱引用在这里就会被垃圾回收器回收。
    10         System.out.println("结果是:"+wr.get());
    11     }
    12 }
    13 class A
    14 {
    15     @Override
    16     protected void finalize() throws Throwable {
    17         System.out.println("被回收!");
    18         super.finalize();
    19     }
    20 }
    弱引用WeakReference

    5.解决思路:

      (1)为了解决DAO层的各个对象使用的Connection不相同的问题,需要在DataSource_c3p0中添加成员变量ThreadLocal<Connection>,该变量负责操作和Connection对象的绑定关系。在分配Connection对象的时候首先判断tl.get()有没有值,如果没有值,则从DataSource对象中分配出来一条Connection,并将该Connection和当前线程绑定。(ThreadLocal的set方法);否则直接返回通过get方法得到的Connection对象。

      (2)在过滤器中处理事务。当浏览器向服务器发起请求的时候,过滤器将会拦截请求,并且同时向数据库连接池请求一条连接,这时候会触发一个事件将当前线程和请求的Connection对象绑定。之后进行异常的捕捉,如果在doFilter方法执行的时候(执行的过程中只要是DAO层发出的请求Connection,返回值一定是最初过滤器请求的Connection对象,这样就保证了Connection对象的一致性)产生了异常(这里使用了Exception进行捕捉),这样一旦出现异常就进行回滚,否则就提交(在doFilter方法执行之前会首先调用方法setAutoCommit方法)。

    6.问题升级。

      (1)第一次刷新请求Servlet没有问题,但是第二次刷新请求Servlet出现问题,原因是什么?

        答:出现的问题是不能操作已经关闭的连接问题,因为一次事务结束之后就会关闭连接,这时候连接将会成为空值。

        代码验证:

    1 public static void main(String[] args) throws SQLException {
    2         Connection conn=DataSourceUtils_c3p0.getConnection();
    3         System.out.println(conn);
    4         conn.close();
    5         System.out.println(conn);
    6     }
    验证连接关闭之后连接成为空值的问题

    输出结果:

    1 com.mchange.v2.c3p0.impl.NewProxyConnection@10cccfb [wrapping: com.mysql.jdbc.JDBC4Connection@1658ade]
    2 com.mchange.v2.c3p0.impl.NewProxyConnection@10cccfb [wrapping: null]
    验证结果

      (2)解决方法

        答:在关闭连接的同时将和当前线程绑定的对象移除。使用remove方法。这样需要在DataSource_c3p0.java中添加一个remove方法,该方法实际上调用了ThreadLoal的remove方法。这样在下一次请求的时候,过滤器会重新向数据库连接池请求一个新的连接,同时该连接会和当前线程重新绑定关系,这样就不会发生空指针或者操作已关闭的Connection的情况了。

    7.源代码(两个核心类)。

      (1)数据库连接池类DataSource_c3p0.java(这里使用c3p0数据库连接池)。

     1 package com.kdyzm.dbutils;
     2 /**
     3  * 数据库连接池工具类。
     4  */
     5 import java.sql.Connection;
     6 import java.sql.SQLException;
     7 
     8 import javax.sql.DataSource;
     9 
    10 import com.mchange.v2.c3p0.ComboPooledDataSource;
    11 
    12 public class DataSourceUtils_c3p0 {
    13     private static ThreadLocal<Connection>tl=new ThreadLocal<Connection>();
    14     private static DataSource ds=null;
    15     static{
    16         ds=new ComboPooledDataSource("namedconfig");
    17     }
    18     public static Connection getConnection(){
    19         Connection conn=tl.get();
    20         if(conn==null)
    21         {
    22             try {
    23                 conn=ds.getConnection();
    24                 tl.set(conn);//这句代码十分重要,千万不能忘(将当前线程和请求的Connection对象绑定)
    25             } catch (SQLException e) {
    26                 e.printStackTrace();
    27             }
    28         }
    29         return conn;
    30     }
    31     public static DataSource getDataSource(){
    32         return ds;
    33     }
    34     public static void remove(){
    35         tl.remove();//这句代码也十分重要,千万不能忘掉,将会在TransactionFilter中调用
    36     }
    37 }    
    DataSource_c3p0.java类源代码

      (2)过滤器TransactionFiler.java

     1 package com.kdyzm.filter;
     2 
     3 import java.io.IOException;
     4 import java.sql.Connection;
     5 import java.sql.SQLException;
     6 
     7 import javax.servlet.Filter;
     8 import javax.servlet.FilterChain;
     9 import javax.servlet.FilterConfig;
    10 import javax.servlet.ServletException;
    11 import javax.servlet.ServletRequest;
    12 import javax.servlet.ServletResponse;
    13 
    14 import com.kdyzm.dbutils.DataSourceUtils_c3p0;
    15 
    16 public class TransactionFilter implements Filter{
    17 
    18     @Override
    19     public void init(FilterConfig filterConfig) throws ServletException {
    20         System.out.println("初始化事务过滤器!");
    21     }
    22 
    23     @Override
    24     public void doFilter(ServletRequest request, ServletResponse response,
    25             FilterChain chain) throws IOException, ServletException {
    26         System.out.println("拦截过滤:");
    27         Connection conn=null;
    28         try{
    29             conn=DataSourceUtils_c3p0.getConnection();
    30             conn.setAutoCommit(false);
    31             chain.doFilter(request, response);
    32             conn.commit();
    33         }
    34         catch(Exception e){
    35             System.out.println("出错了!回滚!!");
    36             try {
    37                 conn.rollback();
    38             } catch (SQLException e1) {
    39                 e1.printStackTrace();
    40             }
    41             e.printStackTrace();
    42         }
    43         finally{
    44             try {
    45                 conn.close();
    46                 DataSourceUtils_c3p0.remove();//这句代码十分重要,解决了两次刷新产生空指针异常、不能操作已经关闭的Connection的问题。
    47             } catch (SQLException e) {
    48                 e.printStackTrace();
    49             }
    50         }
    51     }
    52     @Override
    53     public void destroy() {
    54         System.out.println("销毁事务过滤器!");
    55     }
    56 
    57 }
    TransactionFilter.java类源代码

    8.扩展

      如果数据库的操作都成功了,只是JSP页面的一个/0的错误,这样的一个错误仍然会被过滤器拦截并导致数据库回滚,该怎样避免该问题的发生?

      答:在回滚之前判断异常类型,如果是SQLException类型的则回滚,否则不回滚。

  • 相关阅读:
    绪论
    Linux回到上次目录
    松下伺服电机控制器参数设置
    更新github上的文件
    pytorch
    从本地上传文件到github
    Linux常用命令
    使用colab训练神经网络
    深度学习模型训练过程
    anaconda安装ubuntu20.4中
  • 原文地址:https://www.cnblogs.com/kuangdaoyizhimei/p/4579163.html
Copyright © 2011-2022 走看看