Java 如何用 tomcat 9.0 , flexmark 0.6 和 mermaid.min.js 10.9 版本来实现 Markdown 中 Mermaid 图表的渲染,请提供一个完整可运行的示例方案。逻辑参考上一篇 server.js
完整实现方案:Tomcat 9 + Flexmark 0.6 + Mermaid 10.9 渲染 Markdown 中的 Mermaid 图表
一、技术栈说明
| 组件 | 版本要求 | 核心作用 |
|---|---|---|
| Tomcat | 9.0.x | Java Web 服务器,部署 Web 应用 |
| Flexmark | 0.64.8(兼容0.6+) | Markdown 解析渲染核心库 |
| Flexmark-ext-mermaid | 0.64.8 | Mermaid 语法扩展插件 |
| Mermaid.js | 10.9.0 | 前端渲染 Mermaid 图表(流程图/关系图等) |
| JDK | 8+ | 运行 Java 应用 |
二、项目结构(Maven Web 工程)
MermaidMarkdownWeb/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── mermaid/
│ │ │ ├── controller/
│ │ │ │ └── MarkdownController.java // 核心接口控制器
│ │ │ ├── util/
│ │ │ │ └── CsvToMermaidUtil.java // TXT→Mermaid转换工具(参考server.js逻辑)
│ │ │ └── config/
│ │ │ └── FlexmarkConfig.java // Flexmark配置类
│ │ ├── resources/
│ │ └── webapp/
│ │ ├── index.html // 前端上传页面
│ │ ├── WEB-INF/
│ │ │ ├── web.xml // Web配置(可选,用注解可省略)
│ │ │ └── lib/ // 若不使用Maven,手动导入JAR包
│ │ └── static/
│ │ └── js/
│ │ └── mermaid.min.js // 10.9版本Mermaid前端库
│ └── test/
└── pom.xml // Maven依赖配置
三、核心配置与代码实现
1. Maven 依赖(pom.xml)
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>MermaidMarkdownWeb</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>
<!-- Servlet API(Tomcat 9兼容) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<!-- Flexmark 核心 + Mermaid扩展 -->
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-all</artifactId>
<version>${flexmark.version}</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-ext-mermaid</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>MermaidMarkdownWeb</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)
核心作用:配置 Markdown 解析规则,启用 Mermaid 扩展(参考 server.js 中 markdown-it 的 mermaid 插件逻辑)
java
package com.mermaid.config;
import com.vladsch.flexmark.ext.mermaid.MermaidExtension;
import com.vladsch.flexmark.html.HtmlRenderer;
import com.vladsch.flexmark.parser.Parser;
import com.vladsch.flexmark.util.data.MutableDataSet;
public class FlexmarkConfig {
// 初始化 Markdown 解析器和 HTML 渲染器
public static Parser getParser() {
MutableDataSet options = new MutableDataSet();
// 启用基础功能(类似 server.js 的 html: true, linkify: true)
options.set(Parser.ENABLE_HTML_BLOCKS, true);
options.set(Parser.ENABLE_HTML_INLINE, true);
options.set(Parser.LINKIFY, true);
// 启用 Mermaid 扩展(关键:支持 ```mermaid 代码块)
options.set(Parser.EXTENSIONS, java.util.Collections.singletonList(MermaidExtension.create()));
return Parser.builder(options).build();
}
public static HtmlRenderer getHtmlRenderer() {
MutableDataSet options = new MutableDataSet();
// 配置 Mermaid 渲染(生成 <div class="mermaid"> 标签,与 server.js 一致)
options.set(HtmlRenderer.EXTENSIONS, java.util.Collections.singletonList(MermaidExtension.create()));
return HtmlRenderer.builder(options).build();
}
}
3. TXT→Mermaid 转换工具(CsvToMermaidUtil.java)
完全参考 server.js 的 validateAndConvertCsvToMermaid 逻辑,实现 TXT(CSV格式)到 Mermaid 代码块的转换
java
package com.mermaid.util;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class CsvToMermaidUtil {
/**
* 校验并转换 TXT(CSV格式)为 Mermaid 代码块
* @param fileName 文件名
* @param content 文件内容
* @return 转换结果(valid:是否成功,content:转换后内容/错误信息)
*/
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 文本文件格式错");
}
// 校验首行格式(必须是 source,target,weight)
String firstLine = lines.get(0);
if (!"source,target,weight".equals(firstLine)) {
return new Result(false, ".txt 文本文件格式错");
}
// 生成 Mermaid 横向流程图(graph LR,与 server.js 一致)
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() : "";
// 避免节点名冲突,添加前缀(与 server.js 一致)
String sourceNode = "node_" + source;
String targetNode = "node_" + target;
// 拼接权重标签(有则显示,无则省略)
String weightLabel = weight.isEmpty() ? "" : "|" + weight + "|";
// 拼接 Mermaid 语法:node_张三["张三"] -->|10000| node_李四["李四"]
mermaidContent.append(" ").append(sourceNode)
.append("[\"").append(source).append("\"] -->")
.append(weightLabel).append(" ")
.append(targetNode).append("[\"").append(target).append("\"]\n");
}
// 包装为 Markdown 代码块(含标题和转换时间)
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;
}
// getter/setter
public boolean isValid() { return valid; }
public String getContent() { return content; }
}
}
4. 核心控制器(MarkdownController.java)
处理文件上传、格式校验、转换、Markdown渲染,对应 server.js 的 / 和 /upload-md 接口
java
package com.mermaid.controller;
import com.mermaid.config.FlexmarkConfig;
import com.mermaid.util.CsvToMermaidUtil;
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 {
// Flexmark 解析器和渲染器(全局单例,避免重复初始化)
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 {
// 根路径请求:返回前端上传页面(对应 server.js 的 app.get('/'))
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 {
// 1. 校验是否为文件上传请求
if (!ServletFileUpload.isMultipartContent(req)) {
out.write(buildErrorHtml("请通过文件上传表单提交!"));
return;
}
// 2. 初始化文件上传组件
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(5 * 1024 * 1024); // 限制文件大小 5MB(与 server.js 一致)
// 3. 解析上传文件
List<FileItem> items = upload.parseRequest(req);
FileItem fileItem = null;
for (FileItem item : items) {
if (!item.isFormField() && "mdFile".equals(item.getFieldName())) { // 对应前端表单的 name="mdFile"
fileItem = item;
break;
}
}
// 4. 校验文件是否存在
if (fileItem == null) {
out.write(buildErrorHtml("请选择要上传的文件!"));
return;
}
// 5. 校验文件类型(仅支持 .md 和 .txt,与 server.js 一致)
String fileName = fileItem.getName();
String fileExt = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
if (!".md".equals(fileExt) && !".txt".equals(fileExt)) {
out.write(buildErrorHtml("仅支持上传 .md 或 .txt 格式的文件!"));
return;
}
// 6. 读取文件内容
String fileContent = new String(fileItem.getInputStream().readAllBytes(), "UTF-8");
String renderContent = fileContent;
// 7. 若为 TXT 文件,执行转换(参考 server.js 的 txt 处理逻辑)
if (".txt".equals(fileExt)) {
CsvToMermaidUtil.Result convertResult = CsvToMermaidUtil.validateAndConvert(fileName, fileContent);
if (!convertResult.isValid()) {
out.write(buildErrorHtml(convertResult.getContent()));
return;
}
renderContent = convertResult.getContent();
}
// 8. Markdown 渲染为 HTML(含 Mermaid 代码块转换)
String html = HTML_RENDERER.render(MARKDOWN_PARSER.parse(renderContent));
// 9. 拼接最终 HTML(注入渲染结果到页面,加载 Mermaid.js)
String finalHtml = buildFinalHtml(html);
out.write(finalHtml);
} catch (Exception e) {
e.printStackTrace();
out.write(buildErrorHtml("文件处理出错,请稍后重试!"));
} finally {
out.close();
}
}
/**
* 构建错误页面(参考 server.js 的错误响应)
*/
private String buildErrorHtml(String message) {
return "<!DOCTYPE html>" +
"<html lang='zh-CN'>" +
"<head><meta charset='UTF-8'><title>处理失败</title></head>" +
"<body style='text-align:center;margin-top:50px;'>" +
"<h1 style='color:red;'>处理失败</h1>" +
"<p style='font-size:18px;'>" + message + "</p>" +
"<a href='/' style='font-size:16px;'>返回上传页面</a>" +
"</body></html>";
}
/**
* 构建最终渲染页面(注入 Mermaid.js,确保图表能在前端渲染)
*/
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; }" +
" blockquote { color: #7f8c8d; border-left: 4px solid #bdc3c7; padding-left: 10px; }" +
"</style>" +
// 引入 Mermaid 10.9 前端库(本地或CDN均可)
"<script src='./static/js/mermaid.min.js'></script>" +
"<script>" +
" // 初始化 Mermaid(确保图表渲染)" +
" mermaid.initialize({ startOnLoad: true, theme: 'default' });" +
"</script>" +
"</head>" +
"<body>" +
renderedMdHtml + // 注入 Flexmark 渲染后的 HTML(含 <div class="mermaid">)
"</body></html>";
}
}
5. 前端上传页面(index.html)
放在 webapp/ 目录下,提供文件选择和上传功能(对应 server.js 的 public/index.html)
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; }
.upload-container { border: 2px dashed #3498db; padding: 40px; border-radius: 8px; margin-top: 30px; }
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; }
button:hover { background: #2980b9; }
.tip { color: #7f8c8d; margin-top: 20px; }
</style>
</head>
<body>
<h1>Markdown + Mermaid 上传渲染工具</h1>
<div class="upload-container">
<form action="/upload-md" method="post" enctype="multipart/form-data">
<input type="file" name="mdFile" accept=".md,.txt" required>
<br>
<button type="submit">上传并渲染</button>
</form>
<p class="tip">支持格式:.md(直接渲染)、.txt(需符合 source,target,weight 格式,自动转换为Mermaid关系图)</p>
<p class="tip">文件大小限制:5MB</p>
</div>
</body>
</html>
6. Mermaid 前端库(mermaid.min.js)
- 下载 Mermaid 10.9.5 版本
curl -O https://cdn.jsdelivr.net/npm/mermaid@10.9.5/dist/mermaid.min.js - 放在
webapp/static/js/目录下(与 index.html 中引入路径一致)
四、部署与运行步骤
- 打包项目 :使用 Maven 执行
mvn clean package,生成MermaidMarkdownWeb.war文件 - 部署到 Tomcat :
- 将 war 文件复制到 Tomcat 的
webapps/目录下 - 启动 Tomcat(执行
bin/startup.bat或bin/startup.sh)
- 将 war 文件复制到 Tomcat 的
- 访问应用 :
- 浏览器输入
http://localhost:8080/MermaidMarkdownWeb(默认 Tomcat 端口 8080) - 若修改 Tomcat 端口,需对应调整访问地址
- 浏览器输入
五、功能验证(与 server.js 逻辑一致)
-
上传 .md 文件(含 Mermaid 语法):
-
示例 test1.md(与文档3一致):
markdown### 转账关系图 ```mermaid graph LR A[张三] -->|10000| B[李四] B[李四] -->|9000| C[王五] C[王五] -->|8000| D[刘哥] D[刘哥] -->|5000| A[张三] D[刘哥] -->|3000| B[李四] D[刘哥] -->|2000| C[王五] -
上传后会直接渲染 Markdown 内容和 Mermaid 流程图
-
-
上传 .txt 文件(符合 CSV 格式):
-
示例 test1.txt(与文档2一致):
source,target,weight 张三,李四,10000 李四,王五,9000 王五,刘哥,8000 刘哥,张三,5000 刘哥,李四,3000 刘哥,王五,2000 -
上传后会自动转换为 Mermaid 代码块,再渲染为流程图
-
-
错误场景验证:
- 上传非 .md/.txt 文件:提示"仅支持上传 .md 或 .txt 格式的文件!"
- 上传格式错误的 .txt 文件(首行不是 source,target,weight):提示".txt 文本文件格式错"
- 上传空文件:提示".txt 文本文件格式错"
六、关键逻辑对齐说明(与 server.js 对比)
| 功能点 | server.js(Node.js)实现 | Java(Tomcat+Flexmark)实现 |
|---|---|---|
| 文件类型校验 | multer 的 fileFilter 过滤 .md/.txt | Servlet 中判断文件后缀 |
| 文件大小限制 | multer 配置 limits: 5MB | ServletFileUpload 设置 setFileSizeMax(5MB) |
| TXT→Mermaid 转换 | 解析 CSV 格式,生成 graph LR 语法 | 完全相同的解析逻辑和 Mermaid 语法生成 |
| Mermaid 渲染支持 | markdown-it 插件生成 | Flexmark-mermaid 扩展生成相同格式标签 |
| 前端渲染 | 页面引入 mermaid.js | 相同方式引入 mermaid 10.9 版本 |
| 错误处理 | 返回 HTML 错误页面+返回按钮 | 相同的错误页面样式和跳转逻辑 |
七、注意事项
- Tomcat 端口冲突 :若 8080 端口被占用,修改
conf/server.xml中的Connector port="8080"为其他端口(如 8000) - 文件编码:所有文件(.md/.txt)需使用 UTF-8 编码,避免中文乱码
- Mermaid 语法支持:支持流程图(graph LR/TD)、时序图、类图等 Mermaid 10.9 支持的所有语法
- 静态资源访问 :确保 mermaid.min.js 路径正确,若部署时修改了项目上下文路径(如
/mermaid),需同步调整 index.html 中<script src>路径
该方案完全基于 Java 技术栈实现,与原 Node.js 方案逻辑一致,可直接部署运行,支持 Markdown 原生 Mermaid 渲染和 TXT 格式转换功能。