news 2026/10/17 17:18:00

DBUtils核心QueryRunner与DBCP连接池:简化JDBC开发实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DBUtils核心QueryRunner与DBCP连接池:简化JDBC开发实战

写 SQL 不难,难的是把每次查询前前后后的连接获取、预编译语句、参数绑定、结果集映射、资源关闭这套“管道工程”重复几十遍还不出错。这也是我当年从原生 JDBC 转向 DBUtils 的起因。这个系列走到第三十篇,正好把两块内容放在一起:DBUtils 中的 QueryRunner 核心类,以及 DBCP 连接池。前者负责把“执行 SQL 并返回结果”这个过程从里到外简化,后者负责把数据库连接的创建和复用管起来。这两样搭配,是很多中小型 Java 项目从 JDBC 裸写走向更工程化做法的第一步,也是理解更重 ORM 框架前非常值得吃透的一环。

读这篇内容的,我默认是这样一类人:已经能用 JDBC 完成简单的增删改查,但每写一个 DAO 都要重复一堆 try-catch-finally,面对数据库连接的管理有点力不从心,或者准备在项目里引入连接池却只知道大概意思。如果你处于这个阶段,这篇文章基本可以按目录直接跟着做,代码都是可以落地的,参数也是我实际验证过的。我先说下整体思路:DBUtils 和连接池是两个独立的东西,但它们解决的是同一类问题——让程序员把精力放在 SQL 本身。DBUtils 做的是消除样板代码,连接池做的是消除频繁创建物理连接的开销。理解了这个分工,后面所有细节都不是死记 API,而是围绕“哪个环节省掉了什么”来记忆。

1. 这个方案到底解决了什么问题

1.1 原生 JDBC 的复杂度来自哪里

很多新人在初学 JDBC 时会觉得:不就是DriverManager.getConnection()、Statement.execute()、ResultSet.next()三步走吗?但实际上手写一个完整 DAO 就会发现,真正写业务逻辑的时间不到三分之一,剩下的时间全在写资源管理和异常处理。一个典型的查询方法长这样:加载驱动、获取连接、准备语句、循环设置参数、执行查询、逐行从ResultSet里按索引或者按列名取值、构造对象、关掉 ResultSet、关掉 Statement、关掉 Connection,最后还要在整个方法外面套一层 try-catch,避免连接泄漏。

这还只是单个查询方法。如果一张表有十来个字段,一个模块有十来个查询,代码就开始失控了。最容易出错的地方集中在两个环节:一是参数绑定。用Statement拼接字符串 SQL,一旦值里包含单引号或特殊字符,轻则 SQL 报错,重则构成注入风险。改成PreparedStatement后,虽然参数安全了,但每个方法都要写一堆setString、setInt,字段一多就变得非常机械。二是结果映射。Java 对象的属性与数据库列名往往不完全一致,手动从ResultSet里 get 出来再 set 进对象,写多了就是纯粹的体力活。DBUtils 正是针对这两个痛点设计的:它把参数绑定简化成可变参数Object...,把结果映射简化为一个接口ResultSetHandler<T>,资源关闭则完全交给框架内部处理。

1.2 DBUtils 的定位:不抢 SQL 的 ORM 框架

这里要说清楚一件容易混淆的事:DBUtils 不是 MyBatis 或者 Hibernate 那种完整 ORM 框架。它不会替你生成 SQL,不会做表关系映射,更不会管缓存和懒加载。它的核心价值只有一个——把 JDBC 的固定流程封装起来,让 SQL 和参数暴露给你,把结果转换交还给你。你可以把它理解成“JDBC 的工具箱”,而不是“数据访问的全能框架”。这也是很多老项目愿意引入它的原因:改造代价小,对已有 SQL 零侵入,开发人员仍然完全掌控数据库交互过程。

在实际业务里,DBUtils 最常见的应用场景是工具类、报表模块、批量数据迁移脚本,以及作为学习 ORM 思想前的过渡阶段。它没有复杂的生命周期和代理机制,调试起来非常直观。比如某个统计报表的 SQL 写了三四个 join,MyBatis 里要配置 resultMap,而 DBUtils 中直接写一个MapHandler或自定义ResultSetHandler,拿到的就是一个 Map 或者自定义对象,没有配置文件的负担。这个特性让它在一部分场景下反而比重量级框架更顺手。

1.3 连接池在架构里的真实角色

