资讯动态

JDBC+MySQL手写图书管理系统:Java连接数据库的必修课

发布时间:2026/10/17 19:20:12 来源:尧图企业网站定制
很多学完Java基础语法的人都会纠结下一步到底学什么。我的建议是先别急着冲框架老老实实拿JDBCMySQL写一个迷你图书管理系统把查询、新增、修改、删除和退出这几个功能亲手敲一遍。这个项目看着不起眼却是把Java语法、SQL语句和数据库连接串起来的最短路径也是我带人这几年雷打不动的第一道练手题。说它是玩具项目一点不冤枉没有登录权限没有分页排序没有外键关系总共就一张表。但恰恰是这种简单把JDBC的每一个关键动作都暴露在你面前注册驱动、获取连接、预编译SQL、执行更新、遍历结果集、关闭资源。这套流程今天在主流持久层框架里依然存在只不过被封装得看不见了。你以后用框架遇到诡异报错十有八九要回头看这层东西。下面我把这个项目的完整落地过程捋一遍包括表结构、驱动选择、四个功能的写法和实测中踩过的坑照着一路敲下来你也能交出一份能稳定跑的项目。1. 为什么这个玩具项目人人都该手写一遍1.1 JDBC是Java访问数据库的地基先讲个我观察到的现象很多新人在框架时代入门写的第一个增删改查项目是在Spring Boot里配MyBatis全程只写接口和XML跑起来确实快。但一旦遇到问题就懵了——连接池报错、时区异常、编码乱码根本不知道错误从哪来。原因就是框架把JDBC封装得太深底层原理完全不可见。JDBC全称是Java Database Connectivity它定义了一套Java程序和数据库打交道的标准接口。不管底层是哪种数据库Java代码里看到的都是Connection、Statement/PreparedStatement、ResultSet这几个核心对象。数据库厂商要做的就是提供对应的驱动实现也就是我们常说的mysql-connector-java这个jar包。理解了这一层你再看任何ORM框架眼里看到的都是帮你封装了JDBC的工具。这个迷你图书馆项目好就好在它没有任何框架参与从建表到运行每一行代码都是你自己写的。Connection怎么来的PreparedStatement怎么传参ResultSet怎么一行行读资源什么时候关闭每一个细节都逃不掉。把这些摸透了往后用框架就是享受而不是猜谜。1.2 项目覆盖了哪些高频知识点做练习项目最怕两件事一是太简单练不到东西二是太复杂还没练就放弃了。图书馆管理系统恰好卡在中间功能需求明确但代码量不大一张表就能撑起全部业务。拆开看它其实覆盖了这么几块数据库设计建库建表、字段类型选择、JDBC标准流程连接、执行、处理结果集、SQL增删改查特别是LIKE模糊查询和WHERE条件的写法、Java基础整合实体类封装、集合存储、Scanner交互菜单、try-with-resources资源管理。如果再把退出功能做成循环菜单的跳转逻辑连程序控制流的练习也一并覆盖了。我把这个项目的知识点和对应价值整理了一下模块练到的内容对应Java/数据库知识建库建表字段类型、主键、字符集DDL语句、设计思维DBUtil工具类注册驱动、获取连接、关闭资源JDBC核心流程查询功能结果集遍历、实体映射ResultSet、集合List新增功能占位符传参、返回影响行数PreparedStatement、executeUpdate修改功能按主键更新、判断更新结果SQL UPDATE、受影响行数删除功能主键定位、物理删除SQL DELETE退出功能循环菜单、分支控制while switch这张表你可以当作学习清单做完一项打一个勾心里会非常有底。很多新手喜欢一上来就照着大项目抄结果被异常处理、分页逻辑、多表关联劝退反过来一个只包含七个模块的小项目反而能让你在一两天内跑通全流程成就感是实打实的。2. 立项准备表设计、驱动版本和工程骨架2.1 图书馆表设计一张表就够了先明确一下练习项目不要过度设计。图书管理系统核心就是管理书这个实体所以我只建一张book表。如果硬要加读者表、借阅表那是给有基础的进阶项目准备的新手第一版别搞。项目需求列得清清楚楚查询、新增、修改、删除、退出这五个功能一张表完全撑得住加了多余的表反而干扰你对JDBC本身的注意力。我用的建表语句CREATE DATABASE IF NOT EXISTS library_db DEFAULT CHARACTER SET utf8mb4; USE library_db; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键自增, book_name VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) NOT NULL COMMENT 作者, price DECIMAL(6,2) COMMENT 价格, publisher VARCHAR(100) COMMENT 出版社, stock INT DEFAULT 0 COMMENT 库存数量, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 入库时间 );逐个说下表里的字段为什么这么定。id用INT自增主键这是最简单的做法不用在Java里生成数据库自动管理新增时直接不传这个字段就行。book_name和author给的是定长字符串100和50是我拍脑袋定的对练习够用实际项目再按业务调整。price用DECIMAL(6,2)而不是FLOAT或DOUBLE这是钱相关的字段铁律浮点数在计算机里本身有精度误差图书定价满天都是9.9、39.8这种两位小数用DOUBLE存出来可能是39.799999账对不上。DECIMAL(6,2)表示最多6位有效数字、小数点后2位撑到9999.99对图书价格足够。stock用INT默认0后来做修改功能时改库存数据比较有感觉。create_time直接让数据库生成省得在Java里维护时间。字符集用utf8mb4这个点要特别说。utf8mb4是utf8的超集除了能存中文还能存emoji之类的四字节字符。MySQL 8默认就是utf8mb4但如果你用的是老版本或者自己手工建库很可能会建出utf8出来。本身存中文没问题但为了少踩坑建库语句里最好显式写上DEFAULT CHARACTER SET utf8mb4。至于索引练习阶段不建等以后练到查询性能优化再说。对应Java端的目录结构我习惯在练习阶段就按小工程分层虽然代码少但能培养包管理的习惯library-system/ ├── lib/mysql-connector-java-8.0.33.jar └── src/ ├── com/library/entity/Book.java ├── com/library/util/DBUtil.java ├── com/library/dao/BookDAO.java └── com/library/app/Main.javaentity包放实体类util包放工具类dao包放数据库操作类app包放主程序入口。这个分层看起来绕其实就是把数据长什么样怎么连数据库对数据库做什么操作用户怎么交互四件事分开也是以后做任何项目的骨架意识。2.2 驱动版本和JDK版本怎么选这一步虽然简单却是新手第一道坎选错驱动版本后面全是坑。现在主流是MySQL 8.x对应的驱动包是mysql-connector-java 8.x。如果你用的是MySQL 5.x那用mysql-connector-java 5.1.x更省事。这里有个非常关键的区别驱动类名不一样。MySQL 5时代用Class.forName(com.mysql.jdbc.Driver)MySQL 8开始改成Class.forName(com.mysql.cj.jdbc.Driver)。很多网上老教程还在写旧类名新驱动用它虽然还能跑通但会打一条警告所以我建议直接按新写法来。JDK方面8或以上都行。JDBC是JDK内置的java.sql包不需要额外装东西你只需要把一个jar包放进项目的classpath里。用开发工具操作的话把mysql-connector-java的jar包复制到项目根目录的lib文件夹然后右键选择Add as Library即可。连接URL我建议从一开始就写成这样jdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8拆开看localhost:3306是数据库地址和端口library_db是库名后面三个参数分别是关闭SSL、指定时区、指定字符编码。useSSLfalse在本地练习时一定要加否则会告警甚至报错serverTimezoneAsia/Shanghai是MySQL 8的强制项不加的话驱动会直接抛时区异常characterEncodingutf8保证中文读写不乱码。这串东西现在背下来以后每次写JDBC都用得上。提示驱动jar包一定要和MySQL服务版本匹配版本差太多的典型特征是能加载驱动但连接报协议错误。本地练习优先用同代版本省去排查烦恼。3. 别急着写功能先把连接放大镜式拆开看我在带新人时发现一个规律凡是一上来就照着教程写增删改查的人十个里有八个会卡在连接这一步因为这条链路涉及的信息太多了。所以我把DBUtil单独拎出来讲这个工具类写好了后面四个功能全是重复劳动。3.1 注册驱动到底在做什么很多教程会把注册驱动这行代码放在工具类的最前面新手往往照抄了事以为自己写了个寂寞。我不想让你这样所以单独把它拎出来讲清楚。先看这行最常见的代码Class.forName(com.mysql.cj.jdbc.Driver);Class.forName的作用是让JVM把指定的类加载进内存而MySQL的Driver类里有一个静态代码块会在类加载时自动执行DriverManager.registerDriver来注册自己。也就是说这行代码的本质是触发驱动类的加载和注册。到了较新的驱动包且JDK支持SPI之后这一步其实可以省略驱动会被自动加载但保留Class.forName依然是兼容性最好的写法我建议新手阶段别省。它写在那也提醒你我正在用MySQL驱动。顺带说一句很多人会把驱动加载、连接获取、异常处理全塞在main方法里程序一运行报错就整屏红字根本分不清哪一段出了岔子。拆成DBUtil工具类之后问题定位就清楚多了连接不上先看DBUtilSQL写错先看DAO界面交互不对先看Main。这个按职责拆类的思路本身就是工程化的起点。3.2 DBUtil工具类的写法连接封装成工具类核心是两件事提供一个统一的连接入口、提供一个可靠的关闭入口。我的写法如下package com.library.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的数据库密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(找不到MySQL驱动请检查jar包, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }PASSWORD那里直接写明文练习项目没关系到后面我可以再讲怎么挪到配置文件里。close方法我刻意保留了三个if判断虽然啰嗦但新手看着直观——ResultSet、Statement、Connection的关闭顺序是从内到外先关结果集再关语句对象最后关连接。这和它们之间的依赖关系正好相反先释放内层再释放外层资源才不会被错误占用。3.3 资源泄漏练习时感觉不到实战时会疯JDBC最容易被忽视的就是资源关闭。你可能会想程序退出不就完事了吗问题是练习程序有循环菜单用户操作完查询还要继续新增连接如果不关下一次操作又会开一条新连接。数据库连接是重量级资源背后是真实的网络连接和数据库会话开再多不关数据库端迟早会打满连接数然后你会在控制台看到Too many connections。所以我在DAO层统一用try-with-resources写法。这个语法从Java 7就有了只要资源实现了AutoCloseable接口就能在try块结束后自动关闭比手写finally还稳当。比如查询方法try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { // 遍历结果集 }注意顺序从左到右依次声明结束后按逆序自动关闭。也就是说rs先关、ps再关、conn最后关正好符合前面说的关闭顺序。这一招如果是手写finally要写好几层用try-with-resources一行都不用多写。4. 四个功能逐个落地查询、新增、修改、删除的编码细节4.1 先用菜单把流程结构建起来功能再多入口就是一个循环菜单。我的Main类长这样package com.library.app; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc new Scanner(System.in); BookDAO dao new BookDAO(); boolean running true; while (running) { System.out.println( 迷你图书管理系统 ); System.out.println(1. 查询图书); System.out.println(2. 新增图书); System.out.println(3. 修改图书); System.out.println(4. 删除图书); System.out.println(5. 退出系统); System.out.print(请选择操作); int choice sc.nextInt(); switch (choice) { case 1 - dao.listAll(); case 2 - addBook(sc, dao); case 3 - updateBook(sc, dao); case 4 - deleteBook(sc, dao); case 5 - { System.out.println(已退出系统再见); running false; } default - System.out.println(无效选择请重新输入); } } sc.close(); } }这里用的switch箭头语法需要Java 14以上如果你用的是Java 8改成冒号写法就行。退出功能看着简单其实练了两件事一个是循环的控制变量running另一个是Scanner的关闭时机——很多新手在循环里new Scanner跑几次就报警告正确做法是在main里只创建一次最后用完才close。菜单交互的细节也别小看用户输错数字时给个提示退出时打个招呼这些都会让程序显得像个正经系统。4.2 查询功能ResultSet怎么变成Java对象查询是整个系统里变化最多的部分我先从最简单也最基础的全量查询讲起再给一个按书名模糊查询的写法把SQL里LIKE的用法也带出来。先看一眼实体类它跟book表字段一一对应package com.library.entity; import java.math.BigDecimal; public class Book { private int id; private String bookName; private String author; private BigDecimal price; private String publisher; private int stock; public Book() {} // 省略 getter / setter }全量查询的方法放在BookDAO里public ListBook findAll() { String sql SELECT id, book_name, author, price, publisher, stock FROM book; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setBookName(rs.getString(book_name)); b.setAuthor(rs.getString(author)); b.setPrice(rs.getBigDecimal(price)); b.setPublisher(rs.getString(publisher)); b.setStock(rs.getInt(stock)); list.add(b); } } catch (SQLException e) { e.printStackTrace(); } return list; }ResultSet的游标思维太重要了我必须多说几句。ResultSet刚查出来时游标指向的是第一条记录之前的位置rs.next()把游标往下推一行返回true说明当前有数据再next再推一行直到没有更多记录返回false循环结束。所以while循环体里拿到的分别是第1行、第2行、第3行……每一行通过rs.getInt(id)、rs.getString(book_name)按列名取值。为什么按列名而不是按列下标因为SQL里如果有别名或列顺序变化按列名取值更不容易错代码可读性也高。这个取一行、封装一行、放进List的过程其实就是后来所有ORM框架里实体映射的原始雏形。模糊查询则要配合LIKEpublic ListBook search(String keyword) { String sql SELECT id, book_name, author, price, publisher, stock FROM book WHERE book_name LIKE ? OR author LIKE ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Book b new Book(); b.setId(rs.getInt(id)); b.setBookName(rs.getString(book_name)); b.setAuthor(rs.getString(author)); b.setPrice(rs.getBigDecimal(price)); b.setPublisher(rs.getString(publisher)); b.setStock(rs.getInt(stock)); list.add(b); } } } catch (SQLException e) { e.printStackTrace(); } return list; }LIKE的占位符是重点参数本身不会自动加百分号你要在Java代码里拼成%关键字%。很多人第一次写容易漏结果变成精确匹配搜索功能看着没坏但实际上完全不是模糊查询。试的时候可以输入一个字比如三能命中《三国演义》输三体能命中《三体》这才算对。4.3 新增和修改为什么死也不用Statement新增和修改在JDBC里是最能体现参数怎么传的部分也是新手最容易写出拼接字符串的地方。我把新增方法的完整代码放出来重点看占位符的写法public int add(Book book) { String sql INSERT INTO book(book_name, author, price, publisher, stock) VALUES(?,?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getBookName()); ps.setString(2, book.getAuthor()); ps.setBigDecimal(3, book.getPrice()); ps.setString(4, book.getPublisher()); ps.setInt(5, book.getStock()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } return 0; }这里用PreparedStatement而不是Statement的原因除了防SQL注入之外还有两层一是SQL提前预编译数据库端可以复用执行计划循环插入时性能更好二是参数和SQL结构分离代码可读性高不用手拼字符串。很多新手会纠结写了个练习项目还要考虑SQL注入吗我的回答是练习项目更要养成用占位符的习惯因为这是肌肉记忆将来写生产代码才不会下意识拼字符串。executeUpdate的返回值是受影响的行数。新增成功返回1失败返回0这个方法让调用方知道有没有真的写进去。控制台里读入书名作者等信息的逻辑写在Main里封装成一个Book对象再传给DAO这样DAO层只关心数据操作不关心用户交互职责清晰。修改方法和新增相比代码结构几乎没有变化最大的区别在于SQL语句从INSERT变成了UPDATE并且WHERE条件里带了id主键。一定要看清楚参数设置的顺序public int update(Book book) { String sql UPDATE book SET book_name?, author?, price?, publisher?, stock? WHERE id?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, book.getBookName()); ps.setString(2, book.getAuthor()); ps.setBigDecimal(3, book.getPrice()); ps.setString(4, book.getPublisher()); ps.setInt(5, book.getStock()); ps.setInt(6, book.getId()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } return 0; }注意WHERE id?的占位符是最后一个set的初学最容易犯的错就是把id放在第一个set那样SQL语义就全错了。修改前从控制台读入id再读入新的书名作者等信息先update再判断返回值如果返回0基本可以断定这个id不存在或者数据没有变化可以提示用户。这样用户交互就友好很多而不是静默失败。4.4 删除功能物理删除和逻辑删除的取舍删除是四个功能里代码量最少的逻辑上也是最直接的——按主键定位一行然后删掉。正因为代码短很多人才会忽略它在真实业务里的设计取舍。先看代码public int delete(int id) { String sql DELETE FROM book WHERE id?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } return 0; }但这里我想多讲一句项目设计层面的东西生产环境里业务数据很少直接DELETE而是用逻辑删除——给表加一个is_deleted字段默认0删除时执行UPDATE book SET is_deleted1 WHERE id?查询时全部带上WHERE is_deleted0。这样数据还在库里随时能恢复也方便审计。练习项目用物理删除完全没问题因为你练的是JDBC而不是业务。但我建议你写完物理删除后自己动手把它改成逻辑删除试试——不用改太多加个字段、改两条SQL而已感受一下两种方案在查询和业务上的差异这个思考过程比多写十个功能还有价值。删除操作前也最好再提示一次确认删除吗避免误删这属于交互体验的小细节以后做真实项目会被经常问到的。注意删除和修改前都建议用SELECT确认目标记录存在比如SELECT COUNT(1) FROM book WHERE id?。虽然返回受影响行数也能判断但先查一次再操作交互上更稳妥也顺手练了COUNT聚合查询。5. 跑起来之后典型报错排查链路与值得加码的改进我第一次带人跑这个项目时常见报错就那么几类基本把新手能踩的坑都覆盖了。我按排查链路给你排一个顺序遇到问题从上到下查比瞎试快得多。5.1 五类高频异常的排查顺序异常表现根本原因排查处理ClassNotFoundException: com.mysql.cj.jdbc.Driver驱动jar没引入或类名写错检查lib目录下有没有mysql-connector-java检查是否Add as Library加入classpathCommunications link failure数据库没启动、端口错误、IP连不上启动数据库服务确认3306端口没被占用确认URL里的地址端口正确Access denied for user rootlocalhost用户名或密码错误核对DBUtil里的USER和PASSWORD注意密码含特殊字符的处理Unknown database library_db库名写错或建库SQL没执行在数据库命令行执行SHOW DATABASES确认库名The server time zone value is unrecognizedMySQL 8时区问题在URL加serverTimezoneAsia/Shanghai这里要特别提醒一个隐蔽问题Java和数据库之间可能出现连接成功但一执行SQL就报Table library_db.book doesnt exist。原因通常是你在另一个数据库实例里建了表或者建表语句没有在正确的库下执行。在命令行里USE library_db之后再SHOW TABLES确认一下或者在数据库可视化工具里看左侧的树形结构确认book表确实在library_db下面。这个坑我见过不下五次全是粗心导致的。5.2 中文乱码的三个源头中文乱码在图书管理系统里是必定会遇到的问题因为书名、作者全是中文。出现乱码时按下面三个源头逐层查。第一层数据库本身。建库时有没有指定utf8mb4如果库已经建成utf8了可以执行ALTER DATABASE library_db CHARACTER SET utf8mb4再建表时指定DEFAULT CHARSETutf8mb4。第二层连接URL。JDBC连接字符集没指定的话驱动可能按默认编码传输中文在Java和数据库之间来回转换就变成问号了。确保URL里有characterEncodingutf8。第三层操作系统或开发环境的控制台编码。有些开发环境的控制台默认GBK处理UTF-8的中文会乱码这时候把控制台编码、工程编码全部设为UTF-8即可。这一层特别容易忽略因为你会发现数据库里中文正常Java里打印出来却乱码。排查顺序建议从数据库往代码方向走先看表数据正常不正常再判断是传输层还是展示层出问题。不要一看到乱码就去改代码先查数据源头能省一半时间。5.3 三个锦上添花的小改进如果你把上面四个功能跑通还有富余精力我推荐加下面三个改进难度都不大但能让项目直接上一个档次。第一把连接配置挪到properties配置文件里。DBUtil里写死的URL、用户名、密码改到一个jdbc.properties文件用java.util.Properties加载。这样换环境改配置不用改代码也顺便练了Java读取配置文件的IO操作。第二给DAO加一个判断图书是否存在的方法比如boolean exists(int id)。在修改和删除前先校验配合之前说的受影响行数判断交互会更完整。这也在练SQL的SELECT判断写法SELECT COUNT(1) FROM book WHERE id?然后rs.getInt(1) 0顺带熟练COUNT聚合。第三查询结果在控制台输出时统一用printf格式化对齐。书名、作者、价格列宽不一致的时候控制台输出乱糟糟的System.out.printf(%-5d %-20s %-10s %-10.2f %-10d%n, ...) 这种格式能让输出整齐很多看着也专业。注意Book里的price是BigDecimal格式化时先转成double或者用toPlainString不然会踩类型转换的坑。5.4 一套完整的运行回归步骤项目写完不能就算完了建议按下面这个回归清单过一遍确认五个功能闭环。第一步启动程序进入菜单选择1查询空表时提示暂无图书。第二步选择2新增填书名、作者、价格、出版社、库存用SQL查询确认数据写入。第三步再次选择1查询确认新记录以列表形式打印出来。第四步选择4删除输入刚才记录的id回到查询确认删除生效。第五步选择3修改把某条记录的库存改成66查询确认修改生效。第六步选择5退出确认程序正常结束资源关闭没有异常堆栈。别小看这个回归清单很多功能单独写能跑连起来跑就出问题。比如有人新增时id自增不传没问题但某次手滑手动传了id就会主键冲突有人删除后查询正常但紧接着修改又会因为连接复用出状况。一口气把整个闭环走一遍项目才算真正交付而不是每个方法各跑了一次。最后说点个人体会。这个项目我写过三遍第一遍是刚学JDBC时照葫芦画瓢第二遍是工作后为了带新人重新整理第三遍是这次又快速撸了一遍。每一次写都有新的理解比如之前没注意到的字符集问题比如try-with-resources对关闭顺序的自动保证。所以我不太赞同项目简单就没必要做的说法恰恰是这种简单让你能一眼看穿数据从Java到数据库再回来的全过程。你把DBUtil和四个功能吃透往后学连接池、学持久层框架都会觉得这些东西眼熟。真要说有什么建议别复制我的代码打开编辑器一个字一个字敲把密码输错一次把WHERE id漏掉一次这些错犯过了你才算真正会写JDBC。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