一篇文章带了解如何用SpringBoot在RequestBody中优雅的使用枚举参数

目录
  • 确认需求
  • 定义枚举和对象
  • 实现转换逻辑
    • 方案一:精准攻击
    • 方案二:全范围攻击
  • 测试
  • 总结

确认需求

需求与前文类似,只不过这里需要是在 RequestBody 中使用。与前文不同的是,这种请求是通过 Http Body 的方式传输到后端,通常是 json 或 xml 格式,Spring 默认借助 Jackson 反序列化为对象。

同样的,我们需要在枚举中定义 int 类型的 id、String 类型的 code,id 取值不限于序号(即从 0 开始的 orinal 数据),code 不限于 name。客户端请求过程中,可以传 id,可以传 code,也可以传 name。服务端只需要在对象中定义一个枚举参数,不需要额外的转换,即可得到枚举值。

好了,接下来我们定义一下枚举对象。

定义枚举和对象

先定义我们的枚举类GenderIdCodeEnum,包含 id 和 code 两个属性:

public enum GenderIdCodeEnum implements IdCodeBaseEnum {
    MALE(1, "male"),
    FEMALE(2, "female");
    private final Integer id;
    private final String code;
    GenderIdCodeEnum(Integer id, String code) {
        this.id = id;
        this.code = code;
    }
    @Override
    public String getCode() {
        return code;
    }
    @Override
    public Integer getId() {
        return id;
    }
}

这个枚举类的要求与前文一致,不清楚的可以再去看一下。

在定义一个包装类GenderIdCodeRequestBody,用于接收 json 数据的请求体:

@Data
public class GenderIdCodeRequestBody {
    private String name;
    private GenderIdCodeEnum gender;
    private long timestamp;
}

除了GenderIdCodeEnum参数外,其他都是示例,所以随便定义一下。

实现转换逻辑

前奏铺垫好,接下来入正题了。Jackson 提供了两种方案:

  • 方案一:精准攻击,指定需要转换的字段,不影响其他类对象中的字段
  • 方案二:全范围攻击,所有借助 Jackson 反序列化的枚举字段,全部具备自动转换功能

方案一:精准攻击

这种方案中,我们首先需要实现JsonDeserialize抽象类:

public class IdCodeToEnumDeserializer extends JsonDeserializer<BaseEnum> {
    @Override
    public BaseEnum deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
            throws IOException {
        final String param = jsonParser.getText();// 1
        final JsonStreamContext parsingContext = jsonParser.getParsingContext();// 2
        final String currentName = parsingContext.getCurrentName();// 3
        final Object currentValue = parsingContext.getCurrentValue();// 4
        try {
            final Field declaredField = currentValue.getClass().getDeclaredField(currentName);// 5
            final Class<?> targetType = declaredField.getType();// 6
            final Method createMethod = targetType.getDeclaredMethod("create", Object.class);// 7
            return (BaseEnum) createMethod.invoke(null, param);// 8
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | NoSuchFieldException e) {
            throw new CodeBaseException(ErrorResponseEnum.PARAMS_ENUM_NOT_MATCH, new Object[] {param}, "", e);
        }
    }
}

然后在指定枚举字段上定义@JsonDeserialize注解,比如:

@JsonDeserialize(using = IdCodeToEnumDeserializer.class)
private GenderIdCodeEnum gender;

具体说一下每行的作用:

  • 获取参数值。根据需要,此处可能是 id、code 或 name,也就是源值,需要将其转换为枚举;
  • 获取转换上线文,这个是为 3、4 步做准备的;
  • 获取标记@JsonDeserialize注解的字段,此时currentName的值是gender;
  • 获取包装对象,也就是GenderIdCodeRequestBody对象;
  • 根据包装对象的Class对象,以及字段名gender获取Field对象,为第 5 步做准备;
  • 获取gender字段对应的枚举类型,也即是GenderIdCodeEnum。之所以这样做,是要实现一个通用的反序列化类;
  • 这里是写死的一种实现,就是在枚举类中,需要定义一个静态方法,方法名是create,请求参数是Object;
  • 通过反射调用create方法,将第一步获取的请求参数传入。

我们来看一下枚举类中定义的create方法:

public static GenderIdCodeEnum create(Object code) {
    final String stringCode = code.toString();
    final Integer intCode = BaseEnum.adapter(stringCode);
    for (GenderIdCodeEnum item : values()) {
        if (Objects.equals(stringCode, item.name())) {
            return item;
        }
        if (Objects.equals(item.getCode(), stringCode)) {
            return item;
        }
        if (Objects.equals(item.getId(), intCode)) {
            return item;
        }
    }
    return null;
}

