1. 从零认识Servlet技术栈
第一次接触Java Web开发时,那个黑乎乎的Tomcat控制台输出"Servlet initialized"让我愣了半天。作为承载Java Web二十余年的基石技术,Servlet的重要性怎么强调都不为过——它就像Web应用的神经系统,默默处理着每个HTTP请求的传导与响应。不同于Spring等框架的"开箱即用",理解Servlet能让你真正掌握Web请求的生命周期。
我在电商项目里就吃过亏:当时直接用Spring Boot跳过了Servlet学习,结果遇到Filter顺序问题时完全不知如何调试。后来花了两周时间补课,才发现很多"高级框架特性"其实都是Servlet规范的延伸。这次我们就用最接地气的方式,从HTTP协议与Servlet容器的协作关系讲起,手把手实现一个能处理表单提交的完整Servlet。
2. 环境准备与工具选型
2.1 JDK与IDE的选择困境
推荐使用JDK 11这个长期支持版本(LTS),避免最新版可能存在的兼容性问题。实测在JDK 17下某些旧版Tomcat会出现类加载警告。IDE方面:
- Eclipse:传统选择,但需要手动配置Server Runtime
- IntelliJ IDEA:社区版足够用,对Tomcat集成更好
- VS Code + Java插件:轻量但需要自己配构建工具
个人推荐IDEA+Tomcat 9的组合,平衡了易用性和稳定性。注意避开Tomcat 10这个"踩坑重灾区"——它默认使用Jakarta EE命名空间,与javax.*的旧项目不兼容。
2.2 手动创建Web项目的意义
虽然Maven archetype能快速生成项目骨架,但建议第一次还是手动创建:
- 在IDEA新建空项目
- 添加web模块(记得勾选"Create web.xml")
- 创建WEB-INF和lib目录
这个过程中你会理解:
- web.xml为什么放在WEB-INF下
- 编译输出目录为什么是WEB-INF/classes
- lib目录存放的jar包如何被加载
3. Servlet核心原理拆解
3.1 从HTTP到Servlet的旅程
当你在浏览器输入URL时:
- Tomcat的Connector接收到TCP数据包
- 解析成HttpServletRequest对象
- 根据web.xml映射找到对应的Servlet
- 调用service()方法生成响应
关键点在于:Servlet本身只是个接口,真正干活的是容器提供的实现类。我们写的MyServlet只是被容器调用的回调类。
3.2 生命周期方法实战
在自定义Servlet中重写这三个方法:
public class MyServlet extends HttpServlet { // 容器启动时执行(注意:不是每次请求!) @Override public void init() throws ServletException { System.out.println("正在初始化数据库连接..."); } // 处理GET请求 @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { resp.getWriter().print("Hello from GET"); } // 容器关闭时执行 @Override public void destroy() { System.out.println("释放连接池资源..."); } }常见误区:
- init()里放业务逻辑(应该只做初始化)
- 在service()中直接写响应(应该用doGet/doPost)
- 忘记调用super.destroy()导致资源泄漏
4. 部署与调试技巧
4.1 热部署的三种方式
爆炸式部署:直接修改WEB-INF/classes下的class文件
- 优点:即时生效
- 缺点:容易导致类加载混乱
上下文重载:配置context.xml的reloadable="true"
- 优点:自动检测变化
- 缺点:消耗性能(实测CPU升高15%)
JRebel插件:商业方案,修改代码自动热更新
- 适合大型项目,但需要破解或付费
4.2 必知的调试技巧
- 查看完整请求头:
Collections.list(req.getHeaderNames()) .forEach(name -> System.out.println(name + ": " + req.getHeader(name)));- 获取真实客户端IP(考虑代理情况):
String ip = req.getHeader("X-Forwarded-For") != null ? req.getHeader("X-Forwarded-For") : req.getRemoteAddr();- 强制浏览器不缓存响应:
resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");5. 进阶实战:处理文件上传
5.1 表单配置要点
必须设置enctype属性:
<form action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="myFile"> </form>5.2 Servlet处理代码
使用Apache Commons FileUpload更可靠:
DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 内存缓冲区1MB ServletFileUpload upload = new ServletFileUpload(factory); upload.setSizeMax(50 * 1024 * 1024); // 限制50MB List<FileItem> items = upload.parseRequest(request); for (FileItem item : items) { if (!item.isFormField()) { File uploadedFile = new File("/path/to/save", item.getName()); item.write(uploadedFile); } }5.3 常见坑点
- 忘记配置临时目录导致报错:
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));- 中文文件名乱码:
upload.setHeaderEncoding("UTF-8");- 文件覆盖问题:
String fileName = UUID.randomUUID() + "_" + item.getName();6. 性能优化方向
6.1 线程池配置
在server.xml中调整Connector参数:
<Connector executor="tomcatThreadPool" maxThreads="200" minSpareThreads="20" acceptCount="100" />6.2 异步Servlet实践
适合长时间任务(如报表生成):
@WebServlet(urlPatterns = "/report", asyncSupported = true) public class ReportServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) { AsyncContext ctx = req.startAsync(); executor.submit(() -> { // 模拟耗时操作 Thread.sleep(5000); ctx.getResponse().getWriter().print("Report generated"); ctx.complete(); }); } }6.3 缓存策略实现
利用Last-Modified头实现客户端缓存:
long lastModified = getLastModifiedFromDB(); if (req.getDateHeader("If-Modified-Since") >= lastModified) { resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); return; } resp.setDateHeader("Last-Modified", lastModified);7. 安全防护要点
7.1 SQL注入防御
错误的做法:
String sql = "SELECT * FROM users WHERE name='" + req.getParameter("name") + "'";正确的参数化查询:
PreparedStatement stmt = conn.prepareStatement( "SELECT * FROM users WHERE name=?"); stmt.setString(1, req.getParameter("name"));7.2 XSS防护
输出编码处理:
import org.apache.commons.text.StringEscapeUtils; String safeOutput = StringEscapeUtils.escapeHtml4(userInput); resp.getWriter().print(safeOutput);或者设置响应头:
resp.setHeader("X-XSS-Protection", "1; mode=block");7.3 CSRF防护
添加Token验证:
// 生成Token String token = UUID.randomUUID().toString(); session.setAttribute("csrfToken", token); // 在表单中添加 <input type="hidden" name="csrfToken" value="${csrfToken}"> // 验证Token if (!session.getAttribute("csrfToken").equals(req.getParameter("csrfToken"))) { throw new ServletException("CSRF验证失败"); }8. 现代演进方向
虽然现在流行Spring MVC等框架,但理解Servlet能让你:
- 快速定位框架底层问题
- 编写高性能Filter处理通用逻辑
- 自定义协议支持(如WebSocket初始握手)
比如用Servlet实现简单的REST:
@WebServlet("/api/*") public class RestServlet extends HttpServlet { protected void service(HttpServletRequest req, HttpServletResponse resp) { String path = req.getPathInfo(); switch (path) { case "/users": if ("GET".equals(req.getMethod())) { // 查询用户逻辑 } break; // 其他路由... } } }最后分享一个排查技巧:当遇到"404但路径明明正确"时,检查:
- web.xml的url-pattern是否带斜杠
- 项目Artifact的Context Path配置
- Tomcat的server.xml中Host的appBase路径