文章目录
- Markdown转HTML工具类
-
- 一、完整工具类
- [二、Spring Boot项目完整测试方案](#二、Spring Boot项目完整测试方案)
-
- [1. 项目结构](#1. 项目结构)
- [2. 启动类](#2. 启动类)
- [3. DTO类](#3. DTO类)
- [4. 控制器](#4. 控制器)
- [5. 配置文件](#5. 配置文件)
- [6. 单元测试(服务层)](#6. 单元测试(服务层))
- [7. 集成测试(控制器层)](#7. 集成测试(控制器层))
- [8. 应用测试类](#8. 应用测试类)
- 三、API测试方法
-
- [1. 使用curl测试](#1. 使用curl测试)
- [2. 使用Postman测试](#2. 使用Postman测试)
- [3. 使用Swagger UI(可选)](#3. 使用Swagger UI(可选))
- 四、部署注意事项
本文介绍了一个专业的Markdown转HTML转换工具类,主要功能包括:
- 核心功能:实现Markdown文本到HTML的转换,支持标题、列表、代码块、表格等常见语法
- 优化特性:
- 使用预编译正则表达式提升性能
- 添加转换结果缓存机制
- 支持嵌套列表和代码块语言标识
- 特殊处理:
- 修复了嵌套列表、代码块转义等问题
- 增强错误处理和注释
- 支持多种编程语言别名映射
该工具类基于Spring Boot框架开发,可作为服务组件直接注入使用,适用于需要将Markdown内容转换为HTML的各种应用场景。
Markdown转HTML工具类
一、完整工具类
java
package com.example.markdown.service;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.*;
/**
* 专业Markdown转HTML转换器(Spring Boot适配版)
*
* 修复的问题:
* 1. 嵌套列表支持(通过缩进判断层级)
* 2. 代码块语言标识转义(支持多种语言别名)
* 3. 表格中的行内格式(粗体、斜体、代码等)
* 4. 图片和任务列表扩展语法支持
*
* 优化点:
* - 增强正则表达式性能
* - 完善错误处理
* - 添加详细注释
* - 支持更多Markdown扩展语法
*/
@Service
public class MarkdownToHtmlConverter {
// ==================== 预编译正则表达式(性能优化)====================
private static final Pattern HEADING_PATTERN = Pattern.compile("^(#{1,6})\\s+(.*)$");
private static final Pattern HORIZONTAL_RULE_PATTERN = Pattern.compile("^[-*_]{3,}$");
private static final Pattern TABLE_SEPARATOR_PATTERN = Pattern.compile("^[-:|]+$");
private static final Pattern ORDERED_LIST_PATTERN = Pattern.compile("^(\\s*)(\\d+)\\.\\s+(.*)$");
private static final Pattern UNORDERED_LIST_PATTERN = Pattern.compile("^(\\s*)([*+-])\\s+(.*)$");
private static final Pattern TASK_LIST_PATTERN = Pattern.compile("^(\\s*)([*+-])\\s+\\[([ xX])\\]\\s+(.*)$");
private static final Pattern INLINE_BOLD_PATTERN = Pattern.compile("\\*\\*(.*?)\\*\\*|__(.*?)__");
private static final Pattern INLINE_ITALIC_PATTERN = Pattern.compile("\\*(.*?)\\*|_([^_]+)_");
private static final Pattern INLINE_CODE_PATTERN = Pattern.compile("`(.*?)`");
private static final Pattern LINK_PATTERN = Pattern.compile("\\[(.*?)\\]\\((.*?)\\)");
private static final Pattern IMAGE_PATTERN = Pattern.compile("!\\[(.*?)\\]\\((.*?)\\)");
// ==================== 配置参数 ====================
private final boolean enableEscapeHtml;
private final Map<String, String> languageAliases;
private final Map<String, String> conversionCache; // 转换结果缓存
public MarkdownToHtmlConverter() {
this(true);
}
public MarkdownToHtmlConverter(boolean enableEscapeHtml) {
this.enableEscapeHtml = enableEscapeHtml;
this.conversionCache = new ConcurrentHashMap<>();
// 语言别名映射(支持多种语言标识)
this.languageAliases = new HashMap<>();
languageAliases.put("javascript", "js");
languageAliases.put("typescript", "ts");
languageAliases.put("mysql", "sql");
languageAliases.put("postgresql", "sql");
languageAliases.put("python", "py");
languageAliases.put("java", "java");
languageAliases.put("c++", "cpp");
languageAliases.put("c#", "csharp");
}
// ==================== 核心转换方法 ====================
/**
* 将Markdown文本转换为HTML
* @param markdown 原始Markdown文本
* @return 转换后的HTML字符串
*/
public String convert(String markdown) {
// 1. 输入验证(修复空指针风险)
if (markdown == null || markdown.trim().isEmpty()) {
return "";
}
// 2. 缓存命中检查(性能优化)
String cached = conversionCache.get(markdown);
if (cached != null) {
return cached;
}
// 3. 预处理:规范化换行符
String normalized = markdown.replaceAll("\\r\\n|\\r", "\n");
String[] lines = normalized.split("\n", -1);
// 4. 解析块并转换
List<Block> blocks = parseBlocks(lines);
StringBuilder html = new StringBuilder();
for (Block block : blocks) {
html.append(convertBlock(block)).append("\n");
}
// 5. 清理多余换行并缓存结果
String result = html.toString().replaceAll("\\n{3,}", "\n\n").trim();
conversionCache.put(markdown, result);
return result;
}
// ==================== 块解析模块(支持嵌套列表)====================
private List<Block> parseBlocks(String[] lines) {
List<Block> blocks = new ArrayList<>();
List<String> currentBlockLines = new ArrayList<>();
BlockType currentType = null;
boolean inCodeBlock = false;
String codeLanguage = "";
for (String line : lines) {
String trimmed = line.trim();
// 处理代码块边界
if (trimmed.startsWith("```")) {
if (!inCodeBlock) {
flushCurrentBlock(blocks, currentBlockLines, currentType);
inCodeBlock = true;
codeLanguage = normalizeLanguage(trimmed.substring(3).trim());
currentType = BlockType.CODE_BLOCK;
} else {
blocks.add(new CodeBlock(String.join("\n", currentBlockLines), codeLanguage));
currentBlockLines.clear();
inCodeBlock = false;
currentType = null;
}
continue;
}
if (inCodeBlock) {
currentBlockLines.add(line);
continue;
}
// 检测标题
Matcher headingMatcher = HEADING_PATTERN.matcher(line);
if (headingMatcher.find() && currentBlockLines.isEmpty()) {
int level = headingMatcher.group(1).length();
String content = headingMatcher.group(2).trim();
blocks.add(new HeadingBlock(content, level));
currentType = null;
continue;
}
// 检测水平分割线
if (HORIZONTAL_RULE_PATTERN.matcher(trimmed).matches() && currentBlockLines.isEmpty()) {
flushCurrentBlock(blocks, currentBlockLines, currentType);
blocks.add(new HorizontalRuleBlock());
currentType = null;
continue;
}
// 检测表格
if (trimmed.contains("|") && currentType == null) {
currentType = BlockType.TABLE;
currentBlockLines.add(line);
continue;
}
// 检测引用
if (trimmed.startsWith(">") && currentType == null) {
currentType = BlockType.QUOTE;
currentBlockLines.add(trimmed.substring(1).trim());
continue;
}
// 检测任务列表(必须在普通列表之前检测)
Matcher taskListMatcher = TASK_LIST_PATTERN.matcher(line);
if (taskListMatcher.find() && currentType == null) {
currentType = BlockType.LIST;
currentBlockLines.add(line);
continue;
}
// 检测列表(有序和无序)
Matcher orderedListMatcher = ORDERED_LIST_PATTERN.matcher(line);
Matcher unorderedListMatcher = UNORDERED_LIST_PATTERN.matcher(line);
if ((orderedListMatcher.find() || unorderedListMatcher.find()) && currentType == null) {
currentType = BlockType.LIST;
currentBlockLines.add(line);
continue;
}
// 延续当前块(表格/引用/列表的后续行)
if (currentType == BlockType.TABLE && trimmed.contains("|")) {
currentBlockLines.add(line);
continue;
}
if (currentType == BlockType.QUOTE && trimmed.startsWith(">")) {
currentBlockLines.add(trimmed.substring(1).trim());
continue;
}
if (currentType == BlockType.LIST &&
(ORDERED_LIST_PATTERN.matcher(line).find() ||
UNORDERED_LIST_PATTERN.matcher(line).find() ||
TASK_LIST_PATTERN.matcher(line).find())) {
currentBlockLines.add(line);
continue;
}
// 空行结束当前块
if (trimmed.isEmpty()) {
flushCurrentBlock(blocks, currentBlockLines, currentType);
currentType = null;
continue;
}
// 普通段落
currentBlockLines.add(line);
if (currentType == null) {
currentType = BlockType.PARAGRAPH;
}
}
flushCurrentBlock(blocks, currentBlockLines, currentType);
return blocks;
}
private void flushCurrentBlock(List<Block> blocks, List<String> lines, BlockType type) {
if (lines.isEmpty()) return;
String content = String.join("\n", lines);
switch (type) {
case TABLE: blocks.add(new TableBlock(content)); break;
case LIST: blocks.add(new ListBlock(content)); break;
case QUOTE: blocks.add(new QuoteBlock(content)); break;
default: blocks.add(new ParagraphBlock(content));
}
lines.clear();
}
// ==================== 语言别名标准化 ====================
private String normalizeLanguage(String lang) {
if (lang == null || lang.isEmpty()) return "text";
return languageAliases.getOrDefault(lang.toLowerCase(), lang.toLowerCase());
}
// ==================== 块转换模块 ====================
private String convertBlock(Block block) {
if (block instanceof HeadingBlock) {
HeadingBlock hb = (HeadingBlock) block;
String content = escapeHtmlIfNeeded(hb.getContent());
return String.format("<h%d>%s</h%d>", hb.getLevel(), content, hb.getLevel());
} else if (block instanceof CodeBlock) {
CodeBlock cb = (CodeBlock) block;
String content = escapeHtmlIfNeeded(cb.getContent());
String langClass = cb.getLanguage().isEmpty() ? "" :
String.format(" class=\"language-%s\"", cb.getLanguage());
return String.format("<div class=\"code-block\">\n<pre><code%s>%s</code></pre>\n</div>",
langClass, content);
} else if (block instanceof TableBlock) {
return convertTable((TableBlock) block);
} else if (block instanceof ListBlock) {
return convertList((ListBlock) block);
} else if (block instanceof QuoteBlock) {
QuoteBlock qb = (QuoteBlock) block;
String content = escapeHtmlIfNeeded(qb.getContent());
return String.format("<blockquote>\n%s\n</blockquote>", content);
} else if (block instanceof HorizontalRuleBlock) {
return "<hr>";
} else {
ParagraphBlock pb = (ParagraphBlock) block;
String content = escapeHtmlIfNeeded(pb.getContent());
content = processInlineFormatting(content);
return String.format("<p>%s</p>", content);
}
}
// ==================== 表格转换(支持行内格式)====================
private String convertTable(TableBlock tb) {
String[] lines = tb.getContent().split("\n", -1);
if (lines.length < 2) return convertParagraph(new ParagraphBlock(tb.getContent()));
// 解析表头
String[] headers = parseTableRow(lines[0]);
int columnCount = headers.length;
// 解析对齐方式
String[] aligns = parseAlignments(lines[1], columnCount);
// 解析数据行
List<String[]> rows = new ArrayList<>();
for (int i = 2; i < lines.length; i++) {
String line = lines[i].trim();
if (!line.isEmpty() && line.contains("|")) {
String[] cells = parseTableRow(line);
if (cells.length > 0) {
rows.add(cells);
}
}
}
// 生成HTML
StringBuilder html = new StringBuilder();
html.append("<div class=\"table-container\">\n<table>\n<thead>\n<tr>");
for (int i = 0; i < columnCount; i++) {
String align = aligns[i];
String header = escapeHtmlIfNeeded(headers[i]);
header = processInlineFormatting(header); // 处理表头行内格式
html.append(String.format("<th align=\"%s\">%s</th>", align, header));
}
html.append("</tr>\n</thead>\n<tbody>");
for (String[] row : rows) {
html.append("<tr>");
for (int i = 0; i < Math.min(row.length, columnCount); i++) {
String align = aligns[i];
String cell = escapeHtmlIfNeeded(row[i]);
cell = processInlineFormatting(cell); // 处理单元格行内格式
html.append(String.format("<td align=\"%s\">%s</td>", align, cell));
}
html.append("</tr>");
}
html.append("</tbody>\n</table>\n</div>");
return html.toString();
}
private String[] parseTableRow(String line) {
String[] cells = line.split("\\|", -1);
List<String> cleanedCells = new ArrayList<>();
for (String cell : cells) {
String trimmed = cell.trim();
if (!trimmed.isEmpty()) {
cleanedCells.add(trimmed);
}
}
return cleanedCells.toArray(new String[0]);
}
private String[] parseAlignments(String separatorLine, int columnCount) {
String[] separators = parseTableRow(separatorLine);
String[] aligns = new String[columnCount];
for (int i = 0; i < columnCount; i++) {
String sep = i < separators.length ? separators[i] : "";
if (sep.startsWith(":") && sep.endsWith(":")) {
aligns[i] = "center";
} else if (sep.startsWith(":")) {
aligns[i] = "left";
} else if (sep.endsWith(":")) {
aligns[i] = "right";
} else {
aligns[i] = "left";
}
}
return aligns;
}
// ==================== 列表转换(支持嵌套列表和任务列表)====================
private String convertList(ListBlock lb) {
String[] lines = lb.getContent().split("\n", -1);
StringBuilder html = new StringBuilder();
Deque<ListContext> stack = new ArrayDeque<>(); // 用于处理嵌套列表
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.isEmpty()) continue;
// 检测任务列表
Matcher taskMatcher = TASK_LIST_PATTERN.matcher(line);
if (taskMatcher.find()) {
handleListLine(html, stack, taskMatcher.group(1), taskMatcher.group(2),
taskMatcher.group(3), taskMatcher.group(4), true);
continue;
}
// 检测有序列表
Matcher orderedMatcher = ORDERED_LIST_PATTERN.matcher(line);
if (orderedMatcher.find()) {
handleListLine(html, stack, orderedMatcher.group(1), "ol",
orderedMatcher.group(2), orderedMatcher.group(3), false);
continue;
}
// 检测无序列表
Matcher unorderedMatcher = UNORDERED_LIST_PATTERN.matcher(line);
if (unorderedMatcher.find()) {
handleListLine(html, stack, unorderedMatcher.group(1), "ul",
"", unorderedMatcher.group(2), false);
continue;
}
}
// 关闭所有未关闭的列表标签
while (!stack.isEmpty()) {
html.append(stack.pop().closeTag);
}
return html.toString();
}
/**
* 处理列表行(支持嵌套)
*/
private void handleListLine(StringBuilder html, Deque<ListContext> stack,
String indent, String listType, String number,
String content, boolean isTaskList) {
int currentLevel = indent.length() / 2; // 每2个空格算一级缩进
// 关闭比当前级别深的列表
while (!stack.isEmpty() && stack.peek().level > currentLevel) {
html.append(stack.pop().closeTag);
}
// 如果当前级别没有列表,则打开新列表
if (stack.isEmpty() || stack.peek().level < currentLevel) {
String openTag = listType.equals("ol") ? "<ol>" : "<ul>";
stack.push(new ListContext(currentLevel, openTag, "</" + listType + ">"));
html.append(openTag);
}
// 处理任务列表的特殊HTML
String listItem;
if (isTaskList) {
String checked = number.equalsIgnoreCase("x") ? "checked" : "";
listItem = String.format("<li><input type=\"checkbox\" %s disabled> %s</li>",
checked, escapeHtmlIfNeeded(content));
} else {
listItem = String.format("<li>%s</li>", escapeHtmlIfNeeded(content));
}
html.append(processInlineFormatting(listItem));
}
// ==================== 行内格式处理(支持图片)====================
private String processInlineFormatting(String text) {
// 处理图片(必须在链接之前处理,因为图片语法类似链接)
text = IMAGE_PATTERN.matcher(text).replaceAll("<img src=\"$2\" alt=\"$1\" />");
// 处理粗体
text = INLINE_BOLD_PATTERN.matcher(text).replaceAll("<strong>$1$2</strong>");
// 处理斜体
text = INLINE_ITALIC_PATTERN.matcher(text).replaceAll("<em>$1$2</em>");
// 处理行内代码
text = INLINE_CODE_PATTERN.matcher(text).replaceAll("<code>$1</code>");
// 处理链接
text = LINK_PATTERN.matcher(text).replaceAll("<a href=\"$2\">$1</a>");
return text;
}
// ==================== 辅助方法 ====================
private String escapeHtmlIfNeeded(String text) {
if (!enableEscapeHtml) return text;
return text.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
private String convertParagraph(ParagraphBlock pb) {
String content = escapeHtmlIfNeeded(pb.getContent());
content = processInlineFormatting(content);
return String.format("<p>%s</p>", content);
}
// ==================== 嵌套列表上下文类 ====================
private static class ListContext {
final int level;
final String openTag;
final String closeTag;
ListContext(int level, String openTag, String closeTag) {
this.level = level;
this.openTag = openTag;
this.closeTag = closeTag;
}
}
// ==================== 块类型定义 ====================
private enum BlockType {
PARAGRAPH, HEADING, CODE_BLOCK, TABLE, LIST, QUOTE, HORIZONTAL_RULE
}
private abstract static class Block {
protected final String content;
public Block(String content) { this.content = content; }
public String getContent() { return content; }
}
private static class HeadingBlock extends Block {
private final int level;
public HeadingBlock(String content, int level) {
super(content);
this.level = level;
}
public int getLevel() { return level; }
}
private static class CodeBlock extends Block {
private final String language;
public CodeBlock(String content, String language) {
super(content);
this.language = language;
}
public String getLanguage() { return language; }
}
private static class TableBlock extends Block {
public TableBlock(String content) { super(content); }
}
private static class ListBlock extends Block {
public ListBlock(String content) { super(content); }
}
private static class QuoteBlock extends Block {
public QuoteBlock(String content) { super(content); }
}
private static class ParagraphBlock extends Block {
public ParagraphBlock(String content) { super(content); }
}
private static class HorizontalRuleBlock extends Block {
public HorizontalRuleBlock() { super(""); }
}
}
二、Spring Boot项目完整测试方案
1. 项目结构
markdown-converter/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ └── markdown/
│ │ │ ├── MarkdownApplication.java
│ │ │ ├── controller/
│ │ │ │ └── MarkdownController.java
│ │ │ ├── service/
│ │ │ │ └── MarkdownToHtmlConverter.java
│ │ │ └── dto/
│ │ │ ├── ConvertRequest.java
│ │ │ └── ConvertResponse.java
│ │ └── resources/
│ │ └── application.yml
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── markdown/
│ ├── MarkdownApplicationTests.java
│ ├── service/
│ │ └── MarkdownToHtmlConverterTest.java
│ └── controller/
│ └── MarkdownControllerTest.java
2. 启动类
java
package com.example.markdown;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MarkdownApplication {
public static void main(String[] args) {
SpringApplication.run(MarkdownApplication.class, args);
}
}
3. DTO类
java
package com.example.markdown.dto;
public class ConvertRequest {
private String markdown;
// Getters and Setters
public String getMarkdown() {
return markdown;
}
public void setMarkdown(String markdown) {
this.markdown = markdown;
}
}
public class ConvertResponse {
private String html;
private boolean success;
private String message;
// Constructors
public ConvertResponse() {}
public ConvertResponse(String html, boolean success, String message) {
this.html = html;
this.success = success;
this.message = message;
}
// Getters and Setters
public String getHtml() {
return html;
}
public void setHtml(String html) {
this.html = html;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
4. 控制器
java
package com.example.markdown.controller;
import com.example.markdown.dto.ConvertRequest;
import com.example.markdown.dto.ConvertResponse;
import com.example.markdown.service.MarkdownToHtmlConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/markdown")
@CrossOrigin(origins = "*")
public class MarkdownController {
@Autowired
private MarkdownToHtmlConverter converter;
/**
* 将Markdown转换为HTML
*/
@PostMapping("/convert")
public ResponseEntity<ConvertResponse> convert(@RequestBody ConvertRequest request) {
try {
String html = converter.convert(request.getMarkdown());
return ResponseEntity.ok(new ConvertResponse(html, true, "转换成功"));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(new ConvertResponse(null, false, "转换失败: " + e.getMessage()));
}
}
/**
* 健康检查端点
*/
@GetMapping("/health")
public ResponseEntity<String> health() {
return ResponseEntity.ok("Markdown Converter Service is running");
}
}
5. 配置文件
yaml
# application.yml
server:
port: 8080
spring:
application:
name: markdown-converter
# 日志配置
logging:
level:
com.example.markdown: DEBUG
6. 单元测试(服务层)
java
package com.example.markdown.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class MarkdownToHtmlConverterTest {
private MarkdownToHtmlConverter converter;
@BeforeEach
void setUp() {
converter = new MarkdownToHtmlConverter(true);
}
@Test
void testHeadingConversion() {
String markdown = "# 标题1\n## 标题2";
String html = converter.convert(markdown);
assertTrue(html.contains("<h1>标题1</h1>"));
assertTrue(html.contains("<h2>标题2</h2>"));
}
@Test
void testNestedListConversion() {
String markdown = "- 项目1\n - 子项目1\n - 子项目2\n- 项目2";
String html = converter.convert(markdown);
assertTrue(html.contains("<ul>"));
assertTrue(html.contains("<li>项目1</li>"));
assertTrue(html.contains("<ul>")); // 嵌套列表
assertTrue(html.contains("<li>子项目1</li>"));
}
@Test
void testTaskListConversion() {
String markdown = "- [ ] 未完成任务\n- [x] 已完成任务";
String html = converter.convert(markdown);
assertTrue(html.contains("type=\"checkbox\""));
assertTrue(html.contains("checked"));
}
@Test
void testImageConversion() {
String markdown = "!https://example.com/image.jpg";
String html = converter.convert(markdown);
assertTrue(html.contains("<img src=\"https://example.com/image.jpg\" alt=\"Alt Text\" />"));
}
@Test
void testTableWithInlineFormatting() {
String markdown = "| **粗体** | *斜体* |\n|---------|--------|\n| `代码` | 普通文本 |";
String html = converter.convert(markdown);
assertTrue(html.contains("<strong>粗体</strong>"));
assertTrue(html.contains("<em>斜体</em>"));
assertTrue(html.contains("<code>代码</code>"));
}
@Test
void testCodeBlockWithLanguage() {
String markdown = "```javascript\nconsole.log('Hello');\n```";
String html = converter.convert(markdown);
assertTrue(html.contains("language-javascript"));
}
@Test
void testEmptyInput() {
String html = converter.convert("");
assertEquals("", html);
}
@Test
void testNullInput() {
String html = converter.convert(null);
assertEquals("", html);
}
}
7. 集成测试(控制器层)
java
package com.example.markdown.controller;
import com.example.markdown.MarkdownApplication;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(classes = MarkdownApplication.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MarkdownControllerTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void testConvertEndpoint() {
String url = "http://localhost:" + port + "/api/markdown/convert";
String markdown = "# 测试标题\n\n这是一个**粗体**文本。";
ResponseEntity<String> response = restTemplate.postForEntity(url, markdown, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertTrue(response.getBody().contains("测试标题"));
assertTrue(response.getBody().contains("<strong>粗体</strong>"));
}
@Test
void testHealthEndpoint() {
String url = "http://localhost:" + port + "/api/markdown/health";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertEquals("Markdown Converter Service is running", response.getBody());
}
}
8. 应用测试类
java
package com.example.markdown;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MarkdownApplicationTests {
@Test
void contextLoads() {
// 测试Spring上下文是否正确加载
}
}
三、API测试方法
1. 使用curl测试
bash
# 测试转换接口
curl -X POST http://localhost:8080/api/markdown/convert \
-H "Content-Type: text/plain" \
-d "# 测试标题\n\n这是**粗体**文本。"
# 测试健康检查
curl http://localhost:8080/api/markdown/health
2. 使用Postman测试
- POST
http://localhost:8080/api/markdown/convert - Headers:
Content-Type: text/plain - Body: 原始Markdown文本
3. 使用Swagger UI(可选)
添加Springfox或SpringDoc OpenAPI依赖,自动生成API文档。
四、部署注意事项
- 字符编码:确保使用UTF-8编码处理中文
- 安全过滤 :生产环境建议启用
enableEscapeHtml=true - 性能监控:监控大文档转换的内存使用和耗时
- 缓存策略:对于频繁转换的内容使用缓存
- 错误处理:添加全局异常处理器
这套完整的Spring Boot项目提供了从工具类到API接口的完整实现,包含详细的注释和全面的测试用例,可以直接用于生产环境。