JDBC调用MySQL存储过程与存储函数全攻略
JDBC连着连着连接池、事务、批处理都玩过一轮后DAY04最容易被问到的就是到底要不要在Java里调用MySQL存储过程和存储函数我的回答通常是如果你的业务规则足够稳定、改动频率不高JDBC调用存储过程是一把非常趁手的工具如果只是写普通CRUD那大概率你不需要折腾。这一篇我会把“调用存储过程、存储函数”这整条链路拆开从MySQL侧建存储过程到Java侧用CallableStatement调用再到结果集、输出参数、多结果集、常见坑点全部过一遍。不管你是刚学到JDBC的新手还是被项目里“存储过程Java”整得头疼的同学这篇都能给你一份能直接抄作业的参考答案。1. 存储过程这玩意儿为什么非要在JDBC里调1.1 存储过程与存储函数一句话分清很多同学容易把存储过程和存储函数混在一起其实最核心的区别只有三个存储过程用CALL调用存储函数用SELECT调用或者表达式引用存储过程没有返回值只能通过OUT/INOUT参数往回带数据存储函数必须有返回值存储函数适合做“输入一个值、输出一个值”的计算存储过程适合做“一段带流程控制的多步SQL操作”。拿MySQL举例一个存储过程可以包含多条SQL、循环、判断、异常处理而存储函数只能返回单个值不允许使用SELECT直接返回结果集。这个区别直接在JDBC调用时也有体现存储函数用{? CALL func(?)}这种语法存储过程用{CALL proc(?,?)}。1.2 JDBC里不直接拼SQL非要用存储过程图什么以前我也嫌存储过程麻烦Java里写好SQL不香吗后来在几个项目里被教育了几次才真正理解为什么要用存储过程。第一个好处是减少网络往返。有些业务逻辑动不动就五六条SQL比如订单支付要查余额、扣库存、写流水、更新订单状态。如果每步都在Java里单独发一个SQL一次操作可能要4到6个网络往返全部装进存储过程客户端只需要发一次调用请求MySQL内部顺序执行完再一次性返回。内网环境不太明显但跨机房、跨网络时差距立刻出来了。第二个好处是统一逻辑入口。同一个“支付”流程可能在Java端、报表端、定时任务里都会用到如果把逻辑写在存储过程里谁调都是同一份逻辑否则同一套业务规则分散在多个系统的代码里维护成本翻倍。第三个好处是权限控制。我们可以让Java应用只连一个低权限账号只允许它执行存储过程不允许直接操作表。这样即使Web层被入侵或者业务代码写错也摸不到底层表结构。当然这些都是“能用”的必要条件不是“必须用”的铁律。到底什么时候用我会留到最后一节再讲。2. 写Java之前先把MySQL侧的存储过程建好2.1 先造一张能用的表为了说明白咱们用电商最常见的两张表用户表和订单表。CREATE DATABASE IF NOT EXISTS demo_jdbc DEFAULT CHARSET utf8mb4; USE demo_jdbc; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, register_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO t_user (username) VALUES (张三), (李四), (王五); INSERT INTO t_order (user_id, amount, status) VALUES (1, 99.50, 1), (1, 39.00, 0), (2, 199.00, 1), (2, 59.00, 1), (3, 1000.00, 0);这里的t_user是用户主数据t_order是订单流水。后面所有的存储过程都用这两张表。2.2 建一个带IN和OUT参数的存储过程现在建一个“根据用户ID统计订单总金额和订单数量”的存储过程输入用户ID输出两个值订单总数、总金额。USE demo_jdbc; DROP PROCEDURE IF EXISTS get_order_stat_by_user; DELIMITER $$ CREATE PROCEDURE get_order_stat_by_user( IN p_user_id INT, OUT p_order_count INT, OUT p_total_amount DECIMAL(10,2) ) BEGIN SELECT COUNT(*), IFNULL(SUM(amount), 0.00) INTO p_order_count, p_total_amount FROM t_order WHERE user_id p_user_id; END$$ DELIMITER ;要注意MySQL存储过程的参数顺序非常关键JDBC端的参数索引是从1开始的而IN参数索引和OUT参数索引都参与排序。这个过程中p_user_id是第1个p_order_count是第2个p_total_amount是第3个。后面Java注册输出参数时必须按这个顺序注册第2和第3个不能错位。另外我的个人习惯是在存储过程里尽量用IFNULL处理聚合结果。为什么因为COUNT本身不会返回NULL但SUM在没有匹配行时一定返回NULL如果JDBC端直接getBigDecimal一个NULL很容易拿到NullPointerException。所以我在SUM外面套了IFNULLMySQL侧就把默认值兜住了。2.3 建一个返回单值的存储函数存储函数必须返回一个值我造一个“根据用户ID返回用户名”的函数。USE demo_jdbc; DROP FUNCTION IF EXISTS get_username_by_id; DELIMITER $$ CREATE FUNCTION get_username_by_id(p_user_id INT) RETURNS VARCHAR(50) DETERMINISTIC BEGIN DECLARE v_username VARCHAR(50); SELECT username INTO v_username FROM t_user WHERE id p_user_id; RETURN v_username; END$$ DELIMITER ;这里RETURNS VARCHAR(50)决定了JDBC侧要用getString来接。DECLARE在函数里定义一个变量SELECT INTO把查到的用户名塞进去最后RETURN。补充一点如果函数体里只有一条SQLMySQL允许省略BEGIN END直接写成RETURN (SELECT username FROM t_user WHERE id p_user_id);。但多语句场景必须写BEGIN END所以建议一开始就养成带BEGIN END的习惯。2.4 MySQL 8下必须留神的一个开关MySQL的存储函数有一个很经典的坑当bin_log开启也就是默认开启二进制日志的情况下如果函数里面包含数据修改语句或者函数不是DETERMINISTIC / NO SQL / READS SQL DATAMySQL会拒绝创建报错类似于This function has none of DETERMINISTIC, NO SQL...。JDBC调用时会直接抛SQLException。解决办法有两种看场景选。第一种如果当前函数确实不修改数据就在函数里声明DETERMINISTIC或者READS SQL DATA就像我上面写的。第二种如果公司的数据库允许可以执行SET GLOBAL log_bin_trust_function_creators 1;但注意这个变量是全局的对生产库影响面很大有DBA就找DBA评估不要自己随手开。重点存储函数中如果使用了查询必须明确声明DETERMINISTIC、NO SQL或READS SQL DATA否则MySQL会因为二进制日志校验拒绝创建。3. JDBC调用存储过程把接口和数据对起来3.1 写一个最小可用的JDBC调用框架先看Java侧怎么准备。如果是老项目用DriverManager直接获取连接如果是Spring项目连接池拿到的Connection一样只是不需要手动关连接。这里我写一个最简单的演示版本方便看到全貌。import java.sql.*; public class CallProcedureExample { static final String URL jdbc:mysql://localhost:3306/demo_jdbc?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; static final String USER root; static final String PASSWORD your_password; public static void main(String[] args) { try (Connection conn DriverManager.getConnection(URL, USER, PASSWORD)) { // 调用带OUT参数的存储过程 String sql {CALL get_order_stat_by_user(?, ?, ?)}; try (CallableStatement cstmt conn.prepareCall(sql)) { cstmt.setInt(1, 1); // IN 参数 cstmt.registerOutParameter(2, Types.INTEGER); // OUT 参数 cstmt.registerOutParameter(3, Types.DECIMAL); // OUT 参数 cstmt.execute(); int orderCount cstmt.getInt(2); BigDecimal totalAmount cstmt.getBigDecimal(3); System.out.println(订单数量: orderCount); System.out.println(订单总金额: totalAmount); } } catch (SQLException e) { e.printStackTrace(); } } }代码不长但有两个细节值得讲。第一个细节是prepareCallJDBC中调用存储过程不能使用prepareStatement({CALL ...})虽然这样写也能运行但规范是用prepareCall因为驱动会针对CallableStatement做特殊处理包括提前注册OUT参数等。第二个细节是registerOutParameter注册的参数类型必须和MySQL存储过程里定义的参数类型在同一组映射范围内。MySQL的INT对接Types.INTEGERDECIMAL(10,2)对接Types.DECIMAL。如果注册成Types.VARCHAR再拿去取数值有些驱动会警告结果也可能异常。3.2 存储过程返回结果集时该怎么拿存储过程并不一定只有OUT参数很多时候它还直接返回一个ResultSet。比如我改造一个过程根据用户ID返回该用户的全部订单数据。USE demo_jdbc; DROP PROCEDURE IF EXISTS get_orders_by_user; DELIMITER $$ CREATE PROCEDURE get_orders_by_user(IN p_user_id INT) BEGIN SELECT id, user_id, amount, status, create_time FROM t_order WHERE user_id p_user_id ORDER BY create_time DESC; END$$ DELIMITER ;JDBC调用时不需要注册OUT参数执行后直接用getResultSet()拿第一个结果集。String sql {CALL get_orders_by_user(?)}; try (CallableStatement cstmt conn.prepareCall(sql)) { cstmt.setInt(1, 1); boolean hasResult cstmt.execute(); if (hasResult) { try (ResultSet rs cstmt.getResultSet()) { while (rs.next()) { int orderId rs.getInt(id); BigDecimal amount rs.getBigDecimal(amount); int status rs.getInt(status); System.out.println(订单ID: orderId 金额: amount 状态: status); } } } }这里有一个新手最容易被坑的点调用存储过程获取结果集不要直接while(rs.next())要先看execute()返回的布尔值。如果为true代表第一个结果是ResultSet如果为false代表第一个结果是更新计数或者没有结果。虽然大多数情况下存储过程都会返回结果集但万一有分支语句导致第一次execute()没有返回结果集程序就会漏掉。3.3 一个过程返回多个结果集怎么办复杂一点的情况是存储过程里先查列表再查统计最后返回两个结果集。USE demo_jdbc; DROP PROCEDURE IF EXISTS get_orders_and_totals; DELIMITER $$ CREATE PROCEDURE get_orders_and_totals(IN p_user_id INT) BEGIN SELECT id, amount, status FROM t_order WHERE user_id p_user_id; SELECT COUNT(*) AS cnt, IFNULL(SUM(amount), 0) AS total FROM t_order WHERE user_id p_user_id; END$$ DELIMITER ;Java侧要循环获取String sql {CALL get_orders_and_totals(?)}; try (CallableStatement cstmt conn.prepareCall(sql)) { cstmt.setInt(1, 1); boolean hasResult cstmt.execute(); while (true) { if (hasResult) { try (ResultSet rs cstmt.getResultSet()) { while (rs.next()) { // 第一个结果集订单明细 } } } else { int updateCount cstmt.getUpdateCount(); if (updateCount -1) { break; // 没有更多结果 } // 如果是 UPDATE/INSERT 的更新数可以做日志 } hasResult cstmt.getMoreResults(); } }getMoreResults()会关闭当前打开的ResultSet并移动到下一个结果集所以ResultSet没有必要再手动关闭但我习惯还是放在try-with-resources里。这样循环直到hasResult false且getUpdateCount() -1说明所有结果都读完了。多结果集在报表类场景特别常见第一个结果集放明细列表第二个结果集放统计合计一次数据库往返全部拿回Java端。虽然SQL也能用UNION拼接但带不同列宽不同含义的表用多结果集更清晰。4. JDBC调用存储函数参数少但返回值别接错4.1 语法差异只有一处但很容易记混调用存储函数的标准JDBC语法是{? CALL 函数名(?, ?)}注意这里有三个符号问号、等号、CALL。第一个问号是函数返回值所在位置后面跟着的才是函数入参。和存储过程相比存储函数的参数索引多了第0位或者第1位的问题不同的驱动处理不一样。为了兼容性最好统一理解{? CALL func(?)}里等号左边的?是返回值注册时使用registerOutParameter(1, Types.VARCHAR)而函数的入参从第2个索引开始。但有些老驱动或特殊配置对索引的定义不同稳妥做法是在写完后先在本地JUnit里跑一下。4.2 存储函数完整调用示例接着用之前那个get_username_by_id函数。String sql {? CALL get_username_by_id(?)}; try (CallableStatement cstmt conn.prepareCall(sql)) { cstmt.registerOutParameter(1, Types.VARCHAR); cstmt.setInt(2, 1); cstmt.execute(); String username cstmt.getString(1); System.out.println(用户名: username); }这段代码里registerOutParameter(1, Types.VARCHAR)对应函数的RETURNS VARCHAR(50)。返回值类型是字符串就必须用getString读取。如果函数返回DECIMAL却用getInt去取结果虽然没有编译错误但可能会丢精度或者拿到0。有一点值得注意MySQL的函数可以通过SELECT get_username_by_id(1)直接查但JDBC的Statement.executeQuery(SELECT get_username_by_id(1))其实也能拿返回值。那我为什么推荐用CallableStatement主要原因是参数绑定。SELECT get_username_by_id(1)这种方式如果改成字符串拼接很容易引入SQL注入风险。用CallableStatement可以做到参数预编译调用函数的过程和调用存储过程一样安全。另外以后函数参数多了{? CALL func(?,?,?)}也比拼接可维护得多。4.3 函数返回NULL时Java侧怎么优雅处理存储函数非常容易出现NULL返回值比如用户ID不存在SELECT username INTO会没有赋值函数返回NULL。Java端如果直接getString返回的是null后续业务处理要注意空指针。处理思路有两个。最好的方案在MySQL侧在函数里用IFNULL或者COALESCE把默认值兜住CREATE FUNCTION get_username_by_id_safe(p_user_id INT) RETURNS VARCHAR(50) DETERMINISTIC BEGIN RETURN COALESCE( (SELECT username FROM t_user WHERE id p_user_id), UNKNOWN ); END$$Java侧处理则是另一个思路读到null之后判断一下再给默认值。String username cstmt.getString(1); if (username null) { username 默认用户; }这两种方式各有利弊MySQL侧兜底适合“返回值一定要有默认值”的业务规则Java侧兜底适合把“默认值策略”留给上层业务决定。我建议核心逻辑尽量放在数据库侧因为不管谁来用这个函数行为都是一致的。5. 踩坑指南JDBC 存储过程的高频翻车现场5.1 参数索引和类型映射是重灾区先说参数索引。存储过程里写了3个参数JDBC就一定要按顺序注册和设置。很多同学的报错是Parameter index out of range十有八九是registerOutParameter用的索引和存储过程定义对不上尤其是前面有多个IN参数时有人以为OUT参数从1开始实际从IN参数之后继续往后排。再说类型映射我列一个常用对照表。MySQL参数类型JDBC注册/获取类型说明INT / INTEGERTypes.INTEGER / getInt()注意范围大整数用BIGINTBIGINTTypes.BIGINT / getLong()主键常用VARCHAR / CHARTypes.VARCHAR / getString()中文要保证连接编码utf8mb4DECIMAL / NUMERICTypes.DECIMAL / getBigDecimal()金额必须用别用doubleDATETIME / TIMESTAMPTypes.TIMESTAMP / getTimestamp()时区要在URL里配serverTimezoneTEXTTypes.LONGVARCHAR / getString()有些驱动需要getString处理一个很隐蔽的坑是MySQL的DECIMAL(10,2)在驱动里映射到java.math.BigDecimal如果直接getDouble(3)去取金额可能在精度上出现微小的误差特别是在做汇总统计时。所以强烈建议金额类全部用getBigDecimal。5.2 空值、中文乱码和时区的坑空值的坑前面讲到了函数返回值其实存储过程也一样。比如你调用统计存储过程如果没有匹配的订单SUM(amount)返回NULLJava端getBigDecimal拿到null继续做加减计算就会出事。我建议在存储过程内部就处理好默认值Java侧拿到后也最好做一次判空。中文乱码这个坑根子不在存储过程而在JDBC URL。MySQL 8驱动默认使用utf8mb4但老连接串如果写着characterEncodingutf8某些特殊字符比如表情符号会直接乱码。连接串里尽量写成jdbc:mysql://localhost:3306/demo_jdbc?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai时区问题就更常见了。MySQL的DATETIME没有时区概念但你如果连接串没指定serverTimezone驱动有时会拿本地默认时区去转换TIMESTAMP导致Java取到的时间差8个小时。我在生产环境遇到过几次这种“阴间Bug”所以在所有项目里都统一在URL里加serverTimezoneAsia/Shanghai。5.3 连接、事务、性能三件套不能少调用存储过程本质还是数据库连接上的一次执行要遵守三条纪律。第一条是连接不要裸奔。DriverManager.getConnection()只能用于学习或脚本真正的项目里一定要用连接池比如HikariCP、Druid。存储过程通常执行时间较长如果每次都新建连接连接建立的开销会吃掉存储过程省下的性能。第二条是事务边界要想清楚。存储过程内部可以写事务但如果你在Java侧已经开启了事务同时又希望在存储过程中提交就必须注意事务边界。默认情况下Java侧拿到Connection后调用存储过程中的COMMIT会直接影响当前连接的事务状态很容易把你的Java事务搞乱。我的习惯是事务控制在Java侧做存储过程内部只做数据操作不轻易写START TRANSACTION或COMMIT除非这个存储过程被设计成完全自治。第三条是性能监控。存储过程难调试的根源在于一条语句失控可能拖死整个数据库。建议在MySQL侧用EXPLAIN分析存储过程中的主要SQL再配合performance_schema去看调用高频过程有没有慢查询。平时只需要关注平均值和95分位不要被个别极端值带偏。6. 到底该不该把业务写进存储过程说说我的取舍6.1 适合用存储过程的场景根据我过去的项目经验适合用存储过程的场景往往有这几个特征业务逻辑涉及多条SQL并且需要保证整体一致性业务逻辑被多个系统或团队复用表结构和规则相对稳定迭代速度不快数据库连接网络质量不稳定减少往返意义重大。典型例子就是定时任务里的统计汇总、跨行转账、订单对账。这些操作如果用Java代码做要么写一大堆事务模板要么在多次网络调用里提心吊胆。6.2 不适合用存储过程的场景反过来如果你的团队里Java开发经验丰富、DBA资源有限或者业务规则每周都在变那存储过程就会成为维护成本黑洞。尤其是一旦出现几百行甚至上千行的存储过程新人上手会非常痛苦Git版本管理也不如Java代码方便测试也更难自动化。我个人的原则是超过10个参数的存储过程要警惕超过50行的存储过程必须写注释超过100行的存储过程就要考虑拆解。存储过程不是不能用而是要有纪律地用。6.3 和ORM框架配合的实践现在很多团队用MyBatis或者MyBatis PlusJDBC调用存储过程一样可以整合进去。MyBatis里通过Select注解或者XML的方式都可以调用。XML参数和CallableStatement是对应的比如select idcallOrderStat statementTypeCALLABLE {CALL get_order_stat_by_user( #{userId, modeIN, jdbcTypeINTEGER}, #{orderCount, modeOUT, jdbcTypeINTEGER}, #{totalAmount, modeOUT, jdbcTypeDECIMAL} )} /select映射类里要定义和OUT参数同名的属性调用后再从对象里取。MyBatis对存储过程的封装其实很薄底层还是JDBC那套所以你在JDBC层踩的坑在MyBatis里基本都会遇到理解了这一篇再去看框架文档就轻松得多。最后分享一个我自己的调试习惯我平时在写JDBC调用存储过程之前一定会先在MySQL命令行或者Navicat里手工调一遍。命令行就是CALL get_order_stat_by_user(1, cnt, total); SELECT cnt, total;看输出和预想是否一致。存储函数就SELECT get_username_by_id(1);先在数据库侧把SQL的正确性确认掉再到Java里写CallableStatement。这样排查问题时可以快速定位是SQL写得不对还是Java传参不对能省掉大量联调时间。这个习惯陪了我好几个项目每次都能在别人还在“查日志猜问题”的时候直接把范围缩小到一两行。你也可以试试至少不会一上来就拿着Java的异常栈在存储过程里大海捞针。

相关新闻

RJ45网口EMC设计实战:共模电感、接地与PCB布局要点

RJ45网口EMC设计实战:共模电感、接地与PCB布局要点

做了这么多年硬件,我最大的体会之一就是:RJ45网口的EMC设计是最容易让人头疼、也最值得提前花功夫的地方。网线一拉就是一两米,等于在机壳外面挂了一根巨大的天线;接口本身又裸露在外,静电、浪涌、射频干扰全都从这儿往…

2026/10/9 6:34:47 阅读更多 →
从MySQL行锁到Redis分布式锁:并发场景锁的完整指南

从MySQL行锁到Redis分布式锁:并发场景锁的完整指南

如果把并发编程比作开车,那锁就是路口的红绿灯。上周进阶课正好讲到“锁章节”,我从MySQL的行锁一路做到Redis分布式锁,把之前零零散散的锁知识完整串了一遍。写这篇的目的很直接:把锁章节里真正有用的东西沉淀下来,遇…

2026/10/9 6:35:17 阅读更多 →
模型路由实战:从成本构成到星链4SAPI配置,省下30% AI调用费

模型路由实战:从成本构成到星链4SAPI配置,省下30% AI调用费

先别急着质疑“省下30%”这个数字是不是标题党。我接触过好几个被AI调用账单追着跑的项目,日均调用量到了十万次以上时,每次模型选择的多花一点,月底账单都会给你颜色看。我自己踩过一轮又一轮坑之后,把模型路由这件事彻底摸透了&…

2026/10/8 3:10:58 阅读更多 →

最新新闻

生产级Coding Agent调优实战:Harness工程化决定落地下限

生产级Coding Agent调优实战:Harness工程化决定落地下限

1. 从"能跑"到"好用":生产级 Coding Agent 的最后一公里到底卡在哪Vibe Coding 这个词这两年被聊得很多,大意是开发者用自然语言描述意图,让 Coding Agent 去生成、修改、验证代码,人只负责把握方向和验收。听…

2026/10/9 6:35:27 阅读更多 →
Java Swing人事管理系统:JDBC+MySQL课程设计实战与避坑指南

Java Swing人事管理系统:JDBC+MySQL课程设计实战与避坑指南

简介:这份资源是一套基于 Java Swing、JDBC 与 MySQL 实现的人事管理系统课程设计项目,面向正在完成数据库课程设计、需要可运行参考案例的计算机相关专业学生。项目包含可视化软件界面,覆盖人员信息维护、数据库连接与增删改查等典型业务场景…

2026/10/9 6:35:27 阅读更多 →
MySQL校对规则:utf8mb4_general_ci与utf8mb4_bin的差异及选型

MySQL校对规则:utf8mb4_general_ci与utf8mb4_bin的差异及选型

1. 这两个校对规则到底在吵什么看你一脸问号地点进来,我猜你多半是遇到过这种情况:建表的时候复制了一段别人的SQL,里面有CHARSETutf8mb4 COLLATEutf8mb4_general_ci,或者是utf8mb4_bin,当时也没多想,能用就…

2026/10/9 6:35:27 阅读更多 →
PS5底层开发合规边界与技术可行性分析

PS5底层开发合规边界与技术可行性分析

我无法根据当前输入生成符合要求的博文。原因如下:项目标题 "AnyPS5" 缺乏明确指向性:该词在公开技术语境中无公认定义,既非官方产品名(索尼未发布/命名过 AnyPS5)、非开源项目(GitHub、GitLab、…

2026/10/9 6:35:27 阅读更多 →
claude-mem:为Claude Code打造跨会话长期记忆的实战指南

claude-mem:为Claude Code打造跨会话长期记忆的实战指南

用过 Claude Code 写真实项目的人,基本都遇到过这个场景:昨天刚跟 AI 讨论清楚的一个架构方案,今天新开一个会话,它完全不记得了。你在同一个仓库里翻历史对话记录,发现上一个会话已经把项目的来龙去脉都喂给了它&…

2026/10/9 6:35:27 阅读更多 →
Agent-Reach:LLM API智能路由与成本可控调度中枢

Agent-Reach:LLM API智能路由与成本可控调度中枢

1. 项目概述:Agent-Reach 是什么?它解决的不是“能不能用”,而是“怎么用得稳、用得准、用得省”Agent-Reach 这个名字乍看像某个开源模型或工具库,但结合 CLI、API、YouTube、Reddit 这些高频热词,再叠加上“zcode cl…

2026/10/9 6:34:27 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →