news 2026/9/9 4:01:38

若依RuoYi分页支持POST吗?PageHelper JSON请求失效原因与三种解法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
若依RuoYi分页支持POST吗?PageHelper JSON请求失效原因与三种解法

我先说结论:若依的分页不排斥 POST。网上很多文章说“RuoYi 分页只能走 GET”,包括不少从表单页面复制出来、随手加了一行method:'post'的同学,卡在了“返回 total 一直是 0、点击第二页又跳回第一页”的问题上,最后绕回去用 GET 了。这个结论我不太认同,与其说是框架写死了请求方式,不如说是大家在改造 POST 时没有把分页参数送到 PageHelper 手里。RuoYi 的分页底层是 PageHelper,它其实不关心你是 GET 还是 POST,它关心的是pageNumpageSize这两个值在 SQL 执行前能不能被正确读到。

对需要把列表查询从 GET 改成 POST 的同学来说,这篇文章可以当成一份实操笔记看。我会先把 RuoYi 的分页链路完整拆一遍,找出“POST 请求分页失效”的根源,再给出三种可落地的改造方案,覆盖前端 api 封装、后端 Controller 和常用的请求包装器,顺便把我在真实项目里踩过的一些坑也整理出来。内容以 RuoYi-Vue 前后端分离版本为例,后面统称若依。

1. 先从“POST 改完永远第一页”说起

1.1 问题现场复现

很多项目为了列表查询条件的统一性和请求体安全,会想把查询接口从 GET 改成 POST。改之前代码通常是若依生成器默认的样子,SysUserController 里是@GetMapping("/list"),SysUser 作为查询条件参数接收,前端 API 文件里是method: 'get',表格组件通过params传递分页参数。这时页面一切正常。

接着大家会做两步改动。后端@GetMapping("/list")改成@PostMapping("/list"),前端 api 文件从params: query改成data: query,列表页面把分页参数也并到 query 对象里一起发送。重新刷新页面后会发现,列表数据还在,但左下角的总条数不对,点击第 2 页、第 3 页没有任何反应,永远返回第一页的数据。如果后端在 Controller 方法上顺手加了@RequestBody SysUser user,还可能遇到完全查不到任何数据的情况。

这两类问题,本质上不是同一个错误,但根源都指向同一个点:分页插件根本没有拿到前端传过来的页码。我在排查时是按这个顺序来定位的。

1.2 顺着调用链追到 TableSupport

如果你对若依的列表请求流程还不算陌生,那一定见过这三个类:BaseController、PageUtils、TableSupport。用户列表请求进来以后,Controller 方法调用startPage(),这个方法内部实际去构建一个 PageDomain,而 PageDomain 的数据来源是TableSupport.buildPageRequest()

下面是不带异常处理的简化流程:

// BaseController 里的封装 protected void startPage() { PageUtils.startPage(); }
// PageUtils 里的核心逻辑 public static void startPage() { PageDomain pageDomain = TableSupport.buildPageRequest(); Integer pageNum = pageDomain.getPageNum(); Integer pageSize = pageDomain.getPageSize(); String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy()); PageHelper.startPage(pageNum, pageSize, orderBy).setReasonable(true); }

也就是说,RuoYi 把“从请求中提取分页参数”这件事做成了统一入口,Controller 根本不用关心分页字段是怎么来的。这个设计对 GET 请求很友好,因为 GET 的查询参数会全部进入request.getParameterMap()TableSupport从 request parameter 里能直接拿到pageNumpageSize

问题恰恰在这里:当前端用 POST 且请求体是 JSON 时,request.getParameterMap()是空的。浏览器或 axios 发送的 JSON 请求体是原始字节流,Servlet 容器不会主动把它解析成 parameter key-value。于是TableSupport拿不到任何分页字段,最终PageHelper.startPage()会用一个兜底默认值去执行,页码固定在 1,每页大小固定在 10。这就是为什么页面永远只有第一页。

如果你在这个基础上又给 Controller 方法加了@RequestBody,情况会更隐蔽。@RequestBody能让业务查询条件完整绑定到 SysUser,但 PageHelper 依然走的是TableSupport,它依然拿不到 body 里的分页字段。两条读取路径完全不一致,造成“条件生效了、分页没生效”的怪异局面。

2. 误区复盘:不是请求方法的问题,是参数可见性的问题

2.1 若依并没有在框架层限制 GET

