0

I've been searching for hours on the internet, as well as attempting the solutions i've found in order to work with a custom parameter annotation on a controller method.

The idea behind this is that i want to practice how to map requests, responses and all sort of things with custom annotations when working with spring.

So what i want is to create an annotation parameter which should create a Map instance, my interface is coded this way:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SearchCriteria {
    String value() default "";
}

The resolver:

public class SearchCriteriaResolver implements HandlerMethodArgumentResolver {
    private Logger log = LoggerFactory.getLogger(SearchCriteriaResolver.class);

    private Map<String, Object> parameters = new HashMap<>() {{
        put("name", "");
        put("limit", 10);
    }};

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(SearchCriteria.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        HttpServletRequest request = (HttpServletRequest) nativeWebRequest.getNativeRequest();

        log.info("Parameter test: " + request.getParameter("test"));

        return this.parameters;
    }
}

And the configurer:

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new SearchCriteriaResolver());
    }
}

I've found on the internet multiple times that handlers are created this way. So in the controller, i am making use of the annotation like this:

@RestController
@RequestMapping("/series")
public class SeriesController {
    private static final Logger log = LoggerFactory.getLogger(SeriesController.class);

    @Autowired
    SeriesService seriesService;

    @GetMapping
    public ResponseEntity<List<SeriesBatch>> getSeriesList(@SearchCriteria Map<String, Object> parameters) {
        log.info("GET /series/ -> getSeriesList");
        log.info(parameters.toString());

        List<SeriesBatch> seriesList = this.seriesService.findAll();

        return new ResponseEntity<>(seriesList, HttpStatus.OK);
    }

    ...
}

So i've been checking in the logs everytime this endpoint is triggered, but the log on the resolver is not triggered, and the log in the controller method only shows an empty object. I've debugged the application start to see if the resolvers.add is being invoked, and it is, but for some reason i don't know, the logic for this annotation is not being executed.

NOTE: I am learning spring as well as taking back JAVA after a long time, so i would appreciate if an explanation on why it has to be that way on the answer is given.

1 Answer 1

3

Refactor your code in a way that the data you stored in a java.util.Map instance before will now be stored in some other object, e.g. an instance of a custom class SearchParams. You could even wrap your map as a member in that class to keep things simple for now:

class SearchParams {

   private Map<String, Object> values = new HashMap<>(); 

   public void setValue(String key, Object value) {
       this.values.put(key, value);
   }

   public Object getValue(String key) {
       this.values.get(key);
   }

   public Map<String, Object> list() {
       return new HashMap<>(this.values);
   }
}

Now change your controller method to accept a SearchParams object instead of Map<String, Object>:

    public ResponseEntity<List<SeriesBatch>> getSeriesList(@SearchCriteria SearchParams parameters) { ... }

Last but not least you gotta change your @SearchCriteria annotation implementation, e.g. as follows:

public class SearchCriteriaResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.hasParameterAnnotation(SearchCriteria.class);
    }

    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {

        // somehow determine search params

        // setup + return your new SearchParams object to encapsulate your determined search params
        SearchParams searchParams = new SearchParams();
        searchParams.add("somekey", "somevalue");
        return searchParams;
    }
}

Now, plz try this out and let me know if it worked out well ;-)

Detailed explanation:

Have a look at Spring's org.springframework.web.method.support.HandlerMethodArgumentResolverComposite class, especially it's getArgumentResolver(MethodParameter parameter) method. Under the hood, Spring maintains a list of different HandlerMethodArgumentResolver instances and loops over them to find one that matches a given method argument (and later on map the method argument's value to some other object!). One of the registered HandlerMethodArgumentResolvers is of type org.springframework.web.method.annotation.MapMethodProcessor. MapMethodProcessor also matches if the parameter if of type java.util.Map and matches first (see attached screenshot below). That's why your custom HandlerMethodArgumentResolver never got called...

enter image description here

[[https://reversecoding.net/spring-mvc-requestparam-binding-request-parameters/]] shows an example where a java.util.Map is used as a controller method's argument -> search for '7. @RequestParam with Map'

Possible way to configure order / change priority of custom any HandlerMethodArgumentResolver in WebConfig configuration class:

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
    // add custom resolver to beginning of resolver list
    resolvers.add(0, new SearchCriteriaResolver());
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for your explanation. I did try your method, and it works halfway, but controller now throws an error: IllegalArgumentException: argument type mismatch. It does show the logs i had in the resolver but now the controller method is not invoked. Could this error be happening because i have a request filter?
Nevermind, i was able to figure out the fix. Thanks for your answer.
However, is it possible to have a custom annotation that would feed data into a Map like i was initially trying to attempt?
@OscarReyes I edited my answer. Feel free to try this out - haven't done that before ;-) Maybe your old implementation using the Map passed to the controller method would've worked that way, dunno. Let me know if it worked. Thx.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.