news 2026/9/12 17:28:06

Java课程设计:Swing广告墙系统登录、权限与查询实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java课程设计:Swing广告墙系统登录、权限与查询实现解析

简介:这是一份基于 Java 图形界面开发的“广告墙”课程实验设计项目,面向计算机相关专业在校学生、教师及企业开发者,尤其适合需要完成课设、毕设或项目初期演示的读者。项目实现了管理员登录注册、广告信息增删改查、用户个人广告查询等功能模块,代码结构清晰,可直接运行演示,也可作为二次开发的基础骨架。包体共 138 个文件,包含 82 个 Java 源文件、44 个编译后的 class 文件、6 个 XML 配置文件、1 个 SQL 数据库脚本及 1 个 README 说明文档,压缩包约 1.05MB,整体轻量、便于快速导入 IDE 学习。项目代码均通过测试并成功运行,作者答辩评审平均分达 94.5 分,质量有保证;目前已有 375 人学习下载。下载后对照 SQL 脚本与 README 即可快速搭建数据库并跑通流程,适合 Java 初学者进阶,亦可在现有功能上扩展实现个性化广告管理模块。

1. 广告墙课设:五个类拆出来的登录、发布与权限骨架

广告墙这个题目在 Java 课程设计里出镜率很高,但大多数版本是把登录、注册、发布、查询糅在一起写,能跑通已经算不错。这份资源里反而是十个类分明地铺开,从AdministratorLoginViewRegisterViewHandleAddAdvertisementQueryOneUserADQueryAllUserAD,每一块边界都踩在「管理员、普通用户、广告实体」这三个角色上。也就是说,这看起来是个界面作业,实际骨架已经具备了一个小型内容管理系统的雏形:身份认证、数据写入、按条件查询、权限区隔。对正在找 java 课程设计、java 项目练手素材的人来说,这个代码量刚好能拆开读,也刚好够在答辩现场讲清楚设计思路。本文从视图层入手,把登录、会话、广告写入和查询这四条链路逐个拆开,最后给出一版可以直接在家里重构的小改法。

2. 从 AdvertisingBoardView 看 Swing 视图容器如何承载登录、注册与主面板切换

2.1 视图类的职责划分与类名命名规律

先看这份资源里的类名:AdvertisingBoardViewAdministratorLoginViewRegisterViewModifyPasswordView,凡是带View后缀的类,本质上都是一个JFrameJPanel容器。这种命名方式是 Java 课程设计里最典型的分层写法,不算精致,但胜在直白,读代码的人一眼就知道哪个类负责哪块界面。

AdvertisingBoardView在整套代码里扮演主面板的角色,类似于整个系统的首页。它需要同时容纳几个不同的操作入口:

类名职责与 AdvertisingBoardView 的关系
AdministratorLoginView管理员登录窗口从主面板点「管理员登录」时弹出
RegisterView普通用户注册主面板上的注册入口
AdvertisingBoardView广告墙主界面,承载展示区自身就是核心容器
ModifyPasswordView修改密码登录后调用
IntegrationView汇总入口,串起所有视图从它启动整个程序

IntegrationView可以理解为程序入口类,它持有AdvertisingBoardView以及其他视图对象的引用。很多初次做课设的同学会写成「每一个按钮都new一个窗口」,比如在AdvertisingBoardView里直接new AdministratorLoginView(),这样做的问题是:管理员登录后要回到主界面,但主界面的对象已经失去了引用,只能再创建一个新主界面,状态全部丢失。正确做法是把主面板实例传给子窗口,或者由一个统一的控制器持有主面板引用,需要刷新时调用主面板的刷新方法而不是重新构建。

2.2 面板切换的两种常见实现:卡片布局与窗口弹出

广告墙这类界面天然包含「普通用户视角」和「管理员视角」两种状态,最简单的实现方式是在AdvertisingBoardView里用一个CardLayout管理多个面板。

