SpringBoot practice: elegant use of enumeration parameters in RequestBody

Posted by bschmitt78 on Mon, 20 Dec 2021 11:46:35 +0100

This picture was created by Christian_Crowd stay Pixabay Publish on

Hello, I'm looking at the mountain.

As mentioned earlier Elegant use of enumeration parameters and Implementation principle , this article goes on to talk about how to use enumeration gracefully in RequestBody.

This article will start with the actual combat and talk about how to realize it. stay Elegant use of enumeration parameters Based on the code, we continue to implement. If you want to get the source code, you can pay attention to the public name "mountain viewing cabin" and reply to spring.

Confirm requirements

The requirements are similar to those above, except that they need to be used in the RequestBody. Different from the above, this request is transmitted to the back end through the Http Body, usually in json or xml format. Spring deserializes it as an object by default with the help of Jackson.

Similarly, we need to define the id of int type and the code of String type in the enumeration, The value of id is not limited to sequence number (i.e. orinal data starting from 0), and code is not limited to name. In the process of client request, you can pass id, code or name. The server only needs to define an enumeration parameter in the object and can get the enumeration value without additional conversion.

OK, let's define the enumeration object.

Define enumerations and objects

First define our enumeration class GenderIdCodeEnum, which contains two attributes: id and 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;
    }
}

The requirements of this enumeration class are the same as those above. If you are unclear, you can take another look.

Define a wrapper class GenderIdCodeRequestBody to receive the request body of json data:

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

Except for the GenderIdCodeEnum parameter, others are examples, so just define them.

Implement transformation logic

The prelude is well paved. Let's get to the point. Jackson offers two options:

  • Scheme 1: precise attack, specifying the fields to be converted without affecting the fields in other objects
  • Scheme 2: full range attack. All enumeration fields deserialized by Jackson have automatic conversion function

Scheme 1: precision attack

In this scheme, we first need to implement the JsonDeserialize abstract class:

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);
        }
    }
}

Then define the @ JsonDeserialize annotation on the specified enumeration field, such as:

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

Specifically, the role of each line:

  1. Gets the parameter value. As required, here may be id, code or name, that is, the source value, which needs to be converted into enumeration;
  2. Obtain the conversion online document, which is prepared for step 3 and 4;
  3. Get the field marked with @ JsonDeserialize annotation. At this time, the value of currentName is gender;
  4. Get the wrapper object, that is, the GenderIdCodeRequestBody object;
  5. Obtain the Field object according to the Class object of the wrapper object and the Field name gender to prepare for step 5;
  6. Get the enumeration type corresponding to the gender field, that is, GenderIdCodeEnum. The reason for this is to implement a general deserialization class;
  7. Here is an implementation of write dead, that is, in the enumeration class, you need to define a static method, the method name is create, and the request parameter is Object;
  8. Call the create method through reflection and pass in the request parameters obtained in the first step.

Let's take a look at the create method defined in the enumeration class:

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;
}

For performance considerations, we can define three groups of map s in advance, with id, code and name as key s and enumeration values as values, so that they can be returned through the time complexity of O(1). You can refer to the implementation logic of the Converter class above.

In this way, we can achieve accurate conversion.

Scheme 2: full range attack

This scheme is a full range attack. As long as Jackson participates in the deserialization and there are target enumeration parameters, it will be affected by the logic entering this scheme. This scheme is to define a static conversion method in the enumeration class. Jackson will automatically convert through @ JsonCreator annotation.

The definition of this method is completely consistent with the create method in scheme 1, so you only need to annotate the create method:

@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;
}

The Mode class has four values: DEFAULT, deleting, PROPERTIES and DISABLED. The differences between these four values will be explained in the principle chapter. In other words, for application technology, we can predict its nature, then know its reason, and we must know its reason.

test

First define a controller method:

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

Then define test cases, or with JUnit 5:

@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\": \"Look at the mountain\"}")
    )
            .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("Look at the mountain", genderRequest.getName());
    Assertions.assertTrue(genderRequest.getTimestamp() > 0);
}

Summary at the end of the paper

This article mainly explains how to gracefully use enumeration parameters in the RequestBody. With the help of Jackson's deserialization extension, you can customize the type conversion logic. Due to the length of the article, a large section of code is not listed. Follow the company's "mountain house" and reply to spring to obtain the source code. Pay attention to me. Next, let's enter the principle.

Recommended reading

Hello, I'm looking at the mountain. Swim in the code world and enjoy life. If the article is helpful to you, please like, collect and pay attention to it. I also compiled some excellent learning materials, and I would like to pay attention to the official account of "the mountain view cabin" and get the reply to "information".

Personal homepage: https://www.howardliu.cn
Personal blog: SpringBoot practice: elegant use of enumeration parameters in RequestBody
CSDN home page: https://kanshan.blog.csdn.net/
CSDN blog: SpringBoot practice: elegant use of enumeration parameters in RequestBody

Topics: Java Spring Spring Boot AOP