ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java旅游信息网站设计与实现:从Servlet到部署的完整方案

Java旅游信息网站设计与实现:从Servlet到部署的完整方案 简介这是基于Java的旅游信息网站设计与实现文档面向计算机专业毕业设计、课程设计及Web开发学习者可用于参考旅游类网站系统的完整设计方案。文档采用MySQL数据库、Java后端技术在Tomcat服务器与Eclipse平台上开发围绕用户前台和后台管理两大模块展开前台涵盖景点展示、论坛交流、新闻资讯、购物车、客服等功能后台包括用户管理、旅游景点管理、交流论坛管理、订单管理、系统管理等模块。系统采用权限认证方式保障信息安全同时兼顾代码可读性、扩展性与页面简洁性。压缩包内为单个docx文件共1个文件大小约6.01MB目前已有73人学习下载。文档包含中英文摘要、目录及详细章节对需求分析、系统设计、数据库设计及功能实现均有清晰阐述可作为毕业设计文档的写作模板也能为开发同类信息网站提供参考思路。1. 基于Java的旅游信息网站这标题背后是一套完整可落地的Java Web全栈方案“基于java旅游信息网站设计与实现.docx”这个标题在计算机类毕业设计和课程设计题库里属于常青树每年都有一批学生选它。它本质上是一个典型的Java Web全栈项目前台面向游客和注册用户展示景点、旅游线路、酒店信息支持搜索、收藏、预订下单后台面向管理员维护景点和酒店数据、处理订单、管理用户。适合正在挑毕设题目的人也适合想用一个完整CRUD项目把Servlet、JSP、JDBC、MySQL这些知识点串起来练手的开发者。这个方向的好处是需求明确、参照多、答辩容易讲坏处是细节坑不少——中文乱码、金额精度、连接没关导致假死每一条都能让交付时间多出两三天。下面按从选型到落地的顺序把整个方案讲清楚。2. 系统模块与技术栈选型先拆需求再选框架别一上来就建表写Servlet做这类网站最忌讳的是打开IDE就建表、写Servlet写到一半发现管理端功能没地方放。我一般会先花半天把业务模块拆清楚再决定用什么技术栈。模块拆得清楚后面无论是写代码还是写论文都只需要往里填内容。2.1 系统功能拆解前台用户端和后台管理端各管什么旅游信息网站的业务可以拆成两个端它们共享同一套数据库和公共工具类只是代码路径不同。用户端包含注册登录、景点浏览、景点搜索按城市或名称、旅游线路展示、酒店信息展示、在线预订下单、个人中心查看订单、收藏景点、发表评论。管理端包含景点信息增删改查、景点上下架、线路管理、酒店管理、订单状态处理待支付、已支付、已取消、用户列表管理。模块 | 用户端功能 | 管理端功能 景点模块 | 景点列表、详情、按城市搜索 | 景点CRUD、上架/下架 线路模块 | 线路列表、线路详情 | 线路CRUD 酒店模块 | 酒店列表、酒店详情 | 酒店CRUD 订单模块 | 创建订单、查看订单、取消订单 | 订单状态修改 用户模块 | 注册、登录、个人信息 | 用户列表、启用/禁用 评论模块 | 发表评论、查看评论 | 删除违规评论这个拆分不是拍脑袋。旅游网站的订单和普通电商不同订单里涉及景点门票、线路团期、酒店房型三个维度如果一开始不把订单归属到“景点门票”这种单一业务上后面事务处理和价格计算会变得很难收场。所以第一版做减法订单只做景点门票预订线路和酒店先只做信息展示这是大多数毕设项目的合理边界。2.2 JSPServletJDBC还是Spring Boot答辩友好度决定选型选型是第一个容易翻车的决策点。我的建议是如果这是课程设计或毕业设计优先选传统的 JSP Servlet JDBC MySQL Tomcat 组合。原因不是这套技术新而是答辩时老师问“一个请求从浏览器到数据库怎么走”你可以完整画出链路浏览器发请求 → Tomcat 解析 → 找到对应 Servlet → Service 层处理逻辑 → DAO 层拼接 SQL → JDBC 连接数据库 → 结果逐层返回渲染到 JSP。每一个环节都能落到具体代码。Spring Boot 当然开发效率高自动配置把数据源、事务、JSON序列化全包了但这也意味着答辩时老师一句“自动配置的原理是什么”“Spring Boot 怎么帮你省掉了 web.xml”回答不上来就是明显减分。这不是说 Spring Boot 不能选而是说选它之前得先把传统 Servlet 的原理吃透。我见过不少用 Spring Boot 做得挺顺利的同学答辩时被追问底层机制最后只能靠“框架内部处理的”这种话撑场场面很尴尬。如果导师明确要求 Spring Boot MyBatis那就用但本文的核心表结构、字段设计和业务逻辑依然适用。Spring Boot 只是替换了最外层的请求接入方式数据库设计思路和业务代码组织方式是通用的。另外说一句无论是哪种选型建议把 Java 基础里的数据类型、集合、异常处理先复习一遍这些在写 DAO 层和 Service 层时天天要用。2.3 开发环境与Maven工程骨架版本兼容是最大的隐藏坑环境搭配有一个经典组合JDK 8 Tomcat 8.5 MySQL 5.7 Maven 3.6。这组合兼容性最好网上的参考代码几乎都能直接跑。别轻易尝试 JDK 17 Tomcat 10因为 Tomcat 10 把javax.servlet包换成了jakarta.servlet老教程里的import javax.servlet.http.HttpServlet全部编译报错排查起来非常费时间。环境变量配置是老生常谈但必须做对JAVA_HOME指向 JDK 安装目录PATH里追加%JAVA_HOME%\binMAVEN_HOME指向 Maven 解压目录PATH里追加%MAVEN_HOME%\bin。配好后在命令行执行java -version和mvn -v验证看到版本信息再继续。Maven 工程目录建议按下面这个结构建它把实体、数据库操作、业务逻辑、请求入口分开了travel-website/ ├── pom.xml ├── src/main/java │ ├── com/example/entity # User, Scenic, Order 等实体类 │ ├── com/example/dao # 数据访问层只负责SQL │ ├── com/example/service # 业务逻辑层处理判断和事务 │ ├── com/example/servlet # 控制器层接收请求返回JSON或跳转页面 │ └── com/example/util # MD5加密、DBUtil连接工具 ├── src/main/resources │ ├── db.properties # 数据库连接配置 │ └── jdbc.properties └── src/main/webapp ├── WEB-INF/web.xml # Servlet注册、Filter配置 ├── index.jsp # 首页景点列表入口 ├── css/ # 静态样式 ├── js/ # 前端交互和AJAX请求 └── pages/ # 详情页、登录页、后台管理页在pom.xml里只需要依赖javax.servlet-api、mysql-connector-java、jstl这几个核心库。不要一上来就引一堆框架依赖项目逻辑简单时依赖越少出问题时的排查范围越小。这个工程骨架把代码分层固定下来后面写登录、分页、下单都是在对应包下加类不会出现一个 Servlet 里写几百行 JDBC 代码的情况。3. 数据库设计与建表SQL8张表怎么撑起整个旅游网站数据库设计是这类项目的地基。表结构设计合理后面写 DAO 和 Service 会非常顺畅设计不合理光是订单金额和景点库存的关联就能让你改到怀疑人生。这个网站的规模不需要过度设计但也不能只建两三张表凑数8张表是覆盖前后台功能的最小完整集合。3.1 表结构总览从用户到订单的完整数据链路核心表有 8 张用户表user、管理员表admin、景点表scenic、旅游线路表travel_line、酒店表hotel、订单表orders、评论表comment、收藏表favorite。它们之间的关系是这样的用户和订单是一对多一个用户可以有多个订单用户和景点是多对多通过收藏表关联用户和评论是一对多景点和评论是一对多。订单表通过user_id关联用户、scenic_id关联景点这是整个系统里最重要的一条关联链路。如果后续要加酒店预订再建一张hotel_order表不要让订单表既存门票又存酒店信息否则统计和状态流转会越来越混乱。admin表单独存管理员账号不跟普通用户混在一个表里是为了后台登录时逻辑简单普通用户登录查user表管理员登录查admin表两张表的密码加密方式可以一致但 Session 里存的用户类型不同后台的 Filter 就能根据这个类型做权限判断。3.2 核心建表SQL用户表、景点表、订单表一次建好下面这三张表的建表语句是整套系统的核心建议直接复制后用字段和注释都按毕设标准写了CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 用户名, password VARCHAR(64) NOT NULL COMMENT 加盐MD5后的密文, email VARCHAR(100) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL COMMENT 头像图片路径, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE scenic ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50) NOT NULL COMMENT 所在城市用于按城市搜索, description TEXT COMMENT 景点介绍, price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 门票价格, stock INT NOT NULL DEFAULT 100 COMMENT 每日可售库存, open_time VARCHAR(50) DEFAULT 08:00-17:00, image VARCHAR(255) DEFAULT NULL COMMENT 封面图片相对路径, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_city (city) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号前台展示用, user_id INT NOT NULL, scenic_id INT NOT NULL, ticket_count INT NOT NULL DEFAULT 1 COMMENT 购买张数, total_price DECIMAL(10,2) NOT NULL COMMENT 总价 单价 * 张数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这三张表的设计有几个关键点。订单号order_no用唯一索引业务上展示给用户看的订单号不能依赖自增主键因为id会被用在关联查询里直接暴露给用户不合适。scenic表加了stock字段这是后面写下单事务时要扣减的库存不加这个字段订单和库存就完全脱节两个人同时买最后一张票时就会出现超卖。价格字段全部用DECIMAL(10,2)这是整个项目里最重要的一个选型决定后面详细说。3.3 字段设计细节金额、时间、图片的存法决定了你要不要返工字段设计里最容易返工的是三类金额、时间、图片。金额必须用DECIMAL不能用double或float。double在二进制里无法精确表示 0.1算出来的订单总价会出现 49.999999 这种值往数据库里一存再查出来显示用户看到的就是一个带着一串 9 的价格。用DECIMAL(10,2)配合BigDecimal金额问题从源头就消失了。时间字段用DATETIME不要用VARCHAR存字符串。DATETIME配合 MySQL 的DEFAULT CURRENT_TIMESTAMP插入时不用手动填时间查询时还能用 MySQL 的日期函数做按天统计。图片字段存相对路径比如/upload/scenic/1.jpg不存二进制内容也不要存完整 URL这样项目换服务器时只需要迁移 upload 目录图片显示问题不会变成硬编码的 IP 地址残留。3.4 用户密码存储加盐MD5的工具方法直接抄用户密码不能明文存数据库这是安全底线后台管理员能看到user表明文密码等于把用户账号拱手送人。常见的做法是加盐 MD5核心代码如下public class MD5Util { private static final String SALT travel2024; public static String md5WithSalt(String input) { String text input SALT; try { MessageDigest md MessageDigest.getInstance(MD5); byte[] bytes md.digest(text.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(MD5加密失败, e); } } }这段代码里SALT是固定的盐值实际项目里可以用用户注册时间或随机数做盐但毕设项目固定盐值已经足够重点是密码不以明文形式出现在数据库里。登录验证时把用户输入的密码加上同样的盐做 MD5再和数据库里存的值比对而不是把数据库的密文解密后比对。这里顺带提醒一句上面是简化方案生产环境强烈建议用 BCrypt但毕设答辩场景下加盐 MD5 已经能应付提问了。4. 后端核心接口实现登录、分页、下单三个Servlet把业务串起来数据库建好后接下来就是把业务流程跑通。这个项目的后端代码量不算大核心就是登录、景点列表分页、下单三个接口。把这三个接口写明白其他模块照着抄就行了。这里我用 Servlet 的方式来实现代码结构是经典的三层架构。4.1 分层架构与请求链路Servlet、Service、DAO各司其职分层的目的是让每段代码只干一件事。Servlet 接收 HTTP 请求、解析参数、返回 JSON 或跳转页面不写 SQLService 层处理业务判断比如登录时校验密码、下单时判断库存够不够不碰HttpServletRequestDAO 层只做数据库操作接收参数拼 SQL 返回结果集不关心请求从哪来。一个请求的实际链路是浏览器发请求 → 找web.xml里注册的 Servlet → Servlet 里调 Service 方法 → Service 里调 DAO 方法 → DAO 里用 JDBC 查 MySQL → 结果一层层返回Servlet 把它转成 JSON 输出给前端。这个链路在答辩时一定要能说清楚它是整个项目的主心骨。4.2 登录接口从Request参数到Session的全过程登录是所有业务的前提用户没登录就不能下单所以这个接口必须写得干净。下面是LoginServlet的核心代码WebServlet(/user/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(application/json;charsetUTF-8); String username req.getParameter(username); String password MD5Util.md5WithSalt(req.getParameter(password)); User user userService.login(username, password); PrintWriter out resp.getWriter(); if (user ! null) { req.getSession().setAttribute(loginUser, user); out.write({\success\:true,\msg\:\登录成功\}); } else { out.write({\success\:false,\msg\:\用户名或密码错误\}); } } }这段代码里有几个细节是新手容易漏的。第一行setCharacterEncoding(UTF-8)必须放在读取任何参数之前否则前端传过来的中文用户名会乱码。密码在前端传过来时是明文后端先做加盐 MD5 再拿去查库这样数据库里永远只存密文。登录成功后在 Session 里存了loginUser对象后续的所有需要登录的接口都会通过这个 Session 判断身份。对应的UserDao里查询逻辑是public User findByUsernameAndPassword(String username, String password) { String sql SELECT id, username, email, phone, avatar, create_time FROM user WHERE username ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setEmail(rs.getString(email)); u.setPhone(rs.getString(phone)); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }注意这里SELECT不能查password字段即使查了也不要set到User对象里防止对象在 Session 中保存时把密文暴露给前端页面。PreparedStatement用?占位符传参而不是拼字符串既避免 SQL 注入也避免引号转义的麻烦。4.3 景点列表分页接口LIMIT与参数绑定的边界处理景点列表是首页和搜索页的核心接口它涉及分页、按城市筛选、总记录数统计是一个典型的数据查询场景。分页参数由前端传来page表示第几页size表示每页几条。Servlet 收到参数后要计算出偏移量offset (page - 1) * size然后传给 DAO 做LIMIT offset, size查询WebServlet(/scenic/list) public class ScenicListServlet extends HttpServlet { private ScenicService scenicService new ScenicService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(application/json;charsetUTF-8); PrintWriter out resp.getWriter(); int page parseInt(req.getParameter(page), 1); int size parseInt(req.getParameter(size), 8); String city req.getParameter(city); int offset (page - 1) * size; ListScenic list scenicService.queryPage(city, offset, size); int total scenicService.count(city); int totalPage (int) Math.ceil(total * 1.0 / size); MapString, Object data new HashMap(); data.put(list, list); data.put(total, total); data.put(totalPage, totalPage); data.put(page, page); data.put(size, size); out.write(new Gson().toJson(data)); } private int parseInt(String param, int defaultValue) { if (param null || param.isEmpty()) { return defaultValue; } try { return Integer.parseInt(param); } catch (NumberFormatException e) { return defaultValue; } } }parseInt这个工具方法是必要的前端传pageabc时直接Integer.parseInt会抛异常让整个接口 500。参数异常时给默认值是最稳妥的处理方式。DAO 层查询时只有一条核心 SQL 需要看清楚SELECT id, name, city, price, image, open_time, description FROM scenic WHERE status 1 AND (? OR city ?) ORDER BY create_time DESC LIMIT ?, ?这里? OR city ?的写法是为了处理城市筛选为空的情况前端没传城市参数时city变量是空字符串条件恒为真就查全部传了城市参数就只查该城市的景点。LIMIT的两个问号绑定时一个用setInt绑offset一个绑size顺序不能反。4.4 下单事务一次Connection里完成扣库存与建订单下单是整个项目里唯一涉及事务的接口。一个完整的下单动作要做两件事扣减景点库存、插入订单记录。这两件事必须在同一个数据库事务里完成否则就会出现扣了库存但订单没生成或者订单建了但库存没扣的脏数据。核心代码如下public boolean createOrder(Order order) { String deductSql UPDATE scenic SET stock stock - ? WHERE id ? AND stock ?; String insertSql INSERT INTO orders(order_no, user_id, scenic_id, ticket_count, total_price, status) VALUES (?,?,?,?,?,?); Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement ps1 conn.prepareStatement(deductSql); ps1.setInt(1, order.getTicketCount()); ps1.setInt(2, order.getScenicId()); ps1.setInt(3, order.getTicketCount()); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); return false; } order.setOrderNo(generateOrderNo()); PreparedStatement ps2 conn.prepareStatement(insertSql); ps2.setString(1, order.getOrderNo()); ps2.setInt(2, order.getUserId()); ps2.setInt(3, order.getScenicId()); ps2.setInt(4, order.getTicketCount()); ps2.setBigDecimal(5, order.getTotalPrice()); ps2.setInt(6, 0); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }这段代码的核心在UPDATE scenic SET stock stock - ? WHERE id ? AND stock ?。AND stock ?是一个乐观锁判断如果库存不够executeUpdate返回 0 行说明更新失败直接在事务里回滚从根上防止了超卖。setAutoCommit(false)之后所有 SQL 都不会自动提交只有执行到conn.commit()才真正生效中途任何异常都走rollback()。这里有一个血泪经验finally块里conn.close()之前先setAutoCommit(true)是把连接重置成默认状态再归还给连接池。如果不重置这个连接下次被复用时会继续保持事务状态导致后续所有操作都不自动提交数据迟迟不落库排查起来极其折磨人。5. 联调、部署与避坑五个让新手卡住的高频问题后端接口写完前端页面加载数据最后部署到 Tomcat 跑起来这一步是整个项目从“代码能编译”到“能演示”的关键环节。联调时最常见的问题不是逻辑写错而是前端和后端对数据格式的理解不一致或者环境配置的细节没对齐。5.1 前端AJAX与Servlet的数据契约统一JSON返回格式前端和后端之间必须约定一个统一的返回格式我习惯用{code: 0, msg: success, data: {...}}这种结构。code为 0 表示成功非 0 表示业务失败比如未登录可以定义成code: 401。这样前端只需要判断code不需要关心每个接口的success字段是叫ok还是叫isSuccess。前端用fetch请求后端接口的典型代码如下async function loadScenicList(page, size, city) { const url /travel-web/scenic/list?page${page}size${size}city${encodeURIComponent(city || )}; const resp await fetch(url); const result await resp.json(); if (result.code 0) { renderScenicCards(result.data.list); } else { console.error(加载景点列表失败, result.msg); } }encodeURIComponent处理城市参数是必须的因为用户输入的城市名可能包含中文URL 里直接拼中文在某些浏览器和服务器组合下会乱码。fetch默认不带 Cookie如果后端接口依赖 Session 判断登录状态需要在fetch里加credentials: same-origin否则即使前端已经登录后端拿到的 Session 仍然是空的。5.2 部署到TomcatMaven打包与启动参数项目开发完成后打包部署是固定的流程。在 IDEA 里执行mvn clean package生成target/travel-website.war把这个 war 包复制到 Tomcat 的webapps目录然后启动 Tomcat 即可。也可以用 IDEA 的 Tomcat 集成插件直接部署但把 war 包扔进webapps的方式更适合现场演示因为换一台电脑也能快速部署。启动 Tomcat 时建议修改bin/catalina.batWindows或catalina.shLinux加上JAVA_OPTS-Dfile.encodingUTF-8。这一步看着是玄学实际是系统级编码问题。如果不指定Tomcat 在 Windows 上可能用 GBK 解析 JSP页面上的中文直接变乱码。5.3 避坑清单五个高频问题与排查路径第一个坑Tomcat 启动失败报Address already in use: 8080。现象是双击startup.bat后立刻闪退命令行显示端口被占用。原因多数是之前启动的 Tomcat 没关干净或者其他程序占用了 8080 端口。解决方法是先在命令行执行netstat -ano | findstr 8080看谁占用了端口然后taskkill /PID pid /F结束掉它或者修改 Tomcat 的server.xml把端口改成 8081。这里建议优先杀掉占用进程因为改端口后所有前端请求路径里的端口都要跟着改容易漏。第二个坑页面和数据库里的中文全部变成问号。现象比较经典前端页面上景点名字显示成???数据库里查出来也是???。原因是 JSP 文件编码、Servlet 读取参数编码、数据库连接串编码三处没有统一。解决方法是三处统一用 UTF-8JSP 顶部加% page contentTypetext/html;charsetUTF-8 %Servlet 里在读取参数前执行req.setCharacterEncoding(UTF-8)JDBC 连接串写成jdbc:mysql://localhost:3306/travel?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai。同时 MySQL 建表时指定CHARSETutf8mb4三处对齐后问题自然消失。第三个坑订单总价出现 49.999999。现象是在页面上看到订单金额是 50 元整但订单列表里显示 49.999999。原因是在 Java 代码里用了double做乘法8.5 * 3在二进制里不是精确的 25.5。解决方法是金额计算全链路用BigDecimal从 Servlet 接收参数到 Service 计算总价再到 DAO 写入数据库一律BigDecimal数据库字段用DECIMAL(10,2)。这里特别提醒BigDecimal构造时要用BigDecimal.valueOf(price)或new BigDecimal(String.valueOf(price))不要直接new BigDecimal(8.5)否则精度问题绕了一圈又回来了。第四个坑网站运行半天后所有接口超时Tomcat 假死。现象是刚启动时一切正常过几小时后刷新页面一直转圈后台日志报Connection is not available, request timed out。原因是 DAO 层查询数据库时获取了Connection但没有在finally块里关闭连接池的连接被耗尽。解决方法是所有 DAO 方法里用try-with-resources语句try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { // 处理结果集 } catch (SQLException e) { e.printStackTrace(); }try-with-resources会自动关闭ResultSet、PreparedStatement、Connection从语法层面杜绝连接泄漏。如果项目里大量用了手写finally关闭的老代码可以全局搜索conn.close()数一数有多少个自己关闭的分支漏掉的都要补上。第五个坑JSP 页面里的${name}原样显示没有解析成变量值。现象是页面上直接输出${user.username}字符串。原因是 web.xml 里的 Servlet 版本声明太低或者 JSP 的isELIgnored被设成了true。解决方法是把web.xml的根元素声明改成 Servlet 4.0 标准的版本头或者在这个 JSP 页面的指令里加% page isELIgnoredfalse %。这个坑在传统 JSP 项目里特别常见排查顺序是先看页面指令再看 web.xml 版本。6. 进阶技巧写一个登录拦截器顺带把未登录访问一网打尽做旅游信息网站这类项目最后一定要加一个登录拦截器。它解决的是个很实际的问题用户没登录就直接访问订单页面、收藏接口、个人中心。不加拦截器每写一个需要登录的接口都要手写一遍 Session 判断代码重复不说漏写一个就是安全隐患。拦截器用 Filter 实现一次配置所有需要登录的路径都生效。核心代码如下WebFilter(/user/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.html); return; } chain.doFilter(req, res); } }注意这里用了request.getSession(false)传false的意思是如果没有 Session 就返回 null而不是创建一个新的空 Session。有些新手习惯写request.getSession()那样的话未登录用户第一次访问也会新建一个空 Session虽然不影响拦截结果但它会让每次请求都多创建一个无用对象它也是内存占用问题的起源之一。这段代码在WebFilter上写死拦截/user/*路径以后新增的订单、收藏接口只要放在这个路径下就自动被保护了。写完拦截器后建议做一次完整的验证退出登录状态直接访问http://localhost:8080/travel-website/user/order/list看是否被重定向到登录页登录后再访问看是否能正常返回数据。这一步验证不需要后端调试工具打开浏览器无痕窗口就能完成。想更严谨一点就用命令行验证curl http://localhost:8080/travel-website/user/order/list看返回的 HTTP 状态码是不是 302加-b cookies.txt带上登录后的 Cookie 再看是不是 200这个流程能确认拦截器逻辑真的生效。我做这类项目时最亏的一次就是漏了在 DAO 层关连接演示时前半场还很流畅后半场突然所有页面打不开当时第一反应是代码逻辑炸了排查到最后才发现是连接池被撑爆了。那次之后凡是涉及Connection的代码我都用try-with-resources兜底。这个项目的代码量不大但把登录、分页、下单、拦截器这几条线走通Java Web 的核心知识基本就过了一遍剩下的页面美化就是时间问题。希望帮到你。本文还有配套的精品资源点击获取
返回列表