Thanks the answer of @jkee .
Here is my solution.
First, a custom annotation:
@Inherited
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamName {
  /**
   * The name of the request parameter to bind to.
   */
  String value();
}
A customer 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());
      }
    }
  }
}
A parameter resolver:
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);
  }
  /**
   * Get param mappings.
   * Cache param mappings in memory.
   *
   * @param targetClass
   * @return {@link Map<String, String>}
   */
  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, a bean configuration for adding ParamNameProcessor into the first of argument resolvers:
@Configuration
public class WebConfig {
  /**
   * Processor for annotation {@link ParamName}.
   *
   * @return ParamNameProcessor
   */
  @Bean
  protected ParamNameProcessor paramNameProcessor() {
    return new ParamNameProcessor();
  }
  /**
   * Custom {@link BeanPostProcessor} for adding {@link ParamNameProcessor} into the first of
   * {@link RequestMappingHandlerAdapter#argumentResolvers}.
   *
   * @return BeanPostProcessor
   */
  @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);
}
That's all.