Skip to content

Spring Restful Service

Calvin Xiao edited this page Nov 15, 2013 · 1 revision

##1. Overview JAX-RS协议的参考实现Jersey无论服务端还是客户端都是不错的,SpringSide之前也一直用它。 从RC4版后为了减少技术的使用,才用SpringMVC + RestTemplate进行替代。

##2. JAX-RS JAX-RS毕竟是个规范,在某些场合使用也非常合适,详见[Jersey](Jersey),之前的使用心得. 在Showcase中,顺道用CXF作为JAX-RS框架进行了演示,见AccountJaxRsService.java 和 applicationContext-jaxrs-server.xml.

##3. ServerSide: Spring MVC Spring MVC虽然没有实现JAX-RS协议,但整体风格与annotation和JAX-RS都已经非常接近了。

###3.1 Example 在Quickstart中演示了一个典型的CRUD的Restful Service,演示了各种返回各种状态码(404), 返回新创建对象的URL的方式。

	@RequestMapping(value = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
	@ResponseBody
	public ResponseEntity<?> get(@PathVariable("id") Long id) {
		Task task = taskService.getTask(id);
		if (task == null) {
			return new ResponseEntity(HttpStatus.NOT_FOUND);
		}
		return new ResponseEntity(task, HttpStatus.OK);
	}

###3.2 Content negotiation, 根据url后缀返回JSON与XML两种风格

        <mvc:annotation-driven content-negotiation-manager="contentNegotiationManager">

	<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
	    <property name="mediaTypes" >
	        <value>
	            json=application/json
	            xml=application/xml
	        </value>
	    </property>
	</bean>
        @RequestMapping(value = "/{id}", method = RequestMethod.GET)
	@ResponseBody
	public UserDTO getUser(@PathVariable("id") Long id) {
        }

Content negotiation的最大好处是,可以定义函数只返回Java对象,然后Spring MVC会根据URL后缀自动判定Content-Type及渲染成相应的View,如/api/v1/user/${id},测试用例见showcase中的UserRestFt。 当然,现在既要返回JSON,又要返回XML的接口越来越少了,多数只专心于JSON。

###3.3 异常控制 详见SpringMVC的相关章节,为了避免扰乱别人,Rest专门有自己的Exception。

###3.4 MeidaType 详见SpringMVC的相关章节

##4. ClientSide: Spring RestTemplate RestTemplate胜在够简单,详见Spring官方手册,通过getForObject()/getForEntity(), postForLocation()/postForObject()等方法快速实现访问URL并将结果转换为Object。 在quickstart的functional test 中TaskRestFT.java中,完整演示了其使用。

###4.1 转换类型为List<?>的返回值 如果服务端返回的是一个JSON格式编码的集合时,最快的转换方法是定义一个List的子类,然后作为Class参数.

	private static class TaskList extends ArrayList<Task> {
	};

        TaskList tasks = restTemplate.getForObject(resoureUrl, TaskList.class);

###4.2 处理Header 如果要在Request/Response中处理Header,则比Jersey要复杂一点,需要用到比较原始的exchange()方法,见showcase中的UserRestFT.java.

	HttpHeaders requestHeaders = new HttpHeaders();
	requestHeaders.set(com.google.common.net.HttpHeaders.AUTHORIZATION, Servlets.encodeHttpBasic("admin", "admin"));
	HttpEntity<?> requestEntity = new HttpEntity(requestHeaders);
	HttpEntity<UserDTO> response = restTemplate.exchange(resoureUrl + "/{id}.xml", HttpMethod.GET, requestEntity,UserDTO.class, 1L);

如果有很多函数都有相同的Header处理请求, 又不想用exchange()这么冗长的写法,可以使用ClientHttpRequestInterceptor, 同样在showcase中进行了演示, 下面的代码与上面的例子的效果是一样的。

    ClientHttpRequestInterceptor interceptor = new HttpBasicInterceptor("admin", "admin");
    restTemplate.setInterceptors(Lists.newArrayList(interceptor));
    restTemplate.getForObject(resoureUrl + "/{id}.json", UserDTO.class, 1L);

###4.3 底层Http Connection管理及超时控制 RestTemplate 默认使用SimpleClientHttpRequestFactory, 基于JDK自带的HttpURL Connection. 也可以设置基于Apache HttpClient4.0,并使用了多线程安全的Connection Pool的HttpComponentsClientHttpRequestFactory.(此时注意在退出时要调用其destroy()方法) 无论哪种RequestFactory,都可以调用setConnectTimeout()/setReadTimeout()方法设置超时,以毫秒为单位,0是无超时控制,不设的话则是系统默认值。

同样在showcase的UserRestFt中进行了演示。

##5. XML与JSON格式的转换 无论Server还是client,Spring已自带了一堆Converter:

  • MappingJackson2HttpMessageConverter,负责用Jackson2.0转换JSON格式数据
  • Jaxb2RootElementHttpMessageConverter, 负责用Jaxb2RootElementHttpMessageConverter转换XML格式数据,注意DTO必须用@XmlRootElement标注才会被转换,另外如果DTO之间循环依赖,会抛出Spring MVC会抛出406的返回码,需要在它的writeToResult()方法上设置断点来调试,才能看到真正原因。

如果自定义Converters,在server-side:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="com.google.protobuf.spring.http.ProtobufHttpMessageConverter"/>
    </mvc:message-converters>
</mvc:annotation-driven>

在client-side,调用restTemplate的setMessageConverters()函数:

restTemplate.setMessageConverters(Arrays.asList(new MappingJackson2HttpMessageConverter())

##6. 安全认证 Restful API一般不搞Login/Logout+SessionId的认证方式,而是无状态的每次附带认证信息。 Showcase中演示了Http Basic在Http Header中明文传密码,然后利用Shiro的http basic集成。 当然,也可以自己简单的写个Filter来做这个事情。