news 2026/9/9 13:32:50

深入解析MyBatis分页插件原理:PageHelper与MyBatis Plus实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入解析MyBatis分页插件原理:PageHelper与MyBatis Plus实战

1. 从手写分页到插件接管,先聊清楚分页这件事

做Java后端的人,只要接触过数据库,基本都逃不过分页查询。早期用JDBC的时候,分页是纯手工活,MySQL写LIMIT offset, size,Oracle玩ROWNUM,SQL Server还得折腾ROW_NUMBER() OVER,换一种数据库就要改一遍SQL。后来用MyBatis舒服了一些,但分页依然要自己在Mapper XML里写LIMIT #{offset}, #{pageSize},每个查询都要算offset,代码一多到处是重复劳动,还容易因为参数顺序写错直接报错或者查出脏数据。

MyBatis分页插件解决的就是这个痛点。市面上最主流的方案是PageHelper,另外MyBatis Plus在框架层面也内置了一套分页能力。它们的共同思路是:你只需要告诉插件“我要查第几页、每页几条”,插件在SQL执行前帮你把SQL改写成分页SQL,执行完顺便把总数查出来,最后把结果封装成带页码信息的分页对象。也就是说,SQL还是那个SQL,业务代码里不用再写offset计算,不用再单独写COUNT查询,页面上的“共多少条、第几页、共几页”这些数据自动就有了。

这篇文章不是光讲怎么调用,我会把背后原理拆开来看。因为只有搞明白分页插件是怎么“拦截”你的查询的,你才能在遇到问题时知道去哪里排查。文章里覆盖了PageHelper的完整配置、分页参数传递、PageInfo封装、常见坑排查,也顺带讲清楚MyBatis Plus分页和PageHelper在设计思路上的区别。适合正在用MyBatis做项目、想规范化分页写法的同学,也适合被分页插件搞出过奇奇怪怪bug、想真正看懂原理的人。

2. 分页插件整体设计思路,为什么它能“拦截”分页

2.1 物理分页与逻辑分页,选错方案的代价

分页说白了有两种实现方式。逻辑分页是把所有数据一次性查出来,在内存里截取需要的部分,早期很多ORM框架的默认行为就是这样。逻辑分页在数据量小的时候没啥问题,但数据一旦上了万级,每次翻页都全表加载,内存撑不住,数据库查询也慢得离谱。物理分页是直接在SQL层面操作,数据库只返回当前页需要的数据,这才是正经的分页方案。MyBatis本身没有内置物理分页能力,需要借助数据库方言写不同的分页SQL,这就为分页插件的出现留下了空间。

分页插件的价值不只是“帮你把SQL拼好”,它做了三件你以为很简单但其实很麻烦的事:一是动态判断当前查询是不是分页查询,不是的话就放行;二是改写SQL,在原SQL外面包一层,变成带LIMITROWNUM的真实分页语句;三是额外生成一条COUNT查询。这三件事如果靠手写SQL,每写一个Mapper方法都要重复一遍,而且业务SQL一旦改动,分页SQL和COUNT SQL都要跟着改,非常容易漏。插件把这些全部自动化了。

2.2 MyBatis插件机制是分页拦截的基础

MyBatis允许通过插件机制在四大核心组件上做拦截,分别是ExecutorStatementHandlerParameterHandlerResultSetHandler。分页插件拦截的是Executor,因为Executor负责调度整个查询过程,在这里拦截可以拿到完整的MappedStatement、参数对象和SQL信息,也能控制查询是否继续执行。

PageHelper的实现思路是:执行分页查询时,通过ThreadLocal保存分页参数,然后在Executorquery方法里判断当前线程是否存在分页参数,如果有,就把原始SQL改写成分页SQL,同时查询总数,执行完成后清空ThreadLocal。这里有一个很多初学者忽略的关键点:分页参数只对紧接着的下一条查询生效,查询完就失效。所以你在PageHelper.startPage之后如果先执行了别的查询,再执行目标查询,分页参数早就没了,分页就不生效。这是PageHelper最经典的坑之一,后面我会专门讲。

2.3 从“Mapper层分页”到“插件层分页”的演进

