【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

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

优雅,十分优雅

相关推荐
Jasonakeke10 分钟前
Flask 处理响应
后端·python·flask
ProMan_XY14 分钟前
干货-并发编程提高——线程的唤醒(七)
java·开发语言
夜里慢慢行45618 分钟前
深入源码P3C-PMD:rule (4)
java
明码30 分钟前
python vscode带参数调试
java·vscode·python
宋发元34 分钟前
使用Go语言绘制水平柱状图教程
开发语言·后端·golang
九局下半41 分钟前
【SkyWalking】如何在业务系统中控制SkyWalkingAgent的生命周期
后端
竹竹零1 小时前
集合基础知识及练习
java·windows
ZQT向前进1 小时前
springbootTest报错error create datasource
java·开发语言·数据库
yicj1 小时前
注解Spring @AliasFor使用笔记
java·spring
叫我龙翔1 小时前
【C++】C++11的新特性 — function 包装器 , bind包装器
android·java·数据结构·c++·算法·学习方法