news 2026/10/6 5:13:12

Hibernate分页实战:物理分页原理、性能优化与常见坑排查

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Hibernate分页实战:物理分页原理、性能优化与常见坑排查

在Hibernate里做分页,第一反应都是setFirstResult()配合setMaxResults(),这套API从Hibernate 2.x用到现在的Hibernate 6.x,可以说是最基础也最常用的操作之一。但如果你只停留在“能翻页”这个层面,后面会遇到一堆坑:count数不准、SQL莫名复杂、一对多关联分页直接变成内存分页、Oracle方言生成的SQL看不懂……这篇文章我就把这几年做Hibernate分页的完整思路、底层原理和排错过程一次性说透。

先说清楚适用人群:正准备用Hibernate做列表页、后台管理系统的Java开发者,或者已经从MyBatis转到Hibernate/Spring Data JPA的老司机。不管你是新手还是老鸟,这篇文章的目标是让你看完之后不仅能写出正确分页,还能解释清楚为什么这么写,以及出了问题去哪排查。

1. 先搞清楚一件事:Hibernate分页到底是物理分页还是逻辑分页

1.1 物理分页与逻辑分页的本质区别

经常在群里见到有人问“Hibernate分页是不是就是把所有数据查出来再内存截取?”这种疑问不奇怪,因为很多ORM框架早期确实这么干过。物理分页和逻辑分页完全是两码事:

  • 物理分页:SQL层面就带了LIMIT、OFFSET或ROWNUM这类数据库专有语法,数据库只把当前页需要的那几条数据返回给应用。
  • 逻辑分页:不管页面要多少条,先把满足条件的所有记录从数据库捞出来,放到Java内存里,再通过List.subList()这类方式截取一段。

逻辑分页在数据量小的时候挺好用,比如几千行配置数据,翻页也就无所谓了。但一旦数据量来到几十万、上百万条,逻辑分页就非常难受——数据库到应用之间要传输全量数据,内存里也要维持全量对象,这还不算每条记录关联的嵌套对象创建开销。我见过一个生产事故,就是有人用逻辑分页查50万行数据,列表页直接卡死,GC连续Full GC。

Hibernate默认走的是物理分页路径。当你调用setMaxResults()和setFirstResult()之后,Hibernate会把它们翻译成底层数据库能识别的分页SQL,真正只取需要的行。判断当前是不是物理分页有个笨办法:打开hibernate.show_sql=true,看控制台打印的SQL里有没有limit、offset、rownum等关键字,没有就是内存分页。

1.2 为什么Hibernate默认选择物理分页

早年Hibernate设计分页机制时,数据库方言(Dialect)体系已经非常成熟。Hibernate允许针对每种数据库实现专门的LimitHandler,所以它在框架层面就具备生成物理分页SQL的能力。选物理分页几乎是唯一合理的选择,原因很直接:

第一,数据传输量和内存占用都小。一页20条,数据库就只返回20条;第二,查询时间相对可控。很多场景下配合索引,分页查询能命中索引下推之类的优化,避免全表扫描;第三,语义清晰,开发者不用自己拼数据库方言。

当然,这不是说Hibernate绝对不会做逻辑分页。当它发现当前数据库不支持分页语法,就必须退回内存分页,这种情况在老旧数据库或特定方言配置缺失时会出现。现代主流数据库基本都有分页支持,所以正常项目里物理分页是绝对主流。

1.3 什么情况会退化成内存分页

这里必须提醒一个常见的坑:fetch join一对多集合时,Hibernate很可能“故意”退化成内存分页。原因是setMaxResults()如果在SQL层加上LIMIT,会对主表行数进行限制,但假如同一行主表记录关联了10条子表记录,SQL结果集展开后可能是按子表行数来计数的,这样截取出来的“10条主记录”可能其实只有2条主记录,语义完全错乱。

Hibernate对这个问题的处理策略是在日志里打一条警告HHH000104: firstResult/maxResults specified with collection fetch; applying in memory,意思是:检测到集合抓取,分页改为内存模式。你没看错,Hibernate是主动选择内存分页来保证语义正确。但后果就是,如果这一页关联的数据特别庞大,内存压力会明显上升,甚至出现MemoryLimitExceededException。

