1.6 快速了解Java REST服务
1.6.1 REST工程类型
在REST服务中,资源类是接收REST请求并完成响应的核心类,而资源类是由REST服务的“提供者”来调度的。这一概念类似其他框架中自定义的Servlet类,该类会将请求分派给指定的Controller/Action类来处理。本节将讲述REST中的这个提供者,即JAX-RS2中定义的Application以及Servlet。
Application类在JAX-RS2(JSR339,详见参考资料)标准中定义为javax.ws.rs.core.Application,相当于JAX-RS2服务的入口。如果REST服务没有自定义Application的子类,容器将默认生成一个javax.ws.rs.core.Application类。
本节根据JAX-RS2规范第2章中对REST服务场景的定义,将REST服务分为四种类型,如图1-1所示。
图1-1 REST工程类型示意图
图1-1将JAX-RS2标准中对REST服务的类型图形化,依据不同的条件分为了四种类型。
❏类型一:当服务中没有Application子类时,容器会查找Servlet的子类来做入口,如果Servlet的子类也不存在,则REST服务类型为类型一,对应图1-1中的例1。
❏类型二:当服务中没有Application子类时,存在Servlet的子类,则REST服务类型为类型二,对应图1-1中的例2。
❏类型三:服务中定义了Application的子类,而且这个Application的子类使用了@ApplicationPath注解,则REST服务类型为类型三,对应图1-1中的例3。
❏类型四:如果服务中定义了Application的子类,但是这个Application的子类没有使用@ApplicationPath注解,则REST服务类型为类型四,对应图1-1中的例4。
上面提到的四个示例在下面的“阅读指南”中给出了源代码目录和Github下载地址,需要读者仔细体会示例之间的差异,以更好地理解和使用不同类型的REST服务。
1.REST服务类型一
类型一对应的是图1-1中的例1,相应的逻辑是服务中同时不存在Application的子类和Servlet子类。在JAX-RS2(JSR339)中定义这种情况下应作如下处理:为REST服务动态生成一个名称为javax.ws.rs.core.Application的Servlet实例,并自动探测匹配资源。与此同时,需要根据Servlet的不同版本,在web.xml定义REST请求处理的Servlet为这个动态生成的Servlet,并定义该Servlet对资源路径的匹配。在没有Application的子类存在的情况下,在web.xml中定义Servlet是必不可少的配置。
阅读指南
REST服务类型一所对应的示例,即例1的源代码地址如下。
❏https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.1.myrest-servlet2-webxml。
❏https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.2.myrest-servlet3-webxml。
请使用mvn jetty:run启动服务,使用curl http://localhost:8080/webapi/myresource测试服务。
REST服务类型一的示例包含两个小项目,分别对应Servlet2和Servlet3两种容器依赖场景。我们只须关注Maven配置文件(pom.xml)和Web服务配置文件(web.xml)的区别即可理解无Application子类情况下,如何实现基于Servlet2和Servlet3容器内的服务。
Servlet3的最简配置示例代码如下。
<? xml version="1.0" encoding="UTF-8"? > <web-app version="3.0" xmlns="http://java.sun.com/xml/ns/Java EE" xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation="http:// java.sun.com/xml/ns/Java EE http://java.sun.com/xml/ns/Java EE/web-app_3_0.xsd"> <servlet> <servlet-name>javax.ws.rs.core.Application</servlet-name> </servlet> <servlet-mapping> <servlet-name>javax.ws.rs.core.Application</servlet-name> <url-pattern>/webapi/*</url-pattern> </servlet-mapping> </web-app>
相对于Servlet2而言,在Servlet3中,servlet的定义可以只包含servlet-name。再次强调,Jersey的Servlet3的容器支持包是jersey-container-servlet。Servlet2的最简配置示例代码如下。
<? xml version="1.0" encoding="UTF-8"? > <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/Java EE" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http:// java.sun.com/xml/ns/Java EE http://java.sun.com/xml/ns/Java EE/web-app_2_5.xsd"> <servlet> <servlet-name>Jersey Web Application</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> <param-name>jersey.config.server.provider.packages</param-name> <param-value>com.example</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Jersey Web Application</servlet-name> <url-pattern>/webapi/*</url-pattern> </servlet-mapping> </web-app>
servlet的定义包含servlet-name和servlet-class,其初始化参数需要显示给出要加载的资源类所在的包名,可以看出Servlet2的支持包jersey-container-servlet-core不具备自动扫描资源类的功能。
2.REST服务类型二
类型二对应的是图1-1中的例2,相应的逻辑是不存在Application的子类但存在Servlet的子类。
阅读指南
REST服务类型二所对应的示例,即例2的源代码地址如下。
❏https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.3.myrest-subservlet。
本例定义了Servlet子类AirServlet,该类继承自org.glassfish.jersey.servlet.ServletContainer类,这是Jersey2中Servlet的基类,继承自HttpServlet。AirServlet类的代码示例如下。
@WebServlet( initParams = @WebInitParam( name = "jersey.config.server.provider.packages", value = "com.example"), urlPatterns = "/webapi/*", loadOnStartup = 1) public class AirServlet extends ServletContainer {
AirServlet使用了WebServlet注解来配置Servlet参数。包括初始化参数initParams中定义扫描的资源类所在的包名:com.example, Servlet匹配的资源路径:urlPatterns="/webapi/*"和启动时的加载标识:loadOnStartup=1。
例2是基于Servlet3容器的REST服务,使用了WebServlet注解和无web.xml等Servlet3引入而Servlet2没有的功能。在自定义Servlet3.x子类的场景下,web.xml可以省略,但需要修改Maven的maven-war-plugin插件的配置,添加failOnMissingWebXml为false,这样编译时才不会报错。Maven配置文件中相关信息如下所示。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.3</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency>
3.REST服务类型三
类型三对应的是图1-1中的例3,相应的逻辑是存在Application的子类并且定义了@ApplicationPath注解。
阅读指南
REST服务类型三所对应的示例,即例3的源代码地址如下。
❏https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.4.myrest-servlet3-application。
❏https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.5.myrest-servlet2-rc。
REST服务类型三的示例包含两个小项目。其中,servlet2-rc项目基于Servlet2, AirResourceConfig类继承自Application的子类ResourceConfig类;servlet3-application项目基于Servlet3, AirApplication类继承自Application类。基于Servlet2的REST服务需要定义web.xml(但内容可以是“空的”,即只有web-app的基本定义),基于Servlet3的REST服务可以省略此文件。AirApplication类代码示例如下。
@ApplicationPath("/webapi/*") public class AirApplication extends Application { @Override public Set<Class<? >> getClasses() { final Set<Class<? >> classes = new HashSet<Class<? >>(); classes.add(MyResource.class); return classes; } }
AirApplication类覆盖了getClasses()方法,注册了资源类MyResource,这样在服务启动后,MyResource类提供的资源路径将被映射到内存,以便请求处理时匹配相关的资源类和方法。AirResourceConfig类代码示例如下。
@ApplicationPath("/webapi/\*") public class AirResourceConfig extends ResourceConfig { public AirResourceConfig() { packages("com.example"); } }
AirResourceConfig类在构造子中提供了扫描包的全名,这样在服务启动后,com.example包内资源类所提供的资源路径将被映射到内存。
4.REST服务类型四
类型四对应的是图1-1中的例4,相应的逻辑是一有二无:一有是存在Application的子类;二无是不存在Servlet子类、不存在或者不允许使用注解@ApplicationPath。
阅读指南
REST服务类型四所对应的示例,即例4的源代码地址如下。
❏https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.6.myrest-servlet2-application。
❏https://github.com/feuyeux/jax-rs2-guide-II/tree/master/1.6.7.myrest-servlet3-application。
REST服务类型四的示例包含两个小项目,演示了基于Servlet2和Servlet3两个版本的REST服务,其差异仅此而已,关于差异性配置前面的例子已经讲过,不再冗述。如下以servlet3-application为例说明。AirApplication类是Application的子类,代码示例如下。
public class AirApplication extends Application { @Override public Set<Class<? >> getClasses() { final Set<Class<? >> classes = new HashSet<Class<? >>(); classes.add(MyResource.class); return classes; } }
代码和类型三的示例相仿,但是该类没有定义@ApplicationPath注解,因此我们需要在web.xml中配置Servlet和映射资源路径,代码示例如下。
<? xml version="1.0" encoding="UTF-8"? > <web-app xmlns="http://java.sun.com/xml/ns/Java EE" xmlns:xsi="http://www. w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/ Java EE http://java.sun.com/xml/ns/Java EE/web-app_3_0.xsd"version="3.0"> <servlet> <servlet-name>com.example.AirApplication</servlet-name> </servlet> <servlet-mapping> <servlet-name>com.example.AirApplication</servlet-name> <url-pattern>/webapi/\*</url-pattern> </servlet-mapping> </web-app>
在servlet-name中使用自定义的Application子类com.example.AirApplication的全名作为Servlet名称,并在url-pattern中映射资源路径。
1.6.2 REST应用描述
在明白如何创建和部署各种类型的REST服务后,我们来了解一下部署好的REST服务中一个特殊的成员,REST应用的描述:以XML格式展示当前REST环境中所提供的REST服务接口。这种XML格式的描述就是WADL(Web Application Description Language, Web应用描述语言)。
WADL是用来描述基于HTTP协议的REST式Web服务部署情况的。它采用XML格式,支持多种数据类型的描述。WADL由Sun公司提出,尚未成为W3C或者OASIS的标准,JAX-RS标准中并没有关于WADL的定义和说明。Jersey作为JAX-RS2的参考实现默认支持服务的WADL。通过浏览器访问“服务根路径/application.wadl”即可打开该服务的WADL内容。相对于REST服务,WSDL更为人们所熟知,WSDL是RPC风格的基于SOAP的Web服务的描述语言。两者缩写类似而且都使用XML格式,此外共性不多。
1.应用的描述
以REST服务类型四的示例项目1.6.7.myrest-servlet3-application为例,该应用的WADL路径如下:http://localhost:8080/myrest-servlet3-application/webapi/application.wadl。
通过浏览器访问该路径,可以一览WADL的schema结构。WADL的最外层标签是application,代表应用。然后自上而下分别是doc、grammars和resources。resources是应用提供的资源集合,里面至少包含application.wadl,以及应用中包含的资源描述,比如本例的资源信息描述在资源路径myresource之内,如下所示。
<? xml version="1.0" encoding="UTF-8" standalone="yes"? > <application> <doc jersey:generatedBy="Jersey: 2.32013-09-20 13:59:07"/> <grammars/> <resources base="http://localhost:8080/myrest-servlet3-application/webapi/"> <resource path="myresource">...</resource> <resource path="application.wadl">...</resource> </resources> </application>
2.资源的描述
可以展开myresource来查看具体某个方法的WADL,也可以通过发送一条请求并定义请求头信息来获取。以cURL(详见1.8节)为例,命令如下。
curl -X OPTIONS -H "Allow: application/vnd.sun.wadl+xml" -v http://localhost: 8080/myrest-servlet3-application/webapi/myresource
myrest-servlet3-application提供的资源接口,对照服务器返回的XML,可以更清晰地理解WADL的内容。其WADL内容如下。
<resource path="myresource"> <method id="getIt" name="GET"> <response> <representation mediaType="text/plain"/> </response> </method> <method id="apply" name="OPTIONS"> <request> <representation mediaType="*/*"/> </request> <response> <representation mediaType="application/vnd.sun.wadl+xml"/> </response> </method> <method id="apply" name="OPTIONS"> <request> <representation mediaType="*/*"/> </request> <response> <representation mediaType="text/plain"/> </response> </method> <method id="apply" name="OPTIONS"> <request> <representation mediaType="*/*"/> </request> <response> <representation mediaType="*/*"/> </response> </method> </resource>
在这段代码中,公布了四个方法。其中,getIt方法代码如下。其他三个OPTIONS请求方法是Jersey默认实现的,用以描述getiIt方法,分别返回text/plain类型,*/*类型和application/vnd.sun.wadl+xml类型。
@GET @Produces(MediaType.TEXT_PLAIN) public String getIt() { return "Got it! "; }
getIt方法定义为GET请求方法,@Produces中定义的媒体类型是MediaType.TEXT_PLAIN,即响应过程中生产的数据,其表述性状态以text/plain媒体类型转移。
3.WADL的配置
上述OPTIONS请求方法的实现是Jersey默认支持的,如果读者不希望在REST服务中让Jersey自动生成,可以通过配置jersey.config.server.wadl.disableWadl=true来实现。代码示例如下。
public class AirApplication extends ResourceConfig { public AirApplication() { property(ServerProperties.WADL_FEATURE_DISABLE, true); packages("com.example.resource"); } }
在构造函数中,我们通过定义ServerProperties.WADL_FEATURE_DISABLE属性为true以实现去除WADL自动生成的功能。或者,可以通过修改Web配置文件中servlet启动参数来实现,代码示例如下。
<servlet> <servlet-name>com.example.AirApplication</servlet-name> <init-param> <param-name>jersey.config.server.wadl.disableWadl</param-name> <param-value>true</param-value> </init-param> </servlet>
配置文件中定义了启动参数jersey.config.server.wadl.disableWadl,其值定义为true,以实现去除WADL自动生成的功能。