news 2026/11/2 15:32:47

云工厂网上书城:J2EE实战项目(Servlet+JDBC+JSP)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
云工厂网上书城:J2EE实战项目(Servlet+JDBC+JSP)

简介:这是一份基于J2EE技术栈开发的完整在线图书销售系统——“云工厂-网上书城”,面向Java Web初学者与企业级应用实践者,用于深入理解多层架构设计、前后端协同及数据库集成等核心开发能力。资源包含588个文件,涵盖83个Java源码(如BookInfo、AdminInfoDAO等业务类)、45个JSP页面(实现用户购书、订单管理等交互视图)、313个GIF与36个JPG素材(支撑前端界面展示),以及82个编译后class文件、10个CSS样式表和关键SQL数据库脚本,整体压缩包仅4.09MB,轻量易部署。已有242人学习下载,适合作为课程设计、毕业项目或J2EE技术进阶实训案例。读者可直接运行于Tomcat等Web容器,完整掌握Servlet请求处理、JSP动态渲染、JDBC数据库操作、JavaBeans数据封装及EJB事务管理等典型企业级开发流程。

1. 云工厂-网上书城:一个能跑在Tomcat 8+上的J2EE实战项目,新手练手不翻车,老手改接口三分钟上生产

你刚学完Servlet、JSP、JDBC和Basic MVC,手头只有IDEA和本地MySQL,想立刻验证“J2EE到底长什么样”——别再搭空壳框架了。这个「云工厂-网上书城」就是为你准备的:它不是教学Demo,而是一个真实压缩包(含完整war包、SQL脚本、源码目录),部署到Tomcat 8.5+就能打开首页看到图书列表、用户登录、购物车结算、订单生成全流程。所有DAO层用原生JDBC手写(没用Hibernate/MyBatis),Service层带事务控制,Controller用标准HttpServlet实现,连JSP里的EL表达式和JSTL标签都严格按Java EE 7规范来。某高校实训课连续三年用它当结课项目,学生反馈最实在的一句是:“第一次改完‘加入购物车’逻辑,刷新页面就生效,终于信了MVC不是玄学”。适合两类人:一是刚敲完《Head First Servlets》第5章、急需一个可调试、可打断点、可改数据库字段的真实靶子;二是需要快速交付一个轻量后台管理界面的外包场景,删掉前端皮肤、换掉数据库连接池,两天就能套进客户环境。


2. 环境搭建与一键部署:从解压到首页显示的完整链路

2.1 依赖清单与版本对齐:为什么必须用JDK 8 + Tomcat 8.5+

这个项目基于Java EE 7规范构建,核心约束有三点:第一,JSP中使用了<c:forEach>等JSTL 1.2标签,要求Servlet容器支持3.1规范,Tomcat 7.x仅支持Servlet 3.0,会报The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved;第二,web.xml头部声明为<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.1">,Tomcat 8.0+才完全兼容;第三,JDBC驱动用的是mysql-connector-java-5.1.47.jar,它要求JDK 8+(5.1.46开始弃用JDK 7支持)。所以你的环境必须是:

  • JDK:1.8.0_202 或更高(java -version确认)
  • Tomcat:8.5.90 或 9.0.83(避免8.5.0这种早期bug版)
  • MySQL:5.7.32 或 8.0.26(注意:8.0+需额外配置useSSL=false&serverTimezone=UTC)

提示:不要用IntelliJ内置的Tomcat插件直接Run——它常默认加载tomcat-juli.jar冲突。务必下载官方zip包解压后,在IDEA中选External Tomcat。

2.2 数据库初始化:执行sql脚本前必须改的三处硬编码

项目根目录下sql/bookstore_init.sql是建库建表语句,但其中三处必须手动修改,否则启动报Unknown database 'bookstore'或Table 'bookstore.user' doesn't exist:

-- 文件开头第3行:创建数据库语句(如果你的MySQL不允许CREATE DATABASE权限,直接跳过这行) CREATE DATABASE IF NOT EXISTS bookstore CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 第12行:user表的password字段长度(原为VARCHAR(32),但MD5加密后是32位,实际登录时明文密码被截断) ALTER TABLE `user` MODIFY `password` VARCHAR(100) NOT NULL; -- 改成100,兼容BCrypt加密扩展 -- 第87行:插入管理员账号的SQL(密码是明文'admin123',但代码里用的是MD5(hex)) INSERT INTO `user` VALUES (1,'admin','21232f297a57a5a743894a0e4a801fc3','admin@bookstore.com','13800138000','admin',1,'2023-01-01 00:00:00');

