JSP+Servlet+JDBC+MySQL原生Java Web后台开发实战

发布时间:2026/10/7 6:03:15
JSP+Servlet+JDBC+MySQL原生Java Web后台开发实战 简介这是一套基于Java原生技术栈JSPServletJDBCMySQL开发的轻量级后台管理系统源码面向Java Web初学者与课程设计实践者帮助掌握传统B/S架构下的用户认证、CRUD操作、分页展示及文件上传等核心功能实现。资源包共92个文件包含14个可读性高的Java业务类、12个JSP页面覆盖登录、注册、产品管理主界面等、28个编译后Class文件、8个XML配置文件含web.xml与数据库连接配置以及SQL建表脚本和工程导入说明文档整体压缩包仅3.04MB结构清晰、依赖精简便于快速部署与二次开发。已有3371人学习下载配套提供技术实现说明、数据库脚本、运行必读指南及多版本IDEMyEclipse/Eclipse/IDEA适配说明特别适合夯实Servlet生命周期、Session会话控制、JDBC连接池及过滤器权限拦截等关键知识点。1. 这不是“老古董”而是能快速交付、面试硬通货、上线即用的 Java 原生后台管理系统JSP Servlet JDBC MySQL 四件套真能跑通分页、登录、注册、图片上传全流程你可能在招聘JD里反复刷到“熟悉Java Web基础技术栈”——但真正让你卡住的从来不是Spring Boot自动装配而是当服务器没装Tomcat、没配Maven、甚至连IDE都只有记事本时怎么30分钟搭出一个能登录、能查数据、能传图、能翻页的后台这个标题里的“JSPJSServletJDBCMySQL原生界面”不是怀旧是最小可行交付路径不依赖框架魔力不绕开HTTP本质不抽象掉SQL执行细节。它解决的是三类人的真实痛点校招学生需要拿得出手的、能讲清每行代码流向的毕设/作品中小团队运维资源紧张要一套改几处就能上线的轻量后台还有被Spring事务嵌套搞晕后想回炉重造HTTP请求生命周期的开发者。它不追求高并发吞吐但要求每个环节可调试、可打断点、可查日志——登录失败时你能立刻看到是request.getParameter(pwd)取空了还是ResultSet.next()没命中图片上传失败时你能确认是enctypemultipart/form-data漏写了还是DiskFileItemFactory的临时目录权限不对。这不是过时的技术而是可控性最强的Java Web地基。2. 搭建环境与项目结构从零初始化一个可运行的原生Java Web工程Tomcat 9 JDK 8 MySQL 5.72.1 环境版本选择为什么锁定 Tomcat 9 / JDK 8 / MySQL 5.7很多新手一上来就装最新版Tomcat 10或JDK 17结果跑JSP报javax.servlet.*找不到——因为Tomcat 10默认使用Jakarta EE 9命名空间jakarta.servlet.*而JSP标准库仍大量沿用javax.*包名。血泪经验生产级稳定性和教程兼容性优先选Tomcat 9.0.x如9.0.85、JDK 8u202非最新u392、MySQL 5.7.44。这三个版本组合在阿里云ECS、腾讯云CVM上实测部署成功率超95%且所有主流教材、Stack Overflow高赞答案、GitHub老项目源码均基于此组合。JDK 8保证java.util.Date和SimpleDateFormat线程安全问题可控虽不推荐但原生项目里常见MySQL 5.7对GROUP BY宽松模式支持更好避免sql_modeonly_full_group_by导致查询直接报错Tomcat 9对web.xml的DTD声明兼容性最稳不会因servlet-mapping顺序问题拒绝启动。提示不要用IDE一键创建“Dynamic Web Project”——它会自动生成一堆你根本用不到的META-INF/MANIFEST.MF、WebContent/WEB-INF/lib/冗余jar。我们手动建结构掌控权在自己手里。2.2 手动构建标准Web应用目录结构无IDE依赖核心目录树必须严格遵循Servlet规范否则Tomcat无法识别myadmin/ ├── WEB-INF/ │ ├── web.xml ← 必须存在且是XML格式非web.xml~备份 │ └── lib/ ← 手动放mysql-connector-java-5.1.47.jar别用8.x ├── index.jsp ← 首页含登录表单 ├── login.jsp ← 登录成功跳转页含欢迎信息 ├── userlist.jsp ← 分页用户列表页 ├── upload.jsp ← 图片上传表单页 ├── js/ ← 存放所有.js文件login.js, page.js等 │ ├── login.js │ └── page.js ├── images/ ← 用户上传图片存放目录需Tomcat有写权限 └── WEB-INF/classes/ ← 编译后的.class文件输出目录后续配置build path指向这里关键点说明web.xml必须放在WEB-INF/下且根节点为web-appversion3.0Tomcat 9支持兼容性好lib/下只放一个JDBC驱动mysql-connector-java-5.1.47.jar5.1.x系列对JDK 8兼容性最佳8.x驱动在原生JSP中常因ClassLoader问题报ClassNotFoundExceptionimages/目录不能放在WEB-INF/内该目录受容器保护外部无法访问必须放在WebRoot同级确保img srcimages/xxx.jpg能被浏览器直接加载WEB-INF/classes/是编译输出目标不是源码目录——Java源文件.java应放在src/自建编译后.class才进classes/。2.3 配置Tomcat并验证部署流程不靠IDE纯命令行下载Tomcat 9.0.85zip包解压到/opt/tomcatLinux或C:\tomcatWindows设置环境变量CATALINA_HOME/opt/tomcatLinux或set CATALINA_HOMEC:\tomcatWindows启动Tomcat$CATALINA_HOME/bin/startup.shLinux或%CATALINA_HOME%\bin\startup.batWindows将你的myadmin/整个目录复制到$CATALINA_HOME/webapps/下注意不是war包是文件夹访问http://localhost:8080/myadmin/若看到index.jsp内容说明部署成功。注意如果页面空白或404请立即检查Tomcat日志——$CATALINA_HOME/logs/catalina.out。常见错误是SEVERE: Error starting static Resources原因多为myadmin/目录权限不足Linux下chmod -R 755 myadmin或Windows下路径含中文/空格。3. 核心功能实现从登录验证到分页查询手写Servlet与JDBC逻辑链3.1 登录功能JSP表单 Servlet验证 Session状态管理index.jsp登录表单关键代码!-- 注意form action必须指向Servlet映射路径不是.jsp -- form actionLoginServlet methodpost input typetext nameusername placeholder用户名 required input typepassword namepassword placeholder密码 required button typesubmit登录/button /formLoginServlet.java处理登录逻辑WebServlet(/LoginServlet) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); // 从表单取值 String password request.getParameter(password); // 1. JDBC连接生产环境应抽离为工具类此处为演示 Connection conn null; PreparedStatement ps null; ResultSet rs null; try { Class.forName(com.mysql.jdbc.Driver); // MySQL 5.x驱动类名 conn DriverManager.getConnection( jdbc:mysql://localhost:3306/myadmin?useSSLfalseserverTimezoneUTC, root, 123456); // 2. 参数化查询防SQL注入绝对不用String.format拼接SQL String sql SELECT id, username, email FROM users WHERE username? AND password?; ps conn.prepareStatement(sql); ps.setString(1, username); // 绑定参数 ps.setString(2, password); rs ps.executeQuery(); if (rs.next()) { // 登录成功 // 3. 写入Session存储用户ID和用户名避免存明文密码 HttpSession session request.getSession(); session.setAttribute(userId, rs.getInt(id)); session.setAttribute(username, rs.getString(username)); response.sendRedirect(login.jsp); // 重定向防止刷新重复提交 } else { request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(index.jsp).forward(request, response); } } catch (Exception e) { e.printStackTrace(); // 开发期保留上线需记录到log4j request.setAttribute(errorMsg, 系统繁忙请稍后再试); request.getRequestDispatcher(index.jsp).forward(request, response); } finally { // 4. 必须关闭资源顺序rs → ps → conn if (rs ! null) try { rs.close(); } catch (SQLException e) {} if (ps ! null) try { ps.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} } } }关键逻辑说明WebServlet(/LoginServlet)注解替代web.xml中servlet-mapping更简洁response.sendRedirect(login.jsp)是重定向URL变为/myadmin/login.jsp避免F5刷新重复提交表单request.getRequestDispatcher(...).forward(...)是服务器端转发URL不变适合错误提示场景密码明文存储是严重漏洞实际项目必须用BCrypt加密BCryptPasswordEncoder.encode(pwd)此处仅演示流程。3.2 分页功能手写SQL LIMIT OFFSET JSP动态渲染UserListServlet.java获取分页数据WebServlet(/UserListServlet) public class UserListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int currentPage 1; int pageSize 10; String pageParam request.getParameter(page); if (pageParam ! null !pageParam.isEmpty()) { currentPage Integer.parseInt(pageParam); } int offset (currentPage - 1) * pageSize; Connection conn null; PreparedStatement ps null; ResultSet rs null; ListUser userList new ArrayList(); int totalRecords 0; try { conn DriverManager.getConnection( jdbc:mysql://localhost:3306/myadmin?useSSLfalseserverTimezoneUTC, root, 123456); // 1. 查询总记录数用于计算总页数 ps conn.prepareStatement(SELECT COUNT(*) FROM users); rs ps.executeQuery(); if (rs.next()) { totalRecords rs.getInt(1); } // 2. 查询当前页数据MySQL语法LIMIT offset, size ps conn.prepareStatement(SELECT id, username, email, avatar FROM users ORDER BY id DESC LIMIT ?, ?); ps.setInt(1, offset); ps.setInt(2, pageSize); rs ps.executeQuery(); while (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setEmail(rs.getString(email)); user.setAvatar(rs.getString(avatar)); // 头像路径如 images/avatar_1.jpg userList.add(user); } // 3. 计算分页参数 int totalPages (int) Math.ceil((double) totalRecords / pageSize); request.setAttribute(userList, userList); request.setAttribute(currentPage, currentPage); request.setAttribute(totalPages, totalPages); request.setAttribute(totalRecords, totalRecords); request.getRequestDispatcher(userlist.jsp).forward(request, response); } catch (Exception e) { e.printStackTrace(); request.setAttribute(errorMsg, 数据加载失败); request.getRequestDispatcher(userlist.jsp).forward(request, response); } finally { // 关闭资源... } } }userlist.jsp分页HTML渲染% page importjava.util.* % % page importcom.example.User % % ListUser userList (ListUser) request.getAttribute(userList); int currentPage (int) request.getAttribute(currentPage); int totalPages (int) request.getAttribute(totalPages); int totalRecords (int) request.getAttribute(totalRecords); % table border1 trthID/thth用户名/thth邮箱/thth头像/th/tr % for (User u : userList) { % tr td% u.getId() %/td td% u.getUsername() %/td td% u.getEmail() %/td tdimg src% u.getAvatar() ! null ? u.getAvatar() : images/default.png % width40/td /tr % } % /table !-- 分页导航栏 -- div classpagination % // 上一页 if (currentPage 1) { out.print(a hrefUserListServlet?page (currentPage - 1) 上一页/a ); } else { out.print(span上一页/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) { out.print(strong i /strong ); } else { out.print(a hrefUserListServlet?page i i /a ); } } // 下一页 if (currentPage totalPages) { out.print(a hrefUserListServlet?page (currentPage 1) 下一页/a); } else { out.print(span下一页/span); } % /div p共 % totalRecords % 条记录第 % currentPage % 页 / 共 % totalPages % 页/p参数说明LIMIT ?, ?中第一个?是offset从第几条开始第二个是pageSize取多少条Math.ceil((double) totalRecords / pageSize)精确计算总页数避免整除丢页JSP中out.print()直接输出HTML比% %更灵活控制空格和换行分页链接用UserListServlet?page2Servlet通过request.getParameter(page)获取。3.3 图片上传功能Apache Commons FileUpload 实现二进制流解析upload.jsp上传表单!-- enctype必须为multipart/form-data否则request.getParameter()取不到文件字段 -- form actionUploadServlet methodpost enctypemultipart/form-data input typefile nameavatar acceptimage/* required input typehidden nameuserId value% session.getAttribute(userId) % button typesubmit上传头像/button /formUploadServlet.java处理文件上传WebServlet(/UploadServlet) public class UploadServlet extends HttpServlet { // 1. 配置上传参数重要 private static final long MAX_FILE_SIZE 1024 * 1024 * 5; // 5MB private static final long MAX_REQUEST_SIZE 1024 * 1024 * 10; // 10MB总请求 private static final String SAVE_DIR images; // 相对WebRoot的保存目录 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 2. 判断是否为文件上传请求 if (!ServletFileUpload.isMultipartContent(request)) { request.setAttribute(errorMsg, 请求格式错误必须为multipart/form-data); request.getRequestDispatcher(upload.jsp).forward(request, response); return; } // 3. 配置文件上传工厂 DiskFileItemFactory factory new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 1MB内存阈值超则写临时文件 factory.setRepository(new File(System.getProperty(java.io.tmpdir))); // 临时目录 ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(MAX_FILE_SIZE); upload.setSizeMax(MAX_REQUEST_SIZE); try { // 4. 解析请求获取所有表单项 ListFileItem items upload.parseRequest(request); String userId null; String fileName null; InputStream content null; for (FileItem item : items) { if (item.isFormField()) { // 普通字段如userId if (userId.equals(item.getFieldName())) { userId item.getString(UTF-8); } } else { // 文件字段avatar fileName new File(item.getName()).getName(); // 获取原始文件名 content item.getInputStream(); } } if (userId null || fileName null || content null) { throw new Exception(缺少必要参数); } // 5. 生成唯一文件名防止覆盖和路径遍历 String fileExt fileName.substring(fileName.lastIndexOf(.)).toLowerCase(); String safeFileName System.currentTimeMillis() _ userId fileExt; String uploadPath request.getServletContext().getRealPath() File.separator SAVE_DIR; // 6. 创建目录如果不存在 File uploadDir new File(uploadPath); if (!uploadDir.exists()) { uploadDir.mkdirs(); } // 7. 写入文件 File uploadedFile new File(uploadDir, safeFileName); Files.copy(content, uploadedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); // 8. 更新数据库中用户头像路径相对路径如 images/1678901234567_1.jpg updateAvatarInDB(userId, SAVE_DIR / safeFileName); request.setAttribute(successMsg, 上传成功); request.getRequestDispatcher(upload.jsp).forward(request, response); } catch (SizeLimitExceededException e) { request.setAttribute(errorMsg, 文件大小超过5MB限制); request.getRequestDispatcher(upload.jsp).forward(request, response); } catch (Exception e) { e.printStackTrace(); request.setAttribute(errorMsg, 上传失败 e.getMessage()); request.getRequestDispatcher(upload.jsp).forward(request, response); } } private void updateAvatarInDB(String userId, String avatarPath) throws SQLException { Connection conn null; PreparedStatement ps null; try { conn DriverManager.getConnection( jdbc:mysql://localhost:3306/myadmin?useSSLfalseserverTimezoneUTC, root, 123456); ps conn.prepareStatement(UPDATE users SET avatar? WHERE id?); ps.setString(1, avatarPath); ps.setString(2, userId); ps.executeUpdate(); } finally { if (ps ! null) try { ps.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} } } }关键避坑点enctypemultipart/form-data是硬性要求漏写则ServletFileUpload.isMultipartContent()返回falsefileName必须用new File(item.getName()).getName()提取否则IE浏览器可能传入完整路径如C:\fakepath\avatar.jpg直接存会导致目录穿越request.getServletContext().getRealPath()获取Web应用根目录绝对路径确保images/目录可写Files.copy()替代传统FileOutputStream自动处理流关闭更安全。4. 数据库设计与JDBC连接池MySQL建表脚本与DBCP2连接复用实践4.1 MySQL建表语句含索引与注释-- 创建数据库字符集utf8mb4支持emoji CREATE DATABASE IF NOT EXISTS myadmin CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE myadmin; -- 用户表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密密码, email VARCHAR(100) COMMENT 邮箱, avatar VARCHAR(200) COMMENT 头像路径如 images/avatar_1.jpg, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, INDEX idx_username (username), -- 用户名查询加速 INDEX idx_email (email) -- 邮箱查询加速 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户信息表; -- 插入测试数据密码为BCrypt加密后的$2a$10$...格式 INSERT INTO users (username, password, email, avatar) VALUES (admin, $2a$10$QcNqVzXZvYbKfLmNpOqRrSsTtUuVvWwXxYyZzAaBbCcDdEeFfGgHhIi, adminexample.com, images/admin.jpg), (test, $2a$10$AbCdEfGhIjKlMnOpQrStUvWxYz123456789012345678901234567890, testexample.com, NULL);执行说明utf8mb4是MySQL 5.7推荐字符集utf8在MySQL中实际是utf8mb3不支持4字节Unicode如微信表情INDEX显式声明索引避免WHERE username?全表扫描密码字段长度设为100足够存BCrypt哈希值通常60字符ON UPDATE CURRENT_TIMESTAMP让updated_at自动更新无需代码干预。4.2 从DriverManager到DBCP2连接池为什么原生JDBC必须升级每次HTTP请求都DriverManager.getConnection()创建新连接会导致MySQL默认最大连接数151100并发请求直接报Too many connectionsTCP三次握手认证耗时单次连接建立约50~200ms频繁GCConnection对象JVM压力陡增。引入DBCP2连接池commons-dbcp2-2.9.0.jarcommons-pool2-2.11.1.jar将两个jar包放入WEB-INF/lib/在WEB-INF/classes/下新建db.properties# 数据库连接配置 jdbc.urljdbc:mysql://localhost:3306/myadmin?useSSLfalseserverTimezoneUTCallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456 jdbc.driverClassNamecom.mysql.jdbc.Driver # 连接池配置 initialSize5 maxTotal20 maxIdle10 minIdle5 maxWaitMillis5000 timeBetweenEvictionRunsMillis30000 minEvictableIdleTimeMillis60000 testOnCreatefalse testOnBorrowtrue testOnReturnfalse validationQuerySELECT 1创建DBUtil.java工具类public class DBUtil { private static BasicDataSource dataSource; static { Properties props new Properties(); try (InputStream is DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { props.load(is); dataSource new BasicDataSource(); dataSource.setUrl(props.getProperty(jdbc.url)); dataSource.setUsername(props.getProperty(jdbc.username)); dataSource.setPassword(props.getProperty(jdbc.password)); dataSource.setDriverClassName(props.getProperty(jdbc.driverClassName)); // 读取连接池参数 dataSource.setInitialSize(Integer.parseInt(props.getProperty(initialSize))); dataSource.setMaxTotal(Integer.parseInt(props.getProperty(maxTotal))); dataSource.setMaxIdle(Integer.parseInt(props.getProperty(maxIdle))); dataSource.setMinIdle(Integer.parseInt(props.getProperty(minIdle))); dataSource.setMaxWaitMillis(Long.parseLong(props.getProperty(maxWaitMillis))); dataSource.setTimeBetweenEvictionRunsMillis( Long.parseLong(props.getProperty(timeBetweenEvictionRunsMillis))); dataSource.setMinEvictableIdleTimeMillis( Long.parseLong(props.getProperty(minEvictableIdleTimeMillis))); dataSource.setTestOnBorrow(Boolean.parseBoolean(props.getProperty(testOnBorrow))); dataSource.setValidationQuery(props.getProperty(validationQuery)); } catch (Exception e) { throw new RuntimeException(初始化DBCP2连接池失败, e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) try { rs.close(); } catch (SQLException e) {} if (ps ! null) try { ps.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} // 归还连接到池非真正关闭 } }使用示例替换原DriverManager.getConnection()Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); // 从此处获取连接 ps conn.prepareStatement(SELECT * FROM users WHERE id?); ps.setInt(1, userId); rs ps.executeQuery(); // ...业务逻辑 } finally { DBUtil.close(conn, ps, rs); // 归还连接 }参数调优建议maxTotal20中小项目够用按峰值QPS × 平均SQL耗时秒估算如100QPS × 0.2s 20testOnBorrowtrue每次借连接前执行SELECT 1检测有效性避免拿到失效连接timeBetweenEvictionRunsMillis30000每30秒检查一次空闲连接是否超时及时清理。5. 避坑指南JSPServletJDBC开发中90%人踩过的5个致命陷阱5.1 现象JSP页面中文乱码显示为??或方块原因JSP文件本身编码不是UTF-8记事本默认ANSITomcat默认ISO-8859-1解码POST参数MySQL连接URL未指定characterEncodingutf8。解决确保所有.jsp、.java文件用UTF-8保存IDE中右键文件→Properties→Text file encoding→UTF-8在web.xml中添加全局编码过滤器或在每个Servlet开头加request.setCharacterEncoding(UTF-8)filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingJDBC URL强制指定jdbc:mysql://...?characterEncodingutf8useSSLfalse注意是utf8不是utf8mb4驱动层面兼容。5.2 现象图片上传后img src...显示404原因images/目录权限不足Linux下Tomcat用户无写权限request.getServletContext().getRealPath()返回路径含空格或中文导致File构造失败上传路径写成绝对路径如/var/tomcat/webapps/myadmin/images/xxx.jpg但JSP中src用相对路径引用。解决Linux下执行chown -R tomcat:tomcat /opt/tomcat/webapps/myadmin/imagesWindows下右键images文件夹→属性→安全→编辑→添加Users组并勾选“写入”永远用相对路径存储数据库存images/xxx.jpgJSP中img src% user.getAvatar() %验证路径在Servlet中System.out.println(Save path: uploadPath);打印实际路径。5.3 现象分页时第1页正常第2页数据为空原因LIMIT ?, ?参数顺序写反把pageSize当offsetcurrentPage从0开始计算应从1开始MySQLOFFSET值超过总记录数时返回空结果集而非报错。解决严格按ps.setInt(1, offset); ps.setInt(2, pageSize);顺序绑定int offset (currentPage - 1) * pageSize;确保第1页offset0在SQL执行前加日志System.out.println(Query with offset offset , size pageSize);。5.4 现象登录成功后session.getAttribute(username)为null原因request.getSession()未传true参数且之前无Session时返回nullresponse.sendRedirect()后新请求的Session与原Session不同重定向是新请求web.xml中session-configsession-timeout30/session-timeout/session-config设为0或负数Session永不过期但可能被GC回收。解决登录Servlet中必须用request.getSession(true)true表示无则创建sendRedirect()没问题因为Session ID通过Cookie传递新请求仍能获取同一Session检查浏览器Cookie是否禁用或Tomcat配置Context cookiestrue默认true。5.5 现象ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 8.x驱动类名已改为com.mysql.cj.jdbc.Driver但代码仍用5.x的com.mysql.jdbc.Drivermysql-connector-java-8.0.x.jar放在WEB-INF/lib/但Tomcat 9默认ClassLoader隔离需将jar移至$CATALINA_HOME/lib/Maven项目误将scope设为test导致打包时未包含。解决坚持用mysql-connector-java-5.1.47.jar适配JDK 8 Tomcat 9确认jar包在WEB-INF/lib/下且Tomcat启动时日志有Deploying web application directory ... myadmin在Class.forName()前加System.out.println(Driver path: ClassLoader.getSystemResource(com/mysql/jdbc/Driver.class));验证类路径。6. 进阶技巧让原生Java Web系统更健壮、可维护、易调试的4个实战习惯6.1 把JSP当作纯视图层禁止在JSP中写Java业务逻辑这是原生开发最容易失控的点。我见过太多项目在userlist.jsp里直接写% // ❌ 千万别这样 Connection conn DriverManager.getConnection(...); PreparedStatement ps conn.prepareStatement(SELECT * FROM users...); ResultSet rs ps.executeQuery(); while(rs.next()) { ... } %后果是无法单元测试JSPSQL错误堆栈定位到.jsp行号调试成本翻倍页面修改需重启Tomcat开发效率极低。正确做法JSP只做三件事% %输出变量、c:forEach遍历集合、c:if条件渲染所有数据获取、计算、格式化全部在Servlet或独立Service类中完成使用JSTL标签库c:forEach替代% for(...) { %强制分离逻辑与视图。我的习惯新建src/com/example/service/UserService.java所有数据库操作封装在此Servlet只调userService.listUsers(page, size)。这样改分页逻辑只需动ServiceJSP一行不动。6.2 日志代替System.out.println()用Log4j2实现分级、滚动、异步输出System.out.println(Login success for user: username)在生产环境是灾难无法关闭无时间戳、无线程ID、无类名排查问题如大海捞针高并发下println锁竞争拖慢整个应用。Log4j2配置WEB-INF/classes/log4j2.xml?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders RollingFile nameRollingFile fileNamelogs/myadmin.log filePatternlogs/$${date:yyyy-MM}/myadmin-%d{MM-dd-yyyy}-%i.log.gz PatternLayout Pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/Pattern /PatternLayout Policies TimeBasedTriggeringPolicy / SizeBasedTriggeringPolicy size10 MB/ /Policies DefaultRolloverStrategy max30/ /RollingFile /Appenders Loggers Root levelinfo AppenderRef refRollingFile/ /Root /Loggers /Configuration在Servlet中使用import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class LoginServlet extends HttpServlet { private static final Logger logger LogManager.getLogger(LoginServlet.class); protected void doPost(...) { logger.info(Login attempt p a hrefhttps://download.csdn.net/download/qq_34715954/19402578 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p