티스토리 뷰

1. 요구 사항 

"파라미터에 URI와 동일 변수값을 입력 받는 경우 URI를 우선으로 바인딩 되게 해주세요." 

컨트롤러 

1
2
3
4
5
6
    @GetMapping(value={"/api/brands/{brandCd}/stores"})
    public BaseResult stores(StoreParam pStoreParam) {
        // ... 
        return null;
    }
 
cs

요청 URI : "/brands/bn0001/stores?brandCd=1234"


위와 같이 하는 경우 StoreParam안에 brandCd는 어떤 값이 입력될까요? 
정답은 "1234"입니다. 

서버 로그를 보면 아래와 같은 메시지를 확인 할 수 있습니다. 
Skipping URI variable 'brandCd' because request contains bind value with same name.

"URI에 값이 있는데 값이 전달이 안됩니다. " 
확인을 해보면 파라미터에 같은 이름의 변수가 선언이 되어 있고 값이 없는 경우입니다. 
전체 코드를 보면 컨트롤러에서는 Pathvariables와 request parameters를 Object로 바인딩하게 되어 있습니다.  
화면에서 데이터 처리 시 공통모듈에서 파라미터 값을 자동으로 만들어 주는데 이 값들이 Pathvariable과 동일명인 경우 충돌이 나서 발생하는 경우입니다.  
해결 방안은 공통에서 데이터 전송 시 관련 값들을 제거 해주거나 Controller에서 값을 받은 후 Pathvariable에 있는 값을 다시 넣어 주면 됩니다. But 이렇게 되면 전체적으로 소스가 엉망이 됩니다. 그래서 객체에 데이터 바인딩할 때 URI에 있는 Pathvariable를 우선으로 해야 할 필요가 생겼습니다. 

제약사항
SpringBoot : 2.1.1.RELEASE
Springframework : 5.1.3.RELEASE

2. 스프링에서 Pathvariable에 값 조회 확인  

org.springframework.web.servlet.view.RedirectView.java 참고 
1
2
3
4
5
6
7
    @SuppressWarnings("unchecked")
    private Map<StringString> getCurrentRequestUriVariables(HttpServletRequest request) {
        String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
        Map<StringString> uriVars = (Map<StringString>) request.getAttribute(name);
        return (uriVars != null) ? uriVars : Collections.<StringString> emptyMap();
    }
 
cs

Request parameters와 PathVariables 를 바인딩 하는 곳 
org.springframework.web.servlet.mvc.method.annotation.ExtendedServletRequestDataBinder.java 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package org.springframework.web.servlet.mvc.method.annotation;
 
import java.util.Map;
import javax.servlet.ServletRequest;
 
import org.springframework.beans.MutablePropertyValues;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.HandlerMapping;
 
/**
 * Subclass of {@link ServletRequestDataBinder} that adds URI template variables
 * to the values used for data binding.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 */
public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
 
    /**
     * Create a new instance, with default object name.
     * @param target the target object to bind onto (or {@code null}
     * if the binder is just used to convert a plain parameter value)
     * @see #DEFAULT_OBJECT_NAME
     */
    public ExtendedServletRequestDataBinder(@Nullable Object target) {
        super(target);
    }
 
    /**
     * Create a new instance.
     * @param target the target object to bind onto (or {@code null}
     * if the binder is just used to convert a plain parameter value)
     * @param objectName the name of the target object
     * @see #DEFAULT_OBJECT_NAME
     */
    public ExtendedServletRequestDataBinder(@Nullable Object target, String objectName) {
        super(target, objectName);
    }
 
 
    /**
     * Merge URI variables into the property values to use for data binding.
     */
    @Override
    @SuppressWarnings("unchecked")
    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
        Map<StringString> uriVars = (Map<StringString>) request.getAttribute(attr);
        if (uriVars != null) {
            uriVars.forEach((name, value) -> {
                if (mpvs.contains(name)) {
                    if (logger.isWarnEnabled()) {
                        logger.warn("Skipping URI variable '" + name +
                                "' because request contains bind value with same name.");
                    }
                }
                else {
                    mpvs.addPropertyValue(name, value);
                }
            });
        }
    }
 
}
 
cs