说完 DBUtils,再看连接池。Java 的标准 JDBC 连接获取方式是DriverManager.getConnection(),每次调用都会经历建立 TCP 连接、数据库身份验证、分配会话资源等一套完整流程。在高并发场景下,频繁创建和销毁连接的开销非常可观,甚至可能超过 SQL 本身执行的时间。连接池的思路很简单:预先创建一批连接放在池子里,应用来借的时候给一个,用完还回来,而不是销毁。池子还能控制连接总数,防止某个模块把数据库连接占满拖垮整个应用。

DBCP 是 Apache Commons 下的连接池实现,它实现了javax.sql.DataSource接口。在 Java 里,DataSource是获取连接的标准入口,而 DBCP 的BasicDataSource是这个接口最经典的实现之一。引入 DBCP 之后,项目中几乎所有获取连接的代码都能从DriverManager切换到DataSource.getConnection(),而业务层感知不到区别。这也为后续切换到 Druid、HikariCP 等其它连接池留下了余地,因为上层依赖的是标准接口。

2. QueryRunner 核心类:从方法签名到执行流程

2.1 QueryRunner 的两种构造方式,决定了谁管连接

QueryRunner 是 DBUtils 最核心的类,它把所有 JDBC 操作浓缩成几组方法。使用它之前,先要搞懂构造函数。QueryRunner有两种常见构造方式:无参构造,以及传入一个DataSource。

// 方式一:无参构造+显式传 Connection QueryRunner runner1 = new QueryRunner(); // 方式二:传入 DataSource,由 QueryRunner 自己获取和归还连接 QueryRunner runner2 = new QueryRunner(dataSource);

这两种方式的使用差异很容易被初学者忽略,但实际上非常关键。方式一调用方法时,第一个参数必须传Connection,执行完后这个连接不会被 QueryRunner 关闭,谁传进来的谁负责关。方式二则完全不同,调用时会自动从 DataSource 获取一个连接,执行完 SQL 后自动关闭(归还给连接池),业务代码里不需要碰 Connection 对象。

提示:如果你用无参构造,又在 finally 里写了conn.close(),那没问题;但如果你用 DataSource 构造,还额外去手动调用conn.close(),反而可能搞乱连接池的状态或者抛 Already closed 异常。两种方式选一种,不要混用。

我在开发中一般有两种使用习惯:一个 DAO 的常规查询用DataSource构造,代码极短;涉及事务的写操作则用无参构造加显式Connection,因为事务需要多个操作共用同一个连接。

2.2 增删改操作:update 方法的重载与返回结果

增删改在 DBUtils 中统一由update方法承担。这个方法的返回值是int,表示受影响的行数。常见的几种重载形式如下:

方法签名使用场景
update(String sql)无参数的更新语句
update(String sql, Object... params)带参数的更新语句,DataSource 构造时使用
update(Connection conn, String sql, Object... params)手动传入连接,事务场景使用
update(Connection conn, String sql, Object param)只有一个参数的便捷版本

看一个实际例子。假设用户表t_user有三个字段:id、name、email。插入一个新用户用下面的代码:

QueryRunner runner = new QueryRunner(dataSource); String sql = "INSERT INTO t_user(name, email) VALUES(?, ?)"; int rows = runner.update(sql, "张三", "zhangsan@example.com");

这段代码背后的流程是:从 DataSource 拿连接 → 创建PreparedStatement→ 把"张三"和邮箱逐个填入占位符 → 执行 executeUpdate → 关闭语句和连接。原本十几行的代码,现在一行搞定。当 SQL 需要动态拼接时,只需要保证?的顺序和参数数组的顺序一致,DBUtils 会自动处理类型转换,你传进去的是字符串就 setString,是整数就 setInt。如果你做的是删除或更新,方式一模一样,只是 SQL 不同。

2.3 查询操作:ResultSetHandler 才是灵魂

查询方法和增删改长得不太一样。因为查询除了执行 SQL,还要决定“结果集怎么变成 Java 对象”。query方法的签名大致是:

<T> T query(String sql, ResultSetHandler<T> rsh, Object... params)

ResultSetHandler<T>是一个泛型接口,它的作用是把ResultSet转换成你想要的目标类型 T。DBUtils 自带了好几个现成的实现类,覆盖了绝大多数需求场景。我把它们整理成一张表:

