☰
Java Servlet从入门到进阶:生命周期、会话追踪与Filter实战详解
2026/10/14 4:56:05 网站建设 项目流程

Java Web开发绕不开一个坎,那就是Servlet。哪怕现在Spring Boot铺天盖地,看起来一个注解就能搞定接口,但底层的请求分发、生命周期管理、会话跟踪,根基都扎在Servlet规范里。市面上讲Servlet的教程不少,但多数要么浅尝辄止,要么直接堆代码不讲原理,看完照样不知道怎么排查问题。这篇文章我把Servlet从基础到进阶的核心路径捋一遍,结合实战场景聊聊哪些细节值得深挖,哪些坑踩过就不用再踩了。

1. Servlet的定位:它到底是干什么的

1.1 一个请求的完整旅程

先想一个问题:当你在浏览器地址栏输入一个网址并按下回车,服务器上发生了什么?

抛开网络层的三次握手、TCP连接这些不谈,在Java Web服务器里,请求到达后会经过一套固定的流程:HTTP报文被解析,请求头、参数、Cookie等信息被包装成对象,服务器根据URL匹配到对应的处理逻辑,执行完业务代码后把结果以HTML、JSON或其他格式写回响应。这套流程中,处理请求的那段Java程序,就是Servlet。

有人把Servlet比作餐厅服务员,这个类比挺贴切。顾客(浏览器)下单(发请求),服务员(Servlet)负责接收订单、传给后厨(业务逻辑)、再把做好的菜(响应)端回去。但和普通服务员不同的是,Servlet是常驻内存的,不需要每次请求都重新创建,这也是它能在高并发下保持性能的关键前提。

1.2 Servlet规范解决的核心问题

Servlet本质上是一套接口规范,定义了两个程序之间的交互规则:请求方如何把数据传进来,处理方如何把结果送回去。规范里对生命周期、方法签名、异常处理、并发模型都做了明确约定,这样无论你用的是哪种Web服务器,写出来的Servlet代码都能迁移。

这就像插座标准一样。你买的电器不用关心发电厂怎么发电,只要插头符合标准就能用。Servlet就是Java Web世界里的标准插座,而Tomcat、Jetty这类容器,就是提供插座功能的发电厂。

2. 开发环境准备与第一个Servlet程序

2.1 环境搭建的版本选择

这里直接说结论:JDK用8及以上,Tomcat用9或10,IDE随意(我用的是免费版)。如果你还在用Tomcat 7或者更早的版本,建议尽早升级,因为老版本的HTTP/2支持和安全补丁都有缺失,生产环境迟早要踩雷。

用Maven管理依赖时,最核心的依赖就两个:

<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>

注意这里的scope必须是provided,因为Servlet API由Web容器提供,你打包进WAR里反而可能版本冲突。这个坑我不止一次见到有人踩——把servlet-api.jar打进lib目录,结果部署到服务器上报出LinkageError。

2.2 第一个Servlet的两种写法

写一个最简单的Servlet,输出"Hello Servlet",有两条路可以选择:一是写配置文件web.xml,二是用注解@WebServlet。

走配置文件的方式:

// HelloServlet.java public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h1>Hello Servlet</h1>"); } }
<!-- web.xml --> <servlet> <servlet-name>hello</servlet-name> <servlet-class>com.demo.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>hello</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping>

走注解的方式更简洁:

@WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h1>Hello Servlet</h1>"); } }

两种方式效果一样,但注解写法省了配置文件,直观且不容易漏配,现在项目基本都走这条路。但理解配置文件的做法仍然有价值——当你要兼容老项目或者需要动态注册ServletFilter时,配置文件的方式依然有用武之地。

部署的时候,直接把项目打成WAR包丢到Tomcat的webapps目录下,启动服务器后访问http://localhost:8080/项目名/hello就能看到效果。

3. Servlet生命周期与线程模型:搞懂底层才能游刃有余

3.1 五个阶段的完整闭环

Servlet从创建到销毁,总共经历五个阶段:加载、实例化、初始化、服务、销毁。前两个阶段由容器控制,后三个是你写代码的地方。

初始化对应init方法,只执行一次。这个方法适合做资源初始化——打开数据库连接池、加载配置文件、启动后台线程。我在实际项目中见过有人把初始化逻辑写在构造方法里,这在Servlet里不推荐,因为容器创建实例后不一定马上调用init,时序上不可控。

服务阶段对应service方法。HttpServlet已经在父类里帮我们实现了service方法,它会根据请求的HTTP方法自动调用doGet、doPost、doPut等方法。所以通常不需要重写service,只需要按需实现对应的doXxx方法就行。

