1. 初识Servlet:Web开发的基石
2003年我第一次接触Java Web开发时,Servlet技术就像一堵高墙横在面前。当时为了搞懂一个简单的"Hello World"页面为何能显示在浏览器上,整整折腾了两天。如今作为过来人,我将用最直白的方式带你们穿透这层迷雾。
Servlet本质上是用Java编写的服务器端程序,它像餐厅后厨的厨师——接收来自前厅(浏览器)的点单请求(HTTP Request),烹饪处理数据后,再将做好的菜品(HTTP Response)送回前厅。与直接写HTML不同,Servlet允许我们用Java代码动态生成网页内容,这是所有Java Web框架的底层基础。
关键认知:Servlet没有main()方法,它的生命周期完全由Web容器(如Tomcat)控制。这就像厨师不需要自己开门营业,只需在厨房等待餐厅经理分配任务。
当前主流使用的是Servlet 4.0规范(Jakarta EE 9+),但学习时用Servlet 3.1(Java EE 7)完全足够。需要注意区分几个易混淆概念:
- Servlet:处理请求的核心接口
- Servlet容器:管理Servlet生命周期的运行时环境(如Tomcat)
- Servlet规范:定义容器与Servlet间的交互规则
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:构建你的第一个Servlet厨房
2.1 工具选型与安装
工欲善其事必先利其器,以下是经过实战验证的工具组合:
-
JDK 8/11:推荐Amazon Corretto或OpenJDK
bash复制# 验证安装 java -version javac -version -
IntelliJ IDEA:社区版足够用,比Eclipse更友好的Servlet支持
-
Tomcat 9:最稳定的Servlet容器
bash复制# 启动命令(Linux/macOS) cd apache-tomcat-9.0.xx/bin ./startup.sh
避坑提示:遇到"JAVA_HOME not defined"错误时,需在tomcat的setenv.sh中添加:
bash复制export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))
2.2 项目结构解剖
标准的Servlet项目目录应如下布局:
code复制my-servlet-app
├── src
│ └── main
│ ├── java # Servlet源代码
│ ├── resources # 配置文件
│ └── webapp # WEB-INF和静态资源
│ ├── WEB-INF
│ │ ├── web.xml # 部署描述符(Servlet 3.0+可选)
│ │ └── classes # 编译后的class文件
│ └── index.jsp # 欢迎页面
└── pom.xml # Maven配置
3. 手写第一个Servlet:从HelloWorld深入原理
3.1 创建Servlet类的三种方式
方式一:实现Servlet接口(最基础)
java复制public class BasicServlet implements Servlet {
private ServletConfig config;
@Override
public void init(ServletConfig config) {
this.config = config;
System.out.println("Servlet初始化完成");
}
@Override
public void service(ServletRequest req, ServletResponse res)
throws IOException {
res.getWriter().println("<h1>Hello from raw Servlet!</h1>");
}
// 其他方法省略...
}
方式二:继承GenericServlet(推荐基础方式)
java复制public class BetterServlet extends GenericServlet {
@Override
public void service(ServletRequest req, ServletResponse res)
throws IOException {
res.setContentType("text/html");
PrintWriter out = res.getWriter();
out.println("<html><body>");
out.println("<h2>时间戳:" + System.currentTimeMillis() + "</h2>");
out.println("</body></html>");
}
}
方式三:继承HttpServlet(最常用)
java复制@WebServlet("/hello") // 注解方式配置URL映射
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.getWriter().print("GET请求处理");
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String name = req.getParameter("name");
resp.getWriter().print("你好," + name);
}
}
3.2 生命周期深度图解
Servlet的生命周期就像一个人的职场生涯:
- 出生阶段:容器调用
init(),仅执行一次 - 工作阶段:每次请求触发
service()(或doGet/doPost) - 退休阶段:容器调用
destroy()释放资源
mermaid复制sequenceDiagram
participant Client
participant Container
participant Servlet
Client->>Container: HTTP Request
Container->>Servlet: 首次访问?
alt 首次访问
Servlet-->>Container: 调用init()
end
Container->>Servlet: 调用service()
Servlet-->>Container: 生成Response
Container->>Client: HTTP Response
loop 后续请求
Client->>Container: 更多请求
Container->>Servlet: 调用service()
Servlet-->>Container: 生成Response
Container->>Client: HTTP Response
end
Container->>Servlet: 容器关闭时调用destroy()
实战经验:避免在Servlet中定义实例变量,因为所有线程共享同一个Servlet实例。我曾因在Servlet中定义
SimpleDateFormat导致日期错乱,改用ThreadLocal解决。
4. 部署与调试:让Servlet跑起来
4.1 传统web.xml配置方式
虽然注解方式更简便,但理解传统配置有助于排查问题:
xml复制<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
version="3.1">
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
4.2 现代打包与部署
Maven打包命令:
bash复制mvn clean package
生成的WAR文件结构:
code复制hello.war
├── META-INF
│ └── MANIFEST.MF
└── WEB-INF
├── classes
│ └── com/example/HelloServlet.class
├── lib
│ └── dependency.jar
└── web.xml
部署到Tomcat的三种方式:
- 直接复制WAR到
webapps目录 - 使用Manager应用上传
- 配置Context文件(生产环境推荐)
xml复制<!-- conf/Catalina/localhost/hello.xml -->
<Context docBase="/path/to/hello.war" />
4.3 常见部署问题排查
问题一:ClassNotFoundException
- 检查WEB-INF/classes目录结构
- 确认依赖库在WEB-INF/lib中
问题二:404 Not Found
bash复制# 查看Tomcat日志
tail -f logs/catalina.out
- 确认URL模式匹配
- 检查web.xml或注解配置
问题三:乱码
java复制// 在Servlet开头统一设置
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
5. 进阶实战:Servlet中的核心技巧
5.1 请求参数处理大全
获取表单数据:
java复制// 单个参数
String username = req.getParameter("user");
// 多值参数(如复选框)
String[] hobbies = req.getParameterValues("hobby");
// 获取所有参数名
Enumeration<String> params = req.getParameterNames();
文件上传:
html复制<!-- 表单必须设置 -->
<form method="post" enctype="multipart/form-data">
<input type="file" name="file1">
</form>
java复制@WebServlet("/upload")
@MultipartConfig // 必须添加此注解
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Part filePart = req.getPart("file1");
filePart.write("/path/to/save/" +
Paths.get(filePart.getSubmittedFileName()).getFileName());
}
}
5.2 会话管理实战
Cookie基础操作:
java复制// 创建Cookie
Cookie userCookie = new Cookie("user", "john");
userCookie.setMaxAge(3600); // 1小时有效期
resp.addCookie(userCookie);
// 读取Cookie
Cookie[] cookies = req.getCookies();
Arrays.stream(cookies)
.filter(c -> "user".equals(c.getName()))
.findFirst()
.ifPresent(c -> System.out.println(c.getValue()));
HttpSession典型应用:
java复制// 登录时设置session
HttpSession session = req.getSession();
session.setAttribute("user", authenticatedUser);
// 其他页面获取
User user = (User) req.getSession().getAttribute("user");
if(user == null) {
resp.sendRedirect("/login");
}
性能提示:默认Tomcat会话存储在内存中,集群环境需配置持久化。我曾遇到会话丢失问题,最终采用Redis存储方案解决。
6. 生产环境优化指南
6.1 线程安全实践
危险代码示例:
java复制public class UnsafeServlet extends HttpServlet {
private int count = 0; // 实例变量,所有线程共享
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
count++;
resp.getWriter().print("你是第" + count + "位访客");
}
}
修正方案:
java复制private final AtomicInteger count = new AtomicInteger(0);
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
int current = count.incrementAndGet();
resp.getWriter().print("你是第" + current + "位访客");
}
6.2 性能调优参数
Tomcat配置建议(server.xml):
xml复制<Connector port="8080" protocol="HTTP/1.1"
maxThreads="200" # 最大工作线程数
minSpareThreads="10" # 最小空闲线程
acceptCount="100" # 等待队列长度
connectionTimeout="20000"
enableLookups="false" # 禁用DNS查询
URIEncoding="UTF-8"/>
Servlet初始化参数优化:
java复制@WebServlet(
value = "/heavy",
initParams = {
@WebInitParam(name = "cacheSize", value = "1000"),
@WebInitParam(name = "timeout", value = "5000")
},
loadOnStartup = 1 // 容器启动时立即初始化
)
public class HeavyServlet extends HttpServlet {
private int cacheSize;
public void init() {
this.cacheSize = Integer.parseInt(
getInitParameter("cacheSize"));
}
}
7. 从Servlet到现代Web开发
虽然现在流行Spring Boot等框架,但理解Servlet能让你:
- 深度排查框架底层问题
- 编写高性能过滤器(如认证、日志)
- 自定义特殊需求处理器
典型过滤链配置:
java复制@WebFilter("/*")
public class TimingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
long start = System.currentTimeMillis();
chain.doFilter(req, resp); // 继续执行过滤链
long duration = System.currentTimeMillis() - start;
System.out.println("请求处理耗时:" + duration + "ms");
}
}
在微服务时代,Servlet仍然以新的形式存在。比如Spring MVC的DispatcherServlet本质就是个高级Servlet,而JAX-RS(如Jersey)也是基于Servlet规范实现的。
我最近处理的一个性能问题:某接口响应慢,最终发现是框架封装的Servlet没有正确配置asyncSupported=true,导致阻塞了线程池。这再次证明,框架用得再熟,也不如懂底层原理来得踏实。
