Java Web入门经典:Servlet+JSP+MySQL购物网站实战
简介本资源是一套基于Java Web经典技术栈ServletJSPMySQL实现的完整购物网站系统专为计算机专业学生课程设计、期末大作业及项目实战练习打造已获98分高分评价具备清晰业务逻辑与可运行性。压缩包共69个文件包含15个核心Java源码、13个JSP页面、16个编译后Class文件、11个GIF与6个JPG等静态资源辅以SQL建表脚本、CSS样式、JS交互逻辑及README说明文档整体仅292KB轻量易部署。目前已有234人学习下载适合初学Web开发的学生快速理解MVC分层结构、用户注册登录、商品浏览、购物车管理及订单处理等典型电商功能模块。资源目录结构规范含src源码、web前端页面、WEB-INF配置及数据库脚本开箱即用便于调试、二次开发与教学演示。1. 为什么一个“JavaServletJSPMySQL”的购物网站至今仍是Java Web入门最硬核的通关副本不是因为它多先进——Spring Boot三行代码起服务VueElement Plus拖拽出后台它连前端响应式都得手写CSS也不是因为它多流行——GitHub上Star过千的电商项目十有八九是Spring Cloud微服务架构。恰恰相反正因为它“土”、慢、要写一堆web.xml配置、要手动管理HttpSession生命周期、要在JSP里混写Java脚本片段哪怕只是%user.getName()%才让它成了检验Java Web基本功是否扎穿地壳的试金石。这个组合不是历史包袱而是能力显影液Servlet暴露你对HTTP协议的理解深度GET/POST区别重定向和转发的Header差异Filter链执行顺序JSP逼你直面MVC分层的真实代价什么时候该用jsp:include什么时候必须走Servlet跳转pageContext和requestScope打架时谁赢MySQL则当场验明正身——你写的那个SELECT * FROM product WHERE category_id ? AND status 1 ORDER BY sort ASC LIMIT 0,20有没有加联合索引status1在WHERE里是常量但category_id?是变量索引能用上几成它适合三类人刚学完Java基础、对着System.out.println(Hello World)还心潮澎湃的新手准备Java后端面试、被问到“请手写一个登录校验Filter”的应届生以及想给团队新人搭一个无框架依赖、可逐行调试、每一行代码都看得见来龙去脉的练兵场的Tech Lead。这不是怀旧是把Web开发的“操作系统层”重新擦亮——当你能徒手用PrintWriter写出符合HTTP/1.1规范的响应头再回看Spring MVC的ResponseBody才真正懂什么叫“封装的价值”。2. 从零搭建用最原始的方式跑通首页、商品列表与用户登录2.1 环境筑基JDK 8 Tomcat 9 MySQL 5.7 的黄金三角提示别碰JDK 17或Tomcat 10。Servlet 4.0规范强制要求HTTP/2支持而本项目中大量javax.servlet.*包在Jakarta EE 9已迁移到jakarta.servlet.*改包名会引发连锁编译失败。实测JDK 8u291 Tomcat 9.0.83 MySQL 5.7.44组合兼容性最稳报错信息最友好。安装后验证关键路径# 检查JAVA_HOME指向JDK而非JRE echo $JAVA_HOME # 输出应为 /Library/Java/JavaVirtualMachines/jdk1.8.0_291.jdk/Contents/HomeMac或 C:\Program Files\Java\jdk1.8.0_291Win # Tomcat启动后访问 http://localhost:8080看到Apache Tomcat欢迎页即成功 $CATALINA_HOME/bin/startup.sh # Linux/Mac %CATALINA_HOME%\bin\startup.bat # Windows # MySQL登录验证 mysql -u root -p # 输入密码后进入mysql命令行执行 SELECT VERSION(); 确认是5.7.x为什么坚持用MySQL 5.7datetime类型默认值支持CURRENT_TIMESTAMPMySQL 8.0要求显式声明DEFAULT CURRENT_TIMESTAMPGROUP BY语义宽松8.0严格模式下SELECT id,name FROM user GROUP BY id会报错utf8mb4字符集支持开箱即用无需额外配置my.cnf中的collation-serverutf8mb4_unicode_ci2.2 数据库建模三张表撑起购物核心product/user/order创建shopping_db数据库并执行以下建表SQL。重点观察外键约束与索引设计逻辑-- 创建数据库UTF8MB4编码确保emoji存储 CREATE DATABASE shopping_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE shopping_db; -- 用户表密码必须BCRYPT加密禁止明文 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存储BCRYPT哈希值如 $2a$10$... email VARCHAR(100), phone VARCHAR(20), created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_username (username), -- 用户名查询高频单独建索引 INDEX idx_email (email) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品表分类ID为外键状态字段驱动上下架 CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, category_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 0-下架,1-上架, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category_status (category_id, status), -- 联合索引查某分类下所有上架商品 INDEX idx_name (name(50)) -- 前缀索引避免长文本索引过大 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表用户ID与商品ID均为外键状态机驱动流程 CREATE TABLE order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-待支付,1-已支付,2-已发货,3-已完成,4-已取消, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE CASCADE, FOREIGN KEY (product_id) REFERENCES product(id) ON DELETE RESTRICT, INDEX idx_user_status (user_id, status), -- 用户订单状态查询 INDEX idx_created_time (created_time) -- 按时间倒序查最新订单 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关键设计说明ON DELETE CASCADE用于用户删除时自动清理其订单业务允许场景ON DELETE RESTRICT防止误删商品导致订单数据断裂status字段用TINYINT而非ENUM便于Java代码用switch(status)处理且迁移至其他数据库时兼容性更好所有DATETIME字段均设DEFAULT CURRENT_TIMESTAMP避免Java层手动赋值导致时区混乱2.3 Servlet层从LoginServlet开始理解请求生命周期创建src/main/java/com/shopping/servlet/LoginServlet.javapackage com.shopping.servlet; import com.shopping.dao.UserDao; import com.shopping.model.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.Objects; // WebServlet注解替代web.xml配置更简洁 WebServlet(/login) public class LoginServlet extends HttpServlet { private static final long serialVersionUID 1L; Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取请求参数注意GET请求用getParameterPOST表单也用此方法 String username req.getParameter(username); String password req.getParameter(password); // 2. 参数校验空值检查必须前置 if (username null || username.trim().isEmpty() || password null || password.trim().isEmpty()) { req.setAttribute(error, 用户名或密码不能为空); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; // 阻止后续执行 } // 3. 调用DAO查询用户此处简化实际应使用BCRYPT比对 UserDao userDao new UserDao(); User user userDao.findByUsername(username); // 4. 密码校验真实项目必须用BCRYPT.matches(password, user.getPassword()) if (user ! null Objects.equals(user.getPassword(), password)) { // 5. 登录成功将用户信息存入Session非Cookie HttpSession session req.getSession(true); // true表示不存在则创建 session.setAttribute(currentUser, user); session.setMaxInactiveInterval(30 * 60); // Session超时30分钟 // 6. 重定向到首页避免F5刷新重复提交 resp.sendRedirect(req.getContextPath() /index.jsp); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } // GET请求直接显示登录页 Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher(/login.jsp).forward(req, resp); } }为什么用sendRedirect而非forwardforward是服务器内部跳转URL地址栏不变用户刷新会导致表单重复提交浏览器重发原POST请求sendRedirect是客户端重定向URL变为新地址刷新时只GET新页面彻底规避重复提交代价是丢失request域属性req.setAttribute()设置的数据所以登录成功后用户信息必须存入session2.4 JSP层用c:forEach渲染商品列表告别% %脚本创建index.jsp必须引入JSTL标签库否则c:forEach无法解析% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html head title首页/title meta charsetUTF-8 style .product-card { border: 1px solid #eee; padding: 10px; margin: 10px 0; } .price { color: #e74c3c; font-weight: bold; } /style /head body h1欢迎来到购物网站/h1 !-- 顶部导航栏 -- div c:choose c:when test${not empty sessionScope.currentUser} span欢迎${sessionScope.currentUser.username}/span a href${pageContext.request.contextPath}/logout退出/a /c:when c:otherwise a href${pageContext.request.contextPath}/login.jsp登录/a a href${pageContext.request.contextPath}/register.jsp注册/a /c:otherwise /c:choose /div !-- 商品列表从request域获取productList -- h2热门商品/h2 c:if test${empty requestScope.productList} p暂无商品/p /c:if c:forEach items${requestScope.productList} varproduct div classproduct-card h3${product.name}/h3 p价格span classpricefmt:formatNumber value${product.price} pattern¥#,##0.00//span/p p库存${product.stock}件/p a href${pageContext.request.contextPath}/productDetail?id${product.id}查看详情/a /div /c:forEach /body /html关键细节说明fmt:formatNumber确保价格显示为¥199.00格式避免199.0这种不专业显示${pageContext.request.contextPath}动态获取应用上下文路径如部署为/shopping则生成/shopping/productDetail避免硬编码路径sessionScope.currentUser和requestScope.productList明确指定作用域防止同名变量冲突3. 数据持久化实战DAO层手写JDBC连接池与事务控制3.1 JDBC工具类用DruidDataSource实现连接复用与监控注意不要用DriverManager.getConnection()——每次调用都新建TCP连接高并发下直接打爆数据库。必须用连接池添加Maven依赖pom.xmldependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.16/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version !-- 严格匹配MySQL 5.7 -- /dependency创建src/main/java/com/shopping/utils/DBUtils.javapackage com.shopping.utils; import com.alibaba.druid.pool.DruidDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtils { private static DataSource dataSource; // 静态块初始化连接池类加载时执行一次 static { DruidDataSource druidDataSource new DruidDataSource(); druidDataSource.setUrl(jdbc:mysql://localhost:3306/shopping_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai); druidDataSource.setUsername(root); druidDataSource.setPassword(your_password); // 生产环境务必从配置文件读取 druidDataSource.setInitialSize(5); // 初始化连接数 druidDataSource.setMaxActive(20); // 最大活跃连接数 druidDataSource.setMinIdle(5); // 最小空闲连接数 druidDataSource.setMaxWait(60000); // 获取连接最大等待时间毫秒 druidDataSource.setTimeBetweenEvictionRunsMillis(60000); // 空闲连接检测间隔 druidDataSource.setValidationQuery(SELECT 1); // 连接有效性检测SQL druidDataSource.setTestWhileIdle(true); dataSource druidDataSource; } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 关闭资源必须在finally块中调用 public static void close(Connection conn) { if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }为什么选Druid而非HikariCPDruid提供/druid/index.html监控页面需配置Servlet可实时查看SQL执行时间、慢SQL、连接池状态对新手排查性能问题极其友好setValidationQuery(SELECT 1)确保从连接池取出的连接未被MySQL主动断开MySQL默认wait_timeout28800秒空闲连接8小时后失效3.2 UserDAO实现手写PreparedStatement防SQL注入创建src/main/java/com/shopping/dao/UserDao.javapackage com.shopping.dao; import com.shopping.model.User; import com.shopping.utils.DBUtils; import javax.sql.DataSource; import java.sql.*; public class UserDao { // 根据用户名查询用户登录校验用 public User findByUsername(String username) { String sql SELECT id, username, password, email, phone, created_time FROM user WHERE username ?; try (Connection conn DBUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); // 防SQL注入?占位符绑定参数 try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setEmail(rs.getString(email)); user.setPhone(rs.getString(phone)); user.setCreatedTime(rs.getTimestamp(created_time)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } // 用户注册插入新记录 public boolean save(User user) { String sql INSERT INTO user (username, password, email, phone) VALUES (?, ?, ?, ?); try (Connection conn DBUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); // 实际应为BCRYPT加密后的密文 ps.setString(3, user.getEmail()); ps.setString(4, user.getPhone()); int affectedRows ps.executeUpdate(); if (affectedRows 0) { return false; // 插入失败 } // 获取自增主键ID try (ResultSet generatedKeys ps.getGeneratedKeys()) { if (generatedKeys.next()) { user.setId(generatedKeys.getLong(1)); } } return true; } catch (SQLException e) { e.printStackTrace(); return false; } } }血泪经验PreparedStatement的三个必做动作ps.setString(1, username)永远用setXxx()方法绑定参数绝不用字符串拼接WHERE username username Statement.RETURN_GENERATED_KEYS插入后需获取自增ID时必须传此参数否则getGeneratedKeys()返回空结果集try-with-resources自动关闭Connection/PreparedStatement/ResultSet必须在try()中声明确保异常时自动释放资源避免连接泄漏3.3 事务控制在OrderService中保证“扣库存写订单”原子性创建src/main/java/com/shopping/service/OrderService.javapackage com.shopping.service; import com.shopping.dao.OrderDao; import com.shopping.dao.ProductDao; import com.shopping.model.Order; import com.shopping.model.Product; import com.shopping.utils.DBUtils; import java.sql.Connection; import java.sql.SQLException; public class OrderService { private final OrderDao orderDao new OrderDao(); private final ProductDao productDao new ProductDao(); // 创建订单含库存校验与扣减 public boolean createOrder(Long userId, Long productId, Integer quantity) { Connection conn null; try { conn DBUtils.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 查询商品当前库存 Product product productDao.findById(productId); if (product null || product.getStock() quantity) { throw new RuntimeException(商品库存不足); } // 2. 扣减库存UPDATE操作 int updated productDao.decreaseStock(productId, quantity); if (updated 0) { throw new RuntimeException(库存扣减失败请重试); } // 3. 创建订单 Order order new Order(); order.setUserId(userId); order.setProductId(productId); order.setQuantity(quantity); order.setTotalPrice(product.getPrice().multiply(new java.math.BigDecimal(quantity))); order.setStatus((byte) 0); // 待支付 boolean saved orderDao.save(order); if (!saved) { throw new RuntimeException(订单创建失败); } conn.commit(); // 所有操作成功提交事务 return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); // 任一环节失败回滚全部操作 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.close(); // 归还连接到Druid池 } catch (SQLException e) { e.printStackTrace(); } } } } }为什么事务必须手动控制productDao.decreaseStock()和orderDao.save()是两个独立的DAO方法各自获取连接并执行SQL若不显式开启事务两个操作可能在不同连接上执行无法保证原子性如扣库存成功但订单写入失败导致库存虚减conn.setAutoCommit(false)是事务起点conn.commit()是终点中间任何异常都触发conn.rollback()4. 避坑指南那些让新手调试到凌晨三点的典型问题4.1 现象JSP页面中文乱码显示为??????原因Tomcat默认使用ISO-8859-1解码URL参数而浏览器发送UTF-8编码的中文JSP响应未声明字符集浏览器按默认编码如GBK解析UTF-8字节流解决在web.xml中配置全局编码过滤器src/main/webapp/WEB-INF/web.xmlfilter filter-nameCharacterEncodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping注意即使不用Spring也要复制此Filter类可从Spring Framework jar中提取或手写简易版CharacterEncodingFilter。在所有JSP顶部添加% page contentTypetext/html;charsetUTF-8 languagejava %MySQL连接URL中必须包含?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai4.2 现象登录后session.getAttribute(currentUser)返回null原因HttpSession默认基于Cookie传递Session ID若浏览器禁用CookieJSESSIONID无法传递或Servlet中调用了req.getSession(false)false表示不创建新Session而用户首次访问时Session尚未创建解决前端测试时确保浏览器启用Cookie后端统一使用req.getSession(true)或req.getSession()等价于true关键检查点确认web.xml中session-config未设置tracking-modeURL/tracking-mode该模式会将JSESSIONID拼在URL后但现代浏览器普遍禁用4.3 现象MySQL插入中文报错Incorrect string value: \xF0\x9F\x98\x80原因表/字段字符集为utf8MySQL的utf8实为utf8mb3最多3字节不支持emoji而Java程序发送的是utf8mb4编码的emoji如占4字节解决创建数据库时指定CHARACTER SET utf8mb4已写在2.2节修改MySQL配置文件my.cnfLinux或my.iniWindows[client] default-character-set utf8mb4 [mysql] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci重启MySQL服务后执行SQL修正现有表ALTER DATABASE shopping_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 对product、order表执行相同命令4.4 现象Druid连接池报错maxWait is 60000ms, but no available connection原因数据库连接数耗尽如maxActive20但20个请求同时阻塞在慢SQL上或MySQL服务端max_connections参数过小默认151解决检查MySQL当前连接数SHOW STATUS LIKE Threads_connected; SHOW VARIABLES LIKE max_connections;调整Druid参数DBUtils.java中druidDataSource.setMaxActive(50); // 提高上限 druidDataSource.setMaxWait(30000); // 缩短等待时间快速失败 druidDataSource.setRemoveAbandonedOnBorrow(true); // 移除超时借用的连接 druidDataSource.setRemoveAbandonedTimeoutMillis(60000);根本方案在DAO方法中为所有executeQuery()添加超时ps.setQueryTimeout(5); // 单条SQL执行超过5秒强制中断4.5 现象JSP中c:forEach不解析原样输出标签原因未在web.xml中声明JSTL版本Servlet 3.0容器需要或pom.xml未引入jstl依赖仅taglibs-standard不够解决添加Maven依赖dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency确保web.xml头部声明为Servlet 3.0?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1JSP页面顶部必须有% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %5. 安全加固与生产就绪从实验室走向真实战场5.1 密码安全用BCrypt替换明文存储必须项警告UserDao.save()中直接存password明文是严重安全漏洞必须立即升级。添加BCrypt依赖dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId version5.8.5/version /dependency修改UserDao.save()和LoginServlet// UserDao.java 中 save 方法 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; private static final BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); public boolean save(User user) { // ... 其他代码 ps.setString(2, encoder.encode(user.getPassword())); // 加密后存储 // ... } // LoginServlet.java 中密码校验 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; private static final BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); // 替换原校验逻辑 if (user ! null encoder.matches(password, user.getPassword())) { // matches()比对明文与密文 // 登录成功 }BCrypt参数说明BCryptPasswordEncoder()无参构造使用默认强度10$2a$10$...强度每1计算耗时翻倍生产环境建议new BCryptPasswordEncoder(12)平衡安全性与响应时间绝对禁止使用MD5/SHA1彩虹表可秒破且无盐值salt防护5.2 SQL注入防御所有用户输入必须预编译回顾ProductDao.findByCategory方法示例// ❌ 危险字符串拼接 String sql SELECT * FROM product WHERE category_id categoryId; // ✅ 正确PreparedStatement占位符 String sql SELECT * FROM product WHERE category_id ? AND status 1; ps.setLong(1, categoryId);覆盖所有DAO方法WHERE子句中的所有变量id,username,emailORDER BY字段名不能用占位符必须白名单校验private static final SetString VALID_SORT_FIELDS Set.of(price, created_time, stock); if (!VALID_SORT_FIELDS.contains(sortField)) { throw new IllegalArgumentException(非法排序字段: sortField); } String sql SELECT * FROM product ORDER BY sortField sortOrder;LIMIT参数可用setInt()ps.setInt(1, offset); ps.setInt(2, limit); String sql SELECT * FROM product LIMIT ?, ?;5.3 XSS防护JSP输出必须HTML转义用户输入的内容如商品描述、评论直接输出到JSP会触发XSS攻击。解决方案JSTL内置转义推荐!-- 自动转义HTML特殊字符 -- p${product.description}/p !-- 不转义仅当确认内容安全时用 -- pc:out value${product.description} escapeXmlfalse//p手动转义工具类备用import org.apache.commons.text.StringEscapeUtils; String safeDesc StringEscapeUtils.escapeHtml4(userInput); req.setAttribute(safeDesc, safeDesc);注意StringEscapeUtils需引入commons-text依赖且escapeHtml4()仅处理HTML实体对JavaScript事件onerror等仍需前端CSP策略配合。5.4 部署优化Tomcat生产配置与日志分离修改$CATALINA_HOME/conf/server.xml提升稳定性!-- 1. 禁用shutdown端口防止远程关闭 -- !-- Server port8005 shutdownSHUTDOWN 改为 -- Server port-1 shutdownSHUTDOWN !-- 2. Connector增加连接超时与压缩 -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 maxThreads200 minSpareThreads10 compressionon compressionMinSize2048 noCompressionUserAgentsgozilla, traviata compressableMimeTypetext/html,text/xml,text/plain,application/javascript,application/json/日志分离$CATALINA_HOME/conf/logging.properties# 将应用日志独立到shopping.log 1catalina.org.apache.juli.AsyncFileHandler.level FINE 1catalina.org.apache.juli.AsyncFileHandler.directory ${catalina.base}/logs 1catalina.org.apache.juli.AsyncFileHandler.prefix shopping. # 关闭控制台日志生产环境不需 java.util.logging.ConsoleHandler.level OFF5.5 性能压测用JMeter验证100并发下的订单创建编写JMeter测试计划shopping_test.jmx线程组线程数100Ramp-Up时间10秒循环次数1HTTP请求POST/loginBody Datausernametestpassword123456POST/createOrderBody DataproductId1quantity1需先登录获取JSESSIONIDHTTP Cookie管理器自动处理Session聚合报告关注90% Line响应时间应1000ms、错误率应为0%压测发现的典型瓶颈productDao.findById()未加索引 → 在product表id字段建主键索引已存在orderDao.save()未加索引 → 在order表user_id字段建索引已存在Druid连接池maxActive20不足 → 调整为50并观察ActiveCount峰值6. 终极技巧用Filter实现统一登录拦截与请求日志6.1 登录拦截Filter保护/admin/路径下的所有资源创建src/main/java/com/shopping/filter/LoginFilter.javapackage com.shopping.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; 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; String servletPath req.getServletPath(); // 白名单登录页、注册页、静态资源、验证码接口 if (servletPath.startsWith(/login) || servletPath.startsWith(/register) || servletPath.startsWith(/css/) || servletPath.startsWith(/js/) || servletPath.startsWith(/images/) || servletPath.equals(/captcha)) { chain.doFilter(request, response); return; } // 检查Session中是否存在currentUser HttpSession session req.getSession(false); if (session null || session.getAttribute(currentUser) null) { // 未登录重定向到登录页并保存原请求URL用于登录后跳转 String originalUrl req.getRequestURL().toString(); if (req.getQueryString() ! null) { originalUrl ? req.getQueryString(); } session.setAttribute(originalUrl, originalUrl); resp.sendRedirect(req.getContextPath() /login.jsp); return; } p a hrefhttps://download.csdn.net/download/qq_38140936/89443728 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