RESTful API 入门一(JAX-RS实现)

前言

  关于RESTful API的概念网上已经说过很多,可以用很多语言与框架实现,其规范也十分详尽。我这篇入门教程尽量用自己的理解去总结RESTful API的一些要点,并以JaveEE规范的JAX-RS框架为例,以实战的形式去讲述怎么搭建一套RESTful API,并在实践过程中体现RESTful API的设计规范。

什么是RESTful API

  1. 服务器端提供给开发者的一套接口,开发者通过调用这些接口,访问服务器的资源。这些资源通常是数据库的表抽象出来的对象,程序员通过获取这些对象,把对这些对象的增删改查映射到数据库,从而实现自己前后端的其他业务逻辑。目前移动端很多都是使用RESTful API来访问后台,受移动端影响,浏览器端也越来越多地使用这种规范。
  2. 本质就是http协议。RESTful API的所有操作无非是CRUD,全部映射成http的POST,GET,PUT,PATCH,DELETE等方法,操作RESTful API实际上就是操作http报文,其只是在http协议外面再封装一层。因此操作十分简洁,直观,代码优雅。
  3. 用URI定位所有资源。服务器端把所有供开发者访问的资源都以特定的URI表示。URI与http方法(上述的POST,GET等)组合,便可以实现对所有资源的所有操作。以微博和评论为例,假如新浪微博开放了所有微博和评论允许开发者访问。那么所有微博的URI可能是:
    api/weibo

      id为30691的微博URI可能是:
    api/weibo/30691

      id为30691的微博所有评论的URI可能是:
    api/weibo/30691/comment

      id为30691的微博对应的评论id为2589的评论的URI可能是:
    api/weibo/30691/comment/2589

      出于面向对象的思想,一般每个资源都对应一个对象,应该用名词表示,且倾向于单数。资源之间的依赖关系可以参照上述的weibo与comment。由于评论必定对应于某一条微博,所以可以用类似weibo/30691/comment的形式表示这种依赖。

  显而易见,这种目录式的URI定位十分直观,简单。

搭建基本JAX-RS环境

  JAX-RS是JaveEE框架里专门用来实现RESTful API的技术。搭建十分简单,且大都用注解注入的形式实现,使代码结构十分清晰,代码量很少。除了依赖的包以及一些xml配置文件以外,代码里只需要自己继承实现javax.ws.rs.core.Application类便可以完成最基本的配置。依赖的包和xml配置可自行上网搜索,不是我这篇教程的重点,这里将主要介绍代码里如何实现RESTful API

Application类

  必须创建一个类继承Application类。JAX-RS里我们用到的组件主要有两类,分别是Resource和Provider。Resource就是上述的URI对应的可供用户访问的资源。Provider是运行时自行调用的一些类,起到配置作用,这在下一讲再详细介绍。这两类组件都必须在Application类的实现类的Set> getClasses()方法里进行注册。以下是官网文档的示例:

1
2
3
4
5
6
7
8
public class MyApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> s = new HashSet<Class<?>>();
s.add(HelloWorldResource.class);
return s;
}
}

  HelloWorldResource就是Resource组件的其中一个类,如果RESTful API需要访问这个资源类,则需要用上述的语法实现注册,是不是很简单。

ResourceConfig类

  ResourceConfig是JAX-RS里提供的一个Application的实现类,通过继承这个类并且在其派生类的构造方法里对组件进行注册,可以简化注册的过程。除了对需要使用的Resource和Provider组件进行register以外,还可以通过提供要注册组件所在的包,而自动将包内所有这两类组件注册。以下是示例:

1
2
3
4
5
public class MyApplication extends ResourceConfig {
public MyApplication() {
packages("org.foo.rest;org.bar.rest");
}
}

1
2
3
4
5
6
public class MyRESTExampleApplication extends ResourceConfig {
public MyRESTExampleApplication() {
packages("com.carano.fleet4all.restExample");
register(JacksonFeature.class);
}
}
ApplicationPath注解

  可以为Application的实现类添加@javax.ws.rs.ApplicationPath注解,那么所有RESTful API的URI资源都将包含该注解提供的名字作为前缀。以下是示例:

1
2
3
4
5
6
7
8
9
10
@javax.ws.rs.ApplicationPath("api")
public class ApplicationConfig extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> resources = new java.util.HashSet<>();
addRestResourceClasses(resources);
resources.add(JacksonFeature.class);
return resources;
}
}

  在这里注册的所有资源,其URI定位都必然是/api/…

