Spring MVC, REST, JSON, DispatcherServlet и @RequestMapping - PullRequest
3 голосов
/ 28 февраля 2012

Мне не удается запустить простой тест интеграции клиента REST. Я использую Spring 3.1 MVC с настройкой JavaConfig. Я использую Maven, и я могу без проблем создавать, запускать и развертывать мое текущее веб-приложение.

Во-первых, вот код и конфиг

Мой контроллер

@Controller
@RequestMapping("/rest")
public class StubRestController {

@Inject
private TestData testData;

@RequestMapping(value=Endpoints.GET_RESOURCES, method=RequestMethod.GET, produces="application/json")
@ResponseStatus(HttpStatus.OK)
public @ResponseBody JSONObject getResources(@RequestParam(value="q") String query, @RequestParam int indexFrom, @RequestParam int indexTo) throws JSONException {
    return makeRequest(query, indexFrom, indexTo, testData.getResources());
}

@RequestMapping(value=Endpoints.GET_LOCATIONS, method=RequestMethod.GET, produces="application/json")
@ResponseStatus(HttpStatus.OK)
public @ResponseBody JSONObject getLocations(@RequestParam(value="q") String query, @RequestParam int indexFrom, @RequestParam int indexTo) throws JSONException {
    return makeRequest(query, indexFrom, indexTo, testData.getLocations());
}

private JSONObject makeRequest(String query, int indexFrom, int indexTo, String[] data) throws JSONException {
    int count = 0;
    final JSONArray resources = new JSONArray();
    for (final String resourceName: data) {
        final String lowerColor = resourceName.toLowerCase();
        final int has = lowerColor.indexOf(query.toLowerCase());

        if (!query.isEmpty() && (query.equals("*") || has >= 0)) {
            final JSONObject resource = new JSONObject();
            resource.put("DisplayName", resourceName);
            resource.put("Value", resourceName);   // shouldn't this be a unique id?  e.g., resourceid
            resources.put(resource);
            count++;
        }
    }

    final JSONArray partial = new JSONArray();
    if (resources.length() > 0) {
        final int end = count - 1 > indexTo ? indexTo : count - 1;
        for (int i = indexFrom; i <= end; i++) {
            partial.put(resources.get(i));
        }
    }

    final JSONObject result = new JSONObject();
    result.put("TotalSize", count);
    result.put("Options", partial);
    return result;
}

@ExceptionHandler(JSONException.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason="No data found matching criteria")
public void notFound() { }
}

Мой тест

