序
如何在 SpringBoot3 中使用 Builder 模式 arguement 接收 RequestBody ?
对于熟悉 SpringBoot 的同学,这个问题可能是简单的。
而笔者对于这个问题,却兜兜转转费了很大一番功夫。
在这里,分享解决这一问题的过程,以及一点心得。与君共勉。
问题描述
从一段简单的代码开始
            
            
              less
              
              
            
          
          @RestController
public class CallOtherController {
  @PostMapping("/gitee-oauth")
  public Object giteeOauth (
    @RequestBody OauthBody oauthBody
  ) {
    System.out.println("oauthBody: " + oauthBody);
    return oauthBody;
  }
}众所周知,我们可以使用 @RequestBody 注解, 以接收 POST 请求的 body 。
于是乎,我们在客户端发起一个请求
            
            
              css
              
              
            
          
          const requestParams = {
  baseUrl: 'http://localhost:8080',
  url: '/gitee-oauth',
  data: {
    "code": "123",
    "client_id": "xxx"
  }
}如果我们用 Object 或者 JavaBean 来接收 RequestBody ,那么一切都是美好的。
我们能在控制台收获正确的打印
            
            
              bash
              
              
            
          
          // { "code": "123",  "client_id": "xxx" }
System.out.println("oauthBody: " + oauthBody);但是,如果我们使用 Builder 模式来接收 RequestBody。
            
            
              typescript
              
              
            
          
          public class OauthBody  {
  @JsonProperty("grant_type")
  private String grantType;
  private String code;
  @JsonProperty("client_id")
  private String clientId;
  private OauthBody() { }
  public static class Builder {
    private String grantType = "authorization_code";
    private String code;
    private String clientId = "c136b0b572e74c9ff459";
    public Builder grantType(String grantType) {
      this.grantType = grantType;
      return this;
    }
    public Builder code (String code) {
      this.code = code;
      return this;
    }
    public Builder clientId (String clientId) {
      this.clientId = clientId;
      return this;
    }
    public OauthBody build() {
      OauthBody e = new OauthBody();
      e.grantType = grantType;
      e.code = code;
      e.clientId = clientId;
      return e;
    }
  }
  public String grantType() {
    return grantType;
  }
  public String code() {
    return code;
  }
  public String clientId() {
    return clientId;
  }
}得到打印:
            
            
              ini
              
              
            
          
          oauthBody: OauthBody{grantType='null', code='null', clientId='xxx'}我们发现,OauthBody 的 Builder 模式,似乎并没有生效。 grantType的默认值, code的输入值,都没有被正确的生成。
问题分析
@RequestBody 不能正确的接收 Builder 模式的参数,我们需要想办法解决这个问题。
尝试
我们认为 Builder 模式是不符合标准的 JavaBean 规范的。
那么我们应该扩展 @RequestBody 以支持 Builder 模式的参数接收。
通过查阅资料,发现我们可以
- 通过 configureArgumentResolvers来自定义argument的解析器。
            
            
              typescript
              
              
            
          
          @Configuration
public class WebConfig implements WebFluxConfigurer {
  @Override
  public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
    configurer.addCustomResolver(new BuilderArgumentResolver());
  }
}- 而 customResolver 需要自定义一个注解
            
            
              less
              
              
            
          
          @Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestBodyBuilder {
}
- 替换 @RequestBody为@RequestBodyBuilder
            
            
              less
              
              
            
          
          @RestController
public class CallOtherController {
  @PostMapping("/gitee-oauth")
  public Object giteeOauth (
    @RequestBodyBuilder OauthBody oauthBody
  ) {
    System.out.println("oauthBody: " + oauthBody);
    return oauthBody;
  }
}- 实现解析器
            
            
              java
              
              
            
          
          package com.example.restservice.config;
