How to parse a url and run a method with Spring MVC "reflexively"?

I have a Spring boot application that uses Spring MVC in the usual way, with a set of @RequestMapping methods, @RequestMapping definitions, etc. All this is related to the WebMvcConfigurerAdapter class.

I would like to provide a service in which the user sends a list of valid URLs, and the webapp will work, which controller will be called, pass parameters and return a combined result for each URL - all in one request.

This will save the user from having to make hundreds of HTTP calls, but still allow them to make one-time requests, if necessary. Ideally, I would just enter an automatically configured Spring bean, so I don’t need to repeat URLs, adapt and process what Spring does internally, and the list of controllers of other controllers will never be out of sync with the real list of controllers.

I expected to write something like this (it’s simplified to deal with only 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 had to define Spring beans. I don’t know what they are doing, and repeated some of what Spring is for me, that I’m worried I won’t work in exactly the same way as if the user simply made the call:

 // 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(); } 

It seemed to me that I am moving pretty well along this path, but it does not work with Missing URI template variable errors, and I not only have no idea how to put the request parameters (another way that Spring could handle itself), but I'm not even sure That is the right way to do this. So, how do I simulate a Spring MVC query “reflexively” from within the webapp itself?

+7
java spring spring-mvc
source share
3 answers

JSON API spec. solves this problem by letting you send multiple operations for each request. There is even a fairly mature implementation that supports this feature called Elide . But I think that this may not fully meet your requirements.

Anyway, here is what you can do.

You should be aware that the DispatcherServlet contains a list of handlerMappings , which is used to find the appropriate request handler and handlerAdaptors . The selection strategy for both lists is customizable (see DispatcherServlet#initHandlerMappings and #initHandlerAdapters ).

You must decide how you prefer to receive these handlerMappings / initHandlerAdapters and stay in sync with the DispatcherServlet .

After that, you can implement your own HandlerMapping / HandlerAdaptor (or introduce the Controller method, as in your example), which would process the request to /encode path.

Btw, HandlerMapping , as javadoc says,

The interface that will be implemented by the objects that define the mapping between requests and handler objects

or just say if we take DefaultAnnotationHandlerMapping , which DefaultAnnotationHandlerMapping our HttpServletRequests to @Controller methods annotated using @RequestMapping . If this mapping, the HandlerAdapter prepares an incoming request to the consumption controller method, f.ex. retrieving the params , body request and using them to call the controller method.

After that, you can extract the URL from the main request , create a list of HttpRequests stubs containing the information necessary for further processing, and skip through them:

 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 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, will execute the controller method with parameters.

But with all this, I would not recommend following this approach, instead consider using the JSON API spec or whatever.

+1
source share

How about using Springs RestTemplate as a client for this? You can call the controllers in the spring controller, as if it were 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 eg String ResponseEntity<String> responseEntity = null; try { responseEntity = restTemplate.exchange(requestEntity, String.class); } catch (final HttpClientErrorException ex) { // add your exception handling here, eg 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 does not work for @RequestBody input fields.

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 / .

+1
source share

I agree with the other answers that you should consider this function outside of your project, instead of having it in code. This is a design issue and you can choose the right approach. Based on your comment that these are GET requests, you can achieve what you want with the request manager to initiate your requests as part of your special Controller service method for each URL and display the response using an instance of HttpServletResponseWrapper.

In the following code example, the consolidate method uses comma-separated URLs like this (" http: // localhost: 8080 / index / index1, index2 ", here "index1, index2" is a list of URLs), combines them text output to one payload and returns it. For this example URL, the consolidated outputs http: // localhost: 8080 / index1 and http: // localhost: 8080 / index2 will be returned. You might want to expand / change this with added parameters, validation, etc. For urls. I tested this code with Spring Boot 1.2.x.

 @Controller public class MyController { @RequestMapping("/index/{urls}") @ResponseBody String consolidate(@PathVariable String[] urls, HttpServletRequest request, HttpServletResponse response) { StringBuilder responseBody = new StringBuilder(); //iterate for each URL provided for (String url : urls) { RequestDispatcher dispatcher = request.getServletContext().getRequestDispatcher("/" + url); HttpServletResponseWrapper wrapper = new HttpServletResponseWrapper((HttpServletResponse) response) { private CharArrayWriter output = new CharArrayWriter(); @Override public PrintWriter getWriter() { return new PrintWriter(output); } @Override public String toString() { return output.toString(); } }; try { dispatcher.include(request, wrapper); //append the response text responseBody.append(wrapper.toString()); } catch (ServletException | IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } //This holds the consolidated output return responseBody.toString(); } @RequestMapping("/index1") String index1() { return "index1"; } @RequestMapping("/index2") String index2() { return "index2"; } } 
0
source share

All Articles