场景
需求流程:body中携带参数将参数、签名、文件流(base64)格式,通过Http请求第三方接口进行转义音频
请求参数如下:
{
"sign":"BSKJFKGJKSLQK",
"body":{
"bt":"Base64字符"
}
}
1.第一版代码(OOM)
1.1.流程概要
- 从网络中获取流,将流转成base64字符
- 将base64字符串与其它参数进行toString(),这时由于base64占用很大的堆内存,极易引发OOM
- 通过http发起请求
1.2.概要代码
1.2.1.获取网络中流文件并转Base64字符串(内存占用极高)
/**
* @description: 文件转Base64字符串
* @param fileUrl
* @author: cc
* @date: 2021/5/13 15:52
*/
public String fileToString(String fileUrl) throws IOException {
byte[] fileBytes = FileUtils.getNetWorkFileBytes(fileUrl);
return LybUtil.byteToBase64(fileBytes);
}
/**
* @description: 获取网络中文件流
* @param urlPath 文件路径
* @author: cc
* @date: 2021/1/12 17:12
*/
public static byte[] getNetWorkFileBytes(String urlPath) throws IOException{
URL url = new URL(urlPath);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
//设置超时间为3秒
conn.setConnectTimeout(3*1000);
//防止屏蔽程序抓取而返回403错误
conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
//获取输入流
InputStream inputStream = conn.getInputStream();
//获取文件字节数组
byte[] bytes = readInputStream(inputStream);
return bytes;
}
public static byte[] readInputStream(InputStream inputStream) throws IOException {
byte[] buffer = new byte[1024];
int len = 0;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while ((len = inputStream.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.close();
return bos.toByteArray();
}
/**
* @description: 字节数组转base64
* @author: cc
* @date: 2021/3/1 11:39
*/
public static String byteToBase64(byte[] b) {
return Base64.getEncoder().encodeToString(b);
}
1.2.2.封装参数
JSONObject param = new JSONObject();
param.put("bt", 流的base64字符串);
param.toJSONString();
HttpUtil.sendRequest(sttFileUrl, "POST", params, HttpUtil.ContentTypeEnum.CONTENT_JSON.getValue());
1.2.3.发起请求代码
public static String sendRequest(String requestUrl, String requestMethod, String parameters, String contentType) {
return baseBody(requestUrl, requestMethod, parameters, contentType);
}
/**
* @description: http请求,body参数格式
* @param requestUrl 请求地址
* @param requestMethod 请求类型
* @param parameters 请求参数
* @param contentType 请求格式
* @return: java.lang.String
* @author: cc
* @date: 2021/6/23 15:06
*/
public static String baseBody(String requestUrl, String requestMethod, String parameters, String contentType) {
try {
URL url = new URL(requestUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
conn.setRequestProperty("content-type", contentType);
// 当outputStr不为null时向输出流写数据
if (null != parameters) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(parameters.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
conn.disconnect();
return buffer.toString();
} catch (ConnectException ce) {
System.out.println("连接超时:{}" + ce);
} catch (Exception e) {
System.out.println("https请求异常:{}" + e);
}
return null;
}
以上代码由于会将base64转字符串存到堆内存中,故内存占用极高,会引发OOM异常。
那有没有一种方法,我把base64字符串,通过流的方式一点一点的发过去呢,这样就解决了内存占用问题。答案是有的,接着往下看:
2.第二版代码(base64字符串问题)
基于上面的问题,和同事讨论之后,给出了一个方案,就是通过流一点点传输
2.1 概要代码
// 当outputStr不为null时向输出流写数据
if (!EmptyTool.isNull(lybSttFileDto)) {
LybSttFileDto.LybSttFileBody body = lybSttFileDto.getBody();
OutputStream outputStream = conn.getOutputStream();
outputStream.write("{ \"ver\":\"1.0\", \"sysVer\":\"1.0.0\", \"sys\":\"andriod\", \"body\":{ \"appKey\":\"com.fsk.sttfile\"".getBytes());
outputStream.write((",\"bt\":\"").getBytes());
// 此方法由于实际读取的字节不足3字节,会导致base64时补充=符号,之后会引发录音文件偏大或偏小,无法播放情况
byte[] buffer = new byte[3 * 1024];
int len = 0;
while ((len = lyInputStream.read(buffer)) != -1) {
outputStream.write(base64(Arrays.copyOfRange(buffer, 0, len)).getBytes());
outputStream.flush();
}
outputStream.write(("\"").getBytes());
outputStream.write("}}".getBytes());
outputStream.flush();
outputStream.close();
}
理想情况下,每次读取3kb的数据,之后进行base64,这样就是完整的base64字符串了。
但是由于 read(byte[], 0, len); 方法,实际读取的字节是不足3kb的,那么在进行base64时就会补位操作,字符串后面为追加 == 符号。进而导致第三方将base64转文件时,出现文件无法播放问题。
参考:
3.第三版代码(定位问题、每次只读1kb)
由于第二版代码read()字节时,不足3*1024字节,会导致base64编码问题。
思考之后得出结果:我每次读取一个字节,当读取的字节数等于3kb时,我就进行一次base64,这样就可以解决上述的问题了。
if (!EmptyTool.isNull(lybSttFileDto)) {
LybSttFileDto.LybSttFileBody body = lybSttFileDto.getBody();
OutputStream outputStream = conn.getOutputStream();
outputStream.write("{ \"ver\":\"1.0\", \"sysVer\":\"1.0.0\", \"sys\":\"andriod\", \"body\":{ \"appKey\":\"com.fsk.sttfile\"".getBytes());
outputStream.write((",\"bt\":\"").getBytes());
/*
* 一次读取一个字节,解决上述问题
*/
int size = 3 * 1024;
int b;
byte[] buffer = new byte[size];
int i = 0;
while ((b = lyInputStream.read()) != -1) {
buffer[i] = (byte) b;
i++;
if (i % size == 0) {
outputStream.write(base64(Arrays.copyOfRange(buffer, 0, size)).getBytes());
outputStream.flush();
i = 0;
}
} // while
if (i != 0) {
String tempStr = Base64.getEncoder().encodeToString(Arrays.copyOfRange(buffer, 0, i));
outputStream.write(tempStr.getBytes());
outputStream.flush();
}
outputStream.write(("\"").getBytes());
outputStream.write("}}".getBytes());
outputStream.flush();
outputStream.close();
}
通过上述代码就解决了base64转码问题以及OOM问题。
这个问题个人觉得很有意思,故拿出来分享,欢迎大家讨论
最后附上一个接收base64字符串进行转文件的代码示例,其中有一个意思的是,字符串String类型可以用byte[]数组接收
@RequestMapping("/send")
public BaseResponse<String> send(@RequestBody JSONObject jsonObject) throws IOException {
JSONObject object = JSON.parseObject(jsonObject.toJSONString());
JSONObject body = JSON.parseObject(object.get("bt").toString());
LybSttFileBody lybSttFileBody = JSON.toJavaObject(body, LybSttFileBody.class);
FileOutputStream fos = new FileOutputStream("D:\\file\\文件哈哈.txt");
fos.write(lybSttFileBody.getBt());
fos.flush();
fos.close();
return new BaseResponse<>();
}
@Getter
@Setter
public class LybSttFileBody {
@ApiModelProperty(value = "需识别的音频流 需要流做base64编码处理")
private byte[] bt;
}
JSONObject param = new JSONObject();
param.put("bt", "流的base64字符串");