RuoYi 里分页相关的源码,我翻来翻去也没看到类似if ("GET".equals(request.getMethod()))之类的判断。很多初学者会从前端默认 API 写法反推框架设计:

export function listUser(query) { return request({ url: '/system/user/list', method: 'get', params: query }) }

这是若依代码生成器生成的标准写法,看起来像是“分页列表必须走 GET”。但认真想一下就会明白,GET 只是若依列表页的前端习惯,不是后端能力的上限。PageHelper 本身由PageHelper.startPage(pageNum, pageSize, orderBy)触发,它在 Mapper 执行前把参数放入 ThreadLocal,等下一个 SQL 查询执行时自动拼接 limit 语句。只要这条 SQL 前面的任意位置能拿到 pageNum 和 pageSize,请求方式根本不重要。

哪怕是 POST 请求,只要请求体的编码方式是application/x-www-form-urlencoded,Servlet 容器也会把 key-value 放进request.getParameterMap(),RuoYi 的分页代码就能正常读到页码。这也是为什么有些同学把列表改成 POST 后“碰巧能用”,因为他们用 form 表单或 axios 拼接字符串传参,没有遇到 JSON body 的坑。

2.2 关键结论:分页插件看不见 JSON body

把几种常见请求方式过一遍,结论会更清楚:

请求方式与内容类型request.getParameterMap()能否读到分页参数RuoYi 默认分页代码能否生效
GET + URL query
POST +application/x-www-form-urlencoded
POST +application/json不能不能
POST +text/plain不能不能

RuoYi 的 TableSupport 走的是 Servlet 标准的 parameter 机制,而 axios 默认发送 JSON 时,参数写在原始请求体里。JSON 只有被@RequestBody或专门的 JSON 解析器读取后,才能映射成 Java 对象。这两个体系之间天然隔着一段距离。

想支持 POST JSON,核心就一句话:要么想办法把 JSON 里的pageNumpageSize等字段搬到request.getParameterMap()里,要么干脆绕过 TableSupport,在 SQL 执行前手动把分页参数塞给 PageHelper。后面的三种方案,全是围绕这个核心展开的。

3. POST 分页的三种落地姿势

3.1 方案一:前端改成 POST,但请求体保持表单编码

这是改动量最小的方案。如果你不想动后端 Controller,也不打算引入额外过滤器,可以在前端 api 封装层做一个小转换。页面还是正常传 query 对象,发送时把对象转成 URL 编码字符串,同时把请求头设置成application/x-www-form-urlencoded

import request from '@/utils/request' import qs from 'qs' export function listUser(query) { return request({ url: '/system/user/list', method: 'post', headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, data: qs.stringify(query) }) }

后端 Controller 不用加@RequestBody,保持原来的参数接收方式:

@PostMapping("/list") public TableDataInfo list(SysUser user) // 注意不要加 @RequestBody { startPage(); List<SysUser> list = userService.selectUserList(user); return getDataTable(list); }

Spring MVC 从 request parameter 里绑定对象属性,表单编码的 body 会进 parameter,所以 SysUser 里的 userName、status 等查询条件能绑上,RuoYi 的 TableSupport 也能读到页码。这个方案适合只想小范围改、不想折腾后端的老项目。

但这个方案有几个明显短板。第一,若依的查询参数偶尔会出现params[beginTime]params[endTime]这类 BaseEntity 预留参数,qs 能处理,但遇到复杂嵌套对象时,代码维护起来不够直观。第二,列表搜索条件如果包含数组、对象或文件,表单编码会变得很难受。第三,前后端明明说好用 JSON,最后发出去的是表单,接口文档容易对不上。

所以我实际项目中很少把这个当成“终极改造”,更多是应急用。特别是在临时需要给第三方系统提供 POST 形式的分页接口时,后端不想大改,前端又不是我维护,那就让对接方自己把参数做成表单格式,两边代码都不动。

3.2 方案二:后端手动读取 body,把分页参数直接给 PageHelper

如果你希望保留 POST JSON body 的语义,那更推荐在后端做改造。最直观的做法是,Controller 使用@RequestBody接收一个查询对象,这个对象里包含分页字段和业务查询字段,然后手动调用 PageHelper。

先定义一个分页基础类。若依传统的 BaseController 分页方式依赖 PageDomain,但 PageDomain 是给 TableSupport 用的,它并不会自动从@RequestBody里取值。所以我自己会建立一个独立的分页查询基类:

import com.fasterxml.jackson.annotation.JsonIgnore; public class PageQuery { /** 当前页码 */ private Integer pageNum = 1; /** 每页显示条数 */ private Integer pageSize = 10; /** 排序列 */ private String orderByColumn; /** 排序方向 */ private String isAsc = "asc"; 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; } public String getOrderByColumn() { return orderByColumn; } public void setOrderByColumn(String orderByColumn) { this.orderByColumn = orderByColumn; } public String getIsAsc() { return isAsc; } public void setIsAsc(String isAsc) { this.isAsc = isAsc; } @JsonIgnore public String getOrderBy() { if (StringUtils.isEmpty(orderByColumn)) { return ""; } return StringUtils.toUnderScoreCase(orderByColumn) + " " + isAsc; } }

查询对象再去继承这个分页基础类,同时包含业务过滤字段:

public class SysUserPageQuery extends PageQuery { private String userName; private String status; private String phonenumber; private String beginTime; private String endTime; // getter/setter 省略 }

Controller 改造如下:

import com.github.pagehelper.PageHelper; import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.StringUtils; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @PreAuthorize("@ss.hasPermi('system:user:list')") @PostMapping("/list") public TableDataInfo list(@RequestBody(required = false) SysUserPageQuery query) { if (query == null) { query = new SysUserPageQuery(); } String orderBy = SqlUtil.escapeOrderBySql(query.getOrderBy()); PageHelper.startPage(query.getPageNum(), query.getPageSize(), orderBy); SysUser user = new SysUser(); user.setUserName(query.getUserName()); user.setStatus(query.getStatus()); user.setPhonenumber(query.getPhonenumber()); user.getParams().put("beginTime", query.getBeginTime()); user.getParams().put("endTime", query.getEndTime()); List<SysUser> list = userService.selectUserList(user); return getDataTable(list); }

注意这里我没有调用原来的startPage(),因为这个方法内部会走 TableSupport,它拿不到 body 参数。PageHelper 的职责是拦截数据库查询,只要你把起始页和每页大小传进去了,后续第一次查询就会自动分页。它本身和请求参数来源解耦,所以这种手动传参的方式在逻辑上是成立的。

这个方案的好处很明显:Controller 对 JSON body 的支持是显式、可控的。缺点是得为每个需要 POST 分页的接口额外建一个继承了 PageQuery 的查询对象,如果项目里要改的接口很多,工作量不小。对只想临时支持某个报表系统或第三方对接,倒是够用。

3.3 方案三:用请求包装器把 body JSON 映射为 parameter

第三个方案最省前端和后端接口调整,但对 Servlet 原理要求高一些。思路是写一个 Filter,当请求是 POST 且 Content-Type 为 JSON 时,把 body 字符串读出来,解析成 Map,然后将pageNumpageSizeorderByColumnisAsc这些字段放入自定义的 request 包装器,再放行。这样 TableSupport 从request.getParameterMap()里又能读到分页参数,RuoYi 原来的startPage()自然恢复作用。

这里有一个技术难点:request 的 body 只能读一次。如果 Filter 里先读了 body,后面 Controller 再使用@RequestBody就会读到空流。所以需要缓存 body 并重写getInputStream()/getReader(),让后续读取从缓存的字节数组里拿。下面是包装器的核心框架:

public class CacheBodyRequestWrapper extends HttpServletRequestWrapper { private final byte[] bodyBytes; private final Map<String, String[]> extraParams = new HashMap<>(); public CacheBodyRequestWrapper(HttpServletRequest request, String body, Map<String, Object> jsonMap) throws IOException { super(request); this.bodyBytes = body.getBytes(StandardCharsets.UTF_8); if (jsonMap != null) { for (Map.Entry<String, Object> entry : jsonMap.entrySet()) { Object value = entry.getValue(); if (value == null) { extraParams.put(entry.getKey(), new String[]{}); } else if (value instanceof Collection<?> collection) { extraParams.put(entry.getKey(), collection.stream() .map(String::valueOf).toArray(String[]::new)); } else { extraParams.put(entry.getKey(), new String[]{String.valueOf(value)}); } } } } @Override public String getParameter(String name) { String[] values = getParameterValues(name); if (values == null || values.length == 0) { return null; } return values[0]; } @Override public String[] getParameterValues(String name) { String[] original = super.getParameterValues(name); String[] extra = extraParams.get(name); if (original == null || original.length == 0) { return extra; } if (extra == null || extra.length == 0) { return original; } String[] merged = Arrays.copyOf(original, original.length + extra.length); System.arraycopy(extra, 0, merged, original.length, extra.length); return merged; } @Override public Map<String, String[]> getParameterMap() { Map<String, String[]> result = new HashMap<>(super.getParameterMap()); for (Map.Entry<String, String[]> entry : extraParams.entrySet()) { if (result.containsKey(entry.getKey())) { result.put(entry.getKey(), entry.getValue()); } else { result.put(entry.getKey(), entry.getValue()); } } return result; } @Override public ServletInputStream getInputStream() throws IOException { ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyBytes); return new ServletInputStream() { @Override public int read() throws IOException { return byteArrayInputStream.read(); } @Override public boolean isFinished() { return byteArrayInputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { // 按需实现 } }; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8)); } }

Filter 里做拦截和 JSON 解析。如果 JSON 解析失败,直接放行原始请求,避免影响其他接口:

@Component public class PagePostFilter implements Filter { private static final String CONTENT_TYPE_JSON = "application/json"; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; if ("POST".equalsIgnoreCase(request.getMethod()) && request.getContentType() != null && request.getContentType().startsWith(CONTENT_TYPE_JSON)) { String body = readBody(request); Map<String, Object> jsonMap = parseBody(body); CacheBodyRequestWrapper wrapper = new CacheBodyRequestWrapper(request, body, jsonMap); chain.doFilter(wrapper, response); return; } chain.doFilter(request, response); } // 读取 body、解析 JSON 的辅助方法省略 }

这个方案看起来很“通用”,能让你在几乎不改 Controller 的情况下继续使用 RuoYi 默认的列表方法,甚至前端可以放心地发送 JSON。但我强烈建议,不要为了这个方案把所有 POST JSON 请求都包一层。它有几个实践代价:

  • 每个 POST JSON 请求都会缓存请求体,大字段上传接口会被白白吃掉内存。
  • body 解析后的 Map 与request.getParameterMap()混合,遇到同名参数会发成合并逻辑,容易产生歧义。
  • 若依中很多查询条件不是简单字符串,像params[beginTime]这样的参数如果被错误平铺到 parameter 里,可能破坏原查询逻辑。

我自己的落地方案通常是方案二为主、方案三为辅。只给路由路径包含/list/page的核心接口加这个 Filter,控制拦截范围,不全局拦截。

对比项方案一:表单编码方案二:手动 PageHelper方案三:请求包装器
是否保留 POST JSON否,转为表单
后端 Controller 改动量几乎为零每个接口要调整初始写一次核心逻辑即可
是否影响其他接口需要控制拦截范围
适合场景临时对接、应急调整明确以 POST JSON 为标准的长期项目通用组件、RuoYi 二次开发平台

4. 实操示例:用户管理页 POST JSON 分页改造

下面用若依用户管理页做一个完整的最小改造示例。这个例子既包含前端页面,也包含后端查询对象和 Mapper 的复用,实践性更强。

4.1 后端创建查询对象

用户查询条件包含用户名、手机号、状态、时间范围。我直接把查询对象放在 controller 包下,不一定非要建一堆 DTO 包。

@Data @EqualsAndHashCode(callSuper = true) public class SysUserQuery extends SysUser { /** 当前页码,默认 1 */ private Integer pageNum = 1; /** 每页大小,默认 10 */ private Integer pageSize = 10; /** 排序列名 */ private String orderByColumn; /** 排序方向 */ private String isAsc; /** 开始时间 */ private String beginTime; /** 结束时间 */ private String endTime; }

为什么不直接让 Controller 接收SysUser?因为 SysUser 本身是业务实体,用它承载分页参数会破坏原有语义。继承 SysUser 能保留 MyBatis 的绑定字段映射,查询条件可以直接透传到selectUserList(SysUser user),很省事。

时间范围也可以塞到 SysUser 的 params 属性里,但那样 JSON 结构会变成params: { beginTime: '...' },前端传参更绕。我选择直接把 beginTime、endTime 平铺在查询对象上,Controller 里再转存。

4.2 后端 Controller 改造

@RestController @RequestMapping("/system/user") public class SysUserController extends BaseController { @Autowired private ISysUserService userService; @PreAuthorize("@ss.hasPermi('system:user:list')") @PostMapping("/list") public TableDataInfo list(@RequestBody(required = false) SysUserQuery query) { if (query == null) { query = new SysUserQuery(); } SysUser condition = new SysUser(); condition.setUserName(query.getUserName()); condition.setPhonenumber(query.getPhonenumber()); condition.setStatus(query.getStatus()); condition.getParams().put("beginTime", query.getBeginTime()); condition.getParams().put("endTime", query.getEndTime()); String orderBy = SqlUtil.escapeOrderBySql( StringUtils.isBlank(query.getOrderByColumn()) ? "" : StringUtils.toUnderScoreCase(query.getOrderByColumn()) + " " + query.getIsAsc()); PageHelper.startPage(query.getPageNum(), query.getPageSize(), orderBy).setReasonable(true); List<SysUser> list = userService.selectUserList(condition); return getDataTable(list); } }

几个细节我再强调一下。

StringUtils.toUnderScoreCase是把前端传来的驼峰列名转成数据库下划线风格,这是若依自带工具。不加的话,前端如果传orderByColumn: createTime,MyBatis 拼接 order by 时可能报列名不存在。

setReasonable(true)是 PageHelper 的合理化参数。若依原版PageUtils.startPage()里也保留了这一行。它保证页码小于 1 时自动归一,超过总页数时自动归到最后一页。手动开启能保持和原来一致的行为。

最终我还是通过condition去调用原来的userService.selectUserList。这样 Service 和 Mapper XML 完全不用动,MyBatis 只要看到非空字段就会拼接 where 条件。

4.3 前端 api 文件调整

api/system/user/index.js 的改动很小:

export function listUser(query) { return request({ url: '/system/user/list', method: 'post', data: query }) }

原来的params: query改成了data: query。由于 RuoYi 的 request.js 封装会在请求头里默认带上 JSON Content-Type,所以发送的就是标准 POST JSON。

前端页面getList方法里,query 对象和分页字段通常是分开的。ElementUI 的分页组件切换页码时会触发handleQuery,它把查询条件收集好之后,再通过 listUser 发送。若依页面里标准结构是这样的:

getList() { this.loading = true const query = { pageNum: this.queryParams.pageNum, pageSize: this.queryParams.pageSize, userName: this.queryParams.userName, status: this.queryParams.status, phonenumber: this.queryParams.phonenumber, beginTime: this.queryParams.params && this.queryParams.params.beginTime, endTime: this.queryParams.params && this.queryParams.params.endTime } listUser(query) .then(response => { this.userList = response.rows this.total = response.total this.loading = false }) }

如果你原来的 Vue 页面沿用了若依搜索表单,会有一点需要注意。若依搜索表单里的params.beginTime是封在queryParams.params里的一个子对象,后端直接接收 SysUser 时,SysUser 里也有 params 字段。所以列表搜索能匹配。现在改成 SysUserQuery 后,我额外平铺了 beginTime/endTime,前端如果还是传params:{beginTime:...},则后端query.getBeginTime()拿不到值。改成上面这种扁平写法,再由后端塞回 params,两边都能对上。

4.4 验证改造成果

启动项目后,打开用户管理页,在浏览器开发者工具 Network 面板观察请求。正常情况下能看到一个 POST 请求,路径是/dev-api/system/user/list,请求载荷是 JSON,里面包含 pageNum、pageSize、orderByColumn 和 isAsc。响应里的 total 应该是数据库总数,rows 是当前页数据。

点击第 2 页后再看请求载荷,pageNum 应该变为 2。如果请求体里 pageNum 变了,后端返回的仍是第一页,那大概率是后端用了原来的startPage(),它读不到 body 数据。如果返回的 total 是 0,先检查后端有没有把请求体正确绑定到 SysUserQuery 上,再检查 SysUserQuery 的 getter 是否完整。

5. 常见问题与踩坑实录

这个主题下,我在群里被问到最多的几种情况,基本集中在这几个点上。