addBindValues 메소드를 보면 먼저 URI 값을 읽고 forEach로 돌면서 파라미터값에 값이 있는 경우 위의 메시지를 로그에 남기고 값이 없는 경우 추가해 주는 부분이 있습니다. 
저 부분을 아래와 같이 고쳐 주겠습니다. 
1
2
3
4
5
6
7
8
9
10
11
12
13
    @Override
    @SuppressWarnings("unchecked")
    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
        Map<StringString> uriVars = (Map<StringString>) request.getAttribute(attr);
        if (uriVars != null) {
            uriVars.forEach((name, value) -> {
                if(value != null && !value.isEmpty()) {
                    mpvs.addPropertyValue(name, value);
                }
            });
        }
    }
cs
이렇게 되면 Pathvariable에 값이 있는 경우 파라미터에 값을 무시해 버릴 수 있습니다.  

만약 파라미터에 값이 없는 경우 Pathvariable를 대체해주고 싶거나 이름을 바꾸고 싶거나 뭔가 필요한 로직이 있다면 저 부분을 수정하면 가능합니다. 

3. 관련 소스 확인 

WebMvcConfigurationSupport.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    @Bean
    public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
        RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
        adapter.setContentNegotiationManager(mvcContentNegotiationManager());
        adapter.setMessageConverters(getMessageConverters());
        adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
        adapter.setCustomArgumentResolvers(getArgumentResolvers());
        adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
 
        if (jackson2Present) {
            adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
            adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
        }
 
        AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
        configureAsyncSupport(configurer);
        if (configurer.getTaskExecutor() != null) {
            adapter.setTaskExecutor(configurer.getTaskExecutor());
        }
        if (configurer.getTimeout() != null) {
            adapter.setAsyncRequestTimeout(configurer.getTimeout());
        }
        adapter.setCallableInterceptors(configurer.getCallableInterceptors());
        adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());
 
        return adapter;
    }
 
    /**
     * Protected method for plugging in a custom subclass of
     * {@link RequestMappingHandlerAdapter}.
     * @since 4.3
     */
    protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
        return new RequestMappingHandlerAdapter();
    }
cs


RequestMappingHandlerAdapter.java 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
    @Nullable
    protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
            HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
 
        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        try {
            WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
            ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
 
            ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
            if (this.argumentResolvers != null) {
                invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
            }
            if (this.returnValueHandlers != null) {
                invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
            }
            invocableMethod.setDataBinderFactory(binderFactory);
            invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
 
            ModelAndViewContainer mavContainer = new ModelAndViewContainer();
            mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
            modelFactory.initModel(webRequest, mavContainer, invocableMethod);
            mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
 
            AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
            asyncWebRequest.setTimeout(this.asyncRequestTimeout);
 
            WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
            asyncManager.setTaskExecutor(this.taskExecutor);
            asyncManager.setAsyncWebRequest(asyncWebRequest);
            asyncManager.registerCallableInterceptors(this.callableInterceptors);
            asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
 
            if (asyncManager.hasConcurrentResult()) {
                Object result = asyncManager.getConcurrentResult();
                mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
                asyncManager.clearConcurrentResult();
                LogFormatUtils.traceDebug(logger, traceOn -> {
                    String formatted = LogFormatUtils.formatValue(result, !traceOn);
                    return "Resume with async result [" + formatted + "]";
                });
                invocableMethod = invocableMethod.wrapConcurrentResult(result);
            }
 
            invocableMethod.invokeAndHandle(webRequest, mavContainer);
            if (asyncManager.isConcurrentHandlingStarted()) {
                return null;
            }
 
            return getModelAndView(mavContainer, modelFactory, webRequest);
        }
        finally {
            webRequest.requestCompleted();
        }
    }
 
    private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
        Class<?> handlerType = handlerMethod.getBeanType();
        Set<Method> methods = this.initBinderCache.get(handlerType);
        if (methods == null) {
            methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
            this.initBinderCache.put(handlerType, methods);
        }
        List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
        // Global methods first
        this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
            if (clazz.isApplicableToBeanType(handlerType)) {
                Object bean = clazz.resolveBean();
                for (Method method : methodSet) {
                    initBinderMethods.add(createInitBinderMethod(bean, method));
                }
            }
        });
        for (Method method : methods) {
            Object bean = handlerMethod.getBean();
            initBinderMethods.add(createInitBinderMethod(bean, method));
        }
        return createDataBinderFactory(initBinderMethods);
    }
 
    protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
            throws Exception {
 
        return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
    }
 
 
