你这个资源又没释放——Java中如何优雅地关闭资源

举报
开发者学堂小助 发表于 2017/08/29 19:46:48 2017/08/29
【摘要】 关于Java中的资源关闭,是一个常见的问题,也是最容易被初级程序员忽略的一个问题,这个问题的严重性,吃过亏的人都知道,不需多说。之所以会出现这个问题,主要还是在Java 7之前,语言没有很好地提供资源管理的语法。

关于Java中的资源关闭,是一个常见的问题,也是最容易被初级程序员忽略的一个问题,这个问题的严重性,吃过亏的人都知道,不需多说。之所以会出现这个问题,主要还是在Java 7之前,语言没有很好地提供资源管理的语法。我们先看下面的代码:

Java 代码
01InputStream inputStream = null;
02Workbook wb = null
03Connection con = null;
04PreparedStatement ps = null;
05try {
06    inputStream = new FileInputStream("c:/tmp/a.txt");
07    wb = new XSSFWorkbook(inputStream);
08    con = ServiceLocator.getInstance().getDataSource("jdbc/xxxDS").getConnection();
09    ps = con.prepareStatement(sql);
10    //do business process
11} catch (Exception e) {
12    logger.error(e.getMessage());
13    throw e;
14} finally {
15    if(inputStream != null){
16        try {
17            inputStream.close();
18        } catch (Exception e) {
19            logger.error("Exception "+e.getMessage(),e);
20 
21        }
22    }
23    if(wb != null){
24        try {
25            wb.close();
26        } catch (IOException e) {
27            logger.error("Exception " + e.getMessage(), e);
28        }
29     }
30    if(ps != null){
31        try {
32            ps.close();
33        } catch (SQLException e) {
34            logger.error("Exception "+e.getMessage(),e);
35        }
36    }
37    if(con != null){
38        try {
39            con.close();
40        } catch (SQLException e) {
41            logger.error("Exception "+e.getMessage(),e);
42        }
43    }
44}

声明的资源必须放在try语句块的外面,最后在finally中关闭。这里存在很多容易导致问题的地方:

1、声明的资源在try语句块中打开后,忘记在finally中关闭;

2、关闭资源的代码没有放在try-catch块中,一旦牵涉到多种资源的关闭,前面的抛异常,后面的被跳过,导致对应的关闭代码没有执行;

3、如果资源的关闭没有放在finally中,也会导致打开的资源没有正常关闭。

这些问题比较隐蔽,如果代码相对比较复杂,就非常容易被忽略。还有一个问题,就是代码很长,非常丑陋,非常不利于维护。当然,这个问题很容易想到解决方案,就是把资源关闭的代码抽出来,形成一个公共的工具方法,就像下面这样:

-
Java 代码
01InputStream inputStream = null;
02Workbook wb = null;
03Connection con = null;
04PreparedStatement ps = null;
05try {
06    inputStream = new FileInputStream("c:/tmp/a.txt");
07    wb = new XSSFWorkbook(inputStream);
08    con = ServiceLocator.getInstance().getDataSource("jdbc/xxxDS").getConnection();
09    ps = con.prepareStatement(sql);
10    //do business process
11} catch (Exception e) {
12    logger.error(e.getMessage());
13    throw e;
14} finally {
15    close(inputStream);
16    close(wb);
17    if (ps != null) {
18        try {
19            ps.close();
20        } catch (SQLException e) {
21            logger.error("Exception " + e.getMessage(), e);
22        }
23    }
24    if (con != null) {
25        try {
26            con.close();
27        } catch (SQLException e) {
28            logger.error("Exception " + e.getMessage(), e);
29        }
30    }
31}
32 
33public static void  close(Closeable cloneable) {
34    if (cloneable != null) {
35        try {
36            cloneable.close();
37        } catch (Exception e) {
38            logger.error("Exception " + e.getMessage(), e);
39        }
40    }
41}

由于java.sql.Connection,java.sql.PreparedStatement都没有实现java.io.Closeable接口,所以,这个公共的方法使用不了,当然,我们重新再定义带这两种参数的重载方法就行了,就像下面这样:

-
Java 代码
01public static void  close(Connection cloneable) {
02    if (cloneable != null) {
03        try {
04            cloneable.close();
05        } catch (Exception e) {
06            logger.error("Exception " + e.getMessage(), e);
07        }
08    }
09}
10 
11public static void  close(PreparedStatement cloneable) {
12    if (cloneable != null) {
13        try {
14            cloneable.close();
15        } catch (Exception e) {
16            logger.error("Exception " + e.getMessage(), e);
17        }
18    }
19}

最后,我们的代码像下面的样子:

-
Java 代码
01InputStream inputStream = null;
02Workbook wb = null;
03Connection con = null;
04PreparedStatement ps = null;
05try {
06    inputStream = new FileInputStream("c:/tmp/a.txt");
07    wb = new XSSFWorkbook(inputStream);
08    con = ServiceLocator.getInstance().getDataSource("jdbc/xxxDS").getConnection();
09    ps = con.prepareStatement(sql);
10    //do business process
11} catch (Exception e) {
12    logger.error(e.getMessage());
13    throw e;
14} finally {
15    close(inputStream);
16    close(wb);
17    close(ps);
18    close(con);
19}

OK,这已经比开始简化了不少,如果没有更进一步的追求,到此打住也无可厚非。但我们再仔细研究下就会发现依然存在以下问题:

1、待关闭的资源必须要在最外层声明,有多少种就要声明多少次,这和Java变量声明的原则(哪里用哪里声明)不一致;

2、声明多少次,就要调用多少次close,还是容易遗漏

那最好的方案是什么呢?请看下面的代码: 

-
Java 代码
01MyCloser closer = MyCloser.create();        
02try {
03    InputStream inputStream = closer.register(new FileInputStream("c:/tmp/a.txt"));
04    Workbook wb = closer.register(new XSSFWorkbook(inputStream));
05    DataSource dataSource = ServiceLocator.getInstance().getDataSource("jdbc/xxxDS");
06    Connection con = closer.register(dataSource.getConnection());
07    PreparedStatement ps = closer.register(con.prepareStatement(sql));
08    // do business process
09} catch (Exception e) {
10    logger.error(e.getMessage());
11    throw e;
12} finally {
13    closer.close();
14}

引入MyCloser,变量用的时候再声明,不用放到最外面,资源的关闭,在finally中一行代码解决。下面是MyCloser的代码:

-
Java 代码
001import java.io.Closeable;
002import java.io.IOException;
003import java.sql.Connection;
004import java.sql.ResultSet;
005import java.sql.SQLException;
006import java.sql.Statement;
007 
008import com.google.common.io.Closer;
009 
010/**
011* com.google.common.io.Closer的扩展,对常见的非java.io.Closeable资源<br>
012* 进行了适配,实现资源的注册和集中关闭,以简化客户端代码。
013*
014* @author z00315905
015* @since 2016-10-12
016*/
017public class MyCloser {
018    private final Closer closer;
019     
020    private MyCloser(Closer closer) {
021        this.closer = closer;
022    }
023 
024    public static MyCloser create(){
025        return new MyCloser(Closer.create());
026    }
027     
028    public Connection register(final Connection connection) {
029        closer.register(new Closeable() {
030            @Override
031            public void close() throws IOException {
032                try {
033                    connection.close();
034                } catch (SQLException e) {
035                    throw new IOException(e);
036                }
037            }
038        });
039        return connection;
040    }
041     
042    public <S extends Statement> S register(final S statement) {
043        closer.register(new Closeable() {
044            @Override
045            public void close() throws IOException {
046                try {
047                    statement.close();
048                } catch (SQLException e) {
049                    throw new IOException(e);
050                }
051            }
052        });
053        return statement;
054    }
055     
056    public ResultSet register(final ResultSet resultSet){
057        closer.register(new Closeable() {            
058            @Override
059            public void close() throws IOException {
060                try {
061                    resultSet.close();
062                } catch (SQLException e) {
063                    throw new IOException(e);
064                }
065            }
066        });
067        return resultSet;
068    }
069 
070    public void close() throws IOException {
071        closer.close();
072    }
073 
074    public <C extends Closeable> C register(C closeable) {
075        return closer.register(closeable);
076    }
077 
078    public RuntimeException rethrow(Throwable e) throws IOException {
079        return closer.rethrow(e);
080    }
081 
082    public <X extends Exception> RuntimeException rethrow(Throwable e, Class<X> declaredType) throws IOException, X {
083        return closer.rethrow(e, declaredType);
084    }
085 
086    public <X1 extends Exception, X2 extends Exception> RuntimeException rethrow(Throwable e, Class<X1> declaredType1,
087            Class<X2> declaredType2) throws IOException, X1, X2 {
088        return closer.rethrow(e, declaredType1, declaredType2);
089    }
090 
091    public boolean equals(Object o) {
092        return closer.equals(o);
093    }
094 
095    public int hashCode() {
096        return closer.hashCode();
097    }
098     
099    public String toString() {
100        return closer.toString();
101    }
102}

这是一个典型的适配器模式,因为Google的Guava框架提供的Closer资源管理器只支持实现了java.io.Closeable的资源,对于像java.sql包中的资源,都没有实现该接口,因此,MyCloser提供了对应的适配,使所有的资源管理模式一致。在Java 7之前,这应该是最优雅的资源管理方案。

在Java 7及之后,我们可以使用最新的资源管理语法try-with-resource,上面的代码可以这么写:

-
Java 代码
01try(
02    InputStream inputStream = new FileInputStream("c:/tmp/a.txt");
03    Workbook wb = new XSSFWorkbook(inputStream);
04    Connection con = ServiceLocator.getInstance().getDataSource("jdbc/xxxDS").getConnection();
05    PreparedStatement ps = con.prepareStatement(sql);){
06    //do business process
07}catch (Exception e) {
08    logger.error(e.getMessage());
09    throw e;
10}

可以看到,我们不用关心资源的关闭了,只要在try()中声明即可,这样的代码是最简洁也是最具表现力的,如果生产环境支持Java 7,最先考虑的应该是这个方案。

当然,由于历史原因,我们可能用到了一些第三方的包,牵涉到资源关闭,但对应的类却没有实现java.lang.AutoCloseable接口,当然也就不能使用try-with-resource语法来操作了,这时,有两种办法,一是提供一个适配的子类,实现java.lang.AutoCloseable接口,在其close中实现资源关闭逻辑,这样就能使用try-with-resource语法了;第二种就是使用MyCloser方案。推荐第一种,因为更简单。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

举报
请填写举报理由
0/200