  • Q1:后端方法签名保持SysUser user,前端改成 POST JSON,为什么查询条件全空?

    因为SysUser user不带@RequestBody时,Spring 把它当作 model attribute 绑定,数据来源是 request parameter。POST JSON body 不在 parameter 里,所以绑不到。解决办法要么加@RequestBody,把参数类型改成你自己的查询对象;要么使用方案一让前端发表单编码数据。

  • Q2:加@RequestBody后,RuoYi 的startPage()不报错,但分页无效。

    不报错是正常的。startPage()内部走 TableSupport,TableSupport 从request.getParameterMap()取值。JSON body 不会自动进 parameter map,所以 pageNum、pageSize 是 null。此时分页插件会走默认值或直接抛出“分页参数错误”。你真正要做的不是调用原来的startPage(),而是手动PageHelper.startPage(pageNum, pageSize, orderBy)

  • Q3:启动后报PageHelper 分页插件查询异常

    一般是因为startPage()之后,第一条执行的 SQL 不是目标列表 SQL。比如 Service 里先查了用户角色关联数据、先做了数据权限查询,再查用户列表。PageHelper 会对第一个查询生效,导致目标 SQL 反而没有分页。排查方式是把PageHelper.startPage的位置尽量靠近目标 Mapper 查询,中间不要穿插其他查询操作。

