序
如何在 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
这一简单的解决方案,感受到工程师们的智慧。
优雅,十分优雅