跳到主要内容Java 基于 Tomcat 9 与 Flexmark 实现 Markdown 中 Mermaid 图表渲染 | 极客日志JavaNode.js大前端java
Java 基于 Tomcat 9 与 Flexmark 实现 Markdown 中 Mermaid 图表渲染
Java 使用 Tomcat 9 服务器部署 Web 应用,结合 Flexmark 0.64 解析 Markdown 并手动处理 Mermaid 代码块,通过正则替换标签适配前端 Mermaid.js 10.9 进行图表渲染。方案包含 Maven 依赖配置、Servlet 控制器文件上传处理、TXT 转 Mermaid 语法工具类及 web.xml 配置,解决了第三方扩展缺失问题,支持 .md 直接渲染与 .txt 格式转换,确保中文编码正确及静态资源加载正常。
灵魂摆渡1 浏览 Java 基于 Tomcat 9 与 Flexmark 实现 Markdown 中 Mermaid 图表渲染
一、技术栈说明
| 组件 | 版本要求 | 核心作用 |
|---|
| Tomcat | 9.0.x | Java Web 服务器,部署 Web 应用 |
| Flexmark | 0.64.8(兼容 0.64) | Markdown 解析渲染核心库 |
| Mermaid.js | 10.9.0 | 前端渲染 Mermaid 图表(流程图/关系图等) |
| JDK | 8+ | 运行 Java 应用 |
二、项目结构(Maven Web 工程)
MarkdownWeb/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── mermaid/
│ │ │ ├── controller/
│ │ │ │ └── MarkdownController.java
│ │ │ ├── util/
│ │ │ │ └── CsvToMermaidUtil.java
│ │ │ └── config/
│ │ │ └── FlexmarkConfig.java
│ │ ├── resources/
│ │ └── webapp/
│ │ ├── index.html
│ │ ├── WEB-INF/
│ │ │ └── web.xml
│ │ └── static/
│ │ └── js/
│ │ └── mermaid.min.js
│ └── test/
└── pom.xml
三、核心配置与代码实现
1. Maven 依赖(pom.xml)
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mermaid</groupId>
<artifactId>MarkdownWeb</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Mermaid Markdown Render WebApp</name>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<flexmark.version>0.64.8</flexmark.version>
<servlet-api.version>4.0.1</servlet-api.version>
<commons-fileupload.version>1.4</commons-fileupload.version>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>${flexmark.version}</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
</dependencies>
<build>
<finalName>MarkdownWeb</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. Flexmark 配置类(FlexmarkConfig.java)
package com.mermaid.config;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.data.MutableDataSet;
public class FlexmarkConfig {
public static Parser getParser() {
MutableDataSet options = new MutableDataSet();
options.set(Parser.ENABLE_HTML_BLOCKS, true);
options.set(Parser.ENABLE_HTML_INLINE, true);
options.set(Parser.LINKIFY, true);
return Parser.builder(options).build();
}
public static HtmlRenderer getHtmlRenderer() {
MutableDataSet options = new MutableDataSet();
return HtmlRenderer.builder(options).build();
}
}
3. TXT→Mermaid 转换工具(CsvToMermaidUtil.java)
package com.mermaid.util;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class CsvToMermaidUtil {
public static Result validateAndConvert(String fileName, String content) {
String normalizedContent = content.replace("\r\n", "\n").replace("\r", "\n");
String[] linesArr = normalizedContent.split("\n");
List<String> lines = new ArrayList<>();
for (String line : linesArr) {
if (!line.trim().isEmpty()) {
lines.add(line.trim());
}
}
if (lines.isEmpty()) {
return new Result(false, ".txt 文本文件格式错");
}
String firstLine = lines.get(0);
if (!"source,target,weight".equals(firstLine)) {
return new Result(false, ".txt 文本文件格式错");
}
StringBuilder mermaidContent = new StringBuilder("graph LR\n");
for (int i = 1; i < lines.size(); i++) {
String line = lines.get(i);
String[] parts = line.split(",");
if (parts.length < 2) continue;
String source = parts[0].trim();
String target = parts[1].trim();
String weight = parts.length >= 3 ? parts[2].trim() : "";
String sourceNode = "node_" + source;
String targetNode = "node_" + target;
String weightLabel = weight.isEmpty() ? "" : "|" + weight + "|";
mermaidContent.append(" ").append(sourceNode).append("[\"").append(source).append("\"] -->")
.append(weightLabel).append(" ").append(targetNode).append("[\"").append(target).append("\"]\n");
}
String dateStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
String finalMdContent = "# " + fileName + " 转换为 Md 关系图\n\n" +
"```mermaid\n" + mermaidContent.toString() + "```\n\n" +
"> 转换时间:" + dateStr;
return new Result(true, finalMdContent);
}
public static class Result {
private boolean valid;
private String content;
public Result(boolean valid, String content) { this.valid = valid; this.content = content; }
public boolean isValid() { return valid; }
public String getContent() { return content; }
}
}
4. 核心控制器(MarkdownController.java)
package com.mermaid.controller;
import com.mermaid.config.FlexmarkConfig;
import com.mermaid.util.CsvToMermaidUtil;
import com.mermaid.util.MermaidBlockParser;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@WebServlet(urlPatterns = {"/", "/upload-md"})
public class MarkdownController extends HttpServlet {
private static final Parser MARKDOWN_PARSER = FlexmarkConfig.getParser();
private static final HtmlRenderer HTML_RENDERER = FlexmarkConfig.getHtmlRenderer();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/index.html").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
try {
if (!ServletFileUpload.isMultipartContent(req)) {
out.write(buildErrorHtml("请通过文件上传表单提交!"));
return;
}
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(5 * 1024 * 1024);
List<FileItem> items = upload.parseRequest(req);
FileItem fileItem = null;
for (FileItem item : items) {
if (!item.isFormField() && "mdFile".equals(item.getFieldName())) {
fileItem = item;
break;
}
}
if (fileItem == null) {
out.write(buildErrorHtml("请选择要上传的文件!"));
return;
}
String fileName = fileItem.getName();
String fileExt = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
if (!".md".equals(fileExt) && !".txt".equals(fileExt)) {
out.write(buildErrorHtml("仅支持上传 .md 或 .txt 格式的文件!"));
return;
}
String fileContent = new String(fileItem.getInputStream().readAllBytes(), "UTF-8");
String renderContent = fileContent;
if (".txt".equals(fileExt)) {
CsvToMermaidUtil.Result convertResult = CsvToMermaidUtil.validateAndConvert(fileName, fileContent);
if (!convertResult.isValid()) {
out.write(buildErrorHtml(convertResult.getContent()));
return;
}
renderContent = convertResult.getContent();
}
String rawHtml = HTML_RENDERER.render(MARKDOWN_PARSER.parse(renderContent));
String finalHtml = MermaidBlockParser.replaceMermaidBlocks(rawHtml);
out.write(buildFinalHtml(finalHtml));
} catch (Exception e) {
e.printStackTrace();
out.write(buildErrorHtml("文件处理出错,请稍后重试!"));
} finally {
out.close();
}
}
private String buildErrorHtml(String message) {
return "<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><title>处理失败</title></head>" +
"<body><h1>处理失败</h1><p>" + message + "</p><a href='/'>返回上传页面</a></body></html>";
}
private String buildFinalHtml(String renderedMdHtml) {
return "<!DOCTYPE html><html lang='zh-CN'><head>" +
"<meta charset='UTF-8'><title>Markdown + Mermaid 渲染结果</title>" +
"<style>body { max-width: 1200px; margin: 0 auto; padding: 20px; font-family: sans-serif; }" +
".mermaid { margin: 20px 0; } h1 { color: #2c3e50; }</style>" +
"<script src='./static/js/mermaid.min.js'></script>" +
"<script>mermaid.initialize({ startOnLoad: true, theme: 'default' });</script></head>" +
"<body>" + renderedMdHtml + "</body></html>";
}
}
5. 前端上传页面(index.html)
<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><title>Markdown + Mermaid 上传渲染工具</title>
<style>body{max-width: 800px;margin: 0 auto;padding: 50px 20px;text-align: center;}input[type="file"]{margin: 20px 0;padding: 10px;font-size: 16px;}button{background: #3498db;color: white;border: none;padding: 12px 30px;font-size: 18px;border-radius: 4px;cursor: pointer;}</style></head>
<body><h1>Markdown + Mermaid 上传渲染工具</h1><div style="border: 2px dashed #3498db;padding: 40px;border-radius: 8px;margin-top: 30px;">
<form action="/upload-md" method="post" enctype="multipart/form-data"><input type="file" name="mdFile" accept=".md,.txt" required>
<button type="submit">上传并渲染</button></form><p>支持格式:.md(直接渲染)、.txt(需符合 source,target,weight 格式)</p></div></body></html>
6. Mermaid 前端库(mermaid.min.js)
下载 Mermaid 10.9 版本并放在 webapp/static/js/ 目录下:
四、部署与运行步骤
- 打包项目:使用 Maven 执行
mvn clean package,生成 MarkdownWeb.war 文件。
- 部署到 Tomcat:将 war 文件复制到 Tomcat 的
webapps/ 目录下,启动 Tomcat。
- 访问应用:浏览器输入
http://localhost:8080/MarkdownWeb。
五、功能验证
- 上传 .md 文件:含 Mermaid 语法,直接渲染。
- 上传 .txt 文件:符合 CSV 格式,自动转换为 Mermaid 代码块再渲染。
- 错误场景:非指定格式或空文件会提示错误信息。
六、关键逻辑对齐说明
| 功能点 | Node.js 实现 | Java 实现 |
|---|
| 文件类型校验 | multer 过滤 | Servlet 判断后缀 |
| 文件大小限制 | multer limits | ServletFileUpload setFileSizeMax |
| TXT→Mermaid 转换 | 解析 CSV 格式 | 完全相同的解析逻辑 |
| Mermaid 渲染 | markdown-it 插件 | 手动替换标签 + 前端渲染 |
七、注意事项
- 端口冲突:若 8080 被占用,修改
conf/server.xml 中的 Connector port。
- 文件编码:所有文件需使用 UTF-8 编码。
- 静态资源:确保 mermaid.min.js 路径正确。
八、Web 配置文件配置
在 src/main/webapp/WEB-INF/web.xml 中配置:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4.0.xsd"
version="4.0" metadata-complete="false">
<display-name>MarkdownWeb</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class>
<init-param><param-name>encoding</param-name><param-value>UTF-8</param-value></init-param>
</filter>
<filter-mapping><filter-name>CharacterEncodingFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping>
</web-app>
九、依赖调整说明(移除 flexmark-ext-mermaid)
由于 flexmark-ext-mermaid 包不存在,方案调整为不依赖第三方扩展。核心思路是:在 Flexmark 解析 Markdown 后,手动识别 ```mermaid 代码块并替换为 Mermaid 前端渲染所需的 <div> 标签。
新增 MermaidBlockParser.java 工具类,通过正则匹配 <pre><code class="language-mermaid"> 并替换为 <div class="mermaid">,同时解码 HTML 实体(如 < → <),确保前端 Mermaid.js 能正确识别语法。此方案兼容性更强,无需额外依赖。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online