简介:J2EE编程技术源代码由郝玉龙编著,是一套面向Java企业级应用开发学习者的配套代码资源,覆盖Servlet、JSP、EJB、JMS、JTA等J2EE核心组件实例,适合从入门到进阶的开发者对照研习。资源共429个文件,压缩包约12.3MB,以java源文件(91个)、jsp页面(83个)、class文件(92个)及xml配置文件(97个)为主体,另含properties配置、tld标签库描述与jar依赖包等,完整呈现了从Web层到业务层再到数据持久层的典型企业级项目结构。目前已有186人学习下载。通过研读这些源代码,可直观理解Servlet与JSP协同构建动态Web应用的流程,掌握EJB会话Bean、实体Bean及消息驱动Bean的编写与部署方式,并学习JMS异步通信与JTA分布式事务在实际项目中的运用,是一份兼顾原理与编码实践的J2EE学习资料。
1. J2EE 源码包到底能干什么:老教材是最快的 JavaEE 入门路线
接手一个维护了两年的老系统,想改一处登录逻辑,发现项目里全是 Spring 的注解和拦截器,改一行要顺着调用链翻半天。翻出教材配套的《J2EE编程技术》源码才看明白——Servlet 处理请求、JSP 渲染页面、JDBC 读写数据,框架不过是把这几步包了层皮。这份源码不是教学 demo 拼盘,而是从数据库建表、JDBC 连接,到 Servlet 接收请求、JSP 回显页面的完整可跑通工程链路。适合三类人:教材学完脑子里没整幅画面的在校生、面试前要手推 JavaWeb 请求链路的求职者、被老系统维护逼到墙角的一线开发。
2. 源码包结构与环境准备:先把编译和部署跑通再谈架构
拿到源码包,大部分人第一反应是用 IDE 直接打开,然后被一堆红叉劝退。常见做法是先手动把它跑起来,确认“这代码真的能编译、能部署、能访问页面”,再进 IDE 慢慢改。所以这一章先把目录结构、JDK/Tomcat/MySQL 版本组合定下来,后面拆代码才有参照系。
2.1 解压后先核对目录:WebRoot 才是 Tomcat 加载的根目录
老式 JavaWeb 工程和现在 Maven 工程的目录差别很大,源码包解压后通常是这样的:
J2EESource/ ├── src/ # Java 源码 │ └── com/... │ ├── bean/ # 实体类 │ ├── dao/ # JDBC 数据访问层 │ ├── servlet/ # 控制器 │ └── util/ # 数据库工具类 ├── WebRoot/ # Web 根目录(部署时对应 Tomcat 应用根) │ ├── WEB-INF/ │ │ ├── web.xml # 中央配置文件 │ │ └── classes/ # 编译后的 class 文件 │ ├── css/ │ ├── images/ │ └── jsp/ # 视图页面 └── lib/ # 第三方依赖 jar这段目录结构里有几个关键点。src是源码存放目录,对应现代 Maven 工程里的src/main/java;WebRoot对应src/main/webapp,Tomcat 部署时加载的是 WebRoot,不是 src。很多人在这一步翻车:编译时把 class 输出到了WebRoot/WEB-INF/classes,这个位置不能错,因为 Tomcat 的类加载器只认WEB-INF/classes,多一层目录或者输出到别处,运行时就报ClassNotFoundException。lib目录里的 jar 在部署时要放到WEB-INF/lib下,它与 classes 目录组成了 Web 应用的类路径。拿到包之后,我的习惯是先核对三样东西:web.xml 是否放在WEB-INF下、编译输出目录是否为WEB-INF/classes、lib 下是否有驱动 jar。三样都齐,再考虑版本兼容问题。
提示:如果源码包是以
.war形式给你的,可以直接放进 Tomcat 的webapps目录启动;如果是以工程目录形式给的,按 WebRoot 路径手动整理后再部署。
这里给出一个不依赖 IDE 的编译命令,先把代码编过一遍:
# 在源码根目录执行,先确认 JDK 已安装 java -version # 编译全部源码,输出到 WebRoot/WEB-INF/classes javac -encoding UTF-8 -sourcepath src \ -cp "lib/*" \ -d WebRoot/WEB-INF/classes \ $(find src -name "*.java")这段命令的逻辑是:-encoding UTF-8指定源码编码,避免中文注释和字符串变成乱码;-sourcepath src让 javac 从 src 下找源码;-cp "lib/*"把 lib 下所有 jar 加进编译类路径,包括 servlet-api.jar 和 mysql-connector-java.jar;-d WebRoot/WEB-INF/classes指定 class 输出目录;最后用find src -name "*.java"展开所有 Java 文件,省得手动罗列包路径。需要注意,java -version如果输出 1.8 以上版本没问题,如果显示 1.6 或更老,建议换 JDK 8,老版本 JDK 对 UTF-8 编码的支持比较差。
JDK 版本敲定后,下一步是确认编译产物能部署。把整理好的目录复制到 Tomcat 的webapps下,启动即可:
# 假设 Tomcat 装在 /opt/tomcat rm -rf /opt/tomcat/webapps/ROOT cp -r WebRoot /opt/tomcat/webapps/ROOT cd /opt/tomcat/bin ./startup.sh tail -f ../logs/catalina.outROOT是 Tomcat 的默认应用根,访问http://localhost:8080/时直接命中它;如果你不想占用根路径,也可以把 WebRoot 改名成j2ee复制过去,访问http://localhost:8080/j2ee/。先rm -rf旧的 ROOT 再复制,是为了避免残留旧文件混入新代码。启动后立刻看catalina.out,报错、异常、端口占用都会打印在这里,比在浏览器里等 404 高效得多。
2.2 版本组合是第一道门槛:JDK 8 + Tomcat 8.5 + MySQL 5.7 最稳
教材源码大多基于 JDK 1.4/1.5 时代,Servlet 规范在 2.3/2.4,那会儿还不流行 Maven,也没有 Spring Boot。硬拿 JDK 17 + Tomcat 10 去跑,大概率会因为javax.*到jakarta.*的包名迁移直接编译失败。我一般用下面这套组合:
| 组件 | 教材默认 | 推荐稳定组合 | 说明 |
|---|---|---|---|
| JDK | 1.4 / 1.5 | JDK 8 | 老代码编译选项兼容,也不会触发模块化限制 |
| Tomcat | 5.x / 6.x | Tomcat 8.5 | 同时支持旧 web.xml 头声明和新的 Servlet 规范 |
| MySQL | 5.0 / 5.1 | MySQL 5.7 | 默认认证插件兼容老驱动,乱码问题少 |
| 数据库驱动 | mysql-connector-java 3.x | 5.1.49 | 8.x 驱动需要改类名和连接参数 |
这套组合的意义在于同时保留了老代码的“原生态”,又让日常开发不那么痛苦。JDK 8 是分水岭:往上 JDK 9 开始模块化,老代码里rt.jar相关写法会出问题;Tomcat 8.5 能解析老式web.xml的sun.com/dtd/web-app_2_3.dtd头声明,Tomcat 9/10 就严格得多了。MySQL 5.7 的默认认证插件是mysql_native_password,老驱动认识它;MySQL 8 默认换成caching_sha2_password,老驱动直接连不上。
如果你只有 JDK 11 或更高版本,也不是完全不能跑,但要做好两个准备:第一,编译时加一行参数放宽访问权限;第二,运行时报ClassNotFoundException: javax.xml.bind.JAXBException这类缺失时,手动往WEB-INF/lib补 jar。相比之下,直接装一个 JDK 8 省心得多,这也是我每次给老项目排障时的默认建议——不要和版本兼容性较劲,换一个已知稳的组合,半小时能解决的问题别耗半天。
版本组合确认后,数据库这边要把 SQL 脚本先执行一遍。源码包一般带sql目录或直接在文档里附建库脚本,拿到先看这张表对不对口:
CREATE DATABASE IF NOT EXISTS j2ee DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE j2ee; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE DATABASE ... DEFAULT CHARACTER SET utf8mb4是现在推荐的编码设定,老教材里常写latin1或utf8,建议统一改成utf8mb4,否则后面中文字段存储和查询会埋雷。username和password字段长度别学教材里写VARCHAR(10),现代注册场景 50 个字符都未必够用,顺手放宽不碍事。建完表之后,用SHOW TABLES或者图形化客户端确认表存在,再进入下一步,把源码里的数据库连接参数改成本机配置。
3. 核心模块拆解:从 JDBC 工具类到 MVC 主线的完整调用链
环境跑通之后,源码才真正值得读。教材源码最值钱的部分不是它用了多新潮的技术,而是把 JavaEE 每个角色拆得很清楚:谁负责连接数据库,谁负责处理请求,谁负责显示页面,之间通过什么方法调用。我拆这类源码的习惯是“三步走”:先看工具类,再看一个完整模块的 Servlet + JSP,最后过一遍 web.xml 里的映射关系。这里以最常见的用户注册登录模块为例。
3.1 数据库连接工具类:改密码、换驱动都只动一个文件
老教材几乎都会在util包放一个 JDBC 工具类,名字通常是DBUtil或DatabaseConnection。它的作用是把建立连接的重复代码收拢起来,业务层不需要每次都写Class.forName。简化后的核心逻辑长这样:
package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.ResourceBundle; public class DBUtil { // 从类路径下的 db config 文件读取配置 private static final String DRIVER = ResourceBundle .getBundle("db").getString("driver"); private static final String URL = ResourceBundle .getBundle("db").getString("url"); private static final String USERNAME = ResourceBundle .getBundle("db").getString("username"); private static final String PASSWORD = ResourceBundle .getBundle("db").getString("password"); static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn) { if (conn != null) { try { conn.close(); } catch (SQLException e) { // 关闭失败通常不影响主流程,但建议打印日志 } } } }这段代码的逻辑是:ResourceBundle.getBundle("db")读取类路径下的db.properties文件,把驱动类名、URL、用户名、密码一次性读进静态常量;Class.forName(DRIVER)在静态代码块里加载 JDBC 驱动,只需要执行一次;getConnection()是全局唯一的获取连接入口,所有 DAO 都通过它拿连接。这样设计的直接收益是:改数据库密码、换 MySQL 驱动时,只编辑db.properties,不需要碰任何 Java 代码。
对应的db.properties长这样:
driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/j2ee?useUnicode=true&characterEncoding=utf8&useSSL=false username=root password=123456参数说明:driver在 MySQL 5.7 + 老驱动时写com.mysql.jdbc.Driver;如果驱动升级到 8.x,必须改成com.mysql.cj.jdbc.Driver。url里的useUnicode=true&characterEncoding=utf8保证中文按 UTF-8 传输,useSSL=false是为了避免老驱动连本地 MySQL 时自动启用 SSL 报证书警告。characterEncoding的值要和建库时的字符集一致,这里如果错配,查询出来的中文大概率是问号。这个文件必须放在类路径根目录,也就是WEB-INF/classes下,或者放在src根目录让它编译进去。
说起这个工具类,“玄学”最多的地方在于连接关闭。老教材代码里经常只写conn.close(),结果在连接池场景下把物理连接关掉,池子失效。后来我个人的习惯是:在 DAO 里用 try-with-resources,连接、语句、结果集都放到 try 块里自动关闭,省去手工close的顺序问题。JDK 8 完全支持这种写法,老代码里大量冗余的 finally 块可以直接删掉。
3.2 Servlet 控制器 + JSP 视图:一个登录模块的数据流全解
工具类解决“怎么连数据库”,接下来看“请求怎么进来、页面怎么出去”。以登录为例,教材里的 LoginServlet 通常完成三层职责:接收参数、调 DAO、转发页面。核心代码:
package com.example.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.example.dao.UserDao; import com.example.bean.User; public class LoginServlet extends HttpServlet { private final UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 接收客户端传来的用户名和密码 String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 交到 DAO 层验证 User user = userDao.findByUsernameAndPassword(username, password); // 3. 根据结果决定跳转路径 if (user != null) { request.getSession().setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }这一段把 MVC 里的 C 角色写得明明白白。第一步getParameter拿到的是浏览器表单提交到服务器的字符串,类型永远是 String,所以后面如果要对年龄之类的字段做运算,得自己Integer.parseInt;第二步调UserDao,DAO 里的方法名findByUsernameAndPassword直接反映 SQL 意图,这种命名习惯现在看也不过时;第三步两个分支覆盖了成功和失败两种路径,成则重定向跳首页,败则回登录页并在 request 里带一个错误提示。这里要注意sendRedirect和forward的本质区别:重定向是浏览器发起第二次请求,地址栏会变,request 里的属性会丢;转发是服务器内部跳转,地址栏不变,request 属性保留。教材里经常混用这两个方法,新手照着写时会发现“登录成功了,但首页拿不到用户名”,十有八九就是误用了sendRedirect传数据。
配套的 web.xml 映射是 Servlet 与 URL 之间的桥:
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>servlet-class必须是带包名的完整类名,三个字母写错都会在运行时抛ClassNotFoundException;url-pattern是浏览器地址栏输入的路径,这里配置/login,那么表单的action="login"就命中它。这段配置最大的好处是解耦——类名可以随便改,URL 对外不变,不用通知前端改链接。教材配套源码里如果用了 Servlet 3.0 以后的注解方式@WebServlet("/login"),web.xml 里就不用重复声明;但源码包通常两者都有,注意别配双份导致映射冲突。
再补一个 JSP 侧的核心片段,看页面怎么写:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <html> <body> <h2>用户登录</h2> <% if (request.getAttribute("errorMsg") != null) { %> <p style="color:red"><%= request.getAttribute("errorMsg") %></p> <% } %> <form action="login" method="post"> <input type="text" name="username" placeholder="用户名"/> <input type="password" name="password" placeholder="密码"/> <button type="submit">登录</button> </form> </body> </html>这段 JSP 的重点不是页面样式,而是三个动作:pageEncoding声明页面本身的编码,errorMsg从 request 里取错误提示并输出,表单的action指向 web.xml 里映射的/login。<%= request.getAttribute("errorMsg") %>是 JSP 表达式,等价于out.print(request.getAttribute("errorMsg")),但它直接拼在 HTML 里,如果值是null会自动输出空字符串,不会报错。如果你想看更多细节,教材后面章节还会引入 JSTL 标签库,本质上是把这种 scriptlet 写法换成<c:if>,但底层数据流完全一样。
3.3 EJB 与业务接口的取舍:哪些章节可以跳过
源码包后半部分通常是 EJB 相关章节,Session Bean、Entity Bean 的年代产物。本地开发会遇到两个现实问题:一是需要应用服务器支持,Tomcat 默认不带 EJB 容器,要往 lib 里塞 openjdk 之类的实现,配置繁琐;二是源码里的 EJB 接口大多依赖容器注入,直接new一个实现类跑不通。我的建议是,EJB 部分不用强跑,重点读它的接口定义与实现分离思想——业务接口归接口、实现归实现,这个设计在今天的 Spring 里被发扬光大。你只要在代码里看到@Remote、@Stateless这类注解,知道它是一个 EJB 组件;看到Home接口,知道那是老版本 EJB 的工厂模式写法,就够了。真正对面试和工程有帮助的是前面两节的内容,EJB 部分理解概念,遇到时能认出关键字,不会手足无措即可。
4. 部署避坑与常见问题排查:五个当场翻车的典型案例
源码包本身没问题,但“能跑”和“跑起来”之间隔着一堆环境问题。这些坑分散在不同机器上,我按出现频率整理了五个,每一条都是当时卡了半小时以上的真实经历。
4.1 编译报错:程序包 javax.servlet 不存在
现象:按前面命令执行javac,控制台立刻报程序包 javax.servlet 不存在,Servlet、HttpServlet 全部找不到。
原因:-cp没带servlet-api.jar,或者 lib 目录里压根没有这个 jar。老教材的 lib 目录经常只放了 MySQL 驱动,Servlet API 依赖 Tomcat 自带的lib/servlet-api.jar,但编译时代码里直接引用,必须显式加入类路径。
解决:三选一,把 Tomcat 安装目录lib/servlet-api.jar复制到源码 lib 目录;或者在编译命令的-cp里追加:/opt/tomcat/lib/servlet-api.jar;再稳妥一点,直接下载 Servlet 3.1 规范对应的 api jar 放进 lib。复制进 lib 之后,记得重新编译,别只用 IDE 的自动编译。
如果连HttpServlet都识别不了,还有一个隐蔽原因:IDE 建的是普通 Java 工程而不是 Web 工程,没有引入 Tomcat 运行时。把工程类型改成 Web 工程,或手动把 Tomcat 的 lib 加进 Build Path,问题消失。
4.2 Tomcat 启动失败:端口占用和 web.xml 解析错误
现象:./startup.sh执行后马上退出,catalina.out里同时出现Address already in use: JVM_Bind,或者Error configuring application listener。
原因:8080 端口被其他进程占用;或 web.xml 里的servlet-class写错。两者都会让 Tomcat 启动中断,但日志位置不同——端口问题在启动前几行,web.xml 问题在应用部署阶段。
解决:端口冲突用lsof -i:8080(Linux/macOS)或netstat -ano | findstr 8080(Windows)查占用进程,杀掉或改 Tomcat 的server.xml里 Connector 的port属性。web.xml 的问题,重点复核servlet-class是否含完整包名、以及类名的大小写,com.example.servlet.LoginServlet是三层包,写漏任何一层都是同样的错误提示。这类问题排查顺序我习惯是:先看catalina.out尾部 50 行,再按关键字搜Caused by,最后才改配置重启。
4.3 数据库连不上:Communications link failure
现象:页面能打开,但一点登录,Tomcat 日志里出现Communications link failure或Access denied for user。
原因:两层。Communications link failure最常见是 URL 里主机端口不对,或 MySQL 服务没启动;Access denied则是用户名密码错误,或者 MySQL 8 的认证插件与老驱动不兼容。教材里写jdbc:mysql://localhost:3306/j2ee本身没错,但驱动是 5.x 时代产物时,连 MySQL 8 会触发认证协议不匹配。
解决:先重启 MySQL,确认端口在听。再分两路走:如果库是 MySQL 5.7,保持com.mysql.jdbc.Driver不变,只检查账户密码;如果库是 MySQL 8,把驱动换成com.mysql.cj.jdbc.Driver,URL 里补useSSL=false&serverTimezone=Asia/Shanghai,再执行下面 SQL 把账号认证改为老驱动认识的方式:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;改完驱动记得清一下WEB-INF/classes下的旧 class,避免 Tomcat 缓存。
4.4 中文乱码:页面、请求、数据库三层编码不一致
现象:登录成功后首页用户名显示正常,但往数据库里保存的中文变成了??,或者 JSP 页面上直接显示乱码。
原因:老教材源码里,JSP 的pageEncoding写GBK,数据库连接参数写characterEncoding=GB2312,而 Tomcat 请求默认按ISO-8859-1解码,三层不一致叠加起来,中文大概率坏掉。
解决:统一到 UTF-8,文件层面 JSP 改成pageEncoding="UTF-8",db.properties的 URL 里characterEncoding=UTF-8,数据库建库时用utf8mb4;请求层面,在 web.xml 里配置一个编码过滤器,或者直接在doPost开头执行request.setCharacterEncoding("UTF-8")。比较省事的方案是加一个过滤器类:
package com.example.filter; import javax.servlet.*; import java.io.IOException; public class CharacterEncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); } }这个过滤器的逻辑非常简单:任何请求进入 Servlet 之前,先经过setCharacterEncoding("UTF-8"),把 POST 表单参数按 UTF-8 解码,chain.doFilter再放行到目标处理逻辑。配套的 web.xml 配置里<url-pattern>/*</url-pattern>表示拦截所有请求,但要注意这个过滤器对 GET 请求的 URL 中文无效,Tomcat 对 GET 的 URI 解码取决于server.xml里的URIEncoding="UTF-8"属性,我一般顺手一起加上。
4.5 Eclipse 工程导入 IDE 后直接红叉
现象:双击打开源码包,Eclipse 或 IDEA 里一堆红叉,Run on Server不可用。
原因:老源码是 Eclipse 老版本工程,.classpath和.project文件格式与现代 IDE 不完全兼容,lib 目录没有被识别为依赖。
解决:常见做法是新建一个 Dynamic Web Project,把src拷到新工程的src,把WebRoot里的内容拷到新的WebContent或webapp,再把lib下的 jar 全部加入 Build Path。用 IDEA 的话,新建 Java Enterprise 模块并选择 Web Application,然后Project Structure → Libraries里添加 lib 目录。这条路走完,红叉基本清零。如果还有个别包名标红,多半是 JDK 版本不对,回到第 2 章推荐的 JDK 8 再看一遍,通常能解决。
5. 把教材源码改造成 Maven 工程:老代码也能拥有现代构建体验
5.1 新建 Maven 工程并迁移目录:依赖管理是最直接的收益
跑通源码只是第一步,真要拿它二次开发,我建议做一次 Maven 化改造。改造的收益是依赖管理、构建命令、部署流程全部标准化,后续加个 JSON 库或切换数据库连接池,都只需要改pom.xml。步骤如下:先新建一个空 Maven Web 工程,把 src 下 Java 源码和 WebRoot 下的 webapp 内容对应拷入,然后在pom.xml里声明依赖:
<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> </dependencies>两个依赖的作用不同:javax.servlet-api的scope设成provided,表示编译时需要、运行时不打包进 war,因为 Tomcat 自带;mysql-connector-java不加 scope,默认compile,会打进WEB-INF/lib,保证部署机器上没有安装驱动也能连数据库。版本特意选了 5.1.49,就是为了避开驱动 8.x 对 MySQL 5.7 也要求改类名的问题。
5.2 验证改造与连接池收尾:改造不是终点,能复用才是
改造完成后,原来手写javac的老流程不再需要,mvn package一条命令完成编译、打包。验证改造是否成功,就把生成的 war 部署到 Tomcat,访问一个涉及数据库查询的页面,把第 3 章的登录流程完整走一遍。通过后再顺手把 DBUtil 里的DriverManager换成连接池初始化逻辑,单测时能明显感到页面响应变快。
说句实话,我最早从这套教材源码里得到的不是技术,而是一个习惯:拿到任何老工程,先不问为什么,先把编译、部署、访问全链路跑通,再动手改代码。有一次客户现场数据库版本突然升级,密码、驱动全变了,我当时就是靠着把所有配置收拢到db.properties这一条经验,十分钟定位到问题,避免了整个项目组陪我熬夜。从那以后,我每拿到一个 JavaWeb 源码包,都强制自己先走一遍“编译 → 部署 → 访问 → 改配置 → 再部署”的五步闭环,确认链路通了我才敢往里加新逻辑。希望这套方法和这份源码一样,帮你在 J2EE 这条路上少踩几个坑。
本文还有配套的精品资源,点击获取