谈谈代码:Java IO业务代码优化之路

简介: 前阵子休息天日常在寻找项目里不好的代码,看到了这样的一段代码...
版本 日期 备注
1.0 2019.4.27 文章首发
1.1 2021.5.21 修改标题:从一段代码谈起——浅谈JavaIO接口-> 谈谈代码:Java IO业务代码优化之路
1.1 2022.2.14 部分章节调整

1.前言

前阵子休息天日常在寻找项目里不好的代码,看到了这样的一段代码:

    private Result sshSameExec(Session session, String cmd) {
   
   
        if (log.isDebugEnabled()) {
   
   
            log.debug("shell command: {}", cmd);
        }
        UserInfo ui = getUserInfo();
        session.setUserInfo(ui);
        int exitStatus = 0;
        StringBuilder builder = new StringBuilder();
        ChannelExec channel;
        InputStream in;
        InputStream err;
        try {
   
   
            session.connect(connectTimeout);
            channel = (ChannelExec) session.openChannel("exec");
            channel.setCommand(cmd);
            in = channel.getInputStream();
            err = channel.getErrStream();
            channel.connect();
        } catch (Exception e) {
   
   
            throw new CloudRuntimeException(e);
        }

        try {
   
   
            long lastRead = Long.MAX_VALUE;
            byte[] tmp = new byte[1024];
            while (true) {
   
   
                while (in.available() > 0 || err.available() > 0) {
   
   
                    int i = 0;
                    if (in.available() > 0) {
   
   
                        i = in.read(tmp, 0, 1024);
                    } else if (err.available() > 0) {
   
   
                        i = err.read(tmp, 0, 1024);
                    }
                    if (i < 0) {
   
   
                        break;
                    }
                    lastRead = System.currentTimeMillis();
                    builder.append(new String(tmp, 0, i));
                }
                if (channel.isClosed()) {
   
   
                    if (in.available() > 0) {
   
   
                        continue;
                    }
                    exitStatus = channel.getExitStatus();
                    break;
                }
                if (System.currentTimeMillis() - lastRead > exeTimeout) {
   
   
                    break;
                }
            }
        } catch (IOException e) {
   
   
            throw new CloudRuntimeException(e);
        } finally {
   
   
            channel.disconnect();
            session.disconnect();
        }

        if (0 != exitStatus) {
   
   
            return Result.createByError(ErrorData.builder()
                    .errorCode(ResultCode.EXECUTE_SSH_FAIL.getCode())
                    .detail(builder.toString())
                    .title(ResultCode.EXECUTE_SSH_FAIL.toString())
                    .build());
        } else {
   
   
            return Result.createBySuccess(builder.toString());
        }
    }

简单解释一下这段代码——即通过ssh到一台机器上,然后执行一些命令.对命令输出的东西,开了一个循环,每一次读一定的位置,然后以字节流的形式读回来.

这段代码有点丑,于是我闻到了学习的味道.

首先是对两个Stream的消费,很显然,在多核环境下,我们同时也只能够消费其中一个Stream.其次,这代码太挫了,自己定义一个tmp,然后1024、1024这样的去取出来.

在改良之前,我们先来回顾一下JavaIO的接口定义.

2.JavaIO 接口知识回顾

2.1 低级抽象接口:InputStream 和 OutputStream

这里有同学可能问了,为啥叫它低抽象接口呢?因为它离底层太近了,计算机本来就是处理二进制的,而这两个接口正是用来处理二进制数据流的.

先简单看一眼这两个接口:

  • InputStream

    **
    * This abstract class is the superclass of all classes representing
    * an input stream of bytes.
    *
    * <p> Applications that need to define a subclass of <code>InputStream</code>
    * must always provide a method that returns the next byte of input.
    *
    * @author  Arthur van Hoff
    * @see     java.io.BufferedInputStream
    * @see     java.io.ByteArrayInputStream
    * @see     java.io.DataInputStream
    * @see     java.io.FilterInputStream
    * @see     java.io.InputStream#read()
    * @see     java.io.OutputStream
    * @see     java.io.PushbackInputStream
    * @since   JDK1.0
    */
    public abstract class InputStream implements Closeable {
         
         .....}
    
  • OutputStream

    /**
    * This abstract class is the superclass of all classes representing
    * an output stream of bytes. An output stream accepts output bytes
    * and sends them to some sink.
    * <p>
    * Applications that need to define a subclass of
    * <code>OutputStream</code> must always provide at least a method
    * that writes one byte of output.
    *
    * @author  Arthur van Hoff
    * @see     java.io.BufferedOutputStream
    * @see     java.io.ByteArrayOutputStream
    * @see     java.io.DataOutputStream
    * @see     java.io.FilterOutputStream
    * @see     java.io.InputStream
    * @see     java.io.OutputStream#write(int)
    * @since   JDK1.0
    */
    public abstract class OutputStream implements Closeable, Flushable {
         
         ...}
    

