版本: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 秒。这是一个可以接受的转换时间。但是,我认为你的代码还有改进的空间。
- 你在每次文档处理操作中都配置了默认的
FontSettings。这会导致 Aspose.Words 每次都扫描你的字体源。如果你的字体源中有很多字体,那么这个操作可能会花费相当长的时间。
你可以在应用程序启动时配置一次默认的 FontSettings。这将缩短文档处理时间。
- 在你的代码中,你遍历了
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) });
}
在那台正式环境服务器上,按以下顺序检查:
- FONTS_FOLDER 是不是网络盘 / NFS?(最常见的原因):
df -h $FONTS_FOLDER && mount | grep -i nfs
ls $FONTS_FOLDER | wc -l
time cat $FONTS_FOLDER/*.ttf > /dev/null # 测量原始读取速度
- 系统字体的数量和 fontconfig 的速度(
SystemFontSource 会扫描 /usr/share/fonts 等目录):
time fc-list | wc -l
ls -R /usr/share/fonts | wc -l
有些服务器上最终会安装几千个字体,或者 /usr/share/fonts 被软链接到了慢速存储上。
- 在转换卡住时抓取线程转储(thread dump)——这是最直接的证据;你可以准确看到它卡在哪个文件操作上:
jstack <pid> > dump1.txt # 间隔几秒钟,抓取 3~4 次
查看 convertToPdf 线程栈的底部——看它在 java.io.File.list / FileInputStream.read 中卡在哪个路径上。
- 杀毒 / 安全软件:如果服务器上运行着实时扫描(clamd 或某些主机安全 agent),每次打开 .ttf 文件都会被拦截并扫描——这正好符合"文件数量多、每个都慢"的模式。用
ps aux | grep -iE 'clam|av|sec' 检查。