为了性能考虑,我们可以提前定义三组 map,分别以 id、code、name 为 key,以枚举值为 value,这样就可以通过 O(1) 的时间复杂度返回了。可以参考前文的Converter类的实现逻辑。

这样,我们就可以实现精准转换了。

方案二:全范围攻击

这种方案是全范围攻击了,只要是 Jackson 参与的反序列化,只要其中有目标枚举参数,就会受到这种进入这种方案的逻辑中。这种方案是在枚举类中定义一个静态转换方法,通过@JsonCreator注解注释,Jackson 就会自动转换了。

这个方法的定义与方案一中的create方法完全一致,所以只需要在create方法上加上注解即可:

@JsonCreator(mode = Mode.DELEGATING)
public static GenderIdCodeEnum create(Object code) {
    final String stringCode = code.toString();
    final Integer intCode = BaseEnum.adapter(stringCode);
    for (GenderIdCodeEnum item : values()) {
        if (Objects.equals(stringCode, item.name())) {
            return item;
        }
        if (Objects.equals(item.getCode(), stringCode)) {
            return item;
        }
        if (Objects.equals(item.getId(), intCode)) {
            return item;
        }
    }
    return null;
}

其中Mode类有四个值:DEFAULT、DELEGATING、PROPERTIES、DISABLED,这四种的差别会在原理篇中说明。还是那句话,对于应用类技术,我们可以先知其然,再知其所以然,也一定要知其所以然。

测试

先定义一个 controller 方法:

@PostMapping("gender-id-code-request-body")
public GenderIdCodeRequestBody bodyGenderIdCode(@RequestBody GenderIdCodeRequestBody genderRequest) {
    genderRequest.setTimestamp(System.currentTimeMillis());
    return genderRequest;
}

然后定义测试用例,还是借助 JUnit5:

@ParameterizedTest
@ValueSource(strings = {"\"MALE\"", "\"male\"", "\"1\"", "1"})
void postGenderIdCode(String gender) throws Exception {
    final String result = mockMvc.perform(
            MockMvcRequestBuilders.post("/echo/gender-id-code-request-body")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"gender\": " + gender + ", \"name\": \"看山\"}")
    )
            .andExpect(MockMvcResultMatchers.status().isOk())
            .andDo(MockMvcResultHandlers.print())
            .andReturn()
            .getResponse()
            .getContentAsString();
    ObjectMapper objectMapper = new ObjectMapper();
    final GenderIdCodeRequestBody genderRequest = objectMapper.readValue(result, GenderIdCodeRequestBody.class);
    Assertions.assertEquals(GenderIdCodeEnum.MALE, genderRequest.getGender());
    Assertions.assertEquals("看山", genderRequest.getName());
    Assertions.assertTrue(genderRequest.getTimestamp() > 0);
}

总结

本文主要说明了如何在 RequestBody 中优雅的使用枚举参数,借助了 Jackson 的反序列化扩展,可以定制类型转换逻辑。

