Servlet+JSP协作开发实操:从Maven建项目到JSP编译原理 《Head First Servlet JSP》这本经典的入门书第三章正好是从“纯Servlet输出HTML”过渡到“Servlet JSP 分工协作”的关键节点。这篇笔记03我打算换个玩法不按目录一章章念而是直接把这一阶段最常碰到的实操场景串起来——从Eclipse里建Maven的Servlet项目、写一个JSP个人信息页、到让Servlet调用大模型API接口、再看清楚Tomcat编译后的JSP真实面目。这个阶段搞透了后面学MVC、自定义标签都会顺很多。这篇笔记适合两类人一是刚把Servlet基础语法啃完、想找个完整项目练手的新手二是已经在用IDEA或Eclipse写Web项目但对JSP内部原理还停留在“会用但不清楚为什么”的朋友。我会把每一步操作背后的原理也顺带讲清楚不光是让你能跑起来更重要的是让你知道为什么这么写、这个坑是怎么来的。1. 整体设计与思路拆解1.1 为什么第三章开始推荐用Maven建项目《Head First Servlet JSP》书里的例子基本都是手动把class文件扔到WEB-INF/classes目录下再手工重启Tomcat。这种操作在2008年左右很主流放到今天开发效率实在太低了。现在的Web项目基本都走Maven或Gradle来管理依赖和构建流程。用Maven建Servlet项目的好处有三个依赖版本不用自己满网找jar包、部署打包一条命令完成、同事之间协作时项目结构完全一致。第三章的练习恰好涉及很多第三方库——JSTL标签库、数据库驱动、JSON解析库这时候Maven的依赖管理优势体现得淋漓尽致。我当时在Eclipse里创建一个Maven Web项目选的是maven-archetype-webapp骨架。这个骨架生成的目录结构非常标准src/main/java放Java源码src/main/webapp放JSP、CSS、JS等Web资源。Eclipse里操作路径是File → New → Maven Project → Next → 选中maven-archetype-webappGroup Id和Artifact Id随便填比如com.example和servlet-jsp-demo。创建完后有个小问题默认生成的web.xml是Servlet 2.3的旧版本规范如果用的Tomcat 9以上建议手动把web.xml升级到4.0版本或者在pom.xml里配置maven-war-plugin指定failOnMissingWebXml为false直接走注解方式配置Servlet。我用的是注解方式省掉了web.xml里一大段配置代码。1.2 Servlet和JSP的职责边界第三章的核心思想就是Servlet负责处理请求、调用业务逻辑JSP负责展示数据。这个思想第一次接触时容易犯迷糊因为上一章里Servlet还能用out.println()输出HTML怎么到了第三章突然说“别这么写了”我个人的理解方式是这样的Servlet里拼HTML就像用System.out.println写一篇带格式的公众号文章写的人痛苦、看的人更痛苦。一旦页面复杂Java代码和HTML标签混在一起改个样式都要重新编译重启。而JSP天生就是为“在HTML中嵌入Java代码”设计的浏览器看到的最终结果就是纯HTMLJSP里的Java代码执行完就消失了。实际开发中这个分工还要再细化一层Servlet拿到请求参数后调用Service层处理业务把结果放到request或session作用域里然后forward给JSP。JSP不直接访问数据库、不直接处理请求参数校验只管把数据渲染出来。这个“Model 2”架构就是后面MVC的雏形。第三章练习里比较典型的场景是用户提交一个表单比如个人信息Servlet接收参数后做简单校验封装成一个JavaBean对象存到request作用域再转发到display.jsp展示。这样写的好处是如果以后想换页面样式只改JSP如果想改校验逻辑只改Servlet互不干扰。2. 核心实操细节2.1 创建Servlet并用注解配置映射路径项目骨架建好后开始写第一个Servlet。在src/main/java下建一个InfoServlet类继承HttpServlet重写doGet和doPost方法。Servlet 3.0以后支持注解方式配置访问路径不需要在web.xml里写servlet和servlet-mapping了。我用的是WebServlet(/info)这种方式WebServlet(/info) public class InfoServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Student student new Student(张三, 23, 计算机科学与技术); req.setAttribute(studentInfo, student); req.getRequestDispatcher(/display.jsp).forward(req, resp); } }这里有个关键的forward方法内部机制很重要forward是服务器内部跳转浏览器地址栏不会变整个过程只有一次请求、一次响应。与之相对的sendRedirect是浏览器重新发一次请求地址栏会变。第三章刚开始用forward就够了后面学PRG模式时才用redirect。然后写一个简单的JavaBean类Student三个属性name、age、major配上无参构造器和getter/setter方法。有人可能觉得写getter/setter很啰嗦但JSP在EL表达式${student.name}取值时底层就是通过反射调用get方法没有getter就拿不到值这是硬性约束。2.2 JSP页面编写与EL表达式取值JSP页面放在src/main/webapp下新建display.jsp。页面顶部可以用% page contentTypetext/html;charsetUTF-8 languagejava %声明。然后通过EL表达式从request作用域里取出Servlet放进去的数据% page contentTypetext/html;charsetUTF-8 languagejava % html head title个人信息展示/title /head body h2个人信息/h2 table tr td姓名/td td${studentInfo.name}/td /tr tr td年龄/td td${studentInfo.age}/td /tr tr td专业/td td${studentInfo.major}/td /tr /table /body /htmlEL表达式的取值路径是先找pageContext→request→session→application四个作用域看有没有叫studentInfo的属性找到后调用它的getter方法。所以${studentInfo.name}实际上对应的是request.getAttribute(studentInfo).getName()。如果页面上发现值没显示出来八成是作用域不匹配。比如Servlet里用session.setAttribute存的数据在JSP里用${studentInfo}也能取到但请求转发时用request作用域更合理因为数据只跟本次请求有关不该占session内存。如果是IDEA用户新建JSP时有个容易忽略的问题IDEA默认颜色方案Color Scheme里没有JSP文件的语法高亮选项看起来就像纯文本文件很不舒服。解决方法是在Settings → Editor → File Types里确认*.jsp有没有关联到JSP文件类型或者安装专门支持JSP的插件。这个问题不影响功能但会影响写代码的体验我一开始用IDEA写JSP时总觉得哪里怪怪的后来发现是高亮没生效。2.3 屏蔽JSP离开页面提示的两种思路这个话题在排除页面跳转问题时非常实用。所谓“JSP离开页面提示”本质是浏览器的beforeunload事件在起作用。很多管理后台为了防止用户误关页面会用window.onbeforeunload弹一个“确定要离开吗”的提示框。但如果你在本地调试时每个页面都弹这个框非常烦躁。屏蔽的思路有两种一种是在JSP页面里去掉或者覆盖掉这段JavaScript代码另一种是直接通过URL“白名单”方式控制。实战里我用得比较多的是在公共的footer里加一个全局开关// common.js 中增加全局开关调试时改成false即可 window.ENABLE_UNLOAD_CONFIRM false; if (window.ENABLE_UNLOAD_CONFIRM) { window.onbeforeunload function() { return 数据尚未保存确认离开吗; }; } else { window.onbeforeunload null; }把这段脚本放到公共JS里所有JSP页面统一引用。正式上线时把开关置为true本地调试时置为false。如果只是临时调试直接在浏览器控制台执行window.onbeforeunload null就能一次性解除刷新页面后如果脚本重新赋值才会再次弹窗。这节课对应的场景里更多是“跳转/屏蔽弹窗”这类前端与后端配合的问题属于现代Web开发里很现实的需求。3. 让Servlet调用大模型API接口3.1 思路与准备现在很多项目都会在Servlet后端里调用第三方大模型API接口比如根据用户输入返回一句话、生成摘要、做意图识别。这部分内容在传统《Head First Servlet JSP》里没有但既然很多读者搜到了这个关键词我觉得值得写一段。核心思路很简单Servlet作为后端Java程序通过HTTP客户端工具java.net.http.HttpClient 或 Apache HttpClient向大模型服务发送请求拿到结果后再把数据封装进request/session转发到JSP渲染。先确认JDK版本JDK 11以上自带了java.net.http.HttpClient不需要额外引依赖。如果你用的是JDK 8那就只能引入Apache HttpClient或OkHttp的jar包了。我在这台机器上是JDK 17所以直接用JDK内置的HttpClient。大模型API服务有很多种形态这里以“调用远程HTTP接口”为例你的代码里只需要关心两个东西请求URL和请求体格式。一般通用格式是一个JSON包含你输入给模型的内容。这里不涉及任何特殊网络手段就是用Java发了一个标准的HTTPS请求。3.2 完整的Servlet代码结构下面这个AiServlet是我在个人项目里常用的一个模板。它接收前端传来的文本调用大模型API把返回结果放回页面WebServlet(/ai) public class AiServlet extends HttpServlet { private static final HttpClient httpClient HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String userInput req.getParameter(text); String result callAiApi(userInput); req.setAttribute(aiResult, result); req.getRequestDispatcher(/result.jsp).forward(req, resp); } private String callAiApi(String text) { // 构造JSON请求体 String jsonBody { \messages\:[{\role\:\user\,\content\:\ text \}], \max_tokens\:200 }; HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://your-api-endpoint.example/v1/chat)) .timeout(Duration.ofSeconds(30)) .header(Content-Type, application/json) .header(Authorization, Bearer YOUR_API_KEY) .POST(BodyPublishers.ofString(jsonBody)) .build(); try { HttpResponseString response httpClient.send(request, BodyHandlers.ofString()); // 这里只是简单返回响应体实际项目里需要解析JSON return response.body(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return 请求被中断; } catch (IOException e) { e.printStackTrace(); return 接口调用失败; } } }这段代码要注意的细节Authorization头里的API Key不要硬编码在Java代码里应该放到环境变量、配置文件或数据库配置表中。大模型API接口地址和响应格式因服务商而异我这里留的是占位符实际对接时以你使用的API文档为准。如果响应时间较长前端页面会一直等着。更优雅的方案是异步调用或者在Servlet里用Callable配合线程池但这个属于进阶初学者先同步调用把流程跑通。3.3 JSP页面接收AI结果result.jsp里用EL表达式展示结果div pAI返回结果/p p${aiResult}/p /div这里我踩过一个坑大模型API返回的内容可能是JSON格式直接显示在JSP页面上非常丑。后来我在Servlet里先把response.body()解析一下取出里面需要的字段塞到一个简单的ResultDto对象里再放request作用域。这样JSP页面拿到的就是纯文本内容不需要关心JSON结构。如果大家拿到的返回格式比较复杂建议用Jackson或Gson库来解析Maven里加依赖就行dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.15.2/version /dependency解析代码大概是ObjectMapper mapper new ObjectMapper(); JsonNode root mapper.readTree(responseBody); String content root.path(choices).path(0).path(message).path(content).asText();这样“Servlet调用大模型API接口再转发给JSP展示”的完整链路就通了。这个模式在企业项目里很常见本质上跟调数据库接口没区别只是把“数据源”换成了“外部AI服务”。4. 查看JSP编译后的Java类4.1 Tomcat的work目录藏着JSP的“真身”很多读者搜“web项目配置tomcat后查看jsp编译后的java类”我猜是和我当初一样学完了JSP标签、表达式还想进一步确认JSP底层到底是什么。JSP本质上就是一个Servlet。这个说法所有人都会背但只有真的去Tomcat的work目录里翻出那个编译出来的Java文件才算真正理解它的生命周期。以一个名字叫display.jsp的页面为例。项目发布到Tomcat后Tomcat会把JSP翻译成一个Java类路径一般在{Tomcat安装目录}/work/Catalina/localhost/{项目名}/org/apache/jsp/display_jsp.java。我第一次打开这个文件的感受是很震撼的里面就是一个org.apache.jsp.display_jsp类继承了org.apache.jasper.runtime.HttpJspBase而HttpJspBase最终继承的是HttpServlet。这个类里的_jspService方法就是完整版的service方法。你在JSP里写的静态HTML代码全都被封装成了out.write()语句一行一行输出你在JSP里写的% %脚本片段Java代码会原样保留在_jspService方法体里你用到的request、response、session、application等隐式对象在这里都变成了方法参数或预定义变量。这个文件能看的信息量很大能看到JSP翻译成Java时EL表达式被编译成了什么样的运行时调用。能看到为什么JSP比Servlet更“重”——每次请求都要经过_jspService里的检查和输出逻辑。能看到JSP页面改动后Tomcat如何自动检测并重新编译。4.2 在Eclipse和IDEA里分别怎么查看Eclipse里如果你的项目已经发布到Tomcat可以在Servers视图里双击Tomcat服务器打开“Overview”面板找到“Server Path”和“Deploy Path”那个目录对应的work目录里就有编译后的文件。默认发布目录一般是wtpwebapps对应的work目录是work/Catalina/localhost/项目名。IDEA里的路径稍有不同。IDEA不是把项目发布到Tomcat的webapps目录而是用“exploded war”的方式直接部署。可以在Tomcat配置界面里找到“Tomcat Server → Application server → Tomcat installation directory”然后去{TOMCAT_HOME}/work/Catalina/localhost/{context}里找。如果没有自动生成等第一次通过IDEA启动并访问过该JSP后再去看。IDEA用户还有一个常见问题是Color Scheme里没有JSP的语法高亮。这通常是因为IDEA的Settings里没有识别到JSP文件类型或者缺少JSP相关的插件支持。处理方法是File → Settings → Editor → File Types → 找到“JavaServer Pages”或“JSP”文件类型确认注册了*.jsp后缀。如果文件类型里压根没有JSP可以尝试安装“Jakarta EE: Web”相关插件然后重启IDEA。4.3 从编译类反推JSP的“隐式对象”看编译后的Java类还有一个好处能验证那几个“隐式对象”的本质。request→_jspx_page_context.getRequest()实际类型是ServletRequest实际运行时对象是Tomcat封装后的Request。response→_jspx_page_context.getResponse()。session→pageContext.getSession()如果页面配置了sessionfalse这个变量就不可用。application→pageContext.getServletContext()。out→pageContext.getOut()实际是JspWriter是带缓冲的写输出流。如果你在JSP里用了pageContext.setAttribute(key, value)编译后的代码其实是_jspx_page_context.setAttribute(key, value)。理解了这一层再看《Head First Servlet JSP》第三章关于“JSP是Servlet的模板化实现”的表述就顺理成章了。5. 综合实操JSP学生信息管理系统落地5.1 数据库表与连接准备趁热打铁我把第三章学的知识点融进一个Mini学生信息管理系统这个系统在网上也特别多人搜索。功能很简单学生列表展示、新增学生、删除学生。技术栈是Servlet JSP JDBC。先建表CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, age INT, major VARCHAR(100) );连接数据库用JDBC的DriverManager即可为了省事我没有上连接池。把连接信息放到一个JdbcUtil工具类里public class JdbcUtil { private static final String URL jdbc:mysql://localhost:3306/school?useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD yourpassword; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }Maven里需要加MySQL驱动依赖dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency5.2 Servlet与JSP分层实现项目包结构我习惯这样分com.example.entityStudent实体类com.example.daoStudentDao操作数据库com.example.servletListStudentServlet、AddStudentServlet、DeleteStudentServletwebapplist.jsp、add.jspStudentDao里写一个最简单查询方法public ListStudent findAll() { ListStudent list new ArrayList(); String sql SELECT id, name, age, major FROM student; try (Connection conn JdbcUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setName(rs.getString(name)); s.setAge(rs.getInt(age)); s.setMajor(rs.getString(major)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; }ListStudentServlet里调用dao把列表放到request作用域并转发到list.jspWebServlet(/list) public class ListStudentServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { StudentDao dao new StudentDao(); ListStudent students dao.findAll(); req.setAttribute(students, students); req.getRequestDispatcher(/list.jsp).forward(req, resp); } }list.jsp里用EL JSTL遍历。JSTL标签需要引入依赖dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependencyJSP核心% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table tr thID/th th姓名/th th年龄/th th专业/th th操作/th /tr c:forEach items${students} vars tr td${s.id}/td td${s.name}/td td${s.age}/td td${s.major}/td tda href${pageContext.request.contextPath}/delete?id${s.id}删除/a/td /tr /c:forEach /table这里${pageContext.request.contextPath}一定要用否则项目部署在非根路径时链接会404。这个细节我是踩过坑的直接写/delete?id1在Eclipse嵌入式Tomcat里可能刚好没问题但一旦部署到Tomcat的某个子应用路径下永远找不到资源。新增页面的逻辑类似form提交到AddStudentServletServlet里接收参数、装成Student对象、调用dao.insert、最后resp.sendRedirect(list)重定向到列表页。注意这里用的是重定向而不是转发这样才能避免用户按F5时表单重复提交。5.3 常见问题排查速查表问题现象原因分析解决思路JSP页面报500错误日志里有ClassNotFoundExceptionMaven依赖没打进去或者Tomcat运行时找不到jar包检查WEB-INF/lib里有没有mysql或jstlMaven项目可以执行mvn package后看war内容页面文字乱码请求编码或响应编码不一致Servlet里req.setCharacterEncoding(UTF-8)JSP页面里contentType加上charsetUTF-8URL连接串加上characterEncodingutf8EL表达式输出原样${studentInfo.name}页面没启用EL或者Servlet容器版本过低检查web.xml的web-app版本是否在2.4以上或查看page指令里isELIgnored是否设置成了true点击删除按钮后404链接少了contextPath使用${pageContext.request.contextPath}拼接路径刷新页面数据重复提交新增操作用了forward新增、修改、删除这类“写”操作结束后一定要用sendRedirect重定向到列表页JSP改动后浏览器显示的还是旧页面Tomcat JSP缓存删除Tomcat的work目录下对应项目文件重新启动IDEA里可以执行Build → Rebuild ProjectTomcat启动报端口被占用8005、8080、8009端口被其他进程占用找到占用进程结束或修改Tomcat的server.xml端口这些坑大多不是理论和算法问题纯粹是配置和规范上的细节但排查起来很费时间。我把它们汇总成一张表方便大家对照。6. 个人体会与这个项目的扩展方向按《Head First Servlet JSP》第三章的进度学到这个阶段最明显的感受是Servlet JSP这套组合并没有被时代淘汰而是变成了Java Web开发的“底层语言”。现在流行的Spring MVC核心分发器还是一个ServletDispatcherServlet页面渲染用的是整合后的视图解析器。在学Spring框架之前把Servlet、JSP、Filter、Listener这几个基本功打扎实后面调框架底层问题时能省太多力气。这个学生信息管理系统如果还想继续扩展顺着几个方向往下写都是很好的练手项目加一个Filter做登录拦截和字符编码统一处理、用EL和JSTL替代所有% %脚本、用自己的BaseServlet抽象公共逻辑或者接一个连接池替代JDBC直连。我个人特别建议把“Filter 编码处理”作为下一个目标因为实际项目里几乎没有一个Web应用不涉及Filter这是Servlet规范里最容易被人低估、但生产环境却离不开的组件。最后再分享一个小技巧学习JSP时没必要把所有标签都背下来。真正经常用的就是c:forEach、c:if、c:choose这几个核心标签外加EL表达式。页面上遇到复杂逻辑时优先考虑是不是应该在Servlet这边把数据整理好而不是硬塞一堆Java脚本到JSP里。能保持页面简单的工作方式才是可持续的工作方式。