尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

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 课程设计里出镜率很高但大多数版本是把登录、注册、发布、查询糅在一起写能跑通已经算不错。这份资源里反而是十个类分明地铺开从AdministratorLoginView、RegisterView到HandleAddAdvertisement、QueryOneUserAD、QueryAllUserAD每一块边界都踩在「管理员、普通用户、广告实体」这三个角色上。也就是说这看起来是个界面作业实际骨架已经具备了一个小型内容管理系统的雏形身份认证、数据写入、按条件查询、权限区隔。对正在找 java 课程设计、java 项目练手素材的人来说这个代码量刚好能拆开读也刚好够在答辩现场讲清楚设计思路。本文从视图层入手把登录、会话、广告写入和查询这四条链路逐个拆开最后给出一版可以直接在家里重构的小改法。2. 从 AdvertisingBoardView 看 Swing 视图容器如何承载登录、注册与主面板切换2.1 视图类的职责划分与类名命名规律先看这份资源里的类名AdvertisingBoardView、AdministratorLoginView、RegisterView、ModifyPasswordView凡是带View后缀的类本质上都是一个JFrame或JPanel容器。这种命名方式是 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_WALL和ADMIN是两个卡片的标识名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.txt或admin.txt每行存用户名和密码。文件路径如果没有刻意处理会使用项目根目录下的相对路径这也是课设能跑通但换机器就跑不起来的第一大坑路径写死了。建议在加载用户信息前打印出实际路径System.out.println(new File(users.txt).getAbsolutePath());3.2 会话保持为什么广告墙必须知道「当前是谁」QueryOneUserAD和QueryAllUserAD的区别就藏在会话里前者查当前用户自己的广告后者查所有人的广告。如果程序不保存登录状态就无法区分这两个场景。会话的简单实现方式是静态全局变量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); } }ObjectOutputStream的writeObject会把整个对象图写进文件包括字符串、数值和嵌套对象。读取时用ObjectInputStream的readObject()还原但读回来的对象要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发布者用户名写入时取自 SessioncreateTimeString发布时间格式yyyy-MM-dd HH:mm:ssowner字段是这套代码里权限过滤的核心。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 ListAdvertisement queryByOwner(String owner, String fileName) { ListAdvertisement 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 ListAdvertisement queryAds(String fileName, String ownerFilter) { // ownerFilter 为空时返回全部非空时按发布者过滤 }这个方法把两个类里的重复读文件逻辑收敛到了一处ownerFilter为null时跳过过滤逻辑等价于原QueryAllUserAD传入用户名时等价于原QueryOneUserAD。文件路径和过滤条件作为方法参数而不是硬编码也让后续对接 GUI 时更方便。4.4 数据刷新与表格模型重建查询完成后的界面展示课设里通常用JTable配合DefaultTableModel完成public void refreshTable(ListAdvertisement 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.xml或build.gradle里引入 JUnit把HandleAddAdvertisement.saveAdvertisement和QueryOneUserAD.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(); ListAdvertisement list query.queryByOwner(tester, testFile); assertEquals(1, list.size()); }JUnit 测试的核心价值在于当你把QueryOneUserAD和QueryAllUserAD合并成带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())能帮你快速定位是视图切换问题还是会话丢失问题。本文还有配套的精品资源点击获取
返回列表