Spring Boot 1.4→2.0迁移:Page分页JSON序列化反序列化异常
我之前帮不少开发者踩过Spring Boot版本升级后分页接口序列化的坑,咱们一步步来解决你的问题:
一、先搞懂为什么会出问题
Spring Boot 2.0 对Spring Data分页组件的序列化逻辑做了调整:1.4版本中Page会直接序列化出content、number、size等扁平字段;而2.0默认启用了Spring Data的PageJacksonModule,序列化后的结构会包含更详细的分页元数据(比如pageable对象),甚至如果项目引入了Spring Data Rest,还会混入HAL格式的_embedded、_links字段——这直接导致老的RestTemplate客户端因为结构不匹配反序列化失败。
二、解决序列化与反序列化问题
1. 服务器端(控制器):统一Page输出格式
如果你想让输出格式和1.4保持一致,或者自定义更灵活的结构,有两种方案:
方案一:全局自定义Jackson配置
创建一个配置类,手动覆盖Page的序列化规则,避免默认模块的影响:
@Configuration public class JacksonConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder -> builder.modules(new PageJacksonModule() { @Override public void setupModule(SetupContext context) { super.setupModule(context); // 替换默认序列化器,输出你需要的字段 context.addSerializer(Page.class, new JsonSerializer<Page<?>>() { @Override public void serialize(Page<?> page, JsonGenerator gen, SerializerProvider serializers) throws IOException { gen.writeStartObject(); gen.writeObjectField("content", page.getContent()); gen.writeNumberField("pageNumber", page.getNumber()); gen.writeNumberField("pageSize", page.getSize()); gen.writeNumberField("totalElements", page.getTotalElements()); gen.writeNumberField("totalPages", page.getTotalPages()); gen.writeBooleanField("first", page.isFirst()); gen.writeBooleanField("last", page.isLast()); gen.writeEndObject(); } }); } }); } }
方案二:单个接口自定义序列化
如果只是个别接口需要调整格式,可以在返回值上标注@JsonSerialize指定自定义序列化器:
@RestController public class MyDataController { @GetMapping("/paged-data") @JsonSerialize(using = CustomPageSerializer.class) public Page<MyDTO> getPagedData(Pageable pageable) { return myDataService.fetchPagedData(pageable); } } // 自定义序列化器实现 public class CustomPageSerializer extends JsonSerializer<Page<?>> { @Override public void serialize(Page<?> page, JsonGenerator gen, SerializerProvider serializers) throws IOException { // 写入你需要的分页字段,和上面全局配置的逻辑一致 gen.writeStartObject(); gen.writeObjectField("content", page.getContent()); gen.writeNumberField("pageNumber", page.getNumber()); gen.writeNumberField("pageSize", page.getSize()); gen.writeNumberField("totalElements", page.getTotalElements()); gen.writeNumberField("totalPages", page.getTotalPages()); gen.writeBooleanField("first", page.isFirst()); gen.writeBooleanField("last", page.isLast()); gen.writeEndObject(); } }
2. 客户端(RestTemplate):适配新的Page结构
如果不想修改服务器端的输出,只需要让RestTemplate能正确解析新的Page结构,关键是注册Spring Data的Jackson模块:
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = new ObjectMapper(); // 注册PageJacksonModule,让Jackson识别Page类型 objectMapper.registerModule(new PageJacksonModule()); jacksonConverter.setObjectMapper(objectMapper); // 替换默认的Jackson转换器 restTemplate.getMessageConverters().replaceAll( converter -> converter instanceof MappingJackson2HttpMessageConverter ? jacksonConverter : converter ); return restTemplate; } }
调用时必须用ParameterizedTypeReference指定泛型(避免Java类型擦除导致反序列化失败):
ResponseEntity<Page<MyDTO>> response = restTemplate.exchange( "http://your-server.com/paged-data", HttpMethod.GET, null, new ParameterizedTypeReference<Page<MyDTO>>() {} ); Page<MyDTO> pagedData = response.getBody();
三、MVC控制器处理分页的最佳实践
为了避免后续版本升级再踩类似的坑,推荐几个最佳实践:
- 统一分页参数传递:直接用Spring Data的
Pageable作为接口参数,Spring Boot会自动解析请求中的page(默认0)、size(默认20)、sort(比如sort=id,desc)参数,无需手动处理:
@GetMapping("/data") public PageResponse<MyDTO> getPagedData(Pageable pageable) { Page<MyDTO> page = myService.getPagedData(pageable); return PageResponse.fromPage(page); }
如果需要自定义默认参数,用@PageableDefault注解:
public PageResponse<MyDTO> getPagedData( @PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable )
- 用自定义DTO封装分页响应:不要直接返回
Page类型,而是封装成自己的分页响应DTO,这样格式完全可控,不受Spring版本影响:
public class PageResponse<T> { private List<T> content; private int pageNumber; private int pageSize; private long totalElements; private int totalPages; private boolean first; private boolean last; // 构造方法、getter、setter省略 public static <T> PageResponse<T> fromPage(Page<T> page) { PageResponse<T> response = new PageResponse<>(); response.setContent(page.getContent()); response.setPageNumber(page.getNumber()); response.setPageSize(page.getSize()); response.setTotalElements(page.getTotalElements()); response.setTotalPages(page.getTotalPages()); response.setFirst(page.isFirst()); response.setLast(page.isLast()); return response; } }
控制器返回这个DTO,客户端反序列化也更简单,不用处理Spring Data的Page类型。
- 避免HAL格式意外侵入:如果项目引入了Spring Data Rest,它会默认启用HAL格式,导致
Page响应出现_embedded等字段。不需要的话直接关闭:
spring.data.rest.hal.enabled=false
或者在控制器上强制指定返回JSON格式:@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
- 全局处理分页参数异常:当用户传入非法分页参数(比如
size超过系统限制),Spring会抛出PageableInvalidArgumentException,建议全局捕获返回友好提示:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(PageableInvalidArgumentException.class) public ResponseEntity<ErrorResponse> handleInvalidPageable(PageableInvalidArgumentException e) { ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "分页参数错误:" + e.getMessage()); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } } // 简单的错误响应DTO public class ErrorResponse { private int code; private String message; // 构造方法、getter省略 }
内容的提问来源于stack exchange,提问作者Rwanou