实现类返回类型适用场景
ScalarHandler<T>单行单列的值聚合查询,如SELECT COUNT(*)
BeanHandler<T>单个 JavaBean查询一条记录映射到对象
BeanListHandler<T>List<T>查询列表映射到对象列表
MapHandlerMap<String,Object>查询一条记录但不想建 VO 类
MapListHandlerList<Map<String,Object>>动态报表、多表 join 查询
ColumnListHandler<T>List<T>取某一列的所有值
KeyedHandler<T>Map<Object, Map<String,Object>>按某列做 key 的字典查询
ArrayHandlerObject[]每条记录映射成数组
ArrayListHandlerList<Object[]>多条记录,每条一个数组

我最常用的三个是ScalarHandler、BeanListHandler和MapListHandler。看代码:

// 查询单个用户 String sql = "SELECT id, name, email FROM t_user WHERE id = ?"; BeanHandler<User> handler = new BeanHandler<>(User.class); User user = runner.query(sql, handler, 1001); // 查询所有用户列表 String listSql = "SELECT id, name, email FROM t_user"; List<User> userList = runner.query(listSql, new BeanListHandler<>(User.class)); // 查询总人数 String countSql = "SELECT COUNT(*) FROM t_user"; Long total = runner.query(countSql, new ScalarHandler<Long>());

这里有一个特别容易被坑的点:BeanHandler和BeanListHandler在做字段映射时,要求数据库列名与 JavaBean 属性名保持一致。如果数据库列名是user_name,Java 属性是userName,默认映射会失败,得到的就是 null。解决办法有两个:一是给 SQL 起别名,SELECT user_name AS userName;二是使用BasicRowProcessor配合BeanProcessor开启下划线转驼峰。第二种方式更一劳永逸,我后面实操部分会给出示例。

2.4 批量操作:batch 方法处理多条数据

当需要一次性插入或更新多条记录时,一条一条执行update效率太低。DBUtils 提供了batch方法,底层使用 JDBC 的批量提交机制,网络往返次数大幅下降。它接收一个二维数组,外层是每条记录的参数,内层是单条记录的所有参数。

String sql = "INSERT INTO t_user(name, email) VALUES(?, ?)"; Object[][] params = new Object[][] { {"用户A", "userA@example.com"}, {"用户B", "userB@example.com"}, {"用户C", "userC@example.com"} }; int[] rows = runner.batch(sql, params);

返回值int[]的每个元素对应一条记录的受影响行数。如果其中有某条数据执行失败,默认情况下会抛出SQLException,之前成功执行的记录不会被自动回滚。这里要特别提醒:如果你需要批量操作具备事务性,就必须手动传入同一个Connection,并且在调用前关闭自动提交。batch(Connection conn, String sql, Object[][] params)就是为事务场景准备的。

3. DBCP 连接池的接入与参数调优

3.1 连接池的基本工作逻辑

在配置 DBCP 之前,先把连接池的内部模型说透。一个连接池本质上维护了一个连接集合,对外提供借出和归还两个动作。应用调用dataSource.getConnection()时,池子先看有没有空闲连接,有就借给你;没有就看当前总连接数是否达到上限,没到就新建一个;到了上限就等待,等别的连接归还,或者等超过最大等待时间抛异常。这个“等待”机制非常重要,它决定了系统在连接不够用的时候是排队还是报错。

DBCP 中的核心类是BasicDataSource。它非常重,内部维护了连接池、空闲连接驱逐线程、JMX 注册等大量设施,所以不要在每次方法调用里 new 一个,应该作为应用级的单例。常见做法是在一个独立的数据源工具类里创建,或者在 ServletContextListener 里初始化一次。

3.2 核心配置项的心得

DBCP 的配置项很多,但真正需要关心的是下面这几个:

参数建议值作用与心得
initialSize5启动时预创建的连接数。设太小,高并发一到就疯狂建连接;设太大,冷启动时间变长且浪费资源。
maxTotal20最大连接数,这是保护数据库的顶线。具体值要结合数据库max_connections计算,别超过它。
maxIdle10最大空闲连接数。空闲太多会占用数据库资源,超过的部分会被逐步回收。
minIdle2最低空闲连接数,防止流量低谷后连接全被回收,突然来请求又要重新建连接。
maxWaitMillis3000借连接时最多等多久。设 -1 会导致线程无限等待,应用卡死很难排查,建议必设。
testOnBorrowtrue借出连接前做有效性检查,宁可多一次验证 SQL 的开销,也别把坏连接发给业务。
validationQuerySELECT 1验证连接是否有效的 SQL,MySQL 用SELECT 1,Oracle 要改成SELECT 1 FROM DUAL。

