很多刚开始接触 JavaWeb 的人都有过类似的困惑:Java 基础啃完了,JDBC 也练了,打开 IDEA 新建项目,看着目录里多出来的 webapp 目录发愣——HTML 文件到底放哪?放进去了又怎么通过浏览器访问?更常见的是,HTML 教程学了一大堆,标签认得差不多了,一回到 JavaWeb 项目里就不知道怎么把页面和 Servlet 串起来。
这篇文章就围绕“JavaWeb + HTML”这条主线,把你在实际开发中最可能遇到的一整条链路讲清楚:HTML 在 JavaWeb 项目里扮演什么角色,如何用 IDEA 把项目跑起来,怎么写一个前端表单提交到 Servlet、再存入 MySQL、最后回显的完整案例,以及 HTML 转 PDF、转 Markdown、服务器上排查 HTML 这些“周边杂活”怎么处理。
适合谁看?刚学完 Java 语法、正在做第一个 JavaWeb 项目的学生,或者想搞明白“页面和后端到底怎么连起来”的自学者。文章里没有高深理论,全是我实操踩过坑之后整理出来的可直接抄的步骤。
1. JavaWeb 项目里,HTML 到底扮演什么角色
1.1 先分清 HTML、CSS、JS、Java 各自管什么
很多人学 HTML 的时候,习惯把它当成一门独立的课来学,学完标签、学完表单,却始终没搞明白它在 JavaWeb 里的位置。用一句话概括:HTML 负责搭骨架,CSS 负责化妆,JavaScript 负责做动作,Java 负责算数据、存数据。
四者的分工可以对照来看:
- HTML:决定页面里有什么内容,标题、段落、输入框、按钮、表格,都是它的职责。
- CSS:决定这些内容长什么样,宽高、颜色、间距、字体,全部交给它。
- JavaScript:决定页面能不能“动”,表单校验、按钮点击、异步请求,这些交互逻辑由它承担。
- Java(Servlet/Service/DAO):接收浏览器发来的请求,处理业务,操作数据库,再把结果送回前端。
我用一个餐厅来类比:HTML 是菜单、桌椅、墙上的装饰画,也就是顾客能看到的全部东西;CSS 是店内的装修风格,同样的桌椅可以摆出快餐店或法餐的调性;JavaScript 是服务员,负责响应顾客的招呼、端菜、结账;Java 则是后厨,顾客点菜后你能端出什么菜,取决于后厨有什么食材和菜单。
在 JavaWeb 项目里,HTML 不是孤立存在的文件。它要么是静态页面,躺在 webapp 目录里被 Tomcat 直接读出来返回给浏览器;要么是动态页面,由 JSP(或者 Thymeleaf 这类模板引擎)在服务器端把 Java 处理好的数据填充进去,生成一份带数据的 HTML 再返回给浏览器。后者才是 JavaWeb 的核心形态。
1.2 为什么很多人学完 HTML 还是不会做 JavaWeb
我见过太多的新手,HTML 标签背得滚瓜烂熟,但到了写 JavaWeb 项目时依然无从下手。问题不在 HTML 没学好,而是知识根本没有串成一条线。具体卡点通常是这三个:
第一,HTML 学得过于孤立。教程里只讲标签是干什么的,却不讲表单提交到哪、Servlet 怎么接收、数据怎么回显。你学会了写 <input>,但不知道 name 属性是给后端取参数的钥匙。
第二,不知道 HTML 文件放在哪。有人把 html 文件建在项目根目录,甚至放在 src 下的 java 包里,浏览器访问永远是 404。实际上,静态 HTML 必须放在 webapp 目录(以前叫 WebContent)下,Tomcat 才会把它当资源发布。
第三,理解不了 URL 和代码之间的映射关系。在 JavaWeb 里,你看到的 http://localhost:8080/hello/saveMessage,并不是让 Tomcat 去硬盘上找一个叫 saveMessage 的文件,而是通过 @WebServlet 注解或 web.xml 配置,把这条路径映射到某个 Servlet 类上。HTML 里 <form action="saveMessage"> 里的地址,和后端类的映射,其实是同一张路由表上的两半。
我习惯让初学者做一个练习:用 HTML+CSS 仿写一个百度首页的天气区域,做完之后如果你还能用一张图解释“用户点击查询按钮后,数据从浏览器到服务器再到数据库,最后又回到浏览器”的全过程,那你才真正从 HTML 学习迈进了 JavaWeb 开发的门槛。页面做出来不算会,链路走通才算会。
1.3 一次完整请求的路径:从 HTML 点击到 Java 处理
如果你还不清楚整个链路长什么样,我用文字画一遍。假设用户浏览器地址栏输入 http://localhost:8080/hello/index.html:
- 浏览器向 8080 端口发起 HTTP 请求,请求路径是
/hello/index.html。 - Tomcat 拿到请求后,根据 URL 第一段
/hello匹配到部署名称对应的项目上下文(context path)。 - 在项目上下文内继续找
/index.html。Tomcat 先在 webapp 目录下找这个物理文件,找到了直接读文件内容返回;同时也会把请求交给 servlet 容器去匹配有没有 path 对应的 Servlet。 - 如果请求的是动态资源,比如
/save,Tomcat 会根据映射关系把请求交给你写的SaveServlet类,调用它的doPost或doGet方法。 - Servlet 里通过
req.getParameter("name")拿到 HTML 表单提交的字段,处理完数据后,可以req.setAttribute("list", list)把数据放到 request 作用域里,再forward到 JSP。 - JSP 执行时会逐行生成 HTML,把 Java 数据填充进去,最终整个页面以纯 HTML 字符串的形式响应给浏览器。
这个过程,就是 JavaWeb 与之前你练过的“双击 html 文件打开”最大的区别:在 JavaWeb 中,浏览器永远是通过 URL 去请求资源,而不是通过文件路径。理解这一点,下一节配 IDEA 的时候你会顺畅得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零配置:IDEA 运行 JavaWeb 项目的完整步骤
2.1 工具选型:IDEA 版本和 Tomcat 版本
先说结论,如果你的目标是快速跑通一个传统 JavaWeb 项目,推荐组合是:IDEA 专业版 + Tomcat 9 + MySQL 8 + Maven。可能有人会问,社区版不行吗?体验下来区别很大。
IDEA 社区版是开源的,做纯 Java/Android 项目没问题,但它本身不内置 JavaWeb 工程模板,也不能直接集成 Tomcat 的启动按钮。你要么通过 Maven 构建 war 包丢到 Tomcat 的 webapps 目录下启动,要么装第三方插件硬凑。不是不能跑,但对新手来说,每一步都多出许多概念干扰。
Tomcat 版本为什么推荐 9 而不是 10 或者 11?这个细节最容易踩坑。Tomcat 9 对应的 Servlet API 包名是 javax.servlet,你搜资料的时候,几乎全部老教程、博客、视频用的都是 javax.servlet 开头的代码。而从 Tomcat 10 开始,包名改成了 jakarta.servlet。如果你用的是 Tomcat 10,网上查来的代码复制进去,IDE 直接给你报一堆红色错误,因为 import javax.servlet.* 已经不存在了。对新手来说,先选 Tomcat 9 最大程度保持和教程一致,等项目跑通了,再去看 10 的差异也不迟。
2.2 关键配置五步:从新建项目到浏览器看到页面
第一步,新建 Maven Web 项目。IDEA 里选择 New Project,在生成器列表里选 Maven,Archetype(骨架)选 maven-archetype-webapp。注意别选成普通的 Java 项目——那个不会生成 webapp 目录和 web.xml,后面还得手动补。
第二步,等 Maven 把骨架拉下来后,编辑 pom.xml,加上项目需要的依赖。最基础的两个是 Servlet API(编译用,由 Tomcat 提供)和 MySQL 驱动:
xml复制<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
scope 配成 provided 的原理很简单:这个包 Tomcat 自带,编译的时候要有,但打包进 war 反而会和容器冲突,所以标记成“由运行环境提供”。
第三步,配置 Tomcat。打开 Run/Debug Configurations,点加号,找到 Tomcat Server,选 Local。点击 Configure 找到你的 Tomcat 安装目录,JRE 选当前 JDK,然后切到 Deployment 标签页。这一步很多人忽略:你必须在这里把项目 artifact 添加进去,否则 Tomcat 启动后根本不会加载你的项目。
第四步,设置 Application context。IDEA 里默认可能是 /项目名_war_exploded,太长太丑,建议直接改成 /hello。这个值是 URL 里的第一段路径,也就是请求进来时匹配项目的那一段。改完之后,启动 Tomcat,控制台输出 Server startup 日志,浏览器访问 http://localhost:8080/hello/。
第五步,验证静态 HTML。在 webapp 目录下新建一个 index.html,写上 Hello JavaWeb,再刷新刚才的地址,看到页面出现就说明最基础的链路已经通了。
我在这个环节特别说一下为什么 Deployment 要选 exploded 而不是 war 包。exploded 的意思是“展开的、未压缩的目录”,IDEA 和 Tomcat 直接以 webapp 目录为根进行部署。好处是你改了一个 HTML 或者 CSS,刷新浏览器就能看到效果,不需要重新打 war 包。而 war 是压缩包形式,每次修改都要重新构建再部署,开发效率会低很多。开发阶段一律用 exploded,上线发布时再打 war 包。
2.3 新手最常踩的四个坑:404、乱码、端口占用、空白页
配置过程中一定会遇到问题,下面是一份我整理了无数次的排查表。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 浏览器 404 | Application context 写错,或部署的 artifact 没添加 | 确认 URL 第一段和 Deployment 里的 context 一致 |
| 控制台中文乱码 | Tomcat 日志编码问题 | 修改 conf/logging.properties,把 UTF-8 相关项改成 GBK 或直接固定 UTF-8 |
| 页面提交后中文乱码 | 请求和响应编码没设置 | Servlet 里 req.setCharacterEncoding("UTF-8") 必须在拿参数前执行 |
| Tomcat 启动时报端口占用 | 8080 被其他进程占用 | netstat -ano 找到 PID,任务管理器结束进程,或改 Tomcat 端口 |
还有一个“空白页”的坑值得单独说。Tomcat 启动后访问 /hello/ 返回空白,不是项目出问题,而是 web.xml 里没有配置 welcome-file。你需要在 web.xml 里加:
xml复制<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
不加的话,直接访问根路径找不到默认页面,Tomcat 就返回一个空白响应。你要么每次都手敲 /hello/index.html,要么就老老实实配置 welcome-file。
3. 一个能跑的完整案例:HTML 表单 + Servlet + MySQL
3.1 案例设计与数据库准备
理论和配置讲完,直接上一个能复现的最小完整案例。需求非常简单:做一个留言登记页面,用户填写姓名、邮箱、留言内容,点击提交后存入 MySQL,然后跳转到一个列表页把所有留言回显出来。
这个案例麻雀虽小五脏俱全,它能覆盖 JavaWeb 开发入门最核心的闭环:HTML 采集数据 → HTTP 提交 → Servlet 接收处理 → JDBC 操作数据库 → 数据回显动态页面。
先准备数据库。MySQL 里建一个库和一张表:
sql复制CREATE DATABASE javaweb_demo DEFAULT CHARSET utf8mb4;
USE javaweb_demo;
CREATE TABLE message (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
email VARCHAR(100),
content TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) DEFAULT CHARSET utf8mb4;
字段类型上有一个设计细节:留言内容用 TEXT 而不是 VARCHAR。VARCHAR 最大长度在不同字符集下有差异,而且超长内容它会拒绝存储;TEXT 类型上限 64KB,对留言场景足够,也不用纠结长度设置。邮箱字段我没加唯一索引,因为这不是注册系统,允许一个邮箱多条留言。
3.2 HTML 表单页:不是随便写写就能连后端
先看这个页面完整代码,再逐行解读关键点:
html复制<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>留言登记</title>
</head>
<body>
<h1>留言板</h1>
<form action="saveMessage" method="post">
<p>
<label>姓名:</label>
<input type="text" name="name" required>
</p>
<p>
<label>邮箱:</label>
<input type="email" name="email">
</p>
<p>
<label>留言内容:</label>
<textarea name="content" rows="5" cols="40" required></textarea>
</p>
<p>
<button type="submit">提交留言</button>
</p>
</form>
</body>
</html>
很多初学者写 HTML 只关心页面长什么样,在 JavaWeb 项目里这是远远不够的。你要特别注意三个细节:
第一个是 action="saveMessage"。这个值不能乱写,它必须和 Servlet 的访问路径对上。我在 Servlet 上用了 @WebServlet("/saveMessage"),那 action 就写 saveMessage。如果 Servlet 映射是 /save,那 form 里的 action 就必须是 save,否则请求发出去会打到不存在的地址上,返回 404。
第二个是 method="post"。GET 和 POST 的区别不仅是“能不能看到 URL 里的参数”。留言这种会产生数据修改的操作,必须用 POST。GET 请求参数拼在 URL 后面,长度受限,而且浏览器历史记录里会留下完整参数,不适合传正文内容。POST 请求参数放在请求体里,不会被 URL 长度卡住,也更适合表单提交场景。
第三个是每个输入控件的 name 属性。后端 req.getParameter("name") 拿到的值,完全取决于这个 name 属性。你写 <input type="text" name="username">,后端就必须 getParameter("username")。很多人前端写的是 id="name",后端却用 getParameter("name"),结果一直拿到 null,这就是名字对不上造成的。
关于页面框架里的三件套:DOCTYPE html 告诉浏览器这是 HTML5 标准模式;lang="zh-cn" 声明页面语言;meta charset="utf-8" 指定字符集。尤其是 charset,如果漏了或和后端编码不统一,中文字符就会变成一堆问号。这个案例里,HTML 是 utf-8,MySQL 表是 utf8mb4,Servlet 里也设置 UTF-8,三者统一,中文才不会出乱子。
3.3 Servlet + JDBC:数据怎么从 HTTP 落到 MySQL
前端表单写好后,要写一个类处理提交请求。我用注解方式注册 Servlet,省去 web.xml 配置:
java复制package com.example.servlet;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet("/saveMessage")
public class SaveMessageServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 必须放在 getParameter 之前
req.setCharacterEncoding("UTF-8");
String name = req.getParameter("name");
String email = req.getParameter("email");
String content = req.getParameter("content");
// 没有填写姓名的直接拦截
if (name == null || name.trim().isEmpty()) {
resp.getWriter().write("姓名不能为空");
return;
}
String url = "jdbc:mysql://localhost:3306/javaweb_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
String user = "root";
String password = "123456";
String sql = "INSERT INTO message(name, email, content) VALUES(?, ?, ?)";
// 1. 加载驱动
// 2. 建立连接
// 3. 预编译 SQL
// 4. 填充参数
// 5. 执行更新
try {
Class.forName("com.mysql.cj.jdbc.Driver");
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, name);
ps.setString(2, email);
ps.setString(3, content);
int rows = ps.executeUpdate();
if (rows > 0) {
// 转发到列表页,稍后实现
resp.sendRedirect("messageList");
} else {
resp.getWriter().write("保存失败");
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
这里有几个细节都值得单独说明。
为什么 req.setCharacterEncoding("UTF-8") 必须写在 getParameter 之前?因为浏览器提交 POST 请求时,参数编码是在请求体里。你如果不提前告诉容器“这个请求体里的内容按 UTF-8 解码”,容器就会按 ISO-8859-1 处理,中文拿到手全是乱码。这个坑基本每个做 JavaWeb 的人都踩过,而且踩得莫名其妙,因为页面显示完全正常,一存数据库就面目全非。
为什么用 PreparedStatement 而不是 Statement?这不只是习惯问题。用字符串拼接 SQL 有 SQL 注入风险——用户可以在表单里输入特殊内容改变 SQL 语义。PreparedStatement 是预编译的,参数用 ? 占位符,再通过 setString 等方法绑定值。数据库会把这个 SQL 结构先解析好,参数永远只当数据处理,不会改变语句结构。对于入门阶段,从第一天就养成写 PreparedStatement 的习惯,比后面改掉坏习惯轻松得多。
try-with-resources 语法也是刻意写的。Connection 和 PreparedStatement 都是资源,用完必须关。你不关连接,数据库的连接池迟早被耗尽,项目运行几天后突然开始报 Too many connections,就是这类泄漏造成的。
3.4 数据回显:从“最笨的写法”到“看懂动态 HTML”
数据存进去了,下一步是把所有留言显示到页面上。这就要写第二个 Servlet 来查数据,并转发到一个能渲染动态 HTML 的页面。
我用最传统的 JSP 来演示,因为 JSP 能让你最直观地看到“Java 数据填进 HTML”这件事。
查询 Servlet 长这样:
java复制@WebServlet("/messageList")
public class MessageListServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String url = "jdbc:mysql://localhost:3306/javaweb_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai";
List<Message> list = new ArrayList<>();
String sql = "SELECT id, name, email, content, create_time FROM message ORDER BY id DESC";
try {
Class.forName("com.mysql.cj.jdbc.Driver");
try (Connection conn = DriverManager.getConnection(url, "root", "123456");
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
Message m = new Message();
m.setId(rs.getInt("id"));
m.setName(rs.getString("name"));
m.setEmail(rs.getString("email"));
m.setContent(rs.getString("content"));
m.setCreateTime(rs.getTimestamp("create_time"));
list.add(m);
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 把数据放到 request 作用域
req.setAttribute("list", list);
// 转发到 JSP,JSP 会在服务器端渲染成 HTML
req.getRequestDispatcher("/list.jsp").forward(req, resp);
}
}
关键点是最后两行:setAttribute 把 List 放进请求作用域,getRequestDispatcher(...).forward() 把这次请求连同数据一起转发给 JSP。注意,forward 之后浏览器地址栏还是 /messageList,页面内容却是 JSP 生成的。
JSP 页面里,我能直接在 HTML 中穿插 Java 代码:
jsp复制<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="java.util.List" %>
<%@ page import="com.example.entity.Message" %>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>留言列表</title>
</head>
<body>
<h1>全部留言</h1>
<table border="1" cellspacing="0" cellpadding="6">
<tr>
<th>姓名</th>
<th>邮箱</th>
<th>内容</th>
<th>时间</th>
</tr>
<%
List<Message> list = (List<Message>) request.getAttribute("list");
if (list != null) {
for (Message m : list) {
%>
<tr>
<td><%= m.getName() %></td>
<td><%= m.getEmail() %></td>
<td><%= m.getContent() %></td>
<td><%= m.getCreateTime() %></td>
</tr>
<%
}
}
%>
</table>
</body>
</html>
这个页面的可怕之处,也是它的生动之处:HTML 标签和 Java 代码在同一个文件里揉在一起。JSP 的原理是:服务器执行 JSP 时,先把它翻译成一个 Servlet 类,把普通 HTML 当成 out.write() 的字符串,把 <% %> 里的 Java 代码原样塞进方法体。你每次刷新页面看到的“动态部分”,其实都是 Java 程序执行完的结果。
这种 Scriptlet 写法在今天生产环境里很少有人直接用了,主流方案是 JSTL+EL、Thymeleaf、Freemarker 或者后端直接返回 JSON 由前端渲染。但从理解“动态 HTML”的角度,没见过 Scriptlet 的人很难理解模板引擎到底帮你做了什么。你一旦看懂了这页 JSP 的生成逻辑,后面学任何模板引擎都会很快。
如果你一开始就引入 JSTL,页面会清爽很多:
jsp复制<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:forEach items="${list}" var="m">
<tr>
<td>${m.name}</td>
<td>${m.email}</td>
<td>${m.content}</td>
<td>${m.createTime}</td>
</tr>
</c:forEach>
但 JSTL 需要额外导依赖、配置 taglib URI,新手在这个阶段容易分不清哪些错误来自依赖问题、哪些来自代码问题。所以我建议的顺序是:先用 Scriptlet 把原理跑通,再逐步换成 EL 表达式。先理解再优化,这是效率最高的路径。
4. 处理“杂活”:HTML 在真实开发里的周边任务
项目跑起来之后,你还会碰到一堆和 HTML 相关的“周边活”。这些工作不涉及业务逻辑,但每天都会遇到,而且网上答案常常很散。我把高频的几种整理一遍。
4.1 HTML 转 PDF 并加水印:导出报表的常规操作
实际项目里经常遇到“把页面导成 PDF”的需求,比如合同、订单、报表、成绩单。在 Java 生态里,iText 7 + pdfHTML 是最常见的组合。核心思路是:HTML 字符串不是图片,它是一段有结构的文本,iText 会把它解析成自己的文档对象模型,再输出成 PDF。
简单示例流程:先把 HTML 字符串转成 PDF,再给每一页叠加一行水印文字。
java复制// 依赖:com.itextpdf:html2pdf:4.0.x 与 com.itextpdf:itext7-core:7.2.x
String html = "<html><body><h1>项目报告</h1><p>这是一段要导出成PDF的内容</p></body></html>";
PdfWriter writer = new PdfWriter(new FileOutputStream("report.pdf"));
PdfDocument pdf = new PdfDocument(writer);
// 把 HTML 转换成 PDF 内容
HtmlConverter.convertToPdf(html, pdf, new ConverterProperties());
// 加文字水印
int n = pdf.getNumberOfPages();
PdfFont font = PdfFontFactory.createFont("STSong-Light", "UniGB-UCS2-H");
for (int i = 1; i <= n; i++) {
PdfPage page = pdf.getPage(i);
PdfCanvas canvas = new PdfCanvas(page.newContentStreamAfter());
canvas.beginText()
.setFontAndSize(font, 60)
.setColor(ColorConstants.GRAY, true)
.moveText(120, 400)
.showText("内部资料")
.endText();
}
pdf.close();
这个方案里有几个实际的坑。中文是最大的一个:iText 默认字体不支持中文,渲染出来全是方块或者直接报错,必须显式指定支持中文的字体。其次是水印位置,moveText(120, 400) 的坐标是相对于 PdfCanvas 当前坐标系的,做对角线满铺水印需要按旋转角度计算,想在每页居中、斜铺、调整透明度,建议把这个逻辑封装成 PdfPageEventHelper 事件类,在每次页面新建时自动触发,而不是手动遍历每一页。如果只是少量页面,逐页写就够了。
4.2 HTML 转 Markdown:网页资料整理成文档
做技术调研、写博客、整理项目资料时,经常要复制网页内容到 Markdown 文档里。直接把 HTML 粘贴到 Markdown 编辑器,往往是一堆乱格式。稳妥的路径是用 JSoup 解析 HTML,再按规则转成 Markdown 语法。
JSoup 是 Java 世界最常用的 HTML 解析库,它可以像浏览器一样结构化地操作 HTML。转 Markdown 的基本做法:
java复制import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
String html = "<h1>标题</h1><p>第一段文字</p><ul><li>列表项A</li><li>列表项B</li></ul>";
Document doc = Jsoup.parse(html);
// 去掉无用的标签
doc.select("script, style, meta, link").remove();
StringBuilder md = new StringBuilder();
for (Element h1 : doc.select("h1")) {
md.append("# ").append(h1.text()).append("\n\n");
}
for (Element p : doc.select("p")) {
md.append(p.text()).append("\n\n");
}
for (Element li : doc.select("li")) {
md.append("- ").append(li.text()).append("\n");
}
这套“选择器 + 文本提取”的思路非常简单,但要注意几个细节。JSoup 的 text() 方法会把嵌套标签里的文字全部拼接,遇到 <strong> 你无法保留加粗语义。如果只想提取纯文本做资料整理,这个方案足够用;如果想保留 Markdown 的加粗、链接、代码块这些格式,自己写递归遍历会更完整,或者直接用现成的 html2md 开源库。我个人的习惯是:只提取正文内容时用 JSoup,要保留完整格式时直接找现成库,不重复造轮子。
4.3 服务器上没有浏览器,怎么“看”HTML
JavaWeb 项目上线后,排查页面问题不可能每台服务器都装图形界面。在纯命令行环境里查看 HTML,有三个常用办法。最简单的是 curl 把 HTML 源码拉下来:
bash复制curl -s http://localhost:8080/hello/messageList | head -n 50
只看关键内容时结合 grep:
bash复制curl -s http://localhost:8080/hello/messageList | grep -E "<title>|<h1>|tabel"
如果想把 HTML 渲染成可读的文本界面(去掉所有标签,按文本形式展示),可以用终端浏览器 w3m 或 lynx:
bash复制w3m -dump http://localhost:8080/hello/messageList
lynx -dump http://localhost:8080/hello/messageList
这两个命令会把 HTML 解析成类似纯文本的排版输出,适合快速确认页面在服务器端生成的内容是否正确,而不用关心样式。我曾经排过一个诡异的 Bug:本地页面完全正常,线上却白屏,最后就是靠 curl + grep 发现服务器上返回的是 504 错误页而不是业务页面。这个技能对 JavaWeb 开发者来说是生产环境排障的基本功,建议提前练熟。
4.4 Ubuntu 环境下的 HTML 编辑与批量打包技巧
服务器上临时改页面也是常事。比如你发现线上页面有个联系电话写错了,本地改完再传上去当然没问题,但如果只是改一个几千字节的 HTML,直接在服务器上操作更快。
Ubuntu 环境里优先推荐 VS Code Remote SSH。你本地用 VS Code 连接到服务器,改文件体验跟在本地一模一样,还有语法高亮和补全。如果没有图形环境,就用 vim 或 nano。nano 上手难度低,底部直接显示快捷键,对新手友好;vim 功能强,需要记命令,熟练后效率极高。
批量换内容时不用手工一条条改,用 sed 直接替换:
bash复制# 把 html 目录下所有 .html 文件中的旧链接替换成新链接
sed -i 's#http://old.example.com#http://new.example.com#g' html/*.html
注意我用 # 代替了默认的 / 分隔符,这样 URL 里的斜杠就不用转意了,更容易读。
多目录、多资源的站点上线前,常用 zip 打包:
bash复制zip -r site.zip html/ css/ js/ images/
排除某些目录是压缩时的高频需求:
bash复制zip -r site.zip . -x "*.log" -x "node_modules/*"
还有一个顺手提一下的细节:批量处理多个 HTML 时,如果你要对页面里的图片地址做统一前缀拼接,用上面的 sed 替换即可。手工打开每一个文件去改,浪费时间还容易漏。
另外有两个人问过的高频小需求也在这里一并答了。一个是“HTML 一键返回顶部”,原理很简单:
html复制<button onclick="window.scrollTo({ top: 0, behavior: 'smooth' })">返回顶部</button>
另一个是“桌面程序里怎么显示 HTML 内容”。如果你以前用 Python 写工具,PyQt5 里组件也能直接渲染字符串:
python复制text_browser.setHtml("<h1>标题</h1><p>内容</p>")
这些虽然不属于 JavaWeb 后端本身,但作为一个开发者,各种工具链里遇到 HTML 的场景远比想象中多,掌握几分钟解决一个问题的能力,能省下大量时间。
5. 写在最后的实践建议
回到一开始的话题。每次带新人,我都会反复强调同一句话:先让一条链路通起来,再去纠结怎么优化。学 JavaWeb 最忌讳的是一直看教程、一直抄代码,却从不亲手把一个页面从 HTML 表单一路写到数据库再回显。我见过太多人把 MyBatis、Spring Boot、Redis 都背得很熟,到头来让他手写一个 Servlet 接收表单参数,还是要翻笔记。底层链路没有亲手走一遍,框架用得再多都是空中楼阁。
几个小经验最后一起交代了。第一,本地开发建议先用 VS Code 的 Live Server 插件单独写静态 HTML,把布局和交互调好,再挪进 JavaWeb 项目的 webapp 目录接入后端。这样你能把“前端问题”和“后端问题”分开排查,不会混在一起毫无头绪。第二,所有文件统一 UTF-8 编码,页面 charset、数据库连接串、Tomcat 配置三层对齐,中文乱码能消灭九成。第三,IDEA 里看到“红线”别慌,先分清楚是语法错误、缺少依赖,还是 Maven 还没刷新,多半不是代码本身的问题。
我自己的成长路径是:手写 Servlet + JDBC 做增删改查 → 用 JSP 搭动态页面 → 再学 SSM/Spring Boot 框架。看起来绕了远路,但这几步走下来,你对请求生命周期、数据流、甚至框架的自动配置原理都会有掌控感,而不仅停留在“会调 API”的阶段。你把这个最小闭环跑通了,后面学任何框架,都只是在这个闭环上换工具而已。
