mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-04-09 01:37:34 +00:00
5.0版本 发布 重构下载方法 修复pdf 占用demo文件夹
This commit is contained in:
@@ -60,6 +60,9 @@ public class PdfFilePreviewImpl implements FilePreview {
|
|||||||
String originFilePath; //原始文件路径
|
String originFilePath; //原始文件路径
|
||||||
String cacheName = pdfName+officePreviewType;
|
String cacheName = pdfName+officePreviewType;
|
||||||
String filePassword = fileAttribute.getFilePassword(); // 获取密码
|
String filePassword = fileAttribute.getFilePassword(); // 获取密码
|
||||||
|
if("demo.pdf".equals(pdfName)){
|
||||||
|
return otherFilePreview.notSupportedFile(model, fileAttribute, "不能使用该文件名,请更换其他文件名在进行转换");
|
||||||
|
}
|
||||||
// 查询转换状态
|
// 查询转换状态
|
||||||
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, pdfName, cacheName, fileAttribute);
|
String statusResult = officefilepreviewimpl.checkAndHandleConvertStatus(model, pdfName, cacheName, fileAttribute);
|
||||||
if (statusResult != null) {
|
if (statusResult != null) {
|
||||||
|
|||||||
@@ -3,30 +3,11 @@ package cn.keking.utils;
|
|||||||
import cn.keking.config.ConfigConstants;
|
import cn.keking.config.ConfigConstants;
|
||||||
import cn.keking.model.FileAttribute;
|
import cn.keking.model.FileAttribute;
|
||||||
import cn.keking.model.ReturnResponse;
|
import cn.keking.model.ReturnResponse;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
|
||||||
import io.mola.galimatias.GalimatiasParseException;
|
import io.mola.galimatias.GalimatiasParseException;
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
|
|
||||||
import org.apache.hc.client5.http.config.ConnectionConfig;
|
|
||||||
import org.apache.hc.client5.http.config.RequestConfig;
|
|
||||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
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;
|
|
||||||
import org.apache.hc.core5.http.HttpResponse;
|
|
||||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
|
||||||
import org.apache.hc.core5.util.TimeValue;
|
|
||||||
import org.apache.hc.core5.util.Timeout;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import org.springframework.web.client.RequestCallback;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -34,10 +15,7 @@ import java.io.IOException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static cn.keking.utils.KkFileUtils.*;
|
import static cn.keking.utils.KkFileUtils.*;
|
||||||
|
|
||||||
@@ -52,56 +30,6 @@ public class DownloadUtils {
|
|||||||
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
|
private static final String URL_PARAM_FTP_PASSWORD = "ftp.password";
|
||||||
private static final String URL_PARAM_FTP_CONTROL_ENCODING = "ftp.control.encoding";
|
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 String URL_PARAM_FTP_PORT = "ftp.control.port";
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
|
||||||
|
|
||||||
// 使用静态单例的HttpClient和RestTemplate,实现连接复用
|
|
||||||
private static volatile CloseableHttpClient httpClient;
|
|
||||||
private static volatile RestTemplate restTemplate;
|
|
||||||
|
|
||||||
// 获取单例HttpClient(线程安全)
|
|
||||||
private static CloseableHttpClient getHttpClient() throws Exception {
|
|
||||||
if (httpClient == null) {
|
|
||||||
synchronized (DownloadUtils.class) {
|
|
||||||
if (httpClient == null) {
|
|
||||||
httpClient = createConfiguredHttpClient();
|
|
||||||
logger.info("HttpClient初始化完成,已启用连接池和超时配置");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取单例RestTemplate
|
|
||||||
private static RestTemplate getRestTemplate() throws Exception {
|
|
||||||
if (restTemplate == null) {
|
|
||||||
synchronized (DownloadUtils.class) {
|
|
||||||
if (restTemplate == null) {
|
|
||||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
|
||||||
factory.setHttpClient(getHttpClient());
|
|
||||||
|
|
||||||
// 设置连接和读取超时(毫秒)
|
|
||||||
factory.setConnectTimeout(10000); // 10秒连接超时
|
|
||||||
factory.setReadTimeout(30000); // 30秒读取超时
|
|
||||||
factory.setConnectionRequestTimeout(5000); // 5秒获取连接超时
|
|
||||||
|
|
||||||
restTemplate = new RestTemplate(factory);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return restTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 应用关闭时清理资源
|
|
||||||
public static void shutdown() {
|
|
||||||
if (httpClient != null) {
|
|
||||||
try {
|
|
||||||
httpClient.close();
|
|
||||||
logger.info("HttpClient已关闭");
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("关闭HttpClient失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param fileAttribute fileAttribute
|
* @param fileAttribute fileAttribute
|
||||||
@@ -149,79 +77,31 @@ public class DownloadUtils {
|
|||||||
if (!fileAttribute.getSkipDownLoad()) {
|
if (!fileAttribute.getSkipDownLoad()) {
|
||||||
if (isHttpUrl(url)) {
|
if (isHttpUrl(url)) {
|
||||||
File realFile = new File(realPath);
|
File realFile = new File(realPath);
|
||||||
|
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
|
||||||
// 使用单例的RestTemplate,复用连接池
|
|
||||||
RestTemplate template = getRestTemplate();
|
|
||||||
String finalUrlStr = urlStr;
|
String finalUrlStr = urlStr;
|
||||||
RequestCallback requestCallback = request -> {
|
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> {
|
||||||
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
|
// 获取响应头中的Content-Type
|
||||||
WebUtils.applyBasicAuthHeaders(request.getHeaders(), finalUrlStr);
|
String contentType = responseWrapper.getContentType();
|
||||||
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
|
|
||||||
if(StringUtils.hasText(proxyAuthorization)){
|
|
||||||
Map<String, String> proxyAuthorizationMap = mapper.readValue(
|
|
||||||
proxyAuthorization,
|
|
||||||
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class)
|
|
||||||
);
|
|
||||||
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
final boolean[] hasError = {false};
|
|
||||||
String finalUrlStr1 = urlStr;
|
|
||||||
template.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> {
|
|
||||||
try {
|
|
||||||
// 获取响应头中的Content-Type
|
|
||||||
String contentType = WebUtils.headersType(fileResponse);
|
|
||||||
|
|
||||||
// 如果是Office/设计文件,需要校验MIME类型
|
// 如果是Office/设计文件,需要校验MIME类型
|
||||||
if ( WebUtils.isMimeCheckRequired(fileSuffix)) {
|
if (WebUtils.isMimeCheckRequired(fileSuffix)) {
|
||||||
if (! WebUtils.isValidMimeType(contentType, fileSuffix)) {
|
if (!WebUtils.isValidMimeType(contentType, fileSuffix)) {
|
||||||
logger.error("文件类型错误,期望二进制文件但接收到文本类型,url: {}, Content-Type: {}",
|
logger.error("文件类型错误,期望二进制文件但接收到文本类型,url: {}, Content-Type: {}",
|
||||||
finalUrlStr1, contentType);
|
finalUrlStr, contentType);
|
||||||
hasError[0] = true;
|
responseWrapper.setHasError(true);
|
||||||
// 重要:关闭响应流,不读取后续数据
|
return;
|
||||||
fileResponse.close();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存文件
|
|
||||||
FileUtils.copyToFile(fileResponse.getBody(), realFile);
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("处理文件响应时出错", e);
|
|
||||||
hasError[0] = true;
|
|
||||||
}
|
}
|
||||||
return null;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 如果下载过程中出现错误
|
// 保存文件
|
||||||
if (hasError[0]) {
|
FileUtils.copyToFile(responseWrapper.getInputStream(), realFile);
|
||||||
response.setCode(1);
|
});
|
||||||
response.setContent(null);
|
|
||||||
response.setMsg("文件类型校验失败");
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
} 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.getMessage());
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
// 不再需要finally块中关闭HttpClient,因为复用
|
|
||||||
} else if (isFtpUrl(url)) {
|
} else if (isFtpUrl(url)) {
|
||||||
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
|
String ftpUsername = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_USERNAME);
|
||||||
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
|
String ftpPassword = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_PASSWORD);
|
||||||
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
|
String ftpControlEncoding = WebUtils.getUrlParameterReg(fileAttribute.getUrl(), URL_PARAM_FTP_CONTROL_ENCODING);
|
||||||
String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT);
|
String ftpport = WebUtils.getUrlParameterReg(realPath, URL_PARAM_FTP_PORT);
|
||||||
FtpUtils.download(fileAttribute.getUrl(),ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
FtpUtils.download(fileAttribute.getUrl(), ftpport, realPath, ftpUsername, ftpPassword, ftpControlEncoding);
|
||||||
} else if (isFileUrl(url)) { // 添加对file协议的支持
|
} else if (isFileUrl(url)) { // 添加对file协议的支持
|
||||||
handleFileProtocol(url, realPath);
|
handleFileProtocol(url, realPath);
|
||||||
} else {
|
} else {
|
||||||
@@ -233,7 +113,7 @@ public class DownloadUtils {
|
|||||||
response.setMsg(fileName);
|
response.setMsg(fileName);
|
||||||
return response;
|
return response;
|
||||||
} catch (IOException | GalimatiasParseException e) {
|
} catch (IOException | GalimatiasParseException e) {
|
||||||
logger.error("文件下载失败,url:{}", urlStr, e);
|
logger.error("文件下载失败,url:{}", urlStr);
|
||||||
response.setCode(1);
|
response.setCode(1);
|
||||||
response.setContent(null);
|
response.setContent(null);
|
||||||
if (e instanceof FileNotFoundException) {
|
if (e instanceof FileNotFoundException) {
|
||||||
@@ -243,81 +123,10 @@ public class DownloadUtils {
|
|||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("下载过程发生未知异常", e);
|
throw new RuntimeException(e);
|
||||||
response.setCode(1);
|
|
||||||
response.setContent(null);
|
|
||||||
response.setMsg("下载失败:" + e.getMessage());
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建根据配置定制的HttpClient(连接池版本)
|
|
||||||
*/
|
|
||||||
private static CloseableHttpClient createConfiguredHttpClient() throws Exception {
|
|
||||||
// 使用新的Builder API创建连接池管理器
|
|
||||||
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
|
|
||||||
.setMaxConnTotal(100) // 最大连接数
|
|
||||||
.setMaxConnPerRoute(20) // 每个路由最大连接数
|
|
||||||
.setDefaultConnectionConfig(ConnectionConfig.custom()
|
|
||||||
.setConnectTimeout(Timeout.ofSeconds(10)) // 连接超时10秒
|
|
||||||
.setSocketTimeout(Timeout.ofSeconds(30)) // Socket超时30秒
|
|
||||||
.setTimeToLive(TimeValue.ofMinutes(10)) // 连接存活时间10分钟
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 创建请求配置
|
|
||||||
RequestConfig requestConfig = RequestConfig.custom()
|
|
||||||
.setResponseTimeout(Timeout.ofSeconds(30)) // 响应超时30秒
|
|
||||||
.setConnectionRequestTimeout(Timeout.ofSeconds(5)) // 获取连接超时5秒
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// 创建HttpClientBuilder
|
|
||||||
HttpClientBuilder builder = HttpClients.custom()
|
|
||||||
.setConnectionManager(connectionManager)
|
|
||||||
.setDefaultRequestConfig(requestConfig);
|
|
||||||
|
|
||||||
// 配置Keep-Alive策略
|
|
||||||
ConnectionKeepAliveStrategy keepAliveStrategy = (response, context) -> {
|
|
||||||
// 默认保持30秒
|
|
||||||
return TimeValue.ofSeconds(30);
|
|
||||||
};
|
|
||||||
builder.setKeepAliveStrategy(keepAliveStrategy);
|
|
||||||
|
|
||||||
// 配置SSL
|
|
||||||
if (ConfigConstants.isIgnoreSSL()) {
|
|
||||||
logger.debug("创建忽略SSL验证的HttpClient");
|
|
||||||
// 如果SslUtils支持HttpClient5,则使用它来创建client
|
|
||||||
// 否则,我们需要在这里配置忽略SSL
|
|
||||||
return createHttpClientWithConfig(builder);
|
|
||||||
} else {
|
|
||||||
logger.debug("创建标准HttpClient");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 配置重定向
|
|
||||||
if (!ConfigConstants.isEnableRedirect()) {
|
|
||||||
logger.debug("禁用HttpClient重定向");
|
|
||||||
builder.disableRedirectHandling();
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建配置了忽略SSL的HttpClient
|
|
||||||
*/
|
|
||||||
private static CloseableHttpClient createHttpClientWithConfig(HttpClientBuilder builder) throws Exception {
|
|
||||||
// 如果SslUtils支持直接配置builder,则:
|
|
||||||
// SslUtils.configureIgnoreSsl(builder);
|
|
||||||
// return builder.build();
|
|
||||||
|
|
||||||
// 否则,使用SslUtils创建client
|
|
||||||
CloseableHttpClient sslIgnoredClient = SslUtils.createHttpClientIgnoreSsl();
|
|
||||||
|
|
||||||
logger.warn("SslUtils.createHttpClientIgnoreSsl()可能没有连接池配置,建议修改SslUtils以支持builder配置");
|
|
||||||
return sslIgnoredClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理file协议的文件下载
|
// 处理file协议的文件下载
|
||||||
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
|
private static void handleFileProtocol(URL url, String targetPath) throws IOException {
|
||||||
File sourceFile = new File(url.getPath());
|
File sourceFile = new File(url.getPath());
|
||||||
|
|||||||
355
server/src/main/java/cn/keking/utils/HttpRequestUtils.java
Normal file
355
server/src/main/java/cn/keking/utils/HttpRequestUtils.java
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
package cn.keking.utils;
|
||||||
|
|
||||||
|
import cn.keking.config.ConfigConstants;
|
||||||
|
import cn.keking.model.FileAttribute;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.type.TypeFactory;
|
||||||
|
import org.apache.hc.client5.http.config.ConnectionConfig;
|
||||||
|
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;
|
||||||
|
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
|
||||||
|
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
|
||||||
|
import org.apache.hc.core5.http.io.SocketConfig;
|
||||||
|
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
|
||||||
|
import org.apache.hc.core5.pool.PoolReusePolicy;
|
||||||
|
import org.apache.hc.core5.util.TimeValue;
|
||||||
|
import org.apache.hc.core5.util.Timeout;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.client.RequestCallback;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP请求工具类,统一处理HTTP请求逻辑
|
||||||
|
* 优化版本:支持连接复用,减少开销
|
||||||
|
*/
|
||||||
|
public class HttpRequestUtils {
|
||||||
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(HttpRequestUtils.class);
|
||||||
|
private static final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
|
// 连接池管理器(静态变量,全局共享)
|
||||||
|
private static volatile PoolingHttpClientConnectionManager connectionManager;
|
||||||
|
|
||||||
|
// 用于缓存不同配置的HttpClient实例
|
||||||
|
private static final Map<String, CloseableHttpClient> httpClientCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 用于缓存不同配置的RestTemplate实例
|
||||||
|
private static final Map<String, RestTemplate> restTemplateCache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// 默认连接池配置
|
||||||
|
private static final int DEFAULT_MAX_TOTAL = 200; // 最大连接数
|
||||||
|
private static final int DEFAULT_MAX_PER_ROUTE = 50; // 每个路由最大连接数
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化连接池管理器(懒加载)
|
||||||
|
*/
|
||||||
|
private static PoolingHttpClientConnectionManager getConnectionManager() throws Exception {
|
||||||
|
if (connectionManager == null) {
|
||||||
|
synchronized (HttpRequestUtils.class) {
|
||||||
|
if (connectionManager == null) {
|
||||||
|
// 创建连接池管理器
|
||||||
|
PoolingHttpClientConnectionManagerBuilder builder = PoolingHttpClientConnectionManagerBuilder.create();
|
||||||
|
|
||||||
|
// 如果配置忽略SSL,使用自定义TLS策略
|
||||||
|
if (ConfigConstants.isIgnoreSSL()) {
|
||||||
|
SSLContext sslContext = SslUtils.createIgnoreVerifySSL();
|
||||||
|
DefaultClientTlsStrategy tlsStrategy = new DefaultClientTlsStrategy(
|
||||||
|
sslContext, NoopHostnameVerifier.INSTANCE);
|
||||||
|
builder.setTlsSocketStrategy(tlsStrategy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置连接池参数
|
||||||
|
builder.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.LAX)
|
||||||
|
.setConnPoolPolicy(PoolReusePolicy.LIFO)
|
||||||
|
.setMaxConnTotal(DEFAULT_MAX_TOTAL)
|
||||||
|
.setMaxConnPerRoute(DEFAULT_MAX_PER_ROUTE);
|
||||||
|
|
||||||
|
// 设置Socket配置
|
||||||
|
SocketConfig socketConfig = SocketConfig.custom()
|
||||||
|
.setTcpNoDelay(true)
|
||||||
|
.setSoKeepAlive(true)
|
||||||
|
.setSoReuseAddress(true)
|
||||||
|
.setSoTimeout(Timeout.ofSeconds(30))
|
||||||
|
.build();
|
||||||
|
builder.setDefaultSocketConfig(socketConfig);
|
||||||
|
|
||||||
|
// 设置连接配置
|
||||||
|
ConnectionConfig connectionConfig = ConnectionConfig.custom()
|
||||||
|
.setConnectTimeout(Timeout.ofSeconds(10))
|
||||||
|
.setSocketTimeout(Timeout.ofSeconds(30))
|
||||||
|
.setTimeToLive(TimeValue.ofMinutes(5))
|
||||||
|
.build();
|
||||||
|
builder.setDefaultConnectionConfig(connectionConfig);
|
||||||
|
|
||||||
|
connectionManager = builder.build();
|
||||||
|
|
||||||
|
// 启动空闲连接清理线程
|
||||||
|
startIdleConnectionMonitor();
|
||||||
|
|
||||||
|
logger.info("HTTP连接池管理器初始化完成,最大连接数:{},每个路由最大连接数:{}",
|
||||||
|
DEFAULT_MAX_TOTAL, DEFAULT_MAX_PER_ROUTE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return connectionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动空闲连接监控线程
|
||||||
|
*/
|
||||||
|
private static void startIdleConnectionMonitor() {
|
||||||
|
Thread monitorThread = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
|
synchronized (HttpRequestUtils.class) {
|
||||||
|
Thread.sleep(30000); // 每30秒检查一次
|
||||||
|
if (connectionManager != null) {
|
||||||
|
// 关闭过期的连接
|
||||||
|
connectionManager.closeExpired();
|
||||||
|
// 关闭空闲超过30秒的连接
|
||||||
|
connectionManager.closeIdle(TimeValue.ofSeconds(30));
|
||||||
|
|
||||||
|
// 可选:打印连接池状态
|
||||||
|
if (logger.isDebugEnabled()) {
|
||||||
|
logger.debug("连接池状态:最大连接数={}, 每个路由最大连接数={}",
|
||||||
|
connectionManager.getMaxTotal(),
|
||||||
|
connectionManager.getDefaultMaxPerRoute());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.info("连接池监控线程被中断");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("连接池监控异常", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
monitorThread.setDaemon(true);
|
||||||
|
monitorThread.setName("http-connection-monitor");
|
||||||
|
monitorThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建根据配置定制的HttpClient(支持复用)
|
||||||
|
*/
|
||||||
|
public static CloseableHttpClient createConfiguredHttpClient() throws Exception {
|
||||||
|
String cacheKey = buildHttpClientCacheKey();
|
||||||
|
|
||||||
|
// 尝试从缓存获取
|
||||||
|
CloseableHttpClient cachedClient = httpClientCache.get(cacheKey);
|
||||||
|
if (cachedClient != null) {
|
||||||
|
// HttpClient 5.x 没有 isClosed() 方法,我们需要通过其他方式判断
|
||||||
|
// 暂时假设缓存的客户端都是可用的,如果有问题会在使用时报错
|
||||||
|
return cachedClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的HttpClient
|
||||||
|
synchronized (httpClientCache) {
|
||||||
|
// 双重检查
|
||||||
|
cachedClient = httpClientCache.get(cacheKey);
|
||||||
|
if (cachedClient != null) {
|
||||||
|
return cachedClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建HttpClientBuilder
|
||||||
|
HttpClientBuilder httpClientBuilder = HttpClients.custom()
|
||||||
|
.setConnectionManager(getConnectionManager())
|
||||||
|
.setConnectionManagerShared(true); // 共享连接管理器
|
||||||
|
|
||||||
|
// 使用SslUtils配置HttpClientBuilder
|
||||||
|
CloseableHttpClient httpClient = SslUtils.configureHttpClientBuilder(
|
||||||
|
httpClientBuilder,
|
||||||
|
ConfigConstants.isIgnoreSSL(),
|
||||||
|
ConfigConstants.isEnableRedirect()
|
||||||
|
).build();
|
||||||
|
|
||||||
|
// 缓存HttpClient
|
||||||
|
httpClientCache.put(cacheKey, httpClient);
|
||||||
|
logger.debug("创建并缓存新的HttpClient实例,缓存键:{}", cacheKey);
|
||||||
|
|
||||||
|
return httpClient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建HttpClient缓存键
|
||||||
|
*/
|
||||||
|
private static String buildHttpClientCacheKey() {
|
||||||
|
return String.format("ignoreSSL_%s_enableRedirect_%s",
|
||||||
|
ConfigConstants.isIgnoreSSL(),
|
||||||
|
ConfigConstants.isEnableRedirect());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取缓存的RestTemplate(减少对象创建)
|
||||||
|
*/
|
||||||
|
private static RestTemplate getCachedRestTemplate(CloseableHttpClient httpClient) {
|
||||||
|
String cacheKey = "restTemplate_" + System.identityHashCode(httpClient);
|
||||||
|
|
||||||
|
return restTemplateCache.computeIfAbsent(cacheKey, key -> {
|
||||||
|
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
||||||
|
factory.setHttpClient(httpClient);
|
||||||
|
|
||||||
|
// 设置连接超时和读取超时
|
||||||
|
factory.setConnectTimeout(30000);
|
||||||
|
factory.setReadTimeout(30000);
|
||||||
|
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
restTemplate.setRequestFactory(factory);
|
||||||
|
|
||||||
|
logger.debug("创建并缓存新的RestTemplate实例,缓存键:{}", cacheKey);
|
||||||
|
return restTemplate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行HTTP请求(使用连接池)
|
||||||
|
*/
|
||||||
|
public static void executeHttpRequest(java.net.URL url, CloseableHttpClient httpClient,
|
||||||
|
FileAttribute fileAttribute, FileResponseHandler handler) throws Exception {
|
||||||
|
// 获取缓存的RestTemplate
|
||||||
|
RestTemplate restTemplate = getCachedRestTemplate(httpClient);
|
||||||
|
|
||||||
|
String finalUrlStr = url.toString();
|
||||||
|
RequestCallback requestCallback = createRequestCallback(finalUrlStr, fileAttribute);
|
||||||
|
|
||||||
|
try {
|
||||||
|
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, response -> {
|
||||||
|
FileResponseWrapper wrapper = new FileResponseWrapper();
|
||||||
|
wrapper.setInputStream(response.getBody());
|
||||||
|
wrapper.setContentType(WebUtils.headersType(response));
|
||||||
|
|
||||||
|
try {
|
||||||
|
handler.handleResponse(wrapper);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("处理文件响应时出错", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
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忽略功能或检查证书");
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
// 注意:不再关闭HttpClient,由连接池管理
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建请求回调
|
||||||
|
*/
|
||||||
|
private static RequestCallback createRequestCallback(String finalUrlStr, FileAttribute fileAttribute) {
|
||||||
|
return request -> {
|
||||||
|
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
|
||||||
|
WebUtils.applyBasicAuthHeaders(request.getHeaders(), finalUrlStr);
|
||||||
|
|
||||||
|
// 添加Keep-Alive头
|
||||||
|
request.getHeaders().set("Connection", "keep-alive");
|
||||||
|
request.getHeaders().set("Keep-Alive", "timeout=60");
|
||||||
|
|
||||||
|
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
|
||||||
|
if (StringUtils.hasText(proxyAuthorization)) {
|
||||||
|
Map<String, String> proxyAuthorizationMap = mapper.readValue(
|
||||||
|
proxyAuthorization,
|
||||||
|
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class)
|
||||||
|
);
|
||||||
|
proxyAuthorizationMap.forEach((key, value) -> request.getHeaders().set(key, value));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理资源(在应用关闭时调用)
|
||||||
|
*/
|
||||||
|
public static void shutdown() {
|
||||||
|
logger.info("开始清理HTTP连接池资源...");
|
||||||
|
|
||||||
|
// 关闭所有缓存的HttpClient
|
||||||
|
httpClientCache.values().forEach(client -> {
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("关闭HttpClient失败", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
httpClientCache.clear();
|
||||||
|
|
||||||
|
// 关闭连接池管理器
|
||||||
|
if (connectionManager != null) {
|
||||||
|
try {
|
||||||
|
connectionManager.close();
|
||||||
|
logger.info("HTTP连接池管理器已关闭");
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("关闭连接池管理器失败", e);
|
||||||
|
}
|
||||||
|
connectionManager = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空RestTemplate缓存
|
||||||
|
restTemplateCache.clear();
|
||||||
|
|
||||||
|
logger.info("HTTP连接池资源清理完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件响应处理器接口
|
||||||
|
*/
|
||||||
|
public interface FileResponseHandler {
|
||||||
|
void handleResponse(FileResponseWrapper responseWrapper) throws Exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件响应包装器
|
||||||
|
*/
|
||||||
|
public static class FileResponseWrapper {
|
||||||
|
private InputStream inputStream;
|
||||||
|
private String contentType;
|
||||||
|
private boolean hasError;
|
||||||
|
|
||||||
|
public InputStream getInputStream() {
|
||||||
|
return inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInputStream(InputStream inputStream) {
|
||||||
|
this.inputStream = inputStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContentType() {
|
||||||
|
return contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentType(String contentType) {
|
||||||
|
this.contentType = contentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHasError() {
|
||||||
|
return hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHasError(boolean hasError) {
|
||||||
|
this.hasError = hasError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,8 +80,9 @@ public class SslUtils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建忽略SSL验证的SSLContext
|
* 创建忽略SSL验证的SSLContext
|
||||||
|
* 修改为public访问权限
|
||||||
*/
|
*/
|
||||||
private static SSLContext createIgnoreVerifySSL() throws Exception {
|
public static SSLContext createIgnoreVerifySSL() throws Exception {
|
||||||
// 使用TLSv1.2或TLSv1.3
|
// 使用TLSv1.2或TLSv1.3
|
||||||
SSLContext sc = SSLContext.getInstance("TLSv1.2");
|
SSLContext sc = SSLContext.getInstance("TLSv1.2");
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,12 @@ import cn.keking.service.FilePreview;
|
|||||||
import cn.keking.service.FilePreviewFactory;
|
import cn.keking.service.FilePreviewFactory;
|
||||||
import cn.keking.service.cache.CacheService;
|
import cn.keking.service.cache.CacheService;
|
||||||
import cn.keking.service.impl.OtherFilePreviewImpl;
|
import cn.keking.service.impl.OtherFilePreviewImpl;
|
||||||
import cn.keking.utils.FtpUtils;
|
import cn.keking.utils.*;
|
||||||
import cn.keking.utils.KkFileUtils;
|
|
||||||
import cn.keking.utils.SslUtils;
|
|
||||||
import cn.keking.utils.WebUtils;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.type.TypeFactory;
|
|
||||||
import fr.opensagres.xdocreport.core.io.IOUtils;
|
import fr.opensagres.xdocreport.core.io.IOUtils;
|
||||||
import org.apache.commons.codec.binary.Base64;
|
import org.apache.commons.codec.binary.Base64;
|
||||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpMethod;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
@@ -29,8 +20,6 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.ResponseBody;
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.client.RequestCallback;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -39,7 +28,6 @@ import java.io.InputStream;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
|
import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
|
||||||
import static cn.keking.utils.KkFileUtils.isFtpUrl;
|
import static cn.keking.utils.KkFileUtils.isFtpUrl;
|
||||||
@@ -64,7 +52,6 @@ public class OnlinePreviewController {
|
|||||||
private final CacheService cacheService;
|
private final CacheService cacheService;
|
||||||
private final FileHandlerService fileHandlerService;
|
private final FileHandlerService fileHandlerService;
|
||||||
private final OtherFilePreviewImpl otherFilePreview;
|
private final OtherFilePreviewImpl otherFilePreview;
|
||||||
private static final ObjectMapper mapper = new ObjectMapper();
|
|
||||||
|
|
||||||
public OnlinePreviewController(FilePreviewFactory filePreviewFactory, FileHandlerService fileHandlerService, CacheService cacheService, OtherFilePreviewImpl otherFilePreview) {
|
public OnlinePreviewController(FilePreviewFactory filePreviewFactory, FileHandlerService fileHandlerService, CacheService cacheService, OtherFilePreviewImpl otherFilePreview) {
|
||||||
this.previewFactory = filePreviewFactory;
|
this.previewFactory = filePreviewFactory;
|
||||||
@@ -189,50 +176,9 @@ public class OnlinePreviewController {
|
|||||||
InputStream inputStream = null;
|
InputStream inputStream = null;
|
||||||
logger.info("读取跨域pdf文件url:{}", urlPath);
|
logger.info("读取跨域pdf文件url:{}", urlPath);
|
||||||
if (!isFtpUrl(url)) {
|
if (!isFtpUrl(url)) {
|
||||||
// 根据配置创建HttpClient
|
CloseableHttpClient httpClient = HttpRequestUtils.createConfiguredHttpClient();
|
||||||
CloseableHttpClient httpClient = createConfiguredHttpClient();
|
|
||||||
|
|
||||||
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
|
HttpRequestUtils.executeHttpRequest(url, httpClient, fileAttribute, responseWrapper -> IOUtils.copy(responseWrapper.getInputStream(), response.getOutputStream()));
|
||||||
factory.setHttpClient(httpClient);
|
|
||||||
|
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
|
||||||
restTemplate.setRequestFactory(factory);
|
|
||||||
String finalUrlPath = urlPath;
|
|
||||||
RequestCallback requestCallback = request -> {
|
|
||||||
request.getHeaders().setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
|
|
||||||
WebUtils.applyBasicAuthHeaders(request.getHeaders(), finalUrlPath);
|
|
||||||
String proxyAuthorization = fileAttribute.getKkProxyAuthorization();
|
|
||||||
if(StringUtils.hasText(proxyAuthorization)){
|
|
||||||
Map<String, String> proxyAuthorizationMap = mapper.readValue(
|
|
||||||
proxyAuthorization,
|
|
||||||
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, String.class)
|
|
||||||
);
|
|
||||||
proxyAuthorizationMap.forEach((headerKey, value) -> request.getHeaders().set(headerKey, value));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
restTemplate.execute(url.toURI(), HttpMethod.GET, requestCallback, fileResponse -> {
|
|
||||||
IOUtils.copy(fileResponse.getBody(), response.getOutputStream());
|
|
||||||
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忽略功能或检查证书");
|
|
||||||
}
|
|
||||||
logger.error("获取跨域文件失败", e);
|
|
||||||
} finally {
|
|
||||||
// 确保HttpClient被关闭
|
|
||||||
try {
|
|
||||||
httpClient.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.warn("关闭HttpClient失败", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
|
String filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
|
||||||
@@ -254,20 +200,6 @@ public class OnlinePreviewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建根据配置定制的HttpClient
|
|
||||||
*/
|
|
||||||
private CloseableHttpClient createConfiguredHttpClient() throws Exception {
|
|
||||||
org.apache.hc.client5.http.impl.classic.HttpClientBuilder builder = HttpClients.custom();
|
|
||||||
|
|
||||||
// 配置SSL和重定向
|
|
||||||
return SslUtils.configureHttpClientBuilder(
|
|
||||||
builder,
|
|
||||||
ConfigConstants.isIgnoreSSL(),
|
|
||||||
ConfigConstants.isEnableRedirect()
|
|
||||||
).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过api接口入队
|
* 通过api接口入队
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user