本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于Java Swing与MySQL开发的完整购物系统,涵盖图形界面设计、数据库管理及前后端交互,提供源代码、详细文档和视频教程,是掌握Java GUI编程与数据库应用集成的优质学习资源。系统支持商品浏览、购物车管理、订单处理等核心功能,采用MVC架构模式,结合JDBC实现数据持久化,并融入异常处理、文件操作、多线程及常见设计模式等关键技术,助力开发者全面提升Java桌面应用开发能力。

购物系统开发全栈实战:从Swing界面到数据库安全与并发控制

你有没有遇到过这样的场景?一个用户刚买完最后一件商品,刷新页面却发现还能下单——库存变成负数了。😅 这不是bug,这是典型的“超卖”问题,而它背后藏着的是整个购物系统最核心的挑战: 如何在高并发下保证数据一致性、用户体验流畅且系统安全可靠

今天我们要一起打造的,就是一个基于 Java Swing + MySQL 的桌面端购物系统。别看它是“老派”的桌面应用,但它麻雀虽小五脏俱全:GUI交互、MVC架构、DAO模式、事务管理、SQL注入防护、多线程同步……这些可都是现代企业级开发的核心技能点!🚀


构建稳固基石:Java Swing 与 用户界面设计

说到 Java 桌面开发,很多人第一反应是:“这不是早就被淘汰了吗?”但事实恰恰相反,在很多内部管理系统、POS终端、工业控制软件中,Swing 依然扮演着不可替代的角色。为什么?

因为它轻量、跨平台、无需浏览器支持,启动快,而且完全由 Java 控制——没有外部依赖,部署简单,维护成本低。🎯

我们这个购物系统的前端就用 Swing 来实现。虽然它不像 Web 那样炫酷,但我们照样可以做出清晰、响应灵敏的界面。

窗口骨架:JFrame 是你的主舞台

一切 GUI 的起点,都是 JFrame 。你可以把它想象成网页里的 <body> 标签——所有组件都得往里面塞。

public class MainFrame extends JFrame {
    public MainFrame() {
        setTitle("🛒 购物系统");
        setSize(800, 600);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null); // 居中显示
        JLabel label = new JLabel("欢迎使用购物系统", SwingConstants.CENTER);
        add(label);
        setVisible(true);
    }
}

注意那个 SwingUtilities.invokeLater() 吗?这可不是多余的代码。Swing 是单线程的,所有的 UI 更新必须在 事件调度线程(EDT) 中执行。如果我们在主线程直接创建窗口,可能会导致界面卡顿甚至崩溃。

所以标准写法永远是:

SwingUtilities.invokeLater(() -> new MainFrame());

✅ 小贴士:记住一句话——“不要阻塞 EDT”。任何耗时操作(比如查数据库),都不能放在 UI 线程里干!

组件拼装:JPanel + JButton = 可复用的UI模块

光有窗口不够,还得有按钮、输入框、表格……这些就是 Swing 的“积木块”。

组件 功能 使用建议
JFrame 主窗口容器 设置标题、大小、关闭行为
JPanel 容器面板 分组控件,嵌套布局
JButton 按钮 注册 ActionListener 响应点击
JLabel 文本标签 显示提示信息或动态内容
JTextField / JPasswordField 输入框 获取用户输入,密码框自动掩码

举个例子,做个登录区域:

JPanel topPanel = new JPanel();
topPanel.add(new JButton("登录"));
topPanel.add(new JButton("注册"));
getContentPane().add(topPanel, BorderLayout.NORTH);

这里用了 BorderLayout ,把顶部工具栏固定在上方。是不是有点像 CSS 的 position: fixed ?😄

布局策略:选对“排版方式”事半功倍

Swing 提供了好几种布局管理器,每种都有它的适用场景:

✅ BorderLayout —— 主窗口结构首选

适合划分五大区域:北、南、东、西、中。中间区域会自动填充剩余空间,非常适合做主视图。

setLayout(new BorderLayout());
add(toolbar, BorderLayout.NORTH);
add(tableScrollPane, BorderLayout.CENTER);
add(statusBar, BorderLayout.SOUTH);
✅ GridLayout —— 规则排列好帮手

比如做一个数字键盘或者商品分类网格,用 GridLayout(3, 3) 就能轻松搞定 3x3 的按钮阵列。

JPanel grid = new JPanel(new GridLayout(3, 3, 5, 5)); // 行数、列数、间距
for (int i = 1; i <= 9; i++) {
    grid.add(new JButton(String.valueOf(i)));
}
✅ BoxLayout —— 灵活堆叠神器

支持水平或垂直排列,特别适合需要动态增减组件的列表。

boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS));
boxPanel.add(new JLabel("商品1"));
boxPanel.add(Box.createVerticalStrut(10)); // 插入空白间隔
boxPanel.add(new JLabel("商品2"));

