Java读取文件时完成字符集转换

缘起 最近公司有个需求,将一个utf8编码的csv上传到ftp供下游系统使用。
但是下游系统要求上传到ftp的文件是GBK编码的。
方案一 常规思路,直接使用InputStreamReader和OutputStreamWriter来读写文件就行
(代码抄别人的)

import java.io.*; //转换文件编码:将GBK编码文件转化为UTF-8编码的文件 public class GBKtoUtf { public static void main(String[] args) throws IOException { InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream("C:\\test\\GBK文件.txt"),"GBK"); //这里必须是GBK OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream("C:\\test\\UTF-8文件.txt"),"UTF-8"); int len = 0; while ((len = inputStreamReader.read()) != -1){ outputStreamWriter.write(len); } outputStreamWriter.close(); inputStreamReader.close(); } }

方案二 对于小文件,方案一比较简单明了
对于大文件,方案一需要读写磁盘,性能非常低(磁盘往往是一个操作系统的瓶颈)
对于上传文件,肯定又一次文件的读取,有没有办法在读取的过程完成转码?
分析FtpClient的API 【Java读取文件时完成字符集转换】Java读取文件时完成字符集转换
文章图片

Java读取文件时完成字符集转换
文章图片

由此可知,FtpClient上传文件其实就是字节流的copy而已。
分析OutputStreamWriter 翻阅OutputStreamWriter源码发现它使用了一个StreamEncoder的类来实现编码转换
而StreamEncoder则是实用CharsetEncoder实现编码转换
CharsetEncoder有两个转换API
public final CoderResult encode(CharBuffer in, ByteBuffer out, boolean endOfInput){ ...... }public final ByteBuffer encode(CharBuffer in){ ...... }

文件读取过程完成转码 既然FtpClient上传文件只是字节流的Copy,那只要在它读取字节流的时候完成转码就OK。
CharsetEncoder提供了API把CharBuffer转换为ByteBuffer。
所以,只需要实现一个自定义InputStream即可
import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.StandardCharsets; public class FtpInputStream extends InputStream{ private static final Logger logger = LoggerFactory.getLogger(FtpInputStream.class); private final InputStreamReader inputStreamReader; private final Charset originCharset; private final Charset destCharset; private final CharsetEncoder encoder; private final CharBuffer charBuffer; private final ByteBuffer byteBuffer; private CoderResult coderResult = CoderResult.UNDERFLOW; private boolean end = false; public FtpInputStream(InputStream inputStream, Charset originCharset, Charset destCharset) { this.inputStreamReader = new InputStreamReader(inputStream, originCharset); this.originCharset = originCharset; this.destCharset = destCharset; this.encoder = destCharset.newEncoder(); this.charBuffer = CharBuffer.allocate(1000); this.byteBuffer = ByteBuffer.allocate((int) (1000 * encoder.averageBytesPerChar())); this.byteBuffer.flip(); }@Override public void close() throws IOException { inputStreamReader.close(); }@Override public int read() throws IOException { if (byteBuffer.hasRemaining()) { // byteBuffer还有数据就直接读 return byteBuffer.get(); } else { // byteBuffer已经被读取完了,清空待用 byteBuffer.clear(); } if (end) { return -1; }//byteBuffer没有数据了,charBuffer存在两种情况:已经被消耗完了、未被消耗完 //已经被消耗完了,需要重新从inputStreamReader读取 //未被消耗完,不需从inputStreamReader读取,直接编码 if (coderResult.isUnderflow()) {charBuffer.clear(); int r = inputStreamReader.read(charBuffer); if(r == -1) { //读取结束 end = true; charBuffer.flip(); }else if (r == 0) { //不应该的情况 throw new IOException("read 0 chart"); } else { //翻转被编码器读取 charBuffer.flip(); } }//编码为字节 coderResult = encoder.encode(charBuffer, byteBuffer, end); if (coderResult.isError()) { throw new IOException(coderResult.toString()); } if (end) { encoder.flush(byteBuffer); } //翻转,准备读取 byteBuffer.flip(); if (byteBuffer.hasRemaining()) { return byteBuffer.get(); } else { //不应该出现到清空 throw new IOException("encode result is null"); } } }

    推荐阅读