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

简介:本资源基于Java Swing与JDBC技术,实现了一个完整的图形用户界面后台管理系统,涵盖管理员登录、数据增删改查等功能。系统通过Swing构建可视化界面,使用JDBC连接数据库,支持安全的用户认证与数据操作。附带数据库文件,适合用于学习Java桌面应用开发中GUI设计与数据库交互的核心技术,是提升Java SE综合编程能力的实用项目。

Java Swing与JDBC后台管理系统开发全解析

在当今微服务与Web前端主导的技术生态中,桌面应用似乎已逐渐淡出主流视野。但当你走进一家连锁超市的收银台、医院的挂号系统,或是工厂的设备监控终端时,那些稳定运行着的管理软件,许多依然由Java Swing驱动。这背后并非技术守旧,而是一种深思熟虑后的选择: 跨平台兼容性 + 离线可用性 + 高度定制化界面 = 不可替代的生产力工具

今天我们要构建的,不是一个简单的“Hello World”窗口程序,而是一个具备完整MVC架构、支持数据库交互、拥有现代化视觉反馈机制的后台管理系统模拟器。它将融合Swing的传统优势与现代工程实践——从事件调度线程(EDT)的安全操作,到PreparedStatement防注入攻击;从CardLayout实现的页面路由切换,到基于bcrypt的密码加密存储。整个过程就像搭积木一样,把一个个看似孤立的技术点,拼成一个有机整体。

准备好了吗?让我们从最基础却又最关键的一步开始:如何正确地“打开”一个Java窗口?

如何安全地启动你的第一个Swing窗口

你有没有遇到过这种情况:写了一个漂亮的登录界面,双击运行后闪退,或者点击按钮毫无反应?问题很可能就出在 线程模型 上。Swing不是普通的类库,它是建立在AWT之上的GUI框架,所有UI组件都必须在一个特殊的线程中创建和更新——这就是传说中的 事件调度线程 (Event Dispatch Thread, EDT)。

来看一段典型的错误示范:

public class LoginFrame extends JFrame {
    public LoginFrame() {
        setTitle("登录");
        setSize(400, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true); // ⚠️ 危险!
    }

    public static void main(String[] args) {
        new LoginFrame(); // ❌ 错误方式
    }
}

这段代码虽然能跑起来,但它违反了Swing的核心原则: 非线程安全 。当多个线程同时修改UI状态时(比如后台加载数据的同时用户点击按钮),轻则界面卡顿,重则直接崩溃。

正确的做法是使用 SwingUtilities.invokeLater() 包装初始化逻辑:

public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> {
        try {
            // 可选:设置系统原生外观
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeel());
        } catch (Exception e) {
            e.printStackTrace();
        }
        new LoginFrame().setVisible(true);
    });
}

这样做的好处是什么?我们来看一下底层发生了什么:

graph TD
    A[main线程] --> B{调用invokeLater}
    B --> C[将Runnable任务提交至EDT队列]
    C --> D[EDT线程取出任务]
    D --> E[执行new LoginFrame()]
    E --> F[创建JFrame并绘制组件]
    F --> G[进入事件监听循环]

这个流程确保了所有的UI操作都在同一个线程上下文中完成,避免了并发访问冲突。你可以把它想象成电影院的检票口——不管有多少观众(线程)想进场,最终都只能通过唯一的通道(EDT)依次入场。

💡 小贴士 :如果你希望应用看起来更“本地化”,可以尝试不同的外观风格:

```java
// 使用系统默认风格(推荐)
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeel());

// 或者试试Java自带的Nimbus风格
for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
if (“Nimbus”.equals(info.getName())) {
UIManager.setLookAndFeel(info.getClassName());
break;
}
}
```

现在我们的窗口终于可以安全打开了。接下来的问题是:怎么让它长得不像90年代的老古董?

让Swing界面告别“复古风”的布局艺术

很多人对Swing的第一印象就是“丑”。但这其实是个误解。真正决定一个界面颜值的,从来都不是组件本身,而是 布局策略 。就像同样的食材,不同的厨师做出来味道天差地别。

BorderLayout vs FlowLayout:两种截然不同的哲学

Swing提供了多种布局管理器,每一种都有其适用场景。最常见的两种是 BorderLayout FlowLayout

  • BorderLayout 像是一个十字路口,把容器划分为五个区域:北、南、东、西、中心。
  • FlowLayout 则像是一行文字,按添加顺序水平排列,满了就换行。

