简介这是一份基于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/MyBatisService层带事务控制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 xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1Tomcat 8.0才完全兼容第三JDBC驱动用的是mysql-connector-java-5.1.47.jar它要求JDK 85.1.46开始弃用JDK 7支持。所以你的环境必须是JDK1.8.0_202 或更高java -version确认Tomcat8.5.90 或 9.0.83避免8.5.0这种早期bug版MySQL5.7.32 或 8.0.26注意8.0需额外配置useSSLfalseserverTimezoneUTC提示不要用IntelliJ内置的Tomcat插件直接Run——它常默认加载tomcat-juli.jar冲突。务必下载官方zip包解压后在IDEA中选External Tomcat。2.2 数据库初始化执行sql脚本前必须改的三处硬编码项目根目录下sql/bookstore_init.sql是建库建表语句但其中三处必须手动修改否则启动报Unknown database bookstore或Table bookstore.user doesnt 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,adminbookstore.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.shLinux/Mac或startup.batWin观察$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.jarArtifacts → → Web Application: Archive → Output Directory设为out/artifacts/bookstore_war_explodedRun → Edit Configurations → Tomcat Server → Deployment → → Artifact → 选择bookstore:war exploded启动后修改JSPCtrlS保存即刷新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.jspindex.jsp第一行% include fileheader.jsp%引入公共头header.jsp中c:if test${empty sessionScope.user}判断登录态 → 调用session.getAttribute(user)若未登录页面显示“请登录”点击跳转/login.jsp若已登录index.jsp底部jsp:include pagebook_list.jsp/加载图书列表book_list.jsp通过c:forEach items${requestScope.bookList} varbook遍历数据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和ConnectionDruid会归还连接池 JdbcUtils.close(ps3, ps2, ps1, conn); } }关键点说明FOR UPDATE在查询库存时加行锁避免两个请求同时读到stock1然后都扣减导致负库存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 CartServletHTTP Session状态管理的血泪经验购物车功能藏在src/com/cloudfactory/servlet/CartServlet.java它用HttpSession存储ListCartItem但这里有个经典坑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 ListCartItem cart (ListCartItem) 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位随机数格式例如20231015142305123456public 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服务单机可部署。但此方案在QPS1000时有极小概率重复毫秒内生成超1000个若你后续要扩容建议替换为AtomicLong计数器时间戳组合或接入分布式ID生成器。3.4 避坑J2EE开发中五个高频翻车现场现象 → 原因 → 解决JSP页面显示EL表达式原文如${user.name}而非实际值→ 原因web.xml中web-app版本声明低于3.0或page指令缺isELIgnoredfalse→ 解决确认web.xml头为version3.1并在JSP顶部加% page isELIgnoredfalse %登录成功后session.setAttribute(user, user)但下一个页面session.getAttribute(user)为null→ 原因浏览器禁用了Cookie或Tomcat的context.xml中Context cookiesfalse/→ 解决Chrome开发者工具Application → Cookies确认JSESSIONID存在检查$TOMCAT_HOME/conf/context.xml注释掉cookiesfalseMySQL插入中文乱码数据库和表都是utf8mb4但Java程序存进去是问号→ 原因JDBC URL缺characterEncodingutf8mb4参数或MySQL服务端my.cnf未配collation-server utf8mb4_unicode_ci→ 解决jdbc:mysql://localhost:3306/bookstore?useSSLfalseserverTimezoneUTCcharacterEncodingutf8mb4Druid连接池报maxWait超时但MySQL监控显示连接数远低于maxActive→ 原因druid.properties中removeAbandonedOnBorrowtrue导致连接被误回收或网络闪断未及时检测→ 解决关掉removeAbandonedOnBorrow改用testWhileIdletruetimeBetweenEvictionRunsMillis60000主动检测Tomcat启动后访问/bookstore/login.jsp报404但/bookstore/首页正常→ 原因web.xml中servlet-mapping路径写错如把url-pattern/login/url-pattern写成url-patternlogin/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 TagformatDate.tag格式化日期 ├── index.jsp # 欢迎页包含header/footer引入book_list.jsp ├── login.jsp # 登录页表单提交到LoginServlet ├── cart.jsp # 购物车页遍历session.cart调用CartServlet?actionupdate ├── 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(ListCartItem)会依次调用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/返回404servletservlet-mapping第22/35行将URL路径/login绑定到LoginServlet类点击登录按钮无响应context-param第48行设置全局参数db.drivercom.mysql.jdbc.Driver供JdbcUtils读取Class.forName()找不到驱动类listener第55行注册ContextLoaderListener在Tomcat启动时初始化Druid连接池应用启动时报NullPointerException连接池未创建session-config第62行设定session-timeout为30分钟cookie-http-only为true用户登出后XSS脚本可窃取JSESSIONIDerror-page第69行404错误跳转到/error/404.jsp500跳转到/error/500.jsp服务器异常时暴露堆栈存在信息泄露风险特别注意listenerContextLoaderListener实现了ServletContextListener接口在contextInitialized()中调用DruidDataSource.init()这才是连接池真正创建的时刻。很多新手在JdbcUtils.getConnection()里直接new DruidDataSource()导致每次获取连接都新建池——这是性能杀手。4.4 JSP页面设计JSTLEL如何替代Java Scriptlet项目彻底禁用% %脚本片段全部用JSTLEL实现逻辑。以book_list.jsp为例% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % !-- 1. 判断图书列表是否为空 -- c:if test${empty requestScope.bookList} div classno-data暂无图书/div /c:if !-- 2. 遍历图书列表 -- c:forEach items${requestScope.bookList} varbook varStatusstatus div classbook-item h3${book.title}/h3 p作者${book.author}/p p价格fmt:formatNumber value${book.price} typecurrency//p p库存c:out value${book.stock}//p !-- 3. 条件渲染库存0显示购买按钮否则灰显 -- c:choose c:when test${book.stock 0} a hrefcart?actionaddid${book.id} classbtn-buy加入购物车/a /c:when c:otherwise span classsold-out已售罄/span /c:otherwise /c:choose /div /c:forEach这里展示了JSTL三大核心能力c:if替代% if(...) { %避免Java代码混入HTMLc: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? appidwx1234567890response_typecodescopesnsapi_baseredirect_uri redirectUrl #wechat_redirect); return; } // 构造微信支付参数 MapString, 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 MapString, 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 rootlocalhost 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// 原URLMySQL 5.7 // private static final String URL jdbc:mysql://localhost:3306/bookstore?useSSLfalse; // 新URLMySQL 8.0 private static final String URL jdbc:mysql://localhost:3306/bookstore? useSSLfalse serverTimezoneAsia/Shanghai // 必须指定时区否则Timestamp乱码 allowPublicKeyRetrievaltrue // 允许公钥检索8.0新特性 characterEncodingutf8mb4; // 保持原有字符集血泪经验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 classlogin-box form actionlogin methodpost input typetext nameusername placeholder用户名 classinput-text input typepassword namepassword placeholder密码 classinput-text button typesubmit classbtn-login登录/button /form /div style .login-box { width: 400px; margin: 50px auto; } .input-text { width: 100%; padding: 10px; } .btn-login { background: #007bff; color: white; } /styleBootstrap 5重构响应式组件化% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1 title用户登录 - 云工厂书城/title link hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.2/dist/css/bootstrap.min.css relstylesheet /head body div classcontainer mt-5 div classrow justify-content-center div classcol-md-6 div classcard div classcard-header bg-primary text-white h4 classmb-0用户登录/h4 /div div classcard-body c:if test${not empty errorMsg} div classalert alert-danger${errorMsg}/div /c:if form actionlogin methodpost div classmb-3 label forusername classform-label用户名/label input typetext classform-control idusername nameusername required /div div classmb-3 label forpassword classform-label密码/label input typepassword classform-control idpassword namepassword required /div button typesubmit classbtn btn-primary w-100登录/button /form /div /div /div /div /div script srchttps://cdn.jsdelivr.net/npm/bootstrap5.3.2/dist/js/bootstrap.bundle.min.js/script /body /html改造要点所有div classrow/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.jarlog4j-core-2.20.0.jarlog4j-web-2.20.0.jar放入WEB-INF/lib/2. 加配置在WEB-INF/classes/log4j2.xml写?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console RollingFile nameFile fileNamelogs/bookstore.log filePatternlogs/$${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 size10 MB/ /RollingFile /Appenders Loggers Root levelinfo AppenderRef refConsole p a hrefhttps://download.csdn.net/download/u013983710/7020915 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