我们可以发现,它们都实现了Closeable的接口.因此大家在使用这些原生类时,要注意在结束时调用Close方法哦.

这两个接口的常用实现类有:

  • FileInputStreamFileOutputStream
  • DataInputStreamDataOutputStream
  • ObjectInputStreamObjectOutputStream

2.2 高级抽象接口——Writer和Reader

为啥说它是高级抽象接口呢?我们先来看看它们的注释:

  • Writer
    ```
    /**
    • Abstract class for writing to character streams. The only methods that a
    • subclass must implement are write(char[], int, int), flush(), and close().
    • Most subclasses, however, will override some of the methods defined here in
    • order to provide higher efficiency, additional functionality, or both.
      *
    • @see Writer
    • @see BufferedWriter
    • @see CharArrayWriter
    • @see FilterWriter
    • @see OutputStreamWriter
    • @see FileWriter
    • @see PipedWriter
    • @see PrintWriter
    • @see StringWriter
    • @see Reader
      *
    • @author Mark Reinhold
    • @since JDK1.1
      */

public abstract class Writer implements Appendable, Closeable, Flushable {

- Reader

/**

  • Abstract class for reading character streams. The only methods that a
  • subclass must implement are read(char[], int, int) and close(). Most
  • subclasses, however, will override some of the methods defined here in order
  • to provide higher efficiency, additional functionality, or both.
  • @see BufferedReader
  • @see LineNumberReader
  • @see CharArrayReader
  • @see InputStreamReader
  • @see FileReader
  • @see FilterReader
  • @see PushbackReader
  • @see PipedReader
  • @see StringReader
  • @see Writer
    *
  • @author Mark Reinhold
  • @since JDK1.1
    */

public abstract class Reader implements Readable, Closeable {


我们可以看到,这个抽象类是用来面向`character`的,也就是字符.字符的抽象等级必然比字节高,因为字符靠近上层,即人类.

## 2.3 优化输入和输出——Buffered
如果我们直接使用上述实现类去打开一个文件(如`FileWriter `、`FileReader `、`FileInputStream `、`FileOutputStream `),对其对象调用`read`、`write`、`readLine`等,每个请求都是由基础OS直接处理的,这会使一个程序效率低得多——因为它们都会引发磁盘访问or网络请求等.

为了减少这种开销,Java 平台实现缓冲 I/O 流。缓冲输入流从被称为缓冲区(buffer)的存储器区域读出数据;仅当缓冲区是空时,本地输入 API 才被调用。同样,缓冲输出流,将数据写入到缓存区,只有当缓冲区已满才调用本机输出 API。

用于包装非缓存流的缓冲流类有4个:`BufferedInputStream`和`BufferedOutputStream·用于创建字节缓冲字节流, `BufferedReader`和`BufferedWriter`用于创建字符缓冲字节流.

# 3. 着手优化
之前,我们提到了这段代码写得搓的地方:
- 首先是对两个Stream的消费,很显然,在多核环境下,我们同时也只能够消费其中一个Stream.
- 其次,这代码太挫了,自己定义一个tmp,然后1024、1024这样的去取出来.

故此,我们可以考虑对每个Stream都进行包装,支持用线程去消费,其次我们可以用高级抽象分接口去**适配**Byte,然后去**装饰**成Buffer.


接下来,我们来看一段ZStack里的工具类`ShellUtils`,为了节省篇幅,我们仅仅截出它在IDE里的`Structure`:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3ac3d36e5b744bb2a9c634acc2de54a2~tplv-k3u1fbpfcp-zoom-1.image)

run方法的核心:
![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6cdd0aed02b441bbf66cd06dd7848f7~tplv-k3u1fbpfcp-zoom-1.image)

我们可以看到`StreamConsumer`这个类,我们来看一下它的代码:
private static class StreamConsumer extends Thread {
    final InputStream in;
    final PrintWriter out;
    final boolean flush;

    StreamConsumer(InputStream in, PrintWriter out, boolean flushEveryWrite) {
        this.in = in;
        this.out = out;
        flush = flushEveryWrite;
    }

    @Override
    public void run() {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(in));
            String line;
            while ( (line = br.readLine()) != null) {
                out.println(line);
                if (flush) {
                    out.flush();
                }
            }
        } catch (Exception e) {
            logger.warn(e.getMessage(), e);
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
                logger.warn(e.getMessage(), e);
            }
        }
    }
}

