简介:这是一份基于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包部署(推荐用于生产模拟)
- 将项目根目录下的
bookstore.war复制到$TOMCAT_HOME/webapps/ - 启动Tomcat:
$TOMCAT_HOME/bin/startup.sh(Linux/Mac)或startup.bat(Win) - 观察
$TOMCAT_HOME/logs/catalina.out,等待出现INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deployment of web application archive [...] has finished in [xxx] ms - 浏览器访问
http://localhost:8080/bookstore/,首页应显示“云工厂-网上书城”Banner和图书列表
方式二:IDEA热部署(开发调试用)
- File → Project Structure → Modules → Dependencies → + → JARs or directories → 添加
lib/*.jar(尤其不能漏jstl-1.2.jar和standard-1.1.2.jar) - Artifacts → + → Web Application: Archive → Output Directory设为
out/artifacts/bookstore_war_exploded - Run → Edit Configurations → Tomcat Server → Deployment → + → Artifact → 选择
bookstore:war exploded - 启动后修改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,整个链路如下:
- Tomcat根据
web.xml中<welcome-file-list>匹配到index.jsp index.jsp第一行<%@ include file="header.jsp"%>引入公共头header.jsp中<c:if test="${empty sessionScope.user}">判断登录态 → 调用session.getAttribute("user")- 若未登录,页面显示“请登录”,点击跳转
/login.jsp - 若已登录,
index.jsp底部<jsp:include page="book_list.jsp"/>加载图书列表 book_list.jsp通过<c:forEach items="${requestScope.bookList}" var="book">遍历数据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开发中五个高频翻车现场
现象 → 原因 → 解决
JSP页面显示EL表达式原文如
${user.name},而非实际值
→ 原因:web.xml中<web-app>版本声明低于3.0,或page指令缺isELIgnored="false"
→ 解决:确认web.xml头为version="3.1",并在JSP顶部加<%@ page isELIgnored="false" %>登录成功后
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"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=utf8mb4Druid连接池报
maxWait超时,但MySQL监控显示连接数远低于maxActive
→ 原因:druid.properties中removeAbandonedOnBorrow=true导致连接被误回收,或网络闪断未及时检测
→ 解决:关掉removeAbandonedOnBorrow,改用testWhileIdle=true+timeBetweenEvictionRunsMillis=60000主动检测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); }关键改造步骤:
- 新增依赖:在
WEB-INF/lib/放入wxpay-sdk-3.0.9.jar(微信官方SDK); - 新增回调Servlet:创建
PayNotifyServlet处理微信异步通知,校验签名后调用OrderService.updateOrderStatus(orderNo, "paid"); - 数据库加字段:
ALTER TABLE orders ADD COLUMN pay_status VARCHAR(20) DEFAULT 'unpaid';; - Nginx配置:确保
/pay/notify路径能被外网访问(微信服务器会POST过来); - 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>