SpringBoot项目实战:用Aspose.Words 20.3搞定合同/报告模板动态生成(附完整源码)
·
SpringBoot与Aspose.Words深度整合:打造企业级文档动态生成服务
在企业级应用开发中,合同、报告等文档的动态生成是刚需功能。本文将分享如何基于SpringBoot框架,利用Aspose.Words 20.3构建一个高性能、可复用的文档生成服务。不同于简单的模板替换,我们将从工程化角度出发,涵盖授权管理、异常处理、性能优化等实战细节,并提供可直接集成到生产环境的完整解决方案。
1. 环境准备与授权管理
1.1 依赖配置最佳实践
在SpringBoot项目中引入Aspose.Words时,推荐使用Maven私服或Nexus仓库管理商业库依赖,而非直接放入resources目录。这更符合企业级项目的依赖管理规范:
<dependency>
<groupId>com.aspose</groupId>
<artifactId>aspose-words</artifactId>
<version>20.3</version>
</dependency>
对于需要本地测试的场景,可使用以下命令安装到本地仓库:
mvn install:install-file \
-Dfile=aspose-words-20.3-jdk17.jar \
-DgroupId=com.aspose \
-DartifactId=aspose-words \
-Dversion=20.3 \
-Dpackaging=jar
1.2 授权验证的优雅实现
为避免生成文档出现水印,需要在应用启动时完成授权验证。我们设计一个带有健康检查的授权管理器:
@Slf4j
@Component
public class AsposeLicenseManager implements InitializingBean {
private boolean licenseValid = false;
@Value("classpath:license/license.lic")
private Resource licenseResource;
public boolean validateLicense() {
try (InputStream is = licenseResource.getInputStream()) {
License license = new License();
license.setLicense(is);
licenseValid = true;
return true;
} catch (Exception e) {
log.error("Aspose license validation failed", e);
licenseValid = false;
return false;
}
}
@Override
public void afterPropertiesSet() {
if (validateLicense()) {
log.info("Aspose.Words license activated successfully");
} else {
throw new IllegalStateException("Aspose license validation failed");
}
}
public boolean isLicenseValid() {
return licenseValid;
}
}
关键改进点:
- 实现
InitializingBean确保应用启动时完成验证 - 增加
isLicenseValid()方法供健康检查使用 - 验证失败时直接抛出异常,避免后续生成水印文档
2. 模板引擎服务设计
2.1 核心服务接口定义
我们抽象出文档生成的核心接口,支持多种输出格式:
public interface DocumentGenerator {
byte[] generateDocument(String templatePath,
Map<String, String> textVariables,
Map<String, byte[]> imageVariables,
OutputFormat format) throws DocumentGenerationException;
enum OutputFormat {
PDF, DOCX, HTML
}
}
2.2 模板替换的增强实现
原始方案中的文本和图片替换存在性能瓶颈,我们进行优化:
public class AsposeDocumentGenerator implements DocumentGenerator {
private static final float DEFAULT_IMAGE_WIDTH = 856F;
private static final float DEFAULT_IMAGE_HEIGHT = 540F;
@Override
public byte[] generateDocument(String templatePath,
Map<String, String> textVariables,
Map<String, byte[]> imageVariables,
OutputFormat format) throws DocumentGenerationException {
try {
Document doc = loadTemplate(templatePath);
replaceTextVariables(doc, textVariables);
replaceImageVariables(doc, imageVariables);
return saveDocument(doc, format);
} catch (Exception e) {
throw new DocumentGenerationException("Failed to generate document", e);
}
}
private Document loadTemplate(String templatePath) throws IOException {
ClassPathResource resource = new ClassPathResource(templatePath);
try (InputStream is = resource.getInputStream()) {
return new Document(is);
}
}
private void replaceTextVariables(Document doc, Map<String, String> variables) {
FindReplaceOptions options = new FindReplaceOptions();
options.setMatchCase(false);
options.setUseSubstitutions(true);
variables.forEach((key, value) -> {
doc.getRange().replace(key, value, options);
});
}
private void replaceImageVariables(Document doc, Map<String, byte[]> imageVariables) {
if (imageVariables == null || imageVariables.isEmpty()) {
return;
}
NodeCollection<Shape> shapes = doc.getChildNodes(NodeType.SHAPE, true);
for (Shape shape : shapes) {
if (shape.hasImage() && imageVariables.containsKey(shape.getAlternativeText())) {
try {
shape.getImageData().setImage(imageVariables.get(shape.getAlternativeText()));
} catch (Exception e) {
log.warn("Failed to replace image: {}", shape.getAlternativeText(), e);
}
}
}
}
private byte[] saveDocument(Document doc, OutputFormat format) throws Exception {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
int saveFormat;
switch (format) {
case PDF: saveFormat = SaveFormat.PDF; break;
case HTML: saveFormat = SaveFormat.HTML; break;
default: saveFormat = SaveFormat.DOCX;
}
doc.save(bos, saveFormat);
return bos.toByteArray();
}
}
}
优化点:
- 使用
Shape节点而非段落遍历处理图片,性能提升显著 - 增加异常处理包装,提供更友好的错误信息
- 支持多种输出格式选择
- 采用try-with-resources确保资源释放
3. Spring集成与REST接口
3.1 控制器层设计
提供RESTful接口,支持直接下载或返回Base64编码:
@RestController
@RequestMapping("/api/documents")
@RequiredArgsConstructor
public class DocumentController {
private final DocumentGenerator documentGenerator;
@PostMapping("/generate")
public ResponseEntity<Resource> generateDocument(
@RequestParam String template,
@RequestBody DocumentVariables variables,
@RequestParam(defaultValue = "PDF") DocumentGenerator.OutputFormat format) {
try {
byte[] content = documentGenerator.generateDocument(
"templates/" + template,
variables.getTextVariables(),
variables.getImageVariables(),
format
);
ByteArrayResource resource = new ByteArrayResource(content);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + template + "." + format.name().toLowerCase() + "\"")
.body(resource);
} catch (DocumentGenerationException e) {
throw new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR,
"Document generation failed", e);
}
}
@Data
public static class DocumentVariables {
private Map<String, String> textVariables = new HashMap<>();
private Map<String, byte[]> imageVariables = new HashMap<>();
}
}
3.2 文件上传处理增强
对于需要上传模板的场景,增加模板管理端点:
@PostMapping("/templates")
public String uploadTemplate(@RequestParam MultipartFile file) {
if (!file.getOriginalFilename().endsWith(".docx")) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
"Only .docx templates are supported");
}
try {
String templateId = UUID.randomUUID().toString();
Path templatePath = Paths.get("templates", templateId + ".docx");
Files.createDirectories(templatePath.getParent());
file.transferTo(templatePath);
return templateId;
} catch (IOException e) {
throw new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR,
"Failed to save template", e);
}
}
4. 高级功能与性能优化
4.1 模板缓存机制
频繁读取模板文件会影响性能,我们引入缓存层:
@Service
public class CachedDocumentGenerator implements DocumentGenerator {
private final DocumentGenerator delegate;
private final Cache<String, byte[]> templateCache;
public CachedDocumentGenerator(DocumentGenerator delegate) {
this.delegate = delegate;
this.templateCache = Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
}
@Override
public byte[] generateDocument(String templatePath,
Map<String, String> textVariables,
Map<String, byte[]> imageVariables,
OutputFormat format) throws DocumentGenerationException {
byte[] templateContent = templateCache.get(templatePath, path -> {
try {
ClassPathResource resource = new ClassPathResource(path);
return StreamUtils.copyToByteArray(resource.getInputStream());
} catch (IOException e) {
throw new RuntimeException("Failed to load template", e);
}
});
try (InputStream is = new ByteArrayInputStream(templateContent)) {
Document doc = new Document(is);
replaceTextVariables(doc, textVariables);
replaceImageVariables(doc, imageVariables);
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
doc.save(bos, getSaveFormat(format));
return bos.toByteArray();
}
} catch (Exception e) {
throw new DocumentGenerationException("Generation failed", e);
}
}
// ... 其他方法保持不变
}
4.2 批量生成与异步处理
对于大批量文档生成需求,我们提供异步接口:
@PostMapping("/batch-generate")
public CompletableFuture<ResponseEntity<Resource>> generateDocumentsInBatch(
@RequestParam String template,
@RequestBody List<DocumentVariables> variablesList,
@RequestParam(defaultValue = "PDF") DocumentGenerator.OutputFormat format) {
return CompletableFuture.supplyAsync(() -> {
try {
Document doc = loadTemplate("templates/" + template);
List<byte[]> results = new ArrayList<>();
for (DocumentVariables variables : variablesList) {
Document docCopy = (Document) doc.deepClone();
replaceVariables(docCopy, variables);
results.add(saveDocument(docCopy, format));
}
byte[] zipBytes = createZipArchive(results);
ByteArrayResource resource = new ByteArrayResource(zipBytes);
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"documents.zip\"")
.body(resource);
} catch (Exception e) {
throw new CompletionException("Batch generation failed", e);
}
}, taskExecutor);
}
4.3 监控与指标收集
通过Spring Actuator暴露文档生成指标:
@Configuration
public class DocumentMetricsConfig {
@Bean
public MeterRegistryCustomizer<MeterRegistry> documentMetrics(
DocumentGenerator documentGenerator) {
return registry -> {
Timer.builder("document.generation.time")
.description("Time taken to generate documents")
.tag("type", "aspose")
.register(registry);
if (documentGenerator instanceof CachedDocumentGenerator) {
Gauge.builder("document.template.cache.size",
((CachedDocumentGenerator) documentGenerator)::getCacheSize)
.description("Number of templates in cache")
.register(registry);
}
};
}
}
@Aspect
@Component
@RequiredArgsConstructor
public class DocumentGenerationMetricsAspect {
private final MeterRegistry meterRegistry;
@Around("execution(* com.example..DocumentGenerator.generateDocument(..))")
public Object measureGenerationTime(ProceedingJoinPoint pjp) throws Throwable {
Timer.Sample sample = Timer.start(meterRegistry);
try {
return pjp.proceed();
} finally {
sample.stop(meterRegistry.timer("document.generation.time",
"template", (String) pjp.getArgs()[0]));
}
}
}
更多推荐
所有评论(0)