Resource组件

  搭建了基本的JAX-RS环境,便可以进行真正的业务逻辑的实现了。所谓的业务逻辑,无非对特定URI资源的增删改查操作。这些都是在Resource组件方法里通过特定的注解实现操作和资源的匹配的。所有在类名上面添加了@path注解的,都被认为是Resource组件。这些类方法上添加的@path注解,将与类的@path注解提供的名字以及ApplicationPath注解提供的名字组合在一起,作为其URI。

  以下是Resource组件类示例:

1
2
@Path("message")
public class MessageFacadeREST extends AbstractFacade<Message> {

  以下是某资源方法示例:

1
2
3
4
5
@GET
@Path("danger-unsolved")
@Consumes({"application/json"})
@Produces({"application/json;charset=UTF-8"})
public Response findUnSolved(@QueryParam("page") int page,@QueryParam("per-page") int perPage,@Context HttpServletResponse response) {

  先忽略掉无关的代码,只看@GET和@Path标注。@GET很容易猜到是用http的GET方法。@Path便是该资源的URI的一部分。如果findUnSolved方法是类MessageFacadeREST里的方法,且MessageFacadeREST在ApplicationConfig类里注册,那么findUnSolved方法便可以通过/api/message/danger-unsolved这个URI访问。如果加上@GET方法,那么就是对/api/message/danger-unsolved这个资源用http的GET方法访问,实际上便是调用对应的findUnSolved方法。

HTTP方法

  RESTful API里涉及的HTTP方法一般是GET,POST,PUT,PATCH,DELETE。这些HTTP方法都是用注解形式添加在相应的Resource组件类的方法上。

  1. GET方法:用于对资源的读取,一般返回单个对象或者对象列表。
  2. POST方法:用于创建新对象,需要在客户端传送一个完整对象给服务器,然后将整个对象写入数据库。一般需返回创建好的对象。
  3. PUT方法:用于创建或替换已有对象,需要在客户端传送一个完整对象给服务器,如果数据库没有对应数据,则插入新数据;如果已有旧记录,则用新对象更新。一般需返回创建或更新的对象。
  4. PATCH方法:用于修改某已有对象的部分属性。如果确保对象记录已经在服务器存在,且只需要修改很少的属性,那么推荐用PATCH方法去代替PUT方法。PATCH方法不需要传送整个对象,只需要传送对象id以及需要修改的属性即可。一般需返回更新的对象。
  5. DELETE方法:删除相应对象记录。无返回。

  有一些HTTP proxy只支持GET和POST方法,而不支持其他方法。这种情况在规范里一般在http header里加入X-HTTP-Method-Override的key,里面保存真正的http方法,例如”PUT”或”PATCH”,而请求一律通过POST方法发送到服务器端。具体的代码实现将会在下一讲介绍。

数据传输及序列化与反序列化

  在RESTful API操作的是对象,传出与接收一般是实体的POJO对象。而对象如果传出,需要先进行序列化,以xml或json的形式传送。对象如果接收,则要进行反序列化,把json或xml的数据转换成实体的POJO对象。序列化或进行反序列化的数据类型一般用@Produces和@Consumes注解来实现。下面给出一个例子:

1
2
3
4
5
@PUT
@Path("{id}/block")
@Consumes({"application/json"})
@Produces({"application/json"})
public MessageDTO block(@PathParam("id") Long id,@Context HttpServletRequest request,MessageDTO messageDTO) {

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
@XmlRootElement
public class MessageDTO {
private Long message1Id;
private String message1Word;
private int message1CommentCount;
private int message1TransmissionCount;
private String message1UserName;
private Timestamp message1Date;
private Long message1OwnerId;
private boolean referenced;
private Long message2Id;
private String message2Word;
private int message2CommentCount;
private int message2TransmissionCount;
private String message2UserName;
private Timestamp message2Date;
private Long message2OwnerId;
private boolean blocked;
private boolean deleted;
private Timestamp managedDate;
private String action;
private String operator;
public Timestamp getManagedDate() {
return managedDate;
}
public void setManagedDate(Timestamp managedDate) {
this.managedDate = managedDate;
}
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
public String getOperator() {
return operator;
}
public void setOperator(String operator) {
this.operator = operator;
}
public boolean isBlocked() {
return blocked;
}
public void setBlocked(boolean blocked) {
this.blocked = blocked;
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public MessageDTO() {
}
public Long getMessage1Id() {
return message1Id;
}
public void setMessage1Id(Long message1Id) {
this.message1Id = message1Id;
}
public String getMessage1Word() {
return message1Word;
}
public void setMessage1Word(String message1Word) {
this.message1Word = message1Word;
}
public int getMessage1CommentCount() {
return message1CommentCount;
}
public void setMessage1CommentCount(int message1CommentCount) {
this.message1CommentCount = message1CommentCount;
}
public int getMessage1TransmissionCount() {
return message1TransmissionCount;
}
public void setMessage1TransmissionCount(int message1TransmissionCount) {
this.message1TransmissionCount = message1TransmissionCount;
}
public String getMessage1UserName() {
return message1UserName;
}
public void setMessage1UserName(String message1UserName) {
this.message1UserName = message1UserName;
}
public Timestamp getMessage1Date() {
return message1Date;
}
public void setMessage1Date(Timestamp message1Date) {
this.message1Date = message1Date;
}
public Long getMessage1OwnerId() {
return message1OwnerId;
}
public void setMessage1OwnerId(Long message1OwnerId) {
this.message1OwnerId = message1OwnerId;
}
public boolean isReferenced() {
return referenced;
}
public void setReferenced(boolean referenced) {
this.referenced = referenced;
}
public Long getMessage2Id() {
return message2Id;
}
public void setMessage2Id(Long message2Id) {
this.message2Id = message2Id;
}
public String getMessage2Word() {
return message2Word;
}
public void setMessage2Word(String message2Word) {
this.message2Word = message2Word;
}
public int getMessage2CommentCount() {
return message2CommentCount;
}
public void setMessage2CommentCount(int message2CommentCount) {
this.message2CommentCount = message2CommentCount;
}
public int getMessage2TransmissionCount() {
return message2TransmissionCount;
}
public void setMessage2TransmissionCount(int message2TransmissionCount) {
this.message2TransmissionCount = message2TransmissionCount;
}
public String getMessage2UserName() {
return message2UserName;
}
public void setMessage2UserName(String message2UserName) {
this.message2UserName = message2UserName;
}
public Timestamp getMessage2Date() {
return message2Date;
}
public void setMessage2Date(Timestamp message2Date) {
this.message2Date = message2Date;
}
public Long getMessage2OwnerId() {
return message2OwnerId;
}
public void setMessage2OwnerId(Long message2OwnerId) {
this.message2OwnerId = message2OwnerId;
}
}

  方法block是通过http的PUT方法访问的,需要传入一个MessageDTO对象,并返回一个MessageDTO对象。@Produces定义了返回对象将序列化成为json格式;@Cosumes定义了接收的对象是json格式的,将对其进行反序列化。JAX-RS里本身没有实现对特定对象自动进行序列化与反序列化。一般需实现MessageBodyWriter或MessageBodyReader这两个类,在这两个类里自行进行序列化与反序列化的处理。这两个类都属于Provider,将在下一讲介绍。但实际上,如果只是简单的进行json或xml的转换是非常简单的,并不需要另外实现Provider,只有对具体的格式有要求或者需要进行更多的配置才有必要实现。一般如果进行xml转换,只需要修改@Produces和@Consumes为对应格式,并且在需要进行序列化与反序列化的POJO类上加上@XmlRootElement注解,便可实现。这种做法实际上是JAX-RS后台调用JAXB实现,@XmlRootElement是JAXB注解,而JAXB是专门用于进行xml和JAVA对象转换的技术。如果需要用json格式,同样可以通过JAXB的@XmlRootElement注解实现,只需把@Provider或@Consumes修改为application/json即可。实际上如果你是用glassfish 4以上作为application server container的,如果以json格式进行传输不需要任何其他Provider或@XmlRootElement,只需要修改@Provider或@Consumes即可。因为glassfish 4已经配置了MOXy作为Default JSON-Binding Provider。MOXy是json的一个框架,它可以在Application里自动注册,不需要显示地手动注册资源。

  目前,RESTful API规范建议一律采用json作为数据传输的格式,而不要使用xml。

总结

  这篇教程主要针对JAX-RS的Resource组件的基础知识,介绍了RESTful API的概念,以及HTTP方法,URI资源定位和数据传输格式这三个最核心的内容,并涉及了一些RESTful API的设计规范。更多的设计规范以及关于Provider的知识,将再后面一一介绍。