当前位置: 代码迷 >> Web前端 >> 基于Spring MVC的Web应用开发(六) - Response
  详细解决方案

基于Spring MVC的Web应用开发(六) - Response

热度:1283   发布时间:2012-06-26 10:04:13.0
基于Spring MVC的Web应用开发(6) - Response

本文讲解Spring MVC的Response,深入了解一下@RequestMapping配合@ResponseBody的用法,同时介绍另外一个和Response有关的类ResponseEntity。

首先看看本文演示用到的类ResponseController:

package org.springframework.samples.mvc.response;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class ResponseController {

	@RequestMapping(value="/response/annotation", method=RequestMethod.GET)
	public @ResponseBody String responseBody() {
		return "The String ResponseBody";
	}

	@RequestMapping(value="/response/charset/accept", method=RequestMethod.GET)
	public @ResponseBody String responseAcceptHeaderCharset() {
		return "こんにちは世界! (\"Hello world!\" in Japanese)";
	}

	@RequestMapping(value="/response/charset/produce", method=RequestMethod.GET, produces="text/plain;charset=UTF-8")
	public @ResponseBody String responseProducesConditionCharset() {
		return "こんにちは世界! (\"Hello world!\" in Japanese)";
	}

	@RequestMapping(value="/response/entity/status", method=RequestMethod.GET)
	public ResponseEntity<String> responseEntityStatusCode() {
		return new ResponseEntity<String>("The String ResponseBody with custom status code (403 Forbidden - stephansun)",
				HttpStatus.FORBIDDEN);
	}

	@RequestMapping(value="/response/entity/headers", method=RequestMethod.GET)
	public ResponseEntity<String> responseEntityCustomHeaders() {
		HttpHeaders headers = new HttpHeaders();
		headers.setContentType(MediaType.TEXT_PLAIN);
		return new ResponseEntity<String>("The String ResponseBody with custom header Content-Type=text/plain",
				headers, HttpStatus.OK);
	}

}

?

访问http://localhost:8080/web/response/response/annotation,对应responseBody(),这个方法很典型,之前已经见过多次了,将字符串直接输出到浏览器。

?

访问http://localhost:8080/web/response/charset/accept,对应responseAcceptHeaderCharset(),该方法和responseBody()并没有什么不同,只是,返回的字符串中带有日文。浏览器显示"???????? ("Hello world!" in Japanese)",有乱码出现。

?

访问http://localhost:8080/web/response/charset/produce,对应responseProducesConditionCharset(),该方法跟responseAcceptHeaderCharset()相比,在@RequestMapping中增加了“produces="text/plain;charset=UTF-8"”,浏览器显示"こんにちは世界! ("Hello world!" in Japanese)",乱码没有了。

为了将这两者的区别说清楚,看看日志:

responseAcceptHeaderCharset():

DEBUG: org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Written [こんにちは世界! ("Hello world!" in Japanese)] as "text/html" using [org.springframework.http.converter.StringHttpMessageConverter@6b414655]

responseProducesConditionCharset():

DEBUG: org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor - Written [こんにちは世界! ("Hello world!" in Japanese)] as "text/plain;charset=UTF-8" using [org.springframework.http.converter.StringHttpMessageConverter@6b414655]

?

前者使用默认的"text/html",后者使用了特定的"text/plain;charset=UTF-8"。为什么以"text/html"形式输出日文(其实中文也是一样的)就会乱码的根本原因我还没透彻地搞清楚,但我注意到spring-web-3.1.0.REALEASE.jar中org.springframework.http.converter.StringHttpMessageConverter中有这样一段代码:

public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");

应该跟"ISO-8859-1"有关系吧。

?

后者通过指定@RequestMapping的produces属性为text/plain,字符集为UTF-8,可以避免乱码,这个是可以理解的

?

最后看看responseEntityStatusCode()和responseEntityCustomHeaders()方法,这两个方法和前面的方法的区别是没有@ResponseBody,返回的类型为ResponseEntity<String>。

responseEntityStatusCode()方法返回一个字符串,并附带了HTTP状态码为HttpStatus.FORBIDDEN(即403);

responseEntityCustomHeaders()除了返回一个字符串,HTTP状态码为HttpStatus.OK(即200),还指定了返回内容的content-type为text/plain;

