1. 项目概述与环境准备
在2020年这个时间节点,使用IntelliJ IDEA配合JSP技术开发电商系统仍然是许多Java学习者的首选方案。我最近刚用IDEA 2020.3版本完整实现了一个蛋糕商城项目,过程中积累了不少实战经验。这个技术组合虽然不算最新,但对于理解Java Web开发的核心流程特别有帮助。
首先需要准备的是开发环境:
- IntelliJ IDEA 2020.x版本(社区版足够用)
- JDK 1.8(推荐jdk1.8.0_251)
- Tomcat 9.0.x
- MySQL 5.7
注意:IDEA 2020版对JSP的支持已经比较成熟,但默认不会创建JSP项目模板,需要手动配置Web框架支持。社区版和企业版在这个功能上没有区别。
安装完基础环境后,在IDEA中新建项目时选择"Java Enterprise",勾选"Web Application"选项。这里有个细节容易被忽略 - 务必将"Version"设置为4.0(对应Servlet 3.1规范),因为新版可能会与部分JSP标签库产生兼容性问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 项目结构与基础配置
2.1 标准目录布局
一个规范的JSP项目应该包含以下核心目录:
code复制/src
/main
/java # 业务逻辑代码
/resources # 配置文件
/webapp # 前端资源
/WEB-INF
/lib # 依赖jar包
web.xml # 部署描述符
/static # 静态资源
/jsp # JSP页面
我强烈建议在webapp下单独建立jsp目录存放所有页面文件,而不是直接放在webapp根目录。这样做有两个好处:一是可以通过WEB-INF保护敏感页面(如后台管理页),二是项目结构更清晰。
2.2 必须的依赖配置
在pom.xml中需要添加的关键依赖:
xml复制<!-- JSP支持 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSTL标签库 -->
<dependency>
<groupId>javax.servlet.jsp.jstl</groupId>
<artifactId>javax.servlet.jsp.jstl-api</artifactId>
<version>1.2.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
踩坑提醒:JSTL 1.2.x版本需要配合标准标签库实现,记得同时添加taglibs-standard-impl依赖,否则会报找不到标签处理类的错误。
3. 核心功能实现
3.1 商品展示模块
蛋糕商城的核心是商品展示,我们采用经典的MVC模式实现:
- Model层 - Product.java实体类:
java复制public class Product {
private int id;
private String name;
private String description;
private BigDecimal price;
private String imageUrl;
// 省略getter/setter
}
- Controller层 - ProductServlet.java:
java复制@WebServlet("/products")
public class ProductServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ProductService service = new ProductService();
List<Product> products = service.getAllProducts();
request.setAttribute("productList", products);
request.getRequestDispatcher("/jsp/product-list.jsp").forward(request, response);
}
}
- View层 - product-list.jsp:
jsp复制<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<div class="product-container">
<c:forEach items="${productList}" var="product">
<div class="product-card">
<img src="${product.imageUrl}" alt="${product.name}">
<h3>${product.name}</h3>
<p>${product.description}</p>
<span class="price">¥${product.price}</span>
<button onclick="addToCart(${product.id})">加入购物车</button>
</div>
</c:forEach>
</div>
3.2 购物车功能实现
购物车是电商系统的核心功能,这里采用Session存储方案:
java复制// CartServlet.java
@WebServlet("/cart")
public class CartServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
HttpSession session = request.getSession();
Map<Integer, CartItem> cart = (Map<Integer, CartItem>)session.getAttribute("cart");
if(cart == null) {
cart = new HashMap<>();
session.setAttribute("cart", cart);
}
int productId = Integer.parseInt(request.getParameter("productId"));
int quantity = Integer.parseInt(request.getParameter("quantity"));
// 省略业务逻辑...
}
}
对应的JSP页面中,可以通过EL表达式直接访问Session中的购物车数据:
jsp复制<c:if test="${not empty sessionScope.cart}">
<div class="cart-summary">
共 ${sessionScope.cart.size()} 件商品
</div>
</c:if>
4. 关键技术与优化方案
4.1 JSP页面性能优化
- 页面片段缓存:
jsp复制<%@ page buffer="16kb" autoFlush="false" %>
<%@ include file="/jsp/fragments/header.jsp" %>
- 静态资源版本控制:
jsp复制<link href="/static/css/main.css?v=${applicationScope.version}" rel="stylesheet">
- 避免脚本片段:尽量使用JSTL替代<% %>脚本片段,提升可维护性。
4.2 数据库连接池配置
在WEB-INF/context.xml中配置:
xml复制<Context>
<Resource name="jdbc/CakeDB"
auth="Container"
type="javax.sql.DataSource"
maxTotal="100"
maxIdle="30"
maxWaitMillis="10000"
username="root"
password="123456"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/cake_shop?useSSL=false"/>
</Context>
然后在代码中通过JNDI获取连接:
java复制Context initContext = new InitialContext();
Context envContext = (Context)initContext.lookup("java:/comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/CakeDB");
Connection conn = ds.getConnection();
4.3 文件上传功能
蛋糕商城需要上传产品图片,使用commons-fileupload实现:
java复制@WebServlet("/upload")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024 * 1, // 1MB
maxFileSize = 1024 * 1024 * 10, // 10MB
maxRequestSize = 1024 * 1024 * 15 // 15MB
)
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Part filePart = request.getPart("file");
String fileName = UUID.randomUUID() + "_" + filePart.getSubmittedFileName();
String uploadPath = getServletContext().getRealPath("/static/uploads");
Files.createDirectories(Paths.get(uploadPath));
filePart.write(uploadPath + File.separator + fileName);
// 返回JSON响应
response.setContentType("application/json");
PrintWriter out = response.getWriter();
out.print("{\"url\":\"/static/uploads/" + fileName + "\"}");
}
}
5. 常见问题与解决方案
5.1 JSP页面中文乱码
解决方案分三个层面:
- 页面编码设置:
jsp复制<%@ page contentType="text/html;charset=UTF-8" language="java" %>
- 请求编码过滤(在web.xml中配置):
xml复制<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
- 数据库连接字符串添加参数:
code复制jdbc:mysql://localhost:3306/db?useUnicode=true&characterEncoding=UTF-8
5.2 IDEA中JSP智能提示失效
-
确保安装了JSP支持插件:
- 打开File → Settings → Plugins
- 搜索"JSP"并确保"JSP Support"插件已启用
-
为JSP文件关联正确的Schema:
- 右键点击项目中的.jsp文件
- 选择"Open with" → "JSP Editor"
-
如果EL表达式不识别,检查项目SDK是否配置正确:
- File → Project Structure → Project SDK
- 选择1.8或更高版本的JDK
5.3 部署到Tomcat后资源404
典型症状是本地运行正常,但部署后CSS/JS/images加载失败。解决方案:
- 检查资源路径是否使用绝对路径:
jsp复制<!-- 错误写法 -->
<link href="static/css/main.css" rel="stylesheet">
<!-- 正确写法 -->
<link href="${pageContext.request.contextPath}/static/css/main.css" rel="stylesheet">
- 确保Tomcat的server.xml中Context配置正确:
xml复制<Context docBase="C:\projects\cake-shop\target\cake-shop"
path="/cake-shop"
reloadable="true"/>
- 清理浏览器缓存,有时是缓存导致的资源加载问题。
6. 项目扩展与进阶建议
6.1 加入分页功能
商品列表需要分页显示,建议采用服务端分页:
- DAO层添加分页查询:
java复制public List<Product> getProductsByPage(int pageNum, int pageSize) {
String sql = "SELECT * FROM products LIMIT ?, ?";
// 使用try-with-resources确保连接关闭
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, (pageNum - 1) * pageSize);
stmt.setInt(2, pageSize);
ResultSet rs = stmt.executeQuery();
// 结果集处理...
}
}
- JSP页面添加分页控件:
jsp复制<div class="pagination">
<c:if test="${currentPage > 1}">
<a href="?page=${currentPage - 1}">上一页</a>
</c:if>
<c:forEach begin="1" end="${totalPages}" var="i">
<c:choose>
<c:when test="${i == currentPage}">
<span class="current">${i}</span>
</c:when>
<c:otherwise>
<a href="?page=${i}">${i}</a>
</c:otherwise>
</c:choose>
</c:forEach>
<c:if test="${currentPage < totalPages}">
<a href="?page=${currentPage + 1}">下一页</a>
</c:if>
</div>
6.2 实现搜索功能
- 创建搜索Servlet:
java复制@WebServlet("/search")
public class SearchServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String keyword = request.getParameter("keyword");
List<Product> results = productService.searchProducts(keyword);
request.setAttribute("searchResults", results);
request.getRequestDispatcher("/jsp/search-results.jsp").forward(request, response);
}
}
- 前端搜索表单:
jsp复制<form action="${pageContext.request.contextPath}/search" method="get">
<input type="text" name="keyword" placeholder="搜索蛋糕...">
<button type="submit">搜索</button>
</form>
6.3 添加用户评价系统
- 数据库创建评价表:
sql复制CREATE TABLE reviews (
id INT AUTO_INCREMENT PRIMARY KEY,
product_id INT,
user_id INT,
rating TINYINT CHECK (rating BETWEEN 1 AND 5),
comment TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (product_id) REFERENCES products(id),
FOREIGN KEY (user_id) REFERENCES users(id)
);
- JSP页面显示评价:
jsp复制<div class="reviews">
<h3>用户评价</h3>
<c:forEach items="${product.reviews}" var="review">
<div class="review-item">
<div class="rating">
<c:forEach begin="1" end="5" var="i">
<span class="star ${i <= review.rating ? 'filled' : ''}">★</span>
</c:forEach>
</div>
<p>${review.comment}</p>
<span class="reviewer">${review.user.name}</span>
</div>
</c:forEach>
</div>
7. 项目部署与上线
7.1 打包WAR文件
在IDEA中打包项目:
- 打开Maven工具窗口(右侧边栏)
- 展开项目 → Lifecycle
- 双击package目标
- 生成的WAR文件位于target目录下
提示:可以在pom.xml中配置finalName避免版本号出现在WAR文件名中:
xml复制<build>
<finalName>cake-shop</finalName>
</build>
7.2 Tomcat部署
- 将WAR文件复制到Tomcat的webapps目录
- 启动Tomcat(bin/startup.bat或bin/startup.sh)
- 访问http://localhost:8080/cake-shop
7.3 生产环境建议
- 使用Nginx作为前端代理:
nginx复制server {
listen 80;
server_name cakeshop.example.com;
location / {
proxy_pass http://localhost:8080/cake-shop;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /static/ {
alias /path/to/cake-shop/static/;
expires 30d;
}
}
- 数据库连接池调优:
xml复制<Resource name="jdbc/CakeDB"
...
maxTotal="50"
maxIdle="10"
minIdle="5"
maxWaitMillis="5000"
testOnBorrow="true"
validationQuery="SELECT 1"/>
- 日志配置:使用Log4j2替代默认日志系统,配置每日滚动日志文件。
8. 项目优化与安全
8.1 性能优化措施
- JSP预编译:在构建阶段预编译JSP
xml复制<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<webResources>
<resource>
<directory>src/main/webapp</directory>
<includes>
<include>**/*.jsp</include>
</includes>
<targetPath>WEB-INF/classes</targetPath>
</resource>
</webResources>
</configuration>
</plugin>
- 静态资源CDN加速:
jsp复制<c:set var="cdnUrl" value="https://cdn.example.com"/>
<img src="${cdnUrl}/static/images/${product.imageUrl}">
- 数据库查询优化:
- 为常用查询字段添加索引
- 使用连接池监控工具(如Druid)分析慢查询
8.2 安全防护方案
- XSS防护:使用JSTL的c:out标签自动转义
jsp复制<p><c:out value="${userInput}"/></p>
- SQL注入防护:必须使用PreparedStatement
java复制String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1, username);
stmt.setString(2, password);
- CSRF防护:添加随机token
jsp复制<form action="/checkout" method="post">
<input type="hidden" name="csrfToken" value="${sessionScope.csrfToken}">
<!-- 其他表单字段 -->
</form>
- 密码加密:使用BCrypt
java复制import org.mindrot.jbcrypt.BCrypt;
String hashed = BCrypt.hashpw(rawPassword, BCrypt.gensalt());
boolean matched = BCrypt.checkpw(rawPassword, hashed);
9. 项目调试技巧
9.1 断点调试JSP
- 在JSP中的Java代码块设置断点:
- 打开.jsp文件
- 在左侧行号区域点击设置断点
- 以Debug模式启动Tomcat
- 查看作用域变量:
- 在Debug窗口的"Variables"面板
- 查看request、session、application等作用域中的属性
9.2 日志输出配置
- 在WEB-INF/classes下创建log4j2.xml:
xml复制<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
<File name="File" fileName="logs/cakeshop.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</File>
</Appenders>
<Loggers>
<Logger name="com.cakeshop" level="debug"/>
<Root level="info">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
- 在代码中使用日志:
java复制import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
private static final Logger logger = LogManager.getLogger(ProductServlet.class);
logger.debug("查询商品ID: {}", productId);
logger.error("数据库连接失败", e);
9.3 使用IDEA的HTTP Client测试API
- 在Tools → HTTP Client → Test RESTful Web Service
- 创建测试用例:
code复制POST http://localhost:8080/cake-shop/api/cart
Content-Type: application/json
{
"productId": 123,
"quantity": 2
}
- 可以保存为.http文件方便重复使用
10. 项目重构建议
10.1 引入前端框架
逐步引入Vue.js增强交互体验:
- 在JSP中引入Vue:
jsp复制<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<div id="app">
{{ message }}
</div>
<script>
new Vue({
el: '#app',
data: {
message: '欢迎来到蛋糕商城!'
}
});
</script>
- 使用axios处理AJAX请求:
jsp复制<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
axios.get('/api/products')
.then(response => {
this.products = response.data;
});
</script>
10.2 服务层抽象
将业务逻辑从Servlet移到Service层:
java复制public class ProductService {
private ProductDAO productDAO = new ProductDAO();
public List<Product> getFeaturedProducts() {
List<Product> allProducts = productDAO.findAll();
return allProducts.stream()
.filter(p -> p.isFeatured())
.sorted(comparing(Product::getSales).reversed())
.limit(6)
.collect(toList());
}
}
10.3 使用过滤器处理通用逻辑
创建编码过滤器和日志过滤器:
java复制@WebFilter("/*")
public class EncodingFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}
@WebFilter("/*")
public class LoggingFilter implements Filter {
private static final Logger logger = LogManager.getLogger(LoggingFilter.class);
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
long start = System.currentTimeMillis();
chain.doFilter(request, response);
long duration = System.currentTimeMillis() - start;
HttpServletRequest httpRequest = (HttpServletRequest) request;
logger.info("{} {} - {}ms",
httpRequest.getMethod(),
httpRequest.getRequestURI(),
duration);
}
}
在实际开发中,我发现JSP项目最容易出问题的地方往往是路径处理和字符编码。建议在项目初期就建立统一的路径处理工具类和编码过滤器,可以避免后期大量重复工作。另外,虽然现在流行前后端分离架构,但掌握传统的JSP开发模式对于理解Web开发底层原理非常有帮助。
