news 2026/9/14 15:15:43

JSP+Servlet+MVC+MySQL:手把手构建高并发手机销售网站

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Servlet+MVC+MySQL:手把手构建高并发手机销售网站

简介:一套基于 JSP 与 MVC 模式、使用 MySQL 作为后台数据库的手机销售网站源码,适合 Java Web 初学者、课程设计及毕业设计人群,可帮助读者理解分层开发思想并掌握动态网站构建流程。资源共 58 个文件,以 13 个 Java 类、9 个 JSP 页面、2 个 SQL 数据库脚本和 XML 配置为主,另含编译后的 class 文件、图片素材和 jar 包,压缩后仅 1.74MB,目录结构兼顾源码与发布目录,便于直接导入开发工具学习。项目实现了手机商品展示、购物车管理、订单提交等典型电商功能,通过源码能够学习控制器层、业务层与数据访问层之间的协作方式,并了解 JSP 中表达式语言和标签库的渲染技巧;数据库脚本清晰定义了手机表、用户表、订单表等核心结构,对理解表间关联设计很有帮助。目前该资源已有 382 人学习下载,作为轻量级实战案例,适合用来自上而下串联 Java Web 开发中的关键环节,也可作为课程项目或毕业设计的参考蓝本。

1. 从“能卖手机”到“扛住并发”:这套技术栈到底解决什么问题

手机销售网站这类项目在面试题和课程设计里出现频率极高,但多数实现还停留在“用JSP连MySQL查个表,页面里塞满Java代码”的阶段。JSP+MVC+MySQL三个词拆开看都不陌生,合在一起却经常被误解——很多人以为“用了JSP就是MVC”,实际是Servlet把请求转发到JSP,JSP里又new了DAO,Model和View早就搅成糊。这套组合的成熟落地方式,是用Servlet充当控制器,JSP只做视图渲染,JavaBean或独立Service层处理业务,MySQL负责持久化。按这个边界写出来的代码,后期加一个手机型号筛选、改一次分页逻辑,不需要重写页面。

这篇文章适合两类人:一类是正在做课程设计或毕业设计的学生,需要一套能跑通、能讲清楚的设计思路;另一类是刚转Java Web开发、想弄明白“正规项目为什么不用JSP写业务”的从业者。文章会围绕一个可复现的手机销售网站骨架展开,把MVC分层、MySQL表设计、会话管理、分页搜索这些高频需求逐个落地,最后给出几个不翻车的小技巧。

2. JSP+MVC:别把Servlet写成工具人

2.1 三层职责边界:Model管数据,View管展示,Controller管流转

MVC在Java Web里的典型映射是:JSP对应View,Servlet对应Controller,JavaBean/Service/DAO对应Model。很多项目失败的起点,是Controller里写了一大坨业务判断,或者JSP里直接用<%标签查询数据库。要判断自己写的是不是合格的MVC,最简单的方法是看请求流转路径——浏览器发请求,Servlet接收并解析参数,调用Service层处理业务,把结果放进request或session作用域,最后forward到JSP渲染页面。任何一步跳出这个顺序,都说明分层出了问题。

// 以用户登录为例,Controller层只做三件事:收参、调Service、转发 @WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); // 业务判断全部在Service层,Controller不写if (password.equals(...)) User user = userService.login(username, password); if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/phone/list"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

这段代码的核心逻辑是“Servlet只做路由,不做判断”。UserService.login()返回User对象或null,Servlet根据结果决定重定向还是转发。sendRedirect是重新发起一次请求,地址栏会变化,适合登录成功后的跳转;forward是服务端转发,request作用域里的errorMsg还能带到JSP里——这个区别在面试常被追问,实际项目中乱用会导致刷新页面重复提交表单。

View层要遵守一条纪律:JSP里不允许出现import java.sql.*,不允许直接new DAO对象。页面需要展示的数据,都是在Controller里通过req.setAttribute("phoneList", list)塞进去的。JSP拿到数据后,用JSTL的c:forEach循环渲染商品卡片,或者用EL表达式${phone.price}输出单个字段。

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach var="phone" items="${phoneList}"> <div class="phone-card"> <h3>${phone.name}</h3> <p>价格:${phone.price} 元</p> <p>库存:${phone.stock}</p> <a href="${pageContext.request.contextPath}/phone/detail?id=${phone.id}">查看详情</a> </div> </c:forEach>

