1. 请求与响应基础概念解析
在JavaWeb开发中,HTTP请求与响应构成了客户端与服务器交互的基础通信模型。当用户在浏览器地址栏输入URL点击访问时,就会产生一个完整的"请求-响应"周期。这个过程中,浏览器作为客户端会构造符合HTTP协议规范的请求报文,通过网络传输到服务器端;服务器接收并解析请求后,生成对应的响应数据返回给客户端。
Servlet作为JavaWeb的核心组件,专门用于处理这种请求-响应模型。每个Servlet都必须实现javax.servlet.Servlet接口,其中最关键的service()方法就是用来处理请求并生成响应的入口点。在实际开发中,我们通常继承HttpServlet类,它已经实现了Servlet接口并提供了更便捷的HTTP协议相关方法。
关键理解:HTTP协议是无状态的,意味着服务器不会记住上一次的请求内容。这种设计虽然简化了服务器实现,但也带来了会话管理的新挑战,后续我们会通过Cookie和Session来解决。
2. HttpServletRequest对象深度剖析
2.1 请求对象的核心方法
HttpServletRequest对象封装了客户端发来的所有请求信息,开发中常用的方法包括:
java复制// 获取请求参数
String username = request.getParameter("username");
// 获取请求头信息
String userAgent = request.getHeader("User-Agent");
// 获取请求URL信息
String requestURI = request.getRequestURI(); // 例如:/webapp/login
String contextPath = request.getContextPath(); // 例如:/webapp
String servletPath = request.getServletPath(); // 例如:/login
2.2 请求参数的获取技巧
对于表单提交的数据,除了常规的getParameter()方法外,还需要注意:
- 多值参数处理:当复选框等多选组件提交时,应该使用getParameterValues()
java复制String[] hobbies = request.getParameterValues("hobby");
- 中文乱码解决方案:POST请求需要设置字符编码
java复制request.setCharacterEncoding("UTF-8"); // 必须在第一次getParameter前调用
- GET请求的URL编码问题:需要在服务器配置中修改URIEncoding
xml复制<!-- Tomcat的server.xml配置 -->
<Connector URIEncoding="UTF-8" ... />
2.3 请求域的应用场景
request对象提供了setAttribute()和getAttribute()方法,可以在一次请求转发过程中共享数据:
java复制// 在ServletA中设置属性
request.setAttribute("message", "登录成功");
// 通过转发到ServletB
RequestDispatcher rd = request.getRequestDispatcher("/servletB");
rd.forward(request, response);
// 在ServletB中可以获取该属性
String msg = (String)request.getAttribute("message");
注意事项:请求域的生命周期仅限于本次请求,当响应返回客户端后,其中的属性就会失效。这与后面要讲的Session作用域有本质区别。
3. HttpServletResponse对象实战指南
3.1 响应对象的核心配置
响应对象用于设置返回给客户端的内容,常用配置包括:
java复制// 设置内容类型和编码
response.setContentType("text/html;charset=UTF-8");
// 设置缓存控制
response.setHeader("Cache-Control", "no-cache");
// 设置状态码
response.setStatus(HttpServletResponse.SC_OK); // 200
3.2 响应输出流的正确使用
向客户端输出内容时,需要注意字符流和字节流的区别:
- 字符流输出文本内容(推荐):
java复制PrintWriter out = response.getWriter();
out.println("<h1>欢迎登录系统</h1>");
- 字节流输出二进制数据(如图片):
java复制OutputStream os = response.getOutputStream();
byte[] imageBytes = getImageBytes();
os.write(imageBytes);
常见错误:同时获取字符流和字节流会导致IllegalStateException异常。一个响应中只能使用其中一种输出方式。
3.3 重定向的实现原理
与请求转发不同,重定向是让客户端发起新的请求:
java复制response.sendRedirect("/newLocation");
重定向的实际工作原理:
- 服务器返回302状态码和Location头
- 浏览器自动向新地址发起GET请求
- 地址栏URL会发生变化
- 属于两次独立的请求,request域不共享
4. 请求转发与重定向的深度对比
4.1 核心区别对照表
| 特性 | 请求转发(forward) | 重定向(redirect) |
|---|---|---|
| 请求次数 | 1次 | 2次 |
| URL变化 | 不变 | 变化 |
| 数据共享 | 通过request域共享 | 不能共享request域 |
| 目标资源限制 | 必须为同一web应用 | 可以跨应用甚至跨域名 |
| 浏览器感知 | 不可见 | 可见 |
4.2 典型应用场景选择
- 使用请求转发的场景:
- 需要保持POST请求方法
- 需要在多个Servlet/JSP间共享request数据
- 不希望用户看到实际处理资源的URL
- 使用重定向的场景:
- 完成表单提交后防止重复提交(POST-REDIRECT-GET模式)
- 需要跳转到外部网站
- 登录后跳转到用户请求的原始页面
5. 中文乱码问题的系统解决方案
5.1 请求阶段的乱码处理
- GET请求乱码:
- Tomcat8+默认使用UTF-8解码URI,旧版本需要配置:
xml复制<Connector URIEncoding="UTF-8" ... />
- POST请求乱码:
java复制request.setCharacterEncoding("UTF-8"); // 必须在获取参数前调用
5.2 响应阶段的乱码处理
- 设置响应头Content-Type:
java复制response.setContentType("text/html;charset=UTF-8");
- 或者分别设置:
java复制response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html");
5.3 下载文件时的编码处理
当提供文件下载时,需要对文件名进行编码:
java复制String fileName = "中文文件.txt";
String encodedFileName = URLEncoder.encode(fileName, "UTF-8");
response.setHeader("Content-Disposition",
"attachment; filename=" + encodedFileName);
6. 实战案例:用户登录功能实现
6.1 登录表单设计
html复制<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
6.2 登录Servlet处理
java复制protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1. 设置字符编码
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
// 2. 获取请求参数
String username = request.getParameter("username");
String password = request.getParameter("password");
// 3. 业务逻辑验证
if("admin".equals(username) && "123456".equals(password)) {
// 登录成功,存储用户信息到session
HttpSession session = request.getSession();
session.setAttribute("user", username);
// 重定向到主页,防止重复提交
response.sendRedirect("/index.jsp");
} else {
// 登录失败,返回错误信息
request.setAttribute("errorMsg", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
6.3 登录状态检查过滤器
java复制public class LoginFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
// 检查session中是否存在用户信息
HttpSession session = request.getSession(false);
if(session == null || session.getAttribute("user") == null) {
// 未登录,重定向到登录页面
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
chain.doFilter(request, response);
}
}
7. 性能优化与安全实践
7.1 响应缓存控制
合理设置缓存策略可以显著提升性能:
java复制// 静态资源设置长期缓存
response.setHeader("Cache-Control", "max-age=31536000");
// 动态内容禁用缓存
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
7.2 安全防护措施
- XSS防护:
java复制String userInput = request.getParameter("input");
// 使用ESAPI等工具进行HTML转义
String safeOutput = ESAPI.encoder().encodeForHTML(userInput);
- CSRF防护:
java复制// 生成并存储CSRF Token
String csrfToken = UUID.randomUUID().toString();
request.getSession().setAttribute("csrfToken", csrfToken);
request.setAttribute("csrfToken", csrfToken);
// 在表单中添加隐藏域
<input type="hidden" name="csrfToken" value="${csrfToken}">
- 点击劫持防护:
java复制response.setHeader("X-Frame-Options", "DENY");
8. 调试技巧与常见问题排查
8.1 请求信息日志记录
开发时打印完整请求信息有助于调试:
java复制System.out.println("请求方法:" + request.getMethod());
System.out.println("请求URL:" + request.getRequestURL());
System.out.println("查询字符串:" + request.getQueryString());
// 打印所有请求头
Enumeration<String> headerNames = request.getHeaderNames();
while(headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
System.out.println(name + ": " + request.getHeader(name));
}
// 打印所有参数
Enumeration<String> paramNames = request.getParameterNames();
while(paramNames.hasMoreElements()) {
String name = paramNames.nextElement();
System.out.println(name + "=" + request.getParameter(name));
}
8.2 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 获取中文参数乱码 | 未设置请求编码 | request.setCharacterEncoding("UTF-8") |
| 响应内容乱码 | 未设置响应Content-Type或编码 | response.setContentType("text/html;charset=UTF-8") |
| getWriter()抛出异常 | 之前已经调用了getOutputStream() | 确保一个响应中只使用一种输出流 |
| 重定向后丢失数据 | 重定向是两次独立请求 | 使用Session存储需要跨请求的数据 |
| 表单重复提交 | 没有使用POST-REDIRECT-GET模式 | 提交后重定向到结果页面 |
在实际开发中,我发现合理使用请求转发和重定向能解决大部分页面流转问题,而正确理解请求和响应对象的生命周期则是避免内存泄漏的关键。对于初学者来说,建议先用Postman等工具模拟各种请求,观察服务器端的处理过程,这样能更快掌握请求响应的核心原理。