使用 Flying-Saucer-Pdf + velocity 模板引擎生成 PDF(解决中文和图片问题)

使用 Flying Saucer Pdf + 模板引擎生成 PDF(解决中文和图片问题)

概述

本文原创(参考自实际项目经验)介绍如何使用Flying Saucer(flying-saucer-pdf)(xhtmlrenderer)结合多种模板引擎(Velocity、FreeMarker、XHTML)生成PDF文件,并解决中文字体显示和图片嵌入问题。

核心技术栈

  1. Flying Saucer (xhtmlrenderer) - 将XHTML转换为PDF
  2. 模板引擎 - 支持Velocity、FreeMarker、原生XHTML
  3. Base64图片编码 - 解决图片嵌入问题

完整解决方案

1. Maven依赖配置

<dependencies>
    <!-- Flying Saucer PDF生成 -->
      <dependency>
           <groupId>org.xhtmlrenderer</groupId>
           <artifactId>flying-saucer-pdf</artifactId>
           <version>9.1.22</version>
     </dependency>
    
    <!-- Velocity模板引擎 -->
    <dependency>
        <groupId>org.apache.velocity</groupId>
        <artifactId>velocity-engine-core</artifactId>
        <version>2.3</version>
    </dependency>
    
    <!-- FreeMarker模板引擎 -->
    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.32</version>
    </dependency>
    
    <!-- 工具类 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.16</version>
    </dependency>
</dependencies>

2. 通用PDF生成工具类

package com.example.pdf.core;

import com.lowagie.text.pdf.BaseFont;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.pdf.ITextUserAgent;

import java.io.*;
import java.util.Map;

/**
 * PDF生成核心类(原创实现)
 * 解决中文显示和图片嵌入问题
 */
public class PdfGenerator {
    
    private static final Logger log = LoggerFactory.getLogger(PdfGenerator.class);
    
    /**
     * 生成PDF文件
     * @param htmlContent HTML内容
     * @param outputPath 输出文件路径
     * @param fontPath 中文字体路径
     */
    public static void generatePdf(String htmlContent, String outputPath, String fontPath) {
        try (OutputStream os = new FileOutputStream(outputPath)) {
            ITextRenderer renderer = new ITextRenderer();
            
            // 设置自定义的ResourceLoader,处理Base64图片
            renderer.getSharedContext().setUserAgentCallback(
                new CustomResourceLoader()
            );
            
            // 设置中文字体
            if (fontPath != null && !fontPath.isEmpty()) {
                setChineseFont(renderer, fontPath);
            }
            
            // 渲染PDF
            renderer.setDocumentFromString(htmlContent);
            renderer.layout();
            renderer.createPDF(os);
            
            log.info("PDF生成成功: {}", outputPath);
        } catch (Exception e) {
            log.error("PDF生成失败", e);
            throw new RuntimeException("PDF生成失败", e);
        }
    }
    
    /**
     * 设置中文字体
     */
    private static void setChineseFont(ITextRenderer renderer, String fontPath) throws Exception {
        try {
            // 尝试加载字体文件
            File fontFile = new File(fontPath);
            if (fontFile.exists()) {
                renderer.getFontResolver().addFont(
                    fontPath, 
                    BaseFont.IDENTITY_H, 
                    BaseFont.EMBEDDED
                );
                log.debug("使用字体文件: {}", fontPath);
            } else {
                // 从classpath加载
                InputStream fontStream = PdfGenerator.class
                    .getClassLoader()
                    .getResourceAsStream(fontPath);
                if (fontStream != null) {
                    // 创建临时字体文件
                    File tempFont = File.createTempFile("font_", ".ttc");
                    try (FileOutputStream fos = new FileOutputStream(tempFont)) {
                        byte[] buffer = new byte[1024];
                        int len;
                        while ((len = fontStream.read(buffer)) != -1) {
                            fos.write(buffer, 0, len);
                        }
                    }
                    renderer.getFontResolver().addFont(
                        tempFont.getAbsolutePath(),
                        BaseFont.IDENTITY_H,
                        BaseFont.EMBEDDED
                    );
                    tempFont.deleteOnExit();
                    log.debug("使用classpath字体: {}", fontPath);
                } else {
                    log.warn("字体文件未找到: {}", fontPath);
                }
            }
        } catch (Exception e) {
            log.warn("字体设置失败,使用默认字体", e);
        }
    }
    