public class AdvertisingBoardView extends JFrame { private JPanel mainPanel; private CardLayout cardLayout; // 定义两个卡片:adWall 广告墙展示、adminPanel 管理操作区 private JPanel adWallPanel; private JPanel adminPanel; public AdvertisingBoardView() { cardLayout = new CardLayout(); mainPanel = new JPanel(cardLayout); adWallPanel = new JPanel(); // 放广告列表的滚动面板 adminPanel = new JPanel(); // 放「添加广告」「查询所有广告」等按钮 mainPanel.add(adWallPanel, "AD_WALL"); mainPanel.add(adminPanel, "ADMIN"); this.setContentPane(mainPanel); cardLayout.show(mainPanel, "AD_WALL"); } // 给外部控制器调用:切换到管理员卡片 public void showAdminPanel() { cardLayout.show(mainPanel, "ADMIN"); } // 切换到广告墙卡片 public void showAdWallPanel() { cardLayout.show(mainPanel, "AD_WALL"); } }

这段代码的逻辑说明:CardLayout把多个面板叠放在同一个区域,通过show(Container parent, String name)切换显示哪一层,而不是销毁重建窗口。AD_WALLADMIN是两个卡片的标识名,mainPanel.add(adWallPanel, "AD_WALL")中的第二个参数就是这张卡片的索引名。好处在于:从管理区切回用户视角时,广告列表的滚动位置、已经查询出来的数据不会丢失,因为面板对象始终存在。缺点也明显,所有面板在程序启动时就要一起构建,如果广告列表数据量很大,启动会变慢。

另一种常见做法是把广告查询做成独立的JDialog弹窗,比如点击「我的广告」时弹出QueryOneUserAD窗口。弹窗方案的好处是职责隔离,QueryOneUserAD只管自己的查询和展示,不用理解外层主界面;缺点是弹窗不是模态时,用户可以同时操作两个窗口,容易造成数据不一致。课程设计的答辩环节,老师最爱问的就是「你这个窗口关了,数据刷新了吗」「再登录一次,上次的查询条件还在吗」,用卡片布局能答上「状态保持」,用弹窗就要额外处理关闭事件的监听。

2.3 视图层最容易翻车的三个细节

第一个细节是关闭窗口时的默认操作。很多课设代码里,子窗口setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)直接写在子类构造函数里,导致用户在管理员登录窗口点「X」把整个程序退出了。正确做法是子窗口用DISPOSE_ON_CLOSE,只有主窗口才用EXIT_ON_CLOSE

第二个细节是字体混淆。中文广告内容在 Windows 上没问题,换到 Linux 服务器跑就变成方块,因为代码里写了new Font("宋体", Font.PLAIN, 14)这类硬编码字体。可靠写法是new Font(Font.SANS_SERIF, Font.PLAIN, 14),让系统选择可用字体。

第三个细节是 Swing 线程更新。广告列表加载如果用了多线程,必须在事件分发线程里刷新界面。很多新手直接在线程里调用jTable.setModel(...)更新表格,轻则闪烁,重则直接抛InterruptedException。标准写法是:

SwingUtilities.invokeLater(new Runnable() { @Override public void run() { adTable.setModel(buildTableModel(adList)); } });

invokeLater的意义是:run()方法里的代码会被投递到 Swing 事件线程中排队执行,这样表格组件不会被多个线程同时操作。这个点写在课设报告里,能够直接影响答辩评分。

3. 登录链路拆解:从 AdministratorLoginView 到会话保持的身份校验设计

3.1 登录窗口的实体校验与「账号/密码/角色」三要素

AdministratorLoginView负责管理员登录,RegisterView负责普通用户注册,这两者之间不是 UI 重复,而是校验逻辑不同。管理员账号通常不允许随意注册,所以资源里单独拆了一个AdministratorRegisterView出来,这背后隐含的设计意图是:普通用户走开放注册,管理员走受控注册。

先看登录逻辑的典型实现,这段代码基于账号文件做校验,跟资源的文件存储方案保持一致:

public class AdministratorLoginView extends JFrame { private JTextField userField; private JPasswordField passwordField; // 登录按钮点击事件 private void doLogin() { String username = userField.getText().trim(); String password = new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "账号和密码不能为空"); return; } // 调用查询用户的服务方法,传入账号和密码 Object user = queryUserByAccount(username, password); if (user != null) { // 登录成功:把当前用户信息存到会话对象中 Session.setCurrentUser(user); new AdvertisingBoardView().setVisible(true); this.dispose(); } else { JOptionPane.showMessageDialog(this, "账号或密码错误"); } } }

逻辑说明:JPasswordField.getPassword()返回的是char[]而非String,这是刻意设计——字符串在 Java 中是不可变对象,密码以String形式存在内存里无法主动擦除,而char[]用完后可以填充覆盖。Session.setCurrentUser(user)是后面所有查询操作的凭据来源。trim()去掉首尾空格,避免用户无意输入空格导致登录失败。

queryUserByAccount在这个资源场景下通常的实现方案是读本地文件。课程设计阶段不会上数据库,一般是一个users.txtadmin.txt,每行存用户名和密码。文件路径如果没有刻意处理,会使用项目根目录下的相对路径,这也是课设能跑通但换机器就跑不起来的第一大坑:路径写死了。建议在加载用户信息前打印出实际路径:

System.out.println(new File("users.txt").getAbsolutePath());

3.2 会话保持:为什么广告墙必须知道「当前是谁」

QueryOneUserADQueryAllUserAD的区别就藏在会话里:前者查当前用户自己的广告,后者查所有人的广告。如果程序不保存登录状态,就无法区分这两个场景。

会话的简单实现方式是静态全局变量:

public class Session { // 当前登录用户,登录成功时写入 private static User currentUser; public static void setCurrentUser(User user) { currentUser = user; } public static User getCurrentUser() { return currentUser; } // 退出登录时清除 public static void clear() { currentUser = null; } }

代码逻辑说明:Session类用static字段保存当前登录用户对象,整个 JVM 进程里只有一份,任何视图类里都能通过Session.getCurrentUser()拿到当前操作者。这个方案对课设来说够用,但缺陷在项目重启后会话丢失,刷新页面需要重新登录。

进阶一点的做法是序列化保存,把用户对象写入.session文件,下次启动时自动恢复登录。User类实现Serializable接口后,用ObjectOutputStream序列化写入,登录时先读文件再比对:

public static void saveSession(User user) throws IOException { try (ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("session.dat"))) { oos.writeObject(user); } }

ObjectOutputStreamwriteObject会把整个对象图写进文件,包括字符串、数值和嵌套对象。读取时用ObjectInputStreamreadObject()还原,但读回来的对象要instanceof检查,防止文件被人手动篡改导致反序列化异常。这个技术在答辩时讲「记住我」功能会非常加分,因为大部分同学的登录退出都是纯内存态,重启一次就露怯。

3.3 密码存储的安全边界

课设代码里密码一般是明文写在文件里。关于这一点必须说清楚:明文密码只能用于课设演示,放到生产环境是安全事故。对 5 年以上工程师来说,看到这种代码第一反应不是嘲笑,而是想清楚改造路径:最少要把密码做一次哈希再存。

public static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder hex = new StringBuilder(); for (byte b : digest) { hex.append(String.format("%02x", b)); } return hex.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } }

这段代码把用户输入的密码转成 32 位十六进制摘要,存到文件里就不再是明文。但 MD5 本身已被证明可被彩虹表快速碰撞,所以更稳妥的方案是 SHA-256 加盐:

