Guava | Closer & JDBC Closer

今早讲师提到的一个point让我想做下com.google.common.io.Closer对Connection,PrepareStatment和ResultSet的可关闭性测试,然而误解了讲师的意思....

1
2
3
/**
 * 测试Closer对Connection,PrepareStatment和ResultSet的可关闭性
 *
 * 【测试结果:】
 */
@Test
public void testCloser() {
    Connection connection = DBUtil.getConnection();
    PreparedStatement preparedStatement = null;
    ResultSet resultSet = null;
    String sql = "SELECT id, book_id, author_id, book_name from t_books";

    Closer closer = Closer.create();
    closer.register(connection);

    // 数据查询
    try {
        preparedStatement = connection.prepareStatement(sql);
        resultSet = preparedStatement.executeQuery();
        while (resultSet.next()) {
            logger.info(resultSet.getString("book_name"));
        }
    } catch (SQLException e) {
        logger.error("从结果集中获取查询结果异常", e);
    }

    try {
        // 调用前的关闭情况
        logger.info("调用前的关闭情况");
        logger.info("ResultSet =>> {}", resultSet.isClosed());
        logger.info("PreparedStatement =>> {}", preparedStatement.isClosed());
        logger.info("Connection =>> {}", connection.isClosed());

        // 调用Closer
        closer.close();

        // 调用后的关闭情况
        logger.info("调用后的关闭情况");
        logger.info("ResultSet =>> {}", resultSet.isClosed());
        logger.info("PreparedStatement =>> {}", preparedStatement.isClosed());
        logger.info("Connection =>> {}", connection.isClosed());
    } catch (IOException e) {
        logger.error("Closer执行关闭异常", e);
    } catch (SQLException e) {
        logger.error("数据库异常", e);
    }
}
4

1、兼容Java6和Java7:为抛出try块中的异常而采取的不同的异常处理机制

为什么这么做

我们对异常处理的try-catch-finally语句块都比较熟悉。如果在try语句块中抛出了异常,在控制权转移到调用栈上一层代码之前,finally语句块中的语句也会执行。但是finally语句块在执行的过程中,也可能会抛出异常。如果finally语句块也抛出了异常,那么这个异常会往上传递,而之前try语句块中的那个异常就丢失了

比如下面这个例子:

public class DisappearedException {
    public void show() throws BaseException {
        try {
            Integer.parseInt("Hello");
        } catch (NumberFormatException e1) {
            throw new BaseException(e1);
        } finally {
            try {
                int result = 2 / 0;
            } catch (ArithmeticException e2) {
                throw new BaseException(e2);
            }
        }
    }
    public static void main(String[] args) throws Exception {
        DisappearedException d = new DisappearedException();
        d.show();
    }
}

class BaseException extends Exception {
    public BaseException(Exception ex){
        super(ex);
    }
    private static final long serialVersionUID = 3987852541476867869L;
}

对这种问题的解决办法一般有两种:一种是抛出try语句块中产生的原始异常,忽略在finally语句块中产生的异常。这么做的出发点是try语句块中的异常才是问题的根源。如例:

public class ReadFile {
    public static void main(String[] args) {
        ReadFile rf = new ReadFile();
        try {
            rf.read("F:/manifest_provider_loophole.txt");
        } catch (BaseException2 e) {
            e.printStackTrace();
        }
    }
    public void read(String filename) throws BaseException2 {
        FileInputStream input = null;
        IOException readException = null;
        try {
            input = new FileInputStream(filename);
        } catch (IOException ex) {
            readException = ex;
        } finally {
            if(input != null){
                try {
                    input.close();
                } catch (IOException ex2) {
                    if(readException == null){
                        readException = ex2;
                    }
                }
            }
            if(readException != null){
                throw new BaseException2(readException);
            }
        }
    }
}

class BaseException2 extends Exception {
    private static final long serialVersionUID = 5062456327806414216L;
    public BaseException2(Exception ex){
        super(ex);
    }
}

另外一种是把产生的异常都记录下来。这么做的好处是不会丢失任何异常。在java7之前,这种做法需要实现自己的异常类,而在java7中,已经对Throwable类进行了修改以支持这种情况。在java7中为Throwable类增加addSuppressed方法。当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过addSuppressed方法把这些被抑制的方法记录下来。被抑制的异常会出现在抛出的异常的堆栈信息中,也可以通过getSuppressed方法来获取这些异常。这样做的好处是不会丢失任何异常,方便开发人员进行调试。这种做法的关键在于把finally语句中产生的异常通过 addSuppressed方法加到try语句产生的异常中。

