ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaWeb综合实战:黑马旅游网项目全解析

JavaWeb综合实战:黑马旅游网项目全解析

1. 项目概述:一个真实可跑的JavaWeb综合训练场

“黑马旅游网”这个名字在Java初学者圈子里几乎成了某种共识性符号——它不是某个商业平台,而是一套被反复打磨、验证过教学效果的典型Web应用案例。我带过几届学员,从零开始搭这个项目时,90%的人会在第3天卡在数据库连接池配置上,70%会在第5天被JSTL标签库的路径问题绕晕,还有人直到部署到Tomcat后看到404才意识到web.xml里servlet-mapping写错了斜杠方向。这恰恰说明:它不是一个“玩具项目”,而是一个浓缩了JavaWeb开发全流程痛点的真实训练场。

这个标题里的“day48”很关键——它不是指项目耗时48天,而是课程体系中第48个教学日的内容节点。这意味着它出现在Servlet、JSP、Filter、Listener、JDBC、DBUtils、C3P0/Druid、jQuery、Ajax、分页、文件上传、用户权限控制等核心模块全部讲完之后。此时学员手上有工具、有概念、有零散代码,但缺一个能把所有齿轮咬合起来的完整系统。黑马旅游网就是那台“总装线”:前端用JSP+JSTL+CSS+jQuery做静态页面和简单交互;后端用Servlet做控制器,Service层封装业务逻辑,Dao层用DBUtils操作数据库;数据源用C3P0连接池管理MySQL;权限通过Filter拦截未登录请求;图片上传走commons-fileupload;分页自己手写PageBean;连验证码都用servlet生成。它不追求炫技,但每个环节都踩在企业开发的常见路线上。

标题里特意强调“含源码及所需maven数据仓库”,这其实是给学习者吃定心丸。很多教程只给代码片段,但实际跑起来才发现:pom.xml里少了个scope=provided的servlet-api依赖,或者本地Maven仓库没配镜像源导致junit下载超时,又或者MySQL驱动版本和JDK8不兼容。这个项目把所有依赖版本都锁死了——比如mysql-connector-java用5.1.47(适配JDK8),c3p0用0.9.5.5,junit用4.12,连log4j的配置文件都预置好了。你只要按文档把war包丢进Tomcat/webapps,启动服务,访问localhost:8080/travel就能看到首页。这种“开箱即用”的确定性,对刚走出语法阶段的学习者来说,比任何理论讲解都管用。

它解决的核心问题是“知识孤岛”。学完Servlet知道怎么接收参数,学完JSP知道怎么展示数据,但不知道什么时候该用request.setAttribute(),什么时候该用session.setAttribute();学完Filter知道能拦截请求,但不清楚登录校验该放在哪个Filter链位置;学完DBUtils知道query()方法,但面对多表关联查询时不会组合ResultSetHandler。黑马旅游网把这些割裂的知识点,用一个“用户注册→登录→浏览线路→下单→查看订单”的闭环串了起来。你改一行Dao层SQL,前端列表就变;你注释掉LoginFilter,未登录也能进个人中心——这种即时反馈,是理解MVC分层价值最直接的方式。

适合谁来参考?首先是正在学JavaWeb的在校生或转行者,尤其适合那些跟着视频敲代码但总感觉“懂了又好像没懂”的人;其次是想快速捡起老技术栈的开发者,比如三年没碰JSP现在要维护遗留系统;还包括部分高校教师,这个项目结构清晰、边界明确,拿来当课程设计题目非常合适——学生能独立完成,老师批改也有明确checklist。它不教Spring Boot自动装配,也不讲Vue组件化,但它教会你怎么用最原始的JavaEE技术栈,把一个网站从数据库建表开始,一砖一瓦垒出来。

2. 整体架构设计与技术选型逻辑拆解

2.1 为什么坚持用传统JavaEE而非Spring Boot?

很多人看到“黑马旅游网”第一反应是:“这都什么年代了还用JSP?”但恰恰是这个选择,暴露了教学项目的底层逻辑——它不是为了生产环境效率,而是为了认知路径的平滑。Spring Boot的自动配置像一层厚玻璃:你看到页面出来了,却不知道DispatcherServlet在哪注册、ViewResolver怎么解析JSP路径、DataSource怎么被注入到JdbcTemplate。而黑马旅游网强制你手动写web.xml配置servlet映射,手动在init()方法里初始化C3P0数据源,手动在web.xml里声明Filter链顺序。这种“低效”恰恰是必要的认知成本。

