
简介基于jspservletmysql实现的游戏购买网站是面向JavaWeb初学者、毕业设计及课程设计人群的完整项目源码包。站点包含管理员与用户两类角色管理员可进行登录、订单管理、客户管理、游戏管理和类目管理用户则可完成首页浏览、登录、购买游戏、购物车管理和提交订单基本覆盖电商交易核心流程。资源共246个文件压缩包大小14.85MB主要包含Java源文件与编译后的class文件、JSP页面、SQL脚本、Servlet及DAO层代码同时配有CSS、JavaScript前端样式脚本、Jar依赖库和图片素材目录结构清晰便于直接导入Eclipse、MyEclipse或IDEA配合Tomcat8/9运行。已有95人学习下载比较适合希望在真实项目中理解ServletJSPMVC分层开发、掌握用户权限与订单流程的开发者参考。1. 一个能跑通的jspservletmysql游戏商城从建表到订单闭环你搜“jspservletmysql 游戏购买网站”多半不是想听理论是手里有个课程设计或毕设题目压着要么是导师给了个老技术栈题目要么是你想用最基础的方式把Java Web整条链路跑通。这个项目正好是一个完整的游戏商城案例用户注册登录、游戏商品列表、购物车、下单结算全链路用jspservletmysql实现没有Spring全家桶加持每个环节都能直接看到request和response在怎么流转。我先说结论这套技术栈虽然老但作为教学和毕设项目非常合适它能帮你把javabean、servlet生命周期、session管理、preparedstatement防注入、事务手动提交这些概念全部落地。文章后面每一章都有可抄的代码和参数说明你照着建库建表、部署到tomcat差不多半天能跑起来。新手卡住最多的三个点——中文乱码、jar包冲突、下单选一半库存被扣——我在避坑章节都会给到具体解决方式。2. 技术选型与数据库设计为什么jspservlet还能打2.1 这个项目为什么不用Spring Boot反而是传统jsp项目打包war现在网上大量教程是springboot集成jsp但springboot官方对jsp的支持其实很弱最终要打成war包部署到外部tomcatjsp才能被正确编译。而这个项目采用“传统jsp项目打包war”的方式直接跑在tomcat的webapps目录下jsp会被tomcat的jasper引擎实时编译成servlet类整个链路最朴素也最容易排查。用这个技术栈的核心逻辑有三个。第一这是javaweb头歌实训和很多学校课程设计的标准配置题目就是jspservlet你用springboot反而不符合要求。第二servlet生命周期你可以在代码里清晰看到init()在第一次请求时执行service()处理每次请求destroy()在容器关闭时调用。这种直观性对于理解HTTP无状态协议和服务器端组件的工作原理非常关键。第三jsp本质上就是servlet的模板化封装页面里可以写java代码也可以配合jstl标签库调试起来比前后端分离项目少一层跨域和接口联调成本。我一般会把jspservlet项目理解成MVC的原始形态jsp充当viewservlet充当controllerjavabeandao充当model。这个结构的优势是每个文件的职责边界特别清楚出了问题你知道去哪一行找答案。劣势是当页面交互复杂时jsp里容易塞太多业务代码所以下文我会强调把业务逻辑下沉到service层。2.2 数据库设计用户表、游戏表、订单表、订单明细表游戏购买网站的核心表不用多四张足够user表存账号信息game表存游戏商品名称、价格、封面图路径、库存、类型orders表存订单主信息订单号、用户id、总金额、状态order_item表存订单明细每款游戏买了几份、下单时的单价。为什么要单独拆order_item表因为一张订单可能包含多个游戏如果不拆明细订单表里就要存集合字段这在关系型数据库里是大忌统计销量和退款时都会很难受。CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(64) NOT NULL, email varchar(100) DEFAULT NULL, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE game ( id int(11) NOT NULL AUTO_INCREMENT, game_name varchar(100) NOT NULL, price decimal(10,2) NOT NULL, stock int(11) NOT NULL DEFAULT 0, cover_path varchar(255) DEFAULT NULL, game_type varchar(50) DEFAULT NULL, description text, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;说明用户名加了唯一索引注册时直接用duplicate key报错来拦截重复用户名比先查再插更稳避免并发注册时两条相同用户名的请求同时通过查询校验。游戏表里price用decimal而不是double因为浮点数在价格计算上会出0.10.2不等于0.3的玄学问题decimal(10,2)能存十位数带两位小数游戏售价完全够用。订单表需要着重设计状态字段我建议用tinyint存数字状态而不是直接存字符串0表示待支付1表示已支付2表示已发货如果是实体游戏卡3表示已取消。数字状态的好处是扩展状态机时不用改表结构加一个注释就行。订单号不要用自增id直接暴露给用户我用的是时间戳加用户id加随机数的拼接方式保证唯一且不易被遍历。CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id int(11) NOT NULL, total_price decimal(10,2) NOT NULL, status tinyint(4) DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, game_id int(11) NOT NULL, game_name varchar(100) DEFAULT NULL, price decimal(10,2) NOT NULL, quantity int(11) NOT NULL DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;我会建议order_item里冗余game_name和price字段而不是下单时去join游戏表。因为游戏可能会改价或改名如果只存game_id历史订单的金额就对不上了。电商行业管这个叫快照你用jsp做项目时可能觉得多余但答辩时说出“下单时做了商品快照防止后续商品信息变更影响订单历史”这句话分数会不一样。2.3 数据库连接池别再用DriverManager硬编码了很多新手教程里是Class.forName然后DriverManager.getConnection每次请求都新建连接。这个项目我建议你引入druid连接池jar包放在WEB-INF/lib下配置一个druid.properties文件。连接池的好处是复用连接、控制最大连接数还能监控慢SQL。按这个配置走mysql连接不会因为你频繁开关连接而报Too many connections。driverClassNamecom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/game_shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password你的密码 initialSize5 maxActive20 maxWait60000说明useSSLfalse一定要加mysql8以上版本默认尝试ssl连接本地开发环境你根本没配证书就会报ssl连接错误白屏半天查不到原因。serverTimezoneAsia/Shanghai是处理mysql驱动拿到的时间比本地时间少8小时的问题加了这个参数后数据库里的datetime才能正确对应中国时区。url里useUnicodetrue和characterEncodingutf8是中文存取不乱码的第一道防线后面避坑章节还会说到第二道防线。获取连接的工具类可以直接封装成一个静态方法从druid连接池拿连接每次用完后调用close归还而不是真正关闭这是连接池的核心使用习惯。public class DbUtils { private static DruidDataSource dataSource; static { try { Properties props new Properties(); props.load(DbUtils.class.getClassLoader().getResourceAsStream(druid.properties)); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }逻辑说明静态代码块在类加载时执行一次完成数据源的初始化。getConnection每次调用拿到的都是从池里借出的连接对象使用方finally里close之后连接会回到池中而不是真正断开。参数上需要注意的是druid的初始连接数5和最大活跃数20同时在线人数不高的教学项目20够用如果你用mysql默认配置做压测把maxActive调低反而能避免连接数被占满。3. 用户模块与游戏展示jsp个人信息展示页面的完整链路3.1 注册功能preparedStatement防注入与md5加盐用户注册是第一个完整的“jsp页面→servlet接收参数→dao操作数据库→返回结果”闭环。注册页的form提交到UserRegistServletservlet里先取参数再校验注意我一般不用getParameter取完就直接拼sql而是走preparedStatement占位符方式。WebServlet(/user/regist) public class UserRegistServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(utf-8); String username request.getParameter(username); String password request.getParameter(password); String email request.getParameter(email); String salt UUID.randomUUID().toString().substring(0, 8); String hashedPwd DigestUtils.md5DigestAsHex((password salt).getBytes()); UserDao dao new UserDao(); boolean ok dao.regist(username, hashedPwd, salt, email); if (ok) { response.sendRedirect(request.getContextPath() /login.jsp); } else { request.setAttribute(msg, 注册失败用户名可能已存在); request.getRequestDispatcher(/regist.jsp).forward(request, response); } } }逻辑说明request.setCharacterEncoding(utf-8)必须在取第一个参数之前调用否则中文用户名进来就是乱码。密码没有明文存库先取一个8位随机盐把盐拼在密码后面做md5数据库里存盐值和hash值两个字段。登录时你用同样规则拼接md5再比对就能验证密码。sendRedirect是重定向地址栏会变适合注册成功这种操作避免刷新页面重复提交表单forward是转发request域里的msg还能在jsp页面通过el表达式取到并提示。参数说明md5虽然从密码学角度已经不够安全但课程设计场景没人会拿彩虹表攻击你加盐后彩虹表基本失效。可能有人直接对密码做md5不加盐这在答辩时会被问倒因为同样的密码会产生同样的hash值看到库就能猜出弱口令。3.2 登录状态管理session与cookie的分工登录成功后要解决“用户后续请求怎么证明自己是登录用户”的问题。我用session保存用户对象sessionid通过cookie写到浏览器端。你在login页面验证账号密码通过后执行User user dao.findByUsernameAndPwd(username, hashedPwd); if (user ! null) { HttpSession session request.getSession(); session.setAttribute(loginUser, user); response.sendRedirect(request.getContextPath() /game/list); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); }说明session的默认超时时间是30分钟tomcat的web.xml里可以改但项目里不推荐改全局配置可以用session.setMaxInactiveInterval(60*30)单独控制。把整个user对象放进session而不是只存userid是为了后续页面直接通过${sessionScope.loginUser.username}展示用户名。登出时调用session.invalidate()干掉整个会话而不是只移除loginUser属性防止其他session数据残留。用户是否登录的校验我放在一个叫AuthFilter的过滤器里统一处理而不是在每个servlet里重复写判断。这也是servlet生命周期里除了三大组件外最常被问到的listener和filter。WebFilter(/user/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { ((HttpServletResponse) resp).sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段代码的坑在于request.getSession(false)和getSession()的区别前者在session不存在时返回null后者会新建一个session。如果用户没登录就访问受保护资源getSession()会浪费一个session对象更关键的是过滤逻辑会失效。过滤路径/user/只拦截以/user/开头的请求购物车和订单的servlet路径如果也属于受保护范围需要把过滤路径改成更宽的粒度或者再用一个过滤器挂在/order/、/cart/*上。3.3 游戏列表与搜索mysql模糊搜索与分页参数实践游客能看到游戏列表这个页面不要求登录。我用一个GameListServlet支持两种请求不带参数时查全部带keyword时做模糊搜索。mysql的like查询是最简单的实现方式但要注意中文搜索时的乱码问题本质还是url和请求编码没统一。我给前端搜索表单的提交方式是get因为搜索是幂等操作地址栏暴露关键词也无所谓还能方便用户刷新不弹窗。String keyword request.getParameter(keyword); String sql SELECT * FROM game WHERE game_name LIKE ? OR game_type LIKE ?; try (PreparedStatement ps conn.prepareStatement(sql)) { String likePattern % keyword %; ps.setString(1, likePattern); ps.setString(2, likePattern); ResultSet rs ps.executeQuery(); ListGame gameList new ArrayList(); while (rs.next()) { Game game new Game(); game.setId(rs.getInt(id)); game.setGameName(rs.getString(game_name)); game.setPrice(rs.getBigDecimal(price)); game.setStock(rs.getInt(stock)); game.setCoverPath(rs.getString(cover_path)); game.setGameType(rs.getString(game_type)); gameList.add(game); } request.setAttribute(gameList, gameList); request.getRequestDispatcher(/game_list.jsp).forward(request, response); }逻辑说明这里手动对ResultSet逐行取字段再封装成game对象是最原始的写法。实际做项目时你可以用BeanUtils.populate或者直接用dbutils的QueryRunner但我建议你手动写一遍因为答辩老师问“ResultSet和Statement的生命周期”时你能答得上。分页参数我一般在项目里这样设计pageSize固定12每页展示的游戏数量pageNo由当前请求参数传入总页数通过count查询得到。这里有一个课堂上学不到的细节——mysql的limit优化。当数据量超过几千条后limit 10000, 12会让mysql扫描前面一万行再丢弃性能会断崖下跌。常见做法是记录上一页最后一条记录的id下次查询用where id 上一页最大id limit 12。游戏商品表的id是自增的这个方案几乎零成本。SELECT * FROM game WHERE id #{lastId} ORDER BY id ASC LIMIT 12这个写法的前提是用户只看下一页而不随便跳页游戏商城场景用户基本都是顺序浏览很少会从第3页直接跳到第11页所以我最终采用了这个相对极端的“禁止跳页”方案。如果你确实需要页码跳转就老老实实用count加limit偏移量。4. 购物车与下单session存储与mysql事务的一致性4.1 购物车的两种存储方案session map还是数据库表购物车在这个项目里我建议用session存因为课程设计时间有限而且购物车属于临时数据不需要强一致。session里放一个MapInteger, CartItemkey是游戏idvalue是购物车条目对象。加购逻辑在CartAddServlet中处理HttpSession session request.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new LinkedHashMap(); session.setAttribute(cart, cart); } int gameId Integer.parseInt(request.getParameter(gameId)); Game game gameDao.findById(gameId); CartItem item cart.get(gameId); if (item ! null) { item.setQuantity(item.getQuantity() 1); } else { cart.put(gameId, new CartItem(game, 1)); } response.sendRedirect(request.getContextPath() /cart.jsp);这里用LinkedHashMap而不是HashMap是为了保证购物车列表的展示顺序和加入顺序一致。HashMap的遍历顺序不确定用户连续加三个游戏刷新购物车发现顺序乱了会被当作bug提上来。每次加购时直接查一次数据库拿最新价格和库存而不是用浏览器传过来的价格参数这样能防止用户通过修改表单价格来低价购买——虽然jsp项目不太会有人攻击但这个习惯很重要。如果你要答辩发挥得更好我建议你主动说出这个设计取舍session购物车适合未登录游客也可以加购下单前强制跳登录就行数据库购物车表适合跨设备同步购物车但需要多设计一张表代码量翻倍。课程设计选session方案完全够用。4.2 下单事务先扣库存还是先插订单顺序有讲究下单是最容易翻车的地方核心问题是并发环境下的超卖。我最终采用的事务顺序是先update游戏表扣减库存再insert订单主表再insert订单明细。为什么把扣库存放在最前面因为update是行级锁操作先拿到锁的请求会把库存扣到自己下面后面的请求必须等锁释放。整个流程包在一个事务里任何一个环节失败就整体回滚。Connection conn null; try { conn DbUtils.getConnection(); conn.setAutoCommit(false); // 1. 扣减库存必须带上stock 0条件 String deductSql UPDATE game SET stock stock - 1 WHERE id ? AND stock 0; PreparedStatement deductPs conn.prepareStatement(deductSql); deductPs.setInt(1, gameId); int rows deductPs.executeUpdate(); if (rows 0) { throw new RuntimeException(库存不足); } // 2. 生成订单号并插入订单主表 String orderNo generateOrderNo(userId); String insertOrder INSERT INTO orders(order_no, user_id, total_price, status) VALUES(?,?,?,0); // 通过RETURN_GENERATED_KEYS拿到自增id用来插入明细表 PreparedStatement orderPs conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); // 3. 插入订单明细循环遍历购物车 conn.commit(); } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } response.sendRedirect(request.getContextPath() /order/fail?reason e.getMessage()); } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } }逻辑说明扣库存sql里的stock 0是防超卖的关键条件。没有这个条件两个请求同时读到stock1各自执行update set stockstock-1最终库存变成-1订单却都创建成功了。有了这个条件第二个请求的update影响行数是0直接进入异常分支回滚。conn.setAutoCommit(false)开启手动事务commit后close。还需注意finally里要恢复autoCommit为true再归还连接池否则连接池里的连接全部处于事务未提交状态下一个借用到这个连接的人操作全都不生效——这个坑特别隐蔽我遇到过一次排查了整整一下午。订单号的生成方式我写在下面把时间戳取到毫秒级加用户id后四位再加三位随机数基本够用。千万不要用UUID去掉横线的36位字符串当订单号太长而且没有业务含义打印出来用户也不好看。private String generateOrderNo(int userId) { SimpleDateFormat sdf new SimpleDateFormat(yyyyMMddHHmmssSSS); String timeStr sdf.format(new Date()); int random (int)(Math.random() * 900) 100; return timeStr String.format(%04d, userId % 10000) random; }4.3 下单后购物车清理与session同步问题订单提交成功之后一定要把session里的购物车清空否则用户返回购物车页面发现东西还在再点结算又下一单数据库里出现重复订单。清理时机放在事务commit成功之后不能放在扣库存之前。如果先清购物车事务回滚了用户的购物车也空了但订单没生成体验很差。这里我养成的习惯是用一个独立的OrderFinishServlet处理下单成功后的跳转和购物车清理不在下单servlet里混着做。下单成功后把订单号放到request域转发到success.jsp页面展示“订单编号xxxxx已提交”告诉用户可以到个人中心查看。用户如果暂时不想支付订单就保持在待支付状态。“购物车已清空但订单还在待支付”这个状态需要重点设计一下。我见过很多课程设计项目下完单购物车清了结果订单也支付不了用户也没地方重新提交。你可以把未支付订单的重新结算入口放在个人中心的订单列表里点击“去支付”时把订单里的明细重新加载进session购物车。这个细节做出来你的项目完整度就比一般同学高一截。5. 避坑指南jspservletmysql实战中的五个高频翻车现场5.1 中文乱码三处设置缺一不可现象注册的用户名是中文存入mysql变成问号或者页面展示出来是乱码。原因乱码本质上是字符集链路断裂。浏览器表单页面用什么编码提交servlet用什么编码接收mysql表用什么编码存储这三处必须一致。很多人只在数据库连接url里设置了characterEncodingutf8但servlet里忘了调request.setCharacterEncoding或者jsp页面本身没有声明pageEncodingutf-8。解决第一每个jsp页面头部写死pageEncodingutf-8和contentTypetext/html; charsetutf-8。第二在servlet的doPost方法第一行加request.setCharacterEncoding(utf-8)。第三在过滤器里写一个EncodingFilter统一处理所有请求和响应的编码比在每个servlet里重复写更可靠。public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(utf-8); response.setCharacterEncoding(utf-8); response.setContentType(text/html;charsetutf-8); chain.doFilter(request, response); }这个过滤器要映射到/*并且在web.xml里注意filter-mapping的顺序编码过滤器必须放在其他过滤器之前。如果还有乱码去mysql客户端里执行show variables like character%看看数据库服务端字符集mysql8默认utf8mb4基本不用改mysql5.6老版本可能默认latin1需要改my.cnf重启服务。5.2 缺少mysql驱动jar包导致ClassNotFoundException现象tomcat启动正常但访问任意需要操作数据库的页面时后台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因mysql驱动jar包没有放到WEB-INF/lib目录下或者放到了tomcat的lib目录但项目部署时没加载。还有一种情况是mysql驱动版本和mysql服务器版本不匹配mysql8以上要用com.mysql.cj.jdbc.Drivermysql5.x用com.mysql.jdbc.Driver。解决把mysql-connector-java的jar包放入项目的WEB-INF/lib目录eclipse或idea里记得右键jar包选择Add as Library。如果你在idea新建jsp项目时用artifact方式部署还需要在Project Structure的Artifacts选项卡里手动添加jar包到WEB-INF/lib。很多人import了依赖但没进artifact启动不报错一访问数据库就报错就是这个原因。5.3 tomcat启动后端口被占用或JSP编译失败现象eclipse或idea里启动tomcat报Port 8080 was already in use或者访问jsp页面报Unable to compile class for JSP。原因第一个是端口被其他进程占用你在使用多个tomcat实例或者有个java进程没退出。第二个是jsp页面里的java代码有语法错误tomcat的jasper引擎编译时直接抛异常这种报错通常会把具体行号打印出来。解决端口占用时用netstat -ano | findstr 8080查出占用进程的pid然后taskkill /pid 数字 /f或者干脆改tomcat端口到8081。jsp编译失败的常见原因是页面里写了大段java代码引用了没导入的包比如用了list集合但页面顶部没写import java.util.List。我的习惯是jsp页面除了循环和if判断之外尽量不写java代码用jstl的c:forEach和c:if来替代这样能避免大部分编译错误。5.4 连接mysql报Communications link failure现象项目跑了一段时间突然访问数据库报Communications link failure重启tomcat后又正常了。原因mysql默认的wait_timeout是8小时连接池里长期没有请求的连接会被服务端主动关闭但druid不知道下次借出这个连接时发送sql发现底层socket已经断开。这是连接池的经典坑老项目每年都能遇到几次。解决在druid.properties里配置testWhileIdle和timeBetweenEvictionRunsMillis让连接池定期心跳检测连接是否存活。testWhileIdletrue testOnBorrowfalse timeBetweenEvictionRunsMillis60000 validationQuerySELECT 1说明testWhileIdle在连接空闲时做检测validationQuery是探活sqlSELECT 1是最轻量的查询。testOnBorrow设为false是因为每次从池里拿连接都做一次探测会影响性能空闲检测的周期够用就行。这三个参数配好之后mysql8小时断开的问题基本不会再出现。5.5 页面修改了jsp但刷新还是旧版内容现象改完jsp页面刷新浏览器看不到变化tomcat也不报错。原因tomcat开发模式下jsp的修改会被检测并重新编译但有时因为jsp文件时间戳没变或者浏览器缓存了页面。还有一个很隐蔽的情况是项目部署到tomcat的webapps目录和你的工作目录分离了你改的是源码里的jsptomcat运行的却是另一个拷贝。解决idea的debug模式下会直接使用你项目的web目录eclipse的wtpwebapps目录需要重新发布。我一般会先看tomcat真正加载的路径是哪个右键项目选Open Module Settings看Artifacts的输出目录和部署目录是否一致。确认无误后在浏览器强制刷新ctrlF5再不行就清掉tomcat的work目录下的Catalina缓存。work目录里是jasper编译后的class文件删掉后tomcat会从jsp重新编译这一招对“改了没反应”特别有效。6. 部署到tomcat与war包运维把项目交付出去的最后一步6.1 war包构建与部署传统jsp项目打包war放到tomcat下跑是最符合课程设计验收的方式。你自己开发时用eclipse或idea直接部署但最终交作业通常需要导出一个war包导师电脑上装个tomcat就能跑。在idea里的操作路径是File→Project Structure→Artifacts→点加号→Web Application Exploded→选择项目然后再点一次加号选Web Application Archive。Build→Build Artifacts就能生成war包。生成好的war包直接放到tomcat的webapps目录下启动tomcat时会自动解压。这里有个细节war包的名字决定了访问路径。你打成game_shop.war访问地址就是http://localhost:8080/game_shop/。如果你想直接通过根路径访问把war包改名为ROOT.war覆盖webapps下的ROOT目录。6.2 项目部署后访问空空如也或404原因war包是打出来了但缺失了WEB-INF/lib下的依赖jar包或者jsp页面没有打进war包里。解决解压war看结构是否完整重点检查WEB-INF下有没有lib目录、lib目录里jar包是否齐全、pages或jsp目录是否在web根目录下。如果lib丢jar包看一下Artifacts里的Output Layout选项卡“Available Elements”里的jar包右键选Put into WEB-INF/lib。如果war里没有jsp页面多半是Artifacts的Web Resource Directory配置错误。这一步做完导出的war就是一个独立的交付物拷到哪台有tomcat和mysql的机器都能跑。6.3 给验收方的环境准备清单项目做完了但你的运行环境要配好。我一般会在交付说明里提醒三件事第一mysql账号密码要和druid.properties里配的一致我通常建一个专门账号grant所有权限而不是用root。第二mysql要执行一次建表和初始数据的sql脚本最好在交付文档里写明脚本执行顺序。第三jdk版本和tomcat版本匹配tomcat9对应jdk8以上tomcat10对应jdk11以上。另外别忽略一个老生常谈的坑把mysql的sql脚本里的编码头SET NAMES utf8mb4也带上。用navicat或mysql workbench导入sql文件时如果不带编码头中文注释和数据容易乱掉。我自己的习惯是脚本开头写SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci然后再写建表语句。做这类jsp项目多了之后我慢慢形成了一套固定流程拿到题目先设计四张表然后写dao、servlet、jsp页面最后统一做编码过滤器和登录过滤器。每次跑起来第一件事不是点页面而是先启动tomcat看后台日志有没有红色报错。从那以后我每次毕业设计或者帮别人调试都强制走一遍这个顺序——先配连接池参数再写建表脚本然后跑通注册登录最后才碰购物车订单这种复杂逻辑。建议你也按这个节奏来每一步的输入输出是确定的排查范围才不会被撑大。希望帮到你。本文还有配套的精品资源点击获取