import com.example.annotation.RequestBodyBuilder;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
@Component
public class BuilderArgumentResolver implements HandlerMethodArgumentResolver
{
    private final ObjectMapper objectMapper;
    // ObjectMapper is typically injected through the constructor in Spring.
    public BuilderArgumentResolver(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBodyBuilder.class);
    }
    @Override
    public Mono<Object> resolveArgument(
            MethodParameter param,
            BindingContext bindingContext,
            ServerWebExchange exchange
    ) {
        Mono<Object> theBody = exchange.getRequest()
                .getBody()
                .next()
                .flatMap(dataBuffer -> {
                    try {
                        Class<?> CoreClass = param.getParameterType();
                        // 获取静态 Builder 类
                        Class<?> BuilderClass = Class.forName(CoreClass.getName() + "$Builder");
                        System.out.println("BuilderClass: " + BuilderClass);
            
                        // 读取请求体数据
                        byte[] bytes = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(bytes);
                        String bodyString = new String(bytes, "utf-8");
                        System.out.println("bodyString: " + bodyString);
                        // 将请求体数据反序列化为 Builder 实例
                        Object coreObj = objectMapper.readValue(bodyString, CoreClass);
                        return Mono.just(coreObj);
                    } catch (Exception e) {
                        // If something goes wrong, we return an error signal.
                        return Mono.error(new RuntimeException("Error deserializing builder", e));
                    }
                });
        return theBody;
    }
}问题分析2
实现解析器 ,最关键的部分是, 将请求体数据反序列化为 Builder 实例。
实际上部分的实现,在上述代码中并不完整。
因为 objectMapper 可不认得,我们Builder模式的类。
似乎这便是所有问题的根源。
尝试2
很容易去思考,
Builder类的实现与 Jackson 的默认处理方式不兼容,
可能需要自定义一个JsonDeserializer。
我们做一下尝试,扩展的配置大概是这样的
            
            
              typescript
              
              
            
          
          import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.Module;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonBuilderDeserializerConfig {
    @Bean
    public Module builderModule() {
        SimpleModule module = new SimpleModule();
        
        // 找到所有 Builder
        Set<Class<?>> builderClasses = findAllBuilderClasses();
        for (Class<?> builderClass : builderClasses) {
            
            // [TODO] 实现 BuilderDeserializer
            module.addDeserializer(builderClass, new BuilderDeserializer());
        }
        return module;
    }
    // [TODO] 实现查找
    private Set<Class<?>> findAllBuilderClasses() {
       
        return new HashSet<>();
    }
}显然,自动查找所有使用Builder模式的类是一个非常复杂的任务。这种配置方式,似乎用于配置些特例更有意义。而 Builder是一种相对通用的模式。
方案
最终,我们能够定位到根源的问题,在 jackson 的序列化与反序列化。
通过查阅,我们不难找到@JsonDeserialize 、@JsonPOJOBuilder 等相关的注解
在目标类或Builder类上添加注解来指示Jackson正确地配合Builder模式。
            
            
              kotlin
              
              
            
          
          package com.example.restservice.gitee;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
@JsonDeserialize(builder = OauthBody.Builder.class)
public class OauthBody {
  @JsonProperty("grant_type")
  private String grantType;
  private String code;
  @JsonProperty("client_id")
  private String clientId;
  private OauthBody() { }
  @JsonPOJOBuilder(withPrefix = "")
  public static class Builder {
    private String grantType = "authorization_code";
    private String code;
    private String clientId = "c136b0b572e";
    // ...
    public OauthBody build() {
      OauthBody e = new OauthBody();
      e.grantType = grantType;
      e.code = code;
      e.clientId = clientId;
      return e;
    }
  }
    
    // ...
}⚠️
实际上,我们无需自定义新的注解,在 @RequestBody 解析的内部
也是通过
Jackson反序列化生成的 java 对象
综上,
使用 Builder 模式 arguement 接收 RequestBody ,
仅需要在 Builder 模式类上添加正确的注解,指示 Jackson 正确工作即可
结
通常,探索问题的过程,并没那么容易。
从结果来看,上述的过程,我们像是走了很多弯路。
但也有好的一面,
我们初步涉及了一些,相对底层的API,addCustomResolver 或是 addDeserializer ,
管中窥豹,了解了一点 @RequestBody 可能的原理。
这也许能在我们今后真正用到这些的时候,从容一些。
同时,我们也能通过
@JsonDeserialize 、@JsonPOJOBuilder
这一简单的解决方案,感受到工程师们的智慧。
优雅,十分优雅