JavaWeb源码反编译与工程还原实战:Servlet、Filter与验证码链路

发布时间:2026/9/23 14:04:15
JavaWeb源码反编译与工程还原实战:Servlet、Filter与验证码链路
简介这份JavaWeb开发实战源码面向希望系统掌握Java Web开发流程的个人学习者尤其适合正在从基础语法向完整项目实践过渡的初学者也可作为有经验开发者查漏补缺的参考。资源以webdemo项目为主线完整呈现MVC分层设计Model层演示业务逻辑处理与数据库增删改查View层包含JSP页面、CSS样式与JavaScript交互脚本Controller层则通过Servlet接收请求并调度视图返回同时涉及JDBC连接数据库、预编译SQL防注入及输入过滤防XSS等安全实践。压缩包共450个文件约6.75MB以288个png与32个gif截图辅助理解另有22个jsp、18个java、36个class、16个js、11个xml及4个jar等覆盖页面、逻辑、配置与依赖各环节。目前已有54人学习。通过研读源码读者可直观理解从页面制作到后端处理再到数据存储的完整链路掌握分层结构与代码组织方式并积累可复用的排错与优化思路。1. 从一堆 .class 文件说起这套 JavaWeb 源码到底能跑出什么拿到一个压缩包解压后看到的不是整齐的src目录而是一串UserManageDaoImpl.class、UserManageServlet.class、ValidateCodeServlet.class、UserLoginServlet.class、UserLoginFilter.class——每个文件还重复出现了两次。这是很多人第一次接触网络分享版 JavaWeb 实战源码时的真实场景。它对应的是一套典型的 MVC 分层 Web 应用DAO 层封装数据库增删改查Servlet 充当控制器接收请求并调度Filter 做登录拦截验证码 Servlet 独立生成图形校验码。适合正在学 Servlet/JSP 体系、想找一个能跑通的完整案例来对照理解的人也适合需要快速搭一个用户管理模块原型的开发者。源码本身不复杂但把「请求进来之后到底经过了哪些类」这件事讲清楚了比看十篇理论文章都管用。2. 反编译与工程还原把 .class 变回可读的 Java 源码2.1 为什么拿到的是 .class 而不是 .java网络分享的 JavaWeb 源码包很多时候只保留了编译产物。.class是 JVM 加载执行的字节码格式它保留了类名、方法签名、常量池和大部分逻辑结构但丢掉了注释、局部变量名和泛型信息。这不代表没法用——反编译工具能把字节码还原成接近原始的 Java 代码足够读懂业务逻辑和调用链。常见做法是用 JD-GUI 或 CFR 做反编译。JD-GUI 有图形界面拖进去就能看CFR 是命令行工具对 Java 8 以后的语法支持更好。我一般两个都备着JD-GUI 快速浏览CFR 处理复杂类。# 用 CFR 反编译单个 class 文件输出到指定目录 java -jar cfr.jar UserManageDaoImpl.class --outputdir ./decompiled # 批量反编译整个目录下的所有 class java -jar cfr.jar ./classes --outputdir ./decompiled --caseinsensitivefs true--outputdir指定输出路径--caseinsensitivefs true在 Windows 上避免大小写导致的路径冲突。反编译完成后你会得到一份.java文件包名和类名与原始工程一致。2.2 从反编译结果还原工程结构反编译出来的文件是散落的需要按包名放回目录树。假设反编译后看到com/webdemo/dao/UserManageDaoImpl.java那就在 IDE 里建一个 Maven 或普通 Dynamic Web Project把包结构照着建出来。webdemo/ ├── src/ │ └── com/webdemo/ │ ├── dao/ # UserManageDaoImpl 等数据访问类 │ ├── servlet/ # UserManageServlet、UserLoginServlet、ValidateCodeServlet │ ├── filter/ # UserLoginFilter │ └── util/ # 数据库连接工具类反编译后通常能看到 ├── WebContent/ │ ├── WEB-INF/ │ │ └── web.xml # Servlet 和 Filter 的映射配置 │ ├── css/ │ ├── js/ │ └── *.jsp # 登录页、用户列表页、验证码引用页 └── lib/ # 依赖的 jar 包如果反编译结果里没有web.xml需要根据 Servlet 类上的WebServlet注解或类名推断映射路径。比如UserLoginServlet大概率映射到/userLogin或/loginValidateCodeServlet映射到/validateCode。这一步是整个还原过程中最需要耐心的地方映射错了后面请求全部 404。2.3 补齐依赖与数据库脚本反编译的代码里会引用 JDBC 驱动、可能还有 JSTL、文件上传等库。看import语句就能列出依赖清单。常见的是mysql-connector-java版本根据代码里Class.forName的驱动类名判断——com.mysql.jdbc.Driver对应 5.xcom.mysql.cj.jdbc.Driver对应 8.x。数据库表结构通常能从 DAO 层的 SQL 语句反推。比如UserManageDaoImpl里如果有SELECT * FROM user WHERE username ?那至少有一张user表字段包括username、password可能还有id、email、create_time。把 SQL 语句里的字段名和占位符整理出来建表语句就出来了。-- 根据 DAO 层 SQL 反推的建表语句示例 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, email VARCHAR(100) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时注意字符集用utf8mb4避免中文乱码。如果原代码用的是utf8在 MySQL 8 下可能报错改成utf8mb4即可。3. 在 IDEA 里跑起来Servlet 映射、Filter 拦截与验证码链路3.1 配置 Tomcat 与项目 FacetIDEA 里跑 JavaWeb 项目核心是把 Web 资源目录和编译输出对接上。打开 Project Structure在 Facets 里添加 Web指定WebContent或webapp目录为 Web 资源根目录web.xml路径指向WEB-INF/web.xml。然后在 Artifacts 里添加一个 Web Application: Exploded把编译输出和 lib 依赖都放进去。# 如果项目是 Maven 结构先确保编译通过 mvn clean compile # 检查 target/classes 下是否生成了对应的 class 文件 ls target/classes/com/webdemo/servlet/Tomcat 配置里Deployment 选刚才建的 Exploded ArtifactApplication context 设为/webdemo或/。启动后访问http://localhost:8080/webdemo/看是否出现登录页。3.2 登录请求的完整链路以UserLoginServlet为例一次登录请求会经过UserLoginFilter先判断是否放行登录页和验证码请求不拦截然后UserLoginServlet读取username、password、validateCode三个参数先比对验证码再调用UserManageDaoImpl查库匹配成功则写 Session 并跳转用户列表失败则回登录页带错误信息。// UserLoginServlet 核心逻辑还原反编译后整理 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); String inputCode request.getParameter(validateCode); // 从 Session 取出验证码 Servlet 存入的随机码 String sessionCode (String) request.getSession().getAttribute(validateCode); if (inputCode null || !inputCode.equalsIgnoreCase(sessionCode)) { request.setAttribute(msg, 验证码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } UserManageDaoImpl dao new UserManageDaoImpl(); User user dao.findByUsernameAndPassword(username, password); if (user ! null) { request.getSession().setAttribute(currentUser, user); response.sendRedirect(request.getContextPath() /userList); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } }参数说明validateCode的 Session key 必须和ValidateCodeServlet里setAttribute用的 key 完全一致大小写敏感。equalsIgnoreCase让验证码不区分大小写如果原代码用equals用户输入大写就会失败这是常见的翻车点。3.3 Filter 的拦截规则与放行名单UserLoginFilter通常实现javax.servlet.Filter在doFilter里判断 Session 中是否有currentUser。没有就重定向到登录页有就chain.doFilter放行。// UserLoginFilter 放行逻辑 public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录页、登录请求、验证码、静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(userLogin) || uri.endsWith(validateCode) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } Object user request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); } else { chain.doFilter(req, resp); } }放行名单里必须包含验证码 Servlet 的路径否则登录页加载验证码图片时会被 Filter 拦下来页面显示裂图用户根本没法登录。这个坑我踩过不止一次。3.4 验证码 Servlet 的生成与校验ValidateCodeServlet负责生成随机字符、画到BufferedImage上、输出到响应流同时把字符存入 Session。// ValidateCodeServlet 核心步骤 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int width 100, height 40; BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics g image.getGraphics(); g.setColor(Color.WHITE); g.fillRect(0, 0, width, height); String chars ABCDEFGHJKLMNPQRSTUVWXYZ23456789; StringBuilder code new StringBuilder(); Random random new Random(); for (int i 0; i 4; i) { char c chars.charAt(random.nextInt(chars.length())); code.append(c); g.setColor(new Color(random.nextInt(150), random.nextInt(150), random.nextInt(150))); g.setFont(new Font(Arial, Font.BOLD, 28)); g.drawString(String.valueOf(c), 15 i * 20, 30); } request.getSession().setAttribute(validateCode, code.toString()); response.setContentType(image/jpeg); response.setHeader(Pragma, no-cache); response.setHeader(Cache-Control, no-cache); response.setDateHeader(Expires, 0); ImageIO.write(image, jpeg, response.getOutputStream()); }chars去掉了容易混淆的0/O、1/I这是实战中减少用户输入错误的常见做法。setHeader三行禁用缓存防止浏览器缓存验证码图片导致刷新无效。Session 的 key 必须和登录 Servlet 里读取的 key 一致。4. 避坑与排查还原这套源码时最容易翻车的五个地方4.1 反编译后中文注释和字符串乱码现象反编译出来的.java文件里中文字符串显示为\uXXXX或乱码。原因原始 class 文件编译时用的编码和反编译工具默认编码不一致常见是 GBK 与 UTF-8 冲突。解决CFR 加--encoding utf-8参数JD-GUI 在设置里切换编码。如果仍然乱码用native2ascii或在线工具把\uXXXX转回中文。4.2 Servlet 映射路径对不上导致 404现象Tomcat 启动正常访问登录页能打开但提交表单后 404。原因web.xml里的url-pattern和 JSP 表单action不一致或者反编译后没有web.xml注解映射路径推断错误。解决在浏览器开发者工具 Network 面板看请求的实际 URL对照web.xml或WebServlet注解逐一核对。常见的是少了项目 context path表单action应该用${pageContext.request.contextPath}/userLogin。4.3 数据库驱动版本与连接 URL 不匹配现象启动时报ClassNotFoundException: com.mysql.jdbc.Driver或No suitable driver found。原因MySQL 8 的驱动类名是com.mysql.cj.jdbc.DriverURL 需要加时区参数而代码里写的是 5.x 的类名和 URL。解决换用mysql-connector-java-8.x的 jar把驱动类名改为com.mysql.cj.jdbc.DriverURL 改为jdbc:mysql://localhost:3306/webdemo?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8。4.4 Filter 拦截了静态资源导致页面样式丢失现象登录后跳转到用户列表页页面没有 CSS 样式控制台显示 CSS 文件被重定向到登录页。原因UserLoginFilter的放行名单没有包含/css/、/js/、/images/等静态资源路径。解决在 Filter 的放行判断里加上静态资源目录或者用request.getRequestURI().contains(/static/)统一放行。更稳妥的做法是把静态资源放在 Filter 映射路径之外。4.5 验证码 Session 取不到值现象输入正确验证码仍然提示错误或者 Session 里validateCode为 null。原因ValidateCodeServlet和UserLoginServlet用的 Session key 不一致或者验证码图片请求和登录请求不在同一个 Session跨域或浏览器禁 Cookie。解决全局搜索setAttribute和getAttribute的 key 字符串确保完全一致。本地开发一般不会跨域检查浏览器是否禁用了 Cookie。5. 从能跑到能改二次开发与代码结构优化技巧把项目跑起来只是第一步真正有价值的是能在这个骨架上改出自己的功能。我一般会先做三件事把 JDBC 连接抽成工具类、把 DAO 的重复代码合并、给 Servlet 加一个 BaseServlet 统一处理编码和路径。先看 JDBC 工具类的抽取。反编译出来的代码里每个 DAO 方法可能都写了Class.forName、DriverManager.getConnection、close三件套改数据库密码要改十几个地方。抽成一个DBUtil类用静态代码块加载驱动用ThreadLocal或连接池管理连接。// DBUtil 简化版集中管理连接参数 public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/webdemo?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 按 rs - stmt - conn 顺序关闭每个都判空 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }参数说明URL里的serverTimezone必须设否则 MySQL 8 会报时区错误。close方法按ResultSet→Statement→Connection的顺序关闭每个都判空避免空指针。然后是 BaseServlet 的写法。传统 Servlet 一个类只能处理一个路径用户管理有增删改查四个操作就要写四个 Servlet。用反射根据请求参数中的method值分发到不同方法一个 Servlet 就能处理一组操作。// BaseServlet根据 method 参数反射调用对应方法 public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String methodName request.getParameter(method); if (methodName null || methodName.isEmpty()) { methodName index; } try { Method method this.getClass().getDeclaredMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); method.setAccessible(true); method.invoke(this, request, response); } catch (NoSuchMethodException e) { response.sendError(404, 方法不存在: methodName); } catch (Exception e) { throw new ServletException(e); } } }这样UserManageServlet继承BaseServlet里面写list、add、delete、update四个方法请求时带?methodlist就能路由过去。改完之后原来五个 Servlet 可以合并成两个代码量少一半维护起来也清晰。验证改动是否生效我习惯用 curl 直接打接口不经过浏览器排除前端干扰。# 先获取验证码保存 Session Cookie curl -c cookies.txt http://localhost:8080/webdemo/validateCode -o code.jpg # 带 Cookie 提交登录请求 curl -b cookies.txt -d usernameadminpassword123456validateCodeABCD \ http://localhost:8080/webdemo/userLogin -v-c保存 Cookie-b携带 Cookie-v看响应头和重定向路径。如果返回 302 且 Location 指向用户列表说明登录链路通了。这套流程我每次改完 Filter 或 Servlet 映射都会走一遍比在浏览器里点来点去快得多。从那以后我每次拿到只有 class 文件的源码包都先反编译、再建工程、再用 curl 把核心链路跑通最后才动代码。希望帮到你。本文还有配套的精品资源点击获取