Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to parse a URL and run a method with Spring MVC 'reflectively'?

I have a Spring Boot application that uses Spring MVC in the usual manner, with a bunch of @RequestMapping methods, Freemarker definitions, and the like. This is all tied together with a WebMvcConfigurerAdapter class.

I'd like to provide a service where the user submits a list of valid URLs, and the webapp would work out which controller would be called, passes in the parameters, and returns a combined result for every URL — all in one request.

This would save the user from having to make hundreds of HTTP calls, but would still allow them to make one-off requests if need be. Ideally, I'd just inject an auto-configured Spring bean, so I don't have to repeat the URL resolving and adapting and handling that Spring does internally, and the controller's list of other controllers would never go out of sync with the real list of controllers.

I expected to write something like this (simplified to only deal with one URL, which is pointless but easier to understand):

@Autowired BeanThatSolvesAllMyProblems allMappings;

@PostMapping(path = "/encode", consumes = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public String encode(@RequestBody String inputPath) {
    if (allMappings.hasMappingForPath(inputPath)) {
        return allMappings.getMapping(inputPath).execute();
    } else {
        return "URL didn't match, sorry";
    }
}

Instead, I've had to define Spring beans I don't know what they do and have been repeating some of what Spring is meant to do for me, which I'm worried won't work quite the same as it would if the user just made the call themselves:

// these two are @Beans, with just their default constructor called.
@Autowired RequestMappingHandlerMapping handlers;
@Autowired RequestMappingHandlerAdapter adapter;

@PostMapping(path = "/encode", consumes = MediaType.TEXT_PLAIN_VALUE)
@ResponseBody
public String encode(@RequestBody String inputText) {
    final HttpServletRequest mockRequest = new MockHttpServletRequest(null, inputText);
    final StringBuilder result = new StringBuilder();

    this.handlers.getHandlerMethods().forEach((requestMappingInfo, handlerMethod) -> {
        if (requestMappingInfo.getPatternsCondition().getMatchingCondition(mockRequest) != null) {
            try {
                final MockHttpServletResponse mockResponse = new MockHttpServletResponse();
                result.append("Result: ").append(adapter.handle(mockRequest, mockResponse, handlerMethod));
                result.append(", ").append(mockResponse.getContentAsString());
                result.append("\n");
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
            }
        }
    });

    return result.toString();
}

I thought I was doing quite well going down this path, but it's failing with Missing URI template variable errors, and not only do I have no idea how to put the request parameters in (another thing which Spring could be able to handle itself), but I'm not even sure that this is the right way to go about doing this. So how do I simulate a Spring MVC request "reflectively", from within the webapp itself?

like image 468
Ben S Avatar asked Aug 29 '16 18:08

Ben S


2 Answers

JSON API spec. solves this problem by allowing sending multiple operations per request. There even exists a quite mature implementation that supports this feature which is called Elide. But I guess this is might not fully meet your requirements.

Anyway, here's what you can do.

You have to take into consideration that DispatcherServlet holds handlerMappings list that is used to detect appropriate request handler and handlerAdaptors. The selection strategy for both lists is configurable (see DispatcherServlet#initHandlerMappings and #initHandlerAdapters).

You should work out a way you would prefer to retrieve this lists of handlerMappings/initHandlerAdapters and stay in sync with DispatcherServlet.

After that you can implement your own HandlerMapping/HandlerAdaptor (or present a Controller method as in your example) that would handle the request to /encode path.

Btw, HandlerMapping as javadoc says is

Interface to be implemented by objects that define a mapping between requests and handler objects

or simply saying if we take DefaultAnnotationHandlerMapping that would map our HttpServletRequests to @Controller methods annotated with @RequestMapping. Having this mapping HandlerAdapter prepares incoming request to consuming controller method, f.ex. extracting request params, body and using them to call controller's method.

Having this, you can extract URLs from main request, create a list of stub HttpRequests holding the information needed for further processing and loop through them calling this:

HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    for (HandlerMapping hm : this.handlerMappings) {
        if (logger.isTraceEnabled()) {
            logger.trace(
                    "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");
        }
        HandlerExecutionChain handler = hm.getHandler(request);
        if (handler != null) {
            return handler;
        }
    }
    return null;
}

having a handlerMapping you call

    HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
    for (HandlerAdapter ha : this.handlerAdapters) {
        if (logger.isTraceEnabled()) {
            logger.trace("Testing handler adapter [" + ha + "]");
        }
        if (ha.supports(handler)) {
            return ha;
        }
    }

and then you can finally call

ha.handle(processedRequest, response, mappedHandler.getHandler());

which in turn would execute the controller's method with params.

But having all this, I would not recommend to following this approach, instead, think about usage of JSON API spec or any other.

like image 200
hahn Avatar answered Sep 21 '22 14:09

hahn


How about using Springs RestTemplate as client for this? You could call your controllers within the spring controller as if it would be an external resource:

@ResponseBody
public List<String> encode(@RequestBody List inputPaths) {
    List<String> response = new ArrayList<>(inputPaths.size());
    for (Object inputPathObj : inputPaths) {
        String inputPath = (String) inputPathObj;
        try {
            RequestEntity.BodyBuilder requestBodyBuilder = RequestEntity.method(HttpMethod.GET, new URI(inputPath)); // change to appropriate HttpMethod, maybe some mapping?
            // add headers and stuff....
            final RequestEntity<Void> requestEntity = requestBodyBuilder.build(); // when you have a request body change Void to e.g. String
            ResponseEntity<String> responseEntity = null;
            try {
                responseEntity = restTemplate.exchange(requestEntity, String.class);
            } catch (final HttpClientErrorException ex) {
                // add your exception handling here, e.g.
                responseEntity = new ResponseEntity<>(ex.getResponseHeaders(), ex.getStatusCode());
                throw ex;
            } finally {
                response.add(responseEntity.getBody());
            }
        } catch (URISyntaxException e) {
            // exception handling here
        }
    }
    return response;
}

Note that generic do not work for the @RequestBody inputPaths.

See alse http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.html and https://spring.io/guides/gs/consuming-rest/ .

like image 44
Jürgen Avatar answered Sep 19 '22 14:09

Jürgen