ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Swing学生管理系统:JDBC与PreparedStatement

Java Swing学生管理系统:JDBC与PreparedStatement 简介面向Java初学者与课程设计者这份PDF文档围绕JavaMySQL实现学生信息管理系统展开基于Java Swing与JDBC技术栈完成对学生信息的增删改查可直接支撑高校常见的课设题目。此类项目既能锻炼Swing界面编程能力也能巩固JDBC操作MySQL的核心流程。文档先说明开发环境JDK7、MySQL5、Windows 7再按model-dao-view三层结构组织代码讲解便于读者建立分层开发思维其中数据库部分给出admin和student两张表的建库建表SQL脚本并附有管理员及学生的初始示例数据学生表字段覆盖学号、姓名、性别、院系、籍贯、学分、邮箱、电话等核心信息。核心模型类则摘录了Admin与Student的成员变量、构造方法和getter/setter帮助读者理解对象属性与数据库字段的对应关系。资源包共1个PDF文件大小仅135KB内容紧凑便于查阅已有5010人浏览学习适合需要独立完成Java课设或想快速上手SwingMySQL整合开发的读者。1. 这套 Java Swing 课设源码值得重新拆一遍的不只是增删改查一个学生信息管理系统数据库两张表、JDBC 裸写连接、Swing 做界面听起来是再普通不过的课设作业。但把StudentDAO、BaseDAO、DBUtil这几个类翻完会发现里面藏着几个放在生产环境会被点名批评、放在面试里却正好能答出深度的设计点PreparedStatement的参数化查询到底挡了什么、BaseDAO用Class比较切换单例的写法边界在哪、limit ?,?分页在大数据量下为什么越来越慢。这套代码适合两类人一是正在做 Java 课设、想把JFrame MySQL写到能上台演示的新手二是准备 Java 面试、想找一个我能讲清楚 JDBC 底层机制的落地案例的开发者。仓库地址是github.com/ZhuangM/studentJDK 7 MySQL 5 Windows 7 的环境组合现在用 JDK 8/17 跑也没问题。下面按表结构、DAO 层、视图层三条线拆开讲。2. 三层结构下的表设计admin 与 student 为什么不把业务键直接设为主键2.1 建库建表从 CREATE DATABASE 到 InnoDB 与 utf8 的取舍先看初始化脚本这段 SQL 值得一行行过因为它决定了后面 DAO 层所有方法的写法和边界条件。CREATE DATABASE student; DROP TABLE IF EXISTS admin; CREATE TABLE admin ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(20) NOT NULL, username varchar(20) NOT NULL, password varchar(20) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT2 DEFAULT CHARSETutf8; INSERT INTO admin VALUES (1,admin,admin,admin); DROP TABLE IF EXISTS student; CREATE TABLE student ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(20) NOT NULL, sno varchar(20) NOT NULL, department varchar(20) NOT NULL, hometown varchar(20) NOT NULL, mark varchar(20) NOT NULL, email varchar(20) NOT NULL, tel varchar(20) NOT NULL, sex varchar(20) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT22 DEFAULT CHARSETutf8; INSERT INTO student VALUES (18,张三,001,信息科学技术学院,辽宁,80,zhangsan163.com,13888888888,男), (19,李四,002,理学院,上海,70,lisisina.com,13812341234,男), (20,王五,003,外国语学院,北京,88,wangwu126.com,13698765432,女);SQL 里几个容易被新手忽略但值得记住的点。ENGINEInnoDB保证了事务和行级锁课设虽然用不到事务但如果是 MyISAM后面update和delete在并发演示时可能出现表级锁等待表现就是界面卡住。AUTO_INCREMENT自增主键只是代理键业务上真正唯一的是sno学号所以StudentDAO里add方法先按sno查重、update方法也以sno作为更新条件之一——表设计上把业务唯一键和主键分离这是 DAO 层能写干净的根基。字段全用varchar(20)mark 存的是字符串而不是INT或DECIMAL后面做成绩排序会遇到字典序问题这点放到第 4 章分页时具体说。admin表初始密码是admin/admin登录逻辑走AdminDAO.queryForLogin用username? and password?查询。这里有个值得记的细节password是明文存储的课设可以接受但如果在简历里写这个项目最好补一句生产环境会用 MD5/BCrypt 加盐后存储不然面试官顺着密码字段往下问容易卡住。2.2 model 层 Student 与 Admin为什么字段全部设计成 Stringmodel 层是两个纯 POJOStudent里有sno、name、sex、department、homeTown、mark、email、tel全部是String。这种做法在课设里很常见但它有一个实际代价mark如果存成String按成绩排序时会按字典序排80会排在100前面存成Integer或BigDecimal才能正确排序。另一个点是sex也用varchar(20)如果业务上只允许男/女数据库层没有CHECK约束全靠视图层控制输入。对课设来说String 能减少类型转换、表格JTable直接填充字符串二维数组更省事这个取舍可以理解但要在心里清楚它的边界。Adminmodel 相对简单只有id、name、username、password四个字段对应表结构。这两个类的getId/setId是自增主键但注意StudentDAO.list()里返回的二维数组第一列并不是数据库自增 id而是查询结果的行号这个后面第 4 章表格渲染时会展开是一个真正的视图层坑。2.3 DBUtil 单例封装连接管理里的两个隐蔽问题DBUtil是核心工具类把所有 JDBC 样板代码收拢到一个类里AppConstants提供驱动类名、URL、用户名、密码。关键实现是这两个方法private Connection getConn() { try { if (conn null || conn.isClosed()) { Class.forName(AppConstants.JDBC_DRIVER); conn DriverManager.getConnection( AppConstants.JDBC_URL, AppConstants.JDBC_USERNAME, AppConstants.JDBC_PASSWORD ); } } catch (ClassNotFoundException e) { System.out.println(jdbc driver is not found.); e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } return conn; } public void close() { try { if (rs ! null) { rs.close(); } if (ps ! null) { ps.close(); } if (conn ! null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } }这个封装有两个隐蔽问题。第一close()把Connection也关了下一次操作时getConn()发现conn.isClosed()为 true会重新DriverManager.getConnection。也就是说每次增删改查都经历一次完整的 TCP 握手、MySQL 认证、断开演示时感觉不到但如果循环插入 1000 条数据耗时可能差 5 到 10 倍。第二conn、ps、rs是成员变量DBUtil单例被多个线程共享时会出现一个线程的rs被另一个线程的操作覆盖。课设界面是单线程事件分发这个问题不会暴露但如果你打算在简历里写基于 JDBC 实现至少要知道这个单例不是线程安全的。生产环境的做法是用 HikariCP 或 Druid第 5 章末尾会给出最小的改造思路。3. BaseDAO 与 StudentDAO把 JDBC 增删改查写到能答辩的程度3.1 DAO 工厂BaseDAO.getAbilityDAO 用 Class 比较切换单例的适用边界BaseDAO里有一段不太常见但值得分析的代码public static synchronized BaseDAO getAbilityDAO(DAO dao) { switch (dao) { case AdminDAO: if (baseDAO null || baseDAO.getClass() ! AdminDAO.class) { baseDAO AdminDAO.getInstance(); } break; case StudentDAO: if (baseDAO null || baseDAO.getClass() ! StudentDAO.class) { baseDAO StudentDAO.getInstance(); } break; default: break; } return baseDAO; }这里用getClass() ! AdminDAO.class判断当前缓存的实例是不是目标类型不是就替换。这是单例池的朴素实现只有一个baseDAO槽位谁要用谁顶上去。好处是代码省AdminDAO.getInstance()和StudentDAO.getInstance()各自是synchronized的懒加载单例不会重复创建。坏处是当baseDAO在AdminDAO和StudentDAO之间频繁切换时每次都 new 一个对象单例名存实亡且 switch 没有 default 分支处理未知类型扩一个TeacherDAO就要改这里。实际使用中视图层拿到的是BaseDAO引用往下转成具体 DAO 调用。这种写法在课设规模下完全够用但面试时如果要聊策略模式 工厂模式直接用这段代码当反例讲为什么单例池不适合用 Class 判断会显得理解更到位。3.2 add/update/delete 三个方法参数顺序与 check-then-act 的隐藏风险StudentDAO是整篇代码里信息量最大的类。add 方法的完整逻辑先看一遍public boolean add(Student stu) { boolean result false; if (stu null) { return result; } try { if (queryBySno(stu.getSno()) 1) { return result; } String sql insert into student(name,sno,sex,department,hometown,mark,email,tel) values(?,?,?,?,?,?,?,?); String[] param { stu.getName(), stu.getSno(), stu.getSex(), stu.getDepartment(), stu.getHomeTown(), stu.getMark(), stu.getEmail(), stu.getTel() }; if (db.executeUpdate(sql, param) 1) { result true; } } catch (SQLException se) { se.printStackTrace(); } finally { destroy(); } return result; }逻辑分三段先判空再按sno查重最后执行 insert。这个顺序是对的但注意queryBySno和executeUpdate共用了同一个DBUtil实例里的rs、ps成员变量。queryBySno执行完rs.next()后rs还持有查询结果集没有关闭紧接着db.executeUpdate(sql, param)内部的getConn()发现连接没有关闭直接复用连接然后ps conn.prepareStatement(sql)把引用覆盖。覆盖掉的PreparedStatement在数据库侧可能还有未释放的语句句柄直到destroy()里db.close()才真正关闭。这种成员变量共享 复用一个连接的写法在长时间运行后容易把 MySQL 的连接数拖满排错时看到的表象是Too many connections。修复方式很简单queryBySno里finally直接db.close()或者把executeQuery改成局部变量返回。update 方法里有个参数顺序的细节String sql update student set sex?,department?,email?,tel?,hometown?,mark? where name? and sno?; String[] param { stu.getSex(), stu.getDepartment(), stu.getEmail(), stu.getTel(), stu.getHomeTown(), stu.getMark(), stu.getName(), stu.getSno() };set子句六个字段对应param[0]到param[5]where条件对应param[6]和param[7]。这里?占位符的顺序严格跟随 SQL 文本PreparedStatement.setObject是按位置绑定一旦字段顺序调整而param数组没改数据会写错列。最常见的排错方式是打印 SQL 和参数组对照检查JDBC驱动不会帮你校验参数语义。delete 方法则走了另一个策略直接delete from student where name? and sno?不先查重rowCount 1才返回 true。这里name sno组合条件是为了防止误删同名学生但也说明表里sno并没有唯一索引——如果直接在数据库层给sno加UNIQUEadd 方法的查重步骤可以省掉靠约束兜底更可靠。3.3 ResultSet 的生命周期谁打开、谁消费、谁关闭DBUtil.executeQuery返回ResultSet之后ps和rs的关闭责任完全落在调用方。这套代码的处理是BaseDAO.destroy()protected void destroy() { try { if (rs ! null) { rs.close(); } } catch (SQLException se) { se.printStackTrace(); } finally { db.close(); } }queryByName和list方法都在finally里调用destroy()关闭结果集、语句和连接。问题是queryBySno里查完没有关闭queryByName把rs.next()消费完再buildList填充到ListStudent这个过程如果提前返回rs也会泄漏。ResultSet是流式的只能向前遍历一次不能重复读取所以 DAO 层常见做法是查出来立刻转成 List 或二维数组马上关连接而不是把ResultSet传到视图层。这套代码正是在 DAO 内部完成ResultSet - List - String[][]的转换视图层拿到的已经是纯数据这个方向是对的。4. 分页查询与视图层交互JTable 表格里的数据是怎么流动的4.1 limit 偏移量分页的公式与边界条件list(int pageNum)是分页查询的入口SQL 用了 MySQL 的limit偏移量写法private final int showNum 15; public String[][] list(int pageNum) { ListStudent stus new ArrayListStudent(); int i 0; int beginNum (pageNum - 1) * showNum; String sql select * from student limit ?,?; Integer[] param { beginNum, showNum }; rs db.executeQuery(sql, param); // 遍历 rs填充 List再转成 String[][] }分页公式是beginNum (pageNum - 1) * showNum。第 1 页取limit 0,15第 2 页取limit 15,15。limit的第一个参数是 offset第二个是返回行数。这里有两个边界条件如果pageNum超过最大页数limit不会报错只返回空结果集如果中间删了几条记录数据会整体前移可能出现两条记录跨页重复显示或漏显示这是偏移量分页的固有问题。更稳妥的做法是记录上一页最后一条记录的id用where id ? order by id limit 15做键集分页。演示时数据量小感觉不到差异但 MySQL 处理limit 100000,15时会把前 10 万行扫一遍再丢弃数据量上来后响应时间会指数级变差。AppConstants里还定义了maxPageNum 99MainView中上一页、下一页按钮会拿currPageNum和这个上限比较。这个上限是写死的如果表里超过 99 * 15 1485 条记录最后一页之后还有数据点下一页到第 99 页会停在原地。课设场景够用但如果你想把这段代码写进简历建议改成先select count(*)算出totalPage再和pageNum比较这才是通用的分页逻辑。4.2 queryByName 模糊查询like 参数的正确写法按姓名查询是学生管理系统的高频操作queryByName的实现值得单独看public String[][] queryByName(String name) { if (name.length() 0) { return result; } String sql select * from student where name like ?; String[] param { % name % }; rs db.executeQuery(sql, param); // 遍历 rs - List - String[][] }先说一个容易忽略的 bugif (name.length() 0)永远为 false原本意图应该是name.length() 0或name null。这个判断没有拦截任何空串空查询会变成like %%返回全部记录。修正写法是放在参数化查询之前做兜底避免全表扫描。like ?配合%name%是标准做法注意%是拼进参数里的而不是拼进 SQL 文本里这样name里的单引号、通配符不会被直接解释成 SQL 语法。如果name本身包含%或_会被当成通配符需要ESCAPE关键字处理课设阶段可以用like concat(%, ?, %)或直接参数拼接效果一样。模糊查询的代价是like %xx%无法走 B 树索引数据量大时建议用前缀匹配xx%或者引入全文索引。4.3 MainView 与 JTable二维数组如何变成可排序的表格MainView继承JFrame顶部放查询条件输入框中间是JTable底部是分页按钮和增删改按钮。表格初始化的核心代码如下public static String[] column { id, AppConstants.STUDENT_NAME, AppConstants.STUDENT_SNO, AppConstants.STUDENT_SEX, AppConstants.STUDENT_DEPARTMETN, AppConstants.STUDENT_HOMETOWN, AppConstants.STUDENT_MARK, AppConstants.STUDENT_EMAIL, AppConstants.STUDENT_TEL }; String[][] result studentDAO.list(currPageNum); myTableModel new DefaultTableModel(result, column); jTable new JTable(myTableModel);DefaultTableModel接收二维数组作为行数据一维数组作为列名。有一个细节需要看清楚list方法里buildResult填充的result[0][0]是stu.getId()而这个getId()是buildList里stu.setId(i 1)设置的i是结果集行号不是数据库主键。所以表格第一列的 id 实际是当前页内行号删除某行后行号会重新编排和数据库 id 对不上。演示时如果用户问为什么我删了一行后 id 从 3 变成 2原因就在这里。要让表格显示真实主键应该stu.setId(rs.getInt(id))而不是i 1。DefaultTableModel默认不支持点击表头排序。常见补法是包一层TableRowSorterTableRowSorterDefaultTableModel sorter new TableRowSorterDefaultTableModel(myTableModel); jTable.setRowSorter(sorter);排序时要小心行视图变化后jTable.convertRowIndexToModel()才能拿到真实模型索引否则删错行。这套源码没做RowSorter课设演示也不需要但作为这个项目还能怎么改的加分项给JTable加排序是性价比较高的一步。5. 从课设到面试PreparedStatement 预编译、连接复用与分页深坑的整改方向5.1 面试题里常问的的 PreparedStatement在这套代码里是怎么体现的DBUtil.executeUpdate和executeQuery全部走conn.prepareStatement(sql)没有一处用Statement拼 SQL。这是这套源码最大的亮点也正好是 Java 面试里 JDBC 部分的必问题。PreparedStatement的两个核心特性在这套代码里都能对上第一参数与 SQL 分离。?占位符只代表值不参与语法解析name里就算包含or 11或者drop table student也会被当成普通字符串参数传给 MySQL不会改变 SQL 结构。第二预编译复用。MySQL 的Connector/J默认并不开启服务端预编译需要 JDBC URL 加上useServerPrepStmtstruecachePrepStmtstrue才会真正在服务端缓存语句句柄。很多为什么我的 PreparedStatement 查询还是慢的问题根源就在这里。验证方法是在 MySQL 里SHOW GLOBAL STATUS LIKE Prepared_stmt_count如果开启后这个数稳定不变说明语句被复用了。这里的核心参数说明jdbc:mysql://localhost:3306/student?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiuseServerPrepStmtstruecachePrepStmtstrueuseServerPrepStmtstrue让驱动把 SQL 发给 MySQL 做服务端预编译cachePrepStmtstrue缓存编译结果。课设项目没配这两项也能跑但如果你在简历写使用 PreparedStatement 防止 SQL 注入建议把这几个参数补上免得面试官追问时只说得出参数化查询四个字。5.2 给 DBUtil 加连接池最小整改路径DBUtil单例每次close()关闭连接频繁操作数据库时性能损耗明显。最小改造方案是用 Druid 替换DriverManager.getConnectionprivate static DruidDataSource dataSource; static { dataSource new DruidDataSource(); dataSource.setDriverClassName(com.mysql.jdbc.Driver); dataSource.setUrl(AppConstants.JDBC_URL); dataSource.setUsername(AppConstants.JDBC_USERNAME); dataSource.setPassword(AppConstants.JDBC_PASSWORD); dataSource.setInitialSize(5); dataSource.setMaxActive(20); } public Connection getConn() throws SQLException { return dataSource.getConnection(); }setInitialSize(5)表示启动时预创建 5 个连接setMaxActive(20)限制最大连接数。改成连接池后DBUtil.close()里的conn.close()不会真正断开 MySQL而是把连接归还池子下个操作直接复用省去 TCP 握手和认证开销。改造点只有一个getConn()的获取来源从DriverManager换成dataSource.getConnection()其余 DAO 层代码一行都不用动。验证连接池是否生效可以看SHOW STATUS LIKE Threads_connected原来每次操作连接数反复升降改造后保持在一个稳定区间。分页的整改方向同样明确把maxPageNum 99改成select count(*) from student计算出totalPage再把limit ?,?换成where id ?键集分页。改完这两处这个课设项目的代码质量就已经超过大多数培训机构里的演示项目。判断标准很简单表里塞 1 万条数据连续翻 50 页界面不卡、连接数不涨就说明改到位了。本文还有配套的精品资源点击获取
返回列表