  • Q4:接口改 POST 后,前端出现 404 或 405。

    先确认后端 Controller 是否真的改成了@PostMapping。如果没有,前端 POST 到原 GET 路径上,路由不存在,Spring 会返回 404 或 405。另外看一下若依网关层的 CORS 配置。若依前后端分离开发时,浏览器会先发 OPTIONS 预检,如果网关只放行 GET 请求,POST 会被拦截,表现像是接口不存在。

  • Q5:ElementUI 分页组件的 layout 是 total、size、prev、pager、next,点击总条数没变。

    这不是请求方式问题,而是反馈的数据结构问题。若依的getDataTable返回的是{ rows: [], total: 0 },前端取 response.total。如果后端手动返回对象没有用 getDataTable 包装,total 字段可能取不到。检查 Network 响应体的结构,确保有 rows 和 total 两个字段。

  • Q6:项目同时用了 MyBatis-Plus 和 PageHelper,POST 分页总是不对。

    这是两个分页插件的冲突。若依从某个版本开始对 MyBatis-Plus 有额外支持,如果项目里同时存在 MyBatis-Plus 的PaginationInnerInterceptor和 PageHelper,两个插件会分别尝试解析 SQL,互相干扰。建议统一使用一种。用 MyBatis-Plus 时,分页对象直接用Page<T>,不再需要PageHelper.startPage()

  • Q7:POST JSON 发送大量过滤条件,Filter 缓存 body 后接口变慢。

    请求包装器会把整个 body 读入内存,如果列表过滤条件里带着很长的文本字段,内存和 CPU 成本都会上升。建议把过滤放到 doFilter 之后而不是每次都缓存。如果只是分页参数需要读取,也可以只解析前几 KB,不完整缓存整个 JSON。缓存整个 body 的方案只适合报文偏小、频率可控的业务。

  • Q8:POST 分页下,orderByColumncreateTime报 SQL 语法错误。