我通常还会加上两组参数:一是连接空闲回收,minEvictableIdleTimeMillis设置为 1800000,也就是 30 分钟,让空闲过久的连接被主动关掉,防止数据库端超时断开形成“半死连接”;二是取消除连接池自身的 JMX 以外的一些调试输出,避免日志刷屏。配置的完整代码大概是这样的:

BasicDataSource dataSource = new BasicDataSource(); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/db_demo?useUnicode=true&characterEncoding=utf8"); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); dataSource.setMinIdle(2); dataSource.setMaxIdle(10); dataSource.setMaxTotal(20); dataSource.setMaxWaitMillis(3000); dataSource.setTestOnBorrow(true); dataSource.setValidationQuery("SELECT 1"); dataSource.setMinEvictableIdleTimeMillis(1800000);

注意:URL 里的useUnicode=true&characterEncoding=utf8一定要带上,否则中文很容易出现乱码。虽然新版驱动对编码的默认行为有改进,但显式声明是成本最低的自保手段。

3.3 与 QueryRunner 集成

DBCP 和 DBUtils 的集成不需要任何第三方桥接类,因为 QueryRunner 接受DataSource,而BasicDataSource实现了java.sql.DataSource。直接 new 进去即可:

public class DataSourceUtil { private static final BasicDataSource dataSource = new BasicDataSource(); static { // 这里填上面那段 BasicDataSource 配置 } public static DataSource getDataSource() { return dataSource; } } // 在 DAO 中使用 DataSource ds = DataSourceUtil.getDataSource(); QueryRunner runner = new QueryRunner(ds);

集成完成之后,DAO 里面就不再有获取连接的代码了。连接从哪来、何时关闭,全部由 QueryRunner 内部处理。这带来的一个额外好处是:后续如果想换连接池,比如从 DBCP 换成 HikariCP,只需要改DataSourceUtil一个类,DAO 层的代码完全不动。

4. 实操过程:一个用户管理模块的完整实现

4.1 环境准备与依赖版本

这次实操我用的环境是 Java 8、MySQL 8.0、Maven 3.6。依赖方面需要三样:commons-dbutils、commons-dbcp2,以及 MySQL 驱动。如果是老项目还在用 DBCP 1.x,包名是org.apache.commons.dbcp,配置类叫BasicDataSource但底层实现差异不小,建议新项目直接用 DBCP 2.x。Maven 依赖如下:

<dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.8.1</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-dbcp2</artifactId> <version>2.10.0</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency>

数据库表结构非常简单:

CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50) NOT NULL, email VARCHAR(100) );

这里我故意把字段名设计成user_name而不是userName,就是为了演示下划线转驼峰的配置方法。对应的实体类User属性名是userName。

4.2 DAO 层的常规查询与写入

先处理映射问题。BeanListHandler和BeanHandler默认的字段匹配是精确匹配,user_name匹配不到userName。解决方式是创建一个全局的RowProcessor,交给每个 Handler 使用:

BeanProcessor beanProcessor = new BeanProcessor(true); // 开启下划线转驼峰 BasicRowProcessor rowProcessor = new BasicRowProcessor(beanProcessor); // 定义 DAO public class UserDao { private QueryRunner runner = new QueryRunner(DataSourceUtil.getDataSource()); public User findById(int id) throws SQLException { String sql = "SELECT id, user_name, email FROM t_user WHERE id = ?"; return runner.query(sql, new BeanHandler<>(User.class, rowProcessor), id); } public List<User> findAll() throws SQLException { String sql = "SELECT id, user_name, email FROM t_user"; return runner.query(sql, new BeanListHandler<>(User.class, rowProcessor)); } public int insert(User user) throws SQLException { String sql = "INSERT INTO t_user(user_name, email) VALUES(?, ?)"; return runner.update(sql, user.getUserName(), user.getEmail()); } public int update(User user) throws SQLException { String sql = "UPDATE t_user SET user_name = ?, email = ? WHERE id = ?"; return runner.update(sql, user.getUserName(), user.getEmail(), user.getId()); } public int delete(int id) throws SQLException { String sql = "DELETE FROM t_user WHERE id = ?"; return runner.update(sql, id); } }

注意insert和update方法执行完只是返回受影响行数,并不会自动填充自增主键。如果你需要在插入后立刻拿到主键,就要在update之外另想办法。DBUtils 1.6 版本开始提供了insert方法,配合GeneratedKeysHandler使用,也能拿到自增主键,但实际项目中我更习惯用INSERT ... RETURN_GENERATED_KEYS配合自定义ResultSetHandler处理,这里不展开,免得把主线带偏。

4.3 手工事务的正确姿势