举个具体例子:登录成功后跳转到首页,传统做法是在LoginServlet里写response.sendRedirect("index.jsp")。而Spring Boot可能写成return "redirect:/index"。前者让你立刻意识到HTTP重定向的本质是302响应头+Location字段;后者则隐藏了协议细节。再比如分页功能,手写PageBean需要你理解limit m,n的SQL语法、计算总页数的数学逻辑、处理当前页超出范围的边界条件。这些在MyBatis-Plus的page()方法里一键搞定,但你永远不知道count(*)和limit是如何协同工作的。

技术栈锁定在JDK8+Tomcat8+MySQL5.7这个组合,也是经过权衡的。JDK8的lambda表达式已经能简化部分代码(比如DBUtils的ResultSetHandler),但又不至于让Stream API掩盖集合遍历本质;Tomcat8对Servlet3.1规范支持完善,Filter的@WeblFilter注解可用,但依然保留web.xml配置方式供对比学习;MySQL5.7的JSON类型还没普及,所有数据都用标准字段存储,避免初学者陷入复杂数据类型陷阱。这种“适度落后”的选型,本质是给学习者留出理解底层的时间窗口。

2.2 分层结构如何对应真实开发职责?

整个项目严格遵循经典的MVC三层架构,但每层的职责划分比教科书更“接地气”。Controller层(Servlet)只做三件事:接收请求参数、调用Service、转发或重定向。它不处理任何业务逻辑,连字符串判空都交给Service。比如UserServlet的login()方法,核心就两行:UserService userService = new UserServiceImpl(); ResultInfo resultInfo = userService.login(user); ——所有密码加密、验证码校验、登录失败次数限制都在Service里。

Service层是真正的“业务中枢”。这里你会看到典型的事务控制:在addOrder()方法里,先扣库存再生成订单,两个Dao操作必须同时成功或同时失败。代码里没有@Transactional注解,而是用ThreadLocal绑定Connection,手动调用connection.setAutoCommit(false),在try块里执行所有操作,catch里rollback,finally里commit。这种写法笨重,但让你看清ACID的实现链条。更关键的是,Service层暴露的接口名直击业务本质:比如IUserDao里有findByUsername(String username),而IUserService里是login(User user)——前者是数据操作,后者是业务动作,这种语义分离正是分层的价值。

Dao层则刻意暴露技术细节。比如查询热门线路,DaoImpl里写的是:

String sql = "SELECT * FROM tab_route WHERE cid = ? ORDER BY count DESC LIMIT ?"; return template.query(sql, new BeanPropertyRowMapper<>(Route.class), cid, pageSize);

而不是封装成getHotRoutesByCategory(int cid)。因为初学者需要理解占位符?的作用、参数顺序如何对应、LIMIT子句的位置。连数据库表名tab_route都带着前缀,提醒你这是真实表结构,不是ORM映射类。

2.3 前端交互为何选择jQuery而非原生JS?

标题里没提前端框架,但源码里大量使用jQuery。这不是技术保守,而是教学策略。原生JS操作DOM需要记document.getElementById、addEventListener、querySelectorAll等API,而jQuery用$("#username").val()一句搞定。对于刚学会Java语法的人来说,JavaScript的异步回调、this指向、事件冒泡都是新概念。如果此时还要纠结addEventListener和onclick的区别,学习焦点就偏移了。

更重要的是,jQuery的Ajax封装完美匹配Servlet的请求响应模型。$.post("/user/login", {username:u,pwd:p}, function(data){...}) 这行代码,和LoginServlet里request.getParameter("username")形成直观映射。你甚至能用浏览器开发者工具看到Network面板里真实的POST请求体、响应JSON格式。而Fetch API的Promise链式调用,或者Axios的拦截器,会增加理解负担。等学员把jQuery Ajax用熟了,再过渡到原生JS,认知坡度就平缓得多。

3. 核心模块实现细节与实操要点

3.1 数据库设计:从ER图到建表语句的落地思考

项目用MySQL建了7张表,但真正高频操作的是5张:tab_user(用户)、tab_category(分类)、tab_route(线路)、tab_orders(订单)、tab_order_item(订单项)。设计时有两个反直觉的细节值得深挖。