    /**
     * 自定义资源加载器,支持Base64图片
     */
    private static class CustomResourceLoader extends ITextUserAgent {
        
        @Override
        protected InputStream resolveAndOpenStream(String uri) {
            // 处理Base64图片
            if (uri.startsWith("data:image")) {
                try {
                    // 提取Base64数据
                    String base64Data = uri.substring(uri.indexOf(",") + 1);
                    byte[] imageBytes = java.util.Base64.getDecoder().decode(base64Data);
                    return new ByteArrayInputStream(imageBytes);
                } catch (Exception e) {
                    log.error("Base64图片解析失败", e);
                    return null;
                }
            }
            return super.resolveAndOpenStream(uri);
        }
    }
}

3. 模板引擎封装类

package com.example.pdf.template;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
 * 模板引擎工厂(原创实现)
 * 支持Velocity、FreeMarker和原生XHTML
 */
public class TemplateEngine {
    
    /**
     * Velocity模板引擎
     */
    public static class VelocityRenderer {
        private final VelocityEngine velocityEngine;
        
        public VelocityRenderer() {
            velocityEngine = new VelocityEngine();
            velocityEngine.setProperty("resource.loader", "class");
            velocityEngine.setProperty("class.resource.loader.class", 
                "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
            velocityEngine.setProperty("input.encoding", "UTF-8");
            velocityEngine.setProperty("output.encoding", "UTF-8");
            velocityEngine.init();
        }
        
        public String render(String templatePath, Map<String, Object> data) throws Exception {
            VelocityContext context = new VelocityContext();
            if (data != null) {
                data.forEach(context::put);
            }
            
            String templateContent = loadTemplate(templatePath);
            StringWriter writer = new StringWriter();
            velocityEngine.evaluate(context, writer, "template", templateContent);
            
            return writer.toString();
        }
    }
    
    /**
     * FreeMarker模板引擎
     */
    public static class FreeMarkerRenderer {
        private final Configuration configuration;
        
        public FreeMarkerRenderer(String templateDirectory) throws IOException {
            configuration = new Configuration(Configuration.VERSION_2_3_32);
            configuration.setDirectoryForTemplateLoading(
                new File(templateDirectory)
            );
            configuration.setDefaultEncoding("UTF-8");
        }
        
        public String render(String templateName, Map<String, Object> data) 
                throws IOException, TemplateException {
            
            Template template = configuration.getTemplate(templateName);
            StringWriter writer = new StringWriter();
            template.process(data, writer);
            
            return writer.toString();
        }
    }
    
    /**
     * 原生XHTML模板(直接使用)
     */
    public static class XhtmlRenderer {
        public String render(String templatePath, Map<String, Object> data) throws Exception {
            String templateContent = loadTemplate(templatePath);
            
            // 简单替换变量(实际项目可替换为更复杂的逻辑)
            if (data != null) {
                for (Map.Entry<String, Object> entry : data.entrySet()) {
                    String placeholder = "${" + entry.getKey() + "}";
                    templateContent = templateContent.replace(
                        placeholder, 
                        String.valueOf(entry.getValue())
                    );
                }
            }
            
            return templateContent;
        }
    }
    
    /**
     * 加载模板文件
     */
    private static String loadTemplate(String templatePath) throws IOException {
        try (InputStream is = TemplateEngine.class
                .getClassLoader()
                .getResourceAsStream(templatePath)) {
            
            if (is == null) {
                throw new FileNotFoundException("模板文件未找到: " + templatePath);
            }
            
            return readInputStream(is);
        }
    }
    