销毁阶段对应destroy方法,同样是只执行一次。容器卸载Web应用或服务器关闭时触发,适合释放资源、保存状态。

3.2 单实例多线程模型与线程安全问题

Servlet默认是单实例的,容器只会创建一个Servlet实例来服务所有请求。这意味着每来一个请求,容器就创建一个新线程,多个线程共享同一个Servlet实例的方法。

这个模型带来一个关键问题:Servlet类的成员变量是线程不安全的。如果写上:

public class UnsafeServlet extends HttpServlet { private int count = 0; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { count++; resp.getWriter().write("访问次数" + count); } }

在高并发下,count的自增操作就不是原子的,可能出现丢数据。解决办法有几种:把变量改成局部变量,或者用AtomicInteger,再或者加同步锁。实际开发中,Servlet里最安全的做法就是不写实例变量,所有状态放在局部变量或者request作用域里。

3.3 load-on-startup与预加载策略

默认情况下,Servlet是在首次请求时才被实例化和初始化的,这会导致第一个请求响应很慢——因为要同时完成类加载、实例化、初始化三个步骤。对于核心Servlet,可以用load-on-startup让容器启动时就初始化:

注解方式:

@WebServlet(value = "/init", loadOnStartup = 1)

配置文件方式:

<load-on-startup>1</load-on-startup>

数值越小优先级越高,容器启动时按顺序加载。这个功能对需要预热缓存或预先检测依赖的服务很有价值。我做过一个数据上报接口,启动时初始化一批长连接,如果等请求来了才连,第一个请求的延迟能到十几秒。

4. Servlet核心API:请求、响应与参数的深度解析

4.1 HttpServletRequest获取参数的三种方式

浏览器向服务端传递数据的方式基本有三种:URL参数(GET)、表单体(POST)、路径参数(RESTful风格)。

对应到Servlet里的API分别是:

// 方式一:GET请求的查询字符串 String name = request.getParameter("name"); // 方式二:POST表单字段 String age = request.getParameter("age"); // 方式三:路径参数(需要配合容器配置) // 通过request.getPathInfo()获取路径信息后自己解析 String pathInfo = request.getPathInfo(); // 形如 /user/123

getParameter方法同时兼容GET和POST,容器会把查询字符串和表单里的字段合并到一起。这个设计方便,但有一个隐含陷阱:如果GET和POST传了同名参数,优先取哪个?不同的容器行为不统一,实际开发中不要依赖这种合并行为,参数名设计时尽量避免冲突。

获取多个值的参数要用getParameterValues:

String[] hobbies = request.getParameterValues("hobby");

这个方法在获取复选框、多选下拉框时必不可少。如果参数不存在,getParameter返回null而不是空字符串,用之前务必判空。

4.2 中文乱码问题的根因与解决方案

中文乱码是Servlet新手最常见的头疼问题。乱码的根源是编码和解码使用了不同的字符集,比如浏览器用UTF-8发送,服务器却用ISO-8859-1去解码。

分场景给出解决方案:

POST请求的乱码:在读取任何参数之前设置请求编码:

request.setCharacterEncoding("UTF-8");

这段代码必须在第一次getParameter之前调用,否则容器已经按默认编码解析完参数,再设置就来不及了。Servlet 4.0规范里,这个方法的调用时机对GET和POST是一样的——都要在读取之前。

GET请求的乱码:Tomcat 8.0以上版本默认对URL解码使用UTF-8,所以大多数情况下不做处理也没事。如果是Tomcat 7或更老版本,需要在server.xml里配置:

<Connector port="8080" protocol="HTTP/1.1" URIEncoding="UTF-8" />

响应乱码:给响应设置正确的类型和编码:

resp.setContentType("text/html;charset=UTF-8");

有一个细节值得注意:setContentType必须写在getWriter()之前,否则不会生效。实际经验是,写响应之前,最好先设置ContentType,再写数据。

4.3 请求转发与重定向的区别

请求转发(forward)和重定向(sendRedirect)是Servlet中最容易混淆的两个操作,它们的效果完全不同:

请求转发:

// 服务端内部跳转,地址栏不变,属于同一个请求 RequestDispatcher dispatcher = request.getRequestDispatcher("/target"); dispatcher.forward(request, response);

重定向:

// 浏览器地址栏变化,产生第二次请求 response.sendRedirect("target");

核心区别包括:转发是服务器内部的行为,整个过程只发一次请求;重定向是服务器告知浏览器"你去别处访问",会发两次请求。转发可以携带request中的数据到目标页面,重定向做不到(因为第二次请求是全新的)。转发无法跨应用进行,重定向可以通过全路径跳转到任何网站。

