人人都会AI编程

12.4 返回值处理:JSON 序列化、统一返回封装、视图解析

更新时间:2026-07-10

控制器方法的返回值最终需要转换成客户端能够理解的格式——通常是 JSON 字符串,或者渲染后的 HTML 页面。@ResponseBody@RestController 让 JSON 响应变得极其简单,但真实项目中远不止“返回一个对象”这么直白。你还需要考虑 序列化风格的一致性异常与空值的处理统一响应格式的封装,以及 传统视图渲染的场景。本节将这些高频需求逐一讲透。

12.4.1 JSON 序列化的配置与定制

Spring Boot 默认使用 Jackson 作为 JSON 处理器,并自动注册了 MappingJackson2HttpMessageConverter。通常情况下,返回一个 POJO 就能自动输出 JSON:

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userService.findById(id);
}

但默认的序列化行为往往不符合团队规范。你需要掌控以下方面。

1. 常用 Jackson 配置

application.yml 中即可完成最常见的定制:

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss        # 全局日期格式
    time-zone: GMT+8                         # 时区
    serialization:
      write-dates-as-timestamps: false       # 不输出时间戳
    default-property-inclusion: non_null     # 忽略 null 值
    property-naming-strategy: SNAKE_CASE     # 下划线风格(需 Jackson 2.12+)

这些配置背后,Spring Boot 会自动调整 ObjectMapper 的行为。只需引入 spring-boot-starter-web,无需额外代码。

2. 使用注解精确控制序列化

当全局配置无法满足某个实体的特殊需求时,Jackson 提供了丰富的注解:

public class User {
    @JsonIgnore        // 永远不序列化密码
    private String password;

    @JsonProperty("user_name")  // 重命名字段
    private String username;

    @JsonFormat(pattern = "yyyy/MM/dd")
    private LocalDate birthday;

    @JsonInclude(JsonInclude.Include.NON_EMPTY)  // 仅非空输出
    private String email;
}

3. 自定义 ObjectMapper 扩展

当需要全局添加序列化特性或模块时(例如支持 Java 8 时间类型、长整型转字符串防止前端精度丢失),可以在配置类中定制 Jackson2ObjectMapperBuilder

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
        return builder -> {
            builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
            builder.modules(new JavaTimeModule());  // 支持 LocalDateTime
            builder.serializationInclusion(JsonInclude.Include.NON_NULL);
            // Long 转 String,避免 JS 精度丢失
            builder.serializerByType(Long.class, ToStringSerializer.instance);
            builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
        };
    }
}

4. 处理循环引用与懒加载

JPA 实体双向关联时,序列化可能引发无限递归(StackOverflowError)。解决方案有多种:

  • 在一方标注 @JsonIgnore,切断序列化路径。
  • 使用 @JsonManagedReference@JsonBackReference 配合。
  • 引入 jackson-datatype-hibernate5 模块,它会自动处理未初始化的懒加载代理,输出 null 而不是抛出异常。

在大规模项目中,更推荐将实体与 VO/DTO 分离,序列化永远只针对 DTO 进行,彻底规避实体关联带来的陷阱。

12.4.2 统一返回封装:让 API 风格一致

真实项目中,几乎没有服务会裸奔一个实体出去。统一返回格式能够极大地降低前端对接成本,也便于全局异常处理和日志记录。业界常见的封装格式如下:

{
  "code": 200,
  "message": "success",
  "data": { ... }
}

1. 定义通用返回体

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
    private int code;
    private String message;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(200, "success", data);
    }

    public static <T> ApiResponse<T> error(int code, String message) {
        return new ApiResponse<>(code, message, null);
    }
}

然后在控制器中手动包装:

@GetMapping("/users/{id}")
public ApiResponse<User> getUser(@PathVariable Long id) {
    return ApiResponse.success(userService.findById(id));
}

这样写确实统一了格式,但每处方法都要手动包装,很繁琐,而且容易遗忘。