假设我们要做一个简单的工具栏:

JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 5));
toolbar.add(new JButton("保存"));
toolbar.add(new JButton("取消"));
toolbar.add(new JButton("刷新"));

这里的参数含义如下:
- FlowLayout.CENTER :居中对齐
- 10 :水平间距10像素
- 5 :垂直间距5像素

效果就像是网页里的 .btn-group ,简洁明了。

但如果要做主窗口结构呢?这时候就得请出 BorderLayout

setLayout(new BorderLayout());

JPanel topPanel = new JPanel();
topPanel.add(new JLabel("欢迎使用后台管理系统"));

JPanel centerPanel = new JPanel(new GridLayout(2,2));
centerPanel.add(new JLabel("用户名:"));
centerPanel.add(new JTextField(15));
centerPanel.add(new JLabel("密码:"));
centerPanel.add(new JPasswordField(15));

JPanel bottomPanel = new JPanel();
bottomPanel.add(new JButton("登录"));

add(topPanel, BorderLayout.NORTH);
add(centerPanel, BorderLayout.CENTER);
add(bottomPanel, BorderLayout.SOUTH);

你会发现,中间的表单区域会自动占据剩余空间,无论你怎么拉伸窗口,布局始终保持协调。这就是所谓“响应式设计”的雏形!

不过,现实中的需求往往比这复杂得多。比如要实现一个跨越多列的提交按钮,该怎么办?

GridBagLayout:Swing布局界的“瑞士军刀”

如果说前面两位是普通士兵,那 GridBagLayout 就是特种兵——功能强大,但学习曲线陡峭。它的核心在于 GridBagConstraints 对象,可以通过精细控制每个组件的位置、大小、对齐方式来实现任意复杂度的布局。

举个例子,我们要做一个注册表单,其中“同意协议”复选框要横跨两列:

GridBagLayout gbl = new GridBagLayout();
setLayoutManager(gbl);

GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5); // 外边距
gbc.fill = GridBagConstraints.HORIZONTAL; // 水平填充

// 第一行:用户名
gbc.gridx = 0; gbc.gridy = 0;
add(new JLabel("用户名:"), gbc);
gbc.gridx = 1;
add(new JTextField(20), gbc);

// 第二行:密码
gbc.gridx = 0; gbc.gridy = 1;
add(new JLabel("密码:"), gbc);
gbc.gridx = 1;
add(new JPasswordField(20), gbc);

// 第三行:同意协议(跨两列)
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2;
gbc.anchor = GridBagConstraints.CENTER; // 居中对齐
add(new JCheckBox("我已阅读并同意用户协议"), gbc);

// 第四行:注册按钮
gbc.gridy = 3;
gbc.ipady = 10; // 内部padding
add(new JButton("立即注册"), gbc);
参数 作用
gridx/gridy 网格坐标(从0开始)
gridwidth/gridheight 跨越多少列/行
fill 填充方向(HORIZONTAL/VERTICAL/BOTH)
weightx/weighty 权重,决定多余空间分配比例
anchor 组件在格子内的对齐方式

虽然写起来有点啰嗦,但一旦掌握,几乎能应对任何复杂的界面需求。而且你会发现,这种“声明式”布局比绝对定位更易于维护——改尺寸不用重算坐标,加控件也不会打乱全局。

自定义布局:打造专属UI风格

有时候标准布局也无法满足需求。比如你想做一个标签云(tag cloud),让标签自动折行排列。这时就需要自定义 LayoutManager

public class TagFlowLayout implements LayoutManager {
    private int hgap = 5, vgap = 3;

    @Override
    public void layoutContainer(Container parent) {
        int x = hgap, y = vgap, maxHeight = 0;
        int width = parent.getWidth();

        for (Component comp : parent.getComponents()) {
            Dimension d = comp.getPreferredSize();
            if (x + d.width > width - hgap) { // 需要换行
                x = hgap;
                y += maxHeight + vgap;
                maxHeight = 0;
            }
            comp.setBounds(x, y, d.width, d.height);
            x += d.width + hgap;
            maxHeight = Math.max(maxHeight, d.height);
        }
    }

    @Override
    public Dimension minimumLayoutSize(Container parent) {
        return preferredLayoutSize(parent);
    }

