• Java 利用 ByteArrayOutputStream 和 ByteArrayInputStream 避免重复读取配置文件


    最近参与了github上的一个开源项目 Mycat,是一个mysql的分库分表的中间件。发现其中读取配置文件的代码,存在频繁多次重复打开,读取,关闭的问题,代码写的很初级,稍微看过一些框架源码的人,是不会犯这样的错误的。于是对其进行了一些优化。

    优化之前的代码如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    private static Element loadRoot() {
        InputStream dtd = null;
        InputStream xml = null;
        Element root = null;
        try {
            dtd = ConfigFactory.class.getResourceAsStream("/mycat.dtd");
            xml = ConfigFactory.class.getResourceAsStream("/mycat.xml");
            root = ConfigUtil.getDocument(dtd, xml).getDocumentElement();
        catch (ConfigException e) {
            throw e;
        catch (Exception e) {
            throw new ConfigException(e);
        finally {
            if (dtd != null) {
                try {
                    dtd.close();
                catch (IOException e) { }
            }
            if (xml != null) {
                try {
                    xml.close();
                catch (IOException e) { }
            }
        }
        return root;
    }

    然后其它方法频繁调用 loadRoot():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Override
    public UserConfig getUserConfig(String user) {
        Element root = loadRoot();
        loadUsers(root);
        return this.users.get(user);
    }
     
    @Override
    public Map<String, UserConfig> getUserConfigs() {
        Element root = loadRoot();
        loadUsers(root);
        return users;
    }
     
    @Override
    public SystemConfig getSystemConfig() {
        Element root = loadRoot();
        loadSystem(root);
        return system;
    }
    // ... ...

    ConfigUtil.getDocument(dtd, xml) 方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
        public static Document getDocument(final InputStream dtd, InputStream xml) throws ParserConfigurationException,
                SAXException, IOException {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    //factory.setValidating(false);
            factory.setNamespaceAware(false);
            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setEntityResolver(new EntityResolver() {
                @Override
                public InputSource resolveEntity(String publicId, String systemId) {
                    return new InputSource(dtd);
                }
            });
            builder.setErrorHandler(new ErrorHandler() {
                @Override
                public void warning(SAXParseException e) {
                }
                @Override
                public void error(SAXParseException e) throws SAXException {
                    throw e;
                }
                @Override
                public void fatalError(SAXParseException e) throws SAXException {
                    throw e;
                }
            });
            return builder.parse(xml);
        }

    显然这不是很好的处理方式。因为会多次重复读取配置文件。

    1. 第一次优化:

    为什么不读取一次,然后缓存起来呢?然后其它方法在调用 loadRoot() 时,就直接使用缓存中的就行了。但是遇到一个问题,InputStream 是不能被缓存,然后重复读取的,因为 InputStream 一旦被读取之后,其 pos 指针,等等都会发生变化,无法进行重复读取。所以只能将配置文件的内容读取处理,放入  byte[] 中缓存起来,然后配合 ByteArrayOutputStream,就可以重复读取 byte[] 缓存中的内容了。然后利用 ByteArrayOutputStream 来构造 InputStream 就达到了读取配置文件一次,然后重复构造 InputStream 进行重复读取,相关代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 为了避免原代码中频繁调用 loadRoot 去频繁读取 /mycat.dtd 和 /mycat.xml,所以将两个文件进行缓存,
    // 注意这里并不会一直缓存在内存中,随着 LocalLoader 对象的回收,缓存占用的内存自然也会被回收。
    private static byte[] xmlBuffer = null;
    private static byte[] dtdBuffer = null;
    private static  ByteArrayOutputStream xmlBaos = null;
    private static  ByteArrayOutputStream dtdBaos = null;
     
    static {
        InputStream input = ConfigFactory.class.getResourceAsStream("/mycat.dtd");
        if(input != null){
            dtdBuffer = new byte[1024 512];
            dtdBaos = new ByteArrayOutputStream();
            bufferFileStream(input, dtdBuffer, dtdBaos);
        }
         
        input = ConfigFactory.class.getResourceAsStream("/mycat.xml");
        if(input != null){
            xmlBuffer = new byte[1024 512];
            xmlBaos = new ByteArrayOutputStream();
            bufferFileStream(input, xmlBuffer, xmlBaos);
        }
    }

    bufferFileStream 方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    private static void bufferFileStream(InputStream input, byte[] buffer, ByteArrayOutputStream baos){
        int len = -1;
        try {
            while ((len = input.read(buffer)) > -1 ) {
                baos.write(buffer, 0, len);
            }
            baos.flush();
        catch (IOException e) {
            e.printStackTrace();
            logger.error(" bufferFileStream error: " + e.getMessage());
        }
    }

     loadRoat 优化之后如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    private static Element loadRoot() {
        Element root = null;
        InputStream mycatXml = null;
        InputStream mycatDtd = null;
         
        if(xmlBaos != null)
            mycatXml = new ByteArrayInputStream(xmlBaos.toByteArray());
        if(dtdBaos != null)
            mycatDtd = new ByteArrayInputStream(dtdBaos.toByteArray());
         
        try {
        root = ConfigUtil.getDocument(mycatDtd, mycatXml).getDocumentElement();
        catch (ParserConfigurationException | SAXException | IOException e1) {
            e1.printStackTrace();
            logger.error("loadRoot error: " + e1.getMessage());
        }finally{
            if(mycatXml != null){
                try { mycatXml.close(); } catch (IOException e) {}
            }
            if(mycatDtd != null){
                try { mycatDtd.close(); } catch (IOException e) {}
            }
        }
         
        return root;
    }

    这样优化之后,即使有很多方法频繁调用 loadRoot() 方法,也不会重复读取配置文件了,而是使用 byte[] 内容,重复构造 InputStream 而已。

    其实其原理,就是利用 byte[] 作为一个中间容器,对byte进行缓存,ByteArrayOutputStream 将 InputStream 读取的 byte 存放如 byte[]容器,然后利用 ByteArrayInputStream 从 byte[]容器中读取内容,构造 InputStream,只要 byte[] 这个缓存容器存在,就可以多次重复构造出 InputStream。 于是达到了读取一次配置文件,而重复构造出InputStream,避免了每构造一次InputStream,就读取一次配置文件的问题。

    2. 第二次优化:

    可能你会想到更好的方法,比如:

    为什么我们不将 private static Element root = null; 作为类属性,缓存起来,这样就不需要重复打开和关闭配置文件了,修改如下:

    复制代码
    public class LocalLoader implements ConfigLoader {
        private static final Logger logger = LoggerFactory.getLogger("LocalLoader");
        // ... ..    
        private static Element root = null;
    
        // 然后 loadRoot 方法改为:
        private static Element loadRoot() {
            InputStream dtd = null;
            InputStream xml = null;
    //        Element root = null;
            if(root == null){
                try {
                    dtd = ConfigFactory.class.getResourceAsStream("/mycat.dtd");
                    xml = ConfigFactory.class.getResourceAsStream("/mycat.xml");
                    root = ConfigUtil.getDocument(dtd, xml).getDocumentElement();
                } catch (ConfigException e) {
                    throw e;
                } catch (Exception e) {
                    throw new ConfigException(e);
                } finally {
                    if (dtd != null) {
                        try {
                            dtd.close();
                        } catch (IOException e) { }
                    }
                    if (xml != null) {
                        try {
                            xml.close();
                        } catch (IOException e) { }
                    }
                }
            }
            
            return root;
        }
    复制代码

    这样就不需要也不会重复 打开和关闭配置文件了。只要 root 属性没有被回收,那么 root 引入的 Document 对象也会在缓存中。这样显然比第一次优化要好很多,因为第一次优化,还是要从 byte[] 重复构造 InputStream, 然后重复 build 出 Document 对象。

    3. 第三次优化

    上面是将 private static Element root = null; 作为一个属性进行缓存,避免重复读取。那么我们干嘛不直接将 Document 对象作为一个属性,进行缓存呢。而且具有更好的语义,代码更好理解。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class LocalLoader implements ConfigLoader {
        private static final Logger logger = LoggerFactory.getLogger("LocalLoader");
        // ... ...
        // 为了避免原代码中频繁调用 loadRoot 去频繁读取 /mycat.dtd 和 /mycat.xml,所以将 Document 进行缓存,
        private static Document document = null;
     
        private static Element loadRoot() {
            InputStream dtd = null;
            InputStream xml = null;       
            if(document == null){
               try {
                    dtd = ConfigFactory.class.getResourceAsStream("/mycat.dtd");
                    xml = ConfigFactory.class.getResourceAsStream("/mycat.xml");
                    document = ConfigUtil.getDocument(dtd, xml);
                    return document.getDocumentElement();
                catch (Exception e) {
                    logger.error(" loadRoot error: " + e.getMessage());
                    throw new ConfigException(e);
                finally {
                    if (dtd != null) {
                        try { dtd.close(); } catch (IOException e) { }
                    }
                    if (xml != null) {
                        try { xml.close(); } catch (IOException e) { }
                    }
                }
            }       
            return document.getDocumentElement();
        }

    这样才是比较合格的实现。anyway, 第一种优化,学习到了 ByteArrayOutputStream 和 ByteArrayInputStream 同 byte[] 配合使用的方法。

    ---------------------分割线------------------------------------

    参考文章:http://blog.csdn.net/it_magician/article/details/9240727 原文如下:

    有时候我们需要对同一个InputStream对象使用多次。比如,客户端从服务器获取数据 ,利用HttpURLConnection的getInputStream()方法获得Stream对象,这时既要把数据显示到前台(第一次读取),又想把数据写进文件缓存到本地(第二次读取)。

    但第一次读取InputStream对象后,第二次再读取时可能已经到Stream的结尾了(EOFException)或者Stream已经close掉了。

    而InputStream对象本身不能复制,因为它没有实现Cloneable接口。此时,可以先把InputStream转化成ByteArrayOutputStream,后面要使用InputStream对象时,再从ByteArrayOutputStream转化回来就好了。代码实现如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    InputStream input =  httpconn.getInputStream();
                     
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int len;
    while ((len = input.read(buffer)) > -1 ) {
        baos.write(buffer, 0, len);
    }
    baos.flush();             
     
    InputStream stream1 = new ByteArrayInputStream(baos.toByteArray());
     
    //TODO:显示到前台
     
    InputStream stream2 = new ByteArrayInputStream(baos.toByteArray());
     
    //TODO:本地缓存
  • 相关阅读:
    自助Linux之问题诊断工具strace
    Linux系统与程序监控工具atop教程
    Google C++单元测试框架(Gtest)系列教程之三——测试固件(Test fixture)
    SQLServer2005:在执行批处理时出现错误。错误消息为: 目录名无效
    无法为可更新的订阅设置发布服务器登录名
    忘记SQL SERVER帐户sa的密码
    SQL Server 2008 R2 跟踪标志
    sys.dm_os_volume_stats监控物理磁盘
    SQL SERVER 中常见的高可用方案
    Vim操作的四种模式
  • 原文地址:https://www.cnblogs.com/jmsjh/p/7864821.html
Copyright © 2020-2023  润新知