SpringBoot在RequestBody中使用枚举参数案例详解
前文说到 优雅的使用枚举参数 和 实现原理,本文继续说一下如何在 RequestBody 中优雅使用枚举。
本文先上实战,说一下如何实现。在 优雅的使用枚举参数 代码的基础上,我们继续实现。
确认需求
需求与前文类似,只不过这里需要是在 RequestBody 中使用。与前文不同的是,这种请求是通过 Http Body 的方式传输到后端,通常是 json 或 xml 格式,Spring 默认借助 Jackson 反序列化为对象。
同样的,我们需要在枚举中定义 int 类型的 id、String 类型的 code,id 取值不限于序号(即从 0 开始的 orinal 数据),code 不限于 name。客户端请求过程中,可以传 id,可以传 code,也可以传 name。服务端只需要在对象中定义一个枚举参数,不需要额外的转换,即可得到枚举值。
好了,接下来我们定义一下枚举对象。
定义枚举和对象
【SpringBoot在RequestBody中使用枚举参数案例详解】先定义我们的枚举类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; }@Overridepublic String getCode() {return code; }@Overridepublic Integer getId() {return id; }}
这个枚举类的要求与前文一致,不清楚的可以再去看一下。
在定义一个包装类
GenderIdCodeRequestBody
,用于接收 json 数据的请求体:@Datapublic class GenderIdCodeRequestBody {private String name; private GenderIdCodeEnum gender; private long timestamp; }
除了
GenderIdCodeEnum
参数外,其他都是示例,所以随便定义一下。实现转换逻辑 前奏铺垫好,接下来入正题了。Jackson 提供了两种方案:
- 方案一:精准攻击,指定需要转换的字段,不影响其他类对象中的字段
- 方案二:全范围攻击,所有借助 Jackson 反序列化的枚举字段,全部具备自动转换功能
这种方案中,我们首先需要实现
JsonDeserialize
抽象类:public class IdCodeToEnumDeserializer extends JsonDeserializer{@Overridepublic BaseEnum deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)throws IOException {final String param = jsonParser.getText(); // 1final JsonStreamContext parsingContext = jsonParser.getParsingContext(); // 2final String currentName = parsingContext.getCurrentName(); // 3final Object currentValue = https://www.it610.com/article/parsingContext.getCurrentValue(); // 4try {final Field declaredField = currentValue.getClass().getDeclaredField(currentName); // 5final Class> targetType = declaredField.getType(); // 6final Method createMethod = targetType.getDeclaredMethod("create", Object.class); // 7return (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 的反序列化扩展,可以定制类型转换逻辑。碍于文章篇幅,没有罗列大段代码。关注公号「看山的小屋」回复 spring 可以获取源码。关注我,下一篇我们进入原理篇。
推荐阅读
- SpringBoot 实战:一招实现结果的优雅响应
- SpringBoot 实战:如何优雅的处理异常
- SpringBoot 实战:通过 BeanPostProcessor 动态注入 ID 生成器
- SpringBoot 实战:自定义 Filter 优雅获取请求参数和响应结果
- SpringBoot 实战:优雅的使用枚举参数
- SpringBoot 实战:优雅的使用枚举参数(原理篇)
- SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数
- SpringBoot 实战:在 RequestBody 中优雅的使用枚举参数(原理篇)
推荐阅读
- 你到家了吗
- 闲杂“细雨”
- 杜月笙的口才
- 赢在人生六项精进二阶Day3复盘
- 祖母走了
- 樱花雨
- 眼观耳听美食的日子
- Activiti(一)SpringBoot2集成Activiti6
- “成长”读书社群招募
- 眉头开了