第一个是tab_user表的password字段长度设为32。这不是随便写的,而是MD5加密后的字符串固定长度。项目里密码处理流程是:前端输入明文→Servlet接收→Service层用DigestUtils.md5Hex(pwd)加密→存入数据库。如果你改成SHA256,字段就得扩到64位,否则截断导致登录永远失败。这个细节逼着你去查Apache Commons Codec的文档,理解不同摘要算法的输出长度。

第二个是tab_route表的cid(分类ID)和tab_category的cid主键类型不一致。前者是int,后者是varchar(32)。表面看是设计失误,实则是为后续扩展留的伏笔。比如未来要支持“国内游”“出境游”这种文字分类,cid可能变成“domestic”“oversea”,而线路表保持int型cid是为了关联查询性能。虽然当前项目没用到,但字段类型差异本身就在暗示:数据库设计要考虑业务演进,不是单纯满足当前功能。

建表时最容易翻车的是外键约束。比如tab_orders表的uid字段,外键指向tab_user.uid。但MySQL默认引擎InnoDB才支持外键,如果误用MyISAM,alter table add constraint会静默失败。实操中我建议先执行show create table tab_user,确认Engine=InnoDB再建其他表。另外,所有时间字段统一用datetime类型,不用timestamp——后者受时区影响,本地测试和服务器部署容易出现时间差,而datetime存的是字面值,更可控。

3.2 用户登录模块:从密码安全到会话管理的全链路

登录功能看似简单,但藏着三个关键防线。第一道是验证码校验。项目用VerifyCodeServlet生成图片,核心是BufferedImage和Graphics2D绘图。难点在于干扰线绘制:随机起点、随机终点、随机颜色、透明度设置。很多学员复制代码后验证码一片模糊,其实是g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f))这行没写,导致干扰线完全覆盖字符。实测发现alpha值0.2~0.4之间效果最佳,太低看不见,太高看不清字。

第二道是密码加密。项目用MD5加盐,盐值存在web.xml的 里:

<context-param> <param-name>salt</param-name> <param-value>itcast_2023</param-value> </context-param>

Service层获取盐值:ServletContext context = this.getServletContext(); String salt = context.getInitParameter("salt"); 然后拼接密码:DigestUtils.md5Hex(pwd + salt)。这里有个坑:如果web.xml里param-value写成itcast_2023(没引号),XML解析会报错,必须加引号。这个细节暴露了XML配置的脆弱性,也解释了为什么现代框架都倾向用properties文件。

第三道是会话管理。登录成功后,Servlet把user对象存入session:request.getSession().setAttribute("user", user); 但退出登录时,不能只removeAttribute("user"),必须调用session.invalidate()。因为session里还存着验证码、购物车等其他属性,只删user会导致状态不一致。我在某次调试中发现,用户退出后还能用旧session访问个人中心,就是因为只删了属性没销毁session。

3.3 线路搜索与分页:手写PageBean的底层逻辑

分页是检验DAO层功力的试金石。项目没用PageHelper,而是手写PageBean类,包含currentPage(当前页)、rows(每页条数)、totalCount(总记录数)、totalPage(总页数)、list(当前页数据)五个属性。关键在Service层的findPage()方法:

public PageBean<Route> findPage(int cid, int currentPage, int rows) { // 1. 查询总记录数 String countSql = "SELECT COUNT(*) FROM tab_route WHERE cid = ?"; Long totalCount = template.queryForObject(countSql, new ScalarHandler<Long>(), cid); // 2. 查询当前页数据 String pageSql = "SELECT * FROM tab_route WHERE cid = ? LIMIT ?,?"; int start = (currentPage - 1) * rows; // 计算起始索引 List<Route> list = template.query(pageSql, new BeanPropertyRowMapper<>(Route.class), cid, start, rows); // 3. 封装PageBean PageBean<Route> pb = new PageBean<>(); pb.setTotalCount(totalCount); pb.setRows(rows); pb.setCurrentPage(currentPage); pb.setList(list); pb.setTotalPage((int)Math.ceil(totalCount * 1.0 / rows)); return pb; }

这里有两个易错点:一是start索引计算,(currentPage-1)rows是标准写法,但有人写成currentPagerows导致第1页就跳过首条;二是总页数计算,Math.ceil(totalCount * 1.0 / rows)必须乘1.0转double,否则整数除法结果为0。我在review学员代码时,发现30%的人在这里出错。

前端JSP展示分页时,用JSTL的<c:forEach>遍历pageNav(页码导航数组)。生成页码的逻辑是:当前页左右各显示2个页码,不足时补全。比如总页数10,当前页是3,则显示1 2 3 4 5;当前页是9,则显示6 7 8 9 10。这个算法用Java写要十几行,项目里直接用EL表达式配合JSTL标签实现,既简洁又符合JSP思想。

3.4 文件上传模块:commons-fileupload的避坑指南

图片上传用Apache Commons FileUpload,但配置稍有不慎就会404。核心在UploadServlet的doPost()方法:

// 1. 判断是否为文件上传请求 boolean isMultipart = ServletFileUpload.isMultipartContent(request); if (!isMultipart) { response.getWriter().write("非法请求"); return; } // 2. 配置上传参数 DiskFileItemFactory factory = new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 内存阈值1MB factory.setRepository(new File(this.getServletContext().getRealPath("/upload/temp"))); // 3. 创建上传处理器 ServletFileUpload upload = new ServletFileUpload(factory); upload.setHeaderEncoding("UTF-8"); // 解决中文文件名乱码 upload.setFileSizeMax(1024 * 1024 * 10); // 单文件10MB upload.setSizeMax(1024 * 1024 * 50); // 总请求50MB

最大坑在第三步的setHeaderEncoding("UTF-8")。如果不设,Windows系统上传中文名文件,fileName会变成乱码。但这个方法必须在parseRequest()之前调用,否则无效。另一个坑是temp目录权限:Linux服务器上,如果Tomcat运行用户对/upload/temp无写权限,上传会抛IOException。实操中我习惯在web.xml里配置初始化参数指定temp路径,避免硬编码。

上传后的文件保存路径也暗藏玄机。项目把图片存到webapps/upload/目录,这样URL可以直接访问http://localhost:8080/travel/upload/xxx.jpg。但要注意:这个路径必须在web.xml里配置为welcome-file-list的补充,否则某些Tomcat版本会拒绝访问。我在CentOS上部署时,发现SELinux策略会阻止Tomcat写入upload目录,需要执行setsebool -P tomcat_can_network_connect_db 1才能解决。

4. 实操过程中的典型问题与排查技巧

4.1 Maven依赖冲突:从pom.xml到本地仓库的逐层诊断

最常见的问题是“ClassNotFoundException”。比如运行时报错:java.lang.ClassNotFoundException: org.apache.commons.dbutils.QueryRunner。表面看是DBUtils没引入,但检查pom.xml发现dependency写得完全正确:

<dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.7</version> </dependency>

这时候要按三层排查:第一层看IDEA的External Libraries,确认commons-dbutils-1.7.jar是否在列表里;第二层打开jar包,检查org/apache/commons/dbutils/QueryRunner.class是否存在;第三层进本地Maven仓库(~/.m2/repository/commons-dbutils/commons-dbutils/1.7/),看jar文件是否完整(大小是否正常,比如1.7版本应该约150KB)。我遇到过一次,jar文件只有2KB,是网络中断导致下载不全,删掉后重新mvn clean compile就解决了。

更隐蔽的是传递依赖冲突。比如项目引入了c3p0-0.9.5.5,但某个间接依赖又拉了c3p0-0.9.1,导致ClassCastException。这时要用mvn dependency:tree命令查看依赖树,找到冲突来源。比如执行mvn dependency:tree | grep c3p0,发现spring-jdbc-4.3.28.RELEASE引入了老版本。解决方案是在pom.xml里用 排除:

<exclusion> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> </exclusion>

然后显式声明新版本。这个技巧在处理Log4j和SLF4J桥接时同样适用——很多jar包自带log4j-over-slf4j,和项目里的slf4j-log4j12冲突,必须排除。

4.2 Tomcat部署异常:从404到500的精准定位

404错误分两种:资源不存在和Servlet映射失败。如果是访问http://localhost:8080/travel/出现404,先确认war包名是不是travel.war(不是travel-1.0-SNAPSHOT.war);如果是访问http://localhost:8080/travel/user/login出现404,重点查web.xml的servlet-mapping:

<servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/user/login</url-pattern> </servlet-mapping>

注意url-pattern里不能有多余空格,且必须以/开头。曾经有学员写成 /user/login ,前后空格导致匹配失败。

500错误更棘手。比如登录时抛NullPointerException,堆栈指向UserService.login()第23行。不要急着看代码,先看Tomcat日志(logs/catalina.out)。如果日志里有Caused by: java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/travel,说明数据库驱动没加载。检查WEB-INF/lib/下是否有mysql-connector-java-5.1.47.jar,再确认DriverManager.registerDriver()是否被调用。项目里是在BaseDao的static块里写的:

static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } }

但JDK8+MySQL5.7需要改成com.mysql.cj.jdbc.Driver,否则驱动注册失败。这个细节在MySQL官网文档里有说明,但初学者很容易忽略。

4.3 中文乱码问题:从请求到响应的全链路治理

中文乱码是JavaWeb的“经典诅咒”。项目里有三处必须设UTF-8:
第一处是Tomcat的server.xml,在Connector标签里加URIEncoding="UTF-8":

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />

否则GET请求的中文参数(如?name=张三)会乱码。
第二处是Servlet的request.setCharacterEncoding("UTF-8"),必须在getParameter()之前调用,否则POST参数乱码。
第三处是response.setContentType("text/html;charset=UTF-8"),确保浏览器用UTF-8解析HTML。

但还有个隐藏坑:MySQL连接URL。如果写成jdbc:mysql://localhost:3306/travel,中文插入会乱码。必须加上参数:

jdbc:mysql://localhost:3306/travel?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8

其中serverTimezone=GMT%2B8解决时区问题,%2B8是+8的URL编码。我在Windows上测试时,发现不加这个参数,数据库时间比系统时间慢8小时,导致订单创建时间错乱。

4.4 JSP页面无法解析:从编译错误到EL表达式失效

JSP报错“Unable to compile class for JSP”通常是因为Java版本不匹配。比如JSP里用了Lambda表达式,但Tomcat用JDK7编译,就会失败。解决方案是在pom.xml里指定maven-compiler-plugin的source和target为1.8。

更常见的是EL表达式失效。比如${user.username}不显示内容,页面原样输出。这是因为web.xml的schema版本太低。项目必须用Servlet3.0+的schema:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">

如果写成2.5版本,EL默认是禁用的。启用方式是在page指令里加isELIgnored="false",但治标不治本。所以务必确认web.xml头文件正确。

另一个坑是JSTL标签库。如果<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>报错,说明jstl.jar和standard.jar没引入。但注意:这两个jar包在Maven里对应的是jstl:jstl:1.2,而Jakarta EE9之后改用jakarta.servlet.jsp.jstl,版本要换。项目用1.2版本,所以pom.xml里必须写:

<dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency>

5. 源码结构与Maven仓库配置实战指南

5.1 项目目录结构:从src/main到webapp的物理映射

项目采用标准Maven Web项目结构,但有几个关键目录需要特别关注。src/main/java下是完整的包结构:com.itheima.dao、com.itheima.service、com.itheima.web,这种命名不是随意的,它对应Servlet容器的类加载机制。比如LoginServlet在com.itheima.web包下,编译后class文件在WEB-INF/classes/com/itheima/web/LoginServlet.class,Tomcat启动时会扫描这个路径加载Servlet。

src/main/resources存放配置文件,其中druid.properties是重点。项目用Druid替换C3P0,配置如下:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/travel?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username=root password=123456 initialSize=5 maxActive=10 minIdle=5 maxWait=60000

注意url里的serverTimezone=GMT%2B8,这是JDK8+MySQL5.7的强制要求,否则报时区异常。initialSize设为5表示启动时就创建5个连接,避免首次访问延迟。

webapp目录是前端战场。除了标准的WEB-INF/web.xml,还有两个关键子目录:css/和js/。其中js目录下的jquery.min.js版本是3.6.0,这个版本兼容IE9+,比新版本更稳妥。而images/目录下的logo.png尺寸是200x60像素,如果替换为更大图片,CSS里的height:60px就要同步调整,否则布局错乱。

5.2 Maven仓库配置:从settings.xml到镜像源的优化

pom.xml里声明的依赖,最终要从Maven中央仓库下载。但默认的https://repo.maven.apache.org/maven2/在国内经常超时。必须配置阿里云镜像,在~/.m2/settings.xml里添加:

<mirrors> <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors>

这个配置的关键是 ,表示所有仓库请求都走阿里云。但要注意:如果项目私有仓库(比如公司Nexus)也需要配置,要把私有仓库的mirrorOf设为具体的id,避免被 覆盖。

另一个重要配置是profile,用于区分开发和生产环境。项目在settings.xml里定义了dev profile:

<profiles> <profile> <id>dev</id> <properties> <env>development</env> </properties> <activation> <activeByDefault>true</activeByDefault> </activation> </profile> </profiles>

这样在pom.xml里可以用${env}引用,比如数据库URL根据环境切换。虽然项目当前没用到,但这个结构为后续扩展留了接口。

5.3 源码编译与部署:从mvn clean package到Tomcat热部署

编译命令必须用mvn clean package -Dmaven.test.skip=true。clean是为了清除旧class,package生成war包,-Dmaven.test.skip=true跳过测试(项目测试用例不全,跳过避免失败)。生成的war包在target/travel-1.0-SNAPSHOT.war,但Tomcat要求包名是travel.war,所以要重命名。

部署有两种方式:手动复制和热部署。手动复制是把travel.war丢进Tomcat/webapps/,启动Tomcat自动解压。热部署则需要在pom.xml里配置tomcat7-maven-plugin:

<plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <url>http://localhost:8080/manager/text</url> <server>tomcat-server</server> <path>/travel</path> </configuration> </plugin>

然后在~/.m2/settings.xml里配置server:

<servers> <server> <id>tomcat-server</id> <username>admin</username> <password>123456</password> </server> </servers>

注意:Tomcat的manager用户要在conf/tomcat-users.xml里声明,且角色必须是manager-script,不是manager-gui。

5.4 调试技巧:从IDEA断点到Tomcat远程调试

在IDEA里调试,关键是要配置Remote JVM Debug。在Tomcat/bin/catalina.sh(Linux)或catalina.bat(Windows)里添加:

export JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000"

然后在IDEA的Run/Debug Configurations里新建Remote JVM Debug,端口填8000。启动Tomcat后,在LoginServlet的doPost()方法打个断点,用浏览器访问登录页,IDEA就会停在断点处。

更高效的是热部署调试。安装JRebel插件后,修改Java代码保存,Tomcat自动重载class,不用重启。但要注意:JRebel对JSP修改支持有限,改JSP还是得重启。所以我的习惯是:Java代码用JRebel,JSP修改后Ctrl+F9手动编译。

最后分享个经验:每次部署前,先清空Tomcat/work/Catalina/localhost/下的travel目录。这个目录是JSP编译后的servlet class,如果旧class残留,可能导致新JSP不生效。我见过三次类似问题,都是因为没清这个目录,浪费了两小时排查时间。

6. 项目延伸与能力迁移路径

6.1 从JSP到前后端分离:渐进式重构方案

黑马旅游网的JSP页面完全可以作为前端模板,迁移到Vue或React。比如index.jsp里的线路列表:

<c:forEach items="${page.list}" var="route"> <div class="col-md-4"> <div class="thumbnail"> <img src="${route.rimage}" alt="${route.rname}"> <div class="caption"> <h3>${route.rname}</h3> <p>¥${route.price}起</p> <p><a href="${pageContext.request.contextPath}/route/detail?id=${route.rid}" class="btn btn-primary" role="button">查看详情</a></p> </div> </div> </div> </c:forEach>

这段代码可以轻松转成Vue组件:

<div v-for="route in routes" :key="route.rid" class="col-md-4"> <div class="thumbnail"> <img :src="route.rimage" :alt="route.rname"> <div class="caption"> <h3>{{ route.rname }}</h3> <p>¥{{ route.price }}起</p> <p><router-link :to="'/route/detail/'+route.rid" class="btn btn-primary">查看详情</router-link></p> </div> </div> </div>

关键是把Servlet的JSON接口暴露出来。比如原来LoginServlet返回重定向,现在改成返回JSON:

response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(new ObjectMapper().writeValueAsString(resultInfo));

这样前端用axios.post('/user/login', data)就能拿到结果。这种渐进式改造,比直接重写整个系统风险小得多。

6.2 安全加固:从基础防护到OWASP Top 10

项目当前的安全措施比较基础,但可以按优先级逐步加强。第一级是SQL注入防护。项目用DBUtils的预编译,已经规避了大部分风险,但Dao层仍有拼接SQL的地方,比如模糊查询:

String sql = "SELECT * FROM tab_route WHERE rname LIKE '%"+rname+"%'";

必须改成:

String sql = "SELECT * FROM tab_route WHERE rname LIKE ?"; template.query(sql, new BeanPropertyRowMapper<>(Route.class), "%"+rname+"%");

第二级是XSS防护。用户输入的线路描述可能含

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进