[Markdown] Markdown 及文档格式转换

news/2024/10/8 10:07:14/文章来源:https://www.cnblogs.com/johnnyzen/p/18451095

1 概述 : Markdown

Markdown 的诞生

  • 什么是 Markdown? Markdown 的诞生初衷

Markdown 是一种用于编写结构化文档的纯文本格式,基于在电子邮件和 usenet 帖子中指示格式的约定。
它由 John Gruber 开发(在 Aaron Swartz 的帮助下),并于 2004 年以 语法描述 和用于将 Markdown 转换为 HTML 的 Perl 脚本 ( Markdown.pl) 的形式发布。
在接下来的十年中,许多语言开发了数十种实现。有些扩展了原始 Markdown 语法,增加了脚注、表格和其他文档元素的约定。
有些允许以 HTML 以外的格式呈现 Markdown 文档。Reddit、StackOverflow 和 GitHub 等网站有数百万人使用 Markdown。Markdown 开始在网络之外用于创作书籍、文章、幻灯片、信件和讲义。
Markdown 与许多其他轻量级标记语法的区别在于它的可读性,而这些语法通常更容易编写。
正如 Gruber 所写:

Markdown 格式语法的首要设计目标是使其尽可能易于阅读。
其理念是,Markdown 格式的文档应可按原样以纯文本形式发布,而不像是用标签或格式说明标记的。(http://daringfireball.net/projects/markdown/)

《Markdown 规范》

  • 《Markdown 规范》
  • https://spec.commonmark.org/0.28/
  • 为什么需要 Markdown 规范?

John Gruber对 Markdown 语法的规范描述 并未明确说明语法。以下是它未回答的一些问题示例:

子列表需要缩进多少?规范规定,后续段落需要缩进四个空格,但对子列表没有完全明确规定。人们自然会认为它们也必须缩进四个空格,但这Markdown.pl并不是必需的。这几乎不是一个“极端情况”,在实际文档中,不同实现在这个问题上的分歧常常会给用户带来意外。(请参阅John Gruber 的此评论。)

块引用或标题前是否需要空行?大多数实现不需要空行。但是,这可能会导致文本硬换行的意外结果,并且还会导致解析中的歧义(请注意,某些实现将标题放在块引用内,而其他实现则不这样做)。(John Gruber 也表示支持要求空行。)

缩进的代码块之前是否需要空行?(Markdown.pl需要它,但是文档中没有提到它,并且有些实现不需要它。)

确定列表项何时被包裹在标签中的确切规则是什么<p>?列表可以部分“松散”而部分“紧密”吗?我们应该如何处理这样的列表?

...

  • 遵循本规范的开源组件
  • commonmark-java
  • flexmark-java
  • ...

2 Markdown 转 HTML

依赖组件

  • [Java] commonmark-java 【推荐】
  • 推荐原因: 社区持续活跃度高、组件相对更为成熟
  • 口号

Java library for parsing and rendering Markdown text according to the CommonMark specification (and some extensions).
这是一个Java库,用于根据 CommonMark 规范(以及一些扩展)解析和渲染Markdown文本。

  • URL :
  • 依赖坐标
<!-- commonmark | https://github.com/vsch/flexmark-java -->
<dependency><groupId>com.atlassian.commonmark</groupId><artifactId>commonmark</artifactId><!-- 0.9.0 --><version>${commonmark.version}</version>
</dependency>
  • [java] flexmark 【不推荐,源码演示章节中未实际使用】
  • https://github.com/vsch/flexmark-java
  • https://spec.commonmark.org/0.28/
  • 口号

CommonMark/Markdown Java parser with source level AST. CommonMark 0.28, emulation of: pegdown, kramdown, markdown.pl, MultiMarkdown. With HTML to MD, MD to PDF, MD to DOCX conversion modules.
基于Java的 CommonMark / Markdown 解析器,提供源代码级别抽象语法树(AST)。支持 CommonMark 0.28 版本,并实现了对pegdown、kramdown、markdown.pl和MultiMarkdown的仿真。还包含HTML到Markdown、Markdown到PDF以及Markdown到DOCX的转换模块。
Flexmark-java 是使用块优先、内联之后的 Markdown 解析架构实现的 CommonMark(规范 0.28)解析器的 Java 实现。

  • 依赖坐标

内部依赖了 jsoup 等组件

<!-- Flexmark | https://github.com/vsch/flexmark-java -->
<dependency><groupId>com.vladsch.flexmark</groupId><artifactId>flexmark-all</artifactId><!-- 0.62.2 --><version>${flexmark.version}</version>
</dependency>flexmark-all 组件还包括但不限于含有如下依赖 :
<dependency><groupId>com.vladsch.flexmark</groupId><artifactId>flexmark-html2md-converter</artifactId><version>${flexmark.version}</version>
</dependency>

源码示范

  • 核心思路
  • 基于 commonmark-java 或 flexmark-java

定制化HTML样式 : HtmlNodeCustomStyleAttributeProvider

package xx.xx;import org.commonmark.node.Image;
import org.commonmark.node.Node;
import org.commonmark.renderer.html.AttributeProvider;import java.util.Map;/*** 定制 HTML节点的、标签属性的创建器* @uaage*/
public class HtmlNodeCustomStyleAttributeProvider implements AttributeProvider {private Node targetNode;private String targetTagName;private Map<String, String> targetAttributes;public HtmlNodeCustomStyleAttributeProvider(Node targetNode, String targetTagName, Map<String, String> targetAttributes) {this.targetNode = targetNode;this.targetTagName = targetTagName;this.targetAttributes = targetAttributes;}/*** 设置属性* @param node*  the node to set attributes for*  eg: org.commonmark.node.Image* @param tagName*  the HTML tag name that these attributes are for (e.g. {@code h1}, {@code pre}, {@code code}).* @param attributes*  the attributes, with any default attributes already set in the map*  eg : attributes.put("class", "border");* @usage* Node document = parser.parse("![text](/url.png)");* renderer.render(document);* // "<p><img src=\"/url.png\" alt=\"text\" class=\"border\" /></p>\n"*/@Overridepublic void setAttributes(Node node, String tagName, Map<String, String> attributes) {if( targetNode.getClass().isInstance(node) ){//约等效于: node instanceof Image//attributes.put("class", "border");attributes.putAll( this.targetAttributes );}}
}

MD转HTML工具 : MarkdownConverter

package xx.mdtohtml;import org.commonmark.parser.Parser;
import org.commonmark.node.Node;
//import com.vladsch.flexmark.util.ast.Node;
//import com.vladsch.flexmark.html.HtmlRenderer;
//import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
import org.commonmark.renderer.html.AttributeProvider;
import org.commonmark.renderer.html.AttributeProviderContext;
import org.commonmark.renderer.html.AttributeProviderFactory;
import org.commonmark.renderer.html.HtmlRenderer;import java.util.ArrayList;
import java.util.List;public class MarkdownConverter {private Parser parser = Parser.builder().build();private HtmlNodeCustomStyleAttributeProvider attributeProvider;private HtmlRenderer htmlRenderer;/*** @note 支持对输出 HTML 进行属性定制 (关键类: AttributeProvider)* @param @Nullable attributeProvider*/public MarkdownConverter(HtmlNodeCustomStyleAttributeProvider attributeProvider) {init();}public MarkdownConverter() {this(null);}private void init(){//初始化 htmlRendererif(this.attributeProvider == null){htmlRenderer = HtmlRenderer.builder().build();} else {htmlRenderer = HtmlRenderer.builder().attributeProviderFactory(new AttributeProviderFactory() {public AttributeProvider create(AttributeProviderContext context) {//定制化 HTML 渲染器return attributeProvider; //new ImageAttributeProvider();}}).build();}}/*** Markdown 转 HTML [基于 commonmark-java]* @param markdownContent*  eg: "This is *Sparta*"* @return*  eg: "<p>This is <em>Sparta</em></p>\n"* @reference*  [1] commonmark-java Java 的 Markdown 解析器 - oschina.net - https://www.oschina.net/p/commonmark-java*/public String convertMarkdownToHtml(String markdownContent) {Node document = this.parser.parse(markdownContent);//"This is *Sparta*"HtmlRenderer renderer = this.htmlRenderer; //HtmlRenderer.builder().build();return renderer.render(document);//// "<p>This is <em>Sparta</em></p>\n"}

Demo

MarkdownConverter converter = new MarkdownConverter();String fileContent = "This is *Sparta*"; //
//String fileContent = reader.readHtml(markdownFilePath);
String renderedContent = converter.convertMarkdownToHtml( fileContent );System.out.println(renderedContent);

out

<p>This is <em>Sparta</em></p>

HTML 转 Markdown

源码示范

  • 核心思路 : 基于 Jsoup(解析 HTML文档结构) + 借鉴源码

  • jsoup : HTML 解析工具

参见 : Jsoup : HTML 解析工具 - 博客园/千千寰宇

MarkdownConverter

package xx.htmltomd;import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Entities;
import org.jsoup.nodes.TextNode;
import org.jsoup.parser.Tag;
import org.jsoup.safety.Cleaner;
import org.jsoup.safety.Whitelist;import java.util.ArrayList;
import java.util.List;import xx.MarkdownLine.*;public class MarkdownConverter {private static int indentation = -1;private static boolean orderedList = false;public MarkdownConverter() {}private void init(){}/*** HTML 转 Markdown [基于 jsoup + 借鉴 jHTML2Md ]* @reference-doc*  [1] https://github.com/nico2sh/jHTML2Md/blob/master/src/main/java/com/pnikosis/html2markdown/HTML2Md.java*  [2] java html 转换为 markdown - 51CTO - https://blog.51cto.com/u_16213398/11878678 [不推荐]* @param htmlContent* @return*/public String convertHtmlToMarkdown(String htmlContent){Document document = Jsoup.parse(htmlContent);// 调用自定义方法进行转换// return traverseNodes(document.body());return convertHtmlToMarkdown( document );}/*** 遍历节点* @param node* @return*/
/**private String traverseNodes(org.jsoup.nodes.Node node) {StringBuilder markdown = new StringBuilder();// 遍历每个节点for (org.jsoup.nodes.Node childNode : node.childNodes()) {if (childNode instanceof TextNode) {// 处理文本节点markdown.append(childNode.outerHtml()).append("\n");}  else if (childNode instanceof Element) {markdown.append( handleElement((Element) childNode) );}int childNodeSize = childNode.childNodeSize();if(childNodeSize > 0){String childNodeMarkdownContent = traverseNodes( childNode );markdown.append( childNodeMarkdownContent );}}return markdown.toString();}
**//**private String handleElement(Element element) {StringBuilder result = new StringBuilder();switch (element.tagName().toLowerCase()) {case "h1":result.append("# ").append(element.ownText()).append("\n");break;case "h2":result.append("## ").append(element.ownText()).append("\n");case "h3":result.append("### ").append(element.ownText()).append("\n");case "h4":result.append("#### ").append(element.ownText()).append("\n");case "h5":result.append("##### ").append(element.ownText()).append("\n");case "h6":result.append("###### ").append(element.ownText()).append("\n");break;case "div"://todoresult.append(element.ownText()).append("\n");break;// 可以继续添加其他标签的处理,如 h3, p, ul 等case "p"://todoresult.append(element.ownText()).append("\n");break;default:result.append(element.outerHtml());}return result.toString();}
**/private String convertHtmlToMarkdown(Document htmlDirtyDoc) {indentation = -1;String title = htmlDirtyDoc.title();Whitelist whitelist = Whitelist.relaxed();Cleaner cleaner = new Cleaner(whitelist);Document doc = cleaner.clean(htmlDirtyDoc);doc.outputSettings().escapeMode(Entities.EscapeMode.xhtml);if (!title.trim().equals("")) {return "# " + title + "\n\n" + getTextContent(doc);} else {return getTextContent(doc);}}private static String getTextContent(Element element) {ArrayList<MarkdownLine> lines = new ArrayList<MarkdownLine>();List<org.jsoup.nodes.Node> children = element.childNodes();for (org.jsoup.nodes.Node child : children) {if (child instanceof TextNode) {TextNode textNode = (TextNode) child;MarkdownLine line = getLastLine(lines);if (line.getContent().equals("")) {if (!textNode.isBlank()) {line.append(textNode.text().replaceAll("#", "/#").replaceAll("\\*", "/\\*"));}} else {line.append(textNode.text().replaceAll("#", "/#").replaceAll("\\*", "/\\*"));}} else if (child instanceof Element) {Element childElement = (Element) child;processElement(childElement, lines);} else {System.out.println();}}int blankLines = 0;StringBuilder result = new StringBuilder();for (int i = 0; i < lines.size(); i++) {String line = lines.get(i).toString().trim();if (line.equals("")) {blankLines++;} else {blankLines = 0;}if (blankLines < 2) {result.append(line);if (i < lines.size() - 1) {result.append("\n");}}}return result.toString();}private static void processElement(Element element, ArrayList<MarkdownLine> lines) {Tag tag = element.tag();String tagName = tag.getName();if (tagName.equals("div")) {div(element, lines);} else if (tagName.equals("p")) {p(element, lines);} else if (tagName.equals("br")) {br(lines);} else if (tagName.matches("^h[0-9]+$")) {h(element, lines);} else if (tagName.equals("strong") || tagName.equals("b")) {strong(element, lines);} else if (tagName.equals("em")) {em(element, lines);} else if (tagName.equals("hr")) {hr(lines);} else if (tagName.equals("a")) {a(element, lines);} else if (tagName.equals("img")) {img(element, lines);} else if (tagName.equals("code")) {code(element, lines);} else if (tagName.equals("ul")) {ul(element, lines);} else if (tagName.equals("ol")) {ol(element, lines);} else if (tagName.equals("li")) {li(element, lines);} else {MarkdownLine line = getLastLine(lines);line.append(getTextContent(element));}}private static MarkdownLine getLastLine(ArrayList<MarkdownLine> lines) {MarkdownLine line;if (lines.size() > 0) {line = lines.get(lines.size() - 1);} else {line = new MarkdownLine(MarkdownLine.MDLineType.None, 0, "");lines.add(line);}return line;}private static void div(Element element, ArrayList<MarkdownLine> lines) {MarkdownLine line = getLastLine(lines);String content = getTextContent(element);if (!content.equals("")) {if (!line.getContent().trim().equals("")) {lines.add(new MarkdownLine(MDLineType.None, 0, ""));lines.add(new MarkdownLine(MDLineType.None, 0, content));lines.add(new MarkdownLine(MDLineType.None, 0, ""));} else {if (!content.trim().equals(""))line.append(content);}}}private static void p(Element element, ArrayList<MarkdownLine> lines) {MarkdownLine line = getLastLine(lines);if (!line.getContent().trim().equals(""))lines.add(new MarkdownLine(MDLineType.None, 0, ""));lines.add(new MarkdownLine(MDLineType.None, 0, ""));lines.add(new MarkdownLine(MDLineType.None, 0, getTextContent(element)));lines.add(new MarkdownLine(MDLineType.None, 0, ""));if (!line.getContent().trim().equals(""))lines.add(new MarkdownLine(MDLineType.None, 0, ""));}private static void br(ArrayList<MarkdownLine> lines) {MarkdownLine line = getLastLine(lines);if (!line.getContent().trim().equals(""))lines.add(new MarkdownLine(MDLineType.None, 0, ""));}private static void h(Element element, ArrayList<MarkdownLine> lines) {MarkdownLine line = getLastLine(lines);if (!line.getContent().trim().equals(""))lines.add(new MarkdownLine(MDLineType.None, 0, ""));int level = Integer.valueOf(element.tagName().substring(1));switch (level) {case 1:lines.add(new MarkdownLine(MDLineType.Head1, 0, getTextContent(element)));break;case 2:lines.add(new MarkdownLine(MDLineType.Head2, 0, getTextContent(element)));break;case 3:lines.add(new MarkdownLine(MDLineType.Head3, 0, getTextContent(element)));break;case 4:lines.add(new MarkdownLine(MDLineType.Head4, 0, getTextContent(element)));break;case 5:lines.add(new MarkdownLine(MDLineType.Head5, 0, getTextContent(element)));break;case 6:lines.add(new MarkdownLine(MDLineType.Head6, 0, getTextContent(element)));break;default:throw new RuntimeException("Not Support the tag: "+ element.tagName());}lines.add(new MarkdownLine(MDLineType.None, 0, ""));lines.add(new MarkdownLine(MDLineType.None, 0, ""));}private static void strong(Element element, ArrayList<MarkdownLine> lines) {MarkdownLine line = getLastLine(lines);line.append("**");line.append(getTextContent(element));line.append("**");}private static void em(Element element, ArrayList<MarkdownLine> lines) {MarkdownLine line = getLastLine(lines);line.append("*");line.append(getTextContent(element));line.append("*");}private static void hr(ArrayList<MarkdownLine> lines) {lines.add(new MarkdownLine(MDLineType.None, 0, ""));lines.add(new MarkdownLine(MDLineType.HR, 0, ""));lines.add(new MarkdownLine(MDLineType.None, 0, ""));}private static void a(Element element, ArrayList<MarkdownLine> lines) {MarkdownLine line = getLastLine(lines);line.append("[");line.append(getTextContent(element));line.append("]");line.append("(");String url = element.attr("href");line.append(url);String title = element.attr("title");if (!title.equals("")) {line.append(" \"");line.append(title);line.append("\"");}line.append(")");}private static void img(Element element, ArrayList<MarkdownLine> lines) {MarkdownLine line = getLastLine(lines);line.append("![");String alt = element.attr("alt");line.append(alt);line.append("]");line.append("(");String url = element.attr("src");line.append(url);String title = element.attr("title");if (!title.equals("")) {line.append(" \"");line.append(title);line.append("\"");}line.append(")");}private static void code(Element element, ArrayList<MarkdownLine> lines) {//判断是否是单行行内代码片段Boolean isOneLineCodeSnippet = !( element.ownText().contains("\n") || element.ownText().contains("\r") );StringBuilder codeContent = new StringBuilder();//lines.add(new MarkdownLine(MDLineType.None, 0, "```code"));if(isOneLineCodeSnippet){codeContent.append(" `");} else {codeContent.append( " ```code\n" );}MarkdownLine line = new MarkdownLine(MDLineType.None, 0, "    ");//line.append(getTextContent(element).replace("\n", "    "));//line.append( element.ownText() );//lines.add(line);codeContent.append( element.ownText() );if(isOneLineCodeSnippet){codeContent.append("` ");} else {//lines.add(new MarkdownLine(MDLineType.None, 0, "```"));codeContent.append("\n ``` ");}lines.add( new MarkdownLine( MDLineType.None , 0 , codeContent.toString() ) );}private static void ul(Element element, ArrayList<MarkdownLine> lines) {lines.add(new MarkdownLine(MDLineType.None, 0, ""));indentation++;orderedList = false;MarkdownLine line = new MarkdownLine(MDLineType.None, 0, "");line.append(getTextContent(element));lines.add(line);indentation--;lines.add(new MarkdownLine(MDLineType.None, 0, ""));}private static void ol(Element element, ArrayList<MarkdownLine> lines) {lines.add(new MarkdownLine(MDLineType.None, 0, ""));indentation++;orderedList = true;MarkdownLine line = new MarkdownLine(MDLineType.None, 0, "");line.append(getTextContent(element));lines.add(line);indentation--;lines.add(new MarkdownLine(MDLineType.None, 0, ""));}private static void li(Element element, ArrayList<MarkdownLine> lines) {MarkdownLine line;if (orderedList) {line = new MarkdownLine(MDLineType.Ordered, indentation,getTextContent(element));} else {line = new MarkdownLine(MDLineType.Unordered, indentation,getTextContent(element));}lines.add(line);}
}

X 参考文献

  • CommonMark Spec
  • https://spec.commonmark.org/0.28/
  • commonmark-java
  • https://github.com/commonmark/commonmark-java/
  • flexmark-java

含 flexmark-html2md-converter 、... 等子组件

  • https://github.com/vsch/flexmark-java
  • JohannesKaufmann | html-to-markdown

基于 go 语言

  • https://github.com/JohannesKaufmann/html-to-markdown
  • jHTML2Md

不推荐本项目,但 HTML2Md.java 的源码思路,值得借鉴

  • https://github.com/nico2sh/jHTML2Md/blob/master/src/main/java/com/pnikosis/html2markdown/HTML2Md.java

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/809823.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如果 表名 拼写错误或表不存在,你会看到 #1146 - Table ecms.表名 doesnt exist 的错误

<?php $servername = "localhost"; $username = "your_username"; $password = "your_password"; $dbname = "ecms";// 创建连接 $conn = new mysqli($servername, $username, $password, $dbname);// 检查连接 if ($conn->con…

错误消息:#2002 - Cant connect to local MySQL server through socket /tmp/mysql.sock (2)

错误消息:#2002 - Cant connect to local MySQL server through socket /tmp/mysql.sock (2) 原因:数据库服务未启动。 连接参数错误。解决方法:检查数据库服务:确认 MySQL 服务是否正常运行。sudo service mysql status检查连接参数:确认连接参数(主机名、用户名、密码、…

请问想登录宝塔面板但是忘记密码_宝塔密码忘记了怎么办

通过邮箱找回:如果你在设置宝塔面板时绑定了邮箱,可以通过绑定的邮箱来找回密码。 访问宝塔面板登录页面,找到“忘记密码”选项并点击,按照提示输入已绑定的邮箱地址。 登录邮箱查看收到的重置链接或验证码,按照邮件中的指引完成密码重置。通过SSH命令行重置:首先通过SSH…

网站数据库配置失败怎么办

解决网站数据库配置失败的问题,可以按照以下步骤进行排查和修复:检查配置文件确认数据库连接信息是否正确,包括数据库地址、端口、用户名和密码。 检查数据库名称是否正确。验证数据库服务状态确认数据库服务是否正在运行。 使用命令行工具尝试连接数据库,确认连接是否成功…

公司网站预留电话修改不了

如果公司网站预留电话修改不了,可以尝试以下几种方法来解决问题:检查权限:确认当前登录的账号是否具有足够的权限来修改网站设置。 如果不是管理员账号,尝试联系公司的网站管理员或IT部门获取更高权限的账号。查看错误提示:在尝试修改时,注意查看是否有任何错误提示信息。…

图像数据增强库综述:10个强大图像增强工具对比与分析

在深度学习和计算机视觉领域,数据增强已成为提高模型性能和泛化能力的关键技术。本文旨在全面介绍当前广泛使用的图像数据增强库,分析其特点和适用场景,以辅助研究人员和开发者选择最适合其需求的工具。数据增强在深度学习模型训练中扮演着至关重要的角色,其重要性主要体现…

TEN Framework 入坑记

TEN Framework 是一个开源的多模态实时音视频和AI框架,很好用,很强大。最近,我们使用XSwitch打通了跟TEN的对接,在折腾的过程中有不少汗水也有不少收获,小记一下备忘。如果对大家有帮助,也很欣慰。TL;DR TEN Framework 最初叫 Astra,后改为 TEN,即 Transformative Exte…

SRC漏洞挖掘----信息搜集

信息搜集插件推荐:shodan,findsomething,retire.js,hacktool,FoFa Pro view SRC漏洞挖掘之信息搜集 资产搜集的网站:zoomeye,fofa(收费),360 网络空间测绘系统-360数字安全(免费) 1.FOFA语法学习 介绍: FOFA(Fingerprinting Organizations with Advanced Tools)是…

2024 闽盾杯-黑盾赛道WP

CRYPTO签到题-学会SMhttps://www.json.cn/encrypt/sm3题目要求小写所以需要转换一下或者脚本:import hashlibmessage = "heidun2024"hash_object = hashlib.new(sm3)hash_object.update(message.encode(utf-8))hash_value = hash_object.hexdigest()print(hash_valu…

2024 第七届“巅峰极客”网络安全技能挑战赛初赛 wp

WEBEncirclingGame题目描述:A simple game, enjoy it and get the flag when you complete it.开题,前端小游戏,红点出不去就行直接玩通关了看看如何不玩也能拿到flag,flag存储在后端php文件内,前端找不到。看一下游戏的请求包,里面记录了红点的最后位置和防火墙(黑点)…

SpringBoot.3中的aot.factories到底有什么用?和以前的spring.factories一样吗?

首先,我们来澄清一下 aot.factories 和 spring.factories 之间的区别。这两个文件不仅名称不同,而且在功能上也存在显著差异。接下来,我们将深入探讨这两个文件的具体作用以及它们各自的应用场景。让我们一起来揭开它们的神秘面纱吧! 在我们上一次讨论 Spring Boot 3 版本时…