想知道怎么选?来看这张决策图👇:

graph TD
    A[选择布局策略] --> B{是否为主窗口?}
    B -->|是| C[使用BorderLayout]
    B -->|否| D{组件是否规则排列?}
    D -->|是| E[GridLayout]
    D -->|否| F{是否线性排列?}
    F -->|是| G[BoxLayout]
    F -->|否| H[GridBagLayout]

看到没?大部分情况都不需要用到复杂的 GridBagLayout ,那玩意儿调试起来简直是噩梦 😵‍💫。

自定义面板:封装才是专业之道

如果你发现到处都在重复写“标签+输入框”的组合,说明该封装了!

public class LabeledInputPanel extends JPanel {
    private JTextField textField;

    public LabeledInputPanel(String labelText) {
        setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
        JLabel label = new JLabel(labelText, SwingConstants.RIGHT);
        label.setPreferredSize(new Dimension(80, 25));
        textField = new JTextField(15);

        add(label);
        add(Box.createHorizontalStrut(10));
        add(textField);
    }

    public String getText() {
        return textField.getText();
    }

    public void addActionListener(ActionListener listener) {
        textField.addActionListener(listener);
    }
}

这样一来,创建用户名输入框只需要一行:

LabeledInputPanel usernameField = new LabeledInputPanel("用户名:");

干净利落,代码复用率飙升 💪!


数据大脑:MySQL 设计与 JDBC 持久化层构建

没有数据支撑的应用就像没有地基的房子,风一吹就倒。我们的购物系统,核心围绕三个实体展开: 用户 → 商品 → 订单

ER模型设计:让关系一目了然

先画张ER图理清思路:

erDiagram
    USER ||--o{ ORDER : places
    ORDER ||--o{ ORDER_ITEM : contains
    PRODUCT ||--o{ ORDER_ITEM : included_in
    USER {
        bigint id PK
        varchar(50) username
        varchar(255) password
        varchar(100) email
        enum role
        datetime created_at
    }
    PRODUCT {
        bigint id PK
        varchar(100) name
        decimal(10,2) price
        int stock
        tinyint status
        datetime created_at
    }
    ORDER {
        bigint id PK
        bigint user_id FK
        decimal(10,2) total_amount
        varchar(20) order_status
        datetime created_at
    }
    ORDER_ITEM {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        decimal(10,2) unit_price
    }

四个表,形成典型的星型结构:
- user order 是一对多
- order order_item 是一对多
- product 被多个订单引用

重点来了:为什么要在 order_item 里保存 unit_price ?难道不能直接关联 product.price 吗?

当然能,但不推荐 ❌。因为价格会变!今天卖 99.9 元的商品,明天可能涨价到 199.9。如果不快照当时的价格,历史订单金额就不准确了。

这就是所谓的“反范式优化”——为了业务准确性,适当牺牲规范化原则。🧠

索引优化:查询提速的秘密武器

你有没有经历过等了十几秒才加载出商品列表?很可能是因为缺少索引。

给高频查询字段加索引,效果立竿见影:

查询场景 推荐索引 类型
按用户名登录 user(username) 唯一索引
查某人所有订单 order(user_id) 普通索引
最近订单分页 order(created_at DESC) 普通索引
商品模糊搜索 product(name) 全文索引(FULLTEXT)

不过切记:索引不是越多越好!每次插入/更新都要维护B+树,反而拖慢写入速度。所以要权衡读写比例和字段选择性。

📊 经验法则:选择性 > 10% 的字段才考虑建索引。

连接池:别再用 DriverManager 了!

还在这样写连接代码?

Connection conn = DriverManager.getConnection(url, user, pass);

醒醒吧朋友!生产环境这么干迟早出事 ⚠️。

每次调用都会新建 TCP 连接,经历握手、认证等一系列开销,性能极差。更可怕的是,没人回收的话,数据库连接很快就会被耗尽。

正解是使用 连接池 ,比如 HikariCP(目前最快的 JDBC 连接池):

HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/shopdb");
config.setUsername("root");
config.setPassword("123456");
config.setMaximumPoolSize(20);
config.setConnectionTimeout(30_000);

HikariDataSource dataSource = new HikariDataSource(config);

然后通过单例封装:

public class DBConnectionPool {
    private static volatile HikariDataSource dataSource;

    public static HikariDataSource getInstance() {
        if (dataSource == null) {
            synchronized (DBConnectionPool.class) {
                if (dataSource == null) {
                    dataSource = new HikariDataSource(createConfig());
                }
            }
        }
        return dataSource;
    }
}

以后获取连接只需一行:

try (Connection conn = DBConnectionPool.getInstance().getConnection()) {
    // 执行SQL
}

简洁又高效 ✅。

资源释放:try-with-resources 救我狗命

以前写 JDBC 经常忘记关 ResultSet Statement ,导致连接泄漏,程序跑几天就挂了……

现在有了 try-with-resources ,一切都变得优雅了:

public List<Product> searchByName(String keyword) throws SQLException {
    String sql = "SELECT * FROM product WHERE name LIKE ?";
    try (Connection conn = dataSource.getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {

        pstmt.setString(1, "%" + keyword + "%");

        try (ResultSet rs = pstmt.executeQuery()) {
            List<Product> list = new ArrayList<>();
            while (rs.next()) {
                Product p = new Product();
                p.setId(rs.getLong("id"));
                p.setName(rs.getString("name"));
                p.setPrice(rs.getBigDecimal("price"));
                list.add(p);
            }
            return list;
        }
    }
}

只要实现了 AutoCloseable 接口,都会在 try 块结束时自动关闭,哪怕抛异常也不怕!


架构灵魂:MVC 分层与设计模式实战

如果说数据是大脑,那架构就是神经系统。我们采用经典的 MVC 模式 来组织代码:

  • Model :数据实体 + DAO
  • View :Swing 界面
  • Controller :事件协调者

实体类:POJO 规范不能少

每个表对应一个 Java Bean,遵循 JavaBean 规范:

public class Product {
    private Long id;
    private String name;
    private BigDecimal price;
    private Integer stock;

    // 无参构造函数
    public Product() {}

    // getter/setter...
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    @Override
    public String toString() {
        return "Product{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", price=" + price +
                ", stock=" + stock +
                '}';
    }
}

IDEA 可以一键生成 getter/setter,记得加上 toString() 方便调试输出。

DAO 模式:接口抽象隔离变化

我们不直接操作 JDBC,而是通过 DAO 接口屏蔽细节:

public interface ProductDao {
    List<Product> findAll() throws SQLException;
    Product findById(Long id) throws SQLException;
    void insert(Product product) throws SQLException;
    void update(Product product) throws SQLException;
    void delete(Long id) throws SQLException;
}

实现类负责具体 SQL:

public class ProductDaoImpl implements ProductDao {
    private final Connection conn;

    public ProductDaoImpl(Connection conn) {
        this.conn = conn;
    }

    @Override
    public List<Product> findAll() throws SQLException {
        String sql = "SELECT id, name, price, stock FROM product";
        try (PreparedStatement stmt = conn.prepareStatement(sql);
             ResultSet rs = stmt.executeQuery()) {

            List<Product> products = new ArrayList<>();
            while (rs.next()) {
                Product p = new Product();
                p.setId(rs.getLong("id"));
                p.setName(rs.getString("name"));
                p.setPrice(rs.getBigDecimal("price"));
                p.setStock(rs.getInt("stock"));
                products.add(p);
            }
            return products;
        }
    }
}

将来想换数据库?只要换个实现类就行,上层逻辑完全不用动!

工厂模式:统一创建入口

随着 DAO 越来越多,总不能到处 new 实例吧?来个工厂统一分发:

public class DaoFactory {
    private static DaoFactory instance;
    private final Connection conn;

    private DaoFactory(Connection conn) {
        this.conn = conn;
    }

    public static DaoFactory getInstance(Connection conn) {
        if (instance == null) {
            synchronized (DaoFactory.class) {
                if (instance == null) {
                    instance = new DaoFactory(conn);
                }
            }
        }
        return instance;
    }

    public ProductDao getProductDao() {
        return new ProductDaoImpl(conn);
    }

    public UserDao getUserDao() {
        return new UserDaoImpl(conn);
    }
}

调用方只需要:

ProductDao dao = DaoFactory.getInstance(conn).getProductDao();
List<Product> products = dao.findAll();

干净清爽,耦合度降到最低 💧。

观察者模式:实时刷新库存显示

当多个用户同时抢购同一件商品时,你怎么确保别人看到的是最新库存?

答案是: 观察者模式

// 主题
public interface Subject {
    void registerObserver(Observer o);
    void removeObserver(Observer o);
    void notifyObservers();
}

// 观察者
public interface Observer {
    void update();
}

// 库存服务(单例)
public class InventoryService implements Subject {
    private static InventoryService instance;
    private final List<Observer> observers = new ArrayList<>();

    public static synchronized InventoryService getInstance() {
        if (instance == null) {
            instance = new InventoryService();
        }
        return instance;
    }

    public void reduceStock(Long productId, int quantity) {
        // 扣减库存...
        notifyObservers(); // 通知所有人刷新
    }

    @Override
    public void notifyObservers() {
        for (Observer o : observers) {
            o.update();
        }
    }
}

商品列表面板注册监听:

public class ProductPanel extends JPanel implements Observer {
    @Override
    public void update() {
        SwingUtilities.invokeLater(() -> loadProducts());
    }

    private void loadProducts() {
        try {
            ProductDao dao = DaoFactory.getInstance(DBUtil.getConnection()).getProductDao();
            setProducts(dao.findAll());
        } catch (SQLException e) {
            JOptionPane.showMessageDialog(this, "加载失败:" + e.getMessage());
        }
    }
}

这样一来,只要有人下单,所有打开的商品页都会自动刷新,真正做到“一处变更,处处响应”✨。


安全防线:防注入、防超卖、防崩溃

功能做得再漂亮,安全性不过关也是白搭。下面我们聊聊几个关键防御手段。

SQL注入:PreparedStatement 是唯一出路

看看这个危险的操作:

String sql = "SELECT * FROM user WHERE username = '" + username + 
             "' AND password = '" + password + "'";

攻击者只要输入 ' OR '1'='1 ,就能绕过登录验证!

解决方案只有一个: 预编译语句 + 参数占位符

String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
pstmt.setString(1, username);
pstmt.setString(2, hashedPassword);

数据库会对参数做转义处理,彻底杜绝注入风险。🔒

登录保护:防止暴力破解

连续输错5次就锁账户,怎么做?

private static final Map<String, Integer> attempts = new ConcurrentHashMap<>();
private static final int MAX_ATTEMPTS = 5;

public boolean isLocked(String username) {
    return attempts.getOrDefault(username, 0) >= MAX_ATTEMPTS;
}

public void incrementAttempt(String username) {
    attempts.merge(username, 1, Integer::sum);
}

内存级计数简单有效。生产环境建议持久化到数据库并加入定时解锁机制。

并发超卖:synchronized + 数据库约束双保险

两个用户同时下单,库存只有一件,怎么办?

public synchronized boolean placeOrder(Order order) {
    Product product = productDao.findById(order.getProductId());
    if (product.getStock() < order.getQuantity()) {
        return false;
    }

    // 扣库存 + 写订单
    productDao.reduceStock(product.getId(), order.getQuantity());
    orderDao.insert(order);

    return true;
}

synchronized 保证同一时间只有一个线程进入方法。再加上数据库层面的 CHECK(stock >= 0) 约束,双重保险更安心。


流程闭环:完整业务链路集成测试

最后我们走一遍完整的购物流程:

flowchart TD
    A[用户点击“搜索”按钮] --> B{ActionListener 触发}
    B --> C[Controller: SearchProductAction]
    C --> D[调用 ProductDao.searchByName()]
    D --> E[访问 MySQL 数据库]
    E --> F[返回 ResultSet]
    F --> G[映射为 List<Product>]
    G --> H[调用 view.setProducts()]
    H --> I[JTable 更新显示]
    I --> J[用户看到结果]

每一环都可以打日志、设断点,逐步验证逻辑是否正确。

遇到界面卡顿?很可能是数据库查询在 EDT 线程执行了!赶紧换成 SwingWorker 异步加载:

new SwingWorker<List<Product>, Void>() {
    @Override
    protected List<Product> doInBackground() throws Exception {
        return productDao.searchByName(keyword);
    }

    @Override
    protected void done() {
        try {
            List<Product> results = get();
            panel.setProducts(results);
        } catch (Exception e) {
            JOptionPane.showMessageDialog(panel, "搜索失败:" + e.getMessage());
        }
    }
}.execute();

既不影响界面流畅,又能拿到结果后自动刷新。


结语:技术的价值在于解决问题

这个看似简单的购物系统,其实浓缩了大量工程实践智慧:

  • MVC 解耦关注点
  • DAO + 工厂 提升可维护性
  • 连接池 + try-with-resources 保障资源安全
  • PreparedStatement 防止注入
  • 观察者模式 实现状态同步
  • synchronized + 事务 应对并发

它们不是孤立存在的技巧,而是构成健壮系统的有机整体。

当你下次接到一个新项目时,不妨问问自己:
- 我的数据结构合理吗?
- 我的异常处理完善吗?
- 我的代码容易测试和扩展吗?

记住: 真正的高手,不在炫技,而在稳扎稳打地把每一个细节做到位 。🛠️

现在,轮到你动手了——试着把这个系统加上图片展示、支付模拟、订单导出等功能,让它真正“活”起来吧!💻🔥

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目是一个基于Java Swing与MySQL开发的完整购物系统,涵盖图形界面设计、数据库管理及前后端交互,提供源代码、详细文档和视频教程,是掌握Java GUI编程与数据库应用集成的优质学习资源。系统支持商品浏览、购物车管理、订单处理等核心功能,采用MVC架构模式,结合JDBC实现数据持久化,并融入异常处理、文件操作、多线程及常见设计模式等关键技术,助力开发者全面提升Java桌面应用开发能力。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