本文讲解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)均支持:
[了解一下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 (本小节跟本文关系不大,略去 译者)
===================================================================