Skip to content

使用责任链模式优化文件解析流程

背景

需求背景

  • 因工作需要, 搭建了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("![image](data:%s;base64,%s)", 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());
    }
}