做JavaWeb开发这些年,Ajax这个词几乎贯穿了所有和前端交互相关的需求。最初接手老项目时,页面每次刷新都要重新加载整张HTML,体验差是一回事,更头疼的是用户在表单里填了一半的数据,因为一次校验请求就全部清空。后来把核心交互改成Ajax,才真正体会到什么叫“局部刷新、按需取数”。这篇内容就围绕JavaWeb里Ajax的完整落地过程来写,从原生XMLHttpRequest到项目实战,把请求参数的赋值、编码格式的处理、以及数据库配合JSON返回的完整链路都过一遍,适合正在学JavaWeb的初学者,也适合很久没碰原生Ajax、打算把基础补回来的开发同学。
1. 为什么JavaWeb项目离不开Ajax
1.1 从一次表单提交说起
在没有Ajax的年代,浏览器和服务器之间的交互方式是“请求-响应-整页刷新”。用户在页面上点了一个按钮,浏览器把整个表单数据打包发到Servlet,Servlet做处理之后forward或redirect到另一个页面,浏览器再把这个新页面整个加载出来。这么做的最大问题是:一整个页面里可能只有一小块区域的数据是新的,其余全是重复加载。更难受的是,用户填写的中间状态往往随着页面刷新而丢失。
实际项目里出现频率最高的场景就是注册时的用户名校验。用户填完用户名,点一下“检测”,如果走传统表单提交,整个页面跳走,填到一半的注册信息全没了。这种交互在用户体验上是灾难级的。所以Ajax的出现,本质上解决的是“在不离开当前页面的情况下,把数据发到服务器,再拿回结果”这个需求。
1.2 Ajax在JavaWeb中的定位
Ajax全称是Asynchronous JavaScript and XML,核心是浏览器提供的XMLHttpRequest对象。它的本质没有变:用JavaScript发一个异步HTTP请求,服务器照常处理请求、返回数据,只是浏览器不再因为收到响应而刷新页面。
JavaWeb项目里Ajax的典型使用链路是这样的:
- 前端页面通过XMLHttpRequest或封装库(jQuery的$.ajax、axios等)发出请求;
- 请求到达后端的Servlet,Servlet调用Service层处理业务逻辑;
- Service调用DAO层访问MySQL等数据库;
- Servlet把结果转成JSON字符串写回响应;
- 前端拿到JSON,利用DOM操作把数据渲染到页面指定位置。
这个链路和传统表单提交的后端代码几乎一样,唯一的区别在于响应的处理。传统请求返回的是整个页面,Ajax请求返回的是结构化的数据(JSON、XML或者纯文本)。这也是为什么在JavaWeb教学和项目中,Ajax总是和JSON绑定在一起出现。
1.3 原生写法才是理解原理的钥匙
很多同学一上来就学jQuery的$.ajax或者axios,因为使用上确实简单。但我个人的建议是,至少亲手写过一遍原生XMLHttpRequest,再谈封装库。原因很简单:封装库帮你省掉了细节,却也屏蔽了原理。一旦遇到请求一直不成功、返回数据解析不出来这类问题,不懂底层机制的人往往无从下手,只能盲目改代码碰运气。
原生Ajax请求的完整生命周期包含几个关键状态:创建XMLHttpRequest对象、调用open方法初始化请求、设置请求头、调用send发送请求、通过onreadystatechange监听状态变化、在readyState为4且status为200时处理响应。这串步骤理解透了,后面不管是换jQuery还是axios,都只是在替换发送方式,核心思想不变。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Ajax请求的核心原理与编码处理
2.1 XMLHttpRequest的完整工作流程
XMLHttpRequest对象的readyState属性有5个取值,从0到4分别代表:未初始化、正在加载、已加载、交互中、完成。平时判断请求成功,标准姿势是同时检查readyState等于4和status等于200,这两个条件缺一不可。
一个最基础的GET请求,原生的写法是这样的:
javascript复制var xhr = new XMLHttpRequest();
xhr.open("GET", "user?name=zhangsan", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
console.log(data);
}
};
xhr.send();
这里有个细节值得注意:open方法的第三个参数async,默认就是true,表示异步执行。如果显式写成false,浏览器会阻塞当前脚本直到请求返回,这就是同步请求。同步请求在页面交互上体验极差,而且在主线程里执行同步网络请求,浏览器会直接警告甚至禁止,生产环境千万不要用同步方式。
2.2 GET和POST请求的参数赋值差异
Ajax请求发参数这件事,是很多新手第一个踩坑的地方。GET请求的参数拼接在URL后面,这个耳熟能详,但容易忽略的是参数值必须经过encodeURIComponent编码。举一个真实案例:搜索框里输入“张三”,直接拼URL变成user?name=张三,Tomcat的默认URIEncoding如果是UTF-8,问题不大;但如果输入的是带特殊字符的内容,比如“a&b=1”,这个参数不编码,后端getParameter("name")取到的值就变成“a”,&后面的内容全被当成新的参数了。
所以GET请求的正确写法是:
javascript复制var name = encodeURIComponent(input.value);
xhr.open("GET", "user?name=" + name, true);
POST请求的参数不放在URL里,而是通过send方法的参数传递。同时,要在send之前设置请求头的Content-Type为application/x-www-form-urlencoded,这样后端Servlet的getParameter才能正确解析请求体:
javascript复制var xhr = new XMLHttpRequest();
xhr.open("POST", "user", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("name=" + encodeURIComponent(input.value));
同样是传参,GET和POST在Ajax层面的区别就这么直观。GET适合查询类操作,参数在URL里,浏览器和服务器都有长度限制;POST适合提交类操作,参数放请求体,能承载更大数据量,也相对安全一些。
2.3 中文乱码的根源与三层解决方案
JavaWeb里Ajax请求的中文乱码,几乎是必现问题。我梳理一下乱码出现的三个位置,对应三种不同的解决方案。
第一层,请求参数乱码。对于POST请求,request.getParameter读取请求体时用的字符集由request.setCharacterEncoding指定,必须在第一次调用getParameter之前设置:
java复制request.setCharacterEncoding("UTF-8");
对于GET请求,参数在URL上,字符集由Tomcat的URIEncoding决定。Tomcat 8.0及以上版本默认URIEncoding已经是UTF-8,所以大多数情况下GET请求不会乱码。如果是老版本Tomcat,就需要在server.xml的Connector节点加一个属性:
xml复制<Connector URIEncoding="UTF-8" />
第二层,响应乱码。Servlet往浏览器写数据时,需要同时设置响应内容的类型和字符集:
java复制response.setContentType("application/json;charset=UTF-8");
这行代码要在response.getWriter()之前执行,否则可能不生效。这里建议直接用application/json而不是text/html,一方面语义更准确,另一方面JSON.parse解析时不会遇到类型不匹配的奇怪问题。
第三层,数据库乱码。Ajax请求本身不直接产生数据库乱码,但如果是提交用户输入到MySQL,连接串里必须显式指定UTF-8:
java复制jdbc:mysql://localhost:3306/javaweb?useUnicode=true&characterEncoding=UTF-8
三层都处理到位,中文链路才算是彻底打通。我见过很多项目,页面显示正常但存进数据库乱了,排查半天才发现是连接串少了characterEncoding参数。
3. 实战:从零搭建一个带Ajax的JavaWeb用户查询系统
3.1 环境准备与项目创建
我用的环境是最常见的组合:IDEA 2023版、Tomcat 9、MySQL 8、JDK 1.8。和2023版本IDEA创建JavaWeb项目的完整步骤相关的内容,网上笔记很多,但有几个关键步骤值得强调一下。
创建项目时选择Java Enterprise,SDK选1.8,Web Application勾选上,应用服务器添加本地的Tomcat路径。项目结构生成后,src/main/java里建包,src/main/webapp下面放JSP和静态资源。要做Ajax和MySQL的联动,还需要把MySQL驱动jar包放进WEB-INF/lib目录下,这是初学者容易漏的一步。驱动版本要注意和MySQL版本匹配,我用的是mysql-connector-java-8.0.33,对应com.mysql.cj.jdbc.Driver这个驱动类。
3.2 数据库表设计与JDBC连接
做演示就用最简结构的用户表。建表语句大概长这样:
sql复制CREATE DATABASE javaweb DEFAULT CHARACTER SET utf8mb4;
USE javaweb;
CREATE TABLE t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
phone VARCHAR(20),
email VARCHAR(100)
);
然后往表里插入几十条测试数据,方便后面联调。JDBC这一层,为了不让代码太啰嗦,我用一个简单的工具类封装连接操作:
java复制public class DBUtil {
static {
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
String url = "jdbc:mysql://localhost:3306/javaweb"
+ "?useUnicode=true&characterEncoding=UTF-8"
+ "&useSSL=false&serverTimezone=Asia/Shanghai";
return DriverManager.getConnection(url, "root", "你的密码");
}
}
注意URL里的serverTimezone参数,MySQL 8的驱动如果不指定时区,连接时会直接报错。这个坑我踩过一次,官方文档里写得很清楚,但很多教学笔记没提。
3.3 后端Servlet:接收请求并返回JSON
后端接口的设计思路是:提供一个/user接口,接收name参数,根据姓名模糊查询用户列表,返回JSON数组。用Servlet原生写法如下:
java复制@WebServlet("/user")
public class UserServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
String name = req.getParameter("name");
List<User> users = userDao.findByName(name);
String json = JSON.toJSONString(users);
resp.getWriter().write(json);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 复用doGet的逻辑,或单独实现
}
}
findByName方法里的模糊查询SQL是:
sql复制SELECT id, name, phone, email FROM t_user WHERE name LIKE ?
这里给name拼接上%通配符再传入预编译的PreparedStatement。JSON转换我用的是fastjson,也可以用Jackson,选一个顺手的即可。核心是Servlet只负责接收请求、调用业务层、返回JSON,不要在里面堆SQL和业务逻辑。
@WebServlet注解需要Servlet 3.0以上规范支持,Tomcat 9完全没问题,这样免去了web.xml的Servlet映射配置,项目结构更清爽。
3.4 前端页面:Ajax请求发送与DOM渲染
页面也很简单,一个输入框、一个按钮、一个用于展示结果的列表。核心代码看重合交互这块,我用input事件来触发搜索,这样用户输入时就能实时看到结果,体验好很多:
html复制<input type="text" id="keyword" placeholder="输入姓名关键字">
<ul id="result"></ul>
<script>
document.getElementById("keyword").addEventListener("input", function () {
var keyword = this.value.trim();
var resultUl = document.getElementById("result");
resultUl.innerHTML = "";
if (keyword === "") {
return;
}
var xhr = new XMLHttpRequest();
xhr.open("GET", "user?name=" + encodeURIComponent(keyword), true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
var users = JSON.parse(xhr.responseText);
var fragment = document.createDocumentFragment();
users.forEach(function (user) {
var li = document.createElement("li");
li.textContent = user.name + " —— " + user.phone + " —— " + user.email;
fragment.appendChild(li);
});
resultUl.appendChild(fragment);
}
};
xhr.send();
});
</script>
这段代码里有几个值得注意的细节。第一,输入框内容先trim()一下,去掉首尾空格,避免查询条件变成“张三 ”导致匹配不上。第二,encodeURIComponent编码参数,防止特殊字符破坏URL结构。第三,用document.createDocumentFragment()批量添加DOM节点,而不是在循环里逐个appendChild到真实DOM上,避免反复触发页面重排。
3.5 MySQL联调:从输入到渲染的完整流程
把Tomcat启动起来,访问项目的index页面,在搜索框里输入“张”,回车或者不回车,结果列表就会跟着输入内容变化。后端接收到请求后,走Servlet → DAO → JDBC → MySQL,查到数据后包装成JSON返回,前端解析JSON之后渲染成li节点。
整个流程跑通之后,可以尝试改造成POST请求,把参数放到send里,再验证一下乱码是否处理好。如果你发现POST方式下中文参数乱码,通常就是request.setCharacterEncoding("UTF-8")没写,或者写在getParameter之后了。
这个完整案例虽然小,但五脏俱全,Ajax请求参数赋值、编码格式设置、后端JSON返回、前端DOM渲染、MySQL数据链路该有的全部覆盖了。用它做模板,后面扩展到复杂的业务场景只是换接口的问题。
4. 常见问题与排查技巧实录
4.1 请求一直走error回调,后端却明明有响应
这个问题出现频率极高,排查方向却往往搞反。先在浏览器开发者工具里打开Network面板,刷新一次页面,看请求的状态码。如果是404,说明请求路径不对,检查@WebServlet的URL配置和前端xhr.open里的路径是否一致,注意部署上下文路径是否拼上去了。如果是500,说明Servlet或后端代码抛异常了,看后台控制台报什么错。最常见的是数据库驱动没引入,抛ClassNotFoundException。
如果请求状态是200,但前端依然走error或拿不到数据,问题一般出在onreadystatechange里的判断条件上。我之前见过一个同事写的代码,只判断了readyState === 4,没有判断status === 200,结果服务器返回500时也进入回调去解析JSON,自然就报错了。
4.2 JSON解析失败,responseText看起来却正常
JSON.parse报错有几个经典原因。
第一种,返回的字符串带了多余的前缀或后缀。比如Servlet之前用request.getRequestDispatcher("xxx.jsp")转发过,JSP页面的内容被写进了响应,导致响应不是纯JSON字符串。用Ajax时,后端要么直接通过resp.getWriter().write(json)返回纯数据,要么就走forward返回页面,两种方式不要混用。
第二种,JSON字符串里的引号被转义了。数据库里存的文本带有双引号,序列化成JSON时会被转义,前端拿到后JSON.parse是能正常解析的。但如果数据里有单引号,一些手拼JSON的代码就会出问题。强烈建议不要手动拼JSON,一律用JSON库序列化,这个原则能避免90%的格式问题。
第三种,响应头Content-Type是text/html。虽然JSON.parse在多数浏览器里照样能解析,但某些框架或封装库会依据Content-Type做预处理,遇到text/html就直接进error回调。统一设置application/json;charset=UTF-8能省去这类麻烦。
4.3 中文乱码问题反复出现
中文乱码如果反复出现,按我前面说过的三层方案逐个排查。前端有没有encodeURIComponent编码,后端POST有没有在getParameter前设置request.setCharacterEncoding("UTF-8"),响应有没有设置response.setContentType("application/json;charset=UTF-8"),数据库连接串有没有带characterEncoding=UTF-8。
还有一个隐蔽环节容易被忽略:Tomcat的server.xml里Connector如果设置了URIEncoding,那GET请求的编码以它为准;如果没设置,老版本Tomcat默认是ISO-8859-1。我建议确认一下当前Tomcat版本,8.0及以上默认UTF-8不用管,老版本就在server.xml里补上配置。另外,IDEA控制台输出乱码和页面乱码不是一回事,前者只影响调试,别混在一起排查。
4.4 部署到Tomcat后Ajax请求404
本地开发正常,部署到线上或稍微正式一点的Tomcat后,Ajax请求变得不一样,这一般是项目部署路径或上下文路径变化导致的。本地访问可能是http://localhost:8080/,部署到云服务器或单独Tomcat后,访问路径往往变成http://ip:8080/项目名/。
前端代码里的请求URL如果是写死的相对路径或绝对路径,就很容易出问题。我的建议是,页面里用${pageContext.request.contextPath}获取项目上下文路径,再拼上Servlet的映射路径。如果是纯静态HTML,就用相对路径,确保页面和Servlet在同一个上下文环境下。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 请求404 | URL路径错误或上下文路径缺失 | 检查@WebServlet注解、Network面板请求地址 |
| 请求500 | 后端代码异常,多为JDBC驱动或SQL问题 | 查看Tomcat控制台堆栈 |
| readyState=4但无响应数据 | 判断条件漏了status===200 | 补上状态码判断 |
| JSON.parse报错 | 响应不是纯JSON,或手拼JSON引号出错 | 改用JSON库序列化,检查是否误写了forward |
| 中文参数乱码 | 请求编码未指定或Tomcat URIEncoding不对 | 按POST/GET分别处理编码 |
| 数据库中文乱码 | 连接串缺characterEncoding参数 | 补上useUnicode和characterEncoding |
| 部署后请求失败 | 上下文路径变化 | 用pageContext.request.contextPath拼路径 |
5. 关于Ajax封装的个人心得
原生XMLHttpRequest掌握之后,再去看jQuery的$.ajax、axios之类的库,思路会清晰很多。我自己的习惯是,复杂项目用封装库减少重复代码,但页面里只有一两个Ajax请求时,就手写原生XMLHttpRequest,这样还能保持对数据的掌控感。
写Ajax代码这几年,我最大的体会是:Ajax本身的难点从来不是发送请求那几行代码,而是和它配套的整个数据链路。前端编码、后端解码、数据库存储、JSON序列化,任何一环不统一字符集,问题就追着你跑。所以现在带团队时,我要求新来的同事必须亲手写一个原生Ajax + Servlet + MySQL的最小案例,跑通了再碰框架。这个要求听起来基础,但真能过滤掉很多“复制粘贴代码”式的开发习惯。
最后分享一个小技巧:调试Ajax接口时,不要直接在前端代码里猜问题,先在浏览器地址栏手动访问Servlet的URL,加好参数,看响应内容是什么。如果直接在地址栏访问能返回正确的JSON,说明后端没问题,专心查前端;如果地址栏访问已经不对,那就先修后端,再回来看Ajax。这个分而治之的思路,能帮你省下大量排查时间。
