2026/10/29 13:24:52

JSP旅游管理系统源码实战:SqlServer2012与Tomcat环境搭建及避坑指南

JSP旅游管理系统源码实战:SqlServer2012与Tomcat环境搭建及避坑指南 简介这份资源是面向Java Web开发初学者与课程设计人员的旅游管理系统完整设计与实现文档采用JSP作为前端语言、SqlServer2012作为后台数据库基于B/S架构开发环境为MyEclipse8.5与Tomcat6.0适合具备一定编程基础、需要完成毕业设计或课程实践的技术人员参考。压缩包内仅含1个docx文档体积约934KB内容涵盖绪论、需求分析、系统设计、功能实现与测试等完整章节并附有中英文摘要与目录结构。系统功能覆盖旅游景点管理、旅游线路管理、在线预订、网站论坛及公告管理区分管理员与会员两类用户平台注重权限管理与数据安全性。目前已有68人学习下载读者可借此获取一份结构完整的项目方案用于理解JSP与SqlServer整合开发流程、借鉴模块划分思路并作为论文撰写与系统实现的参考模板。1. 从一份 JSP 旅游管理系统源码说起它到底能跑通什么如果你手头正好有一套基于 JSP 的旅游管理系统源码大概率是课程设计或毕设的产物。这套东西的核心价值不在于技术多新而在于它把 Java Web 最经典的 B/S 三层结构跑通了前台用 JSP 渲染景点列表和线路详情后台用 SqlServer2012 存数据中间靠 Servlet 和 JDBC 做衔接。管理员能管景点、管线路、管会员、发公告普通用户能注册、查景点、订线路、发论坛帖。说白了它就是一个缩小版的旅游电商后台麻雀虽小但五脏俱全。这套源码适合两类人一是正在做 Java Web 课程设计、需要一份能跑起来的参考实现的学生二是想从零搭一个 JSP 项目练手、熟悉 request/session/application 这些内置对象怎么配合 JDBC 使用的开发者。它不涉及 Spring 那套重型框架所有逻辑都写在 JSP 和 Servlet 里代码直白到你能一眼看穿请求从浏览器到数据库再回来的完整链路。但正因为直白坑也直白——乱码、连接泄漏、权限绕过一个都跑不掉。2. 环境搭建与数据库初始化把 SqlServer2012 和 Tomcat6.0 拉起来2.1 为什么这套老技术栈反而更难配这套系统标注的开发环境是 MyEclipse8.5 Tomcat6.0 SqlServer2012 JDK6/7。放在今天你大概率不会真去装 MyEclipse8.5用 IDEA 或 Eclipse 都能替代但 Tomcat 和 SqlServer 的版本选择会直接影响你能不能跑起来。Tomcat6.0 对 Servlet 2.5 和 JSP 2.1 的支持是刚好的如果你换成 Tomcat9 或 10JSP 里那些老式写法可能因为 EL 表达式解析规则变化而报错。SqlServer2012 的 JDBC 驱动是 sqljdbc4.jar这个 jar 包必须放在 WEB-INF/lib 下不能只挂在项目的 Build Path 里否则部署到 Tomcat 后一定报 ClassNotFoundException。我一般会先把 JDK 版本锁在 1.7 或 1.8Tomcat 用 7.0.x 作为折中——它兼容 Servlet 2.5 规范又比 6.0 稳定。SqlServer 这边如果你不想装完整的 2012用 SqlServer Express 也行但要注意实例名默认是 SQLEXPRESS连接字符串里得写成jdbc:sqlserver://localhost\\SQLEXPRESS;databaseNametravel_db。2.2 建库建表与 JDBC 连接配置数据库脚本通常包含在源码的 sql 文件夹里但很多版本只给了表结构没给初始数据。你需要手动执行建表语句核心表包括 t_user、t_jingdian、t_luxian、t_gonggao、t_forum 这几张。下面是一个典型的 JDBC 工具类写法放在 src 下的 util 包里// DBUtil.java import java.sql.*; public class DBUtil { // 注意SqlServer2012 默认端口 1433实例名用分号隔开 private static final String URL jdbc:sqlserver://localhost:1433;databaseNametravel_db;integratedSecurityfalse; private static final String USER sa; private static final String PWD your_password; static { try { // sqljdbc4.jar 对应的驱动类名 Class.forName(com.microsoft.sqlserver.jdbc.SQLServerDriver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(JDBC 驱动加载失败检查 sqljdbc4.jar 是否在 WEB-INF/lib 下); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭顺序rs - stmt - conn任何一个为 null 都要跳过 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }这段代码的关键点有三个驱动类名必须是com.microsoft.sqlserver.jdbc.SQLServerDriver不是老版的net.sourceforge.jtds.jdbc.Driver连接 URL 里如果用了命名实例端口号可以省略但实例名不能省静态代码块里加载驱动只执行一次避免每次 getConn 都反射。参数方面sa 账户默认是禁用的你得在 SqlServer 配置管理器里启用 TCP/IP 协议并重启服务否则连接会报“拒绝访问”。2.3 部署到 Tomcat 的目录结构与启动验证把项目导出为 WAR 包或者直接在 IDE 里配置 Tomcat 运行目录结构应该是这样travel_web/ ├── WEB-INF/ │ ├── web.xml │ ├── lib/sqljdbc4.jar │ └── classes/com/travel/util/DBUtil.class ├── index.jsp ├── admin/ │ ├── jingdian_list.jsp │ └── luxian_add.jsp └── user/ ├── register.jsp └── booking.jsp启动 Tomcat 后先访问http://localhost:8080/travel_web/index.jsp如果页面能出来但数据是空的说明 JDBC 通了但表里没数据如果直接报 500 且堆栈里有ClassNotFoundException那就是驱动没放对位置。常见做法是先在 SqlServer 里执行SELECT COUNT(*) FROM t_jingdian确认有数据再回来看 JSP 里的查询逻辑。3. 核心功能模块的代码实现景点管理、线路预订与权限拦截3.1 旅游景点管理的增删改查与分页景点管理是管理员模块里最典型的 CRUD。JSP 负责展示列表和表单Servlet 负责处理请求和调用 DAO。下面是一个景点列表分页查询的 Servlet 片段// JingdianListServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 当前页码默认第 1 页 int page 1; String pageStr request.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { page Integer.parseInt(pageStr); } int pageSize 10; // 每页 10 条 int offset (page - 1) * pageSize; Connection conn null; PreparedStatement ps null; ResultSet rs null; ListJingdian list new ArrayList(); int total 0; try { conn DBUtil.getConn(); // 先查总数 ps conn.prepareStatement(SELECT COUNT(*) FROM t_jingdian); rs ps.executeQuery(); if (rs.next()) total rs.getInt(1); rs.close(); ps.close(); // SqlServer 分页必须用 ORDER BY OFFSET FETCH ps conn.prepareStatement( SELECT * FROM t_jingdian ORDER BY id OFFSET ? ROWS FETCH NEXT ? ROWS ONLY); ps.setInt(1, offset); ps.setInt(2, pageSize); rs ps.executeQuery(); while (rs.next()) { Jingdian j new Jingdian(); j.setId(rs.getInt(id)); j.setName(rs.getString(name)); j.setDizhi(rs.getString(dizhi)); j.setMenpiao(rs.getString(menpiao)); list.add(j); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(conn, ps, rs); } request.setAttribute(jingdianList, list); request.setAttribute(totalPage, (total pageSize - 1) / pageSize); request.setAttribute(currentPage, page); request.getRequestDispatcher(/admin/jingdian_list.jsp).forward(request, response); }这里有个 SqlServer 特有的坑分页不能用 MySQL 的LIMIT offset, size必须用OFFSET ... ROWS FETCH NEXT ... ROWS ONLY而且这条语句前面必须跟ORDER BY否则直接语法错误。参数方面pageSize 设成 10 是保守值如果你的景点图片字段存的是路径而不是二进制可以适当调大。另外注意rs.close()在第一次查询后就要调用否则同一个 Statement 上再执行新查询会隐式关闭前一个 ResultSet在某些驱动版本下会抛异常。3.2 线路预订的业务逻辑与事务处理线路预订比单纯的 CRUD 多了一层业务约束用户提交预订后系统要同时写入预订表和更新线路的剩余名额。这两步必须在一个事务里完成否则会出现名额扣了但订单没生成的情况。下面是一个典型的事务处理写法// BookingServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(login.jsp); return; } int luxianId Integer.parseInt(request.getParameter(luxianId)); int num Integer.parseInt(request.getParameter(num)); // 预订人数 Connection conn null; try { conn DBUtil.getConn(); conn.setAutoCommit(false); // 开启事务 // 1. 检查剩余名额 PreparedStatement ps1 conn.prepareStatement( SELECT remain FROM t_luxian WHERE id ?); ps1.setInt(1, luxianId); ResultSet rs ps1.executeQuery(); if (!rs.next() || rs.getInt(remain) num) { conn.rollback(); request.setAttribute(msg, 名额不足); request.getRequestDispatcher(error.jsp).forward(request, response); return; } rs.close(); ps1.close(); // 2. 扣减名额 PreparedStatement ps2 conn.prepareStatement( UPDATE t_luxian SET remain remain - ? WHERE id ?); ps2.setInt(1, num); ps2.setInt(2, luxianId); ps2.executeUpdate(); ps2.close(); // 3. 写入预订记录 PreparedStatement ps3 conn.prepareStatement( INSERT INTO t_booking (user_id, luxian_id, num, book_time) VALUES (?, ?, ?, GETDATE())); ps3.setInt(1, user.getUserId()); ps3.setInt(2, luxianId); ps3.setInt(3, num); ps3.executeUpdate(); ps3.close(); conn.commit(); // 提交事务 response.sendRedirect(booking_success.jsp); } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); } finally { try { if (conn ! null) conn.setAutoCommit(true); } catch (SQLException ignored) {} DBUtil.close(conn, null, null); } }事务的边界很清晰从setAutoCommit(false)开始到commit()或rollback()结束。参数方面num 必须做非负校验否则用户传个负数进来就变成反向增加名额了。另外GETDATE()是 SqlServer 的函数换成 MySQL 要写NOW()。常见误用是把 Connection 放在成员变量里跨请求复用那样事务会串得一塌糊涂必须每次请求从 DBUtil 拿新连接。3.3 基于 Session 的登录拦截与权限区分系统分管理员和普通用户两种角色权限控制靠 Session 里存的用户类型来判断。下面是一个简单的登录拦截 Filter// AuthFilter.java public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; String uri request.getRequestURI(); // 放行登录页、注册页和静态资源 if (uri.contains(login.jsp) || uri.contains(register.jsp) || uri.contains(.css) || uri.contains(.js)) { chain.doFilter(req, res); return; } HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 管理员页面只允许 admin 角色访问 if (uri.contains(/admin/)) { User user (User) session.getAttribute(loginUser); if (!admin.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /no_permission.jsp); return; } } chain.doFilter(req, res); } }web.xml 里配置这个 Filter 拦截/*但要注意放行规则别写太宽否则静态资源被拦了页面样式全丢。参数方面request.getSession(false)表示不自动创建新 Session避免未登录用户也生成一堆无用 Session。常见坑是 Filter 里用了sendRedirect之后没有 return代码继续往下走导致重复响应Tomcat 会报“response already committed”。4. 避坑与排查乱码、连接泄漏、SqlServer 驱动报错4.1 中文乱码POST 和 GET 要分开治现象表单提交中文景点名称存到数据库变成问号或者页面显示成乱码。原因JSP 页面默认用 ISO-8859-1 解码请求参数而浏览器发的是 UTF-8。解决POST 请求在 Servlet 里第一行写request.setCharacterEncoding(UTF-8)GET 请求在 Tomcat 的 server.xml 里给 Connector 加URIEncodingUTF-8。另外 JSP 页面头部必须写% page contentTypetext/html;charsetUTF-8 %数据库字段用 nvarchar 而不是 varchar。4.2 数据库连接泄漏Tomcat 跑半天就卡死现象系统刚启动正常访问几十次后越来越慢最后报“Too many connections”。原因DAO 里拿了 Connection 但没在 finally 里关闭或者关闭顺序不对导致 Statement 没关。解决所有数据库操作必须用 try-finally 包裹关闭顺序是 ResultSet → Statement → Connection。更稳妥的做法是用连接池但老项目里直接手写 DBUtil.close 也能救急。检查方法是在 SqlServer 里执行SELECT * FROM sys.dm_exec_sessions WHERE status sleeping看有没有大量空闲连接堆积。4.3 SqlServer 驱动加载失败ClassNotFoundException现象Tomcat 启动时报java.lang.ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver。原因sqljdbc4.jar 只加到了 IDE 的 Build Path没有复制到 WEB-INF/lib 目录。解决手动把 jar 包复制到 WEB-INF/lib 下或者在 IDE 的部署配置里勾选“将依赖复制到 WEB-INF/lib”。注意 sqljdbc4.jar 对应 JDK6如果你用的是 JDK8也可以用 sqljdbc42.jar但驱动类名不变。4.4 分页查询报语法错误OFFSET 必须跟 ORDER BY现象景点列表翻到第二页时报Incorrect syntax near OFFSET。原因SqlServer 的 OFFSET FETCH 语法要求前面必须有 ORDER BY而且 SqlServer2008 不支持这个语法必须 2012 及以上。解决在查询语句里加上ORDER BY id如果用的是 SqlServer2008只能改用ROW_NUMBER() OVER (ORDER BY id)的子查询方式。另外注意 OFFSET 的参数不能是负数page 从 1 开始算offset 从 0 开始算。4.5 权限绕过直接敲 URL 就能进后台现象普通用户不登录直接在浏览器输入/admin/jingdian_list.jsp也能看到管理页面。原因只在 JSP 页面顶部做了 Session 判断但 JSP 被直接访问时可能跳过了判断逻辑或者 Filter 的放行规则写错了。解决所有权限判断必须放在 Filter 或 Servlet 里JSP 只负责展示。Filter 的放行列表要精确到具体路径不要用contains(admin)这种模糊匹配否则/user/admin_info.jsp也会被放行。5. 进阶技巧用 JSTL 替换脚本片段与 SqlServer 连接池调优5.1 把 JSP 里的 Java 代码抽干净原始项目里大量使用% %脚本片段比如% ListJingdian list (ListJingdian) request.getAttribute(jingdianList); %然后在页面里写 for 循环。这种写法维护起来很痛苦页面和逻辑搅在一起。进阶做法是引入 JSTL 标签库把循环和判断换成c:forEach和c:if。你只需要把 jstl.jar 和 standard.jar 放进 WEB-INF/lib然后在 JSP 头部加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %原来的脚本片段就能改写成c:forEach items${jingdianList} varj tr td${j.name}/td td${j.dizhi}/td td${j.menpiao}/td /tr /c:forEach这样改完之后JSP 里几乎看不到 Java 代码前端人员也能直接改页面。参数方面items里写的是 request 或 session 里的属性名var是循环变量名EL 表达式${j.name}会自动调用 getName() 方法。注意 JSTL 的 uri 在 JSTL1.1 之后是http://java.sun.com/jsp/jstl/core老版本可能是http://java.sun.com/jstl/core写错了会报“无法解析标签”。5.2 SqlServer 连接池从 DriverManager 到 DBCP每次请求都DriverManager.getConnection在并发稍高时开销很大。我一般会换成 Apache DBCP 连接池只需要在 WEB-INF/lib 下加 commons-dbcp.jar 和 commons-pool.jar然后写一个 dbcp.propertiesdriverClassNamecom.microsoft.sqlserver.jdbc.SQLServerDriver urljdbc:sqlserver://localhost:1433;databaseNametravel_db usernamesa passwordyour_password initialSize5 maxActive20 maxIdle10 maxWait3000 validationQuerySELECT 1然后在 DBUtil 里用BasicDataSourceFactory.createDataSource(props)创建 DataSourcegetConn 改成dataSource.getConnection()。参数方面maxActive 设成 20 对课程设计够用了maxWait 是获取连接的超时毫秒数validationQuery 用来检测连接是否有效。注意 SqlServer 的 validationQuery 写SELECT 1就行不用加 FROM 子句。5.3 验证系统是否真正跑通的三步检查法改完代码别急着点页面按这三步走一遍能省很多返工时间。第一步在 SqlServer 里执行SELECT VERSION确认数据库版本是 2012 及以上同时确认 TCP/IP 协议已启用。第二步在 Tomcat 的 logs 目录下看 catalina.out搜索“SEVERE”和“Exception”如果有 JDBC 相关报错先解决驱动问题。第三步用浏览器开发者工具的 Network 面板看每个请求的响应码200 是正常302 是跳转500 是服务端异常根据 URL 定位到具体的 Servlet 或 JSP。这三步走完系统能不能用、哪里不能用基本就清楚了。从那以后我每次拿到一份 JSP 老项目都会先把数据库连接和字符编码这两件事单独拎出来验证一遍再去看业务代码。因为这两个地方一旦出问题后面所有功能都是空中楼阁。希望帮到你。本文还有配套的精品资源点击获取