Java实现判断文件类型的完整示例代码
作者:南城花随雪。
一、项目背景详细介绍
在许多应用场景中,需根据文件内容或扩展名来判断文件的真实类型,而非单纯依据用户输入的后缀名,以防止:
- 安全风险:攻击者可能将可执行脚本重命名为
.jpg上传,若不做内容校验,可能被误处理。 - 错误处理:上传系统仅按扩展名分发,导致图片、音视频、文档混淆。
- 业务逻辑:根据文件类型调用不同解析器(如 Excel、PDF、Word、图片)时,需要先判断类型再分发。
常见做法包括读取文件魔数(Magic Number,又称文件头签名)或使用 JDK 内置 URLConnection.guessContentTypeFromStream、第三方库(如 Apache Tika)进行 MIME 类型探测。本项目旨在:
实现一个轻量、无外部依赖的工具,通过检查文件头字节(魔数)及扩展名双重校验,快速准确地判断常见文件类型。
二、项目需求详细介绍
1.支持文件类型
- 图片:JPEG、PNG、GIF、BMP、WEBP;
- 文档:PDF、Microsoft Office(DOC、DOCX、XLS、XLSX、PPT、PPTX)、TXT;
- 压缩包:ZIP、RAR、7Z、GZIP;
- 媒体:MP3、MP4、AVI;
- 其他:根据扩展名或通用二进制流返回
UNKNOWN。
2.双重校验策略
- 魔数检测:读取文件前若干字节与预定义签名比对;
- 扩展名匹配:当魔数不匹配时,尝试根据文件名后缀推断;
- 任一策略匹配即返回对应类型,并提供来源信息(
HEADER或EXTENSION)。
3.易用 API
静态工具类 FileTypeDetector:
enum FileType { JPEG, PNG, GIF, BMP, WEBP, PDF, DOC, DOCX, …, UNKNOWN }
class DetectionResult {
FileType type;
Source source; // HEADER or EXTENSION
}
DetectionResult detect(File file);
Optional<FileType> detectByHeader(File file);
Optional<FileType> detectByExtension(String fileName);
支持 InputStream、byte[] 等多种形式。
4.性能与安全
- 只读取文件开头 最大 16 字节,避免加载整个大文件;
- 对于路径不存在或不可读文件,需抛出或返回
UNKNOWN; - 工具类线程安全,无状态。
5.配置可扩展
- 允许在配置文件或代码中注册新的魔数签名和对应类型;
- 魔数长度与字节序灵活控制。
6.文档与示例
- README 中给出多种文件类型检测示例;
- 包含在常用场景(上传校验、文件批量扫描)中的调用方式。
三、相关技术详细介绍
1.魔数(Magic Number)原理
- 文件头前 N 个字节包含固定签名,如 JPEG 为
FF D8 FF,PNG 为89 50 4E 47 0D 0A 1A 0A; - 利用
InputStream.read(byte[])读取并与预定义byte[]进行前缀比较。
2.扩展名判断
- 根据
fileName.substring(fileName.lastIndexOf('.') + 1)(小写)匹配常见后缀; - 扩展名映射表存储于
Map<String, FileType>。
3.Apache Tika 对比
- Tika 提供丰富类型识别能力,但体积大、依赖多;
- 本项目侧重轻量且常见格式快速识别。
4.Java I/O 与 NIO
- 使用
java.io.FileInputStream或java.nio.file.Files.newInputStream; - 对于大批量文件可结合 NIO 异步读取或内存映射(
MappedByteBuffer)。
5.线程安全与缓存
- 将魔数表和扩展名表定义为不可变静态常量,避免并发修改;
- 可选引入
SoftReference缓存已检测文件结果,提升扫描性能。
四、实现思路详细介绍
实现思路
1.定义枚举与数据结构
enum FileType { JPEG, PNG, GIF, …, UNKNOWN };static final Map<FileType, byte[]> MAGIC_NUMBERS存储每种类型的签名;static final Map<String, FileType> EXTENSION_MAP存储后缀映射。
2.FileTypeDetector 类设计
DetectionResult detect(File file):
- 读取前 16 字节到缓冲区;
- 遍历
MAGIC_NUMBERS,匹配前缀;若匹配成功,返回结果; - 否则调用
detectByExtension(file.getName()); - 若仍无结果,返回
UNKNOWN。
Optional<FileType> detectByHeader(InputStream in):类似 detect(File) 但不关闭流。
Optional<FileType> detectByExtension(String name):根据小写后缀查表。
3.示例应用
- 在
main方法或单元测试中,遍历指定目录下所有文件,输出文件名与检测类型; - 演示对同一文件不同策略(HEADER vs EXTENSION)的结果对比。
4.可扩展注册
- 提供
registerMagic(FileType type, byte[] signature)方法动态添加; - 提供
registerExtension(String ext, FileType type)方法动态映射。
/*
* =====================================================
* File: FileTypeDetector.java
* 工具类:基于魔数和扩展名检测文件类型
* =====================================================
*/
package com.example.filetype;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
public final class FileTypeDetector {
public enum FileType {
JPEG, PNG, GIF, BMP, WEBP,
PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, TXT,
ZIP, RAR, _7Z, GZIP,
MP3, MP4, AVI,
UNKNOWN
}
public enum Source { HEADER, EXTENSION, NONE }
public static class DetectionResult {
private final FileType type;
private final Source source;
public DetectionResult(FileType type, Source source) {
this.type = type; this.source = source;
}
public FileType getType() { return type; }
public Source getSource() { return source; }
@Override public String toString() {
return type + " via " + source;
}
}
// 魔数映射表(仅示例签名前缀)
private static final Map<FileType, byte[]> MAGIC_NUMBERS = Map.ofEntries(
Map.entry(FileType.JPEG, new byte[]{(byte)0xFF,(byte)0xD8,(byte)0xFF}),
Map.entry(FileType.PNG, new byte[]{(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A}),
Map.entry(FileType.GIF, "GIF8".getBytes()),
Map.entry(FileType.BMP, "BM".getBytes()),
Map.entry(FileType.WEBP,"RIFF".getBytes()), // needs further check
Map.entry(FileType.PDF, "%PDF".getBytes()),
Map.entry(FileType.ZIP, new byte[]{0x50,0x4B,0x03,0x04}),
Map.entry(FileType.RAR, "Rar!".getBytes()),
Map.entry(FileType._7Z, new byte[]{0x37,0x7A,0xBC,0xAF,0x27,0x1C}),
Map.entry(FileType.GZIP, new byte[]{(byte)0x1F,(byte)0x8B}),
Map.entry(FileType.MP3, new byte[]{(byte)0xFF,(byte)0xFB}),
Map.entry(FileType.PDF, new byte[]{0x25,0x50,0x44,0x46}),
Map.entry(FileType.MP4, new byte[]{0x00,0x00,0x00,0x18,0x66,0x74,0x79,0x70}),
Map.entry(FileType.AVI, "RIFF".getBytes()) // needs further check
);
// 扩展名映射
private static final Map<String, FileType> EXTENSION_MAP;
static {
Map<String, FileType> m = new HashMap<>();
m.put("jpg", FileType.JPEG); m.put("jpeg", FileType.JPEG);
m.put("png", FileType.PNG);
m.put("gif", FileType.GIF);
m.put("bmp", FileType.BMP);
m.put("webp", FileType.WEBP);
m.put("pdf", FileType.PDF);
m.put("doc", FileType.DOC); m.put("docx", FileType.DOCX);
m.put("xls", FileType.XLS); m.put("xlsx", FileType.XLSX);
m.put("ppt", FileType.PPT); m.put("pptx", FileType.PPTX);
m.put("txt", FileType.TXT);
m.put("zip", FileType.ZIP);
m.put("rar", FileType.RAR);
m.put("7z", FileType._7Z);
m.put("gz", FileType.GZIP);
m.put("mp3", FileType.MP3);
m.put("mp4", FileType.MP4);
m.put("avi", FileType.AVI);
EXTENSION_MAP = Collections.unmodifiableMap(m);
}
private static final int MAX_HEADER_SIZE = 16;
private FileTypeDetector() { /* no instantiation */ }
public static DetectionResult detect(File file) {
try (InputStream in = new FileInputStream(file)) {
Optional<FileType> byHeader = detectByHeader(in);
if (byHeader.isPresent()) {
return new DetectionResult(byHeader.get(), Source.HEADER);
}
} catch (IOException ignored) {}
Optional<FileType> byExt = detectByExtension(file.getName());
return new DetectionResult(byExt.orElse(FileType.UNKNOWN),
byExt.isPresent() ? Source.EXTENSION : Source.NONE);
}
public static Optional<FileType> detectByHeader(InputStream in) throws IOException {
in.mark(MAX_HEADER_SIZE);
byte[] header = new byte[MAX_HEADER_SIZE];
int read = in.read(header);
in.reset();
if (read <= 0) return Optional.empty();
for (var entry : MAGIC_NUMBERS.entrySet()) {
byte[] magic = entry.getValue();
if (read >= magic.length && startsWith(header, magic)) {
return Optional.of(entry.getKey());
}
}
return Optional.empty();
}
private static boolean startsWith(byte[] data, byte[] prefix) {
for (int i = 0; i < prefix.length; i++) {
if (data[i] != prefix[i]) return false;
}
return true;
}
public static Optional<FileType> detectByExtension(String name) {
if (name == null) return Optional.empty();
int idx = name.lastIndexOf('.');
if (idx < 0 || idx == name.length() - 1) return Optional.empty();
String ext = name.substring(idx + 1).toLowerCase(Locale.ROOT);
return Optional.ofNullable(EXTENSION_MAP.get(ext));
}
}
/*
* =====================================================
* File: FileTypeDetectorTest.java
* 单元测试:验证文件类型检测功能
* =====================================================
*/
package com.example.filetype;
import org.junit.jupiter.api.Test;
import java.io.File;
import static org.junit.jupiter.api.Assertions.*;
class FileTypeDetectorTest {
@Test
void testDetectByExtension() {
assertEquals(FileTypeDetector.FileType.PNG,
FileTypeDetector.detectByExtension("image.PNG").get());
assertTrue(FileTypeDetector.detectByExtension("unknown.bin").isEmpty());
}
@Test
void testDetectByHeader() throws Exception {
File png = new File("src/test/resources/sample.png");
var res = FileTypeDetector.detectByHeader(new java.io.FileInputStream(png));
assertEquals(FileTypeDetector.FileType.PNG, res.get());
}
@Test
void testDetectFile() {
File jpg = new File("src/test/resources/sample.jpg");
var dr = FileTypeDetector.detect(jpg);
assertEquals(FileTypeDetector.FileType.JPEG, dr.getType());
assertEquals(FileTypeDetector.Source.HEADER, dr.getSource());
}
@Test
void testUnknown() {
File txt = new File("src/test/resources/readme.txt");
var dr = FileTypeDetector.detect(txt);
assertEquals(FileTypeDetector.FileType.TXT, dr.getType());
assertEquals(FileTypeDetector.Source.EXTENSION, dr.getSource());
}
}
代码详细解读
1.魔数检测
MAGIC_NUMBERS存储各类型的文件头签名;detectByHeader读取最多 16 字节,遍历签名表做前缀匹配。
2.扩展名检测
EXTENSION_MAP为常见后缀到FileType的映射;detectByExtension基于小写后缀查表。
3.综合检测
detect(File) 先尝试魔数检测,若成功返回 HEADER;否则按扩展名检测,未匹配则返回 UNKNOWN。
4.线程安全与性能
- 静态常量表均为不可变,无状态方法可安全并发调用;
- 读取头部仅 16 字节,I/O 开销可控。
项目详细总结
本项目提供了轻量级的文件类型检测工具,优势在于:
- 双策略:优先魔数精确识别,再用扩展名兜底;
- 轻量无依赖:仅使用 JDK API,无需第三方库;
- 易扩展:通过注册表可动态添加新类型;
- 线程安全:无状态静态方法,适合高并发。
项目常见问题及解答
WEBP、AVI 因同 RIFF 前缀如何区分?
魔数表可进一步检查后续字节(如 WEBP 在字节 8–12 区段);本示例简化处理。
扩展名不可信,是否可只用魔数?
可以仅用魔数,但某些类型魔数相同或混合写法需补充更详签名。
如何处理流式数据?
detectByHeader(InputStream) 支持对任意流检测,需确保流支持 mark/reset 或使用 PushbackInputStream。
性能瓶颈在何处?
I/O 读头与循环表匹配,可将魔数表按长度分组加速匹配或引入 Trie 结构。
如何添加新文件类型?
在 MAGIC_NUMBERS 和 EXTENSION_MAP 注册新签名和后缀即可。
扩展方向与性能优化
精细魔数:对 RIFF 类型文件,继续读取并校验字节 8–12 是否为 WEBP/AVI。
高效匹配结构:使用前缀树(Trie)存储魔数,以 O(n) 方式匹配多重签名。
异步批量扫描:使用 CompletableFuture 或并行流对目录进行并发检测,加快大规模文件分析。
缓存检测结果:对已检测文件路径/内容哈希进行 LRU 缓存,避免重复读取。
集成第三方库:结合 Apache Tika,提供更全面的 MIME 类型检测和元数据提取。
以上就是Java实现判断文件类型的完整示例代码的详细内容,更多关于Java判断文件类型的资料请关注脚本之家其它相关文章!
