前言:因信创替代要求,在麒麟服务器部署新的应用。
原先的架构:前端tomcat部署,源码部署java应用(ps:前后端,文件都在同一台服务器上),前端访问后端,再通过后端应用访问图片/视频/报纸等资源;
新的架构:采用了nginx部署前端(A服务器),docker部署后端(B服务器,文件也在该服务器上),访问nginx,然后通过nginx转发到后端,再通过后端应用访问图片/视频/报纸等资源。
现象:大写的.JPG结尾的图片无法访问到
按F12看到的该图片返回的格式是text/plain,打印日志发现,java代码返回的图片格式是null。
验证:我部署了一个nginx,通过nginx访问图片是可以访问的,不管是大写结尾还是小写结尾(证明图片本身没有问题);上传了一张照片以.jpg是访问的,手动改成了.JPG无法访问😌(说明是文件格式问题)。
核心问题:Java MIME映射默认不识别大写扩展名。
当通过 Java 代码读取图片流(而非直接操作文件路径)时,大写.JPG
扩展名无法识别contentType
,核心原因是:流操作场景下,Files.probeContentType(Path)
等依赖扩展名的方法失效,且纯流解析默认不关联扩展名大小写
解决方案
1. 已知扩展名(从路径 / 文件名获取)→ 手动映射
若能从图片资源路径中提取扩展名(如/path/image.JPG
中的JPG
),可先将扩展名转为小写,再通过 “预定义映射表” 或 “自定义逻辑” 匹配contentType
,避免依赖系统配置。
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;public class StreamContentTypeResolver {// 预定义常见图片扩展名→ContentType映射(不区分大小写,统一小写匹配)private static final Map<String, String> EXTENSION_CONTENT_TYPE_MAP = new HashMap<>();static {// 初始化映射表(可扩展其他格式)EXTENSION_CONTENT_TYPE_MAP.put("jpg", "image/jpeg");EXTENSION_CONTENT_TYPE_MAP.put("jpeg", "image/jpeg");EXTENSION_CONTENT_TYPE_MAP.put("png", "image/png");EXTENSION_CONTENT_TYPE_MAP.put("gif", "image/gif");EXTENSION_CONTENT_TYPE_MAP.put("bmp", "image/bmp");}/*** 结合“资源路径”和“流”解析ContentType(优先用扩展名,兜底用流签名)* @param resourcePath 图片资源路径(如"/path/image.JPG")* @param inputStream 图片流(需支持mark/reset,避免解析后流不可用)* @return ContentType(如"image/jpeg")*/public static String resolveContentType(String resourcePath, InputStream inputStream) {try {// 1. 从路径提取扩展名,转为小写Path path = Paths.get(resourcePath);String fileName = path.getFileName().toString().toLowerCase();String extension = null;int lastDotIndex = fileName.lastIndexOf('.');if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {extension = fileName.substring(lastDotIndex + 1);}// 2. 用预定义的映射表匹配ContentType(处理大写扩展名)if (extension != null && EXTENSION_CONTENT_TYPE_MAP.containsKey(extension)) {return EXTENSION_CONTENT_TYPE_MAP.get(extension);}// 3. 兜底:若扩展名匹配失败,用流签名解析(见方案2)return resolveContentTypeByStreamSignature(inputStream);} catch (Exception e) {e.printStackTrace();return null;}}public static void main(String[] args) throws Exception {// 示例:模拟大写JPG路径和流String resourcePath = "/data/images/test.JPG"; // 大写扩展名InputStream inputStream = StreamContentTypeResolver.class.getResourceAsStream("/test.JPG"); // 从类路径读取流String contentType = resolveContentType(resourcePath, inputStream);System.out.println("解析的ContentType:" + contentType); // 输出 "image/jpeg"}
}
2. 未知扩展名(仅能操作流)→ 读取文件签名
若无法获取扩展名(如流来自网络下载、数据库 BLOB),需直接读取流的前几个字节(文件签名) 判断类型 ——JPEG 的签名是FF D8 FF
,与扩展名大小写无关,是最可靠的方式。
关键注意点:
- 流必须支持
mark/reset
(如BufferedInputStream
),避免解析后流被 “读空”,影响后续业务使用; - 读取签名后需
reset()
流,恢复到初始位置。
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;public class StreamContentTypeResolver {// 续上方案1的类,添加流签名解析方法/*** 通过流的文件签名解析ContentType(不依赖扩展名)* @param inputStream 图片流(需包装为BufferedInputStream以支持mark/reset)* @return ContentType(如"image/jpeg")*/public static String resolveContentTypeByStreamSignature(InputStream inputStream) throws IOException {// 包装为BufferedInputStream,确保支持mark/reset(若原流已支持可跳过)BufferedInputStream bis = new BufferedInputStream(inputStream);bis.mark(10); // 标记前10字节(足够读取所有图片格式的签名)try {// 读取前3字节(多数图片格式的签名长度≤3)byte[] signature = new byte[3];int readLen = bis.read(signature);if (readLen < 3) {return null; // 流过短,无法识别}// 匹配文件签名(按优先级排序)// JPEG签名:FF D8 FFif (signature[0] == (byte) 0xFF && signature[1] == (byte) 0xD8 && signature[2] == (byte) 0xFF) {return "image/jpeg";}// PNG签名:89 50 4Eif (signature[0] == (byte) 0x89 && signature[1] == (byte) 0x50 && signature[2] == (byte) 0x4E) {return "image/png";}// GIF签名:47 49 46if (signature[0] == (byte) 0x47 && signature[1] == (byte) 0x49 && signature[2] == (byte) 0x46) {return "image/gif";}// 其他格式可继续扩展(如BMP:42 4D 36)return null;} finally {// 重置流到初始位置,供后续业务使用(关键!)bis.reset();}}public static void main(String[] args) throws Exception {// 示例:仅通过流解析大写JPG(未知扩展名)InputStream rawStream = StreamContentTypeResolver.class.getResourceAsStream("/test.JPG");InputStream bufferedStream = new BufferedInputStream(rawStream); // 包装为支持mark的流String contentType = resolveContentTypeByStreamSignature(bufferedStream);System.out.println("流签名解析的ContentType:" + contentType); // 输出 "image/jpeg"// 后续可继续使用bufferedStream(已reset,位置正确)// ... 业务逻辑(如保存流、处理图片)...}
}
3.统一扩展名大小写(全新系统推荐)
在Java代码中,先将文件路径/文件名的扩展名转为小写,再匹配MIME类型,从源头避免大小写问题。(但是对已上线的问题不友好)
4.手动添加大写扩展名的MIME映射
若无法修改文件名,可在代码中手动注册.JPG对应的MIME类型,强制让java识别
5.使用Files.probeContentType()并处理大小写(我采用下面这个方式解决的)
思考:为什么tomcat部署就没问题
HTTP服务器通过“文件后缀 + MIME配置表” 匹配Content-Type。Tomcat可能默认包含了大小写后缀的MIME配置。Nginx默认不区分文件后缀大小写,且未为大写.JPG配置对应的MIME类型映射。
给Nginx添加大写.JPG的MIME映射,我也试了这种方式,修改nginx的MIME配置表(sed -i 's/jpeg jpg/jpeg jpg JPG JPEG/g' /etc/nginx/mime.types),但是未解决问题,可能某个地方配置问题,空了还是想试下这个方法。