早期项目里很多人写分页是这么干的:先写一个Page对象,然后让所有Mapper方法的参数都继承它,SQL里手动拼LIMIT。这个方案能工作,但污染了Mapper接口的职责,每个查询都要关心分页字段,而且不同数据库方言还得自己适配。插件方式把分页从业务SQL里彻底剥离了,SQL只负责查询条件,分页是横切关注点,由插件统一处理。这其实就是AOP思想的体现,也符合MyBatis插件设计的初衷——把公共的横切逻辑集中管理。

3. Spring Boot中集成PageHelper,五个步骤跑通完整分页

3.1 项目依赖引入,一个坐标解决大部分场景

PageHelper对Spring Boot的支持很完善,官方提供了pagehelper-spring-boot-starter,引入这个依赖后只要配置几个参数就能开箱即用。我用的是Spring Boot 2.7.x,对应的PageHelper版本用的1.4.7,稳定性不错。Maven坐标如下:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.4.7</version> </dependency>

如果你的项目不是Spring Boot,是传统的Spring XML配置,那就用pagehelper普通坐标,然后在mybatis-config.xml里配置PageInterceptor插件。Spring Boot项目我建议直接用starter,省去手工装配的麻烦,配置项也都有默认值,唯一必须关注的是helper-dialect

3.2 核心配置参数,每个都影响分页行为

application.yml里添加如下配置:

pagehelper: helper-dialect: mysql reasonable: false support-methods-arguments: true params: count=countSql auto-runtime-dialect: true

参数说明:

  • helper-dialect:指定数据库方言,可选值包括mysqloraclepostgresql等。配置了这个参数,插件就知道用什么语法生成物理分页SQL。我是MySQL数据库,这里直接写mysql
  • reasonable:分页合理化。设为true时,如果查询页码超过总页数,会自动回到最后一页;如果小于1,自动回到第一页。这个参数看业务需求,有些场景下用户手动输入page参数,超过范围时后端不做约束很容易查出空数据,合理化之后体验会好一些。但我通常设为false,因为很多管理后台需要让前端明确知道“请求的页码不存在”,返回空页比静默纠正更容易排查问题。
  • support-methods-arguments:支持Mapper方法参数中的分页参数。开启后,如果你在Mapper方法里声明了Page类型的参数,插件会自动识别,不需要手动startPage
  • params:配置count=countSql的意思是,当你的SQL里已经包含了COUNT查询,插件会优先使用你的COUNT语句,否则自动生成一条。

auto-runtime-dialect建议开启,这个参数能让插件在运行时根据连接URL自动识别数据库方言,避免环境迁移(比如从MySQL切到PostgreSQL)时改了数据源忘记改方言的尴尬。

3.3 编写Mapper和Service,一个PageInfo搞定所有

先看一个最简单但完整的分页查询流程。Mapper接口里不用声明任何分页相关的方法,就是一个普通查询:

public interface UserMapper { List<User> selectUserList(UserQuery query); }

Mapper XML照样只写业务查询条件,注意不要写LIMIT

<select id="selectUserList" resultType="com.example.entity.User"> select * from user <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="status != null"> and status = #{status} </if> </where> order by create_time desc </select>

Service层的做法是:

public PageInfo<User> pageUser(UserQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); List<User> userList = userMapper.selectUserList(query); return new PageInfo<>(userList); }

PageHelper.startPage(pageNum, pageSize)两个参数一个是页码,一个是每页条数,页码从1开始。紧接着执行的selectUserList会被拦截并改写成分页SQL。new PageInfo<>(userList)会从查询结果中解析出总数、总页数、当前页码、每页条数等完整信息,Controller直接把这个PageInfo序列化给前端就行。

你可能会问,为什么COUNT查询没有在Service里写?插件在改写SQL的时候会同时执行一条COUNT查询,自动把总数塞进PageInfo。这条COUNT是插件根据原SQL生成的,一般能覆盖大部分场景。但注意,如果你的原SQL特别复杂,比如带有多个子查询或者UNION,插件生成的COUNT可能不够精确,这时你可以在对应的查询方法上使用@SelectProvider自定义COUNT逻辑,或者用PageHelperstartPage重载方法指定COUNT SQL。

3.4 前端分页参数传递规范,别踩参数名的坑

前端传来的分页参数通常是pageNumpageSize,也可能是pagelimit。很多人在Controller里直接接收再传给Service,这个没问题,但我建议在后端定义一个统一的分页请求基类,避免每个接口重复声明参数。

