1. 项目概述:基于Tomcat 9的Fetch异步交互实现
最近在重构一个老项目的前端交互模块时,我决定用现代浏览器原生支持的Fetch API替换传统的jQuery.ajax调用。这个简单的示例展示了如何通过Fetch与Tomcat 9后端的Servlet进行异步通信。虽然代码量不大,但其中涉及的前后端衔接细节值得新手特别注意。
2. 核心组件解析
2.1 前端JSP页面实现
先看前端页面的关键代码结构:
html复制<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>fetch</title>
</head>
<body>
<input type="text" id="inputone" name="inputNameOne"
value="fetchGet服务" />
<button onclick="getFromServer()">fetch和服务器交互</button>
<div id="smallRainShow" style="width: 400px; height: 300px;"></div>
这里有几个需要注意的技术点:
- 页面编码声明为UTF-8,这对中文处理至关重要
- 包含一个文本输入框和触发按钮
- 预留了id为smallRainShow的div作为响应数据显示区域
2.2 Fetch交互逻辑实现
JavaScript部分的fetch调用是核心:
javascript复制function getFromServer() {
const valueTo = document.getElementById('inputone').value;
fetch('<%=request.getContextPath()%>/smallRainFetchServlet?inputNameOne=' + valueTo)
.then(smallRainRes => {
return smallRainRes.text();
})
.then(smallRainData => {
document.getElementById("smallRainShow").innerHTML = smallRainData;
})
.catch(error => {
alert("error");
});
}
这段代码的工作流程是:
- 获取输入框的值
- 通过fetch发起GET请求
- 使用Promise链处理响应
- 将返回的纯文本数据显示在页面上
提示:在实际项目中,建议对valueTo进行URL编码处理,使用encodeURIComponent()方法避免特殊字符导致的问题。
3. 后端Servlet实现
3.1 Servlet基础配置
后端使用了一个简单的Servlet:
java复制@WebServlet("/smallRainFetchServlet")
public class SmallRainFetchServlet extends HttpServlet {
@Override
public void doPost(HttpServletRequest smallrainReq, HttpServletResponse smallRainRes)
throws ServletException, IOException {
doGet(smallrainReq, smallRainRes);
}
@Override
public void doGet(HttpServletRequest smallrainReq, HttpServletResponse smallRainRes)
throws ServletException, IOException {
smallRainRes.setCharacterEncoding("UTF-8");
PrintWriter smallrainOut = smallRainRes.getWriter();
String smallRainWeb=smallrainReq.getParameter("inputNameOne");
smallrainOut.print("得到服务器返回:"+smallRainWeb);
}
}
关键点解析:
- 使用@WebServlet注解配置Servlet路径
- 同时处理GET和POST请求(POST转发到GET)
- 设置响应编码为UTF-8
- 获取请求参数并返回简单字符串
3.2 字符编码处理要点
在实际项目中,我遇到过很多中文乱码问题,这里特别强调编码设置:
- JSP页面头部声明UTF-8编码
- Servlet中设置response的字符编码
- Tomcat的server.xml也需要检查URIEncoding配置
注意:如果返回JSON数据,还需要设置contentType为application/json;charset=UTF-8
4. 项目部署与测试
4.1 Tomcat 9部署要点
- 将项目打包为WAR文件
- 部署到Tomcat的webapps目录
- 确保Tomcat版本是9.x
- 检查Tomcat日志确认无启动错误
4.2 常见问题排查
在实际测试中可能会遇到以下问题:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 404错误 | Servlet路径配置错误 | 检查@WebServlet注解路径 |
| 中文乱码 | 编码设置不一致 | 统一设置为UTF-8 |
| 跨域错误 | 不同端口访问 | 配置CORS头部或使用同源 |
5. 进阶优化建议
5.1 错误处理增强
当前示例中的错误处理比较简单:
javascript复制.catch(error => {
alert("error");
});
建议改进为:
javascript复制.catch(error => {
console.error('Fetch错误:', error);
document.getElementById("smallRainShow").innerHTML =
"请求失败: " + error.message;
});
5.2 添加加载状态指示
良好的用户体验应该包含加载状态反馈:
javascript复制function getFromServer() {
const displayDiv = document.getElementById("smallRainShow");
displayDiv.innerHTML = "加载中...";
const valueTo = document.getElementById('inputone').value;
fetch('<%=request.getContextPath()%>/smallRainFetchServlet?inputNameOne=' + valueTo)
// ...then处理保持不变
.catch(error => {
displayDiv.innerHTML = "请求失败";
console.error(error);
});
}
5.3 支持JSON数据交互
现代Web应用更多使用JSON格式:
前端修改:
javascript复制.then(smallRainRes => {
return smallRainRes.json(); // 改为json()
})
后端修改:
java复制smallRainRes.setContentType("application/json;charset=UTF-8");
String jsonResponse = "{\"message\":\"得到服务器返回:" + smallRainWeb + "\"}";
smallrainOut.print(jsonResponse);
6. 性能优化考虑
6.1 请求缓存策略
对于不常变的数据,可以考虑缓存:
javascript复制fetch(url, {
cache: 'force-cache' // 或 'no-cache'
})
6.2 请求超时处理
原生fetch不支持超时设置,可以通过Promise.race实现:
javascript复制const timeout = new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('请求超时')), 5000);
});
Promise.race([
fetch(url),
timeout
]).then(response => {
// 处理响应
}).catch(error => {
// 处理错误
});
7. 安全注意事项
7.1 输入验证
永远不要信任客户端输入:
java复制String smallRainWeb = smallrainReq.getParameter("inputNameOne");
if(smallRainWeb == null || smallRainWeb.trim().isEmpty()) {
smallRainRes.sendError(HttpServletResponse.SC_BAD_REQUEST, "无效输入");
return;
}
7.2 CSRF防护
对于重要操作,应该添加CSRF令牌:
html复制<input type="hidden" name="csrfToken" value="${csrfToken}">
后端验证:
java复制String sessionToken = (String)request.getSession().getAttribute("CSRF_TOKEN");
String requestToken = request.getParameter("csrfToken");
if(!sessionToken.equals(requestToken)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
8. 实际项目中的应用建议
在真实项目开发中,我有以下几点经验分享:
- 将fetch调用封装成统一的API模块,便于维护
- 添加请求拦截器处理公共逻辑(如添加认证头)
- 建立标准的错误处理机制
- 对API响应进行统一包装(包含状态码、消息和数据)
一个简单的封装示例:
javascript复制class ApiClient {
static async request(url, options = {}) {
const headers = {
'Content-Type': 'application/json',
...options.headers
};
try {
const response = await fetch(url, {...options, headers});
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error('API请求错误:', error);
throw error;
}
}
static get(url) {
return this.request(url, {method: 'GET'});
}
static post(url, data) {
return this.request(url, {
method: 'POST',
body: JSON.stringify(data)
});
}
}
// 使用示例
ApiClient.get('/api/data')
.then(data => console.log(data))
.catch(error => console.error(error));
9. 与传统Ajax的对比
相比传统XMLHttpRequest,Fetch API有以下优势:
- 基于Promise,避免回调地狱
- 更简洁的API设计
- 内置对Streaming的支持
- 更现代的请求和响应抽象
但也有一些需要注意的差异点:
| 特性 | Fetch | XHR |
|---|---|---|
| 超时控制 | 需自行实现 | 原生支持 |
| 请求取消 | 使用AbortController | 原生abort() |
| 进度事件 | 需通过响应body处理 | 原生支持 |
10. 浏览器兼容性考虑
虽然Fetch API在现代浏览器中得到广泛支持,但在以下情况需要注意:
- 不支持IE11及更早版本
- 某些老版本移动浏览器可能存在兼容问题
- 某些高级功能(如流式处理)支持度不一
解决方案:
- 使用polyfill如whatwg-fetch
- 对于必须支持老浏览器的项目,考虑提供回退方案
html复制<script>
if (!window.fetch) {
document.write('<script src="https://cdn.jsdelivr.net/npm/whatwg-fetch@3.6.2/dist/fetch.umd.min.js"><\/script>');
}
</script>
11. 调试技巧
在开发过程中,我总结了一些有用的调试方法:
-
使用浏览器开发者工具查看网络请求
- 检查请求头和响应头
- 查看请求payload
- 分析时间线
-
服务端日志调试
- 在Servlet中添加请求日志
- 记录请求参数和响应数据
-
使用Postman等工具测试API接口
-
添加详细的错误日志:
javascript复制fetch(url)
.then(response => {
if (!response.ok) {
console.error('响应状态异常', {
status: response.status,
statusText: response.statusText,
url: response.url
});
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
return response.text();
})
12. 性能监控
对于生产环境,建议添加性能监控:
javascript复制const startTime = performance.now();
fetch(url)
.then(response => {
const duration = performance.now() - startTime;
console.log(`请求耗时: ${duration.toFixed(2)}ms`);
// 或者发送到监控系统
return response;
})
13. 单元测试建议
为了保证代码质量,应该编写单元测试:
前端测试示例(使用Jest):
javascript复制describe('getFromServer', () => {
beforeEach(() => {
global.fetch = jest.fn();
document.body.innerHTML = `
<input id="inputone" value="test" />
<div id="smallRainShow"></div>
`;
});
it('应该正确处理服务器响应', async () => {
fetch.mockResolvedValueOnce({
text: () => Promise.resolve('测试响应')
});
await getFromServer();
expect(document.getElementById('smallRainShow').innerHTML)
.toBe('测试响应');
});
});
后端测试示例(使用JUnit):
java复制@Test
public void testDoGet() throws Exception {
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
PrintWriter writer = mock(PrintWriter.class);
when(request.getParameter("inputNameOne")).thenReturn("测试");
when(response.getWriter()).thenReturn(writer);
SmallRainFetchServlet servlet = new SmallRainFetchServlet();
servlet.doGet(request, response);
verify(writer).print("得到服务器返回:测试");
}
14. 项目结构优化
随着项目规模扩大,建议采用更合理的结构:
code复制src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ ├── controller/ # 控制器类
│ │ ├── service/ # 业务逻辑
│ │ └── util/ # 工具类
│ ├── resources/ # 配置文件
│ └── webapp/
│ ├── WEB-INF/
│ ├── static/ # 静态资源
│ │ ├── js/ # JavaScript
│ │ └── css/ # 样式表
│ └── index.jsp # 入口页面
└── test/ # 测试代码
15. 构建与部署自动化
建议配置自动化构建流程:
- 使用Maven或Gradle管理Java依赖
- 配置前端构建工具如Webpack处理JavaScript
- 设置CI/CD管道自动测试和部署
示例pom.xml配置:
xml复制<build>
<finalName>myapp</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
<configuration>
<path>/</path>
</configuration>
</plugin>
</plugins>
</build>
16. 日志记录最佳实践
完善的日志记录对问题排查至关重要:
java复制import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@WebServlet("/smallRainFetchServlet")
public class SmallRainFetchServlet extends HttpServlet {
private static final Logger logger = LoggerFactory.getLogger(SmallRainFetchServlet.class);
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) {
try {
String param = request.getParameter("inputNameOne");
logger.info("收到请求,参数: {}", param);
// 处理逻辑...
} catch (Exception e) {
logger.error("处理请求时出错", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
17. 安全加固措施
除了前面提到的CSRF防护,还应该:
- 实施HTTPS加密传输
- 设置安全相关的HTTP头
- X-Content-Type-Options
- X-Frame-Options
- Content-Security-Policy
- 对敏感操作进行二次验证
在Servlet中设置安全头:
java复制response.setHeader("X-Content-Type-Options", "nosniff");
response.setHeader("X-Frame-Options", "DENY");
response.setHeader("Content-Security-Policy", "default-src 'self'");
18. 性能调优技巧
根据我的经验,以下几点可以提升性能:
- 启用Tomcat的gzip压缩
- 合理设置HTTP缓存头
- 对静态资源使用CDN
- 考虑启用HTTP/2
在Tomcat的server.xml中启用压缩:
xml复制<Connector port="8080" protocol="HTTP/1.1"
compression="on"
compressionMinSize="1024"
compressableMimeType="text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json"
/>
19. 现代替代方案
虽然这个示例使用JSP+Servlet,但现代Java Web开发还有其他选择:
- Spring Boot + Thymeleaf
- Jakarta EE (原Java EE)
- Micronaut或Quarkus等轻量框架
例如使用Spring Boot的@RestController:
java复制@RestController
@RequestMapping("/api")
public class MyController {
@GetMapping("/data")
public ResponseEntity<String> getData(@RequestParam String input) {
return ResponseEntity.ok("得到服务器返回:" + input);
}
}
20. 前端框架集成
如果使用React、Vue等现代前端框架,交互方式类似但更结构化:
React组件示例:
jsx复制function FetchExample() {
const [input, setInput] = useState('');
const [response, setResponse] = useState('');
const handleSubmit = async () => {
try {
const res = await fetch(`/api/data?input=${encodeURIComponent(input)}`);
const text = await res.text();
setResponse(text);
} catch (error) {
console.error('请求失败:', error);
setResponse('请求失败');
}
};
return (
<div>
<input value={input} onChange={e => setInput(e.target.value)} />
<button onClick={handleSubmit}>提交</button>
<div>{response}</div>
</div>
);
}
21. 异步处理进阶
对于更复杂的异步场景,可以考虑:
- 使用async/await语法
- 实现请求队列
- 添加重试机制
- 处理并发请求
async/await示例:
javascript复制async function getData() {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('请求失败');
return await response.text();
} catch (error) {
console.error('获取数据失败:', error);
throw error;
}
}
// 使用
(async () => {
try {
const data = await getData();
console.log('获取到的数据:', data);
} catch {
// 处理错误
}
})();
22. 项目文档建议
良好的文档应该包含:
- API接口说明
- 请求/响应示例
- 错误代码列表
- 版本变更记录
Markdown格式的API文档示例:
markdown复制## GET /smallRainFetchServlet
获取服务器响应
**请求参数**:
- `inputNameOne`: string - 输入内容
**响应**:
```
得到服务器返回:{输入内容}
```
**示例**:
```javascript
fetch('/smallRainFetchServlet?inputNameOne=测试')
.then(res => res.text())
.then(console.log);
// 输出: "得到服务器返回:测试"
```
23. 持续学习资源
为了深入理解相关技术,我推荐以下资源:
- MDN Fetch文档
- Java Servlet规范
- Tomcat官方文档
- HTTP协议RFC文档
24. 实际项目中的经验教训
在多个项目中使用Fetch后,我总结了这些经验:
- 总是处理网络错误和HTTP错误状态
- 对于关键操作,添加确认对话框
- 考虑添加请求取消功能
- 实现加载状态指示
- 对用户操作进行节流防抖
取消请求示例:
javascript复制const controller = new AbortController();
fetch(url, {
signal: controller.signal
}).catch(err => {
if (err.name === 'AbortError') {
console.log('请求被取消');
}
});
// 取消请求
controller.abort();
25. 项目扩展思路
基于这个简单示例,可以扩展出很多实用功能:
- 文件上传下载
- 服务器推送(SSE)
- WebSocket实时通信
- 分页数据加载
- 表单验证与提交
文件上传示例:
前端:
javascript复制const formData = new FormData();
formData.append('file', fileInput.files[0]);
fetch('/upload', {
method: 'POST',
body: formData
});
后端:
java复制Part filePart = request.getPart("file");
InputStream fileContent = filePart.getInputStream();
// 处理文件内容
26. 跨平台兼容方案
如果需要支持多种客户端,可以考虑:
- 设计统一的REST API
- 使用标准数据格式(JSON)
- 实现版本控制
- 提供详细的API文档
版本控制示例:
java复制@WebServlet("/v1/smallRainFetchServlet")
public class SmallRainFetchServletV1 extends HttpServlet {
// 版本1实现
}
@WebServlet("/v2/smallRainFetchServlet")
public class SmallRainFetchServletV2 extends HttpServlet {
// 版本2实现
}
27. 微服务架构下的调整
如果迁移到微服务架构:
- 前端直接调用各个微服务
- 或通过API网关聚合
- 需要考虑额外的安全措施
- 实现服务发现机制
通过网关调用的示例:
javascript复制fetch('/api-gateway/smallRainFetch?inputNameOne=test')
28. 容器化部署
现代部署方式推荐使用Docker:
Dockerfile示例:
dockerfile复制FROM tomcat:9-jdk11
COPY target/myapp.war /usr/local/tomcat/webapps/ROOT.war
EXPOSE 8080
CMD ["catalina.sh", "run"]
29. 监控与告警
生产环境应该配置:
- 应用性能监控(APM)
- 错误日志收集
- 健康检查接口
- 自动告警机制
健康检查Servlet示例:
java复制@WebServlet("/health")
public class HealthCheckServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.setContentType("application/json");
resp.getWriter().print("{\"status\":\"UP\"}");
}
}
30. 总结与个人建议
经过多个项目的实践,我认为以下几点特别重要:
- 保持前后端接口的清晰定义
- 建立统一的错误处理机制
- 重视日志记录和监控
- 定期进行代码审查
- 持续关注新技术发展但不要盲目跟风
对于初学者,我的建议是从这个简单示例开始,逐步扩展功能,同时注意代码组织和架构设计。随着经验积累,你会自然形成适合自己的最佳实践。