1. 从 JavaWeb 到 Servlet先搞懂它到底在解决什么问题做了这么多年 Java 后端我见过太多初学者一上来就抱着 Spring Boot 啃结果连一个最基础的请求是怎么被处理、响应是怎么返回的都没搞明白。等到面试被问到 Servlet 生命周期或者项目里遇到一个奇怪的 404整个人就懵了。所以这篇我不打算讲什么高大上的架构演进就老老实实把 JavaWeb 最底层的 Servlet 讲透顺带带你手写一个完整的项目案例配 MySQL 那种。先回答一个很多人都问过我的问题JavaWeb 到底是什么说的直白一点就是“用 Java 技术开发的 Web 应用”也就是浏览器访问后台服务器那套东西。而 Servlet 就是这套东西里最核心的“请求处理器”。你可以把 Servlet 理解为饭店里的服务员——客人浏览器点菜发请求服务员Servlet把菜单传给后厨业务逻辑代码再把做好的菜响应数据端回给客人。没有服务员客人就只能干瞪眼。那为什么现在大家天天说 Spring Boot还要学 Servlet因为 Spring Boot 的底层就是 Servlet。Spring MVC 的核心 DispatcherServlet 本身就是一个 Servlet只不过它在 Servlet 的基础上包了一层又一层让你不用直接面对那些繁复的 API。你把 Servlet 学会了后面看 Spring MVC 的源码就跟看自家后院一样特别通透。这篇文章适合谁看不管你是在校学生准备 Java 方向的实习面试还是自学 JavaWeb 想做一个完整的课设项目亦或是已经写了几年 CRUD 但从来没认真捋过 Servlet 生命周期的老开发我建议你都花十分钟读一遍。我会把 Servlet 生命周期、IDEA 里运行 JavaWeb 项目的配置、以及一个带 MySQL 的完整案例都串起来讲保证你能直接上手复制。2. Servlet 生命周期为什么面试官总爱问这个2.1 生命周期的三个关键节点Servlet 生命周期是 JavaWeb 面试里出场率最高的题也是理解整个 Servlet 运行机制的钥匙。它的生命周期非常清晰就三个阶段init、service、destroy外加一个实例化过程。很多背八股文的人能一字不差地背出来但你要问他“为什么 Tomcat 默认只创建一次 Servlet 实例”他就答不上来了。先看一个最简单的 Servlet 代码我在项目里最常用的写法package com.example.demo.servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class HelloServlet extends HttpServlet { Override public void init(ServletConfig config) throws ServletException { super.init(config); System.out.println(1. Servlet 实例创建执行 init()); } Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(3. 处理请求执行 service()); resp.setContentType(text/html;charsetUTF-8); resp.getWriter().write(h1Hello JavaWeb/h1); } Override public void destroy() { System.out.println(4. 容器关闭执行 destroy()); super.destroy(); } }这段代码演示的就是完整生命周期。那整个流程到底是怎么走的我用一个实际请求拆给你看第一步加载和实例化。Tomcat 容器启动的时候会根据配置文件web.xml 或注解找到这个 Servlet 类通过反射调用newInstance()创建对象。这里有一个很多人忽略的细节默认情况下Tomcat 是在第一次收到请求时才创建实例而不是服务器启动时就创建。什么叫“默认情况下”就是 web.xml 里没有配置load-on-startup标签时。如果你希望服务器一启动就创建可以在 web.xml 里加上load-on-startup1/load-on-startup那个数字越小启动优先级越高。第二步初始化。实例创建完后容器立刻调用init()方法并传入一个ServletConfig对象。init()在整个生命周期里只执行一次所以适合做数据库连接池的初始化、读取配置文件参数这类“一次性准备”的工作。注意ServletConfig里能拿到初始化参数比如你可以在 web.xml 里配置servlet servlet-namehelloServlet/servlet-name servlet-classcom.example.demo.servlet.HelloServlet/servlet-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /servlet然后在init()里用config.getInitParameter(encoding)取出来。第三步处理请求。每来一个请求容器就会调用service()方法。HttpServlet 的service()方法内部会根据请求方式GET、POST、PUT、DELETE 等自动分发到doGet()或doPost()等方法。这一步是整个生命周期里执行频率最高的也是并发环境下你需要重点关注的。第四步销毁。当 Tomcat 容器关闭或者检测到该 Servlet 需要卸载时容器会调用destroy()方法释放资源。同样只执行一次适合关闭数据库连接池、清理线程池等收尾工作。2.2 单实例多线程Servlet 的并发模型关于生命周期有一个更深层的机制你必须理解——Servlet 是多线程的。这是面试官最爱追问的点。Tomcat 默认采用单实例多线程模型整个应用里一个 Servlet 就只有一个实例但会有多个线程同时调用这个实例的service()。这意味着什么意味着如果你在 Servlet 里定义了成员变量比如public class UnsafeServlet extends HttpServlet { private int count 0; Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { count; System.out.println(当前 count 值 count); } }那么在高并发下count这行代码就存在线程安全问题。多个线程同时读到 count1同时加一最后可能还是 2。这样的 bug 在测试环境根本复现不出来一上线就冒烟。我自己的习惯是Servlet 里只定义局部变量不定义可变的成员变量。如果实在需要共享状态就加锁或者用原子类。这是新手最容易踩的坑也最能体现你有没有真正理解生命周期背后的含义。顺便说一句service()方法里是可以拿到请求相关信息的比如Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method req.getMethod(); String uri req.getRequestURI(); String param req.getParameter(name); }HttpServletRequest封装了请求行的所有信息HttpServletResponse则负责输出响应。这两个对象是容器每次请求时新建的线程之间互不干扰可以放心使用。3. 手把手做一个 Servlet MySQL 的完整案例3.1 项目结构和数据库设计光讲理论没意思我直接带你写一个能跑起来的完整案例。功能很简单做一个用户列表页面数据库里存几张用户表前端展示所有用户信息通过 Servlet 访问数据库并返回数据。先说技术选型Servlet 4.0 Tomcat 9 MySQL 8.0 IDEA 2023 Maven。实际开发中我用的是这些版本你只要保证版本兼容就不会出幺蛾子。Tomcat 9 对应 Servlet 4.0Tomcat 10 对应 Servlet 5.0 但包名变成了jakarta.servlet这里注意一下别混用。数据库表结构我偷个懒设计一个非常简单的用户表CREATE DATABASE IF NOT EXISTS javaweb_demo DEFAULT CHARACTER SET utf8mb4; USE javaweb_demo; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO t_user (username, password, email) VALUES (zhangsan, 123456, zhangsantest.com), (lisi, 123456, lisitest.com), (wangwu, 123456, wangwutest.com);这里有个细节我要特意强调字符集一定要选 utf8mb4不是 utf8。utf8 在 MySQL 里最多只支持 3 个字节遇到 emoji 表情或者某些生僻字就会报错。utf8mb4 是 utf8 的超集兼容性更好。我在项目里踩过这个坑数据库建好之后一堆中文乱码排查了半天才发现是建表语句的问题。项目和表结构理清之后接下来搭建 IDEA 的 JavaWeb 项目配置。这一步是很多初学者的拦路虎我单独拿出来讲。3.2 IDEA 创建 JavaWeb 项目并配置 Tomcat现在用 IDEA 新建一个 JavaWeb 项目。网上很多老教程还在教你怎么建普通的 Java 项目然后手动加 Web 支持太麻烦了。我推荐直接用 Maven 骨架步骤很简单IDEA 选择 File - New - Project左侧选择 Maven。勾选 Create from archetype然后选择org.apache.maven.archetypes:maven-archetype-webapp这个骨架。GroupId 填com.exampleArtifactId 填servlet-demoVersion 默认即可。项目创建完成后IDEA 会自动生成标准的 Maven Web 目录结构src/main/java存放 Java 源码src/main/resources存放配置资源文件src/main/webapp存放 JSP、HTML、静态资源src/main/webapp/WEB-INF/web.xmlWeb 部署描述文件注意这里有一个 IDEA 的经典坑部分版本的 IDEA 创建出来的 webapp 目录下没有WEB-INF/web.xml文件。没有web.xml并不会影响项目启动因为 Servlet 3.0 之后支持注解开发但如果你是新手强烈建议手动补上这个文件原因有两点一是很多老教程和面试题还是基于 web.xml 的配置方式二是当你要配置load-on-startup、初始化参数、欢迎页面时web.xml 比注解要直观得多。补上 web.xml 后内容长这样?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-nameservlet-demo/display-name welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app接着配置 Tomcat。在 IDEA 右上角点击 Add Configuration选择 Tomcat Server - Local。Name 随意填然后配置 Application Server选择你本地解压好的 Tomcat 目录。这一步只需要做一次以后项目都能复用。配置好之后再设置 Deployment。点击 Deployment 页签里的加号选择 Artifact — 选servlet-demo:war exploded。这里我强烈推荐用war exploded而不是 war 包因为 exploded 模式是解压后的目录IDEA 和 Tomcat 之间不需要打包解压的过程修改代码后热部署速度快很多开发调试效率倍增。最后检查 Server 页签的 URL 设置默认可能是http://localhost:8080/servlet-demo/没问题改不改都行。至此环境就配好了。点击右上角的绿色小虫子图标Debug 模式启动 Tomcat启动成功后浏览器访问http://localhost:8080/servlet-demo/看到 Hello World 页面就代表环境没问题。3.3 编写 JDBC 工具类和用户查询 Servlet环境通了之后我们开始写代码。第一步是加 MySQL 驱动的 Maven 依赖。在pom.xml里加入dependencies dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies注意javax.servlet-api的 scope 一定要是provided意思是编译时需要但打包时不需要打进 WAR 包因为 Tomcat 容器本身就带有 Servlet API打进去反而会冲突。我见过有人把这个依赖打成默认的 compile scope结果部署到服务器时出现NoClassDefFoundError排查了半天最后发现是依赖冲突。然后是 JDBC 工具类专门负责获取数据库连接。我用的是最传统的 DriverManager 方式package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/javaweb_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 你的数据库密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这几个参数我展开解释一下。serverTimezoneAsia/Shanghai是 MySQL 8.0 的硬性要求不写的话会直接报时区错。useSSLfalse是因为本地开发根本没有配置 SSL 证书不开反而更省事。useUnicodetruecharacterEncodingutf8是保证数据库读写中文不乱码的关键字面意思是连接时使用 Unicode 字符集编码格式为 UTF-8。写完工具类接着写一个标准的用户实体类package com.example.entity; import java.util.Date; public class User { private Integer id; private String username; private String password; private String email; private Date createTime; // 无参构造 有参构造 public User() {} public User(Integer id, String username, String password, String email, Date createTime) { this.id id; this.username username; this.password password; this.email email; this.createTime createTime; } // getter 和 setter 方法这里省略 // 每个字段对应一组 getXxx() / setXxx() 方法 }实体类就是个 POJO对应 t_user 表的一条记录。虽然代码啰嗦但这是 JavaWeb 最标准的写法。后面你用 MyBatis、Hibernate 的时候实体类的概念也一模一样。然后写核心的 DAO 层负责查询数据库package com.example.dao; import com.example.entity.User; import com.example.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class UserDao { public ListUser findAll() { ListUser userList new ArrayList(); String sql SELECT id, username, password, email, create_time FROM t_user; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setEmail(rs.getString(email)); user.setCreateTime(rs.getTimestamp(create_time)); userList.add(user); } } catch (SQLException e) { e.printStackTrace(); } return userList; } }这个findAll()方法我用了 JDK 7 的 try-with-resources 语法Connection、PreparedStatement、ResultSet 会自动关闭不用手动写 finally。我强烈建议你从一开始就养成这个习惯因为数据库连接是宝贵的资源忘记关闭在高并发下分分钟把连接池打满整个应用直接卡死。最后写我们的核心 Servlet——UserListServletpackage com.example.servlet; import com.example.dao.UserDao; import com.example.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; WebServlet(/user/list) public class UserListServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); ListUser userList userDao.findAll(); StringBuilder html new StringBuilder(); html.append(!DOCTYPE html); html.append(html langzh-CN); html.append(headmeta charsetUTF-8title用户列表/title/head); html.append(body); html.append(h1JavaWeb 用户列表/h1); html.append(table border1 cellpadding8 cellspacing0); html.append(trthID/thth用户名/thth邮箱/thth创建时间/th/tr); for (User user : userList) { html.append(tr); html.append(td).append(user.getId()).append(/td); html.append(td).append(user.getUsername()).append(/td); html.append(td).append(user.getEmail()).append(/td); html.append(td).append(user.getCreateTime()).append(/td); html.append(/tr); } html.append(/table); html.append(/body/html); resp.getWriter().write(html.toString()); } }注意第九行的WebServlet(/user/list)注解这是 Servlet 3.0 之后的新写法告诉容器这个 Servlet 的访问路径是/user/list不用在 web.xml 里配一堆servlet标签了。访问路径前面那串req.setCharacterEncoding(UTF-8)是处理 POST 请求中文乱码的关键虽然这个案例用不上但我习惯加上以防后面扩展。把这几层写完顺序是 User - UserDao - DBUtil - UserListServletMaven 依赖和 Tomcat 环境都配好后启动项目浏览器输入http://localhost:8080/servlet-demo/user/list就能看到一个表格里面显示着数据库 t_user 表的所有用户。3.4 从访问路径到浏览器显示一条请求的完整航线上面代码跑通了我再用讲故事的方式把整条请求链路串一遍让你脑子里有一张完整的地图。浏览器输入http://localhost:8080/servlet-demo/user/list。请求到达 Tomcat 后Tomcat 的 Connector 组件把 HTTP 请求报文解析成HttpServletRequest对象。根据应用上下文/servlet-demoTomcat 定位到当前部署的这个 Web 应用。根据 URL 里的/user/listTomcat 的映射器在内存中查找对应的 Servlet。它知道这个路径对应UserListServlet因为启动时容器扫描到WebServlet注解已经把映射关系注册好了。判断UserListServlet实例是否存在。不存在就先走生命周期创建并初始化存在就直接复用。调用service()方法因为浏览器发出的请求是 GETservice()内部会调用doGet()。doGet()里通过UserDao.findAll()查询数据库把返回的 List 拼成 HTML 字符串写入HttpServletResponse的 writer。Tomcat 拿这个响应对象按照 HTTP 协议格式组装消息通过 Socket 发回浏览器。浏览器拿到 HTML 源码解析渲染显示出用户列表表格。这一整套链路你捋顺了JavaWeb 的骨架就长在你脑子里了。以后不管换什么框架底层都跳不出这条航线。4. 常见问题与排查技巧实录这部分是压轴干货。我把自己这些年做 JavaWeb 项目踩过的坑、以及学生和同事最常遇到的一堆糟心事全整理出来。每一条都配上排查思路你按图索骥就行。4.1 经典报错速查表报错信息根本原因解决方案ClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL 驱动没有引入或没有打包pom.xml 加依赖并刷新 Maven检查依赖 scope 是否是 providedCommunications link failureMySQL 服务没有启动或连接地址/端口错误检查 MySQL 服务确认 URL 里的localhost:3306是否正确The server time zone value is unrecognizedMySQL 8.0 默认时区配置不合法JDBC URL 加上serverTimezoneAsia/ShanghaiHTTP Status 404Servlet 访问路径不存在或注解路径冲突检查WebServlet的路径清理 Tomcat 缓存重启确认访问的是项目名 映射路径HTTP Status 500服务器内部异常代码抛了运行时异常看控制台或 catalina 日志的 Caused by中文乱码请求或响应的字符集不一致请求侧req.setCharacterEncoding(UTF-8)响应侧resp.setContentType(text/html;charsetUTF-8)数据库 JDBC URL 加上 characterEncodingutf8端口被占用8080 被其他进程占用netstat -anoNoSuchMethodError: javax.servlet...项目中 Servlet API 依赖和 Tomcat 自带版本冲突确认是否已引入javax.servlet-apiscope 改为 provided4.2 处理 404 时的排查思路404 是 JavaWeb 初学者遇到最多的错误八成以上不是代码问题而是路径问题。我给你一套排查顺序第一步先确认 Tomcat 是否成功启动了应用。看 IDEA 控制台有没有Deployment started或者类似的日志以及 Tomcat 的 localhost 日志有没有报错。第二步确认访问地址完整。http://localhost:8080/servlet-demo/user/list其中servlet-demo是应用上下文路径/user/list是 Servlet 映射路径。这两段缺一不可。有人会漏掉/servlet-demo直接访问http://localhost:8080/user/list那当然是 404。第三步确认WebServlet注解里的路径是否正确。注意这个路径必须包含英文字符串例如WebServlet(/user/list)就对应 URL 的/user/list部分。大小写敏感/User/list和/user/list是两回事。第四步确认 Servlet 类是否被正确加载。在doGet()里加上一行System.out.println(进入 doGet());重新 Debug 启动看控制台有没有输出。如果连日志都没有说明你的 Servlet 根本没进到处理逻辑问题在设计 URL 映射阶段。第五步确认编译产物里有这个类。打开 project 目录下的target/classes文件夹看com/example/servlet/UserListServlet.class是否存在。不存在就手动 Build 一下这能帮你快速排除“IDEA 没编译”这种弱智级错误。4.3 中文乱码的整套解决方案乱码问题在 JavaWeb 里属于“经典永流传”几乎每个用中文的人都会碰到。我在实际项目里总结了一套三管齐下的方案第一管请求编码。统一在 Servlet 的service()方法开头用req.setCharacterEncoding(UTF-8)或者使用 Filter 统一设置。这个方法是针对 POST 请求的 body 部分的编码。至于 GET 请求乱码根源在 Tomcat 8 之前的 URL 解码Tomcat 8 之后默认 UTF-8问题不大。第二管响应编码。用resp.setContentType(text/html;charsetUTF-8)告诉浏览器响应的内容类型和编码。注意这个方法要在getWriter()之前调用否则无效。最好再配合resp.setCharacterEncoding(UTF-8)双保险。第三管数据库编码。建表用DEFAULT CHARSETutf8mb4JDBC URL 里加useUnicodetruecharacterEncodingutf8。这三个地方只要有一个不是 UTF-8数据就会在某个环节变成乱码。另外提醒一句IDEA 里Settings - Editor - File Encodings下的 Global Encoding、Project Encoding、Default encoding for properties files 这三项我建议全部改为 UTF-8。否则源码文件的编码不一致代码本身写的就是乱码聊什么都是白搭。4.4 改代码不生效热部署与缓存的坑很多新手有个体验改了代码重新启动 Tomcat发现访问还是旧页面跟中了邪一样。这个原因很多时候是 IDEA 的 Tomcat 热部署没配置对。我建议这样配置在 Tomcat 配置的 Server 页签里把 On Update Action 和 On Frame Deactivation 都改成Update classes and resources。这样你每次修改 Java 代码后按Ctrl F10IDEA 就会热更新 class 文件不需要重启 Tomcat。但是要注意新增方法签名、修改注解配置、修改 web.xml 这些结构性改动热部署是不生效的必须要重启。另外浏览器的缓存也会造成“代码已改但效果没变”的假象。我在开发时习惯把浏览器开发者工具打开勾选 Network 页签里的 Disable cache。排查的时候可以直接 CtrlF5 强制刷新页面或者用无痕窗口访问排除浏览器缓存干扰。4.5 连接数据库报错的常见坑数据库连接报错的排查顺序其实是有套路的。首先看 URL 里的几个关键参数我遇到过几次排查到深夜的问题最后发现是localhost写成了localhsot这种手滑错误。其次看密码MySQL 密码里如果有特殊字符比如#、、%在 URL 里会被特殊解析需要做 URL 编码。最后看防火墙和远程权限如果是连远程数据库确保 MySQL 的 user 表里授权了对应 host 的访问权限。再多说一句千万别在代码里长期硬编码数据库密码。虽然我上面的示例为了简化就是这么写的但工作中我建议至少把数据库连接信息放到jdbc.properties配置文件里用Properties读取这也为后续切换到连接池留好了余地。5. 进阶与思考Servlet 之后还能往哪走如果你把上面的案例完整跑通了我建议你再做两件小事能让这段学习经历瞬间上一个台阶。第一件把 JDBC 工具类换成Druid 连接池。具体改动思路是读取druid.properties配置文件通过DruidDataSourceFactory.createDataSource(properties)获取一个数据源对象然后从数据源里拿连接。这一步是为以后用 Spring、MyBatis 时理解“池化思想”打基础。第二件把页面展示改成JSP JSTL。Servlet 负责查数据库、把数据塞到 request 里然后req.getRequestDispatcher(/userList.jsp).forward(req, resp)转发到 JSP 页面在 JSP 里用c:forEach循环输出表格。这虽然是我现在工作中不太会直接用的写法但它是理解 MVC 分层最直观的例子也是你以后看 Spring MVC 代码的重要认知基础。前面这些都是从开发者的角度讲的再补一个笔试面试的角度。Servlet 相关的面试题我见过的有生命周期是什么、Servlet 和 Filter 的区别、forward 和 redirect 的区别、Cookie 和 Session 的区别、如何实现一个 Filter 做登录校验。这五道题你现在都能答吗如果不能那正好说明这篇文章让你发现了自己知识地图上的空白点这就是收获。最后再讲一个我个人的实操体会。我在工作里已经很多年没有直接写 Servlet 了但每次排查线上问题打开 Spring MVC 的堆栈看到 DispatcherServlet 那几个类的时候心里特别有底。因为我知道底层就是那套生命周期实例化、init、service、destroy。困扰很多人的“框架魔法”说穿了不过是容器和 Servlet 规范之上的一次又一次封装。你把这些基础真正吃透了以后学什么框架都是降维打击。写这些不是为了劝你回到 Servlet 时代恰恰相反——是为了让你用更短的时间冲过 Servlet 这个关口然后带着清晰的认知去拥抱更新的技术。漫漫长路把地基打好后面才能建高楼。