这里最容易困惑我们的就是HTTP状态码了,因为从浏览器的输出看不到任何与HTTP状态码相关的东西。

?

访问http://localhost:8080/web/response/entity/status和http://localhost:8080/web/response/entity/headers

均能正常的将字符串内容显示在网页上,那么HttpStatus.FORBIDDEN起什么作用呢?

ResponseEntity继承于HttpEntity类,HttpEntity类的源代码的注释说"HttpEntity主要是和RestTemplate组合起来使用,同样也可以在SpringMVC中作为@Controller方法的返回值使用"。ResponseEntity类的源代码注释说"ResponseEntity是HttpEntity的扩展,增加了一个HttpStutus的状态码,通常和RestEntity配合使用,当然也可以在SpringMVC中作为@Controller方法的返回值使用"。那么使用RestTemplate写个程序模拟一下吧(RestTemplate的具体用法见本文附录):

package org.springframework.samples.mvc.response;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

public class ResponseControllerTest {

	public static void main(String[] args) {
		RestTemplate template = new RestTemplate();
		ResponseEntity<String> entity = template.getForEntity(
				"http://localhost:8080/web/response/entity/status", String.class);
		String body = entity.getBody();
		MediaType contentType = entity.getHeaders().getContentType();
		HttpStatus statusCode = entity.getStatusCode();
		System.out.println("statusCode:[" + statusCode + "]");
	}
}

访问http://localhost:8080/web/response/entity/status,观察日志:

DEBUG: org.springframework.web.client.RestTemplate - Created GET request for "http://localhost:8080/web/response/entity/status"
DEBUG: org.springframework.web.client.RestTemplate - Setting request Accept header to [text/plain, */*]
WARN : org.springframework.web.client.RestTemplate - GET request for "http://localhost:8080/web/response/entity/status" resulted in 403 (Forbidden); invoking error handler
Exception in thread "main" org.springframework.web.client.HttpClientErrorException: 403 Forbidden
	at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:76)
	at org.springframework.web.client.RestTemplate.handleResponseError(RestTemplate.java:486)
	at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:443)
	at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:401)
	at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:221)
	at org.springframework.samples.mvc.response.ResponseControllerTest.main(ResponseControllerTest.java:12)?

将URL换成http://localhost:8080/web/response/entity/headers,观察日志:

DEBUG: org.springframework.web.client.RestTemplate - Created GET request for "http://localhost:8080/web/response/entity/headers"
DEBUG: org.springframework.web.client.RestTemplate - Setting request Accept header to [text/plain, */*]
DEBUG: org.springframework.web.client.RestTemplate - GET request for "http://localhost:8080/web/response/entity/headers" resulted in 200 (OK)
DEBUG: org.springframework.web.client.RestTemplate - Reading [java.lang.String] as "text/plain" using [org.springframework.http.converter.StringHttpMessageConverter@2e297ffb]
statusCode:[200]

发现使用RestTemplate时,如果它发现返回的信息中HTTP状态码为403时,就抛出异常了,正好符合了我们的期望,至于为什么浏览器上没有体现,暂时还不不太明白(待补完)。

?

===================================================================

?

SpringSource的Team Blog上有一篇文章是关于@RequestMapping的produces属性的讨论。

?

===================================================================

?

附录Spring Reference Documentation中的相关内容:

?

[了解一下@RequesetMapping支持的返回值类型]

16.3.3.2 @RequestMapping注解方法支持的返回值类型

