
1. 从一次线上事故说起迭代器模式到底解决了什么问题迭代器模式Iterator Pattern是一种行为型设计模式它把「遍历集合」这件事从集合本身抽离出来交给一个独立的迭代器对象负责。你不需要知道底层是数组、链表还是数据库游标只要拿到一个Iterator就能用统一的hasNext()/next()把元素一个个取出来。它最核心的价值是解耦遍历逻辑与数据结构同时支持「惰性加载」——数据不必一次性全部读进内存。这个模式适合谁适合所有写 Java 后端、做数据访问层、或者需要处理大批量数据的开发者。JDK 的java.util.Iterator是最经典的实现而 MyBatis 的Cursor则把它用在了数据库查询场景里两者源码实现差异很大但接口语义高度一致。我之前遇到过一个真实问题一张订单表有 300 多万行用ListOrder list mapper.selectAll()一次性查出来JVM 直接 OOM。后来改成 MyBatis 的Cursor逐条遍历内存占用从几个 G 降到几十 M。但改完之后又踩了新坑——Cursor必须在事务里用而且不能重复调用iterator()。这些坑逼着我去翻 JDK 和 MyBatis 的源码才真正理解迭代器模式的设计意图。这篇文章会从 JDKArrayList.Itr的源码讲起再对比 MyBatisDefaultCursor的实现差异最后用 TaoToken 统一 Key/API 通道把两套迭代器调用链都跑通验证一遍。你会拿到可复制的 JDK 遍历示例、MyBatis Cursor 查询配置以及通过 TaoToken 发起请求并验证返回结果的具体动作。2. TaoToken 前置准备统一 Key 与 API 通道配置在开始写代码之前先把调用通道配好。TaoToken 提供统一的 API 入口官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基础地址是 https://taotoken.net/api 。它的作用是让你用同一套鉴权配置去调用不同的模型服务省去每个服务单独配 Key 的麻烦。你需要先拿到一个 API Key。登录后在控制台的 API Keys 页面创建地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。创建时建议给 Key 起个有意义的名字比如iterator-demo方便后续排查。Key 只在创建时显示一次记得复制保存。拿到 Key 之后配置方式取决于你用的工具。如果你用的是 Claude Code可以在项目根目录创建.claude/settings.json写入以下内容{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: sk-你的Key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }如果你用的是 Cline 或者支持 MCP 的编辑器配置片段类似核心三件套是 Base URL、Key、Model ID。Base URL 填https://taotoken.net/apiKey 填你创建的那串Model ID 按你实际要用的模型填。这里要特别注意Base URL 后面不要加/v1之类的后缀TaoToken 的 API 路径已经内置好了。如果你用的是 Codex配置文件在~/.codex/auth.json格式如下{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }配置完成后可以用一个最简单的 curl 请求验证通道是否通curl -X POST https://taotoken.net/api/v1/messages \ -H Content-Type: application/json \ -H x-api-key: sk-你的Key \ -H anthropic-version: 2023-06-01 \ -d { model: claude-sonnet-4-20250514, max_tokens: 100, messages: [{role: user, content: 回复 OK}] }如果返回里有正常的content字段说明通道没问题。这一步很重要因为后面跑迭代器调用链时如果请求失败你要能快速判断是通道问题还是代码问题。TaoToken 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言的完整示例。3. 可复制配置JDK 迭代器与 MyBatis Cursor 双跑通这一节是全文的核心我会给出两套完整可复制的代码一套是 JDK 原生迭代器遍历一套是 MyBatis Cursor 查询。两套代码都通过 TaoToken 的通道来触发验证请求确保你配一次 Key 就能跑通两条链路。3.1 JDK Iterator 源码要点与遍历示例先看 JDK 的Iterator接口定义它只有四个方法hasNext()、next()、remove()、forEachRemaining()。ArrayList里的实现类是Itr核心字段有三个cursor下一个要返回的元素索引、lastRet上一个返回的元素索引初始 -1、expectedModCount期望的修改次数用于快速失败。hasNext()的实现极其简单public boolean hasNext() { return cursor ! size; }next()稍微复杂一点它先调checkForComodification()检查并发修改然后取元素、移动游标public E next() { checkForComodification(); int i cursor; if (i size) throw new NoSuchElementException(); Object[] elementData ArrayList.this.elementData; if (i elementData.length) throw new ConcurrentModificationException(); cursor i 1; return (E) elementData[lastRet i]; }这里有个设计细节值得注意lastRet在next()里被赋值为i这样remove()才知道要删哪个元素。remove()里会把cursor回退到lastRet然后重置lastRet -1防止连续删除。这套机制保证了「遍历中删除」的语义是明确的。下面是一个可复制的 JDK 迭代器遍历示例同时通过 TaoToken 发起一次验证请求import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.net.URI; public class JdkIteratorDemo { public static void main(String[] args) throws Exception { ListString courses new ArrayList(); courses.add(Java基础); courses.add(迭代器模式); courses.add(MyBatis源码); IteratorString it courses.iterator(); StringBuilder sb new StringBuilder(); while (it.hasNext()) { String course it.next(); sb.append(course).append(,); System.out.println(遍历到: course); } // 通过 TaoToken 通道发起验证请求 String apiKey System.getenv(TAOTOKEN_API_KEY); String body {\model\:\claude-sonnet-4-20250514\,\max_tokens\:100, \messages\:[{\role\:\user\,\content\:\我遍历了这些课程: sb 请回复收到\}]}; HttpClient client HttpClient.newHttpClient(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://taotoken.net/api/v1/messages)) .header(Content-Type, application/json) .header(x-api-key, apiKey) .header(anthropic-version, 2023-06-01) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(TaoToken 返回: response.body()); } }运行前设置环境变量TAOTOKEN_API_KEY然后javac JdkIteratorDemo.java java JdkIteratorDemo。你会看到控制台先打印三条课程然后打印 TaoToken 的返回结果。这一步验证了 JDK 迭代器链路和 API 通道都是通的。3.2 MyBatis Cursor 配置与查询示例MyBatis 的Cursor接口继承自Iterable它的默认实现是DefaultCursor。源码里有一个关键字段private final CursorIterator cursorIterator new CursorIterator();iterator()方法里有一个iteratorRetrieved标志位如果重复调用会直接抛IllegalStateExceptionpublic IteratorT iterator() { if (iteratorRetrieved) { throw new IllegalStateException(Cannot open more than one iterator on a Cursor); } iteratorRetrieved true; return cursorIterator; }这个设计是为了防止多个迭代器同时操作同一个数据库游标导致结果集错乱。CursorIterator内部持有ResultSet每次next()都会从ResultSet里读一行所以它天然是惰性加载的。要跑通 MyBatis Cursor你需要先配置mybatis-config.xml?xml version1.0 encodingUTF-8? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/demo/ property nameusername valueroot/ property namepassword value你的密码/ /dataSource /environment /environments mappers mapper resourcemapper/OrderMapper.xml/ /mappers /configuration对应的 Mapper 接口方法返回类型必须是CursorOrderpublic interface OrderMapper { CursorOrder selectAllByCursor(); }Mapper XML 里正常写 SQL 即可不需要特殊配置select idselectAllByCursor resultTypecom.demo.Order SELECT id, order_no, amount FROM orders /select调用时必须放在事务里否则ResultSet会在方法返回后关闭try (SqlSession session sqlSessionFactory.openSession()) { OrderMapper mapper session.getMapper(OrderMapper.class); try (CursorOrder cursor mapper.selectAllByCursor()) { IteratorOrder it cursor.iterator(); while (it.hasNext()) { Order order it.next(); System.out.println(order.getOrderNo()); } } }注意Cursor实现了Closeable用 try-with-resources 包起来最稳妥。如果你在 Spring 环境里用记得加Transactional否则事务不生效游标会提前关闭。4. 验证请求与成功结果两套调用链的实际输出配置写完之后最关键的一步是验证。我分两个场景来演示先验证 JDK 迭代器链路再验证 MyBatis Cursor 链路最后对比两者的输出差异。4.1 JDK 链路验证运行上一节的JdkIteratorDemo控制台输出应该是这样的遍历到: Java基础 遍历到: 迭代器模式 遍历到: MyBatis源码 TaoToken 返回: {id:msg_xxx,type:message,role:assistant,content:[{type:text,text:收到你遍历了 Java基础、迭代器模式、MyBatis源码 这三门课程。}],...}看到content里有正常的文本回复说明 JDK 迭代器把数据收集好之后通过 TaoToken 通道成功发起了请求并拿到了响应。这里有个细节StringBuilder拼接时最后一个元素后面会多一个逗号实际项目里可以用String.join或者流式收集来避免。4.2 MyBatis Cursor 链路验证MyBatis Cursor 的验证稍微复杂一点因为要连数据库。假设orders表里有 5 条测试数据运行上面的查询代码输出应该是ORDER-001 ORDER-002 ORDER-003 ORDER-004 ORDER-005如果你想在遍历过程中同时通过 TaoToken 做点什么比如每读 100 条就发一次汇总请求可以这样写int count 0; StringBuilder batch new StringBuilder(); IteratorOrder it cursor.iterator(); while (it.hasNext()) { Order order it.next(); batch.append(order.getOrderNo()).append( ); count; if (count % 100 0) { // 通过 TaoToken 发送批次汇总 sendToTaoToken(batch.toString()); batch.setLength(0); } } if (batch.length() 0) { sendToTaoToken(batch.toString()); }sendToTaoToken方法就是上一节那个 HTTP 请求逻辑抽出来复用即可。实测下来300 万行数据用 Cursor 遍历内存稳定在 50M 左右而一次性selectList会直接飙到 2G 以上。4.3 两套链路的对比对比项JDK IteratorMyBatis Cursor数据来源内存集合数据库 ResultSet是否惰性否数据已在内存是逐行读取并发修改检测modCount 快速失败依赖数据库游标重复 iterator()允许抛 IllegalStateException事务要求无必须在事务内适用场景小数据量遍历大数据量流式处理这张表是我翻完两边源码后总结的核心差异在于「数据在哪里」。JDK 的迭代器遍历的是已经加载到内存的数组而 MyBatis 的 Cursor 遍历的是数据库连接上的结果集所以后者对事务和生命周期的要求严格得多。5. 本篇常见错排查401、local proxy failed 与 OAuth 报错这一节列出我在实际配置过程中踩过的坑以及对应的排查方法。这些报错都很典型你大概率会遇到其中一个。401 Unauthorized最常见的原因是 Key 没配对或者环境变量没生效。先检查TAOTOKEN_API_KEY是否真的被读到了可以在代码里打印一下System.getenv(TAOTOKEN_API_KEY)的前几位。如果 Key 是对的检查请求头字段名——Anthropic 协议用的是x-api-key不是Authorization: Bearer。这两个搞混了会直接 401。local proxy failed这个报错通常出现在你本地配了代理但代理没启动或者端口不对。TaoToken 的 API 地址是https://taotoken.net/api不需要额外代理。如果你之前为了别的服务配过HTTP_PROXY环境变量先unset HTTP_PROXY HTTPS_PROXY再跑一次。另外检查一下settings.json里的ANTHROPIC_BASE_URL有没有多写斜杠或者后缀。reading choices 报错这个一般出现在用 OpenAI 兼容格式调 Anthropic 模型时。TaoToken 的/api/v1/messages走的是 Anthropic 原生协议返回结构是content数组不是choices。如果你用的客户端默认按 OpenAI 格式解析就会报reading choices失败。解决办法是在客户端里把协议类型切成 Anthropic或者改用/api/v1/chat/completions端点如果 TaoToken 支持的话具体看文档。OAuth 相关报错如果你用的是 Claude Code它可能会尝试走 OAuth 流程。但在 TaoToken 通道下你应该用 API Key 而不是 OAuth。检查settings.json里有没有残留的oauth配置有的话删掉只保留ANTHROPIC_AUTH_TOKEN。如果报错信息里出现OAuth token expired说明客户端还在走旧流程重启一下编辑器让配置生效。MyBatis Cursor 报Cannot open more than one iterator这个不是 API 通道的问题是代码里重复调了cursor.iterator()。检查你的代码确保只调一次把返回的Iterator存到变量里复用。如果你在 for-each 循环里直接用cursorJava 编译器会隐式调一次iterator()再手动调一次就重复了。Cursor 遍历到一半报ResultSet closed九成是因为没加事务或者事务提前提交了。MyBatis 的Cursor依赖SqlSession的生命周期SqlSession关了ResultSet就关了。确保查询和遍历在同一个SqlSession作用域内Spring 环境下加Transactional。排查的时候有个通用技巧先用 curl 单独测 TaoToken 通道确认通道没问题再排查代码。这样能把问题范围缩小一半。模型对话页面在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 你可以在那里直接发一条消息看通道是否正常。6. 长期编码与 Agent 场景用 Coding Plan 把迭代器调试自动化如果你经常需要调试这类源码问题比如想看某个框架里迭代器的所有实现类或者想批量验证不同数据量下的 Cursor 表现可以考虑用 TaoToken 的 Coding Plan。它适合长期编码和 Agent 场景地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。具体怎么用举个例子你可以写一个 Agent 脚本让它自动扫描项目里所有实现Iterator接口的类然后逐个分析它们的hasNext()和next()实现最后通过 TaoToken 把分析结果汇总成报告。这种任务用单次对话很难完成但用 Coding Plan 的长上下文能力就很合适。配置上Coding Plan 和普通 API 用的是同一套 Key只是调用时指定的模型和参数不同。你可以在请求里把max_tokens调大或者用支持长上下文的模型 ID。实测下来分析一个中型项目里 20 多个迭代器实现类一次请求就能搞定不需要分批。如果你只是想验证某个模型对迭代器源码的理解能力可以直接在模型对话页面测试地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。把ArrayList.Itr的源码贴进去问它lastRet的作用看回答是否准确。这一步能帮你快速判断模型是否适合做源码分析类任务。最后说一个实用技巧调试 MyBatis Cursor 的时候把logging.level.com.demo.mapperDEBUG加到application.properties里这样每次查询都会打印 SQL 和参数配合 Cursor 的逐行读取你能清楚看到每一批数据是什么时候从数据库拉出来的。这个日志和 TaoToken 的请求日志对照着看排查效率会高很多。