使用责任链模式优化文件解析流程
背景
需求背景
- 因工作需要, 搭建了java的文件解析服务, 采用开源框架tika作为主要实现, 默认解析所有格式的文件
- 随着需求迭代, 不同格式的文件的解析策略逐渐分化, 如程序文件不用解析, 图片要求提取base64, excel要解析成csv等等, 原来的解析流程逻辑迅速膨胀, 需要进一步拆分优化, 以隔离变化
架构思路
- 首先想到的是"服务提供者框架", 不同格式的文件使用不同的解析service, 但实际有个"若自定义解析失败, 则使用tika解析"的兜底逻辑, 更像是一条执行链
- 因此采用责任链形式进行优化, 并使用@Order来排序中间处理环节
实现
FileParser文件解析接口
- FileParser
public interface FileParser {
void doParse(FileParseRequest req, FileParseResponse resp, FileParserChain chain);
}
- ImageFileParser实现
@Order(Integer.MIN_VALUE + 1)
@Component
public class ImageFileParser implements FileParser {
@Override
public void doParse(FileParseRequest req, FileParseResponse resp, FileParserChain chain) {
String mimeType = FileUtils.getMimeType(req.getBytes(), req.getFullName());
if (mimeType.startsWith("/image")) {
resp.setResult(String.format("", req.getMimetype(),
Base64.getEncoder().encodeToString(req.getBytes())));
} else {
chain.doParse(req, resp);
}
}
}
- RawFileParser实现
@Order(Integer.MIN_VALUE)
@Component
public class RawFileParser implements FileParser {
@Value("#{'${skip.suffix:.txt,.java,.js,.json}'}")
private List<String> skipSuffix;
@Override
public void doParse(FileParseRequest req, FileParseResponse resp, FileParserChain chain) {
String fileExtension = FileUtils.getFileExtension(req.getFullName());
if (CollectionUtil.isNotEmpty(skipSuffix) && skipSuffix.contains(fileExtension)) {
resp.setResult(new String(req.getBytes()));
} else {
chain.doParse(req, resp);
}
}
}
- TikaToMdFileParser实现
@Order(Integer.MAX_VALUE)
@Component
public class TikaToMdFileParser implements FileParser {
@Override
public void doParse(FileParseRequest req, FileParseResponse resp, FileParserChain chain) {
resp.setResult("tika兜底解析: " + new String(req.getBytes()));
}
}
FileParserChain
- FileParserChain
public interface FileParserChain {
void doParse(FileParseRequest req, FileParseResponse resp);
}
- DefaultFileParserChain实现
public class DefaultFileParserChain implements FileParserChain {
private List<FileParser> fileParsers;
private int index = 0;
public DefaultFileParserChain(List<FileParser> fileParsers) {
fileParsers.sort(Comparator.comparing(f -> f.getClass().getAnnotation(Order.class).value()));
this.fileParsers = fileParsers;
}
@Override
public void doParse(FileParseRequest req, FileParseResponse resp) {
if (index < fileParsers.size()) {
FileParser fileParser = fileParsers.get(index);
index++;
fileParser.doParse(req, resp, this);
}
}
}
测试
public class FileParserTest {
@InjectMocks
private ImageFileParser imageFileParser;
@InjectMocks
private RawFileParser rawFileParser;
@InjectMocks
private TikaToMdFileParser tikaToMdFileParser;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testFileParse() {
List<FileParser> fileParsers = Arrays.asList(tikaToMdFileParser, imageFileParser, rawFileParser);
DefaultFileParserChain chain = new DefaultFileParserChain(fileParsers);
FileParseRequest req = new FileParseRequest();
req.setBytes("this is a test".getBytes());
req.setFullName("test.txt");
req.setMimetype(FileUtils.getMimeType(req.getBytes(), req.getFullName()));
FileParseResponse resp = new FileParseResponse();
chain.doParse(req, resp);
System.out.println(resp.getResult());
}
}