前面提到凡是带Connection参数的方法,QueryRunner 不会自动关闭连接,这正是事务控制的切入点。以转账为例,完整的操作应该是:从 DataSource 拿一个连接,关闭自动提交,执行扣款和入账两个 update,全部成功再 commit,任何一个失败都 rollback,最后把连接还回池里。

public void transfer(int fromId, int toId, BigDecimal amount) throws SQLException { Connection conn = null; QueryRunner runner = new QueryRunner(); try { conn = DataSourceUtil.getDataSource().getConnection(); conn.setAutoCommit(false); String sql = "UPDATE t_account SET balance = balance - ? WHERE id = ?"; runner.update(conn, sql, amount, fromId); sql = "UPDATE t_account SET balance = balance + ? WHERE id = ?"; runner.update(conn, sql, amount, toId); conn.commit(); } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }

这里有一个非常容易踩的坑:如果 finally 里直接把连接 close 而不先把 autoCommit 恢复成 true,连接还回池子后仍然处于事务未提交状态。下一次业务代码从池子里借到这条连接,可能会把上一个事务未提交的数据一起带出来,或者因为残留的事务状态导致诡异的行为。虽然 DBCP 在归还连接时通常会做状态重置,但不同版本的行为不完全一致,自己手动恢复是最稳妥的。

提示:如果你只执行一条 DML,不需要手动开事务,用 DataSource 构造的 QueryRunner 一条 update 足够。事务只用于多条 SQL 必须同时成功或同时失败的场景,不要把事务当成所有写操作的默认选项。

5. 常见问题与排查技巧实录

5.1 连接池被打满,应用卡在 getConnection

这是高并发场景下最常见的问题。现象是日志里出现Cannot get a connection, pool error TimeoutWaitingForConnection或者Connection is not available, request timed out。排查思路先看监控里的池状态:如果 active 连接数长期接近 maxTotal,说明有连接被借走之后没有归还,也就是连接泄漏。

最常见的泄漏原因有两个。一是代码里拿到了connection但没有在 finally 中关闭。二是使用了带Connection参数的 QueryRunner 方法,误以为框架会负责关闭连接。正确做法是记清楚规则:无参构造 + 显式传 Connection,连接由调用方关闭;DataSource 构造,连接由 QueryRunner 关闭。出现泄漏时,可以开启 DBCP 的removeAbandonedOnBorrow和removeAbandonedTimeout作为兜底,但这两个参数属于“事后补救”,会让正在执行的长事务连接被强行回收,引发别的问题,建议只在排查阶段临时开启。

5.2 Bean 字段映射全是 null

写了BeanHandler或BeanListHandler,查询出来 list 长度正确,但每个对象的所有属性都是 null。第一反应检查数据库列名和实体字段名是否一致。如果列名是下划线风格,实体是驼峰风格,默认是映射不上的。解决方法是给 SQL 起别名,或者像我前面的示例那样配置BeanProcessor(true)开下划线转驼峰。

还有一个容易忽略的细节:实体类必须有一个无参构造器,并且属性要有对应的 setter。DBUtils 的 Bean 映射基于反射,内部靠PropertyDescriptor来调用 setter,如果你为了“简洁”把 setter 删了,运行时会直接报BeanIntrospectionException。很多测试类里面为了方便把字段设为 public,也会踩这个坑。

5.3 隔了一段时间后,第一次请求特别慢或报连接失败

数据库默认会在一段时间后断开空闲连接,比如 MySQL 的wait_timeout默认可能是 8 小时。而连接池这边不知道连接已经失效,仍然把这条连接当作有效连接存着。结果就是应用第二天早上第一个请求拿到一条死连接,轻则连接超时,重则业务报错。解决方案就是我前面配置的testOnBorrow(true)加validationQuery,再加上空闲驱逐参数minEvictableIdleTimeMillis定期清理。

这里补充一个经验:有些团队为了追求性能把testOnBorrow设为 false,只开testWhileIdle,让后台线程定期验证空闲连接。理论上可行,但“定期”就意味着存在验证窗口期,高峰期可能正好借到一条刚过期尚未被验证的连接。我的建议是业务量没大到不可接受时,testOnBorrow保持开启,性能损失微乎其微,换来的是极大的稳定性。

5.4 事务与连接池的边界认知

