2016-08-29 23 views
7

Mam aplikację Spring Boot, która używa Spring MVC w zwykły sposób, z kilkoma metodami, definicjami Freemarker i tym podobnymi. To wszystko jest powiązane z klasą WebMvcConfigurerAdapter.Jak analizować adres URL i uruchamiać metodę ze "odbiciem" Spring MVC?

Chciałbym świadczyć usługę, w której użytkownik przesyła listę prawidłowych adresów URL, a aplikacja webowa wyszuka, który kontroler zostanie wywołany, przekaże parametry i zwróci łączny wynik dla każdego adresu URL - wszystko w jedna prośba.

W ten sposób użytkownik nie będzie musiał wykonywać setek połączeń HTTP, ale w razie potrzeby nadal będzie mógł wysyłać jednorazowe żądania. Idealnie byłoby po prostu wstrzyknąć automatycznie skonfigurowany komponent Spring bean, więc nie muszę powtarzać adresu URL, który został rozwiązany i przystosowany do obsługi Springa, a lista kontrolerów innych kontrolerów nigdy nie byłaby zsynchronizowana z prawdziwa lista kontrolerów.

Spodziewałem się napisać coś takiego (uproszczony do czynienia z jednym tylko URL, który jest bez sensu, ale łatwiejsze do zrozumienia):

@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"; 
    } 
} 

Zamiast tego, miałem do zdefiniowania Wiosenne fasolę ja nie wiedzą, co robią i zostały powtarzając niektóre, co Wiosna ma zrobić dla mnie, który jestem zmartwiony nie zadziała dość samo jak to będzie, gdy użytkownik po prostu sprawiło, że nazywają się:

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

Myślałem, że robię całkiem dobrze idąc tą ścieżką, ale to nie działa z błędami Missing URI template variable i nie tylko nie mam pojęcia, jak umieścić parametry żądania w (kolejna rzecz, którą Spring mogłaby poradzić sobie sama), ale nie jestem nawet pewien, czy to jest właściwym sposobem na zrobienie tego. Jak więc symulować żądanie Spring MVC "refleksyjnie", z samej aplikacji webowej?

+3

Jakie są tutaj rzeczywiste korzyści? Zwykle klienci wysyłają dużo próśb i nie jest to problemem. Żądania muszą jeszcze zostać przesłane, a ich grupowanie w jakiś sposób nie zapewni znacznego przyspieszenia. – chrylis

+1

Rzeczywistym przypadkiem użycia jest wyodrębnienie adresów URL z dokumentu i zwrócenie tego samego dokumentu z przetworzonymi wynikami, tak aby był dostępny w trybie offline (lub w przypadku awarii serwera). –

+1

Następnie ponownie wywołaj połączenia. – chrylis

Odpowiedz

1

JSON API spec. rozwiązuje ten problem, umożliwiając wysyłanie wielu operacji na żądanie. Istnieje nawet dość dojrzała implementacja obsługująca tę funkcję, która nazywa się Elide. Ale myślę, że to może nie w pełni spełnić twoje wymagania.

W każdym razie, oto, co możesz zrobić.

Trzeba wziąć pod uwagę, że DispatcherServlet posiada handlerMappings listy, który jest używany do wykrywania odpowiedniego request obsługi i handlerAdaptors. Strategię wyboru dla obu list można konfigurować (patrz DispatcherServlet#initHandlerMappings i #initHandlerAdapters).

Należy wypracować sposób wolisz pobrać tę listę handlerMappings/initHandlerAdapters i pobyt w synchronizacji z DispatcherServlet.

Po tym można zaimplementować własną HandlerMapping/HandlerAdaptor (lub przedstawić metodę Controller jak w przykładzie), które obsługiwać request do /encode ścieżkę.

Btw, HandlerMapping jak javadoc mówi jest

Interfejs być realizowane przez obiekty, które definiują odwzorowanie między wniosków i obsługi obiektów

lub po prostu mówiąc, jeśli weźmiemy DefaultAnnotationHandlerMapping że na mapie Nasz HttpServletRequests do @Controller metod opatrzonych przypisami z @RequestMapping. Mając to odwzorowanie HandlerAdapter przygotowuje przychodzące żądanie do zużywania metody kontrolera, np. wyodrębnianie żądania params, body i użycie ich do wywołania metody kontrolera.

Mając to można wyodrębnić URL s od głównego request, utworzyć listę odgałęzienie HttpRequests posiadających informacje potrzebne do dalszego przetwarzania i pętli przez nich wzywającą to:

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; 
} 

posiadające handlerMapping zadzwonić

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; 
     } 
    } 

a następnie można w końcu zadzwonić

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

który z kolei wykonałby metodę kontrolera z parametrami.

Ale mając to wszystko, nie polecałbym podążania za tym podejściem, zamiast tego pomyśl o używaniu specyfikacji JSON API lub jakiejkolwiek innej.

1

Jak korzystać ze sprężyn RestTemplate jako klienta do tego? Można zadzwonić kontrolerów wewnątrz regulatora sprężyny jakby byłoby zewnętrznego zasobu:

@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; 
} 

Zauważ, że generyczny nie działają dla inputPaths @RequestBody.

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

0

Zgadzam się z pozostałymi odpowiedziami, że powinieneś rozważyć tę funkcję poza projektem, zamiast mieć ją w kodzie. Jest to kwestia projektowania i możesz wybrać pożądane podejście. Na podstawie Twojego komentarza, że ​​są to żądania GET, możesz osiągnąć to, co chcesz, za pomocą dispatchera żądań, aby wyzwalać żądania w ramach specjalnej metody obsługi kontrolera dla każdego adresu URL i przechwytywać odpowiedź za pomocą instancji HttpServletResponseWrapper.

W poniższym kodzie przykładowym metoda "konsoliduj" zajmuje adresy URL oddzielone przecinkami, takie jak ten ("http://localhost:8080/index/index1,index2", tutaj "indeks1, indeks2" jest listą adresów URL), konsoliduje ich tekstowe dane wyjściowe do pojedynczego ładunku i zwraca go. W przypadku tego przykładowego adresu URL zostaną zwrócone skonsolidowane wyniki: http://localhost:8080/index1 i http://localhost:8080/index2. Możesz rozszerzyć/zmodyfikować to, dodając parametry, sprawdzanie poprawności itp. Dla adresów URL. Przetestowałem ten kod za pomocą 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"; 
    } 

}