なぜGetリクエストでは、Setterメソッドが必要で、Postリクエストでは不要なの?
Springの機能を再確認していて、GetでのリクエストとPostでのリクエストでBindの使用が異なることに気づいた。
いつも何気なく、Getter、Setterを作成してリクエストをバインドするオブジェクトを作成していたが、以下のような違いがあった。
- Postリクエストは、SetterメソッドがなくてもBindされる
- Getリクエストは、SetterメソッドがないとBindされない
なぜこのような動きになるのか詳細を見ていきます。
環境
Java:11
Spring:Spring Boot 2.4.2
事象
以下ソースで実行した際に、GetではResponseの設定がなく、PostではResponseの設定が行われる。
public class HelloInput {
/** 名前 */
private String name;
/** テストパラメータ */
private String test;
public String getName() {
return name;
}
public String getTest() {
return test;
}
}
@RestController
@RequestMapping(value="/hello")
public class HelloController {
@RequestMapping(method = RequestMethod.GET)
@ResponseBody
public HelloInput index(HelloInput input) {
return input;
}
@RequestMapping(method = RequestMethod.POST)
@ResponseBody
public HelloInput post(@RequestBody HelloInput input) {
return input;
}
}
リクエスト
http://localhost:8080/hello?test=12&name=199
レスポンス
{
"name": null,
"test": null
}
リクエスト
http://localhost:8080/hello
Body:{"test":12, "name":"456"}
レスポンス
{
"name": "456",
"test": "12"
}
Post時のBind
Postでは以下のクラスにてBindを実施している
(@RequestBodyがついているオブジェクトは)
ObjectMapperを利用して、JSON⇒Javaオブジェクトへの変換をしていることが分かる。
そのため、Setterがなくても値のBindができるようです。
※ObjectMapperの仕様についてはまた後日
protected ObjectMapper objectMapper;
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
MediaType contentType = inputMessage.getHeaders().getContentType();
Charset charset = getCharset(contentType);
boolean isUnicode = ENCODINGS.containsKey(charset.name());
try {
if (inputMessage instanceof MappingJacksonInputMessage) {
Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
if (deserializationView != null) {
ObjectReader objectReader = this.objectMapper.readerWithView(deserializationView).forType(javaType);
if (isUnicode) {
return objectReader.readValue(inputMessage.getBody());
}
else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return objectReader.readValue(reader);
}
}
}
if (isUnicode) {
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
else {
Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
return this.objectMapper.readValue(reader, javaType);
}
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
}
}
Get時のBind
Get時のパラメータのBindは2個所でやっていました。
- コンストラクタでのBind
- Setterを使用してのBind
まず、コンストラクタでのBindをみます。
コンストラクタでのBind
以下のメソッドで実施しています。
コンストラクタの引数の名称を取得して、その名称のリクエストがあればBindするような実装になっています。
protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
if (ctor.getParameterCount() == 0) {
// A single default constructor -> clearly a standard JavaBeans arrangement.
return BeanUtils.instantiateClass(ctor);
}
// A single data class constructor -> resolve constructor arguments from request parameters.
String[] paramNames = BeanUtils.getParameterNames(ctor);
Class<?>[] paramTypes = ctor.getParameterTypes();
Object[] args = new Object[paramTypes.length];
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
boolean bindingFailure = false;
Set<String> failedParams = new HashSet<>(4);
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
Class<?> paramType = paramTypes[i];
Object value = webRequest.getParameterValues(paramName);
if (value == null) {
if (fieldDefaultPrefix != null) {
value = webRequest.getParameter(fieldDefaultPrefix + paramName);
}
if (value == null) {
if (fieldMarkerPrefix != null && webRequest.getParameter(fieldMarkerPrefix + paramName) != null) {
value = binder.getEmptyValue(paramType);
}
else {
value = resolveConstructorArgument(paramName, paramType, webRequest);
}
}
}
try {
MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName);
if (value == null && methodParam.isOptional()) {
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
}
else {
args[i] = binder.convertIfNecessary(value, paramType, methodParam);
}
}
catch (TypeMismatchException ex) {
ex.initPropertyName(paramName);
args[i] = null;
failedParams.add(paramName);
binder.getBindingResult().recordFieldValue(paramName, paramType, value);
binder.getBindingErrorProcessor().processPropertyAccessException(ex, binder.getBindingResult());
bindingFailure = true;
}
}
if (bindingFailure) {
BindingResult result = binder.getBindingResult();
for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
if (!failedParams.contains(paramName)) {
Object value = args[i];
result.recordFieldValue(paramName, paramTypes[i], value);
validateValueIfApplicable(binder, parameter, ctor.getDeclaringClass(), paramName, value);
}
}
if (!parameter.isOptional()) {
try {
Object target = BeanUtils.instantiateClass(ctor, args);
throw new BindException(result) {
@Override
public Object getTarget() {
return target;
}
};
}
catch (BeanInstantiationException ex) {
// swallow and proceed without target instance
}
}
throw new BindException(result);
}
return BeanUtils.instantiateClass(ctor, args);
}
SetterでのBind
BeanWrapperImplクラスの内部クラス(BeanPropertyHandler)を利用して実施しています。
PropertyDescriptorからWriterメソッドを利用して値を設定します。
PropertyDescriptorは、JavaBeanオブジェクトのプロパティ値のオブジェクトです。
PropertyDescriptorの設定方法は、Introspectorクラスを見ると分かりやすいと思います。
private final PropertyDescriptor pd;
@Override
public void setValue(@Nullable Object value) throws Exception {
Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ?
((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() :
this.pd.getWriteMethod());
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
ReflectionUtils.makeAccessible(writeMethod);
return null;
});
try {
AccessController.doPrivileged((PrivilegedExceptionAction<Object>)
() -> writeMethod.invoke(getWrappedInstance(), value), acc);
}
catch (PrivilegedActionException ex) {
throw ex.getException();
}
}
else {
ReflectionUtils.makeAccessible(writeMethod);
writeMethod.invoke(getWrappedInstance(), value);
}
}
Introspectorクラス抜粋
JavaBeanの各プロパティのGetter, Setterを取得している個所です。
"set" + プロパティのcapitalizeした値がSetterと認識されています。
static final String GET_PREFIX = "get";
static final String SET_PREFIX = "set";
if (read == null && write != null) {
read = findMethod(result.getClass0(),
GET_PREFIX + NameGenerator.capitalize(result.getName()), 0);
if (read != null) {
try {
result.setReadMethod(read);
} catch (IntrospectionException ex) {
// no consequences for failure.
}
}
}
if (write == null && read != null) {
write = findMethod(result.getClass0(),
SET_PREFIX + NameGenerator.capitalize(result.getName()), 1,
new Class<?>[] { FeatureDescriptor.getReturnType(result.getClass0(), read) });
if (write != null) {
try {
result.setWriteMethod(write);
} catch (IntrospectionException ex) {
// no consequences for failure.
}
}
}
最後に
リクエストのBindってこのようになっていたんですね。
setterメソッドがなくてもBindできること初めて知りました。
まだまだ、勉強が足りませんね。