public class ReadFile2 {
    public static void main(String[] args) {
        ReadFile rf = new ReadFile();
        try {
            rf.read("F:/manifest_provider_loophole.txt");
        } catch (BaseException2 e) {
            e.printStackTrace();
        }
    }
    public void read(String filename) throws IOException {
        FileInputStream input = null;
        IOException readException = null;
        try {
            input = new FileInputStream(filename);
        } catch (IOException ex) {
            readException = ex;
        } finally {
            if(input != null){
                try {
                    input.close();
                } catch (IOException ex2) {
                    if(readException != null){
                        readException.addSuppressed(ex2);    //注意这里
                    }else{
                        readException = ex2;
                    }
                }
            }
            if(readException != null){
                throw readException;
            }
        }
    }
}
  • Running on Java 7, code using this should be approximately equivalent in behavior to the same code written with try-with-resources.

    如果在Java7上运行,希望代码使用try-with-resources结构,那么就要使用try-with-resources语法来写。

    Running on Java 6, exceptions that cannot be thrown must be logged rather than being added to the thrown exception as a suppressed exception.

    如果在Java6上运行,对不能抛出的异常的处理,将其记录总比将其作为被屏蔽的异常要好。

  • If a {@code Throwable} is thrown in the try block, no exceptions that occur when attempting to close resources will be thrown from the finally block. The throwable from the try block will be thrown.

    如果在try块中抛出了异常,那么这个异常将被抛出,然而在finally块中关闭资源时将不会抛出任何异常。

  • If no exceptions or errors were thrown in the try block, the <i>first</i> exception thrown by an attempt to close a resource will be thrown.

    如果try块中没有发生异常,那么第一个抛出的异常可能是尝试关闭资源的异常。

  • Any exception caught when attempting to close a resource that is not thrown (because another exception is already being thrown) is suppressed.

  • An exception that is suppressed is not thrown. The method of suppression used depends on the version of Java the code is running on:

    被屏蔽的异常将不会被抛出,屏蔽异常的方法将取决于代码运行的Java版本:

    Java 7+: Exceptions are suppressed by adding them to the exception that will be thrown using {@code Throwable.addSuppressed(Throwable)}.

    如果是在Java7+上运行:通过Throwable.addSuppressed(Throwable)方法将这个异常屏蔽。

    Java 6: Exceptions are suppressed by logging them instead.

    如果是在Java6上运行:将其输出到日志。

所以在Closer内部,有自己的异常处理机制,而且这种机制取决于代码所处的Java版本环境。
在具体实现上,Closer在初始化的时候就对异常处理机制也做了初始化的选择:

public static Closer create() {
    return new Closer(SUPPRESSOR);
}

SUPPRESSOR是一个内部接口Suppressor的实现类,具体用哪个实行A类取决于当前的Java版本。SUPPRESSOR被做了静态初始化:

/**
 * The suppressor implementation to use for the current Java version.
 */
private static final Suppressor SUPPRESSOR =
      SuppressingSuppressor.isAvailable()
          ? SuppressingSuppressor.INSTANCE
          : LoggingSuppressor.INSTANCE;

看一看Suppressor接口的定义:

  /**
   * Suppression strategy interface.
   */
  @VisibleForTesting
  interface Suppressor {
    /**
     * Suppresses the given exception ({@code suppressed}) which was thrown when attempting to close
     * the given closeable. {@code thrown} is the exception that is actually being thrown from the
     * method. Implementations of this method should not throw under any circumstances.
     */
    void suppress(Closeable closeable, Throwable thrown, Throwable suppressed);
  }

