本文作為購物車實作的進階補充,著重於架構分層、例外處理、併發安全與擴展性設計,幫助開發者從「能跑通」邁向「可維護、可上線」。
cn.com.shopping
├── entity
│ └── Book.java # 實體類別
├── dao
│ └── BookDao.java # 資料存取介面
│ └── BookDaoImpl.java # 實作類(可切換 DB/記憶體)
├── service
│ └── CartService.java # 業務邏輯介面
│ └── CartServiceImpl.java # 購物車核心邏輯
├── web
│ ├── ListBookServlet.java
│ ├── AddToCartServlet.java
│ ├── ViewCartServlet.java
│ └── RemoveFromCartServlet.java
└── util
└── SessionUtil.java # Session 操作工具類
package cn.com.shopping.service;
import cn.com.shopping.entity.Book;
import java.util.List;
public interface CartService {
void addItem(HttpSession session, Book book);
List getCartItems(HttpSession session);
void removeItem(HttpSession session, String bookId);
void clearCart(HttpSession session);
int getItemCount(HttpSession session);
}
package cn.com.shopping.service;
import cn.com.shopping.dao.BookDao;
import cn.com.shopping.entity.Book;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpSession;
public class CartServiceImpl implements CartService {
private static final String CART_KEY = "shopping_cart";
private BookDao bookDao = new BookDaoImpl();
@Override
public void addItem(HttpSession session, Book book) {
@SuppressWarnings("unchecked")
List cart = (List) session.getAttribute(CART_KEY);
if (cart == null) {
cart = new ArrayList<>();
session.setAttribute(CART_KEY, cart);
}
// 併發安全:同步區塊鎖定 Session
synchronized (session) {
cart.add(book);
}
}
@Override
public List getCartItems(HttpSession session) {
@SuppressWarnings("unchecked")
List cart = (List) session.getAttribute(CART_KEY);
return cart != null ? new ArrayList<>(cart) : new ArrayList<>(); // 回傳防禦性副本
}
@Override
public void removeItem(HttpSession session, String bookId) {
@SuppressWarnings("unchecked")
List cart = (List) session.getAttribute(CART_KEY);
if (cart != null) {
synchronized (session) {
cart.removeIf(book -> book.getId().equals(bookId));
}
}
}
@Override
public void clearCart(HttpSession session) {
session.removeAttribute(CART_KEY);
}
@Override
public int getItemCount(HttpSession session) {
@SuppressWarnings("unchecked")
List cart = (List) session.getAttribute(CART_KEY);
return cart != null ? cart.size() : 0;
}
}
package cn.com.shopping.web;
import cn.com.shopping.entity.Book;
import cn.com.shopping.service.CartService;
import cn.com.shopping.service.CartServiceImpl;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
@WebServlet("/cart/add")
public class AddToCartServlet extends HttpServlet {
private CartService cartService = new CartServiceImpl();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String bookId = req.getParameter("bookId");
if (bookId == null || bookId.isEmpty()) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少商品 ID");
return;
}
Book book = new BookDaoImpl().findById(bookId);
if (book == null) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "商品不存在");
return;
}
HttpSession session = req.getSession(true);
cartService.addItem(session, book);
// AJAX 請求回傳 JSON,傳統請求重導向
String accept = req.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
resp.setContentType("application/json;charset=UTF-8");
resp.getWriter().write("{\"success\":true,\"count\":" + cartService.getItemCount(session) + "}");
} else {
resp.sendRedirect(req.getContextPath() + "/cart/view");
}
}
}
Session 物件非執行緒安全,多個 AJAX 請求並發操作購物車時,必須加鎖:
synchronized (session) {
cart.add(book);
}
服務層回傳購物車列表時,回傳副本而非原始引用,防止控制層意外修改:
return cart != null ? new ArrayList<>(cart) : new ArrayList<>();
定義常數避免硬編碼字串散落:
private static final String CART_KEY = "shopping_cart";
400 Bad Request404 Not Found500 Internal Server Error + 記錄日誌透過 Accept 標頭判斷回傳格式,前後端分離與傳統 MVC 兼容。
@WebServlet 註解替代 web.xml,簡化配置