public class PageRequest { private Integer pageNum = 1; private Integer pageSize = 10; public Integer getPageNum() { return pageNum; } public void setPageNum(Integer pageNum) { this.pageNum = pageNum; } public Integer getPageSize() { return pageSize; } public void setPageSize(Integer pageSize) { this.pageSize = pageSize; } }

然后业务查询对象继承这个基类,Controller里统一接收。这样所有分页接口参数风格一致,前端对接成本低很多。另外pageSize一定要做上限限制。我见过很多系统没限制每页条数,前端传一个pageSize=100000,直接把数据库打爆。实现方式可以简单粗暴,超过100就强制设为100,如果你的业务确实需要导出大结果集,那应该走专门的异步导出接口,不该走分页查询。

4. 分页参数与PageInfo深入实践,多表查询和排序技巧

4.1 多表关联查询的分页,到底分的是谁

多表关联分页是最容易出低级错误的地方。看一个场景:查询订单列表,每个订单关联用户表和商品表,SQL是select o.*, u.name as userName, p.name as productName from orders o left join user u on o.user_id = u.id left join product p on o.product_id = p.id

PageHelper对单表简单查询和多表关联查询的处理方式是一样的——直接在原SQL外层套一个LIMIT。绝大多数情况下,业务想要的就是“订单维度的分页”,也就是订单表满足条件的数据分页,结果集里带上关联表的冗余字段,这种处理是对的。

但有一种场景会翻车:一张订单对应多个商品明细,如果明细也需要在同一页展示,SQL会变成一对多联查,同一订单出现多行。这时候LIMIT截的是“行数”,不是“订单数”,结果可能是订单A有10条明细恰好占了半页,看起来就像数据“分页分碎了”。对于这种场景,分页插件帮不了你,根因是SQL本身的一对多结果集。我的建议是这种页面不要走单条SQL联查,先分页查订单主表,再根据当前页的订单ID集合批量查明细,在内存里组装。这种“主表分页+从表聚合”的做法,数据一致性更好,SQL也好维护。

4.2 PageInfo常用字段与JSON序列化

PageInfo对象里字段很多,最常用的是这几个:

PageInfo<User> pageInfo = new PageInfo<>(userList); long total = pageInfo.getTotal(); // 总记录数 int pages = pageInfo.getPages(); // 总页数 int pageNum = pageInfo.getPageNum(); // 当前页码 int pageSize = pageInfo.getPageSize(); // 每页条数 List<User> list = pageInfo.getList(); // 当前页数据

还有一个容易被忽略的字段hasNextPagehasPreviousPage,前端做“加载更多”或者“上一页/下一页”按钮时会用到。PageInfo本身继承了Page类,Page类又继承了ArrayList,所以序列化时如果直接返回PageInfo,前端拿到的JSON里既能按数组方式拿到list,也能拿到totalpageNum这些字段。但PageInfo里的navigatepageNums等导航页字段是分页条组件用的,如果你用vue-element-admin这类现成模板,可能需要把这些字段一起返回。

4.3 排序和分页一起用,SQL别写死

分页查询几乎都伴随着排序需求。不建议在Mapper XML里面写死order by create_time desc,因为业务上管理员可能要按时间、按金额、按状态分别排序。让前端传排序字段,但一定不能直接拼接SQL,存在注入风险。可以用MyBatis的<choose>标签白名单判断:

<choose> <when test="orderByColumn == 'createTime'"> order by create_time </when> <when test="orderByColumn == 'amount'"> order by amount </when> <otherwise> order by create_time </otherwise> </choose>

这样排序字段只能在白名单里选,拼接方向参数时也要校验只能是ascdesc。实际项目中这是很实用的规范,既灵活又安全。

4.4 分页插件的COUNT查询优化

插件自动生成的COUNT查询很多时候是“能用但不够快”的。比如原SQL里有多个LEFT JOIN,但COUNT根本不需要关联那些表,插件生成的select count(0) from ...还是会把JOIN带上。这种情况下,你可以在Mapper里单独定义一个COUNT查询方法,让自己的COUNT SQL只查主表:

<select id="selectUserList_COUNT" resultType="long"> select count(0) from user <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> </where> </select>