这个接口被叫做“屏蔽策略接口(Suppression strategy interface)”,也即根据Java版本来采取具体的屏蔽策略,而实现了这个接口的两个实现类LoggingSuppressor和SuppressingSuppressor正对应了两种不同的屏蔽策略。

  • 第一个实现类:SuppressingSuppressor.
    这个实现类适用于屏蔽Java7中的异常,根据能否通过反射获取到Throwable的addSuppressed方法来判断这种策略的可用性,而这个判断也被用于初始化Closer时Suppressor具体策略类选择的判断依据。
    在Throwable中定义了一个private List<Throwable> suppressedExceptions,而addSuppressed方法就是将异常加入到这个List当中。这个方法只有在Java7+当中提供,参考:Java7的异常处理新特性 - OPEN 开发经验库

    /**
     * Suppresses exceptions by adding them to the exception that will be thrown using JDK7's
     * addSuppressed(Throwable) mechanism.
     */
    @VisibleForTesting
    static final class SuppressingSuppressor implements Suppressor {
    
      static final SuppressingSuppressor INSTANCE = new SuppressingSuppressor();
    
      static boolean isAvailable() {
        return addSuppressed != null;
      }
    
      static final Method addSuppressed = getAddSuppressed();
    
      private static Method getAddSuppressed() {
        try {
          return Throwable.class.getMethod("addSuppressed", Throwable.class);
        } catch (Throwable e) {
          return null;
        }
      }
    
      @Override
      public void suppress(Closeable closeable, Throwable thrown, Throwable suppressed) {
        // ensure no exceptions from addSuppressed
        if (thrown == suppressed) {
          return;
        }
        try {
          addSuppressed.invoke(thrown, suppressed);
        } catch (Throwable e) {
          // if, somehow, IllegalAccessException or another exception is thrown, fall back to logging
          LoggingSuppressor.INSTANCE.suppress(closeable, thrown, suppressed);
        }
      }
    }
    
  • 第二个实现类是LoggingSuppressor,是将被屏蔽的异常以日志的方式输出,适用于Java6:

    /**
     * Suppresses exceptions by logging them.
     */
    @VisibleForTesting
    static final class LoggingSuppressor implements Suppressor {
    
      static final LoggingSuppressor INSTANCE = new LoggingSuppressor();
    
      @Override
      public void suppress(Closeable closeable, Throwable thrown, Throwable suppressed) {
        // log to the same place as Closeables
        Closeables.logger.log(
            Level.WARNING, "Suppressing exception thrown when closing " + closeable, suppressed);
      }
    }
    
总结:
  • 基于接口的、针对不同环境的异常处理策略的实现。

2、基于Stack的资源收集和关闭

(1) 初始化Stack

Closer中定义了一个Stack,用来收集待关闭的资源,需要注意的是,这些资源都 实现自Closeable接口,也就是说,只有实现自这个接口的资源才能被Closer收集和关闭。

// only need space for 2 elements in most cases, so try to use the smallest array possible
  private final Deque<Closeable> stack = new ArrayDeque<Closeable>(4);

stack初始容量为4,根据注释:一般情况下,需要2个大小的空间就足够了(需要关闭的资源数目一般不会超过2),这里use the smallest array possible.

(2) register

在栈顶加入一个带关闭资源closeable:

  /**
   * Registers the given {@code closeable} to be closed when this {@code Closer} is
   * {@linkplain #close closed}.
   *
   * @return the given {@code closeable}
   */
  // close. this word no longer has any meaning to me. <<-- 你是想说这里不会发生close吗?任性...
  @CanIgnoreReturnValue
  public <C extends Closeable> C register(@Nullable C closeable) {
    if (closeable != null) {
      stack.addFirst(closeable);
    }

    return closeable;
  }
(3) close

关闭stack中的所有已经注册的资源,注意顺序是LIFO,后加入的先关闭,先加入的后关闭:

  /**
   * Closes all {@code Closeable} instances that have been added to this {@code Closer}. If an
   * exception was thrown in the try block and passed to one of the {@code exceptionThrown} methods,
   * any exceptions thrown when attempting to close a closeable will be suppressed. Otherwise, the
   * <i>first</i> exception to be thrown from an attempt to close a closeable will be thrown and any
   * additional exceptions that are thrown after that will be suppressed.
   */
  @Override
  public void close() throws IOException {
    Throwable throwable = thrown;

    // close closeables in LIFO order
    while (!stack.isEmpty()) {
      Closeable closeable = stack.removeFirst();
      try {
        closeable.close();
      } catch (Throwable e) {
        if (throwable == null) {
          throwable = e;
        } else {
          suppressor.suppress(closeable, throwable, e);
        }
      }
    }

    if (thrown == null && throwable != null) {
      Throwables.propagateIfPossible(throwable, IOException.class);
      throw new AssertionError(throwable); // not possible
    }
  }

注意这里的异常处理,正体现了开头注释中所讲的那样:如果try块中,也就是close时发生了IO异常,那么这个异常就会被抛出;如果try块中没有发生异常,那么第一个抛出的异常可能是尝试关闭资源的异常。

3、参考Closer封装JDBC Closer

