1. 为什么我们需要回归原生Servlet开发
在Spring Boot大行其道的今天,重新审视原生Servlet开发似乎有些不合时宜。但当我最近接手一个遗留系统维护任务时,发现项目里层层嵌套的框架和自动配置反而成了理解业务逻辑的障碍。这让我开始思考:我们是否过度依赖框架了?
原生Servlet开发就像武术中的基本功马步——看似简单枯燥,却是所有高级技巧的基础。通过手动搭建Servlet项目,你能真正理解:
- HTTP请求从进入容器到返回响应的完整生命周期
- 线程模型与请求处理的关系
- 过滤器链(Filter Chain)的实际工作方式
- 会话管理的底层机制
提示:即使你日常使用Spring MVC,了解Servlet原生工作方式也能帮助你在遇到复杂问题时更快定位原因。就像知道汽车发动机原理的司机,在车辆异常时更能准确判断问题。
最近在排查一个线上问题时,发现由于对Servlet线程模型理解不足,团队中有人误用了实例变量导致并发问题。这更坚定了我认为每个Java Web开发者都应该至少亲手搭建过一个纯Servlet项目的想法。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 工程搭建:从零开始的手动配置
2.1 开发环境准备
与使用Spring Initializr一键生成项目不同,原生Servlet项目需要更多手动配置。以下是基础环境要求:
- JDK 8+:建议使用LTS版本(如JDK 11或17)
- Tomcat 8.5+:这是Servlet 3.1规范的参考实现
- 纯文本编辑器或IDE:IntelliJ IDEA或Eclipse都可以,但需要关闭框架支持
创建项目目录结构:
code复制/project-root
├── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── example
│ │ └── web
│ ├── resources
│ └── webapp
│ ├── WEB-INF
│ │ └── web.xml
│ └── index.jsp
└── pom.xml (或build.gradle)
2.2 手动配置web.xml
在WEB-INF/web.xml中,我们需要明确定义Servlet和映射关系。这是与现代注解方式最大的不同:
xml复制<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>com.example.web.HelloServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
</web-app>
这种显式声明的方式虽然繁琐,但能让你清楚地看到每个组件的定义和关联关系。我在教学中发现,很多使用Spring注解开发的学生其实并不清楚@WebServlet背后的实际工作原理。
2.3 编写第一个Servlet
创建一个简单的HelloServlet:
java复制package com.example.web;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
public class HelloServlet extends HttpServlet {
private boolean debug;
@Override
public void init() throws ServletException {
this.debug = Boolean.parseBoolean(getInitParameter("debug"));
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if(debug) {
System.out.println("Debug: Handling GET request");
}
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><body>");
out.println("<h1>Hello, Servlet World!</h1>");
out.println("<p>Request URI: " + req.getRequestURI() + "</p>");
out.println("</body></html>");
}
}
注意我们手动处理了:
- 初始化参数(通过
init()方法) - 请求方法分发(
doGet) - 响应内容类型设置
- 输出流的直接操作
3. 构建与部署:脱离框架的独立运行
3.1 Maven配置要点
在pom.xml中,我们只需要最基本的依赖:
xml复制<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSP支持 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>my-servlet-app</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
关键点:
provided范围确保这些依赖不会打包到WAR中- 不需要Spring、Hibernate等常见框架依赖
- 保持构建过程尽可能简单
3.2 手动部署到Tomcat
- 运行
mvn clean package生成WAR文件 - 将WAR文件复制到Tomcat的
webapps目录 - 启动Tomcat:
${TOMCAT_HOME}/bin/startup.sh(Linux)或startup.bat(Windows) - 访问
http://localhost:8080/my-servlet-app/hello
注意:现代IDE通常提供一键部署功能,但我建议至少手动完成一次完整部署流程,这能帮助你理解应用服务器实际加载应用的机制。
4. 进阶实践:构建完整功能
4.1 实现用户登录流程
让我们实现一个完整的用户登录功能,展示原生Servlet如何处理:
java复制public class LoginServlet extends HttpServlet {
// 模拟用户存储
private Map<String, String> users = Map.of(
"admin", "admin123",
"user", "user123"
);
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
if(users.containsKey(username) && users.get(username).equals(password)) {
HttpSession session = req.getSession();
session.setAttribute("username", username);
resp.sendRedirect("welcome.jsp");
} else {
req.setAttribute("error", "Invalid credentials");
RequestDispatcher rd = req.getRequestDispatcher("login.jsp");
rd.forward(req, resp);
}
}
}
对应的login.jsp:
jsp复制<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<title>Login</title>
</head>
<body>
<% if(request.getAttribute("error") != null) { %>
<p style="color:red"><%= request.getAttribute("error") %></p>
<% } %>
<form method="post" action="login">
Username: <input type="text" name="username"><br>
Password: <input type="password" name="password"><br>
<input type="submit" value="Login">
</form>
</body>
</html>
这个例子展示了:
- 表单处理
- 会话管理
- 请求转发
- 属性传递
- JSP集成
4.2 过滤器实现:日志与鉴权
添加一个简单的日志过滤器:
java复制public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) {
System.out.println("LoggingFilter initialized");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
long start = System.currentTimeMillis();
HttpServletRequest req = (HttpServletRequest) request;
System.out.println("Request started: " + req.getRequestURI());
chain.doFilter(request, response);
long duration = System.currentTimeMillis() - start;
System.out.println("Request completed in " + duration + "ms");
}
@Override
public void destroy() {
System.out.println("LoggingFilter destroyed");
}
}
在web.xml中注册:
xml复制<filter>
<filter-name>loggingFilter</filter-name>
<filter-class>com.example.filters.LoggingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>loggingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
5. 性能优化与问题排查
5.1 线程安全注意事项
Servlet的线程模型是理解性能问题的关键。每个请求通常由容器线程池中的一个线程处理,这意味着:
- Servlet实例通常是单例的:不要使用实例变量存储请求特定状态
- 正确使用局部变量:方法内的局部变量是线程安全的
- 同步谨慎使用:过度同步会导致性能下降
我曾遇到一个案例:开发者在Servlet中使用了实例变量来缓存数据库连接,导致并发请求时出现连接混乱。正确的做法应该是:
java复制public class SafeServlet extends HttpServlet {
// 错误示例 - 实例变量
// private Connection connection;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 正确做法 - 局部变量
Connection connection = getConnection();
try {
// 使用连接
} finally {
closeConnection(connection);
}
}
}
5.2 内存泄漏排查
原生Servlet开发中常见的内存泄漏场景:
- 静态集合持有请求对象:静态Map缓存HttpRequest对象
- 未关闭的资源:数据库连接、文件流等
- 线程未正确终止:自行创建的线程未随应用停止
使用VisualVM或JProfiler等工具监控:
- 老年代内存增长
- 线程数变化
- 未关闭的连接数
5.3 性能调优技巧
- 响应缓冲:合理设置缓冲区大小
java复制resp.setBufferSize(8192); // 8KB - 内容压缩:减少网络传输量
java复制String acceptEncoding = req.getHeader("Accept-Encoding"); if(acceptEncoding != null && acceptEncoding.contains("gzip")) { resp.setHeader("Content-Encoding", "gzip"); // 使用GZIPOutputStream包装响应流 } - 合理使用include/forward:减少重复逻辑
6. 与现代开发流程的结合
6.1 如何逐步引入框架
当你理解了原生Servlet后,可以更有意识地引入框架:
- 先添加DI容器:如Guice或Dagger
- 然后引入路由框架:如Jersey或Spark
- 最后考虑ORM:如Hibernate或MyBatis
关键是要明白每个框架解决了什么问题,而不是盲目引入。
6.2 保持简单性的原则
在我的项目中,我遵循这些原则来避免过度设计:
- 按需引入依赖:只有当现有代码变得难以维护时才考虑框架
- 保持分层清晰:即使没有框架,也要分离业务逻辑和展示层
- 编写可测试代码:确保Servlet逻辑可以独立于容器测试
java复制// 业务逻辑分离示例
public class OrderService {
public Order createOrder(OrderRequest request) {
// 纯业务逻辑
}
}
public class OrderServlet extends HttpServlet {
private OrderService orderService;
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
OrderRequest request = parseRequest(req);
Order order = orderService.createOrder(request);
writeResponse(resp, order);
}
}
6.3 容器化部署实践
即使简单Servlet应用也可以容器化:
dockerfile复制FROM tomcat:8.5-jre11
COPY target/my-servlet-app.war /usr/local/tomcat/webapps/
EXPOSE 8080
CMD ["catalina.sh", "run"]
构建和运行:
bash复制docker build -t my-servlet-app .
docker run -p 8080:8080 my-servlet-app
这种轻量级部署方式非常适合微服务场景。
