重构我的小屎山代码
我的屎山代码
在写tgDrive的初期,总是觉得怎么简单怎么来,结果就是后期的维护难受的一批。
请好好地,看着我的屎山(请勿展开,展开辣眼睛):
//TODO: need refraction
@Override
public ResponseEntity<StreamingResponseBody> downloadFile(String fileID) {
try {
// 从 botService 获取文件的下载路径和文件名
String fileUrl = botService.getFullDownloadPath(fileID);
String filename = fileMapper.getFileNameByFileId(fileID);
Long fullSize = fileMapper.getFullSizeByFileId(fileID);
if (filename == null) {
filename = botService.getFileNameByID(fileID);
}
// 上传到tg的gif会被转换为MP4
if (filename.endsWith(".gif")) {
filename = filename.substring(0, filename.length() - 4) + ".mp4";
}
if (filename == null || filename.isEmpty()) {
filename = botService.getFileNameByID(fileID);
}
// 设置响应头
HttpHeaders headers = setHeaders(filename, fullSize);
// 创建 OkHttp请求
Request request = new Request.Builder()
.url(fileUrl)
.get()
.build();
// 执行请求
Call call = okHttpClient.newCall(request);
Response response = call.execute();
// 请求失败
if (!response.isSuccessful()) {
log.error("无法下载文件,响应码:" + response.code());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
ResponseBody responseBody = response.body();
if (responseBody == null) {
log.error("响应体为空");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
InputStream inputStream = responseBody.byteStream();
// 尝试解析为 BigFileInfo 并检查 isRecordFile 标识
boolean isRecordFile = false;
BigFileInfo record = null;
try {
String fileContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
record = JSON.parseObject(fileContent, BigFileInfo.class);
isRecordFile = record.isRecordFile();
// 重新获取输入流,因为 readAllBytes 已经读取了流
responseBody.close();
} catch (Exception e) {
log.info("文件不是 BigFileInfo类型,作为普通文件处理");
}
if (isRecordFile && record != null) {
log.info("文件名为:" + record.getFileName());
log.info("检测到记录文件,开始下载并合并分片文件...");
List<String> partFileIds = record.getFileIds();
StreamingResponseBody streamingResponseBody = outputStream -> {
int maxConcurrentDownloads = 3; // 最大并发下载数
ExecutorService executorService = Executors.newFixedThreadPool(maxConcurrentDownloads);
// 使用列表存储 PipedInputStream,索引对应分片顺序
List<PipedInputStream> pipedInputStreams = new ArrayList<>(partFileIds.size());
CountDownLatch latch = new CountDownLatch(partFileIds.size());
try {
// 初始化 PipedInputStream 列表
for (int i = 0; i < partFileIds.size(); i++) {
pipedInputStreams.add(new PipedInputStream());
}
// 提交下载任务
//TODO: 多次尝试下载
for (int i = 0; i < partFileIds.size(); i++) {
final int index = i;
final String partFileId = partFileIds.get(i);
final PipedInputStream pipedInputStream = pipedInputStreams.get(index);
final PipedOutputStream pipedOutputStream = new PipedOutputStream(pipedInputStream);
executorService.submit(() -> {
try (InputStream partInputStream = downloadFileByte(partFileId).byteStream();
OutputStream pos = pipedOutputStream) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = partInputStream.read(buffer)) != -1) {
pos.write(buffer, 0, bytesRead);
pos.flush();
}
} catch (IOException e) {
log.error("分片文件下载失败:{}", partFileId, e);
// 下载失败,关闭流
try {
pipedOutputStream.close();
} catch (IOException ex) {
log.error("关闭 PipedOutputStream 失败", ex);
}
} finally {
latch.countDown();
}
});
}
// 按顺序读取并写入输出流
for (int i = 0; i < partFileIds.size(); i++) {
try (InputStream pis = pipedInputStreams.get(i)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = pis.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
} catch (IOException e) {
String message = e.getMessage();
if (message != null && (message.contains("An established connection was aborted") || message.contains("你的主机中的软件中止了一个已建立的连接"))) {
log.info("客户端中止了连接:{}", message);
return;
} else {
log.error("读取 PipedInputStream 失败", e);
throw new RuntimeException(e);
}
}
}
latch.await();
} catch (Exception e) {
log.error("文件下载终止:{}", e.getMessage(), e);
} finally {
executorService.shutdown();
}
};
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.parseMediaType(getContentTypeFromFilename(filename)))
.body(streamingResponseBody);
} else {
// 解析失败,说明这是普通文件,直接返回文件内容
log.info("文件不是记录文件,直接下载文件...");
StreamingResponseBody streamingResponseBody = outputStream -> {
try (InputStream is = downloadFileByte(fileID).byteStream()) {
byte[] buffer = new byte[4096];
int byteRead;
while ((byteRead = is.read(buffer)) != -1) {
outputStream.write(buffer, 0, byteRead);
}
} catch (IOException e) {
String message = e.getMessage();
if (message != null && (message.contains("An established connection was aborted") || message.contains("你的主机中的软件中止了一个已建立的连接"))) {
// 客户端中止了连接,记录信息级别的日志或忽略
log.info("客户端中止了连接:{}", message);
} else {
// 其他 IOException,可能需要进一步处理
log.error("写入输出流时发生 IOException", e);
throw new RuntimeException(e);
}
} catch (Exception e) {
log.info("文件下载终止");
log.info(e.getMessage(), e);
}
};
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.parseMediaType(getContentTypeFromFilename(filename)))
.body(streamingResponseBody);
}
} catch (Exception e) {
log.error("下载文件失败:" + e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
一个方法,写了近200行,可读性、可维护性差的一批,每次看到这个方法,我就头大,特别是涉及到这个方法的修改时,一看人都麻了,一点改的动力都没有,最后,我还是决定对这份屎山动刀子。
如何重构?
捋顺结构,分步进行
对一个非常长的方法进行重构,首先就要搞懂这个方法的每一步都在干什么,把每一步想清楚后,把这每一步都拆分成一个独立的方法,再在原来的方法调用这些每一步的方法,就能完成重构了。
拿我的这份史码为例,主要分为3步
- 根据
fileID
下载文件并获取inputstream
- 尝试将
inputstream
解析为BigFileInfo
- 根据是否为
record
来选择下载方式
依照分步,抽象方法
分好了步后,就是根据分步来写函数了,例如,①根据fileID
下载文件并获取inputstream
,就可以把原代码中的这一部分的内容抽象出来,形成一个新的方法:
private InputStream downloadFileInputStream(String fileID) throws IOException {
File file = botService.getFile(fileID);
String fileUrl = botService.getFullDownloadPath(file);
Request request = new Request.Builder()
.url(fileUrl)
.get()
.build();
Response response = okHttpClient.newCall(request).execute();
if (!response.isSuccessful()) {
log.error("无法下载文件,响应码:" + response.code());
throw new IOException("无法下载文件,响应码:" + response.code());
}
ResponseBody responseBody = response.body();
if (responseBody == null) {
log.error("响应体为空");
throw new IOException("响应体为空");
}
return responseBody.byteStream();
}
接下来的每一步,都照此步骤进行,就能完成原先的代码的“瘦身”了:
public ResponseEntity<StreamingResponseBody> downloadFile(String fileID) {
try {
// Step 1: 根据`fileID`下载文件并获取`inputstream`
InputStream inputStream = downloadFileInputStream(fileID);
// Step 2: 尝试将`inputstream`解析为`BigFileInfo`
BigFileInfo record = parseBigFileInfo(inputStream);
// Step 3: 根据是否为`record`来选择下载方式
if (record != null && record.isRecordFile()) {
return handleRecordFile(fileID, record);
} else {
return handleRegularFile(fileID, inputStream);
}
} catch (Exception e) {
log.error("下载文件失败:" + e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
这对比一开始的代码,简直是赏心悦目。