下面根据上面对Closer的分析,封装一个用于关闭JDBC中资源的Closer,注意:

  • JDBC中需要关闭的资源有:Connection、PrepareStatment和Resultset,他们的关闭顺序是上述逆序,而他们都实现自AutoCloseable接口;

  • 不同于Closer的是,考虑到JDBC数据库操作的特点,JdbcCloser中没有复杂的异常处理机制,所有异常都会被直接抛出。

      package com.qunar.fresh2017.db;
      
      import com.google.common.base.Preconditions;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      
      import java.util.ArrayDeque;
      import java.util.Deque;
      
      /**
       * Copyright(C),2005-2017,Qunar.com
       * version    date      author
       * ──────────────────────────────────
       * 1.0       17-3-12   wanlong.ma
       * Description:参考 com.google.common.io.Closer 的Jdbc连接关闭类
       *
       * Others:
       * Function List:
       * History:
       */
      public class JdbcCloser implements AutoCloseable {
          private static Logger LOGGER = LoggerFactory.getLogger(JdbcCloser.class);
      
          private JdbcCloser(){}
      
          public static JdbcCloser create(){
              return new JdbcCloser();
          }
      
          // 一般JDBC需要关闭的资源只有3个,所以这里初始化容量为3
          private static Deque<AutoCloseable> stack = new ArrayDeque<>(3);
      
          /**
           * 收集待关闭的数据库资源到Stack中,并返回这个资源
           * @param autoCloseable
           * @param <A>
           * @return
           */
          public <A extends AutoCloseable> A register(A autoCloseable){
              Preconditions.checkNotNull(autoCloseable);
              if(autoCloseable != null){
                  stack.addFirst(autoCloseable);
              }
              return autoCloseable;
          }
      
          /**
           * 以LIFO的顺序从Stack中关闭资源
           * @throws Exception
           */
          @Override
          public void close() {
              do{
                  AutoCloseable ac = stack.removeFirst();
                  try {
                      ac.close();
                  } catch (Exception e) {
                      LOGGER.error("关闭时发生异常",e);
                  }
              }while (!stack.isEmpty());
          }
      }
    

测试类:

    package com.qunar.fresh2017;
    
    import com.qunar.fresh2017.db.DBUtil;
    import com.qunar.fresh2017.db.JdbcCloser;
    import junit.framework.TestCase;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    
    /**
     * Copyright(C),2005-2017,Qunar.com
     * version    date      author
     * ──────────────────────────────────
     * 1.0       17-3-12   wanlong.ma
     * Description:
     * Others:
     * Function List:
     * History:
     */
    public class JdbcCloserTest extends TestCase {
        public void testJdbcCloser() throws SQLException {
            String sql = "SELECT * FROM t_books";
            Logger logger = LoggerFactory.getLogger(JdbcCloserTest.class);
    
            JdbcCloser jdbcCloser = JdbcCloser.create();
    
            Connection connection = jdbcCloser.register(DBUtil.getConnection());
            PreparedStatement preparedStatement = jdbcCloser.register(connection.prepareStatement(sql));
            ResultSet resultSet = jdbcCloser.register(preparedStatement.executeQuery());
    
            logger.info("ResultSet是否关闭:{}",resultSet.isClosed());
            logger.info("PreparedStatement是否关闭:{}",preparedStatement.isClosed());
            logger.info("Connection是否关闭:{}",connection.isClosed());
    
            jdbcCloser.close();
    
            logger.info("ResultSet是否关闭:{}",resultSet.isClosed());
            logger.info("PreparedStatement是否关闭:{}",preparedStatement.isClosed());
            logger.info("Connection是否关闭:{}",connection.isClosed());
        }
    }

运行结果:

ResultSet是否关闭:false
PreparedStatement是否关闭:false
Connection是否关闭:false

ResultSet是否关闭:true
PreparedStatement是否关闭:true
Connection是否关闭:true
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,219评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,363评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,933评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,020评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,400评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,640评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,896评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,597评论 0 199
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,327评论 1 244
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,581评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,072评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,399评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,054评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,083评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,849评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,672评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,585评论 2 270

推荐阅读更多精彩内容

  •  try-with-resources语句是一个声明一个或多个资源的 try 语句。一个资源作为一个对象,必须在程...
    xdoyf阅读 3,420评论 0 0
  • 初识异常(Exception) 比如我们在取数组里面的某个值得时候,经常会出现定义的取值范围超过了数组的大小,那么...
    iDaniel阅读 1,843评论 1 2
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,295评论 18 399
  • 前言 自学了4个多月的安卓,试着写了一个小程序,虽然功能按照预想基本实现了,但是也很清楚代码质量肯定不好。在...
    maxwellyue阅读 59,428评论 2 16
  • 时间流逝,78年出生的我,人已过半百,从出生到2015年每天在茫茫然中度过。 梦想和我好像从未接触过,目标似乎与我...
    胡芳叶梦之蓝阅读 1,293评论 0 0