
Java 实现 Markdown 转 HTML:从 CommonMark 到 flexmark-java 的完整方案
Java 实现 Markdown 转 HTML:从 CommonMark 到 flexmark-java 的完整方案
在很多 Java Web 项目中,我们需要将 Markdown 格式的内容渲染为 HTML 展示给用户——比如技术文档系统、博客平台、CMS 内容管理系统等。本文梳理 Java 生态中主流的 Markdown 解析库,并以实际代码演示如何集成扩展语法、代码高亮和安全过滤。
一、Java 生态中的 Markdown 解析库对比
Java 社区有多个 Markdown 解析方案,各有特点:
| 库名 | 协议 | CommonMark 兼容 | 扩展语法 | 维护状态 | 适用场景 |
|---|---|---|---|---|---|
| flexmark-java | BSD-2 | ✅ 完全兼容 | 表格、任务列表、目录、脚注等 50+ 扩展 | 活跃维护 | 生产环境首选 |
| commonmark-java | Apache-2.0 | ✅ 完全兼容 | 标题锚点、表格、删除线等少量扩展 | 维护模式 | 简单场景 |
| Pegdown | Apache-2.0 | ❌ 部分兼容 | 支持多种扩展 | 已停止维护 | 遗留项目 |
| Markdown4j | Apache-2.0 | ❌ 不兼容 | 有限 | 已停止维护 | 极简场景 |
选型建议:新项目推荐使用 flexmark-java,它是 commonmark-java 作者开发的增强版本,功能更全面,扩展更丰富。如果你的需求非常简单(只需要基础标题、段落、链接),commonmark-java 也足够用。
二、方案一:使用 CommonMark 实现
commonmark-java 是 Atlassian 维护的 CommonMark 规范 Java 实现,适合基础场景。
2.1 Maven 依赖
<!-- CommonMark 核心库 -->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark</artifactId>
<version>0.17.0</version>
</dependency>
<!-- 扩展:标题锚点(为 h1-h6 自动生成 id 属性) -->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-heading-anchor</artifactId>
<version>0.17.0</version>
</dependency>
<!-- 扩展:GFM 表格支持 -->
<dependency>
<groupId>com.atlassian.commonmark</groupId>
<artifactId>commonmark-ext-gfm-tables</artifactId>
<version>0.17.0</version>
</dependency>
2.2 工具类封装
下面是一个完整的工具类,包含基础转换和扩展转换两种模式:
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TablesExtension;
import org.commonmark.ext.heading.anchor.HeadingAnchorExtension;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.AttributeProvider;
import org.commonmark.renderer.html.AttributeProviderContext;
import org.commonmark.renderer.html.AttributeProviderFactory;
import org.commonmark.renderer.html.HtmlRenderer;
public class MarkdownToHtmlUtils {
/**
* 基础转换:仅支持 CommonMark 标准语法
*/
public static String markdownToHtml(String markdown) {
Parser parser = Parser.builder().build();
Node document = parser.parse(markdown);
HtmlRenderer renderer = HtmlRenderer.builder().build();
return renderer.render(document);
}
/**
* 扩展转换:支持标题锚点 + GFM 表格 + 链接新窗口打开
*/
public static String markdownToHtmlExtensions(String markdown) {
// 标题锚点扩展:为 h1-h6 自动生成 id 属性,方便目录跳转
Set<Extension> headingAnchorExtensions = Collections.singleton(
HeadingAnchorExtension.create()
);
// 表格扩展:支持 GFM 风格的管道表格语法
List<Extension> tableExtension = Collections.singletonList(
TablesExtension.create()
);
// 构建解析器,注册扩展
Parser parser = Parser.builder()
.extensions(tableExtension)
.build();
Node document = parser.parse(markdown);
// 构建渲染器,注册扩展和自定义属性提供者
HtmlRenderer renderer = HtmlRenderer.builder()
.extensions(headingAnchorExtensions)
.extensions(tableExtension)
.attributeProviderFactory(new AttributeProviderFactory() {
public AttributeProvider create(AttributeProviderContext context) {
return new CustomAttributeProvider();
}
})
.build();
return renderer.render(document);
}
/**
* 自定义 HTML 属性提供者
* - 链接标签自动添加 target="_blank"(新窗口打开)
* - 表格标签自动添加 CSS class(适配 UI 框架)
*/
static class CustomAttributeProvider implements AttributeProvider {
@Override
public void setAttributes(Node node, String tagName, Map<String, String> attributes) {
if (node instanceof Link) {
attributes.put("target", "_blank");
// 安全考虑:外部链接建议加上 rel="noopener noreferrer"
attributes.put("rel", "noopener noreferrer");
}
if (node instanceof TableBlock) {
attributes.put("class", "ui celled table");
}
}
}
}
关键点说明:
CustomAttributeProvider是 CommonMark 提供的扩展点,可以在渲染 HTML 时修改标签属性。上面的例子中,我们让所有链接在新窗口打开,并为表格添加了 Semantic UI 的样式类。rel="noopener noreferrer"是安全最佳实践,防止新窗口页面通过window.opener访问原页面。
2.3 Spring MVC 控制器集成
在实际项目中,通常需要从 classpath 或数据库读取 Markdown 文件,转换后传递给模板引擎渲染:
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Slf4j
@Controller
@RequestMapping(value = "/portal/docs/")
public class MarkdownToHtmlController {
@RequestMapping(value = "openapi-framework-{mdName}")
public String openapiFramework(
HttpServletRequest request,
HttpServletResponse response,
Model model,
@PathVariable("mdName") String mdName) throws IOException {
// 从 classpath 读取 Markdown 文件
String markdown = getJarResource(mdName);
// 转换为 HTML
String html = MarkdownToHtmlUtils.markdownToHtmlExtensions(markdown);
model.addAttribute("markdownToHtml", html);
return "/docs/docs_index";
}
private String getJarResource(String mdName) throws IOException {
Resource resource = new ClassPathResource("openapi/openapi-framework-" + mdName + ".md");
byte[] binaryData = FileCopyUtils.copyToByteArray(resource.getInputStream());
return new String(binaryData, StandardCharsets.UTF_8);
}
}
2.4 模板页面
在 Thymeleaf 或其他模板引擎中,使用 th:utext 或等效方式输出未转义的 HTML:
<!-- Thymeleaf 示例 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>文档</title>
<link rel="stylesheet" href="/css/markdown-style.css">
</head>
<body>
<article class="markdown-body" th:utext="${markdownToHtml}"></article>
</body>
</html>
注意:使用
utext(unescaped text)时要确保 Markdown 来源可信,否则可能存在 XSS 风险。下一节会讲解安全过滤方案。
三、方案二:使用 flexmark-java(推荐)
flexmark-java 是 commonmark-java 的增强版,支持 50 多种扩展,是生产环境的更优选择。
3.1 Maven 依赖
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>0.64.8</version>
</dependency>
3.2 基础使用
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.data.MutableDataSet;
import com.vladsch.flexmark.ext.tables.TablesExtension;
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension;
import com.vladsch.flexmark.ext.toc.TocExtension;
import com.vladsch.flexmark.ext.tasklist.TaskListExtension;
import java.util.Arrays;
public class FlexmarkDemo {
public static String convert(String markdown) {
// 配置扩展
MutableDataSet options = new MutableDataSet();
options.set(Parser.EXTENSIONS, Arrays.asList(
TablesExtension.create(), // GFM 表格
StrikethroughExtension.create(), // 删除线 ~~text~~
TocExtension.create(), // 目录 [TOC]
TaskListExtension.create() // 任务列表 - [ ] / - [x]
));
// 构建解析器和渲染器
Parser parser = Parser.builder(options).build();
HtmlRenderer renderer = HtmlRenderer.builder(options).build();
Node document = parser.parse(markdown);
return renderer.render(document);
}
}
3.3 代码语法高亮
flexmark-java 本身不包含代码高亮功能,但可以与外部高亮库配合。推荐方案是在渲染后的 HTML 中集成 highlight.js 或 Prism.js:
<!-- 在页面中引入 highlight.js -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>hljs.highlightAll();</script>
flexmark-java 会将代码块渲染为 <pre><code class="language-java">...</code></pre> 格式,highlight.js 会自动识别并高亮。
四、XSS 安全过滤
如果 Markdown 内容来自用户输入(如评论、帖子),必须进行 XSS 过滤,防止恶意用户注入脚本:
// 使用 Jsoup 进行 HTML 清洗
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
public class HtmlSanitizer {
public static String sanitize(String html) {
// 定义允许的安全标签和属性
Safelist safelist = Safelist.relaxed()
.addTags("span", "div", "pre", "code")
.addAttributes("code", "class") // 保留代码语言标识
.addAttributes("a", "target", "rel") // 保留链接属性
.addProtocols("img", "src", "http", "https", "data");
return Jsoup.clean(html, safelist);
}
}
处理流程:Markdown → 解析为 HTML → Jsoup 清洗 → 输出到页面
五、样式方案
Markdown 转 HTML 后,需要配合 CSS 才能获得良好的显示效果。推荐使用以下开源样式:
| 样式方案 | 特点 | 地址 |
|---|---|---|
| GitHub Markdown CSS | 仿 GitHub 风格,最流行 | sindresorhus/github-markdown-css |
| Markdown CSS | 简洁现代风格 | stewartlord/markdown.css |
| 手写 CSS | 完全自定义,匹配项目风格 | 自行编写 |
六、总结
| 场景 | 推荐方案 |
|---|---|
| 简单文档展示 | commonmark-java + 表格/锚点扩展 |
| 功能丰富的内容平台 | flexmark-java + 多种扩展 |
| 用户生成内容(UGC) | 上述方案 + Jsoup XSS 过滤 |
| 需要代码高亮 | 后端转换 + 前端 highlight.js |
在实际项目中,Markdown 转 HTML 看似简单,但涉及扩展语法支持、安全过滤、样式适配等多个环节。选择成熟的库(flexmark-java)并配合安全清洗(Jsoup),可以在保证功能完整的同时避免常见的安全陷阱。