    @Override
    public Dimension preferredLayoutSize(Container parent) {
        synchronized (parent.getTreeLock()) {
            int width = 0, height = 0, rowWidth = 0, rowHeight = 0;
            for (Component comp : parent.getComponents()) {
                Dimension d = comp.getPreferredSize();
                if (rowWidth + d.width > parent.getWidth()) {
                    width = Math.max(width, rowWidth);
                    height += rowHeight + vgap;
                    rowWidth = d.width + hgap;
                    rowHeight = d.height;
                } else {
                    rowWidth += d.width + hgap;
                    rowHeight = Math.max(rowHeight, d.height);
                }
            }
            width = Math.max(width, rowWidth);
            height += rowHeight;
            return new Dimension(width, height);
        }
    }

    // 其他方法省略...
}

然后就可以像使用普通布局一样:

JPanel tagPanel = new JPanel();
tagPanel.setLayout(new TagFlowLayout());
tagPanel.add(new JLabel("#Java"));
tagPanel.add(new JLabel("#Swing"));
tagPanel.add(new JLabel("#JDBC"));
// ...更多标签

这种灵活性正是Swing历经二十多年仍被使用的底气所在。

组件级优化:让每一个按钮都会“呼吸”

有了合理的布局,下一步就是提升细节质感。一个好的GUI,应该能让用户在操作时感受到微妙的反馈,就像机械键盘的段落感一样令人愉悦。

输入框的安全处理:不只是美观

考虑这样一个场景:用户输入密码后,程序需要验证。大多数人会这么写:

String password = passwordField.getText(); // ❌ 危险!

问题在哪?Java中的 String 是不可变对象,一旦创建就在内存中留下副本,直到垃圾回收。这意味着即使用户注销了,他们的密码可能还残留在内存快照里,存在被反序列化提取的风险。

正确姿势是使用 getPassword() 返回字符数组,并及时清除:

char[] pwdChars = passwordField.getPassword();
try {
    String password = new String(pwdChars);
    // 执行认证逻辑...
} finally {
    Arrays.fill(pwdChars, '0'); // 主动清零
}

这个小小的改动,就能大幅提升安全性。类似的技巧还有限制输入长度、过滤非法字符等:

((PlainDocument) usernameField.getDocument()).setDocumentFilter(new DocumentFilter() {
    @Override
    public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
            throws BadLocationException {
        if (fb.getDocument().getLength() + string.length() <= 15) {
            super.insertString(fb, offset, string.toLowerCase(), attr);
        }
    }
});

上面这段代码不仅限制了用户名最多15个字符,还强制转为小写,一举两得。

动态提示与状态反馈:让用户始终知情

没有反馈的界面是最折磨人的。想想看,你点了“提交”按钮,结果什么反应都没有——你是该再点一次,还是以为系统坏了?

我们可以通过 JLabel 实现实时状态提示:

JLabel statusLabel = new JLabel(" ");
statusLabel.setForeground(Color.RED);

// 登录失败
statusLabel.setText("❌ 用户名或密码错误!");
statusLabel.setForeground(Color.RED);

// 登录成功
statusLabel.setText("✅ 登录成功,跳转中...");
statusLabel.setForeground(Color.GREEN);

更高级的做法是支持HTML格式,实现富文本提示:

statusLabel.setText("<html><b><font color='red'>警告:</font></b>账户已被锁定</html>");

配合CSS-like样式管理(如Nimbus LookAndFeel),甚至可以做出渐变色、阴影等效果。

按钮的微交互设计:从“死板”到“灵动”

传统Swing按钮总给人一种呆板的感觉。但我们可以通过监听鼠标事件让它“活”起来:

JButton hoverBtn = new JButton("悬停试试");
hoverBtn.addMouseListener(new MouseAdapter() {
    @Override
    public void mouseEntered(MouseEvent e) {
        hoverBtn.setBackground(new Color(0x2196F3)); // 蓝色高亮
        hoverBtn.setForeground(Color.WHITE);
        hoverBtn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
    }

    @Override
    public void mouseExited(MouseEvent e) {
        hoverBtn.setBackground(UIManager.getColor("Button.background"));
        hoverBtn.setForeground(UIManager.getColor("Button.foreground"));
    }
});

同样的思路也可以用于焦点高亮:

