Spring MVC,REST,JSON,DispatcherServlet和@RequestMapping

时间:2012-02-28 02:21:40

标签: json rest spring-mvc dispatcher

我似乎无法进行简单的REST客户端集成测试。我使用Spring 3.1 MVC与JavaConfig设置。我使用Maven,我可以毫无问题地构建,运行和部署我当前的webapp。

首先,这是一些代码和配置

我的控制器

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

我可以在服务器启动时看到我的Controller方法...

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

可能是什么事?认为它与 servlet-mapping 中的 url-pattern 有关。我试图在设置上遵循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 个答案:

答案 0 :(得分:8)

这绝对是我的学习练习。我必须清除一些关于配置的障碍,以便GWT RPC和Spring MVC很好地协同工作。

我现在有一个工作测试。我的“失礼”试图返回@ResponseBody注释的JSONObject。 不要这样做!我制作了一个自定义DTO并注释了@JsonProperty,@ JsonSerialize和@JsonDeserialize等Jackson注释,以便按照我想要的方式获取I / O.

我将在下面展示我的工作测试和配置更新,以防有人感兴趣...

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

}

<强> PAYLOAD

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将允许请求代码生成资源)

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