cs


createDataBinderFactory에서 ServletRequestDataBinderFactory를 생성하는 부분을 확인할 수 있습니다. 


ServletRequestDataBinderFactory.java 소스 확인

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory {
 
    /**
     * Create a new instance.
     * @param binderMethods one or more {@code @InitBinder} methods
     * @param initializer provides global data binder initialization
     */
    public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
            @Nullable WebBindingInitializer initializer) {
 
        super(binderMethods, initializer);
    }
 
    /**
     * Returns an instance of {@link ExtendedServletRequestDataBinder}.
     */
    @Override
    protected ServletRequestDataBinder createBinderInstance(
            @Nullable Object target, String objectName, NativeWebRequest request) throws Exception  {
 
        return new ExtendedServletRequestDataBinder(target, objectName);
    }
 
}
cs


4. Tasks 

1. CustomExtendedServletRequestDataBinder.java 생성 

2. CustomServletRequestDataBinderFactory.java 생성

3. CustomRequestMappingHandlerAdapter.java 생성 

4. WebMvcConfigurationSupport Override 하기 


1. CustomExtendedServletRequestDataBinder.java 생성 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CustomExtendedServletRequestDataBinder extends ExtendedServletRequestDataBinder{
 
    public CustomExtendedServletRequestDataBinder(@Nullable Object target) {
        super(target);
    }
 
    public CustomExtendedServletRequestDataBinder(@Nullable Object target, String objectName) {
        super(target, objectName);
    }
 
    @Override
    @SuppressWarnings("unchecked")
    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
        Map<StringString> uriVars = (Map<StringString>) request.getAttribute(attr);
        if (uriVars != null) {
            uriVars.forEach((name, value) -> {
                if(value != null && !value.isEmpty()) {
                    mpvs.addPropertyValue(name, value);
                }
            });
        }
    }
 
}
cs

필요한 로직을 입맛대로 구현하면 됩니다. 

2. CustomServletRequestDataBinderFactory.java 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CustomServletRequestDataBinderFactory extends ServletRequestDataBinderFactory{
 
    public CustomServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
            @Nullable WebBindingInitializer initializer) {
        super(binderMethods, initializer);
    }
 
    /**
     * Returns an instance of {@link ExtendedServletRequestDataBinder}.
     */
    @Override
    protected ServletRequestDataBinder createBinderInstance(
            @Nullable Object target, String objectName, NativeWebRequest request) throws Exception  {
 
        return new CustomExtendedServletRequestDataBinder(target, objectName);
    }
}
cs

3. CustomRequestMappingHandlerAdapter.java 생성 

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter{
 
    public CustomRequestMappingHandlerAdapter() {
        super();
    }
 
    protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
            throws Exception {
        return new CustomServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
    }
 
}
cs

4. WebMvcConfigurationSupport Override 하기 

1
2
3
4
5
6
7
8
9
10
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
 
    @Override
    protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
        return new CustomRequestMappingHandlerAdapter();
    }
    ...
    ...
}
cs


springboot 1.5.9에서 2.1.1로 업데이트할 때 기존 WebMvcConfigurerAdapter.java를 상속하여 구현 하던 것을 WebMvcConfigurer.java를 implements하거나 WebMvcConfigurationSupport.java를 상속받아 구현해야 합니다. 

ExtendedServletRequestDataBinder를 커스트마이징하는 것인데 생각보다 작업해야 할게 좀 많았던 거 같습니다. 

 


@InitBinder를 이용하는 방법을 찾고 싶었으나 생각 보다 쉽지 않았던거 같습니다. 

이외 구글 검색하다가 애노테이션을 이용해서 데이터 바인딩시 이름을 바꾸는 내용이 있었는데 나름 재밌었습니다. 

다만 제가 구현할려는 거와 안 맞았지만 나중에 필요 시 참고하면 좋을 거 같습니다. 


참고 

https://stackoverflow.com/questions/8986593/how-to-customize-parameter-names-when-binding-spring-mvc-command-objects

댓글
댓글쓰기 폼