usernameField.addFocusListener(new FocusAdapter() {
    @Override
    public void focusGained(FocusEvent e) {
        ((JComponent)e.getSource()).setBorder(BorderFactory.createLineBorder(Color.BLUE, 2));
    }

    @Override
    public void focusLost(FocusEvent e) {
        ((JComponent)e.getSource()).setBorder(BorderFactory.createEtchedBorder());
    }
});

这些看似微不足道的细节,累积起来就是完全不同的用户体验。它们传达的信息是:“我在听你说话”。

事件驱动编程:让界面真正“动”起来

到目前为止,我们的界面还只是静态的“雕塑”。要让它变成“生命体”,就必须引入事件驱动机制——这是GUI编程的灵魂所在。

事件模型的本质:监听-响应模式

Swing的事件处理基于经典的观察者模式。简单来说,就是“谁感兴趣,谁就注册监听”。

以按钮点击为例:

JButton loginBtn = new JButton("登录");
loginBtn.addActionListener(e -> {
    System.out.println("按钮被点击了!");
});

背后的机制可以用一张图表示:

graph TD
    A[用户点击] --> B(JButton)
    B --> C(ActionEvent)
    C --> D{是否有监听器?}
    D -->|是| E[调用ActionListener.actionPerformed]
    D -->|否| F[忽略]
    E --> G[执行业务逻辑]

这套机制的优势在于解耦。同一个按钮可以绑定多个监听器,不同类型的组件也可以共享同一套处理逻辑。比如菜单项和工具栏按钮执行相同功能:

AbstractAction loginAction = new AbstractAction("登录") {
    @Override
    public void actionPerformed(ActionEvent e) {
        performLogin();
    }
};

JMenuItem menuLogin = new JMenuItem(loginAction);
JButton toolbarLogin = new JButton(loginAction);

一行代码搞定两个地方的功能同步,再也不用担心漏改某处逻辑。

组合拳:KeyEvent + FocusEvent 实现智能校验

光有点击事件还不够。真正的专业级表单,应该能在用户输入过程中就发现问题。

实时输入过滤

防止用户输入非法字符:

usernameField.addKeyListener(new KeyAdapter() {
    @Override
    public void keyTyped(KeyEvent e) {
        char c = e.getKeyChar();
        if (!Character.isLetterOrDigit(c) && c != '_') {
            e.consume(); // 阻止输入
            Toolkit.getDefaultToolkit().beep(); // 发出提示音
        }
    }
});
失焦时完整性检查

当用户离开输入框时进行格式校验:

usernameField.addFocusListener(new FocusAdapter() {
    @Override
    public void focusLost(FocusEvent e) {
        String text = usernameField.getText().trim();
        if (text.length() < 3) {
            showError(usernameField, "用户名至少3位");
        } else {
            clearError(usernameField);
        }
    }
});

private void showError(JComponent comp, String msg) {
    comp.setBorder(BorderFactory.createLineBorder(Color.RED, 2));
    statusLabel.setText(msg);
}

private void clearError(JComponent comp) {
    comp.setBorder(UIManager.getLookAndFeelDefaults().getBorder("TextField.border"));
}

这样的组合使用,既能阻止明显错误,又能引导用户完成正确输入,大大降低服务器端无效请求的压力。

登录模块实战:从前端校验到异步认证

现在让我们把这些知识点整合起来,打造一个完整的登录功能。这不是教科书式的demo,而是接近生产环境的真实实现。

前端多重校验防线

第一道防线是输入时的即时反馈:

private DocumentListener loginFormValidator = new DocumentListener() {
    @Override
    public void insertUpdate(DocumentEvent e) { updateLoginButton(); }
    @Override
    public void removeUpdate(DocumentEvent e) { updateLoginButton(); }
    @Override
    public void changedUpdate(DocumentEvent e) { updateLoginButton(); }

    private void updateLoginButton() {
        boolean isValid = !usernameField.getText().trim().isEmpty()
                       && passwordField.getPassword().length > 0;
        loginBtn.setEnabled(isValid);
    }
};

usernameField.getDocument().addDocumentListener(loginFormValidator);
passwordField.getDocument().addDocumentListener(loginFormValidator);

第二道防线是提交前的整体校验:

private boolean validateLoginForm() {
    String username = usernameField.getText().trim();
    if (username.isEmpty()) {
        showError(usernameField, "请输入用户名");
        return false;
    }
    if (username.length() < 3) {
        showError(usernameField, "用户名至少3个字符");
        return false;
    }
    clearError(usernameField);

    char[] pwd = passwordField.getPassword();
    if (pwd.length == 0) {
        showError(passwordField, "请输入密码");
        return false;
    }
    clearError(passwordField);

    return true;
}

