今早讲师提到的一个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
网友评论