2. 使用 ResponseBodyAdvice 自动包装

Spring 提供的 ResponseBodyAdvice 可以在响应体写出之前进行拦截和替换,实现无侵入的统一封装:

@RestControllerAdvice
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        // 如果已经封装过,或者返回的是 String(避免类型转换异常),则跳过
        return !returnType.getParameterType().equals(ApiResponse.class)
                && !returnType.getParameterType().equals(String.class);
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        // 如果已经是 ApiResponse 类型则不处理(支持)
        if (body instanceof ApiResponse) {
            return body;
        }
        // 否则自动包裹为成功结果
        return ApiResponse.success(body);
    }
}

要点说明:

  • supports 方法判断是否需要拦截,这里避免对已经是 ApiResponse 的返回值重复包装,也避免干扰 String 返回值(因为 String 会被 StringHttpMessageConverter 处理,直接转成 JSON 会出问题)。
  • beforeBodyWrite 进行实际的包装。
  • @RestControllerAdvice 使得该 Advice 对所有 @Controller(含 @RestController)生效。

3. 特殊情况处理

  • String 返回值:如果控制器直接返回 String 且想包装,需要在 Advice 中对 String 特殊处理(将 ApiResponse 转为 JSON 字符串后再返回,注意设置正确的 Content-Type)。
  • 文件下载:返回 ResponseEntity<Resource>byte[] 时不应包装,可在 supports 中增加类型判断。
  • 异常响应:可以结合前文全局异常处理,在异常处理器中直接返回 ApiResponse.error(...),此时 Advice 的 supports 会将其排除,不再进行二次包装。

经过这样的设计,任何控制器方法只需返回业务数据本身,响应都会被自动纳入统一的 ApiResponse 信封,风格极度一致。

12.4.3 视图解析与页面渲染

虽然前后端分离已成主流,但仍有不少场景需要在服务端渲染页面(例如管理后台、邮件模板、或某些传统项目)。Spring MVC 对视图层的支持同样完善。

1. 视图解析器的工作原理

当控制器方法没有标注 @ResponseBody,而是返回一个 视图名称(String)或 ModelAndView 对象时,Spring MVC 会使用 视图解析器(ViewResolver) 去寻找真正的视图资源:

@Controller
public class HomeController {
    @GetMapping("/home")
    public String home(Model model) {
        model.addAttribute("message", "欢迎回来");
        return "home";    // 逻辑视图名
    }
}

InternalResourceViewResolver 默认会把 "home" 解析为 /WEB-INF/views/home.jsp。但在 Spring Boot 中,更推荐使用模板引擎。

2. Thymeleaf 模板引擎的集成

添加 spring-boot-starter-thymeleaf 后,Spring Boot 会自动配置 Thymeleaf 视图解析器,默认视图前缀为 classpath:/templates/,后缀为 .html。上面的 "home" 就会对应到 templates/home.html

Thymeleaf 页面示例:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><title>首页</title></head>
<body>
    <h1 th:text="${message}"></h1>
</body>
</html>

3. 转发与重定向

在控制器返回值中,可以使用特殊前缀来控制跳转行为:

  • return "forward:/api/users" — 服务器内部转发,浏览器地址栏不变。
  • return "redirect:/home" — 302 重定向,浏览器地址栏变为新 URL。

这在表单提交后的 Post-Redirect-Get 模式中非常实用,避免表单重复提交。

4. 同时支持 JSON 与视图

一个控制器类中,可以混用 @ResponseBody 和视图返回。只需在方法上按需标注即可,无需拆分两个 Controller。但建议按职责拆分为 @RestController(纯 API)和 @Controller(页面),使边界更清晰。

从 JSON 序列化的细节打磨,到统一返回封装的设计,再到传统视图渲染的平滑支持,Spring MVC 的返回值处理机制给予了开发者极大的自由度。当你把这些手段组合在一起,就能构建出风格一致、前后端协作流畅、同时兼顾页面与 API 的稳健 Web 层。