2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

なぜSpringのGetリクエストでは、Setterメソッドが必要でPostリクエストでは不要なの?

Posted at

なぜGetリクエストでは、Setterメソッドが必要で、Postリクエストでは不要なの?

Springの機能を再確認していて、GetでのリクエストとPostでのリクエストでBindの使用が異なることに気づいた。
いつも何気なく、Getter、Setterを作成してリクエストをバインドするオブジェクトを作成していたが、以下のような違いがあった。

  1. Postリクエストは、SetterメソッドがなくてもBindされる
  2. Getリクエストは、SetterメソッドがないとBindされない

なぜこのような動きになるのか詳細を見ていきます。

環境

Java:11
Spring:Spring Boot 2.4.2

事象

以下ソースで実行した際に、GetではResponseの設定がなく、PostではResponseの設定が行われる。

Input
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;
    }
}
GET
リクエスト
http://localhost:8080/hello?test=12&name=199

レスポンス
{
  "name": null,
  "test": null
}
POST
リクエスト
http://localhost:8080/hello
Body:{"test":12, "name":"456"}

レスポンス
{
  "name": "456",
  "test": "12"
}

Post時のBind

Postでは以下のクラスにてBindを実施している
@RequestBodyがついているオブジェクトは)
ObjectMapperを利用して、JSON⇒Javaオブジェクトへの変換をしていることが分かる。
そのため、Setterがなくても値のBindができるようです。
※ObjectMapperの仕様についてはまた後日

AbstractJackson2HttpMessageConverter
    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個所でやっていました。

  1. コンストラクタでのBind
  2. Setterを使用してのBind

まず、コンストラクタでのBindをみます。

コンストラクタでのBind

以下のメソッドで実施しています。
コンストラクタの引数の名称を取得して、その名称のリクエストがあればBindするような実装になっています。

ModelAttributeMethodProcessor
    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クラスを見ると分かりやすいと思います。

BeanWrapperImpl
    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できること初めて知りました。
まだまだ、勉強が足りませんね。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?