Thanks, answer @jkee.
Here is my solution.
First user annotation:
@Inherited @Documented @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ParamName { String value(); }
Client DataBinder:
public class ParamNameDataBinder extends ExtendedServletRequestDataBinder { private final Map<String, String> paramMappings; public ParamNameDataBinder(Object target, String objectName, Map<String, String> paramMappings) { super(target, objectName); this.paramMappings = paramMappings; } @Override protected void addBindValues(MutablePropertyValues mutablePropertyValues, ServletRequest request) { super.addBindValues(mutablePropertyValues, request); for (Map.Entry<String, String> entry : paramMappings.entrySet()) { String paramName = entry.getKey(); String fieldName = entry.getValue(); if (mutablePropertyValues.contains(paramName)) { mutablePropertyValues.add(fieldName, mutablePropertyValues.getPropertyValue(paramName).getValue()); } } } }
Parameter Converter:
public class ParamNameProcessor extends ServletModelAttributeMethodProcessor { @Autowired private RequestMappingHandlerAdapter requestMappingHandlerAdapter; private static final Map<Class<?>, Map<String, String>> PARAM_MAPPINGS_CACHE = new ConcurrentHashMap<>(256); public ParamNameProcessor() { super(false); } @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestParam.class) && !BeanUtils.isSimpleProperty(parameter.getParameterType()) && Arrays.stream(parameter.getParameterType().getDeclaredFields()) .anyMatch(field -> field.getAnnotation(ParamName.class) != null); } @Override protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) { Object target = binder.getTarget(); Map<String, String> paramMappings = this.getParamMappings(target.getClass()); ParamNameDataBinder paramNameDataBinder = new ParamNameDataBinder(target, binder.getObjectName(), paramMappings); requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(paramNameDataBinder, nativeWebRequest); super.bindRequestParameters(paramNameDataBinder, nativeWebRequest); } private Map<String, String> getParamMappings(Class<?> targetClass) { if (PARAM_MAPPINGS_CACHE.containsKey(targetClass)) { return PARAM_MAPPINGS_CACHE.get(targetClass); } Field[] fields = targetClass.getDeclaredFields(); Map<String, String> paramMappings = new HashMap<>(32); for (Field field : fields) { ParamName paramName = field.getAnnotation(ParamName.class); if (paramName != null && !paramName.value().isEmpty()) { paramMappings.put(paramName.value(), field.getName()); } } PARAM_MAPPINGS_CACHE.put(targetClass, paramMappings); return paramMappings; } }
Finally, the component configuration for adding ParamNameProcessor to the first of the argument converters:
@Configuration public class WebConfig { @Bean protected ParamNameProcessor paramNameProcessor() { return new ParamNameProcessor(); } @Bean public BeanPostProcessor beanPostProcessor() { return new BeanPostProcessor() { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof RequestMappingHandlerAdapter) { RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean; List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(adapter.getArgumentResolvers()); argumentResolvers.add(0, paramNameProcessor()); adapter.setArgumentResolvers(argumentResolvers); } return bean; } }; } }
Param Pojo:
@Data public class Foo { private Integer id; @ParamName("first_name") private String firstName; @ParamName("last_name") private String lastName; @ParamName("created_at") @DateTimeFormat(pattern = "yyyy-MM-dd") private Date createdAt; }
Controller Method:
@GetMapping("/foos") public ResponseEntity<List<Foo>> listFoos(@RequestParam Foo foo, @PageableDefault(sort = "id") Pageable pageable) { List<Foo> foos = fooService.listFoos(foo, pageable); return ResponseEntity.ok(foos); }
All this.