程序员从Java转JSP服务器端开发入门:从环境配置到网上商城动态页面实战教程
嘿,看到你这个标题我就知道你是谁——Java后端的老手了,Spring、MyBatis玩得溜,但现在公司业务需要,或者你想快速上手一个老项目的维护,必须得接触JSP服务器端开发。别慌,我今天给你整一份从环境搭到项目跑通的实战指南,保证让你不迷路。
一、先说说JSP到底是个什么玩意儿
很多人一听JSP就觉得”这玩意儿都过时了”,确实,在Spring Boot横行的今天,JSP已经不再是主流了。但是!老系统的维护、企业级应用的 legacy 代码、很多政府/金融项目的内部系统,还在大量使用JSP。作为一个成熟程序员,了解它是加分项,不是负担。
JSP(JavaServer Pages)本质上就是把Java代码嵌入到HTML里,服务端渲染好再发给浏览器。它的执行流程很简单:
浏览器请求 → Tomcat接收 → JSP编译成Servlet → 执行Java代码 → 生成HTML → 返回浏览器
你写的是 .jsp 文件,但最终运行的其实是 .java 转出来的 Servlet 类。所以你当Java程序员转过来,最大的优势就是——Java语法你全会了。
二、环境搭建:别被这些坑绊倒
2.1 JDK安装(你已经有了,但还是走一遍)
# 检查当前JDK版本,确保是1.8或以上
java -version
javac -version
如果你用的是Java 8,那是最稳妥的选择。JSP在这上面跑最稳定。
2.2 Maven项目骨架
新建一个标准的Maven Web项目,目录结构一定要这样:
jsp-shop/
├── pom.xml
├── src/
│ └── main/
│ ├── java/ # Java代码放这里
│ ├── webapp/
│ │ ├── WEB-INF/
│ │ │ └── web.xml
│ │ ├── index.jsp
│ │ ├── css/
│ │ ├── js/
│ │ └── jsp/
│ │ ├── product/
│ │ ├── cart/
│ │ └── order/
│ └── resources/
└── target/
2.3 pom.xml 关键依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jsp-shop</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- JSP API -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<!-- JSTL 标签库,这个必须有 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 数据库连接池 HikariCP -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>4.0.3</version>
</dependency>
<!-- Lombok(可选,但强烈推荐)-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>shop</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
</project>
注意几个坑点:
- Servlet和JSP API的scope必须是
provided,因为Tomcat本身已经提供了这些API,重复打包会冲突 javax.servlet.jsp不是jakarta.servlet.jsp,JSP 2.x时代还是javax命名空间- JSTL必须加,不然你的JSP页面上
<c:foreach>根本跑不起来
2.4 Tomcat配置
下载Tomcat 9.x(注意:Tomcat 10.x改用了jakarta命名空间,和JSP 2.x不兼容,新手踩坑重灾区):
# 解压后配置环境变量
export CATALINA_HOME=/opt/tomcat-9.0.80
export PATH=$PATH:$CATALINA_HOME/bin
# 启动
catalina.sh run
启动后访问 http://localhost:8080,看到那只猫就说明OK了。
三、Web.xml:JSP项目的导航地图
<?xml version="1.0" encoding="UTF-8"?>
<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_4_0.xsd"
version="4.0">
<display-name>网上商城</display-name>
<!-- 编码过滤器,防止中文乱码,这个必须放第一个 -->
<filter>
<filter-name>EncodingFilter</filter-name>
<filter-class>com.example.shop.filter.EncodingFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>EncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 主Servlet路由 -->
<servlet>
<servlet-name>ProductServlet</servlet-name>
<servlet-class>com.example.shop.servlet.ProductServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>ProductServlet</servlet-name>
<url-pattern>/product/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>CartServlet</servlet-name>
<servlet-class>com.example.shop.servlet.CartServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CartServlet</servlet-name>
<url-pattern>/cart/*</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>OrderServlet</servlet-name>
<servlet-class>com.example.shop.servlet.OrderServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>OrderServlet</servlet-name>
<url-pattern>/order/*</url-pattern>
</servlet-mapping>
<!-- 欢迎页面 -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
四、核心Servlet:MVC的骨架
很多Java程序员刚转JSP时会困惑:MVC在JSP里怎么体现?其实很简单——Servlet是Controller,JSP是View,JavaBean是Model。
4.1 数据库工具类
package com.example.shop.util;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Slf4j
public class DBUtil {
private static final DataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/jsp_shop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8");
config.setUsername("root");
config.setPassword("your_password");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setConnectionTimeout(30000);
dataSource = new HikariDataSource(config);
log.info("数据库连接池初始化完成");
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void close(AutoCloseable... resources) {
for (AutoCloseable resource : resources) {
if (resource != null) {
try {
resource.close();
} catch (Exception e) {
log.error("资源关闭失败", e);
}
}
}
}
}
4.2 商品实体类
package com.example.shop.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class Product {
private Long id;
private String name;
private String description;
private BigDecimal price;
private Integer stock;
private String imageUrl;
private Integer categoryId;
private Date createTime;
private Integer status; // 1=上架 0=下架
}
4.3 商品Servlet
package com.example.shop.servlet;
import com.example.shop.entity.Product;
import com.example.shop.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
@Slf4j
public class ProductServlet extends HttpServlet {
private ProductService productService;
@Override
public void init() throws ServletException {
productService = new ProductService();
log.info("ProductServlet 初始化完成");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String action = req.getServletPath().substring("/product".length());
switch (action) {
case "/list":
listProducts(req, resp);
break;
case "/detail":
showDetail(req, resp);
break;
default:
listProducts(req, resp);
break;
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doGet(req, resp);
}
private void listProducts(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String categoryId = req.getParameter("categoryId");
String keyword = req.getParameter("keyword");
List<Product> products = productService.searchProducts(categoryId, keyword);
req.setAttribute("products", products);
req.setAttribute("currentPage", "product");
req.getRequestDispatcher("/jsp/product/list.jsp").forward(req, resp);
}
private void showDetail(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String id = req.getParameter("id");
Product product = productService.getProductById(Long.parseLong(id));
if (product == null) {
resp.sendRedirect("/product/list");
return;
}
req.setAttribute("product", product);
req.setAttribute("currentPage", "detail");
req.getRequestDispatcher("/jsp/product/detail.jsp").forward(req, resp);
}
}
五、JSP页面:JSP的核心阵地
5.1 商品列表页
<%--
商品列表页面
作者:一个正在转JSP的Java老手
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%-- 头部引入 --%>
<%-- 这里用Include指令,不是JSP动作,因为静态资源用include directive性能更好 --%>
<jsp:include page="/jsp/common/header.jsp"/>
<div class="container">
<div class="row">
<%-- 左侧分类栏 --%>
<div class="col-md-3">
<div class="card">
<div class="card-header">商品分类</div>
<ul class="list-group list-group-flush">
<c:forEach var="cat" items="${categoryList}">
<li class="list-group-item">
<a href="/product/list?categoryId=${cat.id}"
class="${param.categoryId eq cat.id ? 'active' : ''}">
${cat.name}
</a>
</li>
</c:forEach>
</ul>
</div>
</div>
<%-- 右侧商品列表 --%>
<div class="col-md-9">
<div class="search-bar mb-3">
<form action="/product/list" method="get" class="form-inline">
<input type="text" name="keyword" class="form-control"
placeholder="搜索商品..." value="${param.keyword}"/>
<button type="submit" class="btn btn-primary">搜索</button>
</form>
</div>
<div class="product-grid">
<c:choose>
<c:when test="${empty products}">
<div class="text-center py-5">
<p class="text-muted">暂无商品</p>
</div>
</c:when>
<c:otherwise>
<c:forEach var="product" items="${products}" varStatus="status">
<div class="product-card">
<a href="/product/detail?id=${product.id}">
<img src="${product.imageUrl}" alt="${product.name}"
class="product-img"/>
</a>
<h5 class="product-title">
<a href="/product/detail?id=${product.id}">${product.name}</a>
</h5>
<p class="product-price">
<fmt:formatNumber value="${product.price}" type="currency"
currencySymbol="¥"/>
</p>
<span class="stock-tag ${product.stock > 0 ? 'in-stock' : 'out-of-stock'}">
${product.stock > 0 ? '有货' : '缺货'}
</span>
</div>
<%-- 每4个一行,用模运算 --%>
<c:if test="${(status.count % 4 == 0) && not status.last}">
<div class="w-100"></div>
</c:if>
</c:forEach>
</c:otherwise>
</c:choose>
</div>
</div>
</div>
</div>
<%-- 底部引入 --%>
<jsp:include page="/jsp/common/footer.jsp"/>
5.2 商品详情页
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<jsp:include page="/jsp/common/header.jsp"/>
<div class="container">
<div class="breadcrumb">
<a href="/product/list">首页</a> >
<a href="/product/list?categoryId=${product.categoryId}">分类</a> >
<span>${product.name}</span>
</div>
<div class="product-detail row mt-4">
<div class="col-md-5">
<div class="main-image">
<img src="${product.imageUrl}" alt="${product.name}" class="img-fluid"/>
</div>
</div>
<div class="col-md-7">
<h1>${product.name}</h1>
<div class="price-box">
<span class="current-price">
<fmt:formatNumber value="${product.price}" type="currency" currencySymbol="¥"/>
</span>
</div>
<div class="product-info">
<table class="table table-bordered">
<tr>
<th width="100">商品编号</th>
<td>${product.id}</td>
</tr>
<tr>
<th>库存</th>
<td>
<span class="${product.stock > 0 ? 'text-success' : 'text-danger'}">
剩余 ${product.stock} 件
</span>
</td>
</tr>
<tr>
<th>商品描述</th>
<td>${product.description}</td>
</tr>
</table>
</div>
<div class="buy-box mt-4">
<form action="/cart/add" method="post" class="form-inline">
<input type="hidden" name="productId" value="${product.id}"/>
<input type="hidden" name="productName" value="${product.name}"/>
<input type="hidden" name="productPrice" value="${product.price}"/>
<input type="hidden" name="productImage" value="${product.imageUrl}"/>
<label>数量:</label>
<input type="number" name="quantity" value="1" min="1"
max="${product.stock}" class="form-control mx-2" style="width:80px"/>
<button type="submit" class="btn btn-success btn-lg mx-2">
<i class="icon-cart"></i> 加入购物车
</button>
<button type="button" class="btn btn-danger btn-lg"
onclick="buyNow(${product.id})">
立即购买
</button>
</form>
</div>
</div>
</div>
</div>
<script>
function buyNow(productId) {
// 跳转到购物车结算页,简化处理
window.location.href = '/cart/check?productId=' + productId;
}
</script>
<jsp:include page="/jsp/common/footer.jsp"/>
六、购物车模块:Session的经典用法
购物车是JSP项目里最体现”状态管理”的地方,我们用HttpSession来实现。
6.1 购物车实体类
package com.example.shop.entity;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class CartItem {
private Long productId;
private String productName;
private BigDecimal price;
private Integer quantity;
private String imageUrl;
// 小计
public BigDecimal getSubtotal() {
return price.multiply(new BigDecimal(quantity));
}
}
6.2 购物车Servlet
package com.example.shop.servlet;
import com.example.shop.entity.CartItem;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class CartServlet extends HttpServlet {
// 购物车key
private static final String CART_SESSION_KEY = "shoppingCart";
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String action = req.getServletPath().substring("/cart".length());
switch (action) {
case "/list":
showCart(req, resp);
break;
case "/checkout":
showCheckout(req, resp);
break;
default:
showCart(req, resp);
break;
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String action = req.getParameter("action");
if ("add".equals(action)) {
addToCart(req, resp);
} else if ("remove".equals(action)) {
removeFromCart(req, resp);
} else if ("update".equals(action)) {
updateQuantity(req, resp);
} else {
doGet(req, resp);
}
}
private void addToCart(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
Long productId = Long.parseLong(req.getParameter("productId"));
String productName = req.getParameter("productName");
BigDecimal price = new BigDecimal(req.getParameter("productPrice"));
String imageUrl = req.getParameter("productImage");
int quantity = Integer.parseInt(req.getParameter("quantity"));
HttpSession session = req.getSession();
@SuppressWarnings("unchecked")
List<CartItem> cart = (List<CartItem>) session.getAttribute(CART_SESSION_KEY);
if (cart == null) {
cart = new ArrayList<>();
}
// 检查是否已存在
boolean found = false;
for (CartItem item : cart) {
if (item.getProductId().equals(productId)) {
item.setQuantity(item.getQuantity() + quantity);
found = true;
break;
}
}
if (!found) {
CartItem newItem = new CartItem();
newItem.setProductId(productId);
newItem.setProductName(productName);
newItem.setPrice(price);
newItem.setQuantity(quantity);
newItem.setImageUrl(imageUrl);
cart.add(newItem);
}
session.setAttribute(CART_SESSION_KEY, cart);
// 重定向避免重复提交
resp.sendRedirect("/cart/list");
}
private void removeFromCart(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
Long productId = Long.parseLong(req.getParameter("productId"));
HttpSession session = req.getSession();
@SuppressWarnings("unchecked")
List<CartItem> cart = (List<CartItem>) session.getAttribute(CART_SESSION_KEY);
if (cart != null) {
cart.removeIf(item -> item.getProductId().equals(productId));
session.setAttribute(CART_SESSION_KEY, cart);
}
resp.sendRedirect("/cart/list");
}
private void updateQuantity(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
Long productId = Long.parseLong(req.getParameter("productId"));
int quantity = Integer.parseInt(req.getParameter("quantity"));
HttpSession session = req.getSession();
@SuppressWarnings("unchecked")
List<CartItem> cart = (List<CartItem>) session.getAttribute(CART_SESSION_KEY);
if (cart != null) {
cart.stream()
.filter(item -> item.getProductId().equals(productId))
.forEach(item -> item.setQuantity(quantity));
session.setAttribute(CART_SESSION_KEY, cart);
}
resp.sendRedirect("/cart/list");
}
private void showCart(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
HttpSession session = req.getSession();
@SuppressWarnings("unchecked")
List<CartItem> cart = (List<CartItem>) session.getAttribute(CART_SESSION_KEY);
req.setAttribute("cartItems", cart != null ? cart : new ArrayList<>());
req.getRequestDispatcher("/jsp/cart/list.jsp").forward(req, resp);
}
private void showCheckout(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
HttpSession session = req.getSession();
@SuppressWarnings("unchecked")
List<CartItem> cart = (List<CartItem>) session.getAttribute(CART_SESSION_KEY);
req.setAttribute("cartItems", cart != null ? cart : new ArrayList<>());
req.getRequestDispatcher("/jsp/cart/checkout.jsp").forward(req, resp);
}
}
6.3 购物车列表页
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<jsp:include page="/jsp/common/header.jsp"/>
<div class="container">
<h2>我的购物车</h2>
<c:choose>
<c:when test="${empty cartItems}">
<div class="empty-cart text-center py-5">
<p class="text-muted">购物车是空的,快去逛逛吧~</p>
<a href="/product/list" class="btn btn-primary">去购物</a>
</div>
</c:when>
<c:otherwise>
<table class="table table-bordered cart-table">
<thead>
<tr>
<th width="80">图片</th>
<th>商品名称</th>
<th width="120">单价</th>
<th width="120">数量</th>
<th width="120">小计</th>
<th width="80">操作</th>
</tr>
</thead>
<tbody>
<c:forEach var="item" items="${cartItems}">
<tr>
<td>
<img src="${item.imageUrl}" width="60" height="60" class="img-thumbnail"/>
</td>
<td>${item.productName}</td>
<td>
<fmt:formatNumber value="${item.price}" type="currency" currencySymbol="¥"/>
</td>
<td>
<form action="/cart" method="post" class="form-inline">
<input type="hidden" name="action" value="update"/>
<input type="hidden" name="productId" value="${item.productId}"/>
<input type="number" name="quantity" value="${item.quantity}"
min="1" class="form-control form-control-sm"
style="width:60px"/>
<button type="submit" class="btn btn-sm btn-outline-secondary ml-1">更新</button>
</form>
</td>
<td class="text-danger">
<fmt:formatNumber value="${item.subtotal}" type="currency" currencySymbol="¥"/>
</td>
<td>
<form action="/cart" method="post" style="display:inline">
<input type="hidden" name="action" value="remove"/>
<input type="hidden" name="productId" value="${item.productId}"/>
<button type="submit" class="btn btn-sm btn-danger"
onclick="return confirm('确定删除?')">删除</button>
</form>
</td>
</tr>
</c:forEach>
</tbody>
<tfoot>
<tr>
<td colspan="4" class="text-right"><strong>合计:</strong></td>
<td class="text-danger">
<strong>
<fmt:formatNumber value="${cartItems|sum}" type="currency" currencySymbol="¥"/>
</strong>
</td>
<td></td>
</tr>
</tfoot>
</table>
<div class="cart-actions text-right">
<a href="/product/list" class="btn btn-secondary mr-2">继续购物</a>
<a href="/cart/checkout" class="btn btn-success">去结算</a>
</div>
</c:otherwise>
</c:choose>
</div>
<jsp:include page="/jsp/common/footer.jsp"/>
注意:上面
${cartItems|sum}是示例写法,JSP本身没有内置的列表求和标签,实际项目中需要你在Servlet里计算好总额再传过去,或者用JSTL的<c:forEach>累加。实际代码中我会这样做:
<%-- 在JavaBean或Servlet中预先计算总额 --%>
BigDecimal total = cartItems.stream()
.map(CartItem::getSubtotal)
.reduce(BigDecimal.ZERO, BigDecimal::add);
req.setAttribute("cartTotal", total);
然后在JSP里:${cartTotal}
七、JSP的”三板斧”:脚本、表达式、指令
这部分是很多Java程序员转JSP最容易混淆的地方,我给你掰开揉碎讲。
7.1 脚本片段 <% %>
这是最原始的写法,直接写Java代码:
<%
// 这里可以写任何合法的Java代码
String userName = (String) session.getAttribute("userName");
if (userName == null) {
userName = "游客";
}
%>
<p>欢迎,<%= userName %>!</p>
不建议在JSP里写复杂逻辑,保持页面简洁,业务逻辑放在Servlet里。
7.2 表达式 <%= %>
这是脚本片段的简化版,等价于out.print():
<!-- 这两种写法效果一样 -->
<p>商品名称:<%= product.getName() %></p>
<p>商品名称:<% out.print(product.getName()); %></p>
7.3 指令 <%@ %>
有三种指令,用熟了好用:
<%-- page指令:声明页面属性 --%>
<%@ page contentType="text/html;charset=UTF-8"
language="java"
import="java.util.List, java.util.ArrayList"
isErrorPage="false"
session="true" %>
<%-- include指令:静态包含(编译时) --%>
<%@ include file="/jsp/common/header.jsp" %>
<%-- taglib指令:引入标签库 --%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
include指令 vs jsp:include动作的区别:
@include是静态包含,被包含的文件内容直接合并进来,编译时执行<jsp:include>是动态包含,运行时执行,有独立的请求上下文
对于头部和底部这种固定内容,用@include性能更好。
八、JSTL标签库:告别脚本,拥抱声明式
在JSP里写Java代码是反模式,JSTL才是正解。JSP 2.0之后,原则上不应该在页面中出现任何<% %>脚本片段。
8.1 核心标签库
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%-- 条件判断 --%>
<c:choose>
<c:when test="${cartTotal > 100}">
<span class="badge badge-success">免运费</span>
</c:when>
<c:when test="${cartTotal > 50}">
<span>满50元包邮,还需¥${100 - cartTotal}</span>
</c:when>
<c:otherwise>
<span>满100元包邮</span>
</c:otherwise>
</c:choose>
<%-- 循环遍历 --%>
<c:forEach var="product" items="${products}" varStatus="status">
<div class="product-item">
<c:if test="${status.count <= 1}">
<span class="new-tag">新品</span>
</c:if>
${product.name}
</div>
</c:forEach>
<%-- 去掉HTML特殊字符 --%>
<c:out value="${userInput}" escapeXml="true"/>
<%-- 异常处理 --%>
<c:catch var="exception">
<%-- 可能有异常的操作 --%>
</c:catch>
8.2 格式化标签库
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%-- 日期格式化 --%>
<fmt:formatDate value="${product.createTime}" pattern="yyyy-MM-dd HH:mm"/>
<%-- 数字格式化 --%>
<fmt:formatNumber value="${product.price}" type="currency" currencySymbol="¥"
groupingUsed="true"/>
<%-- 国际化 --%>
<fmt:message key="welcome.message"/>
九、订单模块:事务与状态流转
订单是商城最核心的业务,这里涉及数据库事务处理。
package com.example.shop.service;
import com.example.shop.entity.Order;
import com.example.shop.entity.OrderItem;
import com.example.shop.entity.CartItem;
import com.example.shop.util.DBUtil;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.sql.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Slf4j
public class OrderService {
public Long createOrder(String username, List<CartItem> cartItems, String address)
throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false);
// 1. 插入订单主表
String orderSql = "INSERT INTO orders (username, total_amount, address, "
+ "status, create_time) VALUES (?, ?, ?, 0, ?)";
ps = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS);
BigDecimal totalAmount = BigDecimal.ZERO;
for (CartItem item : cartItems) {
totalAmount = totalAmount.add(item.getSubtotal());
}
ps.setString(1, username);
ps.setBigDecimal(2, totalAmount);
ps.setString(3, address);
ps.setTimestamp(4, new Timestamp(new Date().getTime()));
ps.executeUpdate();
rs = ps.getGeneratedKeys();
Long orderId = null;
if (rs.next()) {
orderId = rs.getLong(1);
}
// 2. 插入订单明细
String itemSql = "INSERT INTO order_items (order_id, product_id, product_name, "
+ "price, quantity) VALUES (?, ?, ?, ?, ?)";
ps = conn.prepareStatement(itemSql);
for (CartItem item : cartItems) {
ps.setLong(1, orderId);
ps.setLong(2, item.getProductId());
ps.setString(3, item.getProductName());
ps.setBigDecimal(4, item.getPrice());
ps.setInt(5, item.getQuantity());
ps.addBatch();
}
ps.executeBatch();
// 3. 扣减库存
String stockSql = "UPDATE products SET stock = stock - ? WHERE id = ?";
ps = conn.prepareStatement(stockSql);
for (CartItem item : cartItems) {
ps.setInt(1, item.getQuantity());
ps.setLong(2, item.getProductId());
ps.addBatch();
}
ps.executeBatch();
conn.commit();
log.info("订单创建成功,订单号:{}", orderId);
return orderId;
} catch (Exception e) {
if (conn != null) {
conn.rollback();
}
log.error("订单创建失败", e);
throw e;
} finally {
DBUtil.close(rs, ps, conn);
}
}
public List<Order> getOrdersByUsername(String username) throws SQLException {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
String sql = "SELECT * FROM orders WHERE username = ? ORDER BY create_time DESC";
ps = conn.prepareStatement(sql);
ps.setString(1, username);
rs = ps.executeQuery();
List<Order> orders = new ArrayList<>();
while (rs.next()) {
Order order = new Order();
order.setId(rs.getLong("id"));
order.setUsername(rs.getString("username"));
order.setTotalAmount(rs.getBigDecimal("total_amount"));
order.setAddress(rs.getString("address"));
order.setStatus(rs.getInt("status"));
order.setCreateTime(rs.getTimestamp("create_time"));
orders.add(order);
}
return orders;
} finally {
DBUtil.close(rs, ps, conn);
}
}
}
package com.example.shop.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@Data
public class Order {
private Long id;
private String username;
private BigDecimal totalAmount;
private String address;
private Integer status;
private Date createTime;
private List<OrderItem> items;
// 状态中文映射
public String getStatusText() {
switch (status) {
case 0: return "待付款";
case 1: return "已付款";
case 2: return "已发货";
case 3: return "已完成";
case 4: return "已取消";
default: return "未知";
}
}
}
十、订单确认页面
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<jsp:include page="/jsp/common/header.jsp"/>
<div class="container">
<h2>确认订单</h2>
<div class="row mt-4">
<div class="col-md-8">
<div class="card">
<div class="card-header">收货地址</div>
<div class="card-body">
<form id="orderForm" action="/order" method="post">
<input type="hidden" name="action" value="create"/>
<div class="form-group">
<label>收货人</label>
<input type="text" name="username" class="form-control"
value="${sessionScope.userName}" required/>
</div>
<div class="form-group">
<label>收货地址</label>
<textarea name="address" class="form-control" rows="3" required></textarea>
</div>
<div class="form-group">
<label>联系电话</label>
<input type="tel" name="phone" class="form-control" required/>
</div>
<div class="form-group">
<label>备注</label>
<textarea name="remark" class="form-control" rows="2"></textarea>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header">订单摘要</div>
<div class="card-body">
<c:forEach var="item" items="${cartItems}">
<div class="d-flex justify-content-between py-1">
<span>${item.productName} × ${item.quantity}</span>
<fmt:formatNumber value="${item.subtotal}" type="currency"
currencySymbol="¥"/>
</div>
</c:forEach>
<hr/>
<div class="d-flex justify-content-between font-weight-bold">
<span>合计:</span>
<fmt:formatNumber value="${cartTotal}" type="currency"
currencySymbol="¥"/>
</div>
</div>
<div class="card-footer">
<button type="submit" form="orderForm" class="btn btn-success btn-block">
确认提交订单
</button>
</div>
</div>
</div>
</div>
</div>
<jsp:include page="/jsp/common/footer.jsp"/>
十一、编码过滤器:中文不乱码的关键
JSP项目最容易踩的坑就是中文乱码。一个过滤器搞定所有编码问题:
package com.example.shop.filter;
import javax.servlet.*;
import java.io.IOException;
public class EncodingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 销毁
}
}
十二、常见坑点汇总
| 问题 | 原因 | 解决方案 |
|---|---|---|
| JSTL标签不生效 | 没加taglib指令或jar包缺失 | 确认jstl-1.2.jar在WEB-INF/lib下 |
| 中文乱码 | 请求/响应编码未设置 | 加EncodingFilter,web.xml里第一个映射 |
| 404找不到JSP | 路径写错 | 检查web.xml的requestDispatcher路径,用绝对路径/jsp/xxx |
| Session拿不到数据 | session被关闭或超时 | 检查session.invalidate()调用,默认超时30分钟 |
| EL表达式不生效 | Tomcat版本不支持 | 确保Tomcat 5.5以上,JSP 2.0+ |
| javax vs jakarta冲突 | Tomcat 10用jakarta,JSP 2.x用javax | Tomcat 9及以下是javax,新手用Tomcat 9 |
十三、从JSP到现代开发的过渡思考
说实话,作为Java老手,你可能已经注意到JSP的局限性:页面和逻辑耦合、标签库语法繁琐、调试困难。这恰恰也是Spring MVC + Thymeleaf取代JSP的原因。
但你学习JSP的价值在于:
- 理解Servlet容器的工作机制——这是所有Java Web框架的底层
- 维护老项目的能力——很多银行/政务系统的核心代码还是JSP
- 对请求/响应生命周期的深刻理解——这些知识迁移到Spring里是无缝的
如果你后续想往Spring方向过渡,JSP里的request.setAttribute对应Spring的Model.addAttribute,HttpSession对应Spring的@SessionAttributes,概念是相通的。
以上就是从环境配置到商城实战的完整教程。JSP这东西,写起来可能没有Spring Boot那么优雅,但它能让你真正理解”Web请求是怎么在服务端变成HTML返回给浏览器的”。这个理解,是你作为后端工程师的根基。
有任何细节问题,随时问我。