执行顺序:先在MySQL命令行登录,执行source /path/to/bookstore_init.sql;再检查SELECT * FROM user;确认admin账号password字段值是21232f297a57a5a743894a0e4a801fc3(即md5('admin123')的十六进制小写)。

2.3 WAR包部署实操:两种方式及日志定位法

方式一:传统war包部署(推荐用于生产模拟)

  1. 将项目根目录下的bookstore.war复制到$TOMCAT_HOME/webapps/
  2. 启动Tomcat:$TOMCAT_HOME/bin/startup.sh(Linux/Mac)或startup.bat(Win)
  3. 观察$TOMCAT_HOME/logs/catalina.out,等待出现INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [...] has finished in [xxx] ms
  4. 浏览器访问http://localhost:8080/bookstore/,首页应显示“云工厂-网上书城”Banner和图书列表

方式二:IDEA热部署(开发调试用)

  1. File → Project Structure → Modules → Dependencies → + → JARs or directories → 添加lib/*.jar(尤其不能漏jstl-1.2.jar和standard-1.1.2.jar)
  2. Artifacts → + → Web Application: Archive → Output Directory设为out/artifacts/bookstore_war_exploded
  3. Run → Edit Configurations → Tomcat Server → Deployment → + → Artifact → 选择bookstore:war exploded
  4. 启动后修改JSP,Ctrl+S保存即刷新(Servlet类需重启,但JSP不用)

注意:如果首页空白且日志报java.lang.ClassNotFoundException: javax.servlet.jsp.JspFactory,说明jsp-api.jar未加载——Tomcat 8.5+已将JSP API内置,必须删除WEB-INF/lib/下所有jsp-api*.jar,否则类加载冲突。

2.4 首页请求链路追踪:从URL到JSP的七步调用

当你访问http://localhost:8080/bookstore/时,实际触发的是IndexServlet,整个链路如下:

  1. Tomcat根据web.xml中<welcome-file-list>匹配到index.jsp
  2. index.jsp第一行<%@ include file="header.jsp"%>引入公共头
  3. header.jsp中<c:if test="${empty sessionScope.user}">判断登录态 → 调用session.getAttribute("user")
  4. 若未登录,页面显示“请登录”,点击跳转/login.jsp
  5. 若已登录,index.jsp底部<jsp:include page="book_list.jsp"/>加载图书列表
  6. book_list.jsp通过<c:forEach items="${requestScope.bookList}" var="book">遍历数据
  7. bookList来自IndexServlet.java第28行:request.setAttribute("bookList", bookService.findAll());

这个链路证明:它不是静态HTML,而是标准J2EE请求转发流程。你可以现在就在IndexServlet.doGet()里加一行System.out.println("IndexServlet triggered at " + new Date());,刷新首页看控制台输出,亲手验证MVC的Controller入口。


3. 核心模块拆解:BookService事务控制与CartServlet并发安全实践

3.1 BookService:手写JDBC事务的四层防护

src/com/cloudfactory/service/BookService.java是全项目最值得细读的类。它没用Spring声明式事务,而是用原生JDBC的Connection.setAutoCommit(false)实现显式事务,覆盖了图书查询、库存扣减、订单生成三个关键场景。以buyBook(int bookId, int userId)方法为例(第45行起):

public boolean buyBook(int bookId, int userId) { Connection conn = null; PreparedStatement ps1 = null; PreparedStatement ps2 = null; PreparedStatement ps3 = null; try { conn = JdbcUtils.getConnection(); // 从Druid连接池获取 conn.setAutoCommit(false); // 关键:关闭自动提交 // 步骤1:查库存(SELECT FOR UPDATE,防止超卖) ps1 = conn.prepareStatement("SELECT stock FROM book WHERE id = ? FOR UPDATE"); ps1.setInt(1, bookId); ResultSet rs = ps1.executeQuery(); if (!rs.next() || rs.getInt("stock") <= 0) { throw new RuntimeException("库存不足"); } // 步骤2:扣库存(UPDATE) ps2 = conn.prepareStatement("UPDATE book SET stock = stock - 1 WHERE id = ?"); ps2.setInt(1, bookId); int updateCount = ps2.executeUpdate(); if (updateCount == 0) { throw new RuntimeException("更新库存失败"); } // 步骤3:生成订单(INSERT) ps3 = conn.prepareStatement("INSERT INTO orders(user_id, book_id, create_time) VALUES (?, ?, ?)"); ps3.setInt(1, userId); ps3.setInt(2, bookId); ps3.setTimestamp(3, new Timestamp(System.currentTimeMillis())); ps3.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // 任一环节失败回滚 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { // 必须关闭所有Statement和Connection(Druid会归还连接池) JdbcUtils.close(ps3, ps2, ps1, conn); } }

关键点说明:

  • FOR UPDATE:在查询库存时加行锁,避免两个请求同时读到stock=1然后都扣减导致负库存;
  • conn.setAutoCommit(false):必须在获取Connection后立即设置,否则后续commit()无效;
  • JdbcUtils.close():封装了null安全的资源关闭,顺序是ps3→ps2→ps1→conn,防止关闭Connection后PreparedStatement失效;
  • 没用try-with-resources:因为Connection要跨多个PreparedStatement复用,且需手动rollback(),传统try-catch-finally更可控。

3.2 CartServlet:HTTP Session状态管理的血泪经验

购物车功能藏在src/com/cloudfactory/servlet/CartServlet.java,它用HttpSession存储List<CartItem>,但这里有个经典坑:CartItem类必须实现Serializable,否则Tomcat集群或Session持久化时抛NotSerializableException。看它的定义:

// src/com/cloudfactory/entity/CartItem.java public class CartItem implements Serializable { // 必须加! private static final long serialVersionUID = 1L; // 建议显式声明 private int id; // 图书ID private String title; // 图书标题(冗余,避免多次查库) private double price; // 单价(冗余) private int quantity; // 数量 // getter/setter省略... @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CartItem cartItem = (CartItem) o; return id == cartItem.id; // 仅ID判等,避免title/price变更影响去重 } }

CartServlet.doPost()处理“添加到购物车”时,核心逻辑是:

// 获取当前Session中的购物车List List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } // 查找是否已存在该图书 CartItem existing = cart.stream() .filter(item -> item.getId() == bookId) .findFirst() .orElse(null); if (existing != null) { existing.setQuantity(existing.getQuantity() + 1); // 已存在则数量+1 } else { // 不存在则新建CartItem并加入 CartItem newItem = new CartItem(); newItem.setId(bookId); newItem.setTitle(book.getTitle()); newItem.setPrice(book.getPrice()); newItem.setQuantity(1); cart.add(newItem); }

注意:这里没用synchronized(session),因为HttpSession本身是线程安全的(Tomcat对每个Session对象加锁),但ArrayList不是。所以实际运行中,高并发添加同一用户购物车可能出错。生产环境必须改用CopyOnWriteArrayList或加同步块——这是你二次开发的第一个改造点。

3.3 OrderService:分库分表前的订单号生成策略

订单号生成在OrderService.generateOrderNo()(第62行),采用yyyyMMddHHmmssSSS + 3位随机数格式,例如20231015142305123456:

public static String generateOrderNo() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS"); String timePart = sdf.format(new Date()); int randomPart = (int)(Math.random() * 900) + 100; // 100-999 return timePart + randomPart; }

为什么不用UUID?因为订单号需满足:

  • 人眼可读性:运维查日志时能大致看出时间;
  • 无序性:避免被爬虫按序号遍历全部订单;
  • 长度可控:32位UUID太长,影响数据库索引效率;
  • 无依赖:不依赖Redis或Snowflake服务,单机可部署。

但此方案在QPS>1000时有极小概率重复(毫秒内生成超1000个),若你后续要扩容,建议替换为AtomicLong计数器+时间戳组合,或接入分布式ID生成器。

3.4 避坑:J2EE开发中五个高频翻车现场

现象 → 原因 → 解决

  1. JSP页面显示EL表达式原文如${user.name},而非实际值
    → 原因:web.xml中<web-app>版本声明低于3.0,或page指令缺isELIgnored="false"
    → 解决:确认web.xml头为version="3.1",并在JSP顶部加<%@ page isELIgnored="false" %>

  2. 登录成功后session.setAttribute("user", user),但下一个页面session.getAttribute("user")为null
    → 原因:浏览器禁用了Cookie,或Tomcat的context.xml中<Context cookies="false"/>
    → 解决:Chrome开发者工具Application → Cookies,确认JSESSIONID存在;检查$TOMCAT_HOME/conf/context.xml,注释掉cookies="false"

  3. MySQL插入中文乱码,数据库和表都是utf8mb4,但Java程序存进去是问号
    → 原因:JDBC URL缺characterEncoding=utf8mb4参数,或MySQL服务端my.cnf未配collation-server = utf8mb4_unicode_ci
    → 解决:jdbc:mysql://localhost:3306/bookstore?useSSL=false&serverTimezone=UTC&characterEncoding=utf8mb4

  4. Druid连接池报maxWait超时,但MySQL监控显示连接数远低于maxActive
    → 原因:druid.properties中removeAbandonedOnBorrow=true导致连接被误回收,或网络闪断未及时检测
    → 解决:关掉removeAbandonedOnBorrow,改用testWhileIdle=true+timeBetweenEvictionRunsMillis=60000主动检测

  5. Tomcat启动后访问/bookstore/login.jsp报404,但/bookstore/首页正常
    → 原因:web.xml中<servlet-mapping>路径写错,如把<url-pattern>/login</url-pattern>写成<url-pattern>login</url-pattern>(缺前导斜杠)
    → 解决:所有<url-pattern>必须以/开头,且<servlet-name>与<servlet>中定义严格一致(区分大小写)


4. 目录结构与源码组织:为什么说它是J2EE教科书级分层范例

4.1 项目根目录:war包结构即运行时结构

解压bookstore.war后,目录结构直击J2EE标准:

bookstore/ ├── WEB-INF/ │ ├── web.xml # 部署描述符:Servlet映射、监听器、上下文参数 │ ├── lib/ # 运行时依赖:druid-1.2.16.jar, jstl-1.2.jar, mysql-connector-java-5.1.47.jar │ ├── classes/ # 编译后的class文件(对应src目录结构) │ │ └── com/cloudfactory/ │ │ ├── servlet/ # Controller层:LoginServlet.class, CartServlet.class │ │ ├── service/ # Service层:BookService.class, OrderService.class │ │ ├── dao/ # DAO层:BookDao.class, UserDao.class │ │ ├── entity/ # 实体类:Book.class, User.class, CartItem.class │ │ └── util/ # 工具类:JdbcUtils.class, StringUtils.class │ └── tags/ # 自定义JSP Tag:formatDate.tag(格式化日期) ├── index.jsp # 欢迎页:包含header/footer,引入book_list.jsp ├── login.jsp # 登录页:表单提交到LoginServlet ├── cart.jsp # 购物车页:遍历session.cart,调用CartServlet?action=update ├── order.jsp # 订单页:显示订单号、金额、收货信息 └── static/ # 静态资源:CSS/JS/IMG(不经过Servlet容器) ├── css/ │ └── style.css ├── js/ │ └── common.js # 表单校验:邮箱格式、密码强度 └── images/ └── logo.png

这个结构完全遵循Java EE 7规范:WEB-INF是安全目录(外部无法直接访问),classes和lib是类加载路径,web.xml是唯一入口配置。对比Spring Boot的resources/static,这里没有魔法,每一步都可追溯。

4.2 Java包命名:com.cloudfactory.xxx 的三层语义

包名设计暴露了作者的工程思维:

  • com.cloudfactory.servlet:纯粹的HTTP协议处理器,只做三件事——解析请求参数、调用Service、设置响应属性或重定向。例如LoginServlet第35行:request.setAttribute("errorMsg", "用户名或密码错误");,然后request.getRequestDispatcher("login.jsp").forward(request, response);—— 它绝不碰数据库,也不处理业务规则。
  • com.cloudfactory.service:业务编排层,一个方法对应一个完整用例(Use Case)。如OrderService.createOrder(List<CartItem>)会依次调用BookService.reduceStock()、UserService.getAddress()、PayService.mockPay(),并用TransactionManager统一控制事务边界。
  • com.cloudfactory.dao:数据访问层,严格遵循“一个DAO对应一张表”,方法名直译SQL意图:BookDao.findByCategory(String category)→SELECT * FROM book WHERE category = ?。没有泛型DAO,没有BaseMapper,所有SQL都在方法体内硬编码,方便审计和优化。

提示:这种分层不是为了炫技,而是为了隔离变化。比如你要把MySQL换成Oracle,只需重写dao包下的所有类,service和servlet完全不动。

4.3 web.xml深度解析:12个关键配置项的作用

WEB-INF/web.xml是整个项目的中枢神经,我们挑最关键的6项解读:

配置项位置作用不配的后果
<welcome-file-list>第15行定义根路径/默认打开的文件,这里是index.jsp访问/bookstore/返回404
<servlet>+<servlet-mapping>第22/35行将URL路径/login绑定到LoginServlet类点击登录按钮无响应
<context-param>第48行设置全局参数db.driver=com.mysql.jdbc.Driver,供JdbcUtils读取Class.forName()找不到驱动类
<listener>第55行注册ContextLoaderListener,在Tomcat启动时初始化Druid连接池应用启动时报NullPointerException(连接池未创建)
<session-config>第62行设定<session-timeout>为30分钟,<cookie-http-only>为true用户登出后,XSS脚本可窃取JSESSIONID
<error-page>第69行404错误跳转到/error/404.jsp,500跳转到/error/500.jsp服务器异常时暴露堆栈,存在信息泄露风险

特别注意<listener>:ContextLoaderListener实现了ServletContextListener接口,在contextInitialized()中调用DruidDataSource.init(),这才是连接池真正创建的时刻。很多新手在JdbcUtils.getConnection()里直接new DruidDataSource(),导致每次获取连接都新建池——这是性能杀手。

4.4 JSP页面设计:JSTL+EL如何替代Java Scriptlet

项目彻底禁用<% %>脚本片段,全部用JSTL+EL实现逻辑。以book_list.jsp为例:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <!-- 1. 判断图书列表是否为空 --> <c:if test="${empty requestScope.bookList}"> <div class="no-data">暂无图书</div> </c:if> <!-- 2. 遍历图书列表 --> <c:forEach items="${requestScope.bookList}" var="book" varStatus="status"> <div class="book-item"> <h3>${book.title}</h3> <p>作者:${book.author}</p> <p>价格:<fmt:formatNumber value="${book.price}" type="currency"/></p> <p>库存:<c:out value="${book.stock}"/></p> <!-- 3. 条件渲染:库存>0显示购买按钮,否则灰显 --> <c:choose> <c:when test="${book.stock > 0}"> <a href="cart?action=add&id=${book.id}" class="btn-buy">加入购物车</a> </c:when> <c:otherwise> <span class="sold-out">已售罄</span> </c:otherwise> </c:choose> </div> </c:forEach>

这里展示了JSTL三大核心能力:

  • <c:if>:替代<% if(...) { %>,避免Java代码混入HTML;
  • <c:forEach>:替代<% for(...) { %>,varStatus提供索引、奇偶判断;
  • <fmt:formatNumber>:国际化数字格式,比String.format("%.2f", price)更安全(防空指针)。

注意:<c:out value="${book.title}"/>比${book.title}多一层XSS防护,会自动转义<script>标签。生产环境所有输出必须用<c:out>。


5. 二次开发实战:从接入微信支付到适配MySQL 8.0的五步改造

5.1 接入微信支付:在OrderServlet中嵌入支付网关

原项目订单生成后直接跳转order_success.jsp,现在要接入微信H5支付。改造点在src/com/cloudfactory/servlet/OrderServlet.java的createOrder()方法(第78行):

// 原逻辑:生成订单后直接跳转 // request.getRequestDispatcher("order_success.jsp").forward(request, response); // 新逻辑:调用微信统一下单API String openId = (String) session.getAttribute("openId"); // 前提:用户已授权获取openId if (openId == null) { // 重定向到微信OAuth2授权页 String redirectUrl = URLEncoder.encode("http://yourdomain.com/bookstore/order", "UTF-8"); response.sendRedirect("https://open.weixin.qq.com/connect/oauth2/authorize?" + "appid=wx1234567890&response_type=code&scope=snsapi_base&redirect_uri=" + redirectUrl + "#wechat_redirect"); return; } // 构造微信支付参数 Map<String, String> params = new HashMap<>(); params.put("appid", "wx1234567890"); params.put("mch_id", "1234567890"); params.put("nonce_str", WXPayUtil.generateNonceStr()); // 工具类生成随机串 params.put("body", "云工厂书城订单"); params.put("out_trade_no", OrderService.generateOrderNo()); // 复用原有订单号生成器 params.put("total_fee", String.valueOf((int)(orderAmount * 100))); // 微信单位为分 params.put("spbill_create_ip", request.getRemoteAddr()); params.put("notify_url", "http://yourdomain.com/bookstore/pay/notify"); // 异步回调地址 params.put("trade_type", "MWEB"); // H5支付 // 签名并发送请求 String sign = WXPayUtil.generateSignature(params, "your_mch_key"); // 商户密钥 params.put("sign", sign); String xmlResult = WXPayUtil.httpPost("https://api.mch.weixin.qq.com/pay/unifiedorder", WXPayUtil.mapToXml(params)); // 解析返回XML,提取mweb_url Map<String, String> result = WXPayUtil.xmlToMap(xmlResult); if ("SUCCESS".equals(result.get("return_code")) && "SUCCESS".equals(result.get("result_code"))) { String mwebUrl = result.get("mweb_url"); // 重定向到微信支付页 response.sendRedirect(mwebUrl + "&redirect_url=" + URLEncoder.encode("http://yourdomain.com/bookstore/pay/result", "UTF-8")); } else { request.setAttribute("errorMsg", "支付初始化失败:" + result.get("err_code_des")); request.getRequestDispatcher("order.jsp").forward(request, response); }

关键改造步骤:

  1. 新增依赖:在WEB-INF/lib/放入wxpay-sdk-3.0.9.jar(微信官方SDK);
  2. 新增回调Servlet:创建PayNotifyServlet处理微信异步通知,校验签名后调用OrderService.updateOrderStatus(orderNo, "paid");
  3. 数据库加字段:ALTER TABLE orders ADD COLUMN pay_status VARCHAR(20) DEFAULT 'unpaid';;
  4. Nginx配置:确保/pay/notify路径能被外网访问(微信服务器会POST过来);
  5. HTTPS强制:微信H5支付要求域名备案且启用HTTPS,本地调试用ngrok映射。

5.2 适配MySQL 8.0:认证插件与时区的双重升级

MySQL 8.0默认认证插件从mysql_native_password改为caching_sha2_password,导致JDBC连接报Client does not support authentication protocol requested by server。解决分两步:

第一步:修改MySQL用户认证方式

-- 登录MySQL 8.0,执行 ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password'; FLUSH PRIVILEGES;

第二步:升级JDBC驱动并加参数

  • 删除旧mysql-connector-java-5.1.47.jar,下载mysql-connector-java-8.0.33.jar放入WEB-INF/lib/;
  • 修改src/com/cloudfactory/util/JdbcUtils.java中getConn()方法的URL:
    // 原URL(MySQL 5.7) // private static final String URL = "jdbc:mysql://localhost:3306/bookstore?useSSL=false"; // 新URL(MySQL 8.0) private static final String URL = "jdbc:mysql://localhost:3306/bookstore?" + "useSSL=false&" + "serverTimezone=Asia/Shanghai&" + // 必须指定时区,否则Timestamp乱码 "allowPublicKeyRetrieval=true&" + // 允许公钥检索(8.0新特性) "characterEncoding=utf8mb4"; // 保持原有字符集

血泪经验:serverTimezone不配会导致new Timestamp(System.currentTimeMillis())存入数据库变成1970年。用SELECT @@global.time_zone, @@session.time_zone;确认MySQL时区是SYSTEM还是+08:00,再决定JDBC参数写Asia/Shanghai还是GMT%2B8。

5.3 前端皮肤替换:用Bootstrap 5重构所有JSP页面

原项目用纯CSS写样式,现在要套Bootstrap 5。以login.jsp为例,改造前后对比:

原代码(脆弱易错):

<div class="login-box"> <form action="login" method="post"> <input type="text" name="username" placeholder="用户名" class="input-text"> <input type="password" name="password" placeholder="密码" class="input-text"> <button type="submit" class="btn-login">登录</button> </form> </div> <style> .login-box { width: 400px; margin: 50px auto; } .input-text { width: 100%; padding: 10px; } .btn-login { background: #007bff; color: white; } </style>

Bootstrap 5重构(响应式+组件化):

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>用户登录 - 云工厂书城</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> </head> <body> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-6"> <div class="card"> <div class="card-header bg-primary text-white"> <h4 class="mb-0">用户登录</h4> </div> <div class="card-body"> <c:if test="${not empty errorMsg}"> <div class="alert alert-danger">${errorMsg}</div> </c:if> <form action="login" method="post"> <div class="mb-3"> <label for="username" class="form-label">用户名</label> <input type="text" class="form-control" id="username" name="username" required> </div> <div class="mb-3"> <label for="password" class="form-label">密码</label> <input type="password" class="form-control" id="password" name="password" required> </div> <button type="submit" class="btn btn-primary w-100">登录</button> </form> </div> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> </body> </html>

改造要点:

  • 所有<div class="row/col">用Bootstrap栅格系统,自动适配手机;
  • 表单控件用form-control、form-label,错误提示用alert-danger;
  • 移除所有内联CSS,样式全由CDN托管;
  • required属性启用浏览器原生校验,比JS校验更可靠。

5.4 日志系统升级:从System.out.println到Log4j2

原项目用System.out.println()打日志,无法分级、无法滚动、无法输出到文件。升级到Log4j2只需三步:

1. 加依赖:log4j-api-2.20.0.jar+log4j-core-2.20.0.jar+log4j-web-2.20.0.jar放入WEB-INF/lib/;
2. 加配置:在WEB-INF/classes/log4j2.xml写:

<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <RollingFile name="File" fileName="logs/bookstore.log" filePattern="logs/$${date:yyyy-MM}/app-%d{MM-dd-yyyy}-%i.log.gz"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <TimeBasedTriggeringPolicy /> <SizeBasedTriggeringPolicy size="10 MB"/> </RollingFile> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="Console <p> <a href="https://download.csdn.net/download/u013983710/7020915" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 15:30:52

基于EAR与dlib的关键点眨眼检测系统设计

简介&#xff1a;本资源是一套面向计算机视觉初学者与Python开发者的实战项目&#xff0c;聚焦实时人眼识别、眨眼检测与闭眼状态判别三大核心功能&#xff0c;适用于疲劳监测、人机交互、注意力分析等实际场景。压缩包共61个文件&#xff0c;含37个Python源码&#xff08;如Wi…

作者头像 李华
网站建设 2026/11/2 15:29:51

数据治理工具怎么选?数据标准质量安全一体化选型

数据治理市场的热度&#xff0c;可以从一组数字看出。Research Nester 测算显示&#xff0c;2025 年全球数据治理市场规模约 56 亿美元&#xff0c;2026 年预计达到 67 亿美元&#xff0c;到 2035 年有望增至 383 亿美元&#xff0c;年均复合增长率约 21.2%。世界经济论坛 2026…

作者头像 李华
网站建设 2026/11/2 15:26:12

5、使用STM32CubeMX新建工程(手把手教会你)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/2 15:25:51

零售与数仓(四):指标定义——从业务问题到可信计算

系列前篇&#xff1a;业务判断&#xff5c;数据证据&#xff5c;建设取舍 运营说促销效果不错&#xff0c;财务却认为还不能判断。双方未必是谁算错了&#xff0c;也可能一个人在看支付金额&#xff0c;另一个人在看扣除退款和费用后的贡献。 这类问题说明&#xff1a;指标定义…

作者头像 李华
网站建设 2026/11/2 15:25:04

音频降噪剪辑工具,音量裁剪一键处理

软件介绍 这次要聊的软件叫 ocenaudio&#xff0c;由巴西团队开发&#xff0c;是一款音频编辑工具。它支持多个平台&#xff0c;Windows、Mac、Linux 都能用&#xff0c;换系统的时候也不用愁找不到顺手的替代品。 版本与上手难度 Windows 这边既有安装版也有绿色版&#xf…

作者头像 李华