最后一个问题其实是个认知问题。我在一些项目里看到开发者在 SERVICE 层写了DataSourceUtils.getConnection(),又在 DAO 层用了new QueryRunner(dataSource),结果事务完全没有生效,因为 DAO 拿到的是数据源新建的另一条连接。记住一个原则:事务必须绑定到同一条连接上。要么让 Service 把连接作为参数传到 DAO 的每个方法,要么使用带Connection的 QueryRunner 重载,绝对不能让 DAO 自己从 DataSource 拿新连接。

如果项目里有多处需要这种跨 DAO 的事务,每次都手动传连接会非常啰嗦。DBUtils 本身不提供事务模板,这时我通常建议直接引入 Spring 的DataSourceTransactionManager,或者至少在项目里封装一个类似TransactionTemplate的小工具。不过这已经是另一个话题了,这个系列后面如果有机会,我专门写一篇手撸事务模板的文章。

最后分享一个小技巧。DBCP 的validationQuery不是所有数据库都通用。MySQL 用SELECT 1没问题,Oracle 必须写SELECT 1 FROM DUAL,SQL Server 则建议SELECT 1。这个 SQL 越轻量越好,因为它可能在每次借连接时都被执行,重型 SQL 会把连接池变成慢速查询器。另外,如果你用的是 PostgreSQL,也可以直接SELECT 1,只是要注意部分老版本驱动对setReadOnly等状态的处理有差异,这一点结合实际环境测试为准。我在实际项目中,通常会在连接池初始化之后写一个简单的自检方法,从池子里取一条连接执行一次validationQuery,确认通了再继续加载后续业务模块,这样启动阶段就能暴露数据库配置问题,而不是等到用户请求进来才报错。

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

红色党建风党政工作总结PPT模板:视觉规范与python-pptx批量生成

简介&#xff1a;红色党建风党政部门工作总结PPT模板&#xff0c;专为党政机关与企事业单位年终述职、阶段汇报等场景设计&#xff0c;帮助使用者高效梳理工作目标、展示成果、总结不足并规划未来。模板内置五大标准板块&#xff1a;工作目标与背景、工作成果与亮点、挑战与解决…

作者头像 李华
网站建设 2026/10/17 17:14:13

Unity3D跑酷游戏源码解析:从零搭建到性能优化

简介&#xff1a;这是一份面向Unity3D初学者与独立开发者的跑酷游戏开发实战资源&#xff0c;围绕《Unity3D跑酷游戏开发详解》展开&#xff0c;帮助读者从零搭建可运行的3D跑酷项目。内容覆盖环境搭建与场景设置、角色模型与Animator动画状态机、Rigidbody与Collider物理碰撞、…

作者头像 李华
网站建设 2026/10/17 17:05:35

金融数据服务平台架构实践:实时指标计算与规则引擎设计

前阵子一直在忙一个代号叫 financial-services 内部项目的重构&#xff0c;这个项目说白了就是一套面向金融服务场景的通用数字化服务平台。不是那种给人一种高深莫测感觉的量化交易系统&#xff0c;也不涉及任何复杂金融模型&#xff0c;它更像是一套“运营中台”——负责把分…

作者头像 李华
网站建设 2026/10/17 17:03:06

测绘程序设计大赛RANSAC算法C#实现与调参避坑指南

简介&#xff1a;这份资源面向参加测绘程序设计大赛的高校学生与算法初学者&#xff0c;聚焦2025年国赛选题一中的RANSAC算法实现&#xff0c;提供一套可直接编译运行的C#完整源代码&#xff0c;帮助读者理解随机采样一致性在直线拟合与粗差剔除中的工程落地方式。压缩包共32个…

作者头像 李华
网站建设 2026/10/17 17:00:38

SAP Data Services 连接 MySQL 实战:ODBC 驱动配置与 Data Store 创建

简介&#xff1a;这份文档面向使用 SAP Data Services 进行数据集成的开发与运维人员&#xff0c;聚焦于如何将 MySQL 数据库接入 Data Services 平台&#xff0c;解决异构数据源连接与元数据导入的实际问题。资源包共 1 个 docx 文件&#xff0c;大小约 1.43MB&#xff0c;内容…

作者头像 李华
网站建设 2026/10/17 16:55:30

Qt操作Windows服务实战:封装SCM API实现服务启停与看门狗

简介&#xff1a;面向系统服务管理场景的Qt项目代码包&#xff0c;适合需要在桌面工具中集成服务控制能力的开发者。代码演示了利用Qt框架对系统服务进行创建、删除、打开、关闭、启动、停止等操作&#xff0c;并覆盖服务启动类型设置与信息查询&#xff0c;同时强调管理员权限…

作者头像 李华