
简介这是一套基于Java原生技术栈JSPServletJDBCMySQL开发的轻量级后台管理系统源码面向Java Web初学者与课程设计实践者帮助掌握传统B/S架构下的用户认证、CRUD操作、分页及文件上传等核心功能实现。资源包共92个文件含14个可读性高的Java业务类、12个JSP页面覆盖登录、注册、产品管理等交互视图、28个编译后Class文件、8个XML配置文件含web.xml与数据库连接配置以及SQL建表脚本、开发环境说明文档和工程导入指南整体压缩包仅3.04MB结构清晰、依赖精简。已有3371人学习下载适合在MyEclipse/EclipseTomcat7JDK1.7环境下快速部署运行完整呈现过滤器权限控制、Session会话管理、JDBC直连MySQL等底层逻辑是理解Java Web基础开发范式的典型教学案例。1. 这不是“过时技术堆砌”而是用原生 Java Web 技术栈落地一个可交付、可调试、可交接的后台管理系统分页、登录注册、图片上传全链路闭环你可能在招聘JD里看到“熟悉JSP/Servlet/JDBC”被列为加分项在面试中被问“如果不用Spring怎么实现用户登录校验”甚至在老项目维护现场面对一个没有Maven、没有XML配置、连web.xml都手写的.war包发呆——这不是考古是真实产线场景。这个标题指向的是一套不依赖任何框架封装、完全基于Java EE规范原生能力构建的后台管理系统源码它用JSP做视图层渲染JavaScript做前端交互与表单校验Servlet处理HTTP请求流转JDBC直连MySQL完成CRUD所有功能模块含带页码跳转的真分页、密码加盐注册、服务端图片尺寸校验与路径存储全部可本地一键启动、断点调试、逐行跟踪。它适合三类人刚学完Java基础想打通Web全流程的新手需要快速交付轻量级内部工具的外包或驻场工程师以及必须接手遗留系统、需要理解“框架之下到底发生了什么”的中级开发者。它不追求炫酷UI但每个按钮点击后request如何进servlet、session如何存用户态、ResultSet怎么映射成List、base64图片怎么转File再存磁盘——全部裸露可见。接下来我将带你从零复现这个系统不跳过任何一个% %和Class.forName()。2. 搭建最小可运行环境JDK 8 Tomcat 9 MySQL 5.7 的三件套配平实操要跑通这套源码环境不是“装上就行”而是必须卡死版本、对齐驱动、绕开默认陷阱。我见过太多人卡在“ClassNotFoundException: com.mysql.cj.jdbc.Driver”或“HTTP Status 404 – /login”上两小时——问题往往不出在代码而出在环境缝合处。2.1 JDK 8 与 Tomcat 9 的强绑定逻辑这套系统基于 Servlet 3.1 和 JSP 2.3 规范而 Tomcat 9 正好完整支持。Tomcat 10 已升级到 Jakarta EE 9所有包名从javax.*变为jakarta.*会导致你的web.xml中servlet-classcom.example.LoginServlet/servlet-class突然报错——因为HttpServlet类路径变了。所以必须用 Tomcat 9.x推荐 9.0.85。JDK 必须是 8u202 或更高但低于 8u301因后续版本移除了部分旧API原因在于JDBC 驱动mysql-connector-java-5.1.47.jar在 JDK 9 的模块化机制下会拒绝加载com.mysql.jdbc.Driver已被标记为 deprecated。验证方式解压 Tomcat 后在bin/catalina.shLinux/Mac或catalina.batWindows顶部添加# Linux/Mac catalina.sh 开头追加 export JAVA_HOME/path/to/jdk1.8.0_202 export JRE_HOME$JAVA_HOME/jre提示Windows 用户务必用set JAVA_HOMEC:\Program Files\Java\jdk1.8.0_202不要用带空格的路径如Program Files (x86)否则 Tomcat 启动脚本会解析失败。2.2 MySQL 5.7 安装与 JDBC 驱动精准匹配MySQL 5.7 是关键分水岭它默认关闭ONLY_FULL_GROUP_BY模式且datetime类型支持毫秒精度这对分页查询的ORDER BY create_time DESC排序稳定性至关重要。安装时务必勾选“Add MySQL to PATH”和“Install as Windows Service”Linux 用户用systemctl enable mysqld。初始化 root 密码后立即执行-- 创建专用数据库避免字符集污染 CREATE DATABASE admin_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建应用用户禁止 root 直连 CREATE USER admin_applocalhost IDENTIFIED BY Admin123; GRANT SELECT, INSERT, UPDATE, DELETE ON admin_system.* TO admin_applocalhost; FLUSH PRIVILEGES;JDBC 驱动必须用mysql-connector-java-5.1.47.jar非 8.x 版本。为什么因为本系统 Servlet 中写的是// LoginServlet.java 片段 Class.forName(com.mysql.jdbc.Driver); // 注意是 com.mysql.jdbc不是 com.mysql.cj.jdbc String url jdbc:mysql://localhost:3306/admin_system?useUnicodetruecharacterEncodingUTF-8; Connection conn DriverManager.getConnection(url, admin_app, Admin123);若你误用mysql-connector-java-8.0.33.jarClass.forName(com.mysql.jdbc.Driver)会直接抛ClassNotFoundException——因为 8.x 驱动已废弃该类只保留com.mysql.cj.jdbc.Driver。解决方法下载mysql-connector-java-5.1.47.jar放入WEB-INF/lib/目录不是 Tomcat 的lib/并在web.xml中确认无resource-ref配置本系统不用 JNDI。2.3 项目结构手工还原拒绝IDE自动识别用最笨但最稳的方式本源码是纯手工 WAR 结构无pom.xml因此不能靠 IDEA “Import Project” 自动解析。必须手动创建目录树admin-system/ ├── WEB-INF/ │ ├── web.xml # 核心servlet mapping 和 welcome-file-list │ ├── lib/ │ │ └── mysql-connector-java-5.1.47.jar │ └── classes/ # 编译后的 .class 文件存放处 │ └── com/example/ │ ├── servlet/ │ │ ├── LoginServlet.class │ │ ├── UploadServlet.class │ │ └── ListUserServlet.class │ └── dao/ │ └── UserDao.class ├── login.jsp # 登录入口页面 ├── index.jsp # 主页需登录后访问 ├── upload.jsp # 图片上传页 └── css/ js/ images/ # 静态资源关键动作将 Java 源码.java文件放在src/目录自建用命令行编译# 假设你在 admin-system 根目录 mkdir -p WEB-INF/classes javac -d WEB-INF/classes -cp WEB-INF/lib/mysql-connector-java-5.1.47.jar src/com/example/servlet/*.java src/com/example/dao/*.java注意-cp参数必须包含 JDBC 驱动否则UserDao.java中的import java.sql.*会编译失败。-d指定输出目录为WEB-INF/classes确保类路径与运行时一致。3. 登录与注册功能落地从密码加盐到 Session 管理的全链路手写实现登录注册看似简单但原生实现中藏着三个致命细节密码不可逆存储、Session 跨请求绑定、注册时的唯一性约束回滚。框架帮你做了而这里你要亲手写try-catch-finally里的每一步。3.1 注册流程用 SHA-256 加盐存储密码拒绝明文与弱哈希本系统注册不走BCrypt需额外jar而是用 JDK 原生MessageDigest实现加盐 SHA-256。关键在“盐值”生成——不是固定字符串而是为每个用户生成随机 16 字节 salt// RegisterServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) { String username request.getParameter(username); String password request.getParameter(password); // 1. 生成随机 salt每次注册都不同 SecureRandom random new SecureRandom(); byte[] salt new byte[16]; random.nextBytes(salt); // 2. 拼接 password salt计算 SHA-256 MessageDigest md MessageDigest.getInstance(SHA-256); md.update(password.getBytes(StandardCharsets.UTF_8)); md.update(salt); // 关键salt 追加到密码后 byte[] hash md.digest(); // 3. 将 salt 和 hash 一起存入数据库用 Base64 编码便于存储 String saltBase64 Base64.getEncoder().encodeToString(salt); String hashBase64 Base64.getEncoder().encodeToString(hash); UserDao userDao new UserDao(); boolean success userDao.register(username, hashBase64, saltBase64); // ... 跳转逻辑 }数据库user表结构必须包含salt VARCHAR(24)字段Base64 编码后长度固定为24。验证登录时从DB查出该用户的salt再用同样逻辑重新计算 hash 对比——这保证了即使两个用户密码相同hash 值也完全不同。3.2 登录校验手动管理 HttpSession拒绝“登录后刷新页面变未登录”很多新手以为request.getSession()就万事大吉却忽略了HttpSession的失效策略和 URL 重写问题。本系统在LoginServlet中强制设置// LoginServlet.java HttpSession session request.getSession(true); // true 表示不存在则创建 session.setMaxInactiveInterval(30 * 60); // 30分钟无操作自动失效 session.setAttribute(username, username); // 存用户名非密码 session.setAttribute(loginTime, System.currentTimeMillis()); // 关键禁用 URL 重写避免 ?jsessionidxxx 暴露在地址栏 response.setHeader(Cache-Control, no-cache, no-store, must-revalidate); response.setHeader(Pragma, no-cache); response.setDateHeader(Expires, 0);前端 JSP 页面如index.jsp必须用session.getAttribute(username)判断登录态而非读 cookie% String username (String) session.getAttribute(username); if (username null) { response.sendRedirect(login.jsp); // 302重定向非 forward return; } % 欢迎% username %注意response.sendRedirect()是客户端跳转会发起新请求确保session在新请求中仍有效而request.getRequestDispatcher().forward()是服务器端跳转若 session 未正确创建forward 后页面仍可能取不到username。3.3 数据库唯一性约束与事务回滚注册时用户名重复的优雅处理UserDao.register()方法必须用 JDBC 事务包裹并捕获SQLIntegrityConstraintViolationExceptionpublic boolean register(String username, String passwordHash, String salt) { String sql INSERT INTO user (username, password_hash, salt, create_time) VALUES (?, ?, ?, NOW()); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); // 关闭自动提交 ps.setString(1, username); ps.setString(2, passwordHash); ps.setString(3, salt); int rows ps.executeUpdate(); conn.commit(); // 显式提交 return rows 1; } catch (SQLIntegrityConstraintViolationException e) { // 捕获唯一索引冲突username 已存在 try { conn.rollback(); // 回滚事务 } catch (SQLException ex) { log.error(rollback failed, ex); } return false; // 告知上层注册失败 } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { log.error(rollback failed, ex); } throw new RuntimeException(e); } }数据库user.username字段必须建唯一索引ALTER TABLE user ADD UNIQUE INDEX uk_username (username);否则SQLIntegrityConstraintViolationException永远不会抛出重复注册会静默成功导致数据污染。4. 分页功能实现手写 LIMIT OFFSET 的真分页拒绝前端假分页所谓“真分页”是指每次请求只从 MySQL 拉取当前页所需数据如第3页每页10条 →LIMIT 20,10而非把全表查出来再用 JavaList.subList()截取。后者在万级数据时直接 OOM。本系统分页由ListUserServlet和list_user.jsp协同完成。4.1 Servlet 层计算 offset 并传递总记录数// ListUserServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) { // 1. 获取页码默认第1页 String pageStr request.getParameter(page); int currentPage 1; if (pageStr ! null !pageStr.trim().isEmpty()) { currentPage Integer.parseInt(pageStr); } int pageSize 10; // 每页10条 int offset (currentPage - 1) * pageSize; // 2. 查询当前页数据 UserDao userDao new UserDao(); ListUser userList userDao.listUsersByPage(offset, pageSize); // 3. 查询总记录数用于计算总页数 int totalCount userDao.getTotalCount(); int totalPages (int) Math.ceil((double) totalCount / pageSize); // 4. 将数据存入 request供 JSP 使用 request.setAttribute(userList, userList); request.setAttribute(currentPage, currentPage); request.setAttribute(totalPages, totalPages); request.setAttribute(totalCount, totalCount); request.getRequestDispatcher(list_user.jsp).forward(request, response); }UserDao.listUsersByPage()的 SQL 必须用LIMIT ?, ?占位符public ListUser listUsersByPage(int offset, int limit) { String sql SELECT id, username, create_time FROM user ORDER BY create_time DESC LIMIT ?, ?; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, limit); ResultSet rs ps.executeQuery(); // ... 将 rs 映射为 User 对象列表 } }4.2 JSP 层动态生成分页 HTML支持首页/末页/跳转list_user.jsp中用 JSTLc:forEach渲染用户列表分页导航则用原生 JSP 脚本片段因本系统不引入 JSTL!-- 分页导航 -- div classpagination !-- 首页 -- a hrefListUserServlet?page1首页/a !-- 上一页 -- % if (currentPage 1) { % a hrefListUserServlet?page% currentPage - 1 %上一页/a % } else { % span classdisabled上一页/span % } % !-- 页码数字最多显示5个 -- % int startPage Math.max(1, currentPage - 2); int endPage Math.min(totalPages, currentPage 2); for (int i startPage; i endPage; i) { if (i currentPage) { % span classcurrent% i %/span % } else { % a hrefListUserServlet?page% i %% i %/a % } } % !-- 下一页 -- % if (currentPage totalPages) { % a hrefListUserServlet?page% currentPage 1 %下一页/a % } else { % span classdisabled下一页/span % } % !-- 末页 -- a hrefListUserServlet?page% totalPages %末页/a !-- 跳转输入框 -- form actionListUserServlet methodget styledisplay:inline; input typenumber namepage min1 max% totalPages % value% currentPage % stylewidth:50px; input typesubmit value跳转 /form /div提示% currentPage %输出当前页码% totalPages %输出总页数。min/max属性限制输入范围防止用户输入非法页码导致 SQL 错误。4.3 避坑分页参数校验与 SQL 注入防护这是新手最容易翻车的环节。必须在ListUserServlet开头加入强校验// ListUserServlet.java 开头 String pageStr request.getParameter(page); if (pageStr null || !pageStr.matches(\\d)) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, 页码必须为正整数); return; } int currentPage Integer.parseInt(pageStr); if (currentPage 1) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, 页码不能小于1); return; } // 同时校验 pageSize若支持前端传 String sizeStr request.getParameter(size); int pageSize 10; if (sizeStr ! null sizeStr.matches(\\d) Integer.parseInt(sizeStr) 0) { pageSize Math.min(Integer.parseInt(sizeStr), 100); // 限制最大100条/页 }为什么必须校验因为offset直接拼入 SQL若page1; DROP TABLE user; --虽本系统用PreparedStatement但若你误写成String sql SELECT ... LIMIT offset , pageSize;就彻底 SQL 注入。而Integer.parseInt()会直接抛NumberFormatException拦截恶意字符串。5. 图片上传功能从 form enctype 到服务端文件保存的完整链路图片上传不是input typefile加个enctypemultipart/form-data就完事。本系统用原生ServletInputStream解析 multipart不依赖commons-fileupload全程可控。5.1 前端表单必须声明 enctype 和 accept 属性!-- upload.jsp -- form actionUploadServlet methodpost enctypemultipart/form-data input typefile nameavatar acceptimage/* required input typetext nameusername placeholder关联用户名 required button typesubmit上传头像/button /form关键点enctypemultipart/form-data告诉浏览器用二进制流发送而非 URL 编码。acceptimage/*前端过滤但不可信服务端必须二次校验。nameavatar服务端通过此 name 获取文件流。5.2 Servlet 解析手动解析 multipart boundary提取文件名与内容UploadServlet不用PartAPIServlet 3.0而是用ServletInputStream手动解析兼容性更强protected void doPost(HttpServletRequest request, HttpServletResponse response) { try (ServletInputStream sis request.getInputStream()) { // 1. 读取整个请求体小图可接受大图需流式处理 byte[] body sis.readAllBytes(); // 2. 提取 boundary从 Content-Type 头获取 String contentType request.getContentType(); String boundary getBoundary(contentType); // 如 ----WebKitFormBoundaryabc123 // 3. 按 boundary 分割找到 avatar 字段 byte[] boundaryBytes (-- boundary).getBytes(StandardCharsets.UTF_8); int start indexOf(body, boundaryBytes, 0); int end indexOf(body, boundaryBytes, start 1); // 4. 提取 filename在 Content-Disposition 头中 String headerSection new String(body, start, end - start, StandardCharsets.UTF_8); String filename extractFilename(headerSection); // 5. 提取文件二进制内容跳过 header取 \r\n\r\n 后的数据 int dataStart headerSection.indexOf(\r\n\r\n) 4; int dataEnd end - 4; // 减去 --\r\n byte[] fileData Arrays.copyOfRange(body, start dataStart, end - 4); // 6. 保存文件带安全校验 if (isImageFile(filename) fileData.length 5 * 1024 * 1024) { // 5MB 限制 String savePath saveImageFile(filename, fileData); // 更新数据库 user.avatar_path 字段 UserDao userDao new UserDao(); userDao.updateAvatarByUsername(request.getParameter(username), savePath); } } } private String getBoundary(String contentType) { if (contentType null) return ; String[] parts contentType.split(;); for (String part : parts) { if (part.trim().startsWith(boundary)) { return part.trim().substring(boundary.length()).trim(); } } return ; }注意indexOf是自定义方法用Arrays.equals()比较字节数组extractFilename()用正则filename(.?);?提取。5.3 文件安全保存校验 MIME 类型与扩展名拒绝 .jsp 伪装服务端校验必须做两层扩展名白名单只允许.jpg,.jpeg,.png,.gifMIME 类型校验用URLConnection.guessContentTypeFromStream()读取文件头private boolean isImageFile(String filename) { String ext getFileExtension(filename).toLowerCase(); if (!Arrays.asList(jpg, jpeg, png, gif).contains(ext)) { return false; } // 读取文件头 32 字节判断真实类型 try (ByteArrayInputStream bais new ByteArrayInputStream(fileData)) { String mimeType URLConnection.guessContentTypeFromStream(bais); return image/jpeg.equals(mimeType) || image/png.equals(mimeType) || image/gif.equals(mimeType); } catch (IOException e) { return false; } } private String saveImageFile(String filename, byte[] data) { String ext getFileExtension(filename); String newFilename System.currentTimeMillis() _ UUID.randomUUID().toString().substring(0, 8) . ext; String uploadDir getServletContext().getRealPath(/uploads/); File dir new File(uploadDir); if (!dir.exists()) dir.mkdirs(); File file new File(dir, newFilename); try (FileOutputStream fos new FileOutputStream(file)) { fos.write(data); return /uploads/ newFilename; // 返回 Web 访问路径 } catch (IOException e) { throw new RuntimeException(保存文件失败, e); } }数据库user表需新增avatar_path VARCHAR(255)字段JSP 中用img src% user.getAvatarPath() %显示。6. 避坑指南5 个血泪经验总结省下你 3 天调试时间这些不是文档里写的“注意事项”而是我在客户现场、外包项目、学生作业中反复踩过的坑每一条都附带现象、根因和可复制的解决方案。6.1 现象登录成功后跳转index.jsp但页面显示usernamenull原因LoginServlet中调用了response.sendRedirect(index.jsp)但sendRedirect()是客户端重定向会发起全新 HTTP 请求而新请求的session是空的——因为你没在重定向前把username存入 session或者session因 Cookie 设置问题未被浏览器携带。解决确保LoginServlet中session.setAttribute(username, username)在sendRedirect()之前执行检查浏览器开发者工具 → Application → Cookies确认JSESSIONIDCookie 的Domain和Path是否匹配应为/若部署在子路径如http://localhost:8080/myapp/login.jsp需在web.xml中配置session-configcookie-path/myapp/cookie-path/session-config。6.2 现象分页点击“第2页”URL 变成ListUserServlet?page2但页面仍显示第1页数据原因ListUserServlet中request.getParameter(page)返回null因为web.xml中 servlet mapping 配置错误导致请求未进入该 Servlet。常见错误是url-pattern写成/list而不是/ListUserServlet或大小写不一致Linux 服务器区分大小写。解决检查web.xml中servlet-mapping的url-pattern是否与 JSP 中href的路径完全一致包括大小写在ListUserServletdoGet()开头加日志System.out.println(page param: request.getParameter(page));确认是否收到参数若用 Tomcat 9确保web.xml的DOCTYPE声明为!DOCTYPE web-app PUBLIC -//Sun Microsystems, Inc.//DTD Web Application 2.3//EN http://java.sun.com/dtd/web-app_2_3.dtd否则某些旧版解析器会忽略url-pattern。6.3 现象图片上传后img标签 404路径为/uploads/1712345678901_abc123.jpg但文件实际在tomcat/webapps/ROOT/uploads/原因getServletContext().getRealPath(/uploads/)返回的是 Tomcat 的物理路径但/uploads/是 Web 应用根路径下的虚拟目录而webapps/ROOT/是默认应用若你的 WAR 包名为admin-system.war则应用上下文路径是/admin-system/uploads/应相对于该路径。解决在web.xml中配置welcome-file-list后确认应用部署路径Tomcat Manager 中看 Context PathsaveImageFile()中返回的路径必须与应用上下文匹配return request.getContextPath() /uploads/ newFilename;确保webapps/admin-system/uploads/目录存在且 Tomcat 进程有写权限Linux 用chown -R tomcat:tomcat webapps/admin-system/uploads。6.4 现象MySQL 报错java.sql.SQLException: The server time zone value йʱ is unrecognized原因MySQL 5.7 默认时区为系统时区中文 Windows 显示为“中国标准时间”但 JDBC 驱动无法识别该字符串要求明确指定serverTimezone。解决修改 JDBC URL添加serverTimezoneGMT%2B8URL 编码后为GMT%2B8String url jdbc:mysql://localhost:3306/admin_system?useUnicodetruecharacterEncodingUTF-8serverTimezoneGMT%2B8;注意%2B是的 URL 编码写成GMT8会解析失败。6.5 现象JSP 页面中文乱码显示为??但数据库查出的中文正常原因JSP 文件本身编码为 GBKWindows 记事本默认但 Tomcat 默认按 ISO-8859-1 解析且response.setContentType(text/html;charsetUTF-8)未生效。解决用 VS Code 或 IDEA 以 UTF-8 编码保存所有.jsp文件右下角状态栏确认在每个 JSP 顶部添加% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%在web.xml中全局配置jsp-config jsp-property-group url-pattern*.jsp/url-pattern page-encodingUTF-8/page-encoding /jsp-property-group /jsp-config7. 进阶技巧用 Filter 统一处理登录拦截与字符编码让代码更干净写完所有功能后你会发现LoginServlet、ListUserServlet、UploadServlet里都重复着HttpSession session request.getSession(); if (session.getAttribute(username)null) {...}——这就是横切关注点该用Filter了。本系统不引入 Spring Interceptor而是用原生Filter实现 AOP。7.1 创建 LoginFilter拦截未登录请求重定向到登录页// src/com/example/filter/LoginFilter.java WebFilter(filterName LoginFilter, urlPatterns {/index.jsp, /ListUserServlet, /UploadServlet}) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); String username (session ! null) ? (String) session.getAttribute(username) : null; // 白名单登录页、注册页、静态资源放行 String requestURI req.getRequestURI(); if (username ! null || requestURI.endsWith(login.jsp) || requestURI.endsWith(register.jsp) || requestURI.startsWith(req.getContextPath() /css/) || requestURI.startsWith(req.getContextPath() /js/) || requestURI.startsWith(req.getContextPath() /images/)) { chain.doFilter(request, response); } else { resp.sendRedirect(req.getContextPath() /login.jsp); } } }WebFilter注解替代web.xml配置urlPatterns指定需拦截的路径。注意/index.jsp是 JSP 页面/ListUserServlet是 Servlet它们都需登录后访问。7.2 创建 EncodingFilter统一设置请求/响应编码终结乱码// src/com/example/filter/EncodingFilter.java WebFilter(filterName EncodingFilter, urlPatterns /*) public class EncodingFilter implements Filter { private String encoding UTF-8; Override public void init(FilterConfig config) throws ServletException { String enc config.getInitParameter(encoding); if (enc ! null !enc.trim().isEmpty()) { encoding enc.trim(); } } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; req.setCharacterEncoding(encoding); resp.setCharacterEncoding(encoding); resp.setContentType(text/html; charset encoding); chain.doFilter(request, response); } }在web.xml中配置初始化参数或用注解WebFilter(initParams {WebInitParam(nameencoding, valueUTF-8)})。7.3 Filter 链执行顺序与调试技巧多个 Filter 按WebFilter的urlPatterns匹配顺序执行但更可靠的是在web.xml中显式声明顺序filter filter-nameEncodingFilter/filter-name filter-classcom.example.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter filter-nameLoginFilter/filter-name filter-classcom.example.filter.LoginFilter/filter-class /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping filter-mapping filter-nameLoginFilter/filter-name url-pattern/index.jsp/url-pattern url-pattern/ListUserServlet/url-pattern url-pattern/UploadServlet/url-pattern /filter-mapping提示EncodingFilter必须在LoginFilter之前因为LoginFilter中req.getParameter(username)需要正确的字符编码才能取到中文。调试时在每个doFilter()开头加System.out.println(Filter X executed);观察控制台输出顺序。我当年第一次给客户交付这个系统时就在LoginFilter里漏写了requestURI.endsWith(login.jsp)导致登录页自己重定向自己浏览器疯狂跳转直到崩溃。后来养成了习惯所有 Filter 的放行逻辑必须用白名单思维而不是黑名单。希望帮到你。本文还有配套的精品资源点击获取