异步认证:不让界面卡住一秒钟

关键来了!数据库连接是耗时操作,绝不能放在EDT线程中执行,否则整个界面就会冻结。解决方案是使用 SwingWorker

private void performLoginAsync() {
    if (!validateLoginForm()) return;

    final String username = usernameField.getText().trim();
    final char[] rawPwd = passwordField.getPassword();
    final String hashedPwd = PasswordUtil.hash(new String(rawPwd));
    Arrays.fill(rawPwd, '0'); // 立即清除明文

    SwingWorker<Boolean, Void> worker = new SwingWorker<>() {
        @Override
        protected Boolean doInBackground() throws Exception {
            // 模拟网络延迟
            Thread.sleep(800);
            return UserDAO.authenticate(username, hashedPwd);
        }

        @Override
        protected void done() {
            loginBtn.setEnabled(true); // 恢复按钮
            try {
                boolean success = get();
                if (success) {
                    dispose(); // 关闭登录窗
                    SwingUtilities.invokeLater(() -> new MainFrame().setVisible(true));
                } else {
                    JOptionPane.showMessageDialog(LoginFrame.this,
                        "用户名或密码错误", "登录失败", JOptionPane.ERROR_MESSAGE);
                }
            } catch (Exception e) {
                JOptionPane.showMessageDialog(LoginFrame.this,
                    "系统异常:" + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
            }
        }
    };

    loginBtn.setEnabled(false); // 防止重复提交
    worker.execute();
}

这里有几个重点:
1. doInBackground() 在后台线程执行,可以安全地做数据库查询
2. done() 在EDT线程回调,可以安全地更新UI
3. 使用 get() 获取结果,可能抛出 ExecutionException
4. 按钮禁用防止重复提交

流程图如下:

sequenceDiagram
    participant U as 用户
    participant UI as 登录界面
    participant T as 后台线程
    participant DB as 数据库

    U->>UI: 点击登录
    UI->>UI: 前端校验
    alt 校验失败
        UI-->>U: 提示错误
    else 校验通过
        UI->>T: 启动SwingWorker
        T->>DB: 发起认证请求
        DB-->>T: 返回结果
        T->>UI: 调用done()
        alt 认证成功
            UI->>U: 打开主界面
        else 认证失败
            UI->>U: 显示错误提示
        end
    end

安全加固:防暴力破解机制

为了防止恶意用户不断尝试密码,我们可以加入简单的锁定策略:

private int failedAttempts = 0;
private Timer lockoutTimer;

private void onLoginFailure() {
    failedAttempts++;
    if (failedAttempts >= 3) {
        loginBtn.setEnabled(false);
        statusLabel.setText("多次失败,请30秒后重试...");

        if (lockoutTimer != null) lockoutTimer.stop();
        lockoutTimer = new Timer(30000, e -> {
            loginBtn.setEnabled(true);
            statusLabel.setText("");
            failedAttempts = 0;
        });
        lockoutTimer.setRepeats(false);
        lockoutTimer.start();
    } else {
        statusLabel.setText("用户名或密码错误");
    }
}

虽然简单,但已经能有效抵御自动化脚本的穷举攻击。

数据库连接与安全访问:别让黑客轻易得手

如果说GUI是脸面,那数据库就是心脏。一旦失守,整个系统就完了。所以我们必须采取最强硬的防护措施。

连接管理:从裸连到连接池

初学者常犯的错误是每次操作都新建连接:

// ❌ 每次都创建新连接,性能极差
Connection conn = DriverManager.getConnection(url, user, pass);

正确做法是使用连接池,比如HikariCP:

public class DataSourceSingleton {
    private static HikariDataSource dataSource;

    public static synchronized DataSource getInstance() {
        if (dataSource == null) {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl("jdbc:mysql://localhost:3306/admin_system");
            config.setUsername("root");
            config.setPassword("your_password");
            config.setMaximumPoolSize(10);
            config.setAutoCommit(false); // 手动控制事务
            dataSource = new HikariDataSource(config);
        }
        return dataSource;
    }
}

配置说明:
- maximumPoolSize=10 :最大连接数,根据实际负载调整
- autoCommit=false :开启手动事务控制
- idleTimeout :空闲连接超时时间
- connectionTimeout :获取连接的最大等待时间

外部通过 DataSourceSingleton.getInstance().getConnection() 获取连接,既高效又安全。

SQL注入防御:PreparedStatement不是可选项

下面这段代码你一定见过:

// ❌ 致命漏洞!
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

攻击者只要输入 ' OR '1'='1 ,就能绕过认证。解决办法只有一个: 永远使用PreparedStatement

// ✅ 正确做法
String sql = "SELECT id, username FROM users WHERE username = ? AND password_hash = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
    pstmt.setString(1, username);
    pstmt.setString(2, passwordHash);
    try (ResultSet rs = pstmt.executeQuery()) {
        if (rs.next()) {
            return new User(rs.getLong("id"), rs.getString("username"));
        }
    }
}

