缘起
最近公司有个需求,将一个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读取文件时完成字符集转换】
文章图片
文章图片
由此可知,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");
}
}
}