Spring MVC 实战:响应字段默认值设置

简介: 前言到今天为止,相信大家开发 Web 项目应该都是前后端分离了吧?前后端分离中一般会使用 json 作为前后端的数据交换格式。json 中可以包含数值、字符串、json 对象、数组等等。

前言


到今天为止,相信大家开发 Web 项目应该都是前后端分离了吧?前后端分离中一般会使用 json 作为前后端的数据交换格式。json 中可以包含数值、字符串、json 对象、数组等等。


由于 json 可以转换为 JavaScript 对象,取对象的字段时需要保证对象不能为 null,因此前端同学通常期望后端接口返回对象或数组类型的字段时设置一个默认值。如果每个接口单独设置默认值将会异常繁琐,我们尝试来进行一个全局性的处理。


分析


提到全局设置默认字段值,首先想到的就是使用 AOP 拦截 Controller 方法的执行,方法执行后设置返回值字段的默认值。不过对于将 Controller 方法返回值作为 json 类型的响应体这种情况,还有更好的处理方式。


在上篇《Spring MVC @RequestBody @ResponseBody 序列化反序列化实现》中我们也提到过,@ResponseBody 注解是由 HttpMessageConverter 处理的,Spring MVC 使用 HttpMessageConverter 将方法返回值写入响应体前事实上是有预留给用户的回调的。


image.png

回调的接口如下。


public interface ResponseBodyAdvice<T> {
  // 是否支持给定的返回类型
  boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
  // HttpMessageConverter 写响应前回调
  @Nullable
  T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
            Class<? extends HttpMessageConverter<?>> selectedConverterType,
            ServerHttpRequest request, ServerHttpResponse response);
}


Spring 会从 ControllerAdviceBean 中查找这个接口的实现,并回调符合条件的接口方法。为了将一个 bean 设置为 ControllerAdviceBean,可以在类上直接添加 @ControllerAdvice 注解。


实现


因此,我们直接定义一个实现 ResponseBodyAdvice 接口的类作为 ControllerAdviceBean 在将 Controller 方法返回值写入响应体前设置字段的默认值就可以了。


这里还是拿前面文章中使用的 User 类做测试,该类定义如下。


@Data
@Accessors(chain = true)
public class User {
    private String username;
    private String password;
    private Address address;
    private List<String> interests;
    private Map<String, Object> extra;
}
@Data
public class Address {
    private String province;
    private String city;
}


注意我们的 User 类字段包括普通的 String 类型,自定义的类型、List 类型 及 Map 类型。

接口返回的通用格式使用 Result 类表示。


@Data
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    public static <T> Result<T> ok(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMessage("OK");
        result.setData(data);
        return result;
    }
}


我们定义一个测试接口如下。


@RestController
public class UserController {
    @GetMapping("/login")
    public Result<User> login() {
        User user = new User();
        Result<User> result = Result.ok(user);
        return result;
    }
}


为了将 login 方法返回的 Result<User> 写入到响应体前设置默认字段值,我们可以定义 ResponseBodyAdvice 实现如下。


@ControllerAdvice
public class ResponseBodyDefaultValueAdvice implements ResponseBodyAdvice<Result<?>> {
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
      // 仅处理 Result 类型
        return returnType.getParameterType() == Result.class;
    }
    @Override
    public Result<?> beforeBodyWrite(Result<?> body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        Object data = body.getData();
        if (data == null) {
            data = BeanUtils.instantiateClass(ReflectionUtils.findField(body.getClass(), "data").getType());
        }
        setDefault(data);
        return body;
    }
    private void setDefault(Object obj) {
        ReflectionUtils.doWithFields(obj.getClass(), field -> {
            ReflectionUtils.makeAccessible(field);
            Object val = ReflectionUtils.getField(field, obj);
            if (val != null) {
                return;
            }
            // 字段默认值
            Object defaultValue = null;
            if (field.getType() == String.class) {
                defaultValue = "";
            } else if (field.getType() == List.class) {
                defaultValue = Collections.emptyList();
            } else if (field.getType() == Map.class) {
                defaultValue = Collections.emptyMap();
            } else if (field.getType().getPackage().getName().startsWith("com.zzuhkp")) {
                defaultValue = BeanUtils.instantiateClass(field.getType());
                setDefault(defaultValue);
            }
            // 设置字段默认值
            ReflectionUtils.setField(field, obj, defaultValue);
        });
    }
}


这里我们只处理 Result 类型的 Controller 方法返回值,返回值写入响应前,如果字段值为 null 则进行处理,并将 String 类型的值设置为空字符串、List 类型的值设置为空列表、Map 类型的值设置为空 Map 值,以及递归处理了自定义的类型。


测试接口返回内容如下。


{
    "code": 200,
    "message": "OK",
    "data": {
        "username": "",
        "password": "",
        "address": {
            "province": "",
            "city": ""
        },
        "interests": [],
        "extra": {}
    }
}


String、List、Map 以及自定义的 Address 类型都进行了处理,前端同学再也不会吐槽后端接口返回 null 了。

目录
相关文章
|
1月前
|
Java 测试技术 程序员
为什么Spring不推荐@Autowired用于字段注入?
作为Java程序员,Spring框架在日常开发中使用频繁,其依赖注入机制带来了极大的便利。然而,尽管@Autowired注解简化了依赖注入,Spring官方却不推荐在字段上使用它。本文将探讨字段注入的现状及其存在的问题,如难以进行单元测试、违反单一职责原则及易引发NPE等,并介绍为何Spring推荐构造器注入,包括增强代码可读性和维护性、方便单元测试以及避免NPE等问题。通过示例代码展示如何将字段注入重构为构造器注入,提高代码质量。
|
25天前
|
自然语言处理 Java API
Spring Boot 接入大模型实战:通义千问赋能智能应用快速构建
【10月更文挑战第23天】在人工智能(AI)技术飞速发展的今天,大模型如通义千问(阿里云推出的生成式对话引擎)等已成为推动智能应用创新的重要力量。然而,对于许多开发者而言,如何高效、便捷地接入这些大模型并构建出功能丰富的智能应用仍是一个挑战。
98 6
|
28天前
|
缓存 NoSQL Java
Spring Boot与Redis:整合与实战
【10月更文挑战第15天】本文介绍了如何在Spring Boot项目中整合Redis,通过一个电商商品推荐系统的案例,详细展示了从添加依赖、配置连接信息到创建配置类的具体步骤。实战部分演示了如何利用Redis缓存提高系统响应速度,减少数据库访问压力,从而提升用户体验。
69 2
|
2月前
|
Java Spring
spring多线程实现+合理设置最大线程数和核心线程数
本文介绍了手动设置线程池时的最大线程数和核心线程数配置方法,建议根据CPU核数及程序类型(CPU密集型或IO密集型)来合理设定。对于IO密集型,核心线程数设为CPU核数的两倍;CPU密集型则设为CPU核数加一。此外,还讨论了`maxPoolSize`、`keepAliveTime`、`allowCoreThreadTimeout`和`queueCapacity`等参数的设置策略,以确保线程池高效稳定运行。
240 10
spring多线程实现+合理设置最大线程数和核心线程数
|
1月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
1月前
|
存储 Java API
简单两步,Spring Boot 写死的定时任务也能动态设置:技术干货分享
【10月更文挑战第4天】在Spring Boot开发中,定时任务通常通过@Scheduled注解来实现,这种方式简单直接,但存在一个显著的限制:任务的执行时间或频率在编译时就已经确定,无法在运行时动态调整。然而,在实际工作中,我们往往需要根据业务需求或外部条件的变化来动态调整定时任务的执行计划。本文将分享一个简单两步的解决方案,让你的Spring Boot应用中的定时任务也能动态设置,从而满足更灵活的业务需求。
84 4
|
1月前
|
Java 数据库连接 Spring
【2021Spring编程实战笔记】Spring开发分享~(下)
【2021Spring编程实战笔记】Spring开发分享~(下)
26 1
|
1月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
54 2
|
2月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
1月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
112 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习