优化多线程转换方法 添加异步转换提示

This commit is contained in:
高雄
2026-01-15 15:43:13 +08:00
parent b5579ae890
commit 2425bea9b6
20 changed files with 3431 additions and 1376 deletions

View File

@@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
import io.mola.galimatias.GalimatiasParseException;
import org.apache.commons.io.FileUtils;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
@@ -41,7 +42,7 @@ public class DownloadUtils {
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
private static final String URL_PARAM_FTP_PORT = "ftp.control.port";
private static final RestTemplate restTemplate = new RestTemplate();
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
private static final HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
private static final ObjectMapper mapper = new ObjectMapper();
@@ -51,12 +52,12 @@ public class DownloadUtils {
* @return 本地文件绝对路径
*/
public static ReturnResponse<String> downLoad(FileAttribute fileAttribute, String fileName) {
// 忽略ssl证书
String urlStr = null;
try {
urlStr = fileAttribute.getUrl().replaceAll("\\+", "%20").replaceAll(" ", "%20");
} catch (Exception e) {
logger.error("忽略SSL证书异常:", e);
logger.error("处理URL异常:", e);
}
ReturnResponse<String> response = new ReturnResponse<>(0, "下载成功!!!", "");
String realPath = getRelFilePath(fileName, fileAttribute);
@@ -90,7 +91,10 @@ public class DownloadUtils {
if (!fileAttribute.getSkipDownLoad()) {
if (isHttpUrl(url)) {
File realFile = new File(realPath);
CloseableHttpClient httpClient = SslUtils.createHttpClientIgnoreSsl();
// 创建配置好的HttpClient
CloseableHttpClient httpClient = createConfiguredHttpClient();
factory.setHttpClient(httpClient);
restTemplate.setRequestFactory(factory);
RequestCallback requestCallback = request -> {
@@ -111,10 +115,25 @@ public class DownloadUtils {
return null;
});
} catch (Exception e) {
// 如果是SSL证书错误给出建议
if (e.getMessage() != null &&
(e.getMessage().contains("SSL") ||
e.getMessage().contains("证书") ||
e.getMessage().contains("certificate")) &&
!ConfigConstants.isIgnoreSSL()) {
logger.warn("SSL证书验证失败建议启用SSL忽略功能或检查证书");
}
response.setCode(1);
response.setContent(null);
response.setMsg("下载失败:" + e);
return response;
} finally {
// 确保HttpClient被关闭
try {
httpClient.close();
} catch (IOException e) {
logger.warn("关闭HttpClient失败", e);
}
}
} else if (isFtpUrl(url)) {
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
@@ -147,6 +166,38 @@ public class DownloadUtils {
}
}
/**
* 创建根据配置定制的HttpClient
*/
private static CloseableHttpClient createConfiguredHttpClient() throws Exception {
org.apache.hc.client5.http.impl.classic.HttpClientBuilder builder = HttpClients.custom();
// 配置SSL
if (ConfigConstants.isIgnoreSSL()) {
logger.debug("创建忽略SSL验证的HttpClient");
// 如果SslUtils有创建builder的方法就更好了这里假设我们直接使用SslUtils
// 或者我们可以创建一个新的方法来返回配置了忽略SSL的builder
return createHttpClientWithConfig();
} else {
logger.debug("创建标准HttpClient");
}
// 配置重定向
if (!ConfigConstants.isEnableRedirect()) {
logger.debug("禁用HttpClient重定向");
builder.disableRedirectHandling();
}
return builder.build();
}
/**
* 创建配置了忽略SSL的HttpClient
*/
private static CloseableHttpClient createHttpClientWithConfig() throws Exception {
return SslUtils.createHttpClientIgnoreSsl();
}
// 处理file协议的文件下载
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
@@ -229,4 +280,4 @@ public class DownloadUtils {
return realPath;
}
}
}

View File

@@ -0,0 +1,257 @@
package cn.keking.utils;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 文件转换状态管理器(增强版)
* 支持实时状态跟踪和状态锁定机制
*/
public class FileConvertStatusManager {
// 存储转换状态key=文件名value=转换状态对象
private static final ConcurrentMap<String, ConvertState> STATUS_MAP = new ConcurrentHashMap<>();
// 记录最终状态(超时或异常),防止重复转换
private static final ConcurrentMap<String, Status> FINAL_STATUS_MAP = new ConcurrentHashMap<>();
/**
* 开始转换,创建初始状态
* @param fileName 文件名
*/
public static void startConvert(String fileName) {
STATUS_MAP.putIfAbsent(fileName, new ConvertState(Status.CONVERTING, "等待转换", 0));
// 清除可能存在的最终状态,因为要开始新的转换
FINAL_STATUS_MAP.remove(fileName);
}
/**
* 更新转换进度
* @param fileName 文件名
* @param message 状态消息
* @param progress 进度百分比(0-100)
*/
public static void updateProgress(String fileName, String message, int progress) {
STATUS_MAP.computeIfPresent(fileName, (key, state) -> {
state.update(message, progress);
logger.debug("更新转换进度: {} -> {} ({}%)", fileName, message, progress);
return state;
});
}
/**
* 标记转换超时 - 记录为最终状态
* @param fileName 文件名
*/
public static void markTimeout(String fileName) {
STATUS_MAP.put(fileName, new ConvertState(Status.TIMEOUT, "转换超时,请重试", 0));
// 记录为最终状态
FINAL_STATUS_MAP.put(fileName, Status.TIMEOUT);
logger.warn("标记文件转换超时: {}", fileName);
}
/**
* 标记转换失败 - 记录为最终状态
* @param fileName 文件名
* @param errorMessage 错误信息
*/
public static void markError(String fileName, String errorMessage) {
STATUS_MAP.put(fileName, new ConvertState(Status.FAILED, errorMessage, 0));
// 记录为最终状态
FINAL_STATUS_MAP.put(fileName, Status.FAILED);
logger.warn("标记文件转换失败: {}, 错误: {}", fileName, errorMessage);
}
/**
* 查询文件转换状态
* @param fileName 文件名
* @return 转换状态对象如果不存在返回null
*/
public static ConvertStatus getConvertStatus(String fileName) {
// 先检查是否有最终状态
Status finalStatus = FINAL_STATUS_MAP.get(fileName);
if ((finalStatus == Status.TIMEOUT || finalStatus == Status.FAILED)) {
ConvertState state = STATUS_MAP.get(fileName);
if (state == null) {
// 如果STATUS_MAP中没有创建一个最终状态
if (finalStatus == Status.TIMEOUT) {
return new ConvertStatus(Status.TIMEOUT, "转换超时,请重试", 0, 0);
} else {
return new ConvertStatus(Status.FAILED, "转换失败", 0, 0);
}
}
// 返回最终状态
return new ConvertStatus(state.status, state.message, state.progress, 0);
}
ConvertState state = STATUS_MAP.get(fileName);
if (state == null) {
return null;
}
// 如果是转换中状态,计算已等待时间
long waitingSeconds = 0;
if (state.status == Status.CONVERTING) {
waitingSeconds = (System.currentTimeMillis() - state.startTime) / 1000;
}
return new ConvertStatus(
state.status,
state.message,
state.progress,
waitingSeconds
);
}
/**
* 转换成功
* @param fileName 文件名
*/
public static void convertSuccess(String fileName) {
STATUS_MAP.remove(fileName);
// 清除最终状态,允许重新转换
FINAL_STATUS_MAP.remove(fileName);
}
/**
* 清理状态(强制重置,允许重新转换)
* @param fileName 文件名
* @return true: 清理成功; false: 清理失败
*/
public static boolean clearStatus(String fileName) {
boolean removed1 = STATUS_MAP.remove(fileName) != null;
boolean removed2 = FINAL_STATUS_MAP.remove(fileName) != null;
logger.info("清理文件状态: {}, STATUS_MAP: {}, FINAL_STATUS_MAP: {}",
fileName, removed1, removed2);
return removed1 || removed2;
}
/**
* 清理过期状态(长时间未清理的状态)
* @param expireHours 过期时间(小时)
* @return 清理的数量
*/
public static int cleanupExpiredStatus(int expireHours) {
long expireMillis = expireHours * 3600 * 1000L;
long currentTime = System.currentTimeMillis();
// 清理STATUS_MAP中的过期状态
int count1 = (int) STATUS_MAP.entrySet().stream()
.filter(entry -> {
ConvertState state = entry.getValue();
if (state.status == Status.CONVERTING) {
return false; // 转换中的不清理
}
long elapsed = currentTime - state.startTime;
return elapsed > expireMillis;
})
.count();
// 清理FINAL_STATUS_MAP中的过期状态
// 注意FINAL_STATUS_MAP没有时间戳无法基于时间清理
// 如果需要清理,可以设置一个独立的过期机制
logger.info("清理了 {} 个过期的转换状态", count1);
return count1;
}
/**
* 转换状态枚举
*/
public enum Status {
CONVERTING("转换中"),
FAILED("转换失败"),
TIMEOUT("转换超时"),
QUEUED("排队中");
private final String description;
Status(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}
/**
* 内部状态存储类
*/
private static class ConvertState {
private final Status status;
private String message;
private int progress; // 0-100
private final long startTime;
public ConvertState(Status status, String message, int progress) {
this.status = status;
this.message = message;
this.progress = Math.max(0, Math.min(100, progress));
this.startTime = System.currentTimeMillis();
}
public void update(String message, int progress) {
this.message = message;
this.progress = Math.max(0, Math.min(100, progress));
}
}
/**
* 对外暴露的转换状态封装类
*/
public static class ConvertStatus {
private final Status status;
private final String message;
private final int progress;
private final long waitingSeconds;
private final long timestamp;
public ConvertStatus(Status status, String message, int progress, long waitingSeconds) {
this.status = status;
this.message = message;
this.progress = progress;
this.waitingSeconds = waitingSeconds;
this.timestamp = System.currentTimeMillis();
}
// 获取实时状态信息
public String getRealTimeMessage() {
if (status == Status.CONVERTING) {
if (progress > 0) {
return String.format("%s: %s (进度: %d%%,已等待 %d 秒)",
status.getDescription(), message, progress, waitingSeconds);
} else {
return String.format("%s: %s已等待 %d 秒",
status.getDescription(), message, waitingSeconds);
}
}
return message;
}
// Getters
public Status getStatus() { return status; }
public String getMessage() { return message; }
public int getProgress() { return progress; }
public long getTimestamp() { return timestamp; }
@Override
public String toString() {
return "ConvertStatus{" +
"status=" + status +
", message='" + message + '\'' +
", progress=" + progress +
", waitingSeconds=" + waitingSeconds +
", timestamp=" + timestamp +
'}';
}
}
// 日志记录器
private static final org.slf4j.Logger logger =
org.slf4j.LoggerFactory.getLogger(FileConvertStatusManager.class);
}

View File

@@ -1,8 +1,8 @@
package cn.keking.utils;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultRedirectStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
@@ -11,7 +11,8 @@ import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.Timeout;
import javax.net.ssl.*;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
/**
@@ -23,40 +24,58 @@ public class SslUtils {
* 创建忽略SSL验证的HttpClient适用于HttpClient 5.6
*/
public static CloseableHttpClient createHttpClientIgnoreSsl() throws Exception {
// 创建自定义的SSL上下文
SSLContext sslContext = createIgnoreVerifySSL();
return configureHttpClientBuilder(HttpClients.custom(), true, true).build();
}
// 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
sslContext, NoopHostnameVerifier.INSTANCE);
/**
* 配置HttpClientBuilder支持SSL和重定向配置
* @param builder HttpClientBuilder
* @param ignoreSSL 是否忽略SSL验证
* @param enableRedirect 是否启用重定向
* @return 配置好的HttpClientBuilder
*/
public static HttpClientBuilder configureHttpClientBuilder(HttpClientBuilder builder,
boolean ignoreSSL,
boolean enableRedirect) throws Exception {
// 配置SSL
if (ignoreSSL) {
// 创建自定义的SSL上下文
SSLContext sslContext = createIgnoreVerifySSL();
// 使用新的PoolingHttpClientConnectionManagerBuilder构建连接管理器
// 使用连接管理器构建器
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tlsStrategy)
.setDefaultSocketConfig(SocketConfig.custom()
.setSoTimeout(Timeout.ofSeconds(10))
.build())
.build();
// 使用SSLConnectionSocketFactoryBuilder构建SSL连接工厂
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
sslContext, NoopHostnameVerifier.INSTANCE);
// 配置连接池参数
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);
// 使用连接管理器构建器
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(tlsStrategy)
.setDefaultSocketConfig(SocketConfig.custom()
.setSoTimeout(Timeout.ofSeconds(10))
.build())
.build();
// 配置连接池参数
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);
builder.setConnectionManager(connectionManager);
}
// 配置请求参数
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofSeconds(10))
.setResponseTimeout(Timeout.ofSeconds(72))
.setConnectionRequestTimeout(Timeout.ofSeconds(2))
.setRedirectsEnabled(true)
.setConnectTimeout(Timeout.ofSeconds(2))
.setRedirectsEnabled(enableRedirect)
.setMaxRedirects(5)
.build();
builder.setDefaultRequestConfig(requestConfig);
return HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(requestConfig)
.setRedirectStrategy(DefaultRedirectStrategy.INSTANCE)
.build();
if (!enableRedirect) {
builder.disableRedirectHandling();
}
return builder;
}
/**
@@ -84,7 +103,7 @@ public class SslUtils {
}
};
sc.init(null, new TrustManager[]{trustManager}, new java.security.SecureRandom());
sc.init(null, new javax.net.ssl.TrustManager[]{trustManager}, new java.security.SecureRandom());
return sc;
}
}