    前端传的是驼峰,数据库列名下划线,需要先用StringUtils.toUnderScoreCase()转换。同时记得用SqlUtil.escapeOrderBySql过滤掉分号、注释等特殊字符,否则有 SQL 注入风险。

6. 项目落地时我的选择与建议

若依的分页组件确实优秀,但它的优秀体现在对 GET 场景的默认封装顺手,而不是真的只接受 GET。理解清楚这一点之后,POST 分页不应再是拦路虎。

我自己在维护的项目里定过一条不成文规则:列表查询这类读取接口,如果查询条件复杂、字段多、后续可能扩展,就尽量统一走 POST JSON;如果只是简单的名称模糊搜索、状态筛选,那保留 GET 也未尝不可。改造时,我会优先把分页字段穷尽到一个基类 PageQuery 里,避免每个 Controller 都复制粘贴 pageNum、pageSize 三个 getter。

6.1 一个少踩坑的落地套路

最后分享一个我带团队时常用的套路。新接口如果一开始就要支持 POST JSON 分页,我不会等到写完再回头改,而是从 Controller 入口就把对象建模清楚。分页字段放 PageQuery 基类,业务过滤字段放子类,Service 的入参保持业务对象,Controller 里负责两个对象之间的转换。代码看起来多几行,但后续维护时最不容易出问题。

@PostMapping("/list") public TableDataInfo list(@RequestBody(required = false) SomePageQuery query) { query = query == null ? new SomePageQuery() : query; PageHelper.startPage(query.getPageNum(), query.getPageSize(), SqlUtil.escapeOrderBySql(query.getOrderBy())); // 转换查询条件,调用 service return getDataTable(list); }

注意 PageHelper 的 pageNum 和 pageSize 默认值。如果前端漏传,直接空指针报错的机会虽然不大,但要兜底。我在 PageQuery 基类里给默认值,同时在 Controller 入口判断空对象,相当于双保险。

6.2 关于“把分页参数放进参数对象”的另一层思考

把分页参数和业务查询参数放在同一个请求对象里,是很多团队反对的,认为分页属于“控制参数”,不该和业务查询条件混在一起。理论上这个分层很有道理,但实际对接前端时,如果你让前端把分页参数放进请求头、其他查询条件放 body,后端 TableSupport 为了兼容请求头反而要额外写代码。我试过一次这种“优雅拆分”,结果前端在 enctype、header 上反复出问题,最后又回到了统一放 body。

所以这里我的经验是:对外提供 HTTP 接口,可以让分页字段和查询字段平级共存;在系统内部,Controller 和服务层之间还是要保持查询对象干净。一层有一层的职责,别把所有维度都压在一个对象里。

6.3 顺带提醒:分页性能优化不靠请求方式

热搜词里我看到不少关于“分页查询慢怎么用 Redis 优化”的咨询。结合 POST 分页改造提一句:请求方式只是传输形式,真正决定性能的是 SQL 和表结构。若依默认的 PageHelper 在数据量超过几十万后,深分页需要扫描很多无用的 offset 行,这是数据库通用问题。要优化可以走覆盖索引、延迟关联、游标分页等方式,跟 GET 还是 POST 没有关系。如果你因为列表慢而把 GET 改成 POST,那是完全走错了方向。

开发若依项目时,很多问题并不是框架能力不够,而是我们对框架的预设不对。分页支持 POST 这件事也是如此——理解清楚参数读取链路后,你会发现其实可以有很多种解法。希望这篇笔记能帮你在下一次改造 POST 分页时少踩几个坑。

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

Go Context取消信号传播机制:源码拆解与实战避坑

做 Go 服务端开发的人&#xff0c;迟早会跟 context 打交道。很多人会用context.WithTimeout给接口套个超时&#xff0c;会用c.Request.Context()给下游调用传上下文&#xff0c;可一旦被问起"取消信号到底是怎么从父 context 一路传到子 context 的"&#xff0c;能讲…

作者头像 李华
网站建设 2026/9/9 3:59:39

RTOS任务同步与互斥:从信号量到优先级反转的uC/OS-II源码解读

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

作者头像 李华
网站建设 2026/9/9 3:59:37

硬件电路设计实战100例:从原理到量产的系统化拆解

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

作者头像 李华
网站建设 2026/9/9 3:56:56

AI会议纪要工具实测:角色分离、行动项提取与时间戳精度深度对比

1. 这不是“转文字”那么简单&#xff1a;为什么会议纪要工具选错&#xff0c;等于白忙活一整天你有没有过这种经历&#xff1a;开完一场两小时的跨部门协调会&#xff0c;满脑子都是待办事项&#xff0c;却卡在最后一步——把录音整理成可用的纪要。手动听写&#xff1f;三小时…

作者头像 李华
网站建设 2026/9/9 3:56:36

HTML+CSS+JS打造简洁漂亮的个人简历网页,纯静态零依赖

简介&#xff1a;一款简洁漂亮的个人简历HTML源码&#xff0c;适配个人主页、个人简介等展示场景&#xff0c;适合前端初学者学习页面布局&#xff0c;也方便求职者快速生成线上简历。源码包为ZIP压缩格式&#xff0c;共含18个文件&#xff0c;以HTML、CSS、JavaScript为核心&a…

作者头像 李华
网站建设 2026/9/9 3:55:51

电机热网络温度预测模型:从参数辨识到在线观测的工程实践

做电机台架试验那阵子&#xff0c;我白天测温升、晚上跑仿真&#xff0c;最头疼的事情不是试验设备出故障&#xff0c;而是仿真模型算出来的绕组温度和实测值对不上。有一次稳态工况下仿真给出的绕组热点只有85℃&#xff0c;实际热电偶已经测到了118℃&#xff0c;差了三十多度…

作者头像 李华