    /**
     * 读取输入流
     */
    private static String readInputStream(InputStream is) throws IOException {
        StringBuilder content = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(is, StandardCharsets.UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");
            }
        }
        return content.toString();
    }
}

4. 图片处理工具类

package com.example.pdf.util;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Base64;

/**
 * 图片处理工具类(原创实现)
 * 解决Flying Saucer图片嵌入问题
 */
public class ImageProcessor {
    
    /**
     * 将图片文件转换为Base64编码
     * @param imageFile 图片文件
     * @return Base64编码的图片字符串
     */
    public static String imageToBase64(File imageFile) throws IOException {
        if (!imageFile.exists()) {
            throw new IllegalArgumentException("图片文件不存在: " + imageFile.getPath());
        }
        
        byte[] imageBytes = Files.readAllBytes(imageFile.toPath());
        String base64 = Base64.getEncoder().encodeToString(imageBytes);
        String mimeType = getMimeType(imageFile.getName());
        
        return String.format("data:%s;base64,%s", mimeType, base64);
    }
    
    /**
     * 将图片字节数组转换为Base64编码
     * @param imageBytes 图片字节数组
     * @param fileName 文件名(用于确定MIME类型)
     * @return Base64编码的图片字符串
     */
    public static String bytesToBase64(byte[] imageBytes, String fileName) {
        if (imageBytes == null || imageBytes.length == 0) {
            return "";
        }
        
        String base64 = Base64.getEncoder().encodeToString(imageBytes);
        String mimeType = getMimeType(fileName);
        
        return String.format("data:%s;base64,%s", mimeType, base64);
    }
    
    /**
     * 根据文件名获取MIME类型
     */
    private static String getMimeType(String fileName) {
        if (fileName == null) {
            return "image/jpeg";
        }
        
        fileName = fileName.toLowerCase();
        if (fileName.endsWith(".png")) return "image/png";
        if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) return "image/jpeg";
        if (fileName.endsWith(".gif")) return "image/gif";
        if (fileName.endsWith(".bmp")) return "image/bmp";
        if (fileName.endsWith(".svg")) return "image/svg+xml";
        if (fileName.endsWith(".tiff") || fileName.endsWith(".tif")) return "image/tiff";
        
        return "image/jpeg";
    }
    
    /**
     * 下载网络图片并转换为Base64
     * @param imageUrl 图片URL
     * @return Base64编码的图片字符串
     */
    public static String downloadImageToBase64(String imageUrl) {
        try {
            // 使用Hutool简化HTTP请求
            byte[] bytes = cn.hutool.http.HttpUtil.downloadBytes(imageUrl);
            return bytesToBase64(bytes, getFileNameFromUrl(imageUrl));
        } catch (Exception e) {
            throw new RuntimeException("下载图片失败: " + imageUrl, e);
        }
    }
    
    /**
     * 从URL提取文件名
     */
    private static String getFileNameFromUrl(String url) {
        if (url == null || url.isEmpty()) {
            return "image.jpg";
        }
        
        int lastSlash = url.lastIndexOf('/');
        if (lastSlash >= 0 && lastSlash < url.length() - 1) {
            return url.substring(lastSlash + 1);
        }
        
        return "image.jpg";
    }
}

5. 使用示例

5.1 使用Velocity模板

