You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot 1.4→2.0迁移:Page分页JSON序列化反序列化异常

Spring Boot 2.0 中 Page 实例的序列化/反序列化解决方案与 MVC 分页最佳实践

我之前帮不少开发者踩过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控制器处理分页的最佳实践

为了避免后续版本升级再踩类似的坑,推荐几个最佳实践:

  1. 统一分页参数传递:直接用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
)
  1. 用自定义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类型。

  1. 避免HAL格式意外侵入:如果项目引入了Spring Data Rest,它会默认启用HAL格式,导致Page响应出现_embedded等字段。不需要的话直接关闭:
spring.data.rest.hal.enabled=false

或者在控制器上强制指定返回JSON格式:@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)

  1. 全局处理分页参数异常:当用户传入非法分页参数(比如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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:42:44