什么时候用哪个?如果是一次表单提交后需要展示结果数据,用转发;如果提交完需要防重复提交或者跳转到别的系统,用重定向。网上商城下完单跳去支付平台,就是重定向的典型场景。

4.4 响应状态码与JSON输出的最佳实践

现代接口开发中,Servlet更多的是输出JSON而不是HTML。手写JSON拼接容易出错,建议引入Fastjson或Gson库:

@WebServlet("/api/user") public class UserApiServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Map<String, Object> result = new HashMap<>(); result.put("code", 200); result.put("message", "success"); result.put("data", Collections.singletonMap("name", "张三")); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(JSON.toJSONString(result)); } }

设置响应状态码用:

resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400

注意:一旦调用了sendError,之前写入响应的内容可能被清空,所以应该在写内容之前设置状态码。

5. 会话追踪的三种武器:Cookie、URL重写和HttpSession

5.1 为什么需要会话保持

HTTP协议是无状态的,服务器处理完一个请求后,不会记得这个请求是哪个客户端发出的。但如果要做登录、购物车、浏览记录这些功能,必须记住访问者身份。

解决办法就是会话追踪,本质上是选择一个唯一标识,让客户端在后续请求中携带这个标识,服务器根据标识找到对应的会话数据。

5.2 Cookie机制与操作细节

Cookie是服务器生成、存在浏览器端的键值对数据。Servlet中操作Cookie很直接:

// 创建Cookie Cookie cookie = new Cookie("username", "admin"); cookie.setMaxAge(60 * 60 * 24 * 7); // 有效期7天,单位是秒 cookie.setPath("/"); // 设置路径 resp.addCookie(cookie); // 读取Cookie Cookie[] cookies = req.getCookies(); if (cookies != null) { for (Cookie c : cookies) { if ("username".equals(c.getName())) { String value = c.getValue(); } } }

几个容易忽略的点:setMaxAge参数如果是负数,Cookie不会被持久化,浏览器关闭就消失,这通常叫会话级Cookie;如果设为0,表示立即删除该Cookie,这个招数常用来让客户端主动清理已无效的身份信息。setPath如果不能细心规划,会出现路径不匹配导致Cookie无法携带的问题。

5.3 HttpSession与超时管理

HttpSession是Servlet规范里的服务器端会话机制。第一次调用request.getSession()时创建会话,并返回一个包含会话ID的Cookie(默认叫JSESSIONID):

HttpSession session = request.getSession(); session.setAttribute("user", userInfo); session.setMaxInactiveInterval(1800); // 30秒乘60,但这里给出的其实是30*60=1800秒 // 读取 User user = (User) session.getAttribute("user"); // 销毁 session.invalidate();

会话ID默认通过Cookie传递。但如果浏览器禁用了Cookie,可以启用URL重写,Servlet容器会自动把会话ID追加到URL后面,但这里有一个根本问题:URL重写只对本应用内使用这个会话的链接生效,外部进来的新URL不会自动带上ID。

实践中,HttpSession的超时时间可以在web.xml里全局配置:

<session-config> <session-timeout>30</session-timeout> </session-config>

单位是分钟。有一个问题:很多人调整了Tomcat的会话超时配置,却忘了一致性要求——容器层配置和应用层配置要同步,否则不同环境表现不一致。

5.4 三种会话追踪方式的适用场景

Cookie适合跨请求携带少量非敏感数据,比如语言偏好、主题皮肤。HttpSession适合保存用户登录状态和购物车数据这些服务端数据。URL重写很少单独用,一般作为Cookie不可用时的降级方案。

一个安全实践的提醒:不要在URL里直接传会话ID。原因很简单,URL可能会被浏览器历史记录、Referer头、日志系统记录下来,会话ID一旦泄露,等于把用户身份拱手让人。

6. Filter与Listener:横切逻辑的高效实现

6.1 Filter实现统一字符编码、登录校验与敏感词过滤

过滤器(Filter)是Servlet规范中另一个重要成员,它的特点是能在请求到达Servlet之前拦截请求,在响应返回客户端之前处理响应。这非常契合日志记录、权限校验、编码设置等横切逻辑。

写一个过滤器的步骤很简单:

@WebFilter("/*") public class CharsetFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { // 读取初始化参数 } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); // 继续传递请求 chain.doFilter(request, response); } @Override public void destroy() { } }

Filter的执行顺序是:多个Filter通过@WebFilter注解配置时,执行顺序不一定可预期,精确控制顺序要在web.xml里配置filter-mapping。这个细节坑过不少人——写了多个过滤注解,结果执行顺序跟预想不一致,排查半天才发现是容器加载顺序的问题。

Filter在登录校验场景下的作用很经典:

@WebFilter(urlPatterns = {"/user/*", "/admin/*"}) public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpSession session = req.getSession(false); if (session == null || session.getAttribute("user") == null) { ((HttpServletResponse) response).sendRedirect("/login"); return; } chain.doFilter(request, response); } }

