API input parameter는 underscore가 많지만 Java는 camelcase가 convention이다.
@RequestBody의 경우 Jackson의 프로퍼티 네이밍 전략 설정을 통해 자동으로 처리되나 @ModelAttribute를 사용하는 경우는 다르다.
추천 : setter 메서드를 underscore에 맞춰서 작성 -> 심플하고 확실하다.
RequestMappingHandlerAdapter 확장 : @ModelAttribute 바인딩 커스터마이징
@Configuration
protected static class WebMvcRegistrationsProvider {
@Bean
public WebMvcRegistrations requestMappingHandlerAdapterProvider() {
return new WebMvcRegistrations() {
@Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return new BaseRequestMappingHandlerAdapter();
}
};
}
}
@SuppressWarnings("squid:MaximumInheritanceDepth")
static class BaseRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
protected InitBinderDataBinderFactory createDataBinderFactory(final List<InvocableHandlerMethod> bm) {
return new ServletRequestDataBinderFactory(bm, super.getWebBindingInitializer()) {
@Override
protected ServletRequestDataBinder createBinderInstance(@Nullable final Object o, final String n, final NativeWebRequest wr) {
return new CamelCaseRequestDataBinder(o, n);
}
};
}
}
@Slf4j
static class CamelCaseRequestDataBinder extends ExtendedServletRequestDataBinder {
private static final CaseConverter CONVERTER = new CaseConverter();
CamelCaseRequestDataBinder(@Nullable final Object target, final String objectName) {
super(target, objectName);
}
@Override
protected void bindMultipart(final Map<String, List<MultipartFile>> mp, final MutablePropertyValues mpvs) {
mp.forEach((k, v) -> {
final String converted = CONVERTER.convert(k);
if (v.size() == 1) {
final MultipartFile value = v.get(0);
if (isBindEmptyMultipartFiles() || !value.isEmpty()) {
mpvs.add(converted, value);
}
} else {
mpvs.add(converted, v);
}
});
}
@Override
protected void addBindValues(final MutablePropertyValues mpvs, final ServletRequest request) {
@SuppressWarnings("unchecked")
final Map<String, String> pathVariables = (Map<String, String>)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (Objects.nonNull(pathVariables)) {
pathVariables.forEach((k, v) -> {
if (mpvs.contains(k)) {
log.warn("PathVariable '{}' Found In RequestParameters", k);
log.warn("Spring's Default Is 'Skip Overwriting' But We Overwrite !!");
}
mpvs.addPropertyValue(k, v);
});
}
}
}
static class ParameterNameWrapper extends HttpServletRequestWrapper {
private final Map<String, String[]> converted;
ParameterNameWrapper(final HttpServletRequest request, final CaseConverter converter) {
super(request);
converted = convertParameters(request.getParameterMap(), converter);
}
@Nullable @Override
public String getParameter(final String name) {
return Optional.ofNullable(converted.get(name)).map(v -> v[0]).orElse(null);
}
@Override
public Map<String, String[]> getParameterMap() {
return converted;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(converted.keySet());
}
@Nullable @Override
public String[] getParameterValues(final String name) {
return converted.get(name);
}
private Map<String, String[]> convertParameters(final Map<String, String[]> source, final CaseConverter converter) {
return source.entrySet().stream().collect(Collectors.toMap(e -> converter.convert(e.getKey()), Map.Entry::getValue));
}
}
static class ParameterNameFilter implements Filter {
private static final CaseConverter CONVERTER = new CaseConverter();
@Override
public void init(final FilterConfig filterConfig) { /* No Operation Here */ }
@Override
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
chain.doFilter(request instanceof HttpServletRequest ? new ParameterNameWrapper((HttpServletRequest)request, CONVERTER) : request, response);
}
@Override public void destroy() { /* No Operation Here */ }
}
@Configuration
protected static class FilterRegistererConfig {
@Bean
public FilterRegistrationBean<ParameterNameFilter> parameterNameFilterRegisterer() {
final FilterRegistrationBean<ParameterNameFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new ParameterNameFilter()); bean.setOrder(Ordered.HIGHEST_PRECEDENCE + 11);
return bean;
}
}