最近刚把一个基于 JavaWeb 的老项目翻新了一遍,发现好多人对 Ajax 在 JavaWeb 里的用法还停留在“复制粘贴能用就行”的阶段。一旦遇到参数乱码、请求格式不对、后端返回的数据前端解析不了这些问题,就卡住了。这篇我就拿一个真实的 JavaWeb 项目作为例子,把 Ajax 从最基础的原生写法到参数传递、编码格式处理、JSON 数据交互、常见问题排查,完整走一遍。内容不挑基础,刚学 JavaWeb 的新人能照着做,写过几年项目的老手也能从里面的细节里捡回一些容易忽略的东西。
1. Ajax 在 JavaWeb 项目中的角色定位
1.1 为什么 JavaWeb 开发离不开 Ajax
先看清楚一个事实:Ajax 并不是某种新技术框架,它本质上就是浏览器内置 XMLHttpRequest 对象提供的一组 API。JavaWeb 的服务端跑的是 Servlet、Filter、JSP 这一套,而页面上的 JavaScript 通过 Ajax 发请求到后端接口,拿到结果后再动态更新页面内容,这套搭配决定了 Ajax 在 JavaWeb 项目里几乎无处不在。
不用 Ajax 的传统方式,就是用户点一个按钮,整个表单提交给服务器,服务器处理完返回一个新的 JSP 页面让浏览器重新渲染。这个过程最让人难受的就是每次都要刷新整个页面,用户填到一半的信息、滚动的位置、页面状态全部没了。Ajax 打破了这个模式,它只向服务器发送需要的请求参数,服务器返回一段纯数据(通常是 JSON 字符串),由前端 JavaScript 解析后局部更新页面中的某一块区域。用户的视觉体验是“页面没有跳转,但数据已经变了”。
从技术分工上看,这正好对应了 JavaWeb 后端和前端 JavaScript 各自的职责边界。后端只负责封装业务逻辑、访问 MySQL 数据库、返回一个标准的 JSON 结果,前端负责收集用户输入、构造请求、解析响应、渲染页面。界限清晰之后,前后端可以并行开发,后端把接口定好,前端拿 mock 数据先跑页面,这也是现在团队协作效率高的原因之一。
1.2 同步请求和异步请求的本质区别
很多初学者会混淆“页面不刷新”和“异步”这两个概念。Ajax 确实可以让页面不刷新,但不代表只要用了 XMLHttpRequest 就一定是异步。XMLHttpRequest 的 open 方法第三个参数可以传 true 或 false,true 表示异步,false 表示同步。
同步请求发出之后,浏览器 JavaScript 引擎会被阻塞在那里等待服务器的响应,期间用户什么都做不了,页面像是卡死了一样。如果网络慢一点,整个页面就僵住了,体验极差。异步请求发出之后,浏览器会立刻返回继续执行后面的代码,真正等响应回来之后再通过回调函数处理结果,用户界面始终是流畅的。
在 JavaWeb 项目里几乎都应该用异步模式。唯一可以考虑同步的场景,是某些业务操作必须依赖接口返回结果才能决定下一步动作,而且操作本身非常快,比如判断用户是否已经登录。即便如此,我仍然建议尽量用异步加回调的方式来处理,把后续逻辑放到回调函数里,而不是依赖同步阻塞。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 开发环境搭建:从 IDEA 到 Tomcat
2.1 新版 IDEA 创建 JavaWeb 项目的关键配置
先说说环境,现在主流的开发工具依旧是 IntelliJ IDEA,如果你用的是 2023 版本及更新的版本,创建 JavaWeb 项目的流程和老版本相比有一些变化,这里值得单独讲清楚。
打开 IDEA 之后,选择 File -> New -> Project。在左侧项目类型里定位到 Jakarta EE 这个分类。里面可以选 Web Application 作为模板,这里要注意,现在的 IDEA 版本默认用的是 Jakarta EE 规范,命名空间是 jakarta.servlet 开头,而不是老项目里常见的 javax.servlet。如果你的项目最终要放到老版本 Tomcat 8 及以下去运行,需要调整为对应版本,否则编译都过不了。选好之后,Build Tool 可以保持默认的 Maven,这样后面引入依赖(比如 MySQL 驱动、JSON 处理库)会方便很多。
项目创建完成后,右键项目根目录,选择 Add Framework Support,确认 Web 模块是否已经添加进去。如果看不到 Web 选项,说明项目类型创建得不对,可以手动在 WEB-INF 目录下补一个 web.xml 文件来标识这是个 Web 应用。IDEA 2023 版本对 Maven 的集成非常友好,pom.xml 里加依赖、刷新项目、打包 war 包,这些操作都比老版本顺手很多。但在配置 Artifact 时要注意一点,IDEA 自动生成的 Artifact 输出的类型是 war exploded(解压目录),这是开发模式下的测试用配置,后面如果要发布到服务器上,记得单独执行 Maven package 打成标准 war 包,再用 Tomcat 去部署。
2.2 配置 Tomcat 运行环境的经验之谈
开发模式下,IDEA 内置支持配置 Tomcat 启动项目,不需要手动去 Apache Tomcat 安装目录里复制 war 包。操作路径是 Run -> Edit Configurations,点击左上角加号,找到 Tomcat Server -> Local。在 Application Server 那里点击 Configure,选择你本机的 Tomcat 安装目录,IDEA 会自动识别版本号。Deployment 标签页里点加号,把 Artifact 添加进去,Application Context 建议设置成 /,这样本地访问的时候就无需输入多余的项目路径前缀。
跑起来之后访问地址就是 http://localhost:8080/。如果端口冲突,可以在 Server 标签页的 HTTP port 里改,比如改成 8081,但要注意端口改了之后,前端 Ajax 请求的地址也要跟着改。我这里有个实操经验,本地调试时我习惯用随机端口启动,因为 8080 经常被其他程序占用,但发布到正式环境时统一使用 80 端口配合 Apache 转发,这样用户访问时不需要带端口号,体验更好。
环境准备好之后,下一步就要开始写代码了。Ajax 部分的代码主要分两块:前端的 JavaScript 代码写在 JSP 页面或静态 HTML 页面里,后端的接收逻辑写在 Servlet 里。
3. 原生 Ajax 请求的完整实现流程
3.1 从零封装一个可复用的 XMLHttpRequest 对象
虽然现在很多人用 JQuery 的 $.ajax 或者 axios 库,但我觉得做 JavaWeb 项目的人还是应该先掌握原生写法。原生的好处是让你知道请求到底是怎么发出去的,遇到问题能快速定位是在哪一层出的错。而且老项目里经常能看到原生 XMLHttpRequest 的代码,真要去维护的时候不至于懵。
先写一个最基础的创建 XMLHttpRequest 对象的方法。现代浏览器都支持 new XMLHttpRequest(),但考虑到某些企业内部系统可能还跑着老版本 IE(不要笑,生产环境真的有),最好加一段兼容判断。
javascript复制function createXHR() {
if (typeof XMLHttpRequest !== "undefined") {
return new XMLHttpRequest();
}
// 兼容老版本 IE 的写法,了解一下就行
if (typeof ActiveXObject !== "undefined") {
return new ActiveXObject("Microsoft.XMLHTTP");
}
throw new Error("当前浏览器不支持 XMLHttpRequest");
}
拿到这个对象之后,调用 open 方法初始化请求,再调用 send 方法发送请求,最后在 onreadystatechange 里监听状态变化。readyState 有 0 到 4 五个值,我们只关心 4,因为只有 readyState === 4 才代表服务器响应已经接收完毕。然后再判断 HTTP 状态码,status 200 表示成功,404、500 这些就按失败处理。
对于刚接触 JavaWeb 的初学者,我建议把这段基础发送逻辑封装成一个简单的函数,参数包括请求方式、请求地址、请求参数、回调函数,这样后面调起来方便。
javascript复制function sendRequest(method, url, data, callback) {
const xhr = createXHR();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
callback(null, xhr.responseText);
} else {
callback(new Error("请求失败,状态码:" + xhr.status));
}
}
};
xhr.open(method, url, true);
if (method.toUpperCase() === "POST") {
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
}
xhr.send(data || null);
}
这里注意一点,open 的第三个参数传的是 true,即异步模式,这个值是绝对不能省略的。省略掉的话,浏览器会按 true 处理,但为了防止有人把代码改成 false,最好显式写上 true。
3.2 GET 请求的三步走:发送、接收、渲染
GET 请求的特点是参数放在 URL 后面,以问号 ? 开始,多个参数之间用 & 连接。在 JavaWeb 项目里,典型场景是前端输入一个关键字,后端从 MySQL 里模糊查询用户列表,再把结果以 JSON 数组形式返回。
前端页面上有一个输入框和一个查询按钮,用户输入关键字后,通过 JavaScript 拿到输入框的值,拼接 URL 发送请求。代码大概是这样的:
javascript复制document.getElementById("btnSearch").onclick = function () {
const keyword = document.getElementById("keywordInput").value;
const url = "userServlet?action=search&keyword=" + encodeURIComponent(keyword);
sendRequest("GET", url, null, function (err, data) {
if (err) {
alert("查询失败:" + err.message);
return;
}
// data 是服务器返回的 JSON 字符串,后续处理往下看
renderUserList(data);
});
};
后端对应的 Servlet 继承 HttpServlet,重写 doGet 方法。通过 request.getParameter("action") 判断是哪种操作,这里就是 search。再用 request.getParameter("keyword") 拿到关键字,调用业务层去 MySQL 做模糊查询。查询结果转换成 JSON 字符串,设置响应的 ContentType 和编码,最后通过 response.getWriter().write(json) 写回去。
java复制protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
String action = request.getParameter("action");
if ("search".equals(action)) {
String keyword = request.getParameter("keyword");
List<User> userList = userService.searchUsers(keyword);
String json = JSONUtil.toJson(userList);
response.getWriter().write(json);
}
}
这是完整的一个链路,从用户输入到服务端查库再到返回前端渲染,一个最小的 JavaWeb + Ajax + MySQL 的闭环就跑通了。
3.3 POST 请求与表单数据的序列化处理
GET 请求虽然简单,但有两个明显的问题:一是 URL 长度有限制,传大量文本的时候会被浏览器截断;二是参数直接暴露在地址栏,不适合传密码等敏感信息。POST 请求把参数放在请求体里发送,既能携带更多数据,也更安全,所以在 JavaWeb 项目里 POST 的使用频率更高。
POST 请求有两种常见的 Content-Type 编码方式,一种是我们刚刚代码里写的 application/x-www-form-urlencoded,这种格式下参数和 GET 一样,仍然是 key=value 的键值对形式,只是放的位置从 URL 变成了请求体。另一种是 application/json,直接发送一段 JSON 字符串,现在前后端分离的项目里用得非常多。
JavaWeb 的传统 Servlet 后端接收 form-urlencoded 格式的参数非常简单,直接用 request.getParameter 就能拿到,所以如果你用的是原生 Servlet,建议前端也保持这种格式。举个注册功能的例子:
javascript复制function doRegister() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const params = "username=" + encodeURIComponent(username) + "&password=" + encodeURIComponent(password);
sendRequest("POST", "registerServlet", params, function (err, data) {
if (err) {
alert("注册请求失败");
return;
}
const result = JSON.parse(data);
alert(result.message);
});
}
后端 doPost 里通过 request.getParameter 去取参数,逻辑和 doGet 完全一致。这里要格外注意一个细节:Servlet 容器在处理 POST 请求体中的中文时,会按照默认的 ISO-8859-1 编码去解析,导致中文乱码。所以 doPost 方法里的第一行必须是 request.setCharacterEncoding("UTF-8"),这句话必须放在读取任何参数之前才有效。
4. 请求参数与编码格式的深度处理
4.1 中文乱码的根源与三次编码设置
中文乱码是 JavaWeb Ajax 开发里最容易踩、也最让人崩溃的问题。乱码的根源其实很清晰,无非就是前端 JavaScript 字符串是 UTF-16 编码,浏览器发送时按某种编码转成字节流,服务器再按另一种编码解析字节流,这三者之间只要不一致,出来的就是乱码。
针对 GET 请求,乱码的情况就又不一样了。URL 参数中的中文在 Tomcat 8.0 及以上版本中,默认已经支持 UTF-8 解码,所以直接传中文也不会有问题。但如果你用的是老版本 Tomcat 7 及以下,就要在 server.xml 的 Connector 节点加上 URIEncoding="UTF-8" 属性。为了保证在各种环境下都不出问题,前端的做法是使用 encodeURIComponent 函数对参数值进行编码。这个函数会把中文转换为 %XX 形式的百分号编码,这是 URL 编码标准,服务器端接收的时候能正确还原。
POST 请求的乱码问题相对好解决,主要分三步。第一步,服务器端在取参数前调用 request.setCharacterEncoding("UTF-8");第二步,前端发送 POST 请求时设置请求头 Content-Type 为 application/x-www-form-urlencoded;charset=UTF-8;第三步,保证 JSP 页面本身的编码是 UTF-8,页头加 <%@ page contentType="text/html;charset=UTF-8" language="java" %>。这里强调一下,response 回的 JSON 字符串同样要设置编码,否则到了前端 alert 弹出来还是乱码。
4.2 使用过滤器统一处理全站编码
项目里接口一旦多起来,每个 Servlet 里都写一遍 setCharacterEncoding 不但啰嗦,还容易出现遗漏,一旦漏一个就是线上乱码事故。更稳妥的做法是在 Web 项目里加一个编码过滤器,统一拦截所有请求和响应。
JavaWeb 规范里提供了 Filter 接口,我们可以实现一个 CharacterEncodingFilter 来全站处理编码。核心逻辑就是重写 doFilter 方法,设置 request 和 response 的编码之后再放行。
java复制public class CharacterEncodingFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
chain.doFilter(req, resp);
}
}
然后在 web.xml 里面配置过滤器映射,注意路径要写成 /*,表示拦截所有请求。配置好之后,项目里所有的 Servlet 都不用再单独设置编码了,省掉很多重复劳动。另外,响应类型我这里固定成 text/html,如果你的接口返回的是 JSON,可以在具体的 Servlet 里再覆盖,把 ContentType 设置成 application/json;charset=UTF-8。
4.3 给 Ajax 请求参数赋值的两种常见写法
关于“给 ajax 请求参数赋值”这个主题,实际开发里通常是两种情况:一种是表单里的数据,另一种是非表单的业务参数。表单数据手动拼接非常麻烦,字段多了容易漏,所以比较推荐用 FormData 对象来处理。FormData 是 HTML5 提供的接口,可以直接把 form 表单里的所有字段自动收集起来,不需要逐一手动获取。
javascript复制const form = document.getElementById("registerForm");
const formData = new FormData(form);
sendRequest("POST", "registerServlet", formData, callback);
用 FormData 发送请求时,前端代码不需要手动设置 Content-Type 请求头,浏览器会自动生成一个带 boundary 分隔符的 multipart/form-data 格式。整个 JavaWeb 项目要处理文件上传时,用 FormData 会特别方便,直接把 input type="file" 的文件对象 append 进去即可。后端对应的 Servlet 需要能解析 multipart 格式,这在 Servlet 3.0 以后的版本中可以通过 request.getPart 取到。
业务参数不是来自表单,而是来自 JavaScript 对象时,可以用 URLSearchParams 来辅助拼接参数,代码看起来也更简洁:
javascript复制const params = new URLSearchParams();
params.append("userId", userId);
params.append("role", role);
sendRequest("POST", "roleServlet", params.toString(), callback);
URLSearchParams 内部会帮我们对 key 和 value 做 URL 编码处理,比手动拼字符串安全得多,推荐优先使用。
5. JSON 数据交互与 DOM 渲染
5.1 JavaWeb 后端生成 JSON 的三种可选方案
Ajax 请求返回的数据格式,实际项目里百分之九十以上都是 JSON。后端服务器把 Java 对象转换成 JSON 字符串的库有很多,U 盘里随便翻一个老项目,大概率能见到下面这三种之一:Gson、Jackson、Fastjson。
Gson 是 Google 出的,特点是轻量、API 简洁,只需要一行 new Gson().toJson(userList) 就能把 List
以 Gson 为例,引入依赖只需在 pom.xml 里加三行:
xml复制<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
后端接口返回数据的推荐格式是统一包装成 {code, msg, data} 的结构,code 代表业务状态码,msg 是对用户的提示信息,data 是真正要展示的数据。这样一个固定结构出来之后,前端解析和错误处理就统一了,不需要每个接口单独各自返回一套风格。
java复制Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "查询成功");
result.put("data", userList);
String json = new Gson().toJson(result);
response.getWriter().write(json);
5.2 前端解析 JSON 并渲染 DOM 的细节
前端拿到后端返回的 JSON 字符串之后,第一步先用 JSON.parse 转成 JavaScript 对象,然后从中取出业务数据,再通过操作 DOM 的方式把内容渲染到页面上。渲染方式取决于页面结构,最传统的是直接拼接 HTML 字符串,设置给某个容器的 innerHTML。这种方式简单粗暴,但在渲染大量数据时不够安全,一旦数据里包含用户输入的标签,有被注入的风险。
对 JavaWeb 项目来讲,如果页面渲染的数据都是后端严格处理过的安全数据,用 innerHTML 问题不大。更现代的方式是遍历数据,用 document.createElement 动态创建节点,再把文本内容通过 textContent 赋值,这样浏览器不会把内容当 HTML 解析,安全上没有隐患。代价是代码量会多一些。下面是一个简单的渲染用户列表的例子:
javascript复制function renderUserList(jsonStr) {
const result = JSON.parse(jsonStr);
if (result.code !== 200) {
alert(result.msg);
return;
}
const userList = result.data;
const container = document.getElementById("userList");
container.innerHTML = "";
userList.forEach(user => {
const item = document.createElement("div");
item.className = "user-item";
item.textContent = user.userName + " —— " + user.userPhone;
container.appendChild(item);
});
}
数据量很小的时候,这个方案跑得很愉快。但是一旦列表数据达到百条以上,频繁操作 DOM 就会带来性能问题。这里可以说明一个简单有效的优化原则:先把所有需要渲染的节点片段拼接到一个 DocumentFragment 或字符串缓冲区里,最后一次性挂到页面上,这样能减少浏览器的回流和重绘次数,用户体验会明显好很多。
6. 高频踩坑与排查技巧实录
6.1 readyState 和 status 分别代表什么,别再搞混
前端调试 Ajax 的时候,打开浏览器开发者工具,Network 面板里能看到请求状态,但代码里依然要区分 readyState 和 HTTP status 这两个概念。readyState 是 XMLHttpRequest 对象自身的状态,从 0 到 4 分别代表未初始化、已打开、已发送、接收中、响应完成。status 是服务器返回的 HTTP 状态码,200 表示成功,404 是资源不存在,500 是服务器内部错误。
实际操作中,我们既要在 readyState === 4 时才去取响应内容,也要判断 status 是否在 2xx 范围内,两者缺一不可。见过不少新手在 onreadystatechange 里只判断 readyState == 4,然后直接拿 responseText 用,结果请求返回 500,前端拿到的是服务器错误页面的 HTML,还一脸困惑。所以判断条件一定写成 a && b 的形式。
6.2 解决请求缓存导致的数据不更新问题
GET 请求在部分浏览器(尤其是老版本浏览器)中会被默认缓存,浏览器的缓存机制会让相同 URL 的请求直接返回上次的结果。表现就是第一次查询正常,后面修改数据之后再查询,页面上还是旧数据。排查时打开 Network 面板看请求,发现某些 GET 请求根本没有发出去,或者显示 from memory cache,就是被缓存了。
规避方式有几种。第一种比较常用,在 URL 后面拼一个随机数或时间戳,确保每次请求的 URL 都不相同,这样浏览器就不会走缓存。比如 url = "userServlet?action=search&keyword=" + keyword + "&t=" + new Date().getTime()。第二种是后端在响应头上加 Cache-Control: no-cache 的响应头。实际开发中,我倾向于两个都加,前端加时间戳最简单,后端加响应头一次性从根源上杜绝缓存。
6.3 Ajax 请求跨域时的处理建议
默认情况下,浏览器出于同源策略的限制,不允许 JavaScript 跨域发送 Ajax 请求。同源的定义是协议、域名、端口完全一致,三个条件缺一个都不行。JavaWeb 项目里出现跨域,最常见的场景是前端页面部署在一个端口,后端接口跑在另一个端口,或者前后端干脆用了两个不同的域名。开发模式下可以通过后端为接口配置 CORS(跨域资源共享)响应头来解决,也就是在 Servlet 的响应里加 Access-Control-Allow-Origin 等头信息。
java复制resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type");
需要注意的是,生产环境安全要求高,Access-Control-Allow-Origin 不建议设置成 *,应该指定允许访问的前端域名,避免任意站点都能调你的接口。而且加了 CORS 之后,跨域请求中会先发一个 OPTIONS 预检请求,后端需要保证对这种请求也返回 200,否则发起正式请求会报错。
6.4 从 MySQL 到页面全链路编码排查清单
排查 JavaWeb + Ajax 的中文乱码问题,建议从上到下按链路排查:MySQL 数据库连接地址的 URL 参数里是否带了 useUnicode=true&characterEncoding=UTF-8;数据库表本身的字符集是否是 utf8mb4;后端连接数据库的 JDBC 驱动是否支持 utf8mb4;Servlet 里返回 JSON 时是否设置了响应编码;前端的请求头是否带了 charset=UTF-8;页面文件本身的编码是否保存为 UTF-8。
任何一环出了问题,最终都会在页面上表现为乱码。按照这个顺序排查,基本不会漏掉。我见过最离谱的一种情况是,连接池配置和页面都是好的,唯独数据库表的排序规则是 latin1_swedish_ci,怎么调都没用,最后把表字符集改成 utf8mb4 才彻底解决。所以建表的时候就要想清楚字符集的问题。
7. 结合 MySQL 的完整案例思路
7.1 一个用户管理的增删改查闭环设计
前面聊了那么多细节,容易显得碎片化,不如把它们放进一个完整的业务场景里整体串一下。以 JavaWeb 项目中最常见的“用户管理”模块为例,这个模块会涉及 Ajax 的几个典型用法:新增用户时用 POST 提交表单数据、查询用户时用 GET 带关键字参数、删除用户时用 POST 传用户 ID、更新用户时用 POST 传完整用户对象。
页面布局可以很简单,顶部是一个查询栏和新增按钮,中间是一个用户列表表格,底部是一个简易分页条。用户在查询栏输入关键字,点击“查询”按钮,前端拼参数发送 Ajax 请求;后端 UserServlet 统一接收 action 参数,分流出不同的业务逻辑;数据存储在 MySQL 的 t_user 表里,字段包括 id、user_name、user_phone、create_time。
这个案例最大的价值在于覆盖了 JavaWeb + Ajax + MySQL 的完整闭环,前端负责交互和数据展示,Servlet 负责路由和参数处理,DAO 层负责操作数据库,业务层封装查询逻辑,每一层都有明确的职责。做项目练手的时候跑通这一个模块,比零散地看知识点管用得多。
7.2 分页查询时 Ajax 如何组织参数
分页查询是 JavaWeb 项目里非常常见的功能。前端需要把当前页码 page 和每页条数 limit(或者 pageSize)以请求参数的形式发给后端。URL 大概长这样:userServlet?action=list&page=1&limit=10。后端拿到参数之后,MyBatis 或者 JDBC 层的 SQL 拼接 LIMIT 开始位置 offset,计算公式是 offset = (page - 1) * limit,最后返回的数据结构里除了当前页的列表,还要带上总记录数 total,前端根据 total 计算总页数渲染分页按钮。
这里有个值得说明的细节:后端返回的 total 是数据库里符合条件的数据总条数。计算的时候,业务层需要先执行 count 查询得到 total,再执行带 limit 的数据查询得到当前页记录。两条 SQL 直接用同一个 DAO 方法返回两个结果也可以,但更推荐封装成一个分页对象,把 list、total、page、limit 都放进去再序列化为 JSON。这样前端拿到的数据结构固定,分页逻辑写起来也顺手。
8. 项目发布:Windows Server 上 Apache 与 Tomcat 协同部署
8.1 从本地调试到服务器发布的差异点
本地 IDEA 里点绿色三角启动 Tomcat 跑项目,其实就是个开发模式。真正要交付给用户使用,必须把项目打包成 war 包,部署到正式环境。Windows Server 上最常见的 JavaWeb 部署方式是 Tomcat 独立运行,或者前面加一层 Apache 做反向代理,把 80 端口的请求转发给 Tomcat 的 8080 端口。
为什么前面要加 Apache,直接访问 Tomcat 不行吗?可以,但有几个问题。Tomcat 默认 8080 端口会暴露在公网上,更容易被扫描到。Apache 可以做静态资源缓存,减轻 Tomcat 处理静态文件的压力。对于需要域名访问的场景,Apache 的虚拟主机配置也更成熟。所以实际生产部署中,Apache + Tomcat 的组合很常见,也就是热搜词里提到的“winserver apache + tomcat 发布javaweb项目”。
整个发布过程的关键步骤是,先在 IDEA 的 Maven 面板里执行 package,生成 war 包;然后上传到 Windows Server 的 Tomcat webapps 目录下,改名为 ROOT.war 方便直接以根路径访问;接着启动 Tomcat,确认 war 包自动解压、项目发布成功;最后在 Apache 里配置反向代理,把 80 端口的请求转发出去。
8.2 部署后 Ajax 请求路径的调整
项目发布到 Tomcat 之后,Ajax 请求的路径要特别注意。本地开发时 Application Context 设置为 /,前端请求路径可以写成相对路径 userServlet。但正式部署如果 war 包改名成了 ROOT.war,那么路径还是 userServlet 没问题,如果没用 ROOT.war 而是保留了项目名(比如 demo.war),那么项目访问路径就变成了 /demo,前端所有的 Ajax 请求也必须带上 /demo 前缀,比如 /demo/userServlet。
这个路径问题非常容易翻车,本地上线了发现所有请求 404,八成就是这个原因。为了避免手动改前端代码里的这么多处路径,写爬坑建议是前端封装一个公共的 baseUrl 变量,页面里所有 Ajax 请求统一拼接这个变量,之后更改部署环境或者改 war 包名时,只需要改一处,省心得多。
Apache 代理配置时把 / 开头的请求转发到 Tomcat 的 8080 端口,负载均衡如果暂不考虑,只需要保证 ProxyPass 和 ProxyPassReverse 的配置正确即可。启动服务后可以先用 ip 地址加上端口和项目路径访问一遍,确认基础功能正常,再配置域名解析。Ajax 请求在代理环境下优先推荐使用相对路径,这样不容易出现域名或端口不匹配的问题。
9. 个人经验里的几个实用习惯
项目做多了,在 JavaWeb + Ajax 这套技术栈上我还攒了几个平常用的习惯。一个是开发时打开浏览器 F12 开发者工具的 Network 面板,每次 Ajax 请求发出后,一定要养成先看请求的 URL、请求头、请求体,再看响应数据的习惯。大部分问题其实从这一步就能定位个七七八八。另一个是写前端 Ajax 代码时统一在回调函数里先把响应文本打印到 console,确认数据格式符合预期了再写 DOM 渲染逻辑,不要跳过调试直接渲染。
JSON 数据格式在交付接口之前可以和前端小伙伴先对齐一次,把每个字段的类型、是否可能为空都定好,比自己闷头写完再对接效率高很多。分页和排序这些通用逻辑尽量抽出来复用,不要在同一个 Servlet 里堆好几套重复的胶水代码。
最后再分享一个小工具型的经验,JavaWeb 项目测试接口时,除了直接在页面里跑,还可以用 Postman 或者 ApiPost 这类工具直接发请求调试后端接口,把前端的因素排除掉,这样判断问题出在前端还是后端会特别清晰。Ajax 请求调不通的时候,先拿工具把接口调通,再回头检查前端代码,排查范围一下就缩小了。这套思路在 JavaWeb 项目里,不管是老项目维护还是新项目开发,都能帮你省下不少时间。