在doFilter里判断用户是否已登录,如果未登录就重定向到登录页并return,不再向后传递。注意getSession(false)的使用——传false的意思是:如果会话不存在,别帮我新创建一个。之前有人用getSession()写登录校验,结果未登录的用户访问时被强制创建了会话,白白浪费服务器内存。

6.2 Listener监听生命周期与应用场景

监听器(Listener)用于监听Web应用中的事件,比如应用启动、停止、会话创建、属性变化等。常用的三类:

ServletContextListener:监听应用启动和停止,适合在启动时做全局初始化,停止时做资源释放。

ServletRequestListener:监听每个请求的创建和销毁,可以统计并发数。

HttpSessionListener:监听会话创建和销毁,可以统计在线人数。

写在线人数统计就用HttpSessionListener:

@WebListener public class OnlineCountListener implements HttpSessionListener { private static int onlineCount = 0; @Override public void sessionCreated(HttpSessionEvent se) { onlineCount++; } @Override public void sessionDestroyed(HttpSessionEvent se) { if (onlineCount > 0) { onlineCount--; } } public static int getOnlineCount() { return onlineCount; } }

监听器设计的好处是解耦:统计逻辑、日志逻辑和应用业务逻辑分开,新需求来的时候不需要改业务代码,加个监听器就行。

7. 实战案例:Servlet + JSP + JDBC开发用户注册登录模块

7.1 项目结构与数据库设计

理论讲了这么多,还是要落到一个真实项目里看看Servlet整条链路怎么配合。这里我从0到1复盘一个用户注册登录模块,麻雀虽小五脏俱全。

数据库先建一张用户表:

CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

项目结构按职责分层:

src/main/java ├── com/demo/filter (登录校验过滤器) ├── com/demo/dao (数据访问层) ├── com/demo/entity (实体类) ├── com/demo/servlet (控制层) └── com/demo/util (工具类)

src/main/webapp ├── JSP页面 ├── WEB-INF └── web.xml

7.2 注册流程完整代码

实体类User:

public class User { private int id; private String username; private String password; private String email; // 省略getter/setter }

DAO层用JDBC操作数据库:

public class UserDao { private static final String URL = "jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USERNAME = "root"; private static final String PASSWORD = "yourpassword"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public boolean findByUsername(String username) throws SQLException { String sql = "SELECT COUNT(*) FROM user WHERE username = ?"; try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ResultSet rs = ps.executeQuery(); rs.next(); return rs.getInt(1) > 0; } } public void insertUser(User user) throws SQLException { String sql = "INSERT INTO user (username, password, email) VALUES (?, ?, ?)"; try (Connection conn = DriverManager.getConnection(URL, USERNAME, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); ps.executeUpdate(); } } }

注册Servlet:

@WebServlet("/register") public class RegisterServlet extends HttpServlet { private final UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); String email = req.getParameter("email"); // 基础校验 if (username == null || username.isEmpty() || password == null || password.isEmpty()) { req.setAttribute("errorMsg", "用户名和密码不能为空"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } try { if (userDao.findByUsername(username)) { req.setAttribute("errorMsg", "用户名已存在"); req.getRequestDispatcher("/register.jsp").forward(req, resp); return; } User user = new User(); user.setUsername(username); user.setPassword(password); user.setEmail(email); userDao.insertUser(user); // 注册成功,重定向到登录页防止重复提交 resp.sendRedirect("login.jsp?registered=true"); } catch (SQLException e) { e.printStackTrace(); req.setAttribute("errorMsg", "系统异常,请稍后再试"); req.getRequestDispatcher("/register.jsp").forward(req, resp); } } }

登录Servlet类似,验证通过就往session里塞用户信息:

@WebServlet("/login") public class LoginServlet extends HttpServlet { private final UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); try { User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("user", user); // 登录成功跳首页 resp.sendRedirect("index.jsp"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } catch (SQLException e) { e.printStackTrace(); req.setAttribute("errorMsg", "系统异常"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

7.3 这个项目里容易踩的坑

连接池问题:上面代码直接用DriverManager获取连接,演示没问题,但真实项目绝不能这么写。每次请求都创建物理连接,高并发下数据库端口瞬间被耗尽。生产环境一定要用HikariCP或Druid连接池。

SQL注入问题:代码全程用了PreparedStatement,这是底线。如果用字符串拼接SQL,一旦用户名里出现单引号,轻则查询报错,重则被脱库。这个场景下我见过太多教训。

密码不能明文存:上面的代码直接存了明文密码,只演示功能。生产环境至少要用BCrypt加盐哈希,这是最基本的安全底线。

事务管理:注册时如果同时插入多张表,就要考虑事务。上面代码只有一张表,但如果后续加积分表、流水表,就要保证一致性。原生JDBC用conn.setAutoCommit(false) + commit/rollback实现,也可以用框架的声明式事务。

8. 常见问题排查实录与调试技巧

8.1 8080端口被占用怎么办

Tomcat启动时报端口被占用是家常便饭。Windows下用命令找占用进程:

netstat -ano | findstr 8080

然后用任务管理器结束对应PID的进程,或者换端口。Linux下:

lsof -i:8080 kill -9 PID

8.2 404和405错误的最全解读

404表示找不到资源。排查顺序是:URL拼接是否正确(注意项目名)、@WebServlet注解里的路径是否写了斜杠、Servlet类是否被容器扫描到(包结构必须在配置的扫描范围内)。

405表示请求方法不被支持。最常见的原因:写了doGet没写doPost,从前端用POST请求过来就报405。修改办法很简单:在Servlet里主动调一下父类方法,或者干脆只实现service方法。

8.3 启动时ClassNotFoundException和NoClassDefFoundError

ClassNotFoundException通常是缺少依赖jar包。检查WEB-INF/lib目录下有没有对应的包。

NoClassDefFoundError则更麻烦,它出现在编译链接成功但运行时类加载失败——往往是因为静态初始化抛了异常,类加载失败后再次使用就报这个错。

8.4 调试Servlet的实用技巧

没有IDE远程调试时,最朴素的调试工具就是日志。别用System.out.println,因为线上环境输出混杂且没有级别控制。引入slf4j + logback,按级别记录日志:

private static final Logger logger = LoggerFactory.getLogger(LoginServlet.class); logger.info("用户登录,username={}", username); logger.error("登录失败,用户或密码错误", e);

更好的方式是配置日志文件按天滚动、按大小分割,排查历史问题时能按时间点快速定位。

还有一种实用手段:在Filter里统一打印请求路径、方法、耗时。如果你怀疑某个请求慢,往日志里一看就知道卡在哪个环节:

long start = System.currentTimeMillis(); chain.doFilter(request, response); long end = System.currentTimeMillis(); logger.info("{} {} 耗时 {}ms", req.getMethod(), req.getRequestURI(), end - start);

8.5 内存溢出与类加载问题的预防

Tomcat热部署时老版本jar残留、多个Web应用类冲突,都可能引发内存溢出或类加载问题。预防手段就几点:定期清理work目录、避免在静态变量里持有大对象、Web应用解绑classloader的问题要重视(比如在静态变量里保存Context对象)。

9. Servlet的扩展方向:往深水区走的路

Servlet学习到这里算入门了,但远没到精通。要继续往深入的层次走,方向有四个:一是了解Servlet容器内部实现,比如Tomcat如何处理NIO请求、线程池管理和连接器设计;二是研究Servlet 4.0/6.0的新特性,比如HTTP/2支持、异步Servlet的完整机制;三是结合Spring MVC看它如何封装Servlet——DispatcherServlet本质就是一个核心Servlet,拦截所有请求再做路由分发;四是研究前后端分离模式下Servlet如何输出JSON、上传文件、处理CORS跨域。

异步Servlet值得一提:

@WebServlet(urlPatterns = "/async", asyncSupported = true) public class AsyncServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { AsyncContext asyncContext = req.startAsync(); // 把耗时任务丢到新线程里执行 new Thread(() -> { try { String result = longTimeTask(); resp.getWriter().write(result); } catch (IOException e) { e.printStackTrace(); } finally { asyncContext.complete(); } }).start(); } }

这个模式解决了长任务占用容器线程的问题,避免线程池被耗尽。但异步Servlet的复杂度明显更高,要注意线程安全、超时设置、异常处理,新手现阶段了解即可,等到真正遇到线程池耗尽的问题再来深度研究。

回顾我对Servlet学习的整个路径,最深的体会是:不要只停留在会配置会用注解,知道what还不够,一定要往下钻一层了解why。为什么默认是单实例多线程、为什么Filter顺序不可控、为什么重定向和转发场景不同——这些问题的答案,才是在真实项目里跟别人拉开差距的地方。Servlet这个东西,看起来老,但它是所有Java Web框架的地基。把地基打牢了,后面学什么框架都快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询