public static String sha256WithSalt(String password, String salt) { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest((password + salt).getBytes(StandardCharsets.UTF_8)); // 继续把字节数组转成十六进制字符串 }

盐值可以取用户名或注册时间戳,核心思想是让同样的密码在不同用户身上产生不同的摘要,防止批量撞库。这三个方案拿出来就覆盖了「课设能用」到「生产可用」的完整梯度,答辩时被问到密码安全可以直接回答。

4. 广告写入与检索链路:HandleAddAdvertisement、QueryOneUserAD 与权限过滤

4.1 广告实体类的字段设计:title、content、owner、createTime

广告数据的载体需要在多个视图类之间传递,Java 课设最常见的是定义一个Advertisement实体类。字段数量不用贪多,够用就好,关键是能支撑AddAdvertisement写入和QueryOneUserAD查询这两条链路。

字段类型说明
idint主键,自增,也用于删除和详情查询
titleString广告标题,列表展示
contentString广告正文,点击后查看
ownerString发布者用户名,写入时取自 Session
createTimeString发布时间,格式yyyy-MM-dd HH:mm:ss

owner字段是这套代码里权限过滤的核心。AddAdvertisement保存广告时,把Session.getCurrentUser().getUsername()写入owner,这样后续QueryOneUserAD才能用owner.equals(当前用户名)作为过滤条件。

4.2 广告写入链路:AddAdvertisement 的参数校验与数据落盘

AddAdvertisement处理广告的新增操作,与之配套的HandleAddAdvertisement更偏向控制器,相当于把「界面采集的数据」转交给「业务处理方法」。为了不让AddAdvertisement里的保存逻辑变成面条代码,建议把「读取表单 -> 构造对象 -> 写入存储 -> 刷新列表」拆成独立步骤:

public class HandleAddAdvertisement { // 保存一条广告,fileName 是数据文件的路径 public boolean saveAdvertisement(Advertisement ad, String fileName) { // 业务校验:标题不能为空且长度不超过50个字符 if (ad.getTitle() == null || ad.getTitle().trim().isEmpty()) { return false; } if (ad.getTitle().length() > 50) { return false; } try (BufferedWriter writer = new BufferedWriter( new FileWriter(fileName, true))) { // 每行字段用 "|" 分隔,避免与内容里的逗号混用 String line = ad.getId() + "|" + ad.getTitle() + "|" + ad.getContent() + "|" + ad.getOwner() + "|" + ad.getCreateTime(); writer.write(line); writer.newLine(); return true; } catch (IOException e) { e.printStackTrace(); return false; } } }

逻辑说明:FileWriter(fileName, true)的第二个参数true表示追加模式,新广告不会覆盖已有内容。字段分隔符选|而不是逗号,是因为广告正文里出现中文逗号的概率几乎百分之百,如果正文里恰好有分隔符,读回时split会切出错误的字段数。BufferedWriter在这里主要起到缓冲作用,减少磁盘 IO 次数;每次write后调用newLine()写入行分隔符,保证每条记录独立成行。

这段代码隐藏的陷阱是:文件中的 id 需要自行维护自增,而不是依赖数据库。课程设计阶段常见做法是新增前扫描文件里的最大 id 加一:

public int getNextId(String fileName) throws IOException { int maxId = 0; try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) { String line; while ((line = reader.readLine()) != null) { String[] parts = line.split("\\|"); int id = Integer.parseInt(parts[0]); if (id > maxId) { maxId = id; } } } return maxId + 1; }

这里用readLine()逐行读取文件,每行拆出第一段作为 id,取所有 id 中的最大值再加一。split("\\|")中的反斜杠是必须的,因为管道符在正则表达式里表示「或」,需要转义才能当作普通字符处理。注意点是:广告内容里如果也包含|字符,这种切分方式会错位。改进方案是把内容里出现的|替换成全角,或者干脆改用JSONObject序列化整行,利用 JSON 的转义机制规避分隔符冲突。

4.3 查询链路:QueryOneUserAD 与 QueryAllUserAD 的同与不同

这两个类放在一起看才看得出设计意图。QueryAllUserAD查全部广告,QueryOneUserAD只查当前用户的,二者都涉及「从文件读出来 -> 装成列表 -> 绑到表格/列表控件」的流程。

public class QueryOneUserAD { // 查询当前登录用户发布的全部广告 public List<Advertisement> queryByOwner(String owner, String fileName) { List<Advertisement> result = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) { String line; while ((line = reader.readLine()) != null) { String[] parts = line.split("\\|"); // 行的字段数必须满足5个,防止脏数据 if (parts.length < 5) { continue; } // 校验这一行的发布者是不是当前用户 if (parts[3].equals(owner)) { Advertisement ad = new Advertisement(); ad.setId(Integer.parseInt(parts[0])); ad.setTitle(parts[1]); ad.setContent(parts[2]); ad.setOwner(parts[3]); ad.setCreateTime(parts[4]); result.add(ad); } } } catch (IOException e) { e.printStackTrace(); } return result; } }

逻辑说明:这个方法的过滤条件落在parts[3].equals(owner)这一行。parts[3]是写入时保留的发布者字段,owner来自登录会话。parts.length < 5的防御性检查很重要,因为文件里如果混入一行格式错误的数据,直接访问parts[3]会抛出ArrayIndexOutOfBoundsException

QueryAllUserAD与上述代码几乎一样,唯一差别是去掉parts[3].equals(owner)这层判断,把每一行都构造成Advertisement对象加入列表。所以这两个类天然就是重构成一个带条件参数方法的候选:

public List<Advertisement> queryAds(String fileName, String ownerFilter) { // ownerFilter 为空时返回全部,非空时按发布者过滤 }

这个方法把两个类里的重复读文件逻辑收敛到了一处,ownerFilternull时跳过过滤逻辑,等价于原QueryAllUserAD;传入用户名时,等价于原QueryOneUserAD。文件路径和过滤条件作为方法参数而不是硬编码,也让后续对接 GUI 时更方便。

4.4 数据刷新与表格模型重建

查询完成后的界面展示,课设里通常用JTable配合DefaultTableModel完成:

public void refreshTable(List<Advertisement> adList) { String[] columns = {"ID", "标题", "发布者", "发布时间"}; Object[][] rows = new Object[adList.size()][columns.length]; for (int i = 0; i < adList.size(); i++) { Advertisement ad = adList.get(i); rows[i][0] = ad.getId(); rows[i][1] = ad.getTitle(); rows[i][2] = ad.getOwner(); rows[i][3] = ad.getCreateTime(); } DefaultTableModel model = new DefaultTableModel(rows, columns); adTable.setModel(model); }

DefaultTableModel接收两个参数:二维数组rows表示表格每一行的数据,一维数组columns是列名。替换setModel是最快的刷新方式,但表格原有的列宽、排序状态会被重置。对课设来说这不是问题,可如果广告数量达到几百条,每次都全量重建表格性能会很差。一个折中的优化是:只更新数据行、保留列模型,用model.setRowCount(0)清空旧数据,再逐行addRow。本质上没有改变全量重建的现实,但视觉上不会出现整表闪烁。

5. IntegrationView 入口收束与 Session 生命周期:一版可验证的小重构

5.1 把 IntegrationView 从「上帝类」改成启动器

IntegrationView在源码里是程序入口,但很多课设里的入口类会慢慢膨胀成上帝类:既要处理按钮事件,又要管理窗口切换,还要加载数据。它的代码量超过四百行时,维护成本就开始失控。

一个课程设计阶段够用的重构方向是把IntegrationView收敛成「启动器 + 简易路由」,既不引入 Spring 等重框架,又能把职责讲清楚:

public class IntegrationView { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { AdvertisingBoardView mainView = new AdvertisingBoardView(); mainView.setVisible(true); mainView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); }); } }