模板文件:templates/velocity/report.vm

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>业务报告</title>
    <style>
        body { font-family: "SimSun", sans-serif; }
        .header { text-align: center; }
        .logo { width: 100px; height: 60px; }
        .table { border-collapse: collapse; width: 100%; }
        .table th, .table td { border: 1px solid #ddd; padding: 8px; }
    </style>
</head>
<body>
    <div class="header">
        <h1>$report.title</h1>
        <p>生成日期: $dateUtil.format($report.date, "yyyy年MM月dd日")</p>
    </div>
    
    <table class="table">
        <thead>
            <tr>
                <th>序号</th>
                <th>项目名称</th>
                <th>金额</th>
                <th>备注</th>
            </tr>
        </thead>
        <tbody>
            #foreach($item in $report.items)
            <tr>
                <td>$velocityCount</td>
                <td>$item.name</td>
                <td>¥$number.format('#,##0.00', $item.amount)</td>
                <td>$item.remark</td>
            </tr>
            #end
        </tbody>
    </table>
    
    <div style="margin-top: 30px;">
        <p>总计: ¥$number.format('#,##0.00', $report.totalAmount)</p>
        #if($report.signature)
        <p>签章:</p>
        <img src="$report.signature" alt="电子签章" style="width: 120px;"/>
        #end
    </div>
</body>
</html>

Java代码:

// 使用Velocity模板生成PDF
TemplateEngine.VelocityRenderer velocityRenderer = 
    new TemplateEngine.VelocityRenderer();

Map<String, Object> data = new HashMap<>();
// 准备数据...
data.put("report", reportData);

// 将图片转换为Base64
String signatureBase64 = ImageProcessor.imageToBase64(
    new File("signature.png")
);
data.put("report.signature", signatureBase64);

// 渲染模板
String html = velocityRenderer.render(
    "templates/velocity/report.vm", 
    data
);

// 生成PDF
PdfGenerator.generatePdf(
    html, 
    "report.pdf", 
    "fonts/simsun.ttc"
);
5.2 使用FreeMarker模板

模板文件:templates/freemarker/invoice.ftl

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>发票</title>
    <style>
        body { font-family: "SimSun", sans-serif; font-size: 12px; }
        .invoice-table { border-collapse: collapse; width: 100%; }
        .invoice-table th { background-color: #f2f2f2; }
    </style>
</head>
<body>
    <h2 style="text-align: center;">增值税专用发票</h2>
    
    <table class="invoice-table">
        <tr>
            <td colspan="2">购方: ${buyer.name!""}</td>
            <td colspan="2">销方: ${seller.name!""}</td>
        </tr>
        <#list items as item>
        <tr>
            <td>${item_index + 1}</td>
            <td>${item.productName!""}</td>
            <td>${item.quantity!0}</td>
            <td>¥${item.price?string("#,##0.00")}</td>
        </tr>
        </#list>
    </table>
    
    <div style="margin-top: 20px;">
        <p>合计金额: ¥${totalAmount?string("#,##0.00")}</p>
        <#if qrCode??>
        <img src="${qrCode}" alt="二维码" style="width: 80px; height: 80px;"/>
        </#if>
    </div>
</body>
</html>

Java代码:

// 使用FreeMarker模板生成PDF
TemplateEngine.FreeMarkerRenderer freemarkerRenderer = 
    new TemplateEngine.FreeMarkerRenderer("templates/freemarker");

Map<String, Object> data = new HashMap<>();
// 准备数据...

// 生成二维码Base64
String qrCodeBase64 = generateQrCodeBase64("发票编号: INV001");
data.put("qrCode", qrCodeBase64);

// 渲染模板
String html = freemarkerRenderer.render("invoice.ftl", data);

// 生成PDF
PdfGenerator.generatePdf(
    html, 
    "invoice.pdf", 
    "/usr/share/fonts/chinese/SimSun.ttf"
);
5.3 使用原生XHTML模板

模板文件:templates/xhtml/certificate.xhtml

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8"/>
    <title>荣誉证书</title>
    <style>
        @page {
            size: A4 landscape;
            margin: 50px;
        }
        
        body {
            font-family: "SimSun", "STSong", serif;
            background-image: url('${backgroundImage}');
            background-size: cover;
            text-align: center;
        }
        
        .certificate {
            padding: 100px;
        }
        
        .title {
            font-size: 36px;
            font-weight: bold;
            color: #b22222;
            margin-bottom: 60px;
        }
        
        .content {
            font-size: 24px;
            line-height: 1.8;
            margin-bottom: 40px;
        }
        
        .signature {
            font-size: 18px;
            margin-top: 80px;
        }
        
        .stamp {
            position: absolute;
            right: 150px;
            bottom: 150px;
            width: 120px;
            height: 120px;
            opacity: 0.9;
        }
    </style>
</head>
<body>
    <div class="certificate">
        <div class="title">荣誉证书</div>
        <div class="content">
            兹授予 <strong>${recipientName}</strong> 同志<br/>
            在${year}年度${achievement}中表现突出,<br/>
            特发此证,以资鼓励。
        </div>
        <div class="signature">
            <p>${organizationName}</p>
            <p>${issueDate}</p>
        </div>
        <img src="${stampImage}" class="stamp" alt="公章"/>
    </div>
</body>
</html>

Java代码:

// 使用原生XHTML模板生成PDF
TemplateEngine.XhtmlRenderer xhtmlRenderer = 
    new TemplateEngine.XhtmlRenderer();

Map<String, Object> data = new HashMap<>();
data.put("recipientName", "张三");
data.put("year", "2024");
data.put("achievement", "技术研发项目");

// 将印章图片转换为Base64
String stampBase64 = ImageProcessor.imageToBase64(
    new File("stamp.png")
);
data.put("stampImage", stampBase64);

// 背景图片(使用Base64或文件路径)
data.put("backgroundImage", 
    "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAAAAAAAD/...");

// 渲染模板
String html = xhtmlRenderer.render(
    "templates/xhtml/certificate.xhtml", 
    data
);

// 生成PDF
PdfGenerator.generatePdf(
    html, 
    "certificate.pdf", 
    "classpath:fonts/simsun.ttc"
);

6. 高级功能:缓存和性能优化

package com.example.pdf.advanced;

import com.example.pdf.core.PdfGenerator;
import com.example.pdf.template.TemplateEngine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 高级PDF生成服务(原创实现)
 * 包含模板缓存、异步生成等功能
 */
public class AdvancedPdfService {
    
    private static final Logger log = LoggerFactory.getLogger(AdvancedPdfService.class);
    
    // 模板缓存
    private final Map<String, String> templateCache = new ConcurrentHashMap<>();
    
    // 清理缓存的调度器
    private final ScheduledExecutorService scheduler = 
        Executors.newScheduledThreadPool(1);
    
    public AdvancedPdfService() {
        // 每小时清理一次缓存
        scheduler.scheduleAtFixedRate(() -> {
            log.info("清理模板缓存,当前大小: {}", templateCache.size());
            templateCache.clear();
        }, 1, 1, TimeUnit.HOURS);
    }
    
    /**
     * 带缓存的模板渲染
     */
    public String renderWithCache(String templatePath, 
                                  Map<String, Object> data,
                                  String engineType) throws Exception {
        
        String cacheKey = templatePath + "|" + engineType;
        String templateContent = templateCache.get(cacheKey);
        
        if (templateContent == null) {
            templateContent = loadTemplateContent(templatePath);
            templateCache.put(cacheKey, templateContent);
            log.debug("缓存模板: {}", cacheKey);
        }
        
        return renderTemplate(templateContent, data, engineType);
    }
    
    /**
     * 异步生成PDF
     */
    public void generatePdfAsync(String templatePath,
                                 Map<String, Object> data,
                                 String outputPath,
                                 String fontPath,
                                 String engineType) {
        
        Executors.newSingleThreadExecutor().submit(() -> {
            try {
                String html = renderWithCache(templatePath, data, engineType);
                PdfGenerator.generatePdf(html, outputPath, fontPath);
                log.info("异步PDF生成完成: {}", outputPath);
            } catch (Exception e) {
                log.error("异步PDF生成失败", e);
            }
        });
    }
    
    /**
     * 批量生成PDF
     */
    public void batchGeneratePdf(String templatePath,
                                 Iterable<Map<String, Object>> dataList,
                                 String outputPattern,
                                 String fontPath,
                                 String engineType) {
        
        int index = 0;
        for (Map<String, Object> data : dataList) {
            String outputPath = String.format(outputPattern, index++);
            generatePdfAsync(templatePath, data, outputPath, fontPath, engineType);
        }
    }
    
    /**
     * 加载模板内容
     */
    private String loadTemplateContent(String templatePath) throws Exception {
        try (java.io.InputStream is = getClass()
                .getClassLoader()
                .getResourceAsStream(templatePath)) {
            
            if (is == null) {
                throw new IllegalArgumentException("模板未找到: " + templatePath);
            }
            
            return new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
        }
    }
    
    /**
     * 渲染模板
     */
    private String renderTemplate(String templateContent,
                                  Map<String, Object> data,
                                  String engineType) throws Exception {
        
        switch (engineType.toLowerCase()) {
            case "velocity":
                return renderVelocity(templateContent, data);
            case "freemarker":
                return renderFreemarker(templateContent, data);
            case "xhtml":
                return renderXhtml(templateContent, data);
            default:
                throw new IllegalArgumentException("不支持的模板引擎: " + engineType);
        }
    }
    
    private String renderVelocity(String templateContent, Map<String, Object> data) 
            throws Exception {
        // Velocity渲染实现...
        return templateContent; // 简化示例
    }
    
    private String renderFreemarker(String templateContent, Map<String, Object> data) 
            throws Exception {
        // FreeMarker渲染实现...
        return templateContent; // 简化示例
    }
    
    private String renderXhtml(String templateContent, Map<String, Object> data) {
        // XHTML简单变量替换
        String result = templateContent;
        for (Map.Entry<String, Object> entry : data.entrySet()) {
            String placeholder = "${" + entry.getKey() + "}";
            result = result.replace(placeholder, 
                entry.getValue() != null ? entry.getValue().toString() : "");
        }
        return result;
    }
    
    /**
     * 关闭服务
     */
    public void shutdown() {
        scheduler.shutdown();
        try {
            if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
            }
        } catch (InterruptedException e) {
            scheduler.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

关键问题解决方案

1. 中文字体显示问题

核心解决方案

// 关键代码:添加中文字体
renderer.getFontResolver().addFont(
    "fonts/simsun.ttc",  // 字体文件路径
    BaseFont.IDENTITY_H, // 使用Unicode编码
    BaseFont.EMBEDDED    // 嵌入字体
);

字体文件获取

  1. Windows系统:C:/Windows/Fonts/simsun.ttc
  2. Linux系统:/usr/share/fonts/chinese/SimSun.ttf
  3. 将字体文件打包到项目resources中

2. 图片显示问题

Base64解决方案
在PDF中插入 png图片的base64格式

<!-- 在HTML模板中使用Base64图片 -->
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." />

参考GitHub Issue

3. 性能优化建议

  1. 模板缓存:避免重复读取和解析模板文件
  2. 字体缓存:字体只需加载一次
  3. 异步生成:批量处理时使用线程池
  4. 图片优化
    • 压缩图片减小体积
    • 使用WebP格式(需转换)
    • 懒加载非必要图片

总结

本文提供了完整的Flying Saucer PDF生成解决方案,具有以下特点:

核心优势

  1. 多模板引擎支持:Velocity、FreeMarker、原生XHTML
  2. 完美中文支持:通过嵌入字体解决乱码问题
  3. 图片兼容性好:Base64编码避免路径问题
  4. 高性能设计:支持缓存和异步生成

使用场景

  • 业务报表:销售报表、财务报表
  • 证书文档:毕业证、荣誉证书
  • 合同协议:电子合同、协议文件
  • 发票单据:增值税发票、收据

注意事项

  1. 字体文件需要合法授权
  2. Base64图片会增加HTML体积
  3. 复杂布局需要CSS支持

参考资料

本文代码为原创实现,参考了实际项目经验。相关技术参考:

  1. Flying Saucer GitHub Repository
  2. Base64 Image Support Issue
  3. Velocity Engine Documentation
  4. FreeMarker Documentation
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值