@ContextConfiguration(classes={ RestClientContext.class }, loader=AnnotationConfigContextLoader.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class StubRestClientITCase {

private static final String SCHEME = "http";
private static final String HOST = "localhost";
private static final int PORT = 8081;

// requests must match <url-pattern> in order to be handled by DispatcherServlet
private static final String ENDPOINT_PREFIX = "spp-emkt-mui-experimental/EMKT/services/rest";

private static final String QUERY_KEY = "q";
private static final String INDEX_FROM_KEY = "indexFrom";
private static final String INDEX_TO_KEY = "indexTo";
private static final String TOTAL_SIZE_KEY = "TotalSize";

private Logger log = LoggerFactory.getLogger(StubRestClientITCase.class);

@Inject
RestTemplate restTemplate;

@Test
public void testGetResources() {

    // Case 1:  Discover all resources using * (asterisk), first 25
    final URI uri = buildUri("*", 0, 24, Endpoints.GET_RESOURCES);
    final HttpEntity<JSONObject> response = obtainResponse(uri);

    try {
        Assert.assertTrue(response.hasBody());
        Assert.assertEquals(25, Integer.parseInt(response.getBody().getString(TOTAL_SIZE_KEY)));
    } catch (final JSONException je) {
        fail("Could not obtain \"" + TOTAL_SIZE_KEY + "\" from JSON payload for getResources().\n" + je.getMessage());
    }

}

private URI buildUri(String query, int indexFrom, int indexTo, String endPointUrl) {
    final UriComponents uriComponents =
            UriComponentsBuilder.newInstance()
            .scheme(SCHEME).host(HOST).port(PORT).path(ENDPOINT_PREFIX + endPointUrl)
            .queryParam(QUERY_KEY, query)
            .queryParam(INDEX_FROM_KEY, indexFrom)
            .queryParam(INDEX_TO_KEY, indexTo)
            .build()
            .encode();

    final URI uri = uriComponents.toUri();
    return uri;
}

private HttpEntity<JSONObject> obtainResponse(URI uri) {
    final HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.setAccept(Arrays.asList(new MediaType[] {MediaType.APPLICATION_JSON}));
    requestHeaders.setAcceptCharset(Arrays.asList(new Charset[] {Charset.forName("UTF-8")}));
    final HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
    final HttpEntity<JSONObject> response = restTemplate.exchange(uri, HttpMethod.GET, requestEntity, JSONObject.class);
    return response;
}
}

Мой web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

<context-param>
    <param-name>contextClass</param-name>
    <param-value>
        org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
</context-param>

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>com.spp.mui.gwt.server.config.WebAppContextExperimental</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>

<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<servlet>
    <servlet-name>gwt</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>gwt</servlet-name>
    <url-pattern>/EMKT/service/*</url-pattern>
</servlet-mapping>

<welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
</welcome-file-list>

<session-config>
    <session-timeout>0</session-timeout>
</session-config>
</web-app>

Я вижу, как мои методы контроллера отображаются при запуске сервера ...

17:48:58,651 INFO  [RequestMappingHandlerMapping] Mapped "{[/rest/resources],methods=[GET],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}"
17:48:58,651 INFO  [RequestMappingHandlerMapping] Mapped "{[/rest/locations],methods=[GET],params=[],headers=[],consumes=[],produces=[application/json],custom=[]}"

Когда тест запускается, я получаю ...

17:49:00,617 DEBUG [AutowiredAnnotationBeanPostProcessor] Autowiring by type from bean name 'com.spp.mui.gwt.server.controller.stub.StubRestClientITCase' to bean named 'restTemplate'
17:49:00,648 DEBUG [RestTemplate] Created GET request for "http://localhost:8080/EMKT/services/rest/resources?q=*&indexFrom=0&indexTo=24"
17:49:00,680 DEBUG [RestTemplate] Setting request Accept header to [application/json]
17:49:00,742 WARN  [RestTemplate] GET request for "http://localhost:8080/EMKT/services/rest/resources?q=*&indexFrom=0&indexTo=24" resulted in 404 (Not Found); invoking error handler

Наконец ...

Tests in error:
  testGetResources(com.spp.mui.gwt.server.controller.stub.StubRestClientITCase): 404 Not Found

В чем может быть дело? Думая, что это как-то связано с url-pattern in servlet-mapping . Я пытался следовать документации Spring по настройке, но безуспешно. Примечание: я негибко настроен на это, поскольку у меня есть настройка GWT-SL для сопоставления служебных интерфейсов RPC.

ОБНОВЛЕНИЕ # 1

Если я попытаюсь использовать

curl --verbose -H "Accept: application/json" "localhost:8081/spp-emkt-mui-experimental/EMKT/service/rest/resources?q=*&indexFrom=0&indexTo=24"

против развертывания Tomcat 6 (с использованием грузового плагина) я получаю другой результат:

* About to connect() to localhost port 8081 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 8081 (#0)
> GET /spp-emkt-mui-experimental/EMKT/service/rest/resources?q=*&indexFrom=0&indexTo=24 HTTP/1.1
> User-Agent: curl/7.21.1 (i686-pc-mingw32) libcurl/7.21.1 OpenSSL/0.9.8r zlib/1.2.3
> Host: localhost:8081
> Accept: */*
>
< HTTP/1.1 406 Not Acceptable
< Server: Apache-Coyote/1.1
< Content-Type: text/html;charset=utf-8
< Content-Length: 1070
< Date: Tue, 28 Feb 2012 05:00:26 GMT

Что, похоже, указывает на то, что я не отправляю соответствующую информацию заголовка в моем запросе Hmmmmmm.

ОБНОВЛЕНИЕ № 2

Тест, как написано, возвращает 404. И завиток 406.

Я просмотрел так много, казалось бы, связанных постов и перепробовал так много вещей, что мне интересно, могут ли Spring MVC, GWT-RPC и GWT-SL быть объединены в одном контейнере. Рассматривается переход на RestyGWT с серверной стороной Spring MVC. Комментарии?

1 Ответ

8 голосов
/ 01 марта 2012

Это было определенно учебное упражнение с моей стороны. Мне пришлось преодолеть ряд препятствий в отношении конфигурации, чтобы GWT RPC и Spring MVC хорошо играли вместе.

Теперь у меня есть рабочий тест. «Поддельный» с моей стороны пытался вернуть аннотированный @ ResponseBody JSONObject. Не делайте этого! Я создал собственный DTO и аннотировал аннотации Джексона, такие как @JsonProperty, @JsonSerialize и @JsonDeserialize, чтобы получить ввод-вывод так, как я этого хотел.

Я представляю свои рабочие тесты и обновления конфигурации ниже на случай, если кому-то будет интересно ...

TEST

@ContextConfiguration(classes={ RestClientContext.class }, loader=AnnotationConfigContextLoader.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SuggestionsClientITCase {

private static final String SCHEME = "http";
private static final String HOST = "localhost";
private static final int PORT = 8080;

private static final String QUERY_KEY = "q";
private static final String INDEX_FROM_KEY = "indexFrom";
private static final String INDEX_TO_KEY = "indexTo";;

private Logger log = LoggerFactory.getLogger(SuggestionsClientITCase.class);

@Inject
RestTemplate restTemplate;

@Test
public void testGetResources() {

    // Case 1:  Discover all resources using * (asterisk), first 25
    // -- the total # of resources in TestData is 250
    // -- the total # of options returned should be constrained by indexTo - indexFrom = 25
    happyPathAssertions(Endpoints.GET_RESOURCES, "*", 0, 24, 250, 25);

}

@Test
public void testGetLocations() {

    // Case 1:  Discover all resources using * (asterisk), first 25
    // -- the total # of locations in TestData is 4316
    // -- the total # of options returned should be constrained by indexTo - indexFrom = 25
    happyPathAssertions(Endpoints.GET_LOCATIONS, "*", 0, 24, 4316, 25);

}

private void happyPathAssertions(String endpointUrl, String query, int indexFrom, int indexTo, int expectedTotal, int expectedOptionsPerPage) {
    final URI uri = buildUri(query, indexFrom, indexTo, endpointUrl);
    final HttpEntity<SuggestionsPayload> response = obtainResponse(uri);
    Assert.assertTrue(response.hasBody());
    Assert.assertEquals(expectedTotal, response.getBody().getTotalSize());
    Assert.assertEquals(expectedOptionsPerPage, response.getBody().getOptions().size());
}

private URI buildUri(String query, int indexFrom, int indexTo, String endPointUrl) {
    final UriComponents uriComponents =
            UriComponentsBuilder.newInstance()
            .scheme(SCHEME).host(HOST).port(PORT).path(Endpoints.REST_PREFIX + endPointUrl)
            .queryParam(QUERY_KEY, query)
            .queryParam(INDEX_FROM_KEY, indexFrom)
            .queryParam(INDEX_TO_KEY, indexTo)
            .build()
            .encode();

    final URI uri = uriComponents.toUri();
    return uri;
}

private HttpEntity<SuggestionsPayload> obtainResponse(URI uri) {
    final HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.setAccept(Arrays.asList(new MediaType[] {MediaType.APPLICATION_JSON}));
    requestHeaders.setAcceptCharset(Arrays.asList(new Charset[] {Charset.forName("UTF-8")}));
    final HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
    final HttpEntity<SuggestionsPayload> response = restTemplate.exchange(uri, HttpMethod.GET, requestEntity, SuggestionsPayload.class);
    return response;
}

}

CONTROLLER

@Controller
@RequestMapping("/" + Endpoints.REST_PREFIX)
public class StubSuggestionsController {

@Inject
private TestData testData;

@RequestMapping(value=Endpoints.GET_RESOURCES, method=RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody SuggestionsPayload getResources(@RequestParam(value="q") String query, @RequestParam int indexFrom, @RequestParam int indexTo) {
    return makeRequest(query, indexFrom, indexTo, testData.getResources());
}

@RequestMapping(value=Endpoints.GET_LOCATIONS, method=RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
@ResponseStatus(HttpStatus.OK)
public @ResponseBody SuggestionsPayload getLocations(@RequestParam(value="q") String query, @RequestParam int indexFrom, @RequestParam int indexTo) {
    return makeRequest(query, indexFrom, indexTo, testData.getLocations());
}

private SuggestionsPayload makeRequest(String query, int indexFrom, int indexTo, String[] data) {
    int count = 0;
    final List<SuggestionOption> possibilities = new ArrayList<SuggestionOption>();
    for (final String resourceName: data) {
        final String key = resourceName.toLowerCase();
        final int has = key.indexOf(query.toLowerCase());

        if (!query.isEmpty() && (query.equals("*") || has >= 0)) {
            final SuggestionOption possibility = new SuggestionOption();
            possibility.setDisplayName(resourceName);
            possibility.setValue(resourceName);   // shouldn't this be a unique id?  e.g., resourceid
            possibilities.add(possibility);
            count++;
        }
    }

    final List<SuggestionOption> options = new ArrayList<SuggestionOption>();
    if (possibilities.size() > 0) {
        final int end = count - 1 > indexTo ? indexTo : count - 1;
        for (int i = indexFrom; i <= end; i++) {
            options.add(possibilities.get(i));
        }
        // sort the suggestions by display name
        Collections.sort(options, new Comparator<SuggestionOption>() {

            @Override
            public int compare(SuggestionOption o1, SuggestionOption o2) {
                final int comparison = o1.getDisplayName().compareTo(o2.getDisplayName());
                return comparison;
            }

        });
    }

    final SuggestionsPayload result = new SuggestionsPayload();
    result.setTotalSize(count);
    result.setOptions(options);
    return result;
}

}

ПОЛЕЗНЫЙ

public class SuggestionsPayload {

@JsonProperty("TotalSize")
@JsonSerialize @JsonDeserialize
private int totalSize;
@JsonProperty("Options")
@JsonSerialize @JsonDeserialize
private List<SuggestionOption> options;

public int getTotalSize() {
    return totalSize;
}

public void setTotalSize(int totalSize) {
    this.totalSize = totalSize;
}

public List<SuggestionOption> getOptions() {
    return options;
}

public void setOptions(List<SuggestionOption> options) {
    this.options = options;
}

public static class SuggestionOption {

    @JsonProperty("Value")
    @JsonSerialize @JsonDeserialize
    private String value;
    @JsonProperty("DisplayName")
    @JsonSerialize @JsonDeserialize
    private String displayName;

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

}
}

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">    

<!-- Java-based annotation-driven Spring container definition -->
<context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>

<!-- Location of Java @Configuration classes that configure the components that makeup this application -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>com.spp.mui.gwt.server.config.WebAppContextExperimental</param-value>
</context-param>

<!-- Specifies the default mode of this application, to be activated if no other profile (or mode) is specified -->
<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>standard</param-value>
</context-param>

<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- Exposes request on current thread of execution, required for beans
     employing <aop:scoped-proxy /> or e.g., @Scope(value="request", proxyMode=ScopedProxyMode.TARGET_CLASS) -->
<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>

<!-- Reads request input using UTF-8 encoding -->
<filter>
    <filter-name>characterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value>
    </init-param>
    <init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>characterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<!-- Enables support for DELETE and PUT request methods with web browser clients -->
<filter>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>hiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<!-- Secures the application -->
<!-- 
<filter>
    <filter-name>securityFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <init-param>
        <param-name>targetBeanName</param-name>
        <param-value>springSecurityFilterChain</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>securityFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
-->

<!-- Handles requests into the application -->
<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <!-- No explicit configuration file reference here: everything is configured in the root container for simplicity -->       
        <param-name>contextConfigLocation</param-name>
        <param-value></param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

<welcome-file-list>
    <welcome-file>Application.html</welcome-file>
</welcome-file-list>

<session-config>
    <session-timeout>0</session-timeout>
</session-config>

КОНФИГУРАЦИЯ

@Configuration
@Import(value={ AopConfig.class, GwtServiceConfig.class, ComponentConfig.class, MvcConfig.class })
public class WebAppContextExperimental {
// context used for web application

}

и в моем MvcConfig я должен был обязательно добавить их (особенно записи реестра для поддержки GWT, чтобы Spring DispatcherServlet разрешал запросы на ресурсы кода gen)

// serve static resources

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // i.e., images, JS, and CSS
    registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
    // i.e., GWT module code-generated resources
    registry.addResourceHandler("/*").addResourceLocations("/");
    registry.addResourceHandler("/EMKT/**").addResourceLocations("/EMKT/");
}

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(new MappingJacksonHttpMessageConverter());
}
...