遇到过这种场景的兄弟应该知道,改法通常是拆成两步:先用分页查询主表ID列表,再用IN批量查子表数据,或者干脆放弃join抓取,改用batch_size加载、@NamedEntityGraph这类次级方案。后面第4章我会专门写一个完整示例。

2. 三种主流写法:从HQL到Criteria再到Hibernate 6

2.1 最经典的HQL写法:setFirstResult与setMaxResults

HQL分页是最直观、也最常见的老写法。假设有一张图书表,我要按书名模糊查询并分页,代码如下:

// 页码从0开始,每页20条 int pageNo = 0; int pageSize = 20; try (Session session = sessionFactory.openSession()) { Query<Book> query = session.createQuery( "from Book b where b.title like :kw order by b.id", Book.class); query.setParameter("kw", "%" + keyword + "%"); query.setFirstResult(pageNo * pageSize); query.setMaxResults(pageSize); List<Book> books = query.list(); }

setFirstResult(int)控制从第几行开始取,setMaxResults(int)控制最多取多少行。这里有一个容易踩的惯性问题:很多人习惯页号从1开始,于是写成setFirstResult((pageNo - 1) * pageSize),这没有错,但要和前端约定好。我见过接口文档写“页码从0开始”,前端从1传,结果第二页永远和第一页重复,排查了半天。

很多项目里HQL通常不会只查实体本身,可能只查某些字段:

Query<Tuple> query = session.createQuery( "select b.id, b.title, b.price from Book b where b.publisher = :pub order by b.id", Tuple.class); query.setParameter("pub", publisher); query.setFirstResult(10); query.setMaxResults(20);

这种写法叫“标量查询”或“部分字段查询”,好处是减少完整实体加载的开销。唯一要注意的是类型是Tuple而不是Object[],取值时tuple.get(0)、tuple.get("title")都行。老项目里用Object[]的写法也能跑,但Hibernate 6以后更推荐Tuple。

2.2 动态查询首选Criteria API,附带count写法

条件不固定时,字符串拼HQL特别容易漏空格、少括号。Criteria API(JPA的CriteriaBuilder)在动态场景下更稳。Hibernate 5.2+完全支持JPA的Criteria写法,Hibernate 6下就是同一套API。写个带关键字、价格区间、状态的动态分页查询:

public PageResult<Book> searchBooks(String keyword, BigDecimal minPrice, BigDecimal maxPrice, int pageNo, int pageSize, Session session) { CriteriaBuilder cb = session.getCriteriaBuilder(); // 1. 拼条件 CriteriaQuery<Book> query = cb.createQuery(Book.class); Root<Book> root = query.from(Book.class); List<Predicate> predicates = new ArrayList<>(); if (keyword != null && !keyword.isBlank()) { predicates.add(cb.like(root.get("title"), "%" + keyword + "%")); } if (minPrice != null) { predicates.add(cb.greaterThanOrEqualTo(root.get("price"), minPrice)); } if (maxPrice != null) { predicates.add(cb.lessThanOrEqualTo(root.get("price"), maxPrice)); } query.where(predicates.toArray(new Predicate[0])); query.orderBy(cb.asc(root.get("id"))); // 2. 查总数 CriteriaQuery<Long> countQuery = cb.createQuery(Long.class); Root<Book> countRoot = countQuery.from(Book.class); countQuery.select(cb.count(countRoot)); // count条件必须重新设置,不能复用上面的root if (!predicates.isEmpty()) { countQuery.where(predicatesToArray(countRoot, cb, keyword, minPrice, maxPrice)); } Long total = session.createQuery(countQuery).getSingleResult(); // 3. 查当前页数据 Query<Book> query = session.createQuery(cq); query.setFirstResult(pageNo * pageSize); query.setMaxResults(pageSize); List<Book> rows = query.getResultList(); return PageResult.of(total, rows); }

许多人会尝试把Root<Book>在数据查询和count查询之间复用,实际容易出问题,因为CriteriaQuery.from()和where()都绑定在特定query实例上,强行复用会和count语义冲突。最稳妥的做法是像上面一样count查询重新create一个Root,条件重新构造一遍。

如果项目用的还是老的org.hibernate.Criteria(Hibernate 5之前的原生API),写法是session.createCriteria(Book.class).add(Restrictions.like("title", "%" + keyword + "%")),再调setFirstResult和setMaxResults。但Hibernate 6已经移除这套原生Criteria API,还在维护老项目的朋友建议尽早迁移到JPA Criteria。

2.3 Hibernate 6.x的新分页API:Page与getResultList(Page)

从Hibernate 6.2开始,官方推荐了一套新的分页方式,用Page对象封装页码和大小。以Hibernate 6.2+为例:

Page page = Page.page(20).withFirstResult(40); List<Book> books = session.createSelectionQuery( "from Book where title like :kw order by id", Book.class) .setParameter("kw", "%" + keyword + "%") .getResultList(page);

Page.page(20)表示每页20条,withFirstResult(40)表示从第41行开始。整体比之前的两个set方法简洁不少,也更容易在Service层传递。老接口setFirstResult/setMaxResults在6.x早期还是可以用的,只是部分方法被标了@Deprecated。如果你用的是Spring Data JPA 3 + Hibernate 6,Pageable会映射到这里的分页逻辑上,原理是相通的。

新API还有一个额外好处:SelectionQuery接口支持getResultCount()这种更直接的计数方式?这个我只能说不同版本差异比较大,我没法断言所有版本都有。所以新项目建议直接看当前Hibernate版本的Javadoc,把Page、SelectionQuery、MutationQuery这几个类翻一遍,比网上抄旧代码靠谱。

3. 分页SQL到底是怎么拼出来的:方言机制拆解

3.1 Dialect与LimitHandler的分工

Hibernate分页SQL的生成不是靠写死一套模板,而是通过方言机制。每个数据库方言类(比如MySQLDialect、PostgreSQLDialect、OracleDialect)内部都注册了一个LimitHandler,这个组件负责把setFirstResult/setMaxResults转成最终的分页子句。

整个流程大致是:

  1. 解析HQL/JPQL,生成语义上“没有分页”的SQL。
  2. 会话工厂根据配置的hibernate.dialect或者底层DatabaseMetaData选中方言。
  3. 执行查询前,LimitHandler介入,把原始SQL包装成带分页语法的SQL。
  4. 预编译参数化时,分页参数(offset和limit)会作为绑定参数拼到SQL里,避免SQL注入。

你不需要背这些类名,但理解这层机制对排查问题很有帮助。比如同一个应用从MySQL迁移到Oracle,HQL一点不用改,底层SQL会自动从limit换成Oracle的rownum或offset ... fetch,靠的就是方言切换。如果你发现分页SQL风格和预期不符,第一反应应该是查方言版本和LimitHandler实现,而不是去翻业务代码。

3.2 MySQL、PostgreSQL、Oracle分页SQL的真实形态

为了让你对“方言翻译”有直观感受,我把同一条HQL在三种数据库下生成的SQL形态列出来,原始HQL就一句:

/* HQL */ select b from Book b order by b.id

MySQL 8使用标准LIMIT:

select b.id, b.title, b.price from book b order by b.id limit ?, ?

PostgreSQL风格是LIMIT和OFFSET分开:

select b.id, b.title, b.price from book b order by b.id limit ? offset ?

Oracle旧版本(12c之前)用三层嵌套的ROWNUM:

select * from ( select inner_.*, rownum as rn from ( select b.id, b.title, b.price from book b order by b.id ) inner_ where rownum <= ? ) where rn > ?

Oracle 12c及以后可以用标准OFFSET/FETCH:

select b.id, b.title, b.price from book b order by b.id offset ? rows fetch next ? rows only

看到MySQL和PostgreSQL生成的SQL,你可能会想“这不就是普通SQL吗?”没错,对支持LIMIT的数据库来说,Hibernate只是把参数填进去而已。但Oracle的旧方言就很能体现Hibernate的设计:它要保证外层WHERE ROWNUM和内层RN同时生效,生成逻辑比较复杂。这也是为什么很多老系统一升级数据库版本,顺便把Hibernate版本升级一下,分页性能就可能有变化——因为新方言可能切换到更现代的OFFSET/FETCH语法。

3.3 为什么分页必须配order by:排序稳定性问题

分页查询不写order by,等于把自己交给数据库的“物理顺序”。MySQL里通常意味着主键顺序或索引扫描顺序,但这并不是强保证。一旦数据分布变化、优化器选择不同索引或并行执行,下一页数据可能和上一页出现重叠或缝隙。

我实际踩过一回:一个订单列表分页,用户在第1页看到单号1001,往下翻到第2页又看到1001,客户直接反馈“数据重复”。排查之后发现查询里只有where status = 1,没有任何排序。数据库默认按主键物理顺序返回,但这种顺序在分页场景下压根不可靠。

正确做法是至少给一个唯一性排序字段:

query.orderBy(cb.asc(root.get("status")), cb.asc(root.get("id")));

status作为业务排序,id作为稳定排序的兜底。这样即使业务排序字段有大量相同值,最终顺序也不会乱。如果你用HQL,就写成order by b.status asc, b.id asc。

4. 实战:一个带条件查询的分页接口完整实现

4.1 需求与表结构

以图书管理为例。表结构就三张:book(图书)、author(作者)、book_author(多对多关联表),book表字段有id、title、price、publisher、status。需求是:前端传入关键字、价格区间、状态,后端返回分页后的图书列表,每一条Book里要带该书的作者列表。

很多项目做到“带作者列表”就会自然写出这样的查询:

// 危险写法,后面会解释 from Book b left join fetch b.authors a where b.title like :kw

然后一执行就遇到警告:HHH000104: firstResult/maxResults specified with collection fetch; applying in memory。如果你没注意日志,功能看起来正常,但数据量一大就卡死。这就是4.2节要解决的问题。

4.2 Service层编写:count与数据列表双查询

我建议的实现方法是:第一步,普通分页查询Book的主表字段,不join抓取作者集合;第二步,拿到当前页Book ID集合,再用一条in查询把作者关联关系查出来,手动组装。

public PageResult<BookVO> pageBooks(String keyword, BigDecimal minPrice, BigDecimal maxPrice, int pageNo, int pageSize, Session session) { CriteriaBuilder cb = session.getCriteriaBuilder(); // 1. count查询 CriteriaQuery<Long> countCq = cb.createQuery(Long.class); Root<Book> countRoot = countCq.from(Book.class); List<Predicate> countPreds = buildPredicates(cb, countRoot, keyword, minPrice, maxPrice); countCq.select(cb.count(countRoot)).where(countPreds.toArray(new Predicate[0])); long total = session.createQuery(countCq).getSingleResult(); // 2. 当前页主表查询,不做集合fetch CriteriaQuery<Book> pageCq = cb.createQuery(Book.class); Root<Book> pageRoot = pageCq.from(Book.class); List<Predicate> pagePreds = buildPredicates(cb, pageRoot, keyword, minPrice, maxPrice); pageCq.select(pageRoot) .where(pagePreds.toArray(new Predicate[0])) .orderBy(cb.asc(pageRoot.get("id"))); Query<Book> pageQuery = session.createQuery(pageCq); pageQuery.setFirstResult(pageNo * pageSize); pageQuery.setMaxResults(pageSize); List<Book> books = pageQuery.getResultList(); // 3. 批量查询当前页作者关联,避免N+1 List<Long> ids = books.stream().map(Book::getId).toList(); Map<Long, List<Author>> authorMap = findAuthorsByBookIds(session, ids); // 4. 组装VO List<BookVO> voList = books.stream().map(book -> { BookVO vo = new BookVO(); vo.setId(book.getId()); vo.setTitle(book.getTitle()); vo.setPrice(book.getPrice()); vo.setAuthors(authorMap.getOrDefault(book.getId(), List.of())); return vo; }).toList(); return new PageResult<>(total, voList); }

findAuthorsByBookIds的实现加了个join fetch查询作者,但因为查询范围是当前页ID集合,行数很短,尽管join后结果集有重复,也可以接受:

private Map<Long, List<Author>> findAuthorsByBookIds(Session session, List<Long> bookIds) { if (bookIds == null || bookIds.isEmpty()) { return Map.of(); } String hql = "select distinct b from Book b left join fetch b.authors a where b.id in :ids"; List<Book> books = session.createQuery(hql, Book.class) .setParameter("ids", bookIds) .list(); Map<Long, List<Author>> result = new HashMap<>(); for (Book b : books) { result.put(b.getId(), new ArrayList<>(b.getAuthors())); } return result; }

这个方案避开了集合fetch+分页导致的“内存分页”问题,绝大部分真实业务场景都用这套路子。代价是多一条SQL,但换来的是稳定可控的执行计划和内存占用,值得。

4.3 一对多关联的分页陷阱:fetch join会导致内存分页

再深入说下为什么fetch join+ 分页会出问题。看这个HQL:

from Book b left join fetch b.authors a where b.status = 1

假设第1页最多取20条Book。数据库在执行时,book表里有50条记录满足条件,其中20条Book各有3个作者。如果直接在SQL层写LIMIT 20,这条SQL在join fetch后返回的行数可能远超20——因为每个作者都占一行。结果就是:SQL的limit限制的不是“主表20行”,而是“结果集展开后20行”,导致只返回了部分Book的部分作者,语义错乱。

Hibernate为了避免这种错误,检测到这种组合时干脆不走物理分页,改成先查出所有满足条件的主表数据,再在内存中截取20条。这也就是HHH000104警告的含义。我实测过,当Book表20万行、关联作者平均3行时,这个查询的内存结果集直接膨胀到数十万对象,内存翻车几乎是必然的。

解决思路不外乎三种:

  1. 不要一次性fetch集合,改用@BatchSize或@Fetch(SUBSELECT)。
  2. 分页查主表ID,再批量查关联子表。
  3. 用@NamedEntityGraph或者DTO投影代替。

第2种就是我上面代码用的方案,也是我目前在项目里最推荐的方式。它把“分页不稳定”问题从根本上消解了——主表分页永远只作用于单表SQL,关联数据后续用IN再取。

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

5.1 count计数总是慢:先看三件套,再谈优化

分页接口一般都是两条SQL:一个count,一个查列表。很多项目优化重点全放在列表SQL上,结果接口还是慢,一看数据库慢日志,count反而成了最大的瓶颈。

count慢的原因通常就三个:

第一,count条件里的字段没有索引。模糊查询like %keyword%本身就很难走索引,如果条件里还有status、category_id等字段,优先给它们建组合索引。

第二,多表join导致count基数变大。Hibernate会把HQL里的隐式关联都翻译成join,如果你count的根实体是多对多的一端,join会重复计数,很多人不得不用select count(distinct b)。distinct一旦出现,count性能就要打折扣。更合理的做法是count查询里去掉一切不必要的join,只留下过滤条件。

第三,大偏移量。经典问题:用户翻到第1000页,limit 19980, 20,数据库仍然要扫描前19980行。这种场景下优化空间有限,常规方案是“前端禁止深翻页”或“改为基于游标/ID的分页”——下一批数据从上一批最后一条id开始往后查,而不是从头数offset。

排查count问题时,直接把Hibernate打印的count SQL拿到数据库执行一遍,看EXPLAIN,基本能定位80%的问题。

5.2 “分页失效”场景回顾:MyBatis Plus分页失效与Hibernate的区别

网上时不时看到“mybatisplus分页失效”相关热搜,这个背景也值得顺带聊一聊。MyBatis Plus的分页依赖PaginationInnerInterceptor拦截器,失效原因通常集中在几个地方:拦截器没注册或顺序不对、Page对象没有作为查询方法参数传入、传入的Page参数被当成普通条件、结果类型不匹配导致拦截器没有触发分页改写。

Hibernate的分页是框架内置流程,不依赖这类外部拦截器,所以基本不存在“忘了加拦截器导致分页失效”的说法。你只要调用了setFirstResult/setMaxResults或getResultList(page),分页逻辑就会执行。如果你发现Hibernate分页“失效”,先检查两件事:

第一,是不是没有执行到查询方法。比如缓存命中、短路逻辑,直接返回了之前的数据。

第二,是不是出现了HHH000104警告,集合fetch导致内存分页。这种情况打印SQL里看不到limit,会让人觉得分页没生效。

另外要留神:Hibernate二级缓存也可能让你误以为分页失效。如果查询结果被缓存,只有完全相同的分页参数和条件才会命中缓存,参数一变就会重新查,正常情况下不会导致“翻页重复”。真正遇到翻页重复,优先查排序稳定性。

5.3 顺手排雷:大偏移量分页与N+1问题

先补充一个经常被忽略的细节:firstResult参数不能是负数,这很容易理解。但是很多数据库方言对offset大小有限制,尤其是32位整数上限。如果某个页面业务异常,传入一个超大页号,pageNo * pageSize可能直接溢出成负数,抛IllegalArgumentException。我建议在Controller或Service入口统一做参数校验,页号上限可以设置成1000这样保守的值,同时给前端一个明确提示。

再说N+1问题。分页本身不会制造N+1,但只要你在翻页后遍历每本书去查作者,天然会变成N+1。以一个20条的分页列表来讲,等于是1次列表查询+20次作者查询,总共21次SQL。如果每本书又关联评论、标签,SQL数量会指数膨胀。

常见解法是前面提到的“按页ID批量查子表”:

String hql = "select a from Author a join a.books b where b.id in :ids"; List<Author> authors = session.createQuery(hql, Author.class) .setParameter("ids", bookIds) .list();

然后在Java层按bookId分组。控制往返次数稳定在2~3条SQL,比Hibernate自己的一对多集合加载和N+1都高效得多。如果是Spring Data JPA项目,直接定义@EntityGraph(attributePaths = "authors")配合Pageable也能达到类似效果。

另一个小坑是集合属性上的@BatchSize。很多人以为加了@BatchSize(size = 30)就万事大吉,它确实能把20次SQL合并成1次WHERE id IN (...),但对“分页”本身没有任何优化作用。它解决的是集合加载问题,不是分页SQL生成问题。这点要分清楚,别搞混。

最后,我个人的排查顺序是:先看日志有没有HHH000104,没有就确认SQL里有物理分页关键字;再看count和列表两条SQL的执行计划;最后把主表ID一旦拿到,立刻用批量IN查询关联数据。这套顺序帮我解决过好几起分页性能事故,写下来供你参考。

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

SSM框架下的个性化图书馆推荐系统毕设实战:协同过滤与全流程设计

搞java毕设的同学应该都遇到过这种情况&#xff1a;题目看起来简单&#xff0c;真正动手才发现坑不少。就拿这个“个性化图书馆推荐系统”来说&#xff0c;关键词拆开无非是java、SSM、推荐系统三件事&#xff0c;但合在一起&#xff0c;既要完成图书馆的图书入库、借阅归还、读…

作者头像 李华
网站建设 2026/10/6 5:11:42

锂电池保护板DW01+8205A方案:3种保护阈值实测与外围元件选型

1. 锂电池保护板的核心需求与方案选型1.1 为什么单节锂电必须配保护板单节锂离子电芯的标称电压是3.7V&#xff0c;满电4.2V&#xff0c;放电截止一般标2.75V到3.0V。这个电压窗口非常窄&#xff0c;一旦越界&#xff0c;后果不是“性能下降”这么简单。过充到4.3V以上&#xf…

作者头像 李华
网站建设 2026/10/6 5:11:26

阻焊开窗设计原理与嘉立创量产工艺适配指南

1. 为什么阻焊开窗不是“画个框就完事”——从嘉立创打样返工单说起去年帮一个做工业传感器的客户改板&#xff0c;原理图没问题&#xff0c;布线也干净&#xff0c;嘉立创下单前我特意检查了所有焊盘和过孔&#xff0c;确认没有漏铜。结果PCB回来一上电&#xff0c;三块板里有…

作者头像 李华
网站建设 2026/10/6 5:10:39

Mac桌面文件太多?用工作区思路让桌面装下无限文件

简介&#xff1a;这份资源是一份面向Mac用户的桌面文件管理教程文档&#xff0c;针对桌面文件越堆越多、整理费时费力的痛点&#xff0c;介绍如何借助SaneDesk应用实现高效收纳。文档以Workspace为核心概念&#xff0c;讲解如何创建多个独立工作区&#xff0c;将文档、图片等分…

作者头像 李华
网站建设 2026/10/6 5:08:34

微信小程序钢琴弹奏:从音频延迟到交互优化的实践指南

简介&#xff1a;面向微信小程序入门者与音乐爱好者&#xff0c;《微信趣味小程序-钢琴弹奏》提供了一套轻量完整的微信端虚拟钢琴交互实现。压缩包共36个文件、仅427KB&#xff0c;包含21个按音阶命名的mp3钢琴采样、6个json配置与儿歌谱数据、4个js逻辑脚本&#xff0c;以及3…

作者头像 李华
网站建设 2026/10/6 5:07:37

Skills:AI工程的新原子单元与K8s原生运行范式

1. “skills”不是功能模块&#xff0c;而是新一代AI工程范式的命名锚点最近在多个技术社区和开发者群聊里&#xff0c;频繁看到“skills”这个词被单独拎出来讨论——不是作为“技能”的泛义词&#xff0c;而是像一个专有名词那样被引用&#xff1a;有人发截图说“刚在Gemini …

作者头像 李华