逻辑说明:main方法只负责三件事,创建主视图、显示主视图、设置关闭行为。窗口的跳转完全交给AdvertisingBoardView内部去处理,比如登录按钮的事件回调里创建AdministratorLoginView,而不要再让IntegrationView持有所有窗口的引用。这样新增一个「忘记密码」窗口时,只改AdvertisingBoardView和新增类,IntegrationView不需要改动。

5.2 如何验证重构没改坏原功能

课设场景里没有自动化测试环境,最直接的验证路径是编译、运行、走通四条业务链路。这里给一份可以照着做的检查表:

检查项操作步骤预期结果
注册打开主界面,进入RegisterView注册一个新用户注册成功且用户信息写入users.txt
登录用刚才的账号登录登录成功后Session.getCurrentUser()返回该用户
发布广告AddAdvertisement里填标题和正文,确定ads.txt末尾追加一行,字段数 5 个
查询我的广告调用QueryOneUserAD只出现当前用户的记录
管理员查询全部管理员登录后调QueryAllUserAD所有用户的广告都可见

比这个检查表更严谨的是在pom.xmlbuild.gradle里引入 JUnit,把HandleAddAdvertisement.saveAdvertisementQueryOneUserAD.queryByOwner单独写单元测试。这两个方法都是纯方法,不依赖 GUI 组件,非常适合先测试后重构:

@Test public void testSaveAndQueryAd() throws IOException { // 使用临时文件,避免污染正式数据 String testFile = "test_ads.txt"; HandleAddAdvertisement handler = new HandleAddAdvertisement(); Advertisement ad = new Advertisement(); ad.setId(1); ad.setTitle("测试广告"); ad.setContent("内容"); ad.setOwner("tester"); ad.setCreateTime("2025-01-01 12:00:00"); // 保存并断言返回 true boolean saved = handler.saveAdvertisement(ad, testFile); assertTrue(saved); // 查询并断言只有一条记录 QueryOneUserAD query = new QueryOneUserAD(); List<Advertisement> list = query.queryByOwner("tester", testFile); assertEquals(1, list.size()); }

JUnit 测试的核心价值在于:当你把QueryOneUserADQueryAllUserAD合并成带ownerFilter参数的方法后,跑一遍这个测试就能知道过滤逻辑有没有写错。测试文件用test_ads.txt隔离了正式数据,不会把测试记录混进课程设计的演示数据里。

5.3 最后一块拼图:Session 生命周期与登出入口

课设里Session.clear()往往没有对应的调用点,导致「退出登录」按钮形同虚设。补全这个缺口只需要三样东西:主界面一个「退出登录」按钮,点击时清除会话,然后回到登录视图。

private void doLogout() { Session.clear(); new AdministratorLoginView().setVisible(true); this.dispose(); }

关键点在dispose()必须放在setVisible(true)之后执行,否则窗口销毁后新窗口会在错误的事件循环里创建。Session.clear()把静态字段设为null,下一次Session.getCurrentUser()就会返回空,广告查询链路的parts[3].equals(owner)也会因为没有owner而不满足条件,数据自然就查不出来——这个行为本身就是安全兜底。动手改的时候顺手在setVisible(true)前面加一行System.out.println("当前用户:" + Session.getCurrentUser()),能帮你快速定位是视图切换问题还是会话丢失问题。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 17:25:31

如何用 SMFS 把 Supermemory 容器挂载为本地目录并配置 memory paths

如何用 SMFS 把 Supermemory 容器挂载为本地目录并配置 memory paths 【免费下载链接】supermemory Memory and context engine app that is extremely fast, scalable, and can be run fully locally. The Memory API for the AI era. 项目地址: https://gitcode.com/GitHu…

作者头像 李华
网站建设 2026/9/12 17:24:43

MCP Agent把Embedding全改成了随机数:补完深度学习入门才看懂日志

MCP Agent把Embedding全改成了随机数:补完深度学习入门才看懂日志 周一下午部门突然接到需求:给智能Agent接入Model Context Protocol,让它能直接查数据库、调API。“这不就是给模型装手和脚嘛”,我当时在工位上搓着手想。三小时后,Agent成功接上Model Context Protocol,可它做…

作者头像 李华
网站建设 2026/9/12 17:24:39

合规审计查出19处开源引用缺失,CodeWhisperer的Reference Tracker帮我扛住了律师函

合规审计查出19处开源引用缺失,CodeWhisperer的Reference Tracker帮我扛住了律师函 周五下午三点多,我正在改API网关的超时逻辑,屏幕右下角弹出了合规团队的消息:“第三方审计发现,当前分支有19个文件缺少必要的开源引用声明,其中包括GPL-3.0代码,请48小时内完成整改,否则可能…

作者头像 李华