前几天有个读者来找我排查问题,他维护的是一个传统电商后台,登录、商品、订单全是用 JSP 写的。他问了我一句:现在前端都用 Vue、React 了,学 JSP 还有用吗?我反问他:你先说说 JSP 和 Servlet 是什么关系?他愣了一下,说不太清楚。
这个场面我想很多人都不陌生。很多人把精力全扑在新技术上,但 Java Web 岗位的面试里,JSP 八股文依然高频出现;企业里,十年八年前写好的 JSP 商城页面也还在线上跑着。作为 Java 后端开发,你可以不用 JSP 去开发新项目,但必须看得懂、改得动、讲得清 JSP 页面。
这篇就以一个 Java(JSP)实现的简单商城页面前端示例为主线,从搭环境开始,把 JSP 指令、脚本片段、EL 表达式、JSTL、JavaBean 动作标签、JavaScript 和 jQuery 配合、常见坑位全部串起来讲一遍。内容偏向实际动手,适合正在学 Java Web 的初学者,也适合准备面试想快速捡起 JSP 的开发者。
1. 为什么还在用 JSP 写商城页面——它的价值和边界
1.1 JSP 在今天的真实处境
JSP 全称 JavaServer Pages,核心思想是在 HTML 中嵌入 Java 代码,让页面能够动态生成内容。它本质上是一个 Servlet——第一次被请求时,容器会把 JSP 翻译成 Servlet 源码,再编译执行。理解了这一句,后面很多面试题都能自己想明白。
那为什么造出 JSP?早期做动态网站,如果全用 Servlet 输出 HTML,代码是这样的:
java复制out.println("<html>");
out.println("<head><title>商品列表</title></head>");
out.println("<body>");
for (Product p : products) {
out.println("<div>" + p.getName() + "</div>");
}
out.println("</body></html>");
写页面像拼字符串,标签结构一复杂根本没法维护。JSP 反过来了,让人直接写 HTML,在需要动态数据的位置插入 Java 片段。这个思路在当时非常先进,也让做 Java 的团队第一次体会到"模板化开发"的快乐。
今天再看 JSP,它确实不像 Vue、React 那样时髦,但远没有"死"。很多老牌企业信息系统、学校管理平台、传统电商项目都是 JSP 技术栈。你去维护这样的系统,就必须懂 JSP 页面的组织方式、标签用法、脚本片段边界。面试官问 JSP,其实是在考察你对整个 Java Web 请求链路熟不熟——谁掌握了底层 Servlet 机制,谁才算真正入了 Java Web 的门。
1.2 简单商城示例到底适合谁
这个项目的定位是"简单商城页面前端示例",也就是说它重点解决的是页面这一层的问题:JSP 页面怎么拆、商品数据怎么循环渲染、购物车表单怎么回显、前端 JavaScript 怎么和 JSP 配合。
如果你每天写的是纯前端,这个例子可能帮不上大忙;但如果你是一个 Java 后端,需要偶尔碰 JSP 页面,或者你正在准备 Java 面试,这个例子就是一份可以照着敲的练习材料。它不涉及复杂框架,不依赖 Spring,只用最基础的 JSP 语法和少量 Servlet 转发,跑起来没有任何心智负担。
我把 JSP 和新一代前后端分离方案放在一起对比过,方便你判断什么场景选什么:
| 对比维度 | JSP 服务端渲染 | 前后端分离(Vue/React) |
|---|---|---|
| 渲染位置 | 服务端动态生成 HTML | 浏览器端渲染或服务端组件 |
| 数据传递 | Servlet 往 request/session 放数据,JSP 读取 | 浏览器通过 AJAX 调接口拿 JSON |
| 开发协作 | 后端写页面,前端参与度低 | 前后端明确分工,并行开发 |
| 部署方式 | 依赖 Web 容器(Tomcat 等) | 静态资源可独立部署 CDN |
| 维护成本 | 小项目很快,大项目容易混乱 | 组件化,大型项目更可控 |
| 适用场景 | 存量系统、中小型项目、实训 | 大型平台、高交互重体验应用 |
需要强调的是,用 JSP 不等于"落后"。很多团队的 JSP 项目跑得稳,是因为业务模型和部署方式足够简单,没必要为了技术时髦去重写。这也是我写这个示例想传达的一个态度:技术选型要匹配场景,而不是跟着热点跑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零搭出可运行的 JSP 商城项目:环境准备与目录结构
2.1 JDK 与 Tomcat:先把运行环境跑通
动手写页面之前,先把环境跑通。我用的组合是 JDK 8 + Tomcat 8.5,这两个版本兼容性好,网上资料也多。JDK 8 之后的版本也能跑 JSP,但很多老项目运行时就锁在 JDK 8,所以我建议你练习时就用这个组合,更贴近真实项目。
安装 JDK 后,先配置环境变量。Windows 上添加一个 JAVA_HOME,指向 JDK 安装目录,例如 C:\Program Files\Java\jdk1.8.0_202,然后在 Path 里加上 %JAVA_HOME%\bin。配置完成后,在命令行执行 java -version,能输出版本号说明环境 OK。
Tomcat 的安装更简单:去官网下载 zip 包,解压到一个不含中文和空格的目录(比如 D:\apache-tomcat-8.5.100),双击 bin\startup.bat 启动。浏览器访问 http://localhost:8080/,看到 Tomcat 默认首页就算启动成功。如果端口被占用,可以改 conf\server.xml 里的端口号,但一般 8080 很少冲突。
2.2 IDEA 创建 Web 项目的两种方式
IDE 我推荐 IntelliJ IDEA,社区版就够用。创建项目有两种方式:
第一种,新建 Project 时选择 Java Enterprise,勾选 Web Application,IDEA 会生成标准的 Web 项目结构。这种方式适合不想引入额外构建工具的情况。
第二种,创建 Maven 项目,然后在 src/main 下补一个 webapp 目录。我更推荐这种方式,因为 Maven 能统一管理依赖,比如后面要用到的 JSTL,直接在 pom.xml 里加坐标就行,不用手动下载 jar 丢到 WEB-INF/lib。
两种方式最终的项目骨架都差不多:
code复制src/main/java Java 源码:Servlet、JavaBean、工具类
src/main/webapp Web 根目录:JSP、CSS、JS、图片
src/main/webapp/WEB-INF 受保护资源,不能通过 URL 直接访问
src/main/webapp/WEB-INF/web.xml Web 配置入口
src/main/webapp/css 样式文件
src/main/webapp/js JavaScript 文件
src/main/webapp/common 公共页面片段
这里有个很容易被新手忽略的点:WEB-INF 目录下的 JSP 文件,浏览器直接输 URL 是访问不到的。想访问必须经过 Servlet 转发。这是一个安全机制,适合放后台页面。商城这种需要直接展示的页面,就放在 webapp 根目录。
2.3 第一个可访问的 JSP 页面
在 webapp 根目录建一个 index.jsp,写最简版本:
jsp复制<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>商城首页</title>
</head>
<body>
<h1>XX商城</h1>
<p>当前时间:<%= new java.util.Date() %></p>
</body>
</html>
配好 Tomcat 运行,浏览器打开 http://localhost:8080/项目名/index.jsp,能看到标题和当前时间,说明整个链路通了。<%= %> 是 JSP 表达式,作用是把 Java 表达式的结果输出到响应里。这里先混个脸熟,下一章会详细展开。
这个阶段最容易踩的坑是编码问题:JSP 文件保存编码、IDE 文件编码、pageEncoding、contentType 里的 charset 必须一致,否则中文乱码。我建议全部统一成 UTF-8,不要用 IDEA 默认的 GBK。别问我是怎么知道的,问就是当初被乱码支配过。
3. 商城首页布局:用 include 指令把页面切成零件
3.1 商城首页的模块拆分思路
一个典型的商城首页,从上往下大概是:顶部栏(登录/注册入口)、Logo 和搜索框、商品分类导航、轮播位、今日推荐、商品网格、底部版权。如果整个页面写成一个 JSP,文件会非常长,谁都不愿意往下翻。
更合理的做法是把稳定区域拆成公共片段,在多个页面里复用。我在这个项目里建了一个 common 目录,放公共片段:
common/top.jsp:顶部栏 + 导航common/footer.jsp:版权信息
这样 index.jsp 只需要引入公共片段,再填充自己的核心区域即可。以后如果要做商品列表页、详情页、购物车页,也能复用这两个片段,改一处全站生效。
3.2 静态 include 与动态 jsp:include 的区别
JSP 提供了两种引入方式,这个区别是面试常问点,也是实际项目里必须搞清楚的。
静态引入用指令:
jsp复制<%@ include file="common/top.jsp" %>
动态引入用动作标签:
jsp复制<jsp:include page="common/top.jsp"/>
区别到底在哪?静态引入是在 JSP 翻译成 Servlet 的阶段,把 top.jsp 的源码直接复制进当前页面,最终只生成一个 Servlet 文件。动态引入则是在运行时请求转发,被引入的页面会被单独编译成 Servlet,再执行输出。
因此:
- 纯静态的大块公共区域,用静态 include,性能好,不会有额外的请求处理开销。
- 如果一个模块需要独立逻辑、可能需要传参数,用动态 jsp:include 更灵活。
商城首页的 header、footer 是稳定区域,用静态 include 就够了。轮播位如果后续要做成独立动态模块,再考虑 jsp:include。
我用一个表格帮你快速记住核心差异:
| 对比点 | 静态 include | 动态 jsp:include |
|---|---|---|
| 合成时机 | 翻译阶段源码合并 | 运行时请求转发 |
| 最终类 | 只有一个 Servlet | 多个 Servlet |
| 变量共享 | 直接共享页面变量 | 默认不共享,可传参 |
| 适用场景 | 稳定公共片段 | 动态模块 |
3.3 公共片段怎么写才不会踩坑
common/top.jsp 我通常这样写:
jsp复制<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div class="topbar">
<div class="container">
<span>欢迎来到 XX商城</span>
<a href="${pageContext.request.contextPath}/cart.jsp">我的购物车</a>
</div>
</div>
<header class="header">
<div class="logo">XX商城</div>
<div class="search">
<input type="text" placeholder="搜索商品">
<button>搜索</button>
</div>
</header>
注意几个细节:
第一,公共片段里不要重复写 <html>、<head> 这类标签。因为静态 include 是源码复制,片段被塞到 index.jsp 中间,如果带了完整的 HTML 骨架,页面结构会错乱。
第二,所有链接和资源路径都用 ${pageContext.request.contextPath} 拼前缀。直接写 /css/style.css 在部署到子路径时会 404,用 contextPath 能自动适配不同部署场景。这个习惯越早养成越好。
第三,每个公共片段最好也有 page 指令,特别是 contentType,避免在引入后出现编码不一致问题。
index.jsp 组装公共片段:
jsp复制<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>XX商城-首页</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">
</head>
<body>
<%@ include file="common/top.jsp" %>
<div class="banner">
<img src="${pageContext.request.contextPath}/images/banner.png" alt="banner">
</div>
<div class="product-grid" id="productGrid">
<!-- 商品列表在第4章实现 -->
</div>
<%@ include file="common/footer.jsp" %>
<script src="${pageContext.request.contextPath}/js/jquery-3.6.0.min.js"></script>
<script src="${pageContext.request.contextPath}/js/common.js"></script>
</body>
</html>
这样一拆,页面结构一目了然,后面改导航、改页脚都不需要进业务页面了。
4. 商品列表动态渲染:从脚本片段到 EL + JSTL
4.1 三种脚本元素是 JSP 的地基
JSP 里有三种 Java 嵌入方式,这是最基础的考点,必须分清楚:
声明用 <%! %>,定义成员变量或方法:
jsp复制<%!
private String getTag(double price) {
return price >= 100 ? "包邮" : "不包邮";
}
%>
脚本片段用 <% %>,写页面过程中需要执行的 Java 语句:
jsp复制<%
List<Product> products = new ArrayList<>();
// 模拟数据
%>
表达式用 <%= %>,把结果输出到响应流:
jsp复制<%= product.getName() %>
很多人刚学的时候分不清 <% %> 和 <%= %>,记忆技巧是:带等号的表示"输出什么",不带等号的表示"做什么"。
如果你以前没接触过 JSP 的这些写法,别急,这段先混个脸熟。真实开发中,这三种写法里只有 <%= %>(或者 EL)用得最多,另外两种会越用越少——原因在第 6 章讲。
4.2 用脚本片段渲染商品列表
先写一个稍微"原始"但能跑的版本,让数据流看起来直观。我在 JSP 里直接创建了一个商品列表:
jsp复制<%@ page import="java.util.*" %>
<%
List<Map<String, Object>> products = new ArrayList<>();
Map<String, Object> p1 = new HashMap<>();
p1.put("id", 1);
p1.put("name", "无线蓝牙耳机");
p1.put("price", 199);
p1.put("image", "images/headphone.png");
products.add(p1);
Map<String, Object> p2 = new HashMap<>();
p2.put("id", 2);
p2.put("name", "机械键盘");
p2.put("price", 399);
p2.put("image", "images/keyboard.png");
products.add(p2);
Map<String, Object> p3 = new HashMap<>();
p3.put("id", 3);
p3.put("name", "便携显示器");
p3.put("price", 1299);
p3.put("image", "images/monitor.png");
products.add(p3);
request.setAttribute("products", products);
%>
然后循环输出商品卡片:
jsp复制<% for (Map<String, Object> p : (List<Map<String, Object>>) request.getAttribute("products")) { %>
<div class="product-card" data-id="<%= p.get("id") %>" data-price="<%= p.get("price") %>">
<img src="${pageContext.request.contextPath}/<%= p.get("image") %>" alt="<%= p.get("name") %>">
<h3><%= p.get("name") %></h3>
<p class="price">¥<%= p.get("price") %></p>
<button class="btn-add">加入购物车</button>
</div>
<% } %>
这段代码能跑,但我必须坦白说:把所有数据逻辑都堆在 JSP 里并不是好实践。这里是为了演示脚本片段的写法,真实项目中商品列表应该是 Servlet 查询数据库后放到 request 域里的,JSP 只负责读取和渲染。越早明白这个边界,后面越省事。
4.3 用 EL + JSTL 让页面变干净
脚本片段最大的问题是一旦页面结构复杂,Java 代码和 HTML 大量交织,读起来非常痛苦。于是有了 EL 表达式和 JSTL 标签库。
EL 表达式用 ${} 取值,比如 ${product.name} 等价于调用 product.getName()。如果 product 是 Map,则等价于 product.get("name")。EL 对 Map 和 JavaBean 都做了约定,所以写法统一,非常方便。
JSTL 里最常用的就是 c:forEach 循环标签。先引入标签库:
jsp复制<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
然后商品列表可以这样写:
jsp复制<c:forEach var="p" items="${products}" varStatus="st">
<div class="product-card" data-id="${p.id}" data-price="${p.price}">
<img src="${pageContext.request.contextPath}/${p.image}" alt="${p.name
