J2EE编程技术配套源码实战:从环境搭建到轻量迁移
简介《J2EE编程技术源代码》由郝玉龙编著是一份面向Java企业级应用开发的学习资源适合正在入门J2EE或希望深入理解Servlet、JSP、EJB、JMS、JTA等核心技术的开发者。资源包共包含429个文件包括91个Java源文件、83个JSP页面、92个字节码文件与97个XML配置文件覆盖了业务逻辑、视图呈现、组件编译和配置描述的典型示例整体仅12.3MB便于下载后直接研读。目前已有186人学习代码目录结构清晰通过逐个分析这些实例读者能掌握J2EE分层架构、数据库交互、事务控制等关键实践并学会整合各核心组件解决企业级开发中的常见问题。更重要的是这套源代码中的许多示例都围绕实际应用场景展开能够帮助读者理解从请求处理、业务封装到持久化存储的完整流程无论是课程配套练习还是项目参考都能提供直观的落地参考。1. 先看清这套源码的全局为什么教材代码“有源码也未必能跑”郝玉龙编著的《J2EE编程技术》配套源代码是很多人学习Java Web开发时手边的一份完整示例集。我见过不少同学拿到源码包之后直接解压、导入开发工具然后卡在数据库连接串、JDK版本、部署容器这些地方。问题不在于代码本身而在于缺少一套“环境坐标”。这份源码覆盖Servlet、JSP、JDBC还包含EJB的入门演示技术栈偏“重量级”如果用新版JDK和容器直接跑报错是大概率事件。这篇笔记就按“体系拆解→本地跑通→源码阅读→轻量迁移→避坑排错”的顺序讲落地路径适合想拿这套源码练手的学生和刚转后端的开发者。2. J2EE编程技术那套体系先把技术栈和源码目录对上2.1 分层架构和源码目录的对应别在运行之前先迷路拿到教材配套源代码第一件事不是看代码而是先盘目录。J2EE的核心思想是分层Web层负责接收请求和渲染页面业务层处理逻辑数据层访问数据库。老式教材的源码工程结构非常规整通常分成这样几块目录/位置典型内容对应J2EE分层src 或 src/main/javaServlet类、业务类、DAO类Web层业务层数据层WebContent 或 webJSP、静态资源、WEB-INF/web.xmlWeb层展示与配置lib 或 WebContent/WEB-INF/libMySQL驱动、JSTL、commons等jar包第三方依赖sql 或 db建库建表脚本、初始数据数据库准备我一般会先看有没有sql目录没有就先找代码里的Connection字符串反推表结构再手动建库。这个动作比任何编译错都重要因为很多教材源码的建表脚本和代码里的字段是对不上的或者脚本要在某个特定字符集下执行执行完还要检查编码是否为utf8。J2EE的标准玩法和现在的Spring Boot工程差别很大。Spring Boot把容器内嵌、自动配置都做了而老式J2EE工程要求开发者自己管理部署和依赖这正是源码包看起来“乱”的根源。理解这一点很多编译期报错就能定位到“缺jar包”或者“web.xml位置不对”上。常见做法是把整个项目直接导入集成开发环境等它把依赖识别出来。但如果lib目录里全是旧jar开发工具会把这些jar一股脑加进构建路径新版Tomcat或JDK下经常出现包冲突。所以我更建议先按目录结构记录每个jar的来源再决定哪些保留。这一步做完后续编译问题会少一半。2.2 Servlet与JSP的协作看懂Web层两个文件的关联J2EE教材里最核心的配套代码几乎都围绕“JSP做表单Servlet做控制”展开。以最常见的登录功能为例一个典型的Servlet类长这样package demo.web; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; // 老教材常用web.xml声明映射这里也支持注解方式 public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 接收JSP表单提交的参数 req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 2. 调用业务层或数据访问层 UserDao dao new UserDao(); User user dao.findByUsernameAndPassword(username, password); // 3. 根据结果决定跳转页面 if (user ! null) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /index.jsp); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }这段代码覆盖了两个关键知识点request.getParameter是从请求体里取表单字段getRequestDispatcher().forward()是服务器内部跳转sendRedirect是浏览器重定向。配套源码里大量请求都走这个模式网上关于“forward和redirect区别”的问题其实在源码里对照一遍就通了。老式配置里Servlet还要在web.xml中声明访问路径。这也是源码里常见的两个文件的对应关系servlet servlet-nameloginServlet/servlet-name servlet-classdemo.web.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameloginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping看到这里你会发现JSP页面的提交到/loginweb.xml把这个URL交给LoginServlet处理。这就是“配置文件驱动”的经典结构。源码里如果有很多类似片段说明这份教材的代码风格偏传统适合用来理解Java Web的原始模型。2.3 EJB在源码里的戏份概念归概念跑通是另一回事EJB是J2EE里最劝退的部分。教材配套源码中EJB示例通常单独放一个目录要么是Session Bean的远程接口加实现类要么是JNDI查找的客户端代码。我在实际学习过程中发现绝大部分读者需要的不是把EJB完整部署到应用服务器上而是理解“业务逻辑为什么需要事务和分布式调用”。有一个很实际的建议如果只为完成功能验证用Tomcat跑通ServletJSPJDBC就够了EJB示例可以先跳过当你需要学分或需要应付考试里的部署题再去折腾应用服务器。手头如果有GlassFish或老版本JBoss按教材步骤部署如果只想理解思路把EJB的接口和实现类当成普通Java接口加实现类阅读即可。依赖是另一个坑。EJB示例会引入一堆Java EE API包这些包和Tomcat自带的servlet-api.jar、el-api.jar存在版本重叠不加控制地全部扔进lib目录后果就是启动时抛出各种LinkageError。我一般会把EJB相关的库单独放一个目录不参与Web项目的构建只在需要阅读源码时作为参考。这样既不影响主力功能运行也不会把整个项目环境搅乱。3. 把配套源码跑起来极简本地方案与三个必检查点3.1 环境配对JDK、Tomcat、数据库的版本怎么选老教材源码编写时的主流环境是JDK 6或7、Tomcat 6/7、MySQL 5.x而现在的开发机器上普遍是JDK 8或更高版本。版本选太高编译报错一堆版本太低操作系统又可能不适配。所以先定一个兼容性最好的组合我常用的是组件推荐版本说明JDK8对老代码兼容性最好也支持大部分新写法Tomcat8.5或9.08.5以下对JDK8支持不完整9.0仍使用javax命名空间MySQL5.7或8.05.7最省事8.0需修改驱动类名和时区参数MySQL驱动5.1.x老代码或8.0.x驱动类名不同详见下文环境变量在命令行里的设置方式如下# Linux / macOS 临时设置 export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64 export CATALINA_HOME/opt/tomcat9 export PATH$JAVA_HOME/bin:$CATALINA_HOME/bin:$PATH # Windows 临时设置命令行一次生效 set JAVA_HOMEC:\jdk1.8.0_202 set CATALINA_HOMED:\apache-tomcat-9.0.89 set PATH%JAVA_HOME%\bin;%CATALINA_HOME%\bin;%PATH%JAVA_HOME指向JDK安装根目录不是bin目录这点在Windows上经常被搞反。CATALINA_HOME指向Tomcat解压后的根目录。如果源码工程里带有.classpath或.project文件说明原配环境是Eclipse用开发工具导入后记得检查“Java Build Path”里的JRE版本和项目编译级别通常要改成1.8才能避免一堆“方法不存在”的报错。3.2 部署一个带数据库连接的Web项目最小可运行步骤先把数据库备好。配套源码一般有建表脚本没有也没关系按代码里的SQL反推。下面是最小示例CREATE DATABASE IF NOT EXISTS demo DEFAULT CHARSET utf8; USE demo; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL, password VARCHAR(64) NOT NULL, nickname VARCHAR(64) ) ENGINEInnoDB DEFAULT CHARSETutf8; INSERT INTO t_user(username, password, nickname) VALUES (admin, 123456, 管理员);然后写一个最简单的JDBC调用验证数据库连接。这一步不要先连Servlet直接用Java类跑能把“数据库问题”和“Web容器问题”分开定位package demo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class DbUtil { private static final String URL jdbc:mysql://127.0.0.1:3306/demo?useUnicodetruecharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你自己的密码; static { try { // 教材源码里大多是这句MySQL驱动5.x对应这个类 Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static void main(String[] args) throws Exception { try (Connection conn DriverManager.getConnection(URL, USER, PASSWORD); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT username FROM t_user)) { while (rs.next()) { System.out.println(user rs.getString(1)); } } } }如果驱动版本是8.xClass.forName要改成com.mysql.cj.jdbc.DriverURL里还要拼接serverTimezoneAsia/Shanghai否则驱动会报时区异常。代码里先用Class.forName注册驱动是老写法之后的代码都用DriverManager.getConnection连接URL参数里useUnicode和characterEncoding要成对出现这是老工程中文不乱码的底线。代码跑通后把整个Web项目打成war包放进Tomcat的webapps目录再启动# 进入Tomcat bin目录后 ./startup.sh # Windows startup.bat访问地址是http://127.0.0.1:8080/项目名/。war包爆炸式部署后项目名默认就是war文件名。这一步失败最常见的现象是404先看Tomcat的logs/catalina.out和logs/localhost.*.log比乱设开发工具配置有效得多。提示数据库密码写死在代码里这件事教材源码可以这么干真实项目千万别学。练手阶段先把功能跑通后续做迁移改造时再考虑把配置抽到properties文件里。3.3 改一个查询参数走通全链路验证你确实吃透了源码跑通只是开始。真正检验是否掌握这套源码是改一个参数并让整个链路按你预期地变化。下面是一个最简的改造练习。在JSP页面里加一个按昵称模糊搜索的入口% page contentTypetext/html;charsetUTF-8 languagejava % html body form actionsearchUser methodget input typetext namekeyword placeholder请输入昵称关键字/ button typesubmit搜索/button /form /body /html对应的Servlet取到这个参数后传入数据层package demo.web; import demo.dao.UserDao; import demo.entity.User; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; public class SearchUserServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String keyword req.getParameter(keyword); // 与JSP里name一致 UserDao dao new UserDao(); ListUser list dao.searchByNickname(keyword); req.setAttribute(userList, list); req.getRequestDispatcher(/userList.jsp).forward(req, resp); } }在UserDao中实现模糊查询public ListUser searchByNickname(String keyword) { ListUser list new ArrayList(); String sql SELECT id, username, nickname FROM t_user WHERE nickname LIKE ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setNickname(rs.getString(nickname)); list.add(user); } } } catch (Exception e) { throw new RuntimeException(e); } return list; }这套改造能跑通说明你看懂了四件事表单name和getParameter参数的对应关系、Servlet怎么调用DAO、用户提交的参数会拼进SQL、SQL采用PreparedStatement而不是字符串拼接。参数不在代码里写死而是从请求里取这正是J2EE Web应用最核心的数据流。做到这一步才算把源码“读懂了”。4. 源码阅读路线从页面追到数据层再改造成轻量框架4.1 从请求路径出发读源码先web.xml后Java类拿到一份不熟悉的源码最忌讳的是按包名顺序从aaa包读到zzz包。我建议顺着请求路径走第一步是打开WEB-INF/web.xml把里面所有 收集起来形成一张“URL到Servlet”的映射表。这样一来代码里任何界面上的按钮都能找到对应的入口类。如果项目用了注解入口类上会有类似WebServlet(/searchUser)的写法。无论哪种形式目标都是先找到入口再顺着doGet或doPost一类方法往下走。我把这个过程称为“做请求路由笔记”用文字记录即可。比如login.jsp → LoginServlet → UserDao.findByUsernameAndPassword → t_user表。这份笔记写完后你再看源码脑子里会有一条完整的调用链而不是一堆孤立类。我在翻阅老教材源码时还会顺便记录每个页面用到的请求参数名。比如“username”“password”“id”这是因为后续迁移或测试时参数名不一致往往是最大的隐性故障源。参数名在JSP表单项里能看到在Servlet的getParameter里能再次确认只要两边不匹配页面上就会多出一个null或空字符串。4.2 数据层的套路手写JDBC里的固定模板与SQL拼接风险教材配套源码的数据层代码高度雷同几乎每个DAO类里都有一套“获取连接→创建语句→执行→处理结果→关闭资源”的固定代码而且关闭资源的代码常常写在finally块里。这类代码很好读但也很容易在实际环境里踩坑。例如package demo.dao; import demo.entity.User; import demo.util.DbUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class UserDao { public User findByUsername(String username) { String sql SELECT id, username, password, nickname FROM t_user WHERE username ?; try (Connection conn DbUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setNickname(rs.getString(nickname)); return user; } } } catch (Exception e) { throw new RuntimeException(e); } return null; } }首先要注意PreparedStatement用?占位符这是安全写法如果源码里出现字符串拼接SQL请格外小心// 危险写法直接拼接用户输入很容易产生注入问题 String sql SELECT * FROM t_user WHERE username username ;在读源码阶段遇到两种写法都要能认出来。配套源码里如果大量拼接用户输入适合拿来做“改造练习”如果是PreparedStatement说明这套教材安全观念比较靠前。无论哪种都要明确连接资源由谁关闭。老代码常见的是手动finally关闭新一点的写法是try-with-resources两套代码都能在DAO里找到。关闭顺序一般是先关闭ResultSet再关闭Statement最后关闭Connection。4.3 保留页面不动只换持久层往MyBatis迁移的最小改法读透源码之后最有价值的练习是往轻量框架迁移。推荐先迁移数据层因为页面和Servlet的请求、跳转逻辑都与业务功能绑定改动小、验证容易。把UserDao改成MyBatis实现只需要三件事一个配置文件、一个Mapper XML、一个接口。先看配置?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://127.0.0.1:3306/demo?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value你的密码/ /dataSource /environment /environments mappers mapper resourcedemo/dao/UserMapper.xml/ /mappers /configuration然后是Mapper XML里的SQL和原DAO里的查询基本一一对应mapper namespacedemo.dao.UserMapper select idsearchByNickname resultTypedemo.entity.User SELECT id, username, password, nickname FROM t_user WHERE nickname LIKE #{keyword} /select /mapper接口定义package demo.dao; import demo.entity.User; import java.util.List; public interface UserMapper { ListUser searchByNickname(String keyword); }原来的UserDao如果使用方在Servlet里new出来的迁移时有两种办法一种是让UserMapper的实现类保持同名UserDao把方法签名改一遍另一种更稳妥是写一个MyBatisUserDao实现类内部持有SqlSessionFactoryServlet里改用这个新类。迁移完成后页面不变、Servlet的调用形态不变只有数据访问底层变了。这个改造是理解“接口与实现分离”的最好练习也是老教材源码比较典型的二次开发方向。5. 经典难点与避坑从环境变量到中文乱码的5条排查记录下面5条是我在多个J2EE教材源码工程上翻车后的排查记录每一条都按现象、原因、解决来写。你在跑源码时遇到的90%问题基本都能对号入座。5.1 部署后访问页面报404项目列表里却能看到工程目录现象Tomcat正常启动访问http://127.0.0.1:8080/项目名/时返回404。原因这类教材源码的web.xml里把欢迎页配置成了某个具体JSP比如index.html或login.jsp而工程里并没有这个文件也可能是url-pattern里的路径和表单action对不上。解决先看web.xml里的 把欢迎页改成实际存在的文件。访问前先用浏览器直接访问项目根目录再访问具体页面确认是容器层面404还是应用路由404。这个排查能把“部署问题”和“代码映射问题”分开。5.2 启动报ClassNotFoundException: com.mysql.jdbc.Driver现象应用启动、第一次访问数据库功能时报找不到驱动类但lib目录里明明有mysql-connector-java.jar。原因lib里放的是MySQL 8.x驱动而8.x不再打包com.mysql.jdbc.Driver旧类。老教材代码写的是Class.forName(com.mysql.jdbc.Driver)自然加载不了。解决把驱动降级为5.1.x或者把代码里的类名改成com.mysql.cj.jdbc.Driver同时给URL加serverTimezoneAsia/Shanghai。我个人更建议改代码因为你迟早要接触新驱动。5.3 页面中文全部是问号数据库里却是正常的现象JSP页面显示正常但从前端提交中文后存进数据库变成问号或者页面上显示乱码。原因三层编码不一致。JSP页面声明的字符集、Servlet里req.setCharacterEncoding、数据库连接URL里的characterEncoding、数据库表的charset只要有某一层不是UTF-8就会出问题。解决统一四层。页面用charsetUTF-8Servlet入口处设置请求编码URL带useUnicodetruecharacterEncodingutf8建库建表都用utf8。如果是在老Tomcat下做过滤器可以写一个编码过滤器统一处理package demo.filter; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class EncodingFilter implements Filter { private String encoding UTF-8; Override public void init(FilterConfig config) { // 优先读取web.xml里的encoding参数没配就用默认值 } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(encoding); response.setCharacterEncoding(encoding); chain.doFilter(request, response); } }这个过滤器用一句话解释把进入应用的每个请求都强制按UTF-8解码。加了它Servlet里就可以不必每个方法都写setCharacterEncoding但注意它必须在请求体读取之前生效所以过滤器顺序要排在前面。5.4 Tomcat 10下启动就报错javax.servlet不存在现象把老工程部署到最新Tomcat 10启动后大量NoClassDefFoundError或Servlet无法加载。原因Tomcat 10起把Java EE迁移到Jakarta EE命名空间servlet包由javax.servlet改为jakarta.servlet。老教材源码全部基于javax编写容器找不到类。解决把Tomcat版本降到9.0.x。除非你想把源码里的所有import javax.servlet改成import jakarta.servlet那是另一个工作量。这种“升级换命名空间”的问题在源码本身没有维护计划时最稳妥的答案是锁版本。5.5 改了JSP页面不生效改了Java类也不生效现象代码里改了内容刷新浏览器毫无变化重启Tomcat也一样。原因Tomcat的work目录下缓存了编译后的JSP或者开发工具没有把编译后的class同步到部署目录又或者浏览器缓存了旧页面。解决三步走先CtrlF5强刷浏览器再删掉Tomcat的work/Catalina目录最后在开发工具里执行“清理并重新部署”。排查时始终记住页面变化看JSP编译缓存Java变化看class输出目录。这套顺序处理完90%的“改不动”都能解决。6. 把教材源码变成自己的东西三个验证方法与一个高效习惯读完并跑通《J2EE编程技术》的配套源代码后真正拉开差距的是怎么把别人的代码变成自己的。第一个方法是用浏览器开发者工具追踪请求链路。打开F12在Network里观察一次表单提交能看到请求URL、请求参数、响应状态码把这三个信息和web.xml里的映射一一比对等于给整套源码做了一次实时体检。第二个方法是给源码加一个“并发冒烟测试”。老式JDBC代码如果不使用连接池每次请求都创建Connection压测时错误率会迅速上升。用一个简单的压测工具同时发20个请求观察数据库连接是否被正确关闭。这一步能暴露出运行时最严重的资源泄漏问题比读十遍代码都管用。第三个方法最见功底把一个JSP页面改造成输出JSON。思路是让Servlet不再forward到JSP而是用PrintWriter直接输出一段JSON字符串前端页面再用原生方式解析。这个过程会逼你把页面和接口分离是理解前后端分离的第一步。完成这个改造后你会发现整套源码从“教材项目”向“可维护工程”迈进了一大步。还有一个我坚持了很久的习惯每读完一个关键类就把代码文件关闭凭记忆重新实现一遍然后跟源码对比差异。最初实现的结果哪怕错误百出也没关系差异点就是你还没掌握的知识点。当年我曾经跳过环境验证直接翻到后面的综合案例源码结果因为JDK版本问题返工了整整三天那之后我再也没绕开过“先跑通最小示例再扩展阅读”这一步。希望帮到你。本文还有配套的精品资源点击获取