本篇文章就到这里了,希望能给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Java SpringBoot在RequestBody中高效的使用枚举参数原理案例详解

    在优雅的使用枚举参数(原理篇)中我们聊过,Spring对于不同的参数形式,会采用不同的处理类处理参数,这种形式,有些类似于策略模式.将针对不同参数形式的处理逻辑,拆分到不同处理类中,减少耦合和各种if-else逻辑.本文就来扒一扒,RequestBody参数中使用枚举参数的原理. 找入口 对 Spring 有一定基础的同学一定知道,请求入口是DispatcherServlet,所有的请求最终都会落到doDispatch方法中的ha.handle(processedRequest, respons

  • SpringBoot在RequestBody中使用枚举参数案例详解

    前文说到 优雅的使用枚举参数 和 实现原理,本文继续说一下如何在 RequestBody 中优雅使用枚举. 本文先上实战,说一下如何实现.在 优雅的使用枚举参数 代码的基础上,我们继续实现. 确认需求 需求与前文类似,只不过这里需要是在 RequestBody 中使用.与前文不同的是,这种请求是通过 Http Body 的方式传输到后端,通常是 json 或 xml 格式,Spring 默认借助 Jackson 反序列化为对象. 同样的,我们需要在枚举中定义 int 类型的 id.String

  • SpringBoot @RequestParam、@PathVaribale、@RequestBody实战案例

    实例User package com.iflytek.odeon.shipper.model.rx; import io.swagger.annotations.ApiModelProperty; public class Student { @ApiModelProperty(value = "名称", example = "zhangsan", required = true) private String name; private Integer call;

  • Springboot拦截器如何获取@RequestBody参数

    Springboot拦截器获取@RequestBody参数 HttpContextUtils import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader;

  • 关于Springboot | @RequestBody 接收到的参数对象属性为空的问题

    背景 今天在调试项目的时候遇到一个坑,用Postman发送一个post请求,在Springboot项目使用@RequestBody接收时参数总是报不存在,但是多次检查postman上的请求格式以及项目代码都没有问题 Postman: 请求参数: { "firstName":"fdsaf", "lastName":"dfasdf" } Controller: Entity 通过debug模式可以发现传进到实体的参数都为null

  • SpringBoot如何使用RequestBodyAdvice进行统一参数处理

    SpringBoot RequestBodyAdvice参数处理 在实际项目中 , 往往需要对请求参数做一些统一的操作 , 例如参数的过滤 , 字符的编码 , 第三方的解密等等 , Spring提供了RequestBodyAdvice一个全局的解决方案 , 免去了我们在Controller处理的繁琐 . RequestBodyAdvice仅对使用了@RqestBody注解的生效 , 因为它原理上还是AOP , 所以GET方法是不会操作的. package com.xbz.common.web;

  • 一篇文章带了解如何用SpringBoot在RequestBody中优雅的使用枚举参数

    目录 确认需求 定义枚举和对象 实现转换逻辑 方案一:精准攻击 方案二:全范围攻击 测试 总结 确认需求 需求与前文类似,只不过这里需要是在 RequestBody 中使用.与前文不同的是,这种请求是通过 Http Body 的方式传输到后端,通常是 json 或 xml 格式,Spring 默认借助 Jackson 反序列化为对象. 同样的,我们需要在枚举中定义 int 类型的 id.String 类型的 code,id 取值不限于序号(即从 0 开始的 orinal 数据),code 不限于

  • 一篇文章带你搞定SpringBoot中的热部署devtools方法

    一.前期配置 创建项目时,需要加入 DevTools 依赖 二.测试使用 (1)建立 HelloController @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello devtools"; } } 对其进行修改:然后不用重新运行,重新构建即可:只加载变化的类 三.热部署的原理 Spring Boot 中热部

  • 一篇文章带你搞定SpringBoot不重启项目实现修改静态资源

    一.通过配置文件控制静态资源的热部署 在配置文件 application.properties 中添加: #表示从这个默认不触发重启的目录中除去static目录 spring.devtools.restart.exclude=classpath:/static/** 或者使用: #表示将static目录加入到修改资源会重启的目录中来 spring.devtools.restart.additional-paths=src/main/resource/static 此时对static 目录下的静态

  • 一篇文章带你了解Java SpringBoot四大核心组件

    目录 一.Spring Boot Starter 1.1 Starter的应用示例 二.Spring Boot Autoconfigure 2.1 autoconfigure 简介 三.Spring Boot CLI 四.Spring Boot actuator 总结 一.Spring Boot Starter 1.1 Starter的应用示例 <dependency> <groupId>org.springframework.boot</groupId> <ar

  • 一篇文章带你了解Java SpringBoot Nacos

    目录 1.什么是Nacos 1.1与eureka对比 1.2与zookeeper对比 1.3与springcloud config 对比 2.Spring Cloud Alibaba 套件 3.Nacos 的架构和安装 3.1.Nacos 的架构 3.2.Nacos Server 的下载和安装 4.Nacos Server 的运行 4.1两种模式 4.2.standalone 模式 4.3.cluster 模式 4.输入以下命令即可启动服务: 总结 1.什么是Nacos Nacos 致力于帮助您

  • 一篇文章带你了解java Object根类中关于toString,equals的方法

    目录 toString: 代码案例: equals: 代码案例: 总结 toString: 概念:拼接对象的地址值:toString()方法用于返回表示对象值的字符串(返回的是String对象). 快捷写法:Alt+Insert,直接会显示toString的方法,选取需要返回的对象就行. 代码案例: 定义一个person类,属性如下: (1)身份证号,性别,姓名,年龄,户籍,出生日期(Data类型,需要引用java.uitl.Data)功能: (2)自我介绍:介绍格式:(toString) 身份

  • 一篇文章带你使用SpringBoot基于WebSocket的在线群聊实现

    一.添加依赖 加入前端需要用到的依赖: <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <

  • 一篇文章带你解决 IDEA 每次新建项目 maven home directory 总是改变的问题

    Maven是基bai于项目对象模型,可以通du过一小段描述信息来管理zhi项目的构建,报告和文档的软件项dao目管理工具. 重装个系统,各种问题,idea 也出现各种问题 装了个新版的 idea 2020 2.x 版本的,不知道咋回事,其他都好使,就是创建 SpringBoot 项目时: 加载 pom.xml 总是出错,原因就是,新建立的项目 maven home directory 总是乱,没有安装 设置的默认方式 我试了,改当前项目的,不好使 该默认设置,不好使,网上的其他方法也试了,很奇怪

随机推荐