一. 问题分析
-
前端浏览器如何工作: 当浏览器通过URL请求一张图片时,COS服务器在返回图片数据的同时,会在HTTP响应头(HTTP Header)中包含一个叫做 Content-Type 的字段。例如,对于一张JPEG图片,这个头信息会是 Content-Type: image/jpeg。浏览器看到这个头信息,就知道“这是一个JPEG图片”,然后就会正确地将其渲染出来。因此,只要 Content-Type 是正确的,即使URL末尾没有 .jpg 后缀,浏览器也能正常显示。
-
COS控制台如何工作: COS的网页控制台是一个管理工具,它为了快速、简单地展示文件列表和缩略图,通常会采取一种“偷懒”的策略:直接通过对象名称(文件名)的后缀来判断文件类型。当它看到一个像 ..._WBjlp6PwStfS1Rc0 这样没有后缀的文件名时,它会认为这是一个未知的二进制文件,因此不会尝试去生成预览图。
二.解决方案
第1步:添加 Apache Tika 依赖
<!-- pom.xml --> <dependency> <groupId>org.apache.tika</groupId> <artifactId>tika-core</artifactId> <version>2.9.1</version> <!-- 建议使用最新的稳定版本 --> </dependency>
第2步:创建文件后缀工具类
package com.yupi.yupicturebackend.exception;
import cn.hutool.core.util.StrUtil;
import org.apache.tika.mime.MimeType;
import org.apache.tika.mime.MimeTypeException;
import org.apache.tika.mime.MimeTypes;
/**
* 文件后缀工具类
*/
public class FileExtensionUtils {
/**
* 根据MIME类型获取标准的文件后缀名
* @param mimeType 例如 "image/jpeg"
* @return 例如 ".jpg"
*/
public static String getExtensionFromMimeType(String mimeType) {
if (StrUtil.isBlank(mimeType)) {
return "";
}
try {
MimeTypes allTypes = MimeTypes.getDefaultMimeTypes();
MimeType type = allTypes.forName(mimeType);
String extension = type.getExtension();
// 将 .jpeg 这种官方但不太常用的后缀,统一为 .jpg
if (".jpeg".equals(extension)) {
return ".jpg";
}
return extension;
} catch (MimeTypeException e) {
// 如果MIME类型无法识别,可以记录一个警告日志
return "";
}
}
}
第3步:修改 UrlPictureUpload.java
package com.yupi.yupicturebackend.manager.upload;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.Method;
import com.yupi.yupicturebackend.exception.BusinessException;
import com.yupi.yupicturebackend.exception.ErrorCode;
import com.yupi.yupicturebackend.exception.ThrowUtils;
import com.yupi.yupicturebackend.exception.FileExtensionUtils;
import org.springframework.stereotype.Service;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
/**
* URL 图片上传
*/
@Service
public class UrlPictureUpload extends PictureUploadTemplate {
// 【新增】创建一个ThreadLocal来在线程内传递 Content-Type
private static final ThreadLocal<String> contentTypeThreadLocal = new ThreadLocal<>();
@Override
protected void validPicture(Object inputSource) {
// 在每次校验开始前,清理一下ThreadLocal,防止上次失败的请求数据残留
contentTypeThreadLocal.remove();
String fileUrl = (String) inputSource;
// 1. 校验非空
ThrowUtils.throwIf(StrUtil.isBlank(fileUrl), ErrorCode.PARAMS_ERROR, "文件地址为空");
// 2. 校验 URL 格式
try {
new URL(fileUrl);
} catch (MalformedURLException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件地址格式不正确");
}
// 3. 校验 URL 的协议
ThrowUtils.throwIf(!fileUrl.startsWith("http://") && !fileUrl.startsWith("https://"),
ErrorCode.PARAMS_ERROR, "仅支持 HTTP 或 HTTPS 协议的文件地址"
);
// 4. 发送 HEAD 请求验证文件是否存在
HttpResponse httpResponse = null;
try {
httpResponse = HttpUtil.createRequest(Method.HEAD, fileUrl)
.execute();
// 未正常返回,无需执行其他判断
if (httpResponse.getStatus() != HttpStatus.HTTP_OK) {
// 如果文件不存在或无法访问,这里直接返回即可,模板的后续流程会处理
return;
}
// 5. 文件存在,文件类型校验
String contentType = httpResponse.header("Content-Type");
// 获取到contentType后,存入ThreadLocal
if (StrUtil.isNotBlank(contentType)) {
contentTypeThreadLocal.set(contentType);
}
// 不为空,才校验是否合法,这样校验规则相对宽松
if (StrUtil.isNotBlank(contentType)) {
// 允许的图片类型
final List<String> ALLOW_CONTENT_TYPES = Arrays.asList("image/jpeg", "image/jpg", "image/png", "image/webp");
ThrowUtils.throwIf(!ALLOW_CONTENT_TYPES.contains(contentType.toLowerCase()),
ErrorCode.PARAMS_ERROR, "文件类型错误");
}
// 6. 文件存在,文件大小校验
String contentLengthStr = httpResponse.header("Content-Length");
if (StrUtil.isNotBlank(contentLengthStr)) {
try {
long contentLength = Long.parseLong(contentLengthStr);
final long ONE_M = 1024 * 1024;
ThrowUtils.throwIf(contentLength > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2MB");
} catch (NumberFormatException e) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "文件大小格式异常");
}
}
} finally {
// 记得释放资源
if (httpResponse != null) {
httpResponse.close();
}
}
}
@Override
protected String getOriginFilename(Object inputSource) {
String fileUrl = (String) inputSource;
// 从 ThreadLocal 中获取 Content-Type
String contentType = contentTypeThreadLocal.get();
// 无论是否获取到,用完后立即清理 ThreadLocal,防止内存泄漏和线程复用问题
contentTypeThreadLocal.remove();
// 如果在 validPicture 阶段没有获取到 Content-Type,则沿用旧逻辑
if (StrUtil.isBlank(contentType)) {
return FileUtil.getName(fileUrl);
}
// 使用工具类将 content-type 转换为后缀
String extension = FileExtensionUtils.getExtensionFromMimeType(contentType);
// 获取URL中的原始文件名 (例如 "cat.jpg" 或 "cat")
String originalFilename = FileUtil.getName(fileUrl);
// 获取不带后缀的主文件名 (例如 "cat")
String mainName = FileUtil.mainName(originalFilename);
// 如果无法转换得到后缀,或者原始文件名已经有正确的后缀,则直接返回原始文件名
if (StrUtil.isBlank(extension) || originalFilename.toLowerCase().endsWith(extension)) {
return originalFilename;
}
// 否则,为主文件名拼接上正确的后缀
return mainName + extension;
}
@Override
protected void processFile(Object inputSource, File file) throws Exception {
String fileUrl = (String) inputSource;
// 下载文件到临时目录 (此方法无需改动)
HttpUtil.downloadFile(fileUrl, file);
}
}

被折叠的 条评论
为什么被折叠?