PageHelper约定,如果存在方法名_COUNT的Mapper方法,它会优先使用这个方法替代自动生成的COUNT查询。这个约定看似简单,大数据量场景下性能差异可能达到几十倍。我优化过一个查询,原SQL有四个JOIN,COUNT要扫描大量关联数据,改成独立COUNT只查主表索引,接口响应时间从1.8秒降到0.2秒。

5. MyBatis Plus分页与PageHelper的区别,到底选哪个

5.1 MyBatis Plus分页的核心API

MyBatis Plus是MyBatis的增强框架,内置BaseMapper,自带很多通用方法,分页也是开箱自带的。使用MyBatis Plus分页需要先配置一个分页插件:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

业务代码里这样用:

Page<User> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.eq(User::getStatus, 1).orderByDesc(User::getCreateTime); Page<User> result = userMapper.selectPage(page, wrapper);

selectPage是BaseMapper内置方法,传入Page对象和查询条件,result里就有记录、总数、页码信息。这个API天然面向对象,不需要手写SQL,写起来确实爽。MyBatis Plus的分页插件机制和PageHelper完全不同,它的核心是一个基于JsqlParser的解析器,能够把查询SQL解析成抽象语法树,再根据方言改写成分页SQL,最后把总数查询、分页查询都处理完。

5.2 两者设计哲学的核心差异

PageHelper和MyBatis Plus分页最本质的区别在于是否“侵入Mapper方法”。

PageHelper是弱侵入的,你的Mapper方法签名不用做任何改动,甚至不用让Mapper继承任何接口,普通接口加个XML就能分页。好处是迁移成本低,原来几十个自定义查询方法不用动,只要调用前加一句PageHelper.startPage就行。坏处是这个操作有隐藏依赖,容易忘记清除ThreadLocal导致后续查询被意外分页。

MyBatis Plus则要求所有分页查询都走BaseMapper的selectPage方法,或者用IService.page方法。查询条件通过Wrapper构造,SQL由框架自动生成。这种方式约束更强,代码结构更规范,但如果你已经有一套复杂的自定义SQL,想迁移过来需要把很多Mapper方法改成Wrapper写法,工作量大。

从使用体验来说,如果你的项目是全新的,并且决定全量使用MyBatis Plus,那直接用框架自带分页最省事。如果你的项目是维护一个老系统,Mapper XML里已经有大量手写SQL,只想要一个“无侵入”的分页能力,PageHelper显然更合适。

5.3 共存场景是否可行

有些项目会同时引入MyBatis Plus和PageHelper,技术上没有硬冲突,两个插件拦截器同时注册也能工作。但不建议这么干,原因很简单:两个插件都会在Executor层做拦截,分页逻辑可能出现双重改写,运维的时候也不清楚每个查询到底走的哪套分页机制,排查问题成本高。二选一就好。

6. 分页插件背后的实现细节,看懂源码才敢深度排查

6.1 ThreadLocal里的page对象生命周期

PageHelper的startPage方法把分页参数放到了ThreadLocal里,紧接着的查询会消费这个参数。这个设计在单线程场景下没问题,因为一次请求一个线程,查询完参数就清掉。但如果你在代码里用了线程池,比如在子线程里去执行查询,ThreadLocal里的分页参数是拿不到的,因为子线程和父线程的ThreadLocal不共享,分页会静默失效。

更隐蔽的问题是,如果startPage之后执行的查询抛了异常,ThreadLocal里的参数可能没被及时清理干净,下一次查询到同一个线程(线程池复用)时,新查询会被无端分页。PageHelper的拦截器在finally块里会做清理,但如果你绕过了Executor走了一些奇怪的查询路径,异常分支就有可能残留。所以有个很实用的习惯:尽量不要把startPage和查询隔得太远,中间别穿插别的数据库操作,保持在同一个方法里连续执行。

6.2 插件如何改写SQL

PageHelper内部对SQL的改写依赖jsqlparser库。它会解析原始SQL,识别出SELECTFROMWHEREORDER BY这些SQL片段,然后根据方言拼接。比如MySQL方言,PageHelper会把原查询包装成SELECT ... FROM (原SQL) AS TEMP LIMIT ?,?,再单独生成一条SELECT COUNT(0) FROM (原SQL) AS TEMP来查总数。

