docx转PDF,耗时异常

版本:23.8
编程语言:java

有个文件转换耗时很长,麻烦帮看下是什么原因呢,这是耗时监控:

源文件:
0202-续签劳动合同&保密协议&脱密期协议-无固定期限-上传版.docx (146.8 KB)

转换代码:

public void convertToPdf(InputStream in, OutputStream out,List<WordParams> params,int saveFormat) throws Exception { 
		logger.info("使用新版本aspose"); 
		Document doc = createDocument(in); 
		if(params != null && params.size() > 0) { 
			for(WordParams wordParams :  params) { 
				FindReplaceOptions options = new FindReplaceOptions(); 
				Color color = getColor(wordParams.getColor()); 
				if(color != null) { 
					options.getApplyFont().setColor(color); 
				} 
				if (BooleanUtils.isTrue(wordParams.getHighlight())) { 
					options.getApplyFont().setHighlightColor(Color.YELLOW); 
				} 
				doc.getRange().replace(wordParams.getName(), wordParams.getValue(), options); 
			} 
		} 
 
		FontSettings fontSettings = FontSettings.getDefaultInstance(); 
		FolderFontSource folderFontSource = new FolderFontSource(FONTS_FOLDER, false, 1); 
		SystemFontSource systemFontSource = new SystemFontSource(2); 
		fontSettings.setFontsSources(new FontSourceBase[] { systemFontSource, folderFontSource }); 
		doc.setFontSettings(fontSettings); 
		doc.getLayoutOptions().setCommentDisplayMode(CommentDisplayMode.HIDE); 
		doc.acceptAllRevisions(); 
 
		if (saveFormat == SaveFormat.PDF){ 
			PdfSaveOptions opts = new PdfSaveOptions(); 
			opts.setUpdateFields(false); 
			opts.setSaveFormat(saveFormat); 
			doc.save(out, opts); 
		} else { 
			doc.save(out, saveFormat); 
		} 
 
	}```

@ZhonghaoSun 在我这边,转换本身大约需要 7 秒。这是一个可以接受的转换时间。但是,我认为你的代码还有改进的空间。

  1. 你在每次文档处理操作中都配置了默认的 FontSettings。这会导致 Aspose.Words 每次都扫描你的字体源。如果你的字体源中有很多字体,那么这个操作可能会花费相当长的时间。

你可以在应用程序启动时配置一次默认的 FontSettings。这将缩短文档处理时间。

  1. 在你的代码中,你遍历了 List<WordParams>,并对每个项执行单独的查找/替换操作。你可以将所有键合并到一个正则表达式中,然后在一次查找/替换操作中全部替换它们,从而达到同样的效果。请看以下修改后的代码:

这部分代码应该移到类的静态构造函数中,或者在应用程序启动事件中执行:

FontSettings fontSettings = FontSettings.getDefaultInstance();
FolderFontSource folderFontSource = new FolderFontSource(FONTS_FOLDER, false, 1);
SystemFontSource systemFontSource = new SystemFontSource(2);
fontSettings.setFontsSources(new FontSourceBase[] { systemFontSource, folderFontSource });

修改后的 convertToPdf 方法:

public static void convertToPdf(InputStream in, OutputStream out,List<WordParams> params,int saveFormat) throws Exception {
    Document doc = new Document(in);
    if(params != null && params.size() > 0) {
        FindReplaceOptions options = new FindReplaceOptions();
        options.setReplacingCallback(new MyFindReplaceCallback(params));
        getColor(wordParams.getColor());
        if (color != null) {
            options.getApplyFont().setColor(color);
        }
        if (BooleanUtils.isTrue(wordParams.getHighlight())) {
            options.getApplyFont().setHighlightColor(Color.YELLOW);
        }
        doc.getRange().replace(getPattern(params), "", options);
    }

    doc.getLayoutOptions().setCommentDisplayMode(CommentDisplayMode.HIDE);
    doc.acceptAllRevisions();
        
    if (saveFormat == SaveFormat.PDF){
        PdfSaveOptions opts = new PdfSaveOptions();
        opts.setUpdateFields(false);
        opts.setSaveFormat(saveFormat);
        doc.save(out, opts);
    } else {
        doc.save(out, saveFormat);
    }
}
private static Pattern getPattern(List<WordParams> params)
{
    String regexPattern = params.stream()
            .map(WordParams::getName)
            .map(Pattern::quote) // Escapes special regex characters
            .collect(Collectors.joining("|")); // Joins with an OR separator

    return Pattern.compile(regexPattern);
}
private static class MyFindReplaceCallback implements IReplacingCallback
{
    public MyFindReplaceCallback(List<WordParams> params)
    {
        mParams = params;
    }
        
    @Override
    public int replacing(ReplacingArgs replacingArgs) throws Exception {
            
        // Get the matched text.
        String matchValue = replacingArgs.getMatch().group();
        // Get the replacement value.
        WordParams value = mParams.stream()
                .filter(p -> matchValue.equals(p.getName()))
                .findAny()
                .orElse(null);
        // There is nothing to replace.
        if(value==null)
            return ReplaceAction.SKIP;
            
        replacingArgs.setReplacement(value.getValue());
        return ReplaceAction.REPLACE;
    }
        
    private  List<WordParams> mParams;
}

我们用相同的代码,在很多机器环境耗时都正常
现在有一个正式环境有耗时长的问题 ,推测应该是服务器环境的问题
怎么能排查是服务器哪部分影响的

@ZhonghaoSun 从性能分析数据来看,原因已经很清楚了- 99% 的时间都花在字体扫描上,而不是文档转换本身:

调用 耗时 占比
FontSettings.setFontsSources() ~132 秒 50.27%
Document.save() ~130 秒 49.7%
其余所有操作(replace、acceptAllRevisions 等) <100ms ~0%

这两项其实是同一个问题:setFontsSources() 会触发字体目录扫描,而 save() 在渲染 PDF 时还需要再次解析字体。在正常机器上,扫描系统字体只需要几百毫秒;而在这台服务器上需要 2 分钟,这说明枚举/读取字体文件本身极其缓慢。

你们在每次转换时都调用 FontSettings.getDefaultInstance() 然后调用 setFontsSources() - 这是一个全局单例,每次调用都会清空字体缓存并强制重新扫描。此外,日志显示有多个线程(pool-28-thread-4/5)在并发执行转换;多个线程同时重置同一个全局实例还会造成锁竞争和反复重新扫描。应该只初始化一次:

static {
    FontSettings fs = FontSettings.getDefaultInstance();
    fs.setFontsSources(new FontSourceBase[] {
        new SystemFontSource(2), new FolderFontSource(FONTS_FOLDER, false, 1) });
}

在那台正式环境服务器上,按以下顺序检查:

  1. FONTS_FOLDER 是不是网络盘 / NFS?(最常见的原因):
df -h $FONTS_FOLDER && mount | grep -i nfs
ls $FONTS_FOLDER | wc -l
time cat $FONTS_FOLDER/*.ttf > /dev/null   # 测量原始读取速度
  1. 系统字体的数量和 fontconfig 的速度SystemFontSource 会扫描 /usr/share/fonts 等目录):
time fc-list | wc -l
ls -R /usr/share/fonts | wc -l

有些服务器上最终会安装几千个字体,或者 /usr/share/fonts 被软链接到了慢速存储上。

  1. 在转换卡住时抓取线程转储(thread dump)——这是最直接的证据;你可以准确看到它卡在哪个文件操作上:
jstack <pid> > dump1.txt   # 间隔几秒钟,抓取 3~4 次

查看 convertToPdf 线程栈的底部——看它在 java.io.File.list / FileInputStream.read 中卡在哪个路径上。

  1. 杀毒 / 安全软件:如果服务器上运行着实时扫描(clamd 或某些主机安全 agent),每次打开 .ttf 文件都会被拦截并扫描——这正好符合"文件数量多、每个都慢"的模式。用 ps aux | grep -iE 'clam|av|sec' 检查。