【Java 实战】如何在 SpringBoot3 中使用 Builder 模式变量 接收 RequestBody ?

如何在 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'}

我们发现,OauthBodyBuilder 模式,似乎并没有生效。 grantType的默认值, code的输入值,都没有被正确的生成。

问题分析

@RequestBody 不能正确的接收 Builder 模式的参数,我们需要想办法解决这个问题。

尝试

我们认为 Builder 模式是不符合标准的 JavaBean 规范的。

那么我们应该扩展 @RequestBody 以支持 Builder 模式的参数接收。

通过查阅资料,发现我们可以

  1. 通过 configureArgumentResolvers 来自定义 argument 的解析器。
typescript 复制代码
@Configuration
public class WebConfig implements WebFluxConfigurer {
​
  @Override
  public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
    configurer.addCustomResolver(new BuilderArgumentResolver());
  }
}
  1. 而 customResolver 需要自定义一个注解
less 复制代码
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestBodyBuilder {
}
​
  1. 替换 @RequestBody@RequestBodyBuilder
less 复制代码
@RestController
public class CallOtherController {
​
  @PostMapping("/gitee-oauth")
  public Object giteeOauth (
    @RequestBodyBuilder OauthBody oauthBody
  ) {
    System.out.println("oauthBody: " + oauthBody);
    return oauthBody;
  }
}
  1. 实现解析器
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

这一简单的解决方案,感受到工程师们的智慧。

优雅,十分优雅

相关推荐
蓝澈112113 分钟前
迪杰斯特拉算法之解决单源最短路径问题
java·数据结构
Kali_0720 分钟前
使用 Mathematical_Expression 从零开始实现数学题目的作答小游戏【可复制代码】
java·人工智能·免费
rzl0232 分钟前
java web5(黑马)
java·开发语言·前端
君爱学习37 分钟前
RocketMQ延迟消息是如何实现的?
后端
guojl1 小时前
深度解读jdk8 HashMap设计与源码
java
Falling421 小时前
使用 CNB 构建并部署maven项目
后端
guojl1 小时前
深度解读jdk8 ConcurrentHashMap设计与源码
java
程序员小假1 小时前
我们来讲一讲 ConcurrentHashMap
后端
爱上语文1 小时前
Redis基础(5):Redis的Java客户端
java·开发语言·数据库·redis·后端