为什么PageHelper不改原SQL的LIMIT,而是选择套一层子查询?因为原SQL可能自带ORDER BY,直接在外面套LIMIT在某些场景下会破坏排序的语义,而且GROUP BYDISTINCT的情况下,COUNT的计算逻辑应该是基于子查询的结果来的。套子查询是通用做法,虽然性能上多了一层子查询开销,但正确性优先。当然这也是为什么复杂分页SQL最好自己写COUNT的原因——子查询套得越多,性能越差。

6.3 手写一个简化版分页拦截器

理解了原理之后,自己写一个极简版的分页拦截器其实很有意思,也能加深对MyBatis插件机制的理解。核心步骤是:实现Interceptor接口,用@Intercepts注解声明拦截Executorquery方法,在拦截逻辑里解析SQL、拼接分页部分。

@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class SimplePageInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement) invocation.getArgs()[0]; Object parameter = invocation.getArgs()[1]; // 解析BoundSql,拿到原始SQL BoundSql boundSql = ms.getBoundSql(parameter); String originalSql = boundSql.getSql(); // 判断是否需要分页,这里简化判断条件 if (parameter instanceof Page) { Page page = (Page) parameter; String pageSql = originalSql + " LIMIT " + page.getOffset() + "," + page.getPageSize(); // 替换BoundSql中的SQL Field sqlField = BoundSql.class.getDeclaredField("sql"); sqlField.setAccessible(true); sqlField.set(boundSql, pageSql); } return invocation.proceed(); } }

这个简化版本还有很多问题,比如没处理COUNT查询、没考虑不同数据库方言,但足以展示核心机制。理解了这个流程,后面遇到分页插件相关的bug,至少能判断问题出在SQL解析层还是参数传递层。

7. 常见问题与排查技巧实录,都是真实项目里踩过的坑

7.1 常见问题速查表

问题现象可能原因解决方案
分页不生效,返回全部数据startPage和目标查询之间有其他查询确保startPage后紧跟目标Mapper调用
分页数据正常但总数不对COUNT SQL自动生成不准确自定义方法名_COUNT方式覆盖
翻页时数据重复或丢失ORDER BY字段不唯一追加主键排序,如order by create_time desc, id desc
使用线程池子线程分页失败ThreadLocal不跨线程传递子线程内单独执行startPage
MySQL分页正常,切到Oracle报错方言配置写死开启auto-runtime-dialect
多个Mapper方法连续调用,第一个正常第二个被分页ThreadLocal残留检查异常分支、排查是否在同一线程复用

7.2 嵌套结果查询的分页陷阱

如果你的Mapper返回结构是resultMap里配置了collection的嵌套查询,就是那种“查订单列表,每个订单里再查明细”的写法,分页会出现一个非常隐蔽的问题:LIMIT作用在主查询上,但主查询只有几条订单,每个订单的内部集合又查出很多明细,最终页面上看到的明细条数可能远大于pageSize。这在功能上其实符合“订单分页”的预期,但如果前端预期的是“行分页”,两边就对不上了。

我的建议是尽量避免这种“嵌套查询+分页”的组合。要么改成联表分页查主表,然后在Service层聚合明细;要么明确告诉前端这里的分页维度是主表记录,不是明细行。

7.3 动态表名和视图分页的处理

有些系统按时间分表,比如订单表按月拆分order_202501order_202502,Mapper XML里用${tableName}动态拼表名。PageHelper对这种情况的支持是有的,因为SQL拿到的是已经拼接了真实表名的语句,解析器能正常处理。但要注意,如果表名是通过自定义Interceptor在SQL执行前动态替换的,就要留意分页插件和表名插件的执行顺序,顺序不对可能SQL已经被分页改写了,表名还没替换上去,直接报表不存在。

处理办法是调整两个插件的order属性,确保表名替换插件先执行。配置顺序在Spring Boot中可以通过@Bean的加载顺序间接控制,必要时在插件中显式指定拦截优先级。

7.4 分页插件加持下的性能优化思路

分页查询慢,很多时候不是分页本身慢,而是COUNT慢。定位思路是:先看慢SQL日志,确认是分页SQL慢还是COUNT慢。如果是COUNT慢,优先优化COUNT语句,减少不必要的JOIN,尽量覆盖索引;如果是分页SQL慢且深分页严重(比如翻到第1000页),可以考虑“延迟关联”方案,先分页查出主键ID,再用主键ID关联回原表查完整记录:

<select id="selectUserListByPage" resultType="com.example.entity.User"> select u.* from user u inner join ( select id from user <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> </where> order by create_time desc limit #{offset}, #{pageSize} ) t on u.id = t.id </select>

这种写法牺牲了一点SQL复杂度,换来了深分页时临时表扫描量的显著下降,数据量大的时候非常值得用。

8. 分页插件的选型建议与我的个人使用体会

分页插件看似是个小工具,选型时却牵动着整个数据访问层的架构方向。用了这么多年,我的体会是:没有最好的分页插件,只有最适合当前项目的方案。

如果是纯MyBatis项目,XML里自定义SQL多,我首选PageHelper,因为它的侵入性最小,老项目接入成本极低。数据库方言切换频繁的项目,开启auto-runtime-dialect后基本不用改代码。如果是新项目且团队决定用MyBatis Plus,那我直接用它的selectPage,理由很简单,框架已经替你做了分页,没必要再引入一套体系。MyBatis Plus的分页API和Service层结合得也很好,IService.page方法自带Lambda条件构造器,代码写起来干净不少。

最后提醒一件事,分页插件不是万能的。它只能处理“SQL改写和参数传递”这一层,业务上你是否应该用深分页、COUNT查询怎么写更高效、多表关联怎么组织,这些都需要你自己做决策。理解插件的边界,比学会调用API重要得多。希望这篇文章能帮你把分页插件用得明明白白,少踩几个坑。

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

级联H桥SVG/STATCOM三相不平衡补偿的三层控制策略与仿真实践

1. 项目概述与整体设计思路 搞电力电子的朋友应该都有体会&#xff0c;SVG&#xff08;静止无功发生器&#xff09;和STATCOM&#xff08;静止同步补偿器&#xff09;在行业内基本被当成同一个东西用&#xff0c;只是叫法不同&#xff0c;一个侧重低压配电&#xff0c;一个侧重…

作者头像 李华
网站建设 2026/9/9 13:32:12

Seelen-UI 插件系统实战:4 类内置模块,从 0 到能用的桌面定制

Seelen-UI 插件系统实战&#xff1a;4 类内置模块&#xff0c;从 0 到能用的桌面定制 【免费下载链接】Seelen-UI The Fully Customizable Desktop Environment for Windows 10/11. 项目地址: https://gitcode.com/GitHub_Trending/se/Seelen-UI 一句话讲清楚它是什么 …

作者头像 李华
网站建设 2026/9/9 13:31:36

从被AI气晕到理性共处:大模型能力边界、偏见与落地实践

1. 从"被AI气晕"到"重新认识AI"&#xff1a;我的认知转变1.1 早期我对AI的理解&#xff1a;死板的规则引擎大概十年前&#xff0c;我对人工智能的看法还停留在一个非常朴素的层面&#xff1a;某个程序能不能"听懂人话"&#xff0c;本质上就是一堆…

作者头像 李华
网站建设 2026/9/9 13:29:26

Ant Design表单焦点错乱:同名Field注册冲突的成因与解法

把时间拨回两周前&#xff0c;我正对着一个Bug工单发愁。同事的描述很简短&#xff1a;“弹窗里的输入框&#xff0c;鼠标点一下&#xff0c;光标却跑到页面顶部的搜索框里去了&#xff1b;弹出的字符没有进弹窗&#xff0c;反而把搜索框填满了。” 我第一反应是“怎么可能”&a…

作者头像 李华
网站建设 2026/9/9 13:29:25

TongWeb版本怎么选?场景化选购指南与部署避坑实践

TongWeb这个牌子&#xff0c;圈里搞信创和国产中间件的人应该都不陌生。但我在社群和后台经常看到一类提问&#xff0c;上来就是“我项目该用哪个版本”&#xff0c;或者“下了一个TongWeb怎么部署还报错”。说句实在话&#xff0c;TongWeb的版本选购真不是看哪个新就选哪个&am…

作者头像 李华
网站建设 2026/9/9 13:29:15

magnum-cli:轻量级本地大模型推理服务工具

1. “magnitude”不是个动词&#xff0c;而是一个被误读的开源推理服务工具名 最近在几个技术社区里频繁刷到“magnitude”这个词&#xff0c;尤其和CLI、本地模型、inference server这些词绑在一起。有人发帖问“magnitude怎么装”&#xff0c;有人报错“unable to locate the…

作者头像 李华