这里的 ? 是占位符,数据库会在预编译阶段就确定SQL结构,后续传入的参数只会作为纯数据处理,从根本上杜绝注入风险。

密码存储:SHA-256够用吗?

很多教程教人用SHA-256哈希密码,但这是不够安全的。因为SHA系列算法设计目标是快速计算,反而利于暴力破解。

真正应该用的是专为密码设计的慢哈希算法,比如bcrypt:

<!-- Maven依赖 -->
<dependency>
    <groupId>org.mindrot</groupId>
    <artifactId>jbcrypt</artifactId>
    <version>0.4</version>
</dependency>
import org.mindrot.jbcrypt.BCrypt;

public class SecurePassword {
    public static String hash(String password) {
        return BCrypt.hashpw(password, BCrypt.gensalt(12));
    }

    public static boolean verify(String password, String hashed) {
        return BCrypt.checkpw(password, hashed);
    }
}

gensalt(12) 表示工作因子为12,意味着每次哈希需要 $2^{12}$ 次迭代运算。这在用户看来只是毫秒级延迟,但对于攻击者来说,破解成本呈指数级增长。

注册时存哈希值:

String hashed = SecurePassword.hash("user_password");
UserDAO.save(username, hashed);

登录时验证:

String storedHash = UserDAO.findHashByUsername(username);
boolean isValid = SecurePassword.verify(inputPassword, storedHash);

这才是符合现代安全标准的做法。

CRUD全流程实现:打通最后一公里

最后,让我们完成整个系统的闭环——后台数据管理功能。

查询展示:JTable的高级玩法

JTable 是Swing中最强大的数据显示组件。结合 DefaultTableModel ,我们可以轻松实现动态表格:

String[] columns = {"ID", "姓名", "职位", "薪资", "入职日期"};
tableModel = new DefaultTableModel(columns, 0); // 0表示初始无数据
table = new JTable(tableModel);
table.setFillsViewportHeight(true);
table.setAutoCreateRowSorter(true); // 支持点击列头排序

JScrollPane scrollPane = new JScrollPane(table);
add(scrollPane, BorderLayout.CENTER);

加载数据的方法要封装好:

public void loadUsers() {
    List<User> users = UserDAO.findAll();
    tableModel.setRowCount(0); // 清空旧数据

    for (User user : users) {
        Object[] row = {user.getId(), user.getName(), user.getPosition(),
                       user.getSalary(), user.getHireDate()};
        tableModel.addRow(row);
    }
}

为了让表格更好看,还可以自定义列宽:

public void autoResizeColumns() {
    for (int col = 0; col < table.getColumnCount(); col++) {
        int maxWidth = 50;
        for (int row = 0; row < Math.min(100, table.getRowCount()); row++) {
            TableCellRenderer renderer = table.getCellRenderer(row, col);
            Component comp = table.prepareRenderer(renderer, row, col);
            maxWidth = Math.max(maxWidth, comp.getPreferredSize().width);
        }
        table.getColumnModel().getColumn(col).setPreferredWidth(maxWidth + 10);
    }
}

增删改操作:带上确认对话框更安全

新增记录用模态对话框:

public void showAddDialog() {
    JTextField nameField = new JTextField(15);
    JComboBox<String> posBox = new JComboBox<>(new String[]{"经理", "工程师", "销售"});
    JTextField salField = new JTextField(15);

    JPanel form = new JPanel(new GridLayout(3,2));
    form.add(new JLabel("姓名:")); form.add(nameField);
    form.add(new JLabel("职位:")); form.add(posBox);
    form.add(new JLabel("薪资:")); form.add(salField);

    int result = JOptionPane.showConfirmDialog(this, form,
        "新增员工", JOptionPane.OK_CANCEL_OPTION);

    if (result == JOptionPane.OK_OPTION) {
        try {
            User user = new User();
            user.setName(nameField.getText().trim());
            user.setPosition((String)posBox.getSelectedItem());
            user.setSalary(new BigDecimal(salField.getText()));
            UserDAO.save(user);
            loadUsers(); // 刷新
        } catch (Exception e) {
            JOptionPane.showMessageDialog(this, "输入格式错误");
        }
    }
}

删除前一定要确认:

int selectedRow = table.getSelectedRow();
if (selectedRow == -1) {
    JOptionPane.showMessageDialog(this, "请先选择要删除的行");
    return;
}

String name = (String)table.getValueAt(selectedRow, 1);
int confirm = JOptionPane.showConfirmDialog(this,
    "确定要删除员工 " + name + " 吗?", "确认删除",
    JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);

if (confirm == JOptionPane.YES_OPTION) {
    long id = (Long)table.getValueAt(selectedRow, 0);
    UserDAO.delete(id);
    loadUsers();
}

编辑功能则需要回显原数据:

int row = table.getSelectedRow();
if (row < 0) return;

Object[] data = {
    table.getValueAt(row, 0),
    table.getValueAt(row, 1),
    table.getValueAt(row, 2),
    table.getValueAt(row, 3)
};

if (showEditDialog(data)) {
    User user = new User();
    user.setId((Long)data[0]);
    user.setName((String)data[1]);
    user.setPosition((String)data[2]);
    user.setSalary((BigDecimal)data[3]);
    UserDAO.update(user);
    loadUsers();
}

至此,一个完整的CRUD系统就成型了。每一次操作都能即时反映在界面上,形成流畅的用户体验。

工程化落地:从玩具项目到可交付产品

最后一步,是如何把这一切打包成一个真正可用的应用程序。

包结构规范:让代码井井有条

建议采用以下分层结构:

src/
├── com/example/ui/          # 所有界面类
│   ├── LoginFrame.java
│   ├── MainFrame.java
│   └── UserManagementPanel.java
├── com/example/dao/         # 数据访问对象
│   ├── UserDAO.java
│   └── UserDAOImpl.java
├── com/example/entity/      # 实体类
│   └── User.java
├── com/example/util/        # 工具类
│   ├── DatabaseUtil.java
│   └── PasswordUtil.java
└── com/example/App.java     # 启动类

类命名也要规范:
- LoginFrame extends JFrame
- UserManagementPanel extends JPanel
- DatabaseUtil.class

打包发布:一键运行不是梦

使用IDE导出Runnable JAR文件,包含所有依赖库。最终发布的目录结构应该是这样的:

dist/
├── management-system.jar
├── schema.sql           # 数据库建表脚本
├── README.md            # 使用说明
└── lib/                 # (可选)外部依赖

README.md 内容示例:

# 后台管理系统

## 运行条件
- JDK 8+
- MySQL 5.7+

## 安装步骤
1. 执行 schema.sql 创建数据库
2. 双击运行 management-system.jar
3. 默认账号:admin / 密码:admin

## 故障排查
- 报错"Driver not found" → 检查JAR是否完整
- 连接失败 → 确认MySQL服务已启动

再加上一个图标文件 .ico 和启动脚本,你就拥有了一个可以分发给客户的成熟产品。


回顾整个旅程,我们从一个简单的窗口开始,逐步构建出了一个功能完整、安全可靠、用户体验良好的桌面应用。这其中没有黑科技,只有扎实的基础知识和严谨的工程思维。正如一位老程序员所说:“ 优秀的软件不是写出来的,是长出来的 。”

当你下次面对一个新的技术挑战时,不妨问问自己:能不能也用这种方式,一步一步地把它“养大”? 🌱

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

简介:本资源基于Java Swing与JDBC技术,实现了一个完整的图形用户界面后台管理系统,涵盖管理员登录、数据增删改查等功能。系统通过Swing构建可视化界面,使用JDBC连接数据库,支持安全的用户认证与数据操作。附带数据库文件,适合用于学习Java桌面应用开发中GUI设计与数据库交互的核心技术,是提升Java SE综合编程能力的实用项目。


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

Logo

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

更多推荐