REST(全称Representational State Transfer)是Roy Thomas Fielding
在2000年提出的一种基于HTTP的接口服务设计风格,而非一种新的传输协议,由于这种设计简洁优雅,近些年来逐渐流行起来。在JavaEE中,JAX-RS是一组用于构建RESTful风格的API,它使得在JavaEE平台上开发和部署RESTful服务变得更加简单。JAX-RS提供了基于注解的简洁的方式来定义资源以及如何与之交互,使得开发者可以轻松地将Java类和方法映射到HTTP请求方法和URI上。
这篇笔记我们学习什么是RESTful风格,以及JavaEE中JAX-RS的使用。本章节基于当前最新的JDK8、JavaEE8版本和Wildfly14应用服务器,Wildfly14中JAX-RS的实现框架和JAX-WS相同,都是CXF。
REST基于HTTP现有的功能,定义了资源定位和资源操作(其实就是资源增删改查)两种类型的使用方式,包括利用HTTP协议的GET,POST,PUT,DELETE四种动作实现以下4种类型的资源操作:
REST风格要求URL参数中尽量不要有动词,因为“动作”已经在HTTP协议的动作字段中指定了。举例来说,获取id
为1
的用户所有订单,如果使用传统的HTTP接口定义风格,我们可能会如下设计:
GET /getOrdersByUserId?userId=1
但RESTful要求我们按如下设计:
GET /v1/users/1/orders?_type=xml
/v1
:我们设计的API可以携带一个版本号,以免API升级时给原来用户造成不便。/users/1/orders
:/users
代表所有用户集合,/1
表示在所有用户集合中取id
为1
的用户,/orders
表示这个用户的所有订单集合,REST接口的资源定位描述大致就是按照这种形式构造的。?_type=xml
:指定返回的数据类型是xml。REST接口可以带有参数,但是参数的作用类似于形容词,而不应该是动词。更多的例子如下。
# 新建一个用户
POST /v1/users
# 修改id为1的用户信息
PUT /v1/users/1
# 删除id为1的用户
DELETE /v1/users/1
REST相对HTTP使用的历史来说算是个新技术。由于一些历史原因,某些HTTP客户端或服务器不支持PUT和DELETE请求,此时一种替代方案是用POST代替,但在POST中加一个特殊参数例如method=put
,用来表示这个POST请求是否按照REST形式解析,这种方式用的也十分广泛。
RESTful风格接口是对HTTP协议很好的诠释。使用REST接口,对网络资源的定位更加简单,RESTful风格结构清晰便于理解。但是当URL的层级过多时,REST接口的可读性就不好了。
当然上面说的这些都是理论上的,我个人认为REST接口用于参数较少,比较简单的资源定位还是十分方便的。比如就像前面举的例子,查询用户、订单等,此时RESTful风格的接口可读性非常好,层次十分清晰。但是一旦复杂起来就不行了,复杂的查询条件会让REST接口编写十分困难(换句话说就是RESTful风格下没法写出太复杂的查询条件),强行按RESTful设计的结果就是接口冗长而且可读性几乎没有,给接口调用带来困难。好在大多数情况下接口都是属于前者,因此RESTful风格一定程度上得以流行。
这里我们基于Wildfly14应用服务器开发WebService,由于应用服务器已经内置了CXF,我们只需要引入JavaEE SDK即可。
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-api</artifactId>
<version>8.0.1</version>
<scope>provided</scope>
</dependency>
下面例子和之前JAX-WS相同,我们仍然编写天气查询接口,它接收一个城市名称作为输入,并返回该城市的天气信息,工程目录结构如下。
|_ src/main
|_ java
|_ model
|_ Weather.java
|_ service
|_ CityService.java
|_ impl
|_ CityServiceImpl.java
|_ webapp
|_ WEB-INF
|_ web.xml
wildfly14中,使用内置的CXF实现JAX-RS需要一些额外配置,我们需要在web.xml
中配置javax.ws.rs.core.Application
的映射。
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="false">
<servlet-mapping>
<servlet-name>javax.ws.rs.core.Application</servlet-name>
<url-pattern>/ws/*</url-pattern>
</servlet-mapping>
</web-app>
配置中,<url-pattern>/ws/*</url-pattern>
表示所有以/ws
开头的请求都会被映射到javax.ws.rs.core.Application
,交给CXF的JAX-RS模块执行。
对于实体类Weather
,由于这里我们需要使用XML格式输出,因此额外标注了一个JAXB注解@XmlRootElement
,这个注解是必须的(对于XML输出方式来说),如果不指定CXF会因为找不到该如何序列化这个类的对象而报错,对于其它字段我们也可以使用JAXB注解进行修饰。
package com.gacfox.demo.demows.model;
import lombok.Data;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Date;
@Data
@XmlRootElement(name = "weather")
public class Weather {
private String city;
private String weather;
private int temperature;
private Date date;
}
下面代码我们定义了天气查询接口的接口类和实现类。
package com.gacfox.demo.demows.service;
import com.gacfox.demo.demows.model.Weather;
import javax.jws.WebService;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@WebService
@Path("/api/cities")
public interface CityService {
@GET
@Path("/{city}/weather")
@Produces(MediaType.APPLICATION_XML)
Weather queryCityWeather(@PathParam("city") String city);
}
接口类中,同时使用@WebService
和@Path
会被识别为定义了JAX-RS服务,类级别的@Path
中我们指定了基础路径/api/cities
,而具体的方法中则对应于一个具体的Restful风格的接口,此时访问/api/cities/{city}/weather
就会调用queryCityWeather
方法。
除此之外,我们还在方法上指定了@GET
标识该接口使用GET请求,@Produces
标识了该接口的输出格式为XML。
package com.gacfox.demo.demows.service.impl;
import com.gacfox.demo.demows.model.Weather;
import com.gacfox.demo.demows.service.CityService;
import java.util.Date;
public class CityServiceImpl implements CityService {
@Override
public Weather queryCityWeather(String city) {
// ... 具体业务逻辑
return weather;
}
}
实现类没有什么特殊之处,上述代码都编写完成后,此时我们将工程部署到Wildfly即可看到效果。如果我们的ContextPath的demows
,我们访问例如:http://localhost:8080/demows/ws/api/cities/DemoCity/weather
,即可看到输出的XML报文。
至于为什么我们这里定义了CityService
,而不是起名为WeatherService
呢?因为从嵌套逻辑上说,“天气”是一个“城市”的子级资源,按照RESTful设计它的资源定位符应该是类似/cities/{城市ID}/weather
,因此将其编写在CityService
是更加合适的。
HTTP中GET和DELETE没要请求体,但POST和PUT需要请求体来映射到Java实体类的,这里演示下如何实现POST接口。下面例子我们实现了一个接口,用于添加城市。
package com.gacfox.demo.demows.service;
import com.gacfox.demo.demows.model.City;
import javax.jws.WebService;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
@WebService
@Path("/api/cities")
public interface CityService {
@POST
@Path("")
@Consumes(MediaType.APPLICATION_XML)
void addCity(City city);
}
和之前一样,实体类需要使用JAXB注解来标识,此外这里由于我们传入了一个XML格式数据,因此需要标注@Consumes(MediaType.APPLICATION_XML)
。我们可以用HTTP调试工具发起POST请求进行测试。
请求路径(假设工程的ContextPath为demows
):
POST /demows/ws/api/cities
请求体:
<city>
<cityCode>001</cityCode>
<cityName>DemoCity</cityName>
</city>
JSON(JavaScript Object Notation)是一种轻量级的新型数据交换格式,它易于人们阅读和编写,并易于机器解析和生成。JSON格式的灵感来源于JavaScript对象字面量语法,但它也能够被很多其他编程语言支持,JSON因其轻量级、简洁性、易用性和广泛的支持而成为了一种近年来逐渐流行的数据交换格式,在部分场景可以作为XML的替代。JAX-RS标准和CXF框架也对JSON进行了支持。
对于JSON数据,我们将之前的@Produces
和@Consumes
中的类型改为MediaType.APPLICATION_JSON
即可。