这段代码已经达到了我们的理想状态:线程消费,高级抽象.

## 3.1 使用Kotlin 

## 3.1.1 Kotlin IO
闲话不多说,先贴代码为敬:
```kotlin
import java.io.InputStream
import java.io.InputStreamReader

class StreamGobbler(private val inputStream: InputStream, private var result: StringBuilder) : Runnable {

    override fun run() {
        InputStreamReader(inputStream).buffered().use {
            it.lines().forEach { r -> result.append(r) }
        }
    }
}

还是一样熟悉的配方,我们逐行来解读:

  1. 定义一个类,并且要求构造函数必须传入InputStream和一个StringBuilder.且实现了Runnable接口,这意味着它可以被线程消费.
  2. 覆写run方法.我们可以看到InputStream被适配成了InputStreamReader,这意味着它可以输出字符流了,然后我们使用了Kotlin的接口将其装饰成了Buffer.
  3. 读每一行buffer,并appned到result这个StringBuilder里去.
  4. 读完就可以告辞了,close.(use会将其关闭)

3.1.2 Kotlin Coroutine

先看一下上面的图,我们都知道内核态线程是由OS调度的,但当一个线程拿到时间片时,却调到了阻塞IO,那么只能等在那边,浪费时间.

而协程则可以解决这个问题,当一个Jobhang住的时候,可以去做别的事情,绕开阻塞.更好的利用时间片.

目录
相关文章
|
16天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
1月前
|
XML 安全 Java
Java反射机制:解锁代码的无限可能
Java 反射(Reflection)是Java 的特征之一,它允许程序在运行时动态地访问和操作类的信息,包括类的属性、方法和构造函数。 反射机制能够使程序具备更大的灵活性和扩展性
41 5
Java反射机制:解锁代码的无限可能
|
2天前
|
安全 Java API
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。
|
27天前
|
jenkins Java 测试技术
如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例详细说明
本文介绍了如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例,详细说明了从 Jenkins 安装配置到自动构建、测试和部署的全流程。文中还提供了一个 Jenkinsfile 示例,并分享了实践经验,强调了版本控制、自动化测试等关键点的重要性。
60 3
|
1月前
|
存储 安全 Java
系统安全架构的深度解析与实践:Java代码实现
【11月更文挑战第1天】系统安全架构是保护信息系统免受各种威胁和攻击的关键。作为系统架构师,设计一套完善的系统安全架构不仅需要对各种安全威胁有深入理解,还需要熟练掌握各种安全技术和工具。
92 10
|
28天前
|
分布式计算 Java MaxCompute
ODPS MR节点跑graph连通分量计算代码报错java heap space如何解决
任务启动命令:jar -resources odps-graph-connect-family-2.0-SNAPSHOT.jar -classpath ./odps-graph-connect-family-2.0-SNAPSHOT.jar ConnectFamily 若是设置参数该如何设置
|
26天前
|
Java
Java代码解释++i和i++的五个主要区别
本文介绍了前缀递增(++i)和后缀递增(i++)的区别。两者在独立语句中无差异,但在赋值表达式中,i++ 返回原值,++i 返回新值;在复杂表达式中计算顺序不同;在循环中虽结果相同但使用方式有别。最后通过 `Counter` 类模拟了两者的内部实现原理。
Java代码解释++i和i++的五个主要区别
|
2月前
|
搜索推荐 Java 数据库连接
Java|在 IDEA 里自动生成 MyBatis 模板代码
基于 MyBatis 开发的项目,新增数据库表以后,总是需要编写对应的 Entity、Mapper 和 Service 等等 Class 的代码,这些都是重复的工作,我们可以想一些办法来自动生成这些代码。
33 6
|
2月前
|
Java
通过Java代码解释成员变量(实例变量)和局部变量的区别
本文通过一个Java示例,详细解释了成员变量(实例变量)和局部变量的区别。成员变量属于类的一部分,每个对象有独立的副本;局部变量则在方法或代码块内部声明,作用范围仅限于此。示例代码展示了如何在类中声明和使用这两种变量。
|
2月前
|
存储 Java API
优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。
【10月更文挑战第19天】本文介绍了如何优雅地使用Java Map,通过掌握其高级特性和技巧,让代码更简洁。内容包括Map的初始化、使用Stream API处理Map、利用merge方法、使用ComputeIfAbsent和ComputeIfPresent,以及Map的默认方法。这些技巧不仅提高了代码的可读性和维护性,还提升了开发效率。
69 3