以下返回值的类型(return types)均支持:

  • ModelAndView对象,with the model implicitly enriched with command objects and the results of @ModelAttributes annotated reference data accessor methods.(恕我实在翻译不出 TAT)。
  • Model对象,with the view name implicitly determined through a RequestToViewNameTranslator and the model implicitly enriched with command objects and the results of @ModelAttribute annotated reference data accessor methods.
  • Map对象,for exposing a model, with the view name implicitly determined through a RequestToViewNameTranslator and the model implicitly enriched with command objects and the results of @ModelAttribute annotated reference data accessor methods.
  • View对象,with the model implicitly determined through command objects and @ModelAttribute annotated reference data accessor methods. The handler method may also programmatically enrich the model by declaring a Model argument (see above).
  • String,value that is interpreted as the logical view name, with the model implicitly determined through command objects and @ModelAttribute annotated reference data accessor methods. The handler method may also programmatically enrich the model by declaring a Model argument (see above).
  • void,if the method handles the response itself (by writing the response content directly, declaring an argument of type ServletResponse / HttpServletResponse for that purpose) or if the view name is supposed to be implicitly determined through a RequestToViewNameTranslator (not declaring a response argument in the handler method signature).
  • @ResponseBody,If the method is annotated with @ResponseBody, the return type is written to the response HTTP body. The return value will be converted to the declared method argument type using HttpMessageConverters. See Section 16.3.3.5, “Mapping the response body with the @ResponseBody annotation”.
  • HttpEntity或者ResponseEntityHttpEntity<?>或者ResponseEntity<?>对象可以取到Servlet的response的HTTP头信息(headers)和内容(contents)。这个实体(entity body)可以通过使用HttpMessageConverter类被转成Response流。See Section 16.3.3.6, “Using HttpEntity<?>”.
  • Any other return type is considered to be a single model attribute to be exposed to the view, using the attribute name specified through @ModelAttribute at the method level (or the default attribute name based on the return type class name). The model is implicitly enriched with command objects and the results of @ModelAttribute annotated reference data accessor methods.

?

[了解一下RestTemplate,下一篇文章有用]文档中有关RestTemplate的内容:

?

20.9 在客户端访问RESTful服务

RestTemplate是客户端访问RESTful服务的核心类。它在概念上同Spring中的其它模板类相似,如JdbcTemplate和JmsTemplate还有一些其它Spring portfolio工程中的模板类。RestTemplate提供了一个回调方法,使用HttpMessageConverter将对象marshal到HTTP请求体里,并且将response unmarshal成一个对象。使用XML作为消息格式是一个很普遍的做法,Spring提供了MarshallingHttpMessageConverter类,该类使用了Object-to-XML框架,该框架是org.springframe.oxm包的一部分。你有多种XML到Object映射技术可选。

本节介绍了如何使用RestTemplate以及和它相关的HttpMessageConverters。

20.9.1 RestTemplate

在Java中调用RESTful服务的一个经典的做法就是使用一个帮助类,如Jakarta Commons的HttpClient,对于通用的REST操作,HttpClient的实现代码比较底层,如下:

String uri = "http://example.com/hotels/1/bookings";

PostMethod post = new PostMethod(uri);
String request = // create booking request content
post.setRequestEntity(new StringRequestEntity(request));

httpClient.executeMethod(post);

if (HttpStatus.SC_CREATED == post.getStatusCode()) {
  Header location = post.getRequestHeader("Location");
  if (location != null) {
    System.out.println("Created new booking at :" + location.getValue());
  }
}

?RestTemplate对HTTP的主要六种提交方式提供了更高层的抽象,使得调用RESTful服务时代码量更少,并且使REST表现的更好。

表格20.1 RestTemplate方法预览

?

HTTP方法 RestTemplate方法

DELETE delete

GET getForObject getForEntity

HEAD headForHeaders(String url, String... urlVariables)

OPTIONS optionsForAllow(String url, String... urlVariables)

POST postForLocation(String url, Object request, String... urlVariables) postForObject(String url, Object request, Class<T> responsetype, String... uriVariables)

PUT put(String url, Object request, String... urlVariables)

?

RestTemplate的方法名称遵循着一个命名规则,第一部分说明调用的是什么HTTP方法,第二部分说明的是返回了什么。比如,getForObject()方法对应GET请求,将HTTP response的内容转成你需要的一种对象类型,并返回这个对象。postForLocation()方法对应POST请求,converting the given object into a HTTP request and return the response HTTP Location header where the newly created object can be found.如果在执行一个HTTP请求时出现异常,会抛出RestClientException异常;可以在RestTemplate自定义ResponseErrorHandler的实现来自定义这种异常。

这些方法通过HttpMessageConverter实例将传递的对象转成HTTP消息,并将得到的HTTP消息转成对象返回。给主要mime类型服务的转换器(converter)默认就注册了,但是你也可以编写自己的转换器并通过messageConverters()bean属性注册。该模板默认注册的转换器实例是ByteArrayHttpMessageConverter,StringHttpMessageConverter,FormHttpMessageConverter以及SourceHttpMessageConverter。你可以使用messageConverters()bean属性重写这些默认实现,比如在使用MarshallingHttpMessageConverter或者MappingJacksonHttpMessageConverter时你就需要这么做。

