0

I use map to accept parameter, I send an Integer type parameter,I receive a String in Map ,I want use @initbinder to make the value into Integer, but my code don't work. and if I don't write @RequestParam the map will be null. What should I do? any advice are appreciative.

I debugged with spring source code just now ,I find a way to make it:

1.@RequestParam Map map will be resolved by RequestParamMapMethodArgumentResolver 2. I write my bean to overwrite it , so i can change the map value into integer. and i did it

  1. but,spring find MethodArgumentResolvers in order ,once it find RequestParamMapMethodArgumentResolver ,the loop will break ,so my bean never work.

so is there any way to overwrite default MethodArgumentResolver or remove it?

public class MapEditor extends PropertyEditorSupport {

@Override
public void setAsText(String text) throws IllegalArgumentException {
    System.out.println("+++++++++++++++++++++++++++++++++++");
    if(text.matches("\\d+")){
        super.setValue(Integer.parseInt(text));
    }
    super.setAsText(text);
}

@Override
public String getAsText() {
    System.out.println("======================================");
    return super.getAsText();
}
}

@InitBinder
public void map(WebDataBinder binder){
    binder.registerCustomEditor(Map.class,new MapEditor());
    binder.registerCustomEditor(Date.class,new DateEditor());
}

    @GetMapping("/loadMessageList2")
public String loadMessageList2(@RequestParam Map<String,Object> map){
    return "hello world" + map.get("start");
}

----------------after debug--------------------------

public class MyRequestParamMapMethodArgumentResolver extends 
RequestParamMapMethodArgumentResolver {
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable 
    ModelAndViewContainer mavContainer,
                              NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    Map map = (Map) super.resolveArgument(parameter,mavContainer,webRequest, binderFactory);
    Set<Map.Entry> set = map.entrySet();
    for (Map.Entry entry : set) {
        if(entry.getValue().toString().matches("\\d+")){
            entry.setValue(Integer.parseInt(entry.getValue().toString()));
        }
    }
    return map;
}
}

and register it

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> 
argumentResolvers) {
        argumentResolvers.add(new MyRequestParamMapMethodArgumentResolver());
    }
}
Liam lin
  • 385
  • 1
  • 3
  • 10

2 Answers2

0

First of all, the parameter map must with @RequestParam, so it could be resolver by RequestParamMapMethodArgumentResolver . this resolver will do the most job , resolver the parameter into map. So my job is to look through all the map's value and see if the value is a number and change it into Integer.Here is my final solution Write my resolver extents RequestParamMapMethodArgumentResolver :

public class MyRequestParamMapMethodArgumentResolver extends 
RequestParamMapMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(MyMap.class) != null;
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable 
ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, @Nullable 
WebDataBinderFactory binderFactory) throws Exception {
        Map map = (Map) super.resolveArgument(parameter,mavContainer,webRequest, 
binderFactory);
        Set<Map.Entry> set = map.entrySet();
        for (Map.Entry entry : set) {
            if(entry.getValue().toString().matches("\\d+")){
                entry.setValue(Integer.parseInt(entry.getValue().toString()));
            }
        }
        return map;
    }
}

register it with spring

    @Configuration
    public class MyRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
        @Override
        public void afterPropertiesSet() {
            super.afterPropertiesSet();
            List<HandlerMethodArgumentResolver> argumentResolvers = getArgumentResolvers();
            if (argumentResolvers != null) {
                List<HandlerMethodArgumentResolver> argumentResolversort = new ArrayList<>();
                boolean flag = false;
                for (HandlerMethodArgumentResolver argumentResolver : argumentResolvers) {
                    if (argumentResolver instanceof MyRequestParamMapMethodArgumentResolver) {
                        argumentResolversort.add(argumentResolver);
                        argumentResolvers.remove(argumentResolver);
                        flag = true;
                        break;
                    }
                }
                if(!flag){
                    argumentResolversort.add(new MyRequestParamMapMethodArgumentResolver());
                }
                argumentResolversort.addAll(argumentResolvers);
                setArgumentResolvers(argumentResolversort);
            }
        }
    }

it does not very gracefully , but it's for reason. wish someone could find a better way . wish it helps.

Liam lin
  • 385
  • 1
  • 3
  • 10
0

This is old but might help someone. So I have an app that has many @RestController class that use @InitBinder for validating specific DTO's. They all call @Service classes. I had a similar issue that you did with an argument that is a Map.

The argument is map because it is a set of filters that can vary and all parameters are optional with certain combinations valid.

For a List, I used this: Validation of a list of objects in Spring

But it didn't seem to work so easily for Map.

For validating which keys(request parameters) in the map were used, I called a method directly in my controller before the service call.

  @SneakyThrows
  public void validateDirectly(Map paramMap) {
    BindingResult errors = new BeanPropertyBindingResult(null, "paramMap");
    validate(paramMap, errors);
    if(errors.hasErrors()) {
      Method method = this.getClass().getMethod("validateDirectly", Map.class); //passing this method for convenience
      MethodParameter methodParameter = new MethodParameter(method , 0); //only 1 arg
      throw new MethodArgumentNotValidException(methodParameter, errors); //this will dovetail back into our config
    }
  }

So exception thrown is the same one the Spring Validation throws and we catch in our exception handler with:

  @ExceptionHandler({
            MethodArgumentNotValidException.class
    })
    public ResponseEntity<JsonError> handleValidationBadRequest
       ...
       ...

Then in the controller:

  @GetMapping()
  public List<MyDto> getStrains(@RequestParam @Valid Map<String,String> paramMap) { //Spring -> LinkedHashMap

    validator.validateDirectly(paramMap); //because initBinder won't do java.util.Map easily (at all?)

    if(paramMap != null) {
      return myService.findByCustomQuery(paramMap);
    } else {
      return myService.findAll();
    }
  }

Since this is a one off in this app, I am moving on with this. If anyone has a link to a solution like the ValidList link above, please post it.

Randy
  • 729
  • 5
  • 14