2.2 自己写MVC框架:前端控制器加反射,比Struts2轻得多

一些项目被要求“不能使用Spring MVC”,此时可以自己封装一个极简MVC核心,核心思想是前端控制器模式——所有请求先进入一个Servlet,通过URL映射找到对应的Action类和方法,省略掉每个业务写一个Servlet的重复工作。

// 一个统一的DispatcherServlet,拦截 *.do 请求 @WebServlet("*.do") public class DispatcherServlet extends HttpServlet { private Map<String, Object> actionMap = new HashMap<>(); public void init() { // 正常项目里这里会扫描包路径下的Action类并初始化到map中 actionMap.put("/user/login", new UserAction()); actionMap.put("/phone/list", new PhoneAction()); } protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String uri = req.getRequestURI(); String actionPath = uri.substring(req.getContextPath().length(), uri.lastIndexOf(".do")); Object action = actionMap.get(actionPath); if (action == null) { resp.sendError(404); return; } // 用反射调用统一的方法名,例如 execute(HttpServletRequest, HttpServletResponse) try { Method m = action.getClass().getMethod("execute", HttpServletRequest.class, HttpServletResponse.class); String result = (String) m.invoke(action, req, resp); // result形如 "forward:/phone/list.jsp" 或 "redirect:/phone/list.do" if (result.startsWith("forward:")) { req.getRequestDispatcher(result.substring(8)).forward(req, resp); } else if (result.startsWith("redirect:")) { resp.sendRedirect(req.getContextPath() + result.substring(9)); } } catch (Exception e) { throw new ServletException("Action执行失败", e); } } }

DispatcherServlet只做三件事:解析URL、查Map拿Action实例、反射调用统一方法。每个业务Action不再需要继承HttpServlet,只需提供execute方法并返回一个字符串控制跳转方向。这种设计的优点是新增一个页面只要加一个Action类并往actionMap里注册一行,缺点是需要自己处理事务和参数封装,不如用Spring MVC时一个@RequestMapping注解来得快。

如果项目没有“禁止使用框架”的硬性要求,直接上Spring MVC会更划算——@Controller@RequestMapping@RequestParam三个注解就能省掉上面30行代码。但理解这个自定义Dispatcher的原理,对于面试回答“Spring MVC的DispatcherServlet是怎么工作的”会有很大帮助。

2.3 拦截器和过滤器:登录校验到底该放哪

手机销售网站必然有用户中心和购物车,这就需要在访问受保护资源前判断用户是否登录。很多新手把校验代码写在每个Servlet开头复制粘贴,一旦校验逻辑分了三份,后面加“强制改密”功能就痛苦了。正确的做法是写一个Filter,用@WebFilter("/*")注解注册,在doFilter里统一检查。

@WebFilter("/*") public class LoginFilter implements Filter { 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(".css") || uri.endsWith(".js") || uri.endsWith(".jpg")) { chain.doFilter(req, resp); return; } if (uri.endsWith("/login.jsp") || uri.endsWith("/login.do") || uri.endsWith("/register.do")) { chain.doFilter(req, resp); return; } // 检查session中是否有登录标记 Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

需要注意Filter的执行时机——它拦截的是Servlet容器层面的所有请求,包括静态资源。上面代码用白名单方式放过了CSS、JS和图片,但这只是简易做法,正规项目通常把静态资源放在/static/目录下,在web.xml或Spring配置里单独放行,而不是在Filter里拼后缀。另外,chain.doFilter(req, resp)放行时要传ServletRequestServletResponse类型,如果你的后续代码需要HttpServletRequest里的方法,可以在放行前用HttpServletRequest引用操作session,也可以放行后在Servlet里强转,两种做法都常见。

3. MySQL数据库设计:手机SKU表加库存,连接池参数是个坑

3.1 表结构设计:SKU和SPU为什么要分开

手机销售网站上架的商品有品牌、型号、颜色、存储容量、价格、库存等属性。如果把所有信息塞进一张tb_phone表,颜色和容量是“规格参数”,每个规格组合产生的价格和库存不同。正确做法是拆成SPU表和SKU表——SPU是商品抽象(iPhone 15),SKU是具体可下单的规格组合(iPhone 15 蓝色 256G)。

CREATE TABLE tb_phone_spu ( id INT PRIMARY KEY AUTO_INCREMENT, brand VARCHAR(50) NOT NULL COMMENT '品牌,如 Apple、Huawei', model VARCHAR(100) NOT NULL COMMENT '机型,如 iPhone 15', release_date DATE DEFAULT NULL, description TEXT COMMENT '商品详情', main_image VARCHAR(255) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT '1上架 0下架', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE tb_phone_sku ( id INT PRIMARY KEY AUTO_INCREMENT, spu_id INT NOT NULL, color VARCHAR(30) NOT NULL, storage VARCHAR(20) NOT NULL COMMENT '如 128G、256G', price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, sku_code VARCHAR(50) UNIQUE COMMENT '如 APL-IP15-BLUE-256', FOREIGN KEY (spu_id) REFERENCES tb_phone_spu(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

DECIMAL(10,2)是价格字段的标准选择,用FLOAT存价格会出现0.1+0.2不等于0.3的精度问题。stock字段有并发隐患——用户下单时要先判断stock > 0再扣减,但两个请求同时读到库存为1时,都可能执行扣减导致超卖。自己实现MVC+MySQL时,最简单的防超卖方案是使用乐观锁:

UPDATE tb_phone_sku SET stock = stock - 1 WHERE id = ? AND stock > 0;

UPDATE语句在InnoDB默认的REPEATABLE READ隔离级别下会对命中行加锁,stock > 0条件确保扣减前库存为正,执行后通过int rows = preparedStatement.executeUpdate()判断是否等于1,为0表示库存不足或并发冲突,回滚订单。

3.2 连接池选型:阿里巴巴Druid的配置与监控

JSP页面直连MySQL最不该犯的错误,是用DriverManager.getConnection()在每次请求时新建连接。数据库连接的创建和销毁开销远大于SQL执行本身,高并发下直接打满数据库线程数。这里建议用阿里Druid连接池,除了性能好,还自带监控页面,排查慢SQL时特别方便。

# db.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/phone_shop?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=your_password # Druid初始连接数 druid.initialSize=5 druid.minIdle=5 druid.maxActive=20 druid.maxWait=60000 druid.validationQuery=SELECT 1 druid.testWhileIdle=true druid.timeBetweenEvictionRunsMillis=3000

com.mysql.cj.jdbc.Driver是MySQL 8.0以上版本驱动的类名,老项目里的com.mysql.jdbc.Driver在8.0已废弃。serverTimezone=Asia/Shanghai指定时区,不设置会报The server time zone value '�й���׼ʱ��' is unrecognized的错误。allowPublicKeyRetrieval=true是MySQL 8.0使用caching_sha2_password认证时客户端首次连接需要的参数。

Druid连接池的核心参数里,maxWait是获取连接的最大等待毫秒数,超时抛异常,避免线程无限阻塞。validationQuery是验证连接是否有效的测试SQL,testWhileIdle控制空闲连接是否定期检测,搭配timeBetweenEvictionRunsMillis,能避免MySQL的wait_timeout默认8小时回收空闲连接后,连接池仍拿着失效连接给用户使用。

3.3 MySQL存储过程处理订单事务

订单表数据多了以后,会涉及主表和子表事务。通常做法是在Service层方法上加`@Transactional`注解,让Spring管理事务边界,但不用框架时就要自己控制`Connection`的提交与回滚。存储过程把多条SQL打包放到数据库端执行,适合像下单这种既有订单表写入、又有SKU表扣减、还有用户积分变动的场景。
DELIMITER $$ CREATE PROCEDURE sp_create_order( IN p_user_id INT, IN p_sku_id INT, IN p_quantity INT, OUT p_order_id INT ) BEGIN DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; -- 生成订单主表记录 INSERT INTO tb_order (user_id, total_amount, status, create_time) VALUES (p_user_id, 0, 'PENDING', NOW()); SET p_order_id = LAST_INSERT_ID(); -- 插入订单明细(金额在业务代码中算好后传入,这里简化处理) INSERT INTO tb_order_item (order_id, sku_id, quantity, price) SELECT p_order_id, id, p_quantity, price FROM tb_phone_sku WHERE id = p_sku_id; -- 更新SKU库存,利用乐观锁条件防止超卖 UPDATE tb_phone_sku SET stock = stock - p_quantity WHERE id = p_sku_id AND stock >= p_quantity; -- 检查受影响行数,如果为0说明库存不足 IF ROW_COUNT() = 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足'; END IF; COMMIT; END $$ DELIMITER ;

存储过程里的DECLARE EXIT HANDLER FOR SQLEXCEPTION类似Java里的try-catch,发生异常会自动回滚整个事务并将异常上抛。SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足'是主动抛出业务异常。使用存储过程有一个代价——业务逻辑分散到了SQL和Java两层,数据库升级迁移时会多一份脚本需要维护。一般项目里,简单的单表更新用Java层事务控制就好,存储过程更适用于订单、库存这种跨表强一致性的核心链路。

4. 核心模块实战:商品列表页的分页、搜索与购物车会话

4.1 分页与搜索的SQL拼接:排序字段不能拼字符串

手机列表页必然有“按品牌筛选”“按价格排序”和分页。先给出DAO层的分页查询代码,再指出其中的安全陷阱。

public List<PhoneSku> queryPhoneList(String brand, String keyword, String orderBy, int page, int pageSize) { StringBuilder sql = new StringBuilder( "SELECT s.id, s.color, s.storage, s.price, s.stock, p.brand, p.model, p.main_image " + "FROM tb_phone_sku s JOIN tb_phone_spu p ON s.spu_id = p.id WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (brand != null && !brand.isEmpty()) { sql.append("AND p.brand = ? "); params.add(brand); } if (keyword != null && !keyword.isEmpty()) { sql.append("AND (p.model LIKE ? OR p.description LIKE ?) "); params.add("%" + keyword + "%"); params.add("%" + keyword + "%"); } String safeOrder; if ("price_asc".equals(orderBy)) { safeOrder = "s.price ASC"; } else if ("price_desc".equals(orderBy)) { safeOrder = "s.price DESC"; } else { safeOrder = "s.id DESC"; } sql.append("ORDER BY ").append(safeOrder).append(" LIMIT ? OFFSET ?"); params.add(pageSize); params.add((page - 1) * pageSize); // 执行PreparedStatement并封装结果... }

WHERE 1=1是动态拼接的惯用技巧,它让后续每一个AND子句都可以无脑追加,不用额外判断是否是第一个条件。?占位符配合PreparedStatement能防止SQL注入,LIKE关键字本身不做转义,所以用户输入里带%会把所有数据查出来——这个问题可以接受,不需要额外处理。

关键点是排序字段。orderBy参数如果直接拼字符串,用户传入price_desc; DROP TABLE tb_phone_sku;--就直接注入了。这里用白名单方式把orderBy限定为三个固定值,映射到安全的SQL片段,至少能保证不会出现语法错误和注入风险。需要扩展排序维度时,在if分支里继续加即可。

4.2 商品详情页的URL设计:REST风格与JSP路径的匹配

商品详情页的URL如果写成/phone/detail?id=123,是传统的查询字符串风格;如果写成/phone/detail/123,是REST风格路径。Tomcat 8及以上版本支持Servlet 3.1的路径参数,但JSP里输出链接时要保持一致。

<a href="${pageContext.request.contextPath}/phone/detail/${sku.id}">查看详情</a>

对应Servlet的映射处理方式是使用@WebServlet("/phone/detail/*"),然后从request.getPathInfo()中截取商品ID:

@WebServlet("/phone/detail/*") public class PhoneDetailServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String pathInfo = req.getPathInfo(); // 形如 "/123" if (pathInfo == null || pathInfo.length() <= 1) { resp.sendError(400); return; } int skuId; try { skuId = Integer.parseInt(pathInfo.substring(1)); } catch (NumberFormatException e) { resp.sendError(400); return; } PhoneSku sku = skuService.queryBySkuId(skuId); if (sku == null) { resp.sendError(404); return; } req.setAttribute("sku", sku); req.getRequestDispatcher("/phone_detail.jsp").forward(req, resp); } }

getPathInfo()在Tomcat下的表现是/detail/后面的部分,以/开头。这里截取时用substring(1)去掉前导斜杠。如果项目里同时存在/phone/detail?id=123风格的旧链接,兼容做法是既读取pathInfo又读取getParameter("id"),优先用pathInfo,为空时改用getParameter。这种兼容写法在系统升级时很常见,部署时不需要让用户改收藏夹里的旧链接。

4.3 购物车会话:购物车放Session还是数据库

手机销售网站的购物车是典型的不同用户状态场景。登录用户的购物车建议存数据库,存tb_cart_item表,字段包含user_idsku_idquantity,这样用户换电脑登录购物车还在,也方便后台分析用户偏好。未登录用户的购物车可以先放Session,登录后把Session中的临时条目合并进数据库。

public void addToCart(HttpServletRequest req, int skuId, int quantity) { User user = (User) req.getSession().getAttribute("loginUser"); CartService cartService = new CartService(); if (user != null) { cartService.addItemToDb(user.getId(), skuId, quantity); } else { Cart cart = (Cart) req.getSession().getAttribute("cart"); if (cart == null) { cart = new Cart(); req.getSession().setAttribute("cart", cart); } cart.addItem(skuId, quantity); } }

Cart类内部实现为一个Map<Integer, Integer>,键是skuId,值是数量。放在Session的优点是响应快、不占数据库资源,缺点是一旦session.invalidate()或者服务器重启,数据就丢了。所以登录后的第二次请求,需要把Session里的临时购物车数据批量搬到数据库。常见做法是写一个mergeCart方法,在用户登录成功的一刻调用。

4.4 JSP个人信息展示页面与密码修改

个人信息页在JSP中的展示要特别注意XSS漏洞——用户名、昵称这类用户可控字段,在JSP页面必须用fn:escapeXml()${fn:escapeXml(user.nickname)}转义。JSTL的c:out标签默认也会转义,比${}表达式更安全:

<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> <div class="user-info"> <span>用户昵称:</span> <span><c:out value="${user.nickname}" /></span> </div>

修改密码功能要重点处理“新密码与旧密码是否相同”“新密码与确认密码是否一致”“密码加密方式”三件事。密码存储禁止明文,也不要用MD5——MD5撞库速度极快,至少用BCryptPBKDF2。自己实现可以用JDK自带的MessageDigest做带盐的SHA-256,盐值用SecureRandom生成并和密码一起存储。

5. 部署与排错:MySQL 8.0的坑、JSP编译class文件和Tomcat配置

5.1 本地环境搭建:MySQL免安装版的初始化步骤

手机销售网站要跑起来需要JDK 8+、Tomcat 8.5+、MySQL 5.7或8.0+以及IDEA。MySQL从官网下载的是压缩版,解压后不能直接使用,需要手动初始化和创建数据库:

# 1. 进入MySQL解压目录下的bin文件夹 mysqld --initialize-insecure # 执行完毕后,data目录生成,root用户密码为空 # 2. 安装Windows服务 mysqld --install MySQL8 --defaults-file="D:\mysql-8.0.46-winx64\my.ini" # 3. 启动服务 net start MySQL8 # 4. 进入命令行客户端 mysql -u root -p # 5. 设置root密码并创建项目数据库 ALTER USER 'root'@'localhost' IDENTIFIED BY 'your_password'; CREATE DATABASE phone_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

--initialize-insecure会生成一个密码为空的root账户,方便首次登录。my.ini是MySQL的配置文件,解压版默认没有这个文件,需要自己创建,至少包含[mysqld]basedirdatadir三项。注意basedirdatadir的路径分隔符,建议用正斜杠或双反斜杠。MySQL 8.0还要求character-set-server=utf8mb4,否则从连接池写入中文可能会乱码。查询当前字符集配置的命令是SHOW VARIABLES LIKE 'character_set%';

5.2 IDEA配置JSP编译输出位置:jsp编译class文件保存路径

JSP文件在Tomcat运行时才被编译成Servlet类,编译后的class文件并不是固定保存在项目的target目录。在IDEA里部署Web项目后,JSP编译生成的Java和class文件保存在Tomcat庄配置的Work目录,针对IDEA内嵌Tomcat,工作目录默认是%USERPROFILE%\.IntelliJIdea2019.1\system\tomcat\项目名

出现“查看jsp编译后的java类”的需求,通常是为了排查JSP模板中的语法错误或变量作用域问题。定位方式是在IDEA的Run Configuration中找到Tomcat Server配置,查看Working directory字段,那里标注了C:\Users\xxx\.IntelliJIdea...\Tomcat\...路径。进入该目录后找到work\Catalina\localhost\项目名\org\apache\jsp,能看到login_005fpage_jsp.java这类文件,打开即可看到JSP被翻译成的Servlet源码。如果IDE打开的是Eclipse,路径则位于E:\apache-tomcat-9.0.xx\work\Catalina\localhost\下。

排查JSP编译错误除了看这个文件,更快的办法是直接看Tomcat日志catalina.out或IDEA的Console窗口。JSP编译错误会抛出org.apache.jasper.JasperException,错误信息会具体到哪一行使用了未定义的变量或标签。

5.3 MySQL驱动版本与ClassNotFoundException的对应关系

手机销售网站部署到自购的腾讯云轻量服务器或阿里云ECS时,拷好war包放到Tomcat的webapps下,启动后访问JSP页面报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。这通常意味着mysql-connector-java的jar包没有放到WEB-INF/lib目录下。IDEA里Add as Library只会把jar引入编译环境,不会自动打包进war。正确的做法是在Project Structure的Artifacts里,把mysql-connector-java-8.0.x.jar拖进“WEB-INF/lib”区域。部署到外部Tomcat后,也可以直接把jar拷到Tomcat的lib目录,但这个全局jar会影响该Tomcat下的所有应用,不推荐。

MySQL 8.x的驱动jar需要和JDK版本匹配:mysql-connector-java 8.0.x要求JDK 8+,个别老项目的JDK 1.6只能改用5.1.49版本驱动,但5.1.49连接MySQL 8.0又可能遇到认证插件不兼容问题。如果服务器上的MySQL是5.7,使用MySQL 5.1.49连接,无需配置com.mysql.cj.jdbc.Driver,用com.mysql.jdbc.Driver即可。

5.4 MySQL排序的常见陷阱和索引优化

手机列表页价格排序慢、品牌筛选慢是高频问题。如果表中数据量到了几十万行,ORDER BY s.price LIMIT 10 OFFSET 0这种查询,如果只有主键索引,MySQL需要进行全表扫描和文件排序(filesort)。解决办法是在tb_phone_sku表上为spu_id加索引,避免每次JOIN tb_phone_spu时发生全表扫描:

ALTER TABLE tb_phone_sku ADD INDEX idx_spu_id (spu_id);

如果是按品牌进商品筛选,WHERE p.brand = 'Apple' ORDER BY s.price这种涉及两表关联,则需要在tb_phone_spu表的brand字段建立二级索引。但要注意,索引不是越多越好——每个索引都会拖慢INSERTUPDATE速度,做项目时先看执行计划再决定:

EXPLAIN SELECT s.id, s.price, p.brand FROM tb_phone_sku s JOIN tb_phone_spu p ON s.spu_id = p.id WHERE p.brand = 'Apple' ORDER BY s.price LIMIT 10;

EXPLAIN输出结果中的type列是关键指标:ALL表示全表扫描,数据量大时需要优化;eq_refref表示走索引且效率较高。Extra列出现Using filesort时,说明ORDER BY字段没有索引支撑,性能会受影响。

6. 三个能立竿见影的优化技巧:连接复用、AJAX异步加载和SQL拦截

6.1 JSP底部统一输出执行时间

给页面加上“查询耗时xx毫秒”这个细节,既能在答辩时展示你对性能有感知,也方便在页面上直观验证数据库索引是否生效。实现方式是在最外层的index.jsp或公共布局里使用过滤器统计处理时间,并通过request.setAttribute传给JSP。

DispatcherServletservice方法开始前记录long start = System.currentTimeMillis();,在finally块里把耗时塞进request作用域,JSP页面的底部Footer.jsp中用<span>Page Load: ${requestScope.costTime} ms</span>输出。注意这个时间包含Servlet处理和JSP渲染两部分,单独看数据库查询耗时还需要在DAO层再埋一个计时点。

6.2 AJAX异步加载商品库存

手机详情页的“查看剩余库存”功能如果用同步刷新,每次点击都重新加载整个页面,体验糟糕还浪费带宽。正确做法是用AJAX调用独立的Servlet接口,返回JSON格式的库存数据,前端用JavaScript局部更新。

function checkStock(skuId) { fetch(contextPath + '/phone/stock/' + skuId) .then(resp => resp.json()) .then(data => { document.getElementById('stock-info').innerText = '剩余库存:' + data.stock + ' 件'; }) .catch(() => { document.getElementById('stock-info').innerText = '库存查询失败,请刷新重试'; }); }

对应的Servlet返回JSON需要引入fastjsonGson库,注意处理中文乱码时要在resp.setContentType("application/json;charset=utf-8")后才能getWriter().write(jsonString)charset=utf-8不可省略。JSP页面里要引用JSON对象时,注意引入fastjson后,JSON.toJSONString(map)对日期字段默认输出时间戳格式,要格式化得为Date字段配@JSONField(format="yyyy-MM-dd HH:mm:ss")注解。

6.3 拦截SQL打印的隐藏方法

自己写的JDBC框架可以动态代理PreparedStatement来打印执行SQL和参数,快速定位慢查询和参数错误。常见做法是在DAO层统一封装一个JdbcTemplate工具类,在该类中重写createStatementprepareStatement方法,把最终执行的SQL和绑定参数拼出来打印到控制台:

// 拿到的PreparedStatement包装对象,invoke时截获execute*方法日志 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = method.invoke(realStatement, args); if (method.getName().startsWith("execute")) { System.out.println("[SQL] " + sql + " — params: " + Arrays.toString(params)); } return result; }

需要注意,代理PreparedStatement后,getGeneratedKeysgetResultSet这类方法会频繁被调用,日志打印条件只需要判断方法名是executeQueryexecuteUpdateexecute即可。线上环境建议加一个开关控制是否打印SQL,否则日志文件会膨胀得很快——通常用DEBUG级别日志输出SQL,INFO级别只打印慢查询(执行时间超过200ms的SQL)。

6.4 JSP禁止离开页面提示的关闭方式

开发JSP页面时,如果总弹出“系统检测到您有未保存的修改”之类的提示,通常是在<body>标签上加了onbeforeunload事件。反查方法是不好使的,要在浏览器开发者工具的Console中执行window.onbeforeunload = null并刷新页面。如果项目里全局引入了某个JS文件并注册了beforeunload监听器,需要在初始化JS里找到对应代码注释掉这段逻辑。这个提示的本意是防止用户意外丢失表单内容,但调试时会造成极大的干扰,一般在非生产环境直接屏蔽。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 15:14:09

企业官网响应式模板改造:从HTML源码到移动端适配实战

简介&#xff1a;企业网站HTML源码包是一套专为企业官网场景设计的响应式网页模板&#xff0c;内置移动端适配能力&#xff0c;适合前端开发者、中小企业建站人员及需要在短时间内部署企业展示页面的程序员使用&#xff0c;可有效解决从零开发耗时、不同屏幕适配难等常见问题。…

作者头像 李华
网站建设 2026/9/14 15:13:42

Apache Arrow C++ 示例解析:用 compute 比较列数据并写出 CSV 文件

Apache Arrow C 示例解析&#xff1a;用 compute 比较列数据并写出 CSV 文件 【免费下载链接】arrow Apache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics 项目地址: https://gitcode.com/GitHub_…

作者头像 李华
网站建设 2026/9/14 15:12:05

Arduino UNO三轮智能小车例程:从差速运动学到循迹避障

简介&#xff1a;智能小车是嵌入式系统与机器人控制的经典入门项目&#xff0c;其运动控制核心在于差速驱动模型&#xff1a;通过左右驱动轮的转速差实现前进、转弯与原地旋转&#xff0c;而万向轮仅作支撑。Arduino UNO作为主控平台&#xff0c;负责读取循迹传感器、超声波或蓝…

作者头像 李华
网站建设 2026/9/14 15:10:52

Git文件状态标志详解:U、M、D、A在编辑器中究竟代表什么?

如果你用的是VS Code这类带Git集成的编辑器&#xff0c;应该早就注意到一个现象&#xff1a;文件列表里某些文件名的末尾会挂着小字母——有时是U&#xff0c;有时是M&#xff0c;偶尔是D&#xff0c;颜色还各不相同。我当年第一次看到时&#xff0c;第一反应是“插件是不是装坏…

作者头像 李华
网站建设 2026/9/14 15:10:27

C盘爆满不用慌:从根源揪出空间吸血鬼,一套方法彻底搞定

这几年帮人修电脑、做系统维护&#xff0c;几乎每周都能遇到“C盘又红了”的求助。磁盘占用一高&#xff0c;电脑就开始卡顿&#xff0c;软件装不上&#xff0c;系统更新卡死&#xff0c;连聊天软件都打不开——所有问题最后都指向同一个原因&#xff1a;C盘满了。我自己的主力…

作者头像 李华