每个方法有有两种类型的参数形式,一种是可变长度的String变量,另一种是Map<String, String>,比如:

String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}",
                                          String.class,"42", "21");

使用可变长度参数,

Map<String, String> vars = Collections.singletonMap("hotel", "42");
String result =
  restTemplate.getForObject("http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);

使用一个Map<String, String> 。

你可以直接使用RestTemplate的默认构造器创建一个RestTemplate的实例。它会使用jdk中java.net包的标准Java类创建HTTP请求。你也可以自己指定一个ClientHttpRequestFactory的实现。Spring提供了CommonsClientHttpRequestFactory的实现,该类使用了Jakarta Commons的HttpClient创建HTTP请求。CommonsClientHttpRequestFactory配置了org.apache.commons.httpclient.HttpClient的一个实例,该实例反过来被credentials information或者连接池配置。

前面那个使用了Jakarta Commons的HttpClient类的例子可以直接使用RestTemplate重写,如下:

uri = "http://example.com/hotels/{id}/bookings";

RestTemplate template = new RestTemplate();

Booking booking = // create booking object

URI location = template.postForLocation(uri, booking, "1");

通用的回调接口是RequestCallback,该接口在execute方法被调用时被调用。

public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
                     ResponseExtractor<T> responseExtractor,
                     String... urlVariables)


// also has an overload with urlVariables as a Map<String, String>.

RequestCallback接口定义如下:

public interface RequestCallback {
 void doWithRequest(ClientHttpRequest request) throws IOException;
}

该接口允许你管控(manipulate)request的header并且象request体中写数据。当使用execute方法时,你不必担心任何资源管理问题的,模板总是会关闭request,并且处理任何error,你可以参考API文档来获得更多有关使用execute方法及该模板其它方法参数含义的信息。

20.9.1.1 同URI一起工作

对HTTP的主要方法,RestTemplate的第一个参数是可变的,你可以使用一个String类型的URI,也可以使用java.net.URI类型的类,

String类型的URI接受可变长度的String参数或者Map<String, String>,这个URL字符串也是假定没有被编码(encoded)并且需要被编码的(encoded)的。举例如下:

restTemplate.getForObject("http://example.com/hotel list", String.class);

这行代码使用GET方式请求http://example.com/hotel%20list。这意味着如果输入的URL字符串已经被编码了(encoded),它将被编码(encoded)两次 -- 比如:http://example.com/hotel%20list会变成http://example.com/hotel%2520list。如果这不是你所希望的,那就使用java.net.URI,它假定URL已经被编码(encoded)过了,如果你想要将一个单例的URI(fully expanded)复用多次的话,这个方法也是有用的。

UriComponentsBuilder类(我们已经见过了,不是吗? 译者)可以构建和编码一个包含了URI模板支持的URI,比如你可以从一个URL字符串开始:

UriComponents uriComponents = 
        UriComponentsBuilder.fromUriString("http://example.com/hotels/{hotel}/bookings/{booking}").build()
            .expand("42", "21")
            .encode();
            
URI uri = uriComponents.toUri();

?或者单独地指定每个URI组件:

UriComponents uriComponents = 
        UriComponentsBuilder.newInstance()
            .scheme("http").host("example.com").path("/hotels/{hotel}/bookings/{booking}").build()
            .expand("42", "21")
            .encode();

URI uri = uriComponents.toUri();

?

?20.9.1.2 处理request和response的头信息(headers)

除了前面已经说到的方法外,RestTemplate还有一个exchange()方法,该方法可以用在基于HttpEntity类的任意HTTP方法的执行上。

也许最重要的一点是,exchange()方法可以用来添加request头信息(headers)以及读response的头信息(headers)。举例:

HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("MyRequestHeader", "MyValue");
HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);

HttpEntity<String> response = template.exchange("http://example.com/hotels/{hotel}",
  HttpMethod.GET, requestEntity, String.class, "42");

String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
String body = response.getBody();

在上面这个例子中,我们首先准备了一个request实体(entity),该实体包含了MyRequestHeader头信息(header)。然后我们取回(retrieve)response,读到这个MyResponseHeader和返回体(body)。

20.9.2 HTTP Message Conversion (本小节跟本文关系不大,略去 译者)

?

===================================================================

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

?

  相关解决方案