RESTful Web服务的Java™API(JAX-RS)是接口和Java注释的集合,可简化服务器端REST应用程序的开发。 感谢JAX-RS,您可以通过注释类来响应特定的HTTP方法(例如GET或POST)和统一资源标识符(URI)模式来指定RESTful Web服务行为。 JAX-RS的当前版本是2.0版,并且是Java Platform Enterprise Edition(Java EE)7中必需的API。
本文演示了如何使用IBM®WebSphere®Application Server内置的功能(例如对象反序列化,定制序列化和异常处理)创建简单的,兼容JAX-RS 2.0的REST服务。 本文还包括三个Eclipse项目,可以从GitHub下载这些项目,在阅读本文时可以遵循这些项目。
可下载的项目文件
在阅读本文之前,请从GitHub下载三个项目文件。 这三个项目是EAR项目,EJB项目和Web项目。 通过下载这些项目,您可以在阅读本文时按照它们进行操作。 这些项目文件是从Eclipse 4.5.2(火星)IDE与传统的Mars WebSphere Application Server V9.x开发人员工具(可从Eclipse Marketplace获得)导出的。
EJB项目中的persistence.xml
文件配置为使用WebSphere Application Server V9提供的内置Derby JTA数据源。 如果要配置和使用其他JTA数据源,请在WebSphere Application Server V9管理控制台中配置JTA数据源,然后编辑persistence.xml
文件以指定新的JTA数据源。
WebSphere Application Server中的Java EE 7 JAX-RS功能
从WebSphere Liberty Profile v8.5.5.6和传统的WebSphere Application Server V9开始,提供了对JAX-RS 2.0的支持。 这些WebSphere Application Server运行时提供以下功能:
- 基于Apache CXF 3.0的JAX-RS 2.0服务器运行时
- 内置的普通旧Java对象(POJO)实体提供程序支持,由Jackson JSON处理器提供
- 内置的IBM JSON4J实体提供程序支持
JAX-RS 2.0引入了以下新功能:
- 客户端API
- 异步请求处理
- 超媒体支持(超媒体作为状态引擎(HATEOS))
- 新注释(增强了对上下文和依赖注入(CDI)的支持)
- Bean验证
- 过滤器和处理程序
- 更丰富的内容协商
设计和实现一个简单的RESTful应用程序
在本文中,您将学习如何设计和实现学生信息系统的基础,该系统是学生注册应用程序(SRA)。 SRA演示了JAX-RS 2.0提供的一些新功能,特别是HATEOAS支持以及改进的注入和bean验证。
下图说明了应用程序的组件及其高层交互。
图1.学生注册申请组件
SRA提供以下功能来管理学生注册:
- 新生注册
- 编辑注册的学生信息
- 列出注册学生
- 删除学生注册
SRA由以下组件组成:
- 提供RESTful服务的Web模块
- 提供数据库创建,检索,更新和删除逻辑的EJB模块
- 保存学生记录的数据库
JAX-RS运行时使用StudentRegistrationApplication配置类来确定SRA中的类,即资源类和提供者类。 以下清单显示了其中注册了资源和提供者类的StudentRegistrationApplication
类。
清单1. StudentRegistrationApplication配置类
@ApplicationPath("rest")
public class StudentRegistrationApplication extends Application {
}
WebSphere Application Server V9提供了JAX-RS 2.0感知的Web容器,因此StudentRegistrationApplication
类上的@ApplicationPath
批注将用于将/rest/*
URL模式映射到StudentRegistrationApplication
类。 然后,WebSphere Application Server Web容器会自动确保对/rest/*
URL模式的引用会启动适当的资源类。
不需要其他配置,WebSphere Application Server Web容器在启动过程中扫描资源类( @Path
@Provider
)和提供程序类( @Provider
)。
SRA具有单个资源类,即StudentResource
类。 StudentResource
类代表注册学生的整个集合。 以下清单显示了该类的初始实现。
清单2.学生资源类
@Path( "/students" )
@Stateless
public class StudentResource {
private static final String LINK_SELF = "self";
@EJB
IStudent studentService;
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response create(@Context UriInfo uriInfo, BaseStudentDto student) {
Student newStudent = createNewStudent(student);
URI uri = uriInfo.getBaseUriBuilder()
.path(StudentResource.class)
.path(newStudent.getId())
.build();
return Response.created(uri).build();
}
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response createFromForm(@Context UriInfo uriInfo, @Valid @BeanParam BaseStudentDto student) {
Student newStudent = createNewStudent(student);
URI uri = uriInfo.getBaseUriBuilder()
.path(StudentResource.class)
.path(newStudent.getId())
.build();
return Response.created(uri).build();
}
private Student createNewStudent(BaseStudentDto student) {
Student newStudent = new Student();
newStudent.setEmail(student.getEmail());
newStudent.setFirstName(student.getFirstName());
newStudent.setLastName(student.getLastName());
newStudent.setGender(student.getGender());
newStudent = studentService.create(newStudent);
return newStudent;
}
@DELETE
@Path( "/{id}")
public Response delete( @PathParam( "id" ) String id ) {
studentService.delete( id );
return Response.status( Status.NO_CONTENT ).build();
}
@GET
@Produces( MediaType.APPLICATION_JSON )
public Response getAll(@Context UriInfo uriInfo) {
List<Student> students = studentService.get();
List<HateoasStudentDto> wrappedStudents = new ArrayList<HateoasStudentDto>();
for (Student student : students) {
HateoasStudentDto wrappedStudent = new HateoasStudentDto(student);
Link link = buildSelfLink(uriInfo, wrappedStudent);
wrappedStudent.addLink(link);
wrappedStudents.add(wrappedStudent);
}
return Response.ok(wrappedStudents).build();
}
@GET
@Path( "/{id}" )
@Produces( MediaType.APPLICATION_JSON )
public Response getById(@PathParam("id") String id) {
Student student = studentService.findById(id);
if (student != null) {
HateoasStudentDto wrappedStudent = new HateoasStudentDto(student);
Link link = buildSelfLink(uriInfo, wrappedStudent);
wrappedStudent.addLink(link);
return Response.ok(wrappedStudent).build();
} else {
return Response.status(Status.NOT_FOUND).build();
}
}
private Link buildSelfLink(UriInfo uriInfo, HateoasStudentDto student) {
URI uri = uriInfo.getBaseUriBuilder().path(StudentResource.class).path(student.getId()).build();
Link link = Link.fromUri(uri).rel(LINK_SELF).type(MediaType.APPLICATION_JSON).build();
return link;
}
@PUT
@Consumes( MediaType.APPLICATION_JSON )
public Response update( @Context UriInfo uriInfo, Student student ) {
Student updatedStudent = studentService.update( student );
return Response.ok( updatedStudent ).build();
}
}
@Path( "/students" )
注释意味着学生资源类将处理对context_root/rest/students
请求。
资源类定义以下方法:
- 创建 。 您可以使用JSON中新学生信息的HTTP POST创建新学生记录。
- createFromForm。 您可以使用表单中的新学生信息的HTTP POST创建新学生记录。
- 删除 。 您可以使用HTTP DELETE删除特定的学生记录。
- getAll 。 您可以使用HTTP GET获得所有学生。
- getById 。 您可以使用HTTP GET通过学生ID获得特定学生。
- 更新 。 您可以使用HTTP PUT更新学生记录。
不要对学生的收集器收集范围的删除方法,因为你可能无意中删除注册的所有学生。
注释StudentResource
类的方法以使用或产生JSON内容,这是通过将媒体类型设置为application/json
来强制实施的。 createFromForm
方法是一个例外,它接受application/x-www-form-urlencoded
内容。
StudentResource
类使用EJB执行所需的数据库操作。 该@Stateless
在WebSphere Application Server的类级信号注释对待StudentResource
类为EJB。 这样,WebSphere Application Server会自动注入该类所依赖的EJB(即StudentService
EJB)。
REST服务中支持Java对象
上一个清单中显示的方法可以接受或返回BaseStudentDto
或HateoasStudentDto
类的实例(或实例列表)。 BaseStudentDto
类用作创建新学生记录的输入。
清单3. BaseStudentDto实体类
public class BaseStudentDto {
@NotNull
@FormParam("firstName")
String firstName;
@NotNull
@FormParam("lastName")
String lastName;
@NotNull
@FormParam("email")
String email;
@NotNull
@FormParam("gender")
Gender gender;
// Getters and setters omitted for brevity
}
HateoasStudentDto
实体类用于输出学生记录。
清单4. HateoasStudentDto实体类
@JsonPropertyOrder({ "id", "firstName", "lastName", "gender", "email", "registeredOn", "studentNumber", "_links" })
@JsonTypeName("student")
public class HateoasStudentDto extends BaseStudentDto {
private String id;
private Calendar registeredOn;
private String studentNumber;
private List<Link> links;
// Some getters and setters omitted for brevity
@JsonProperty("_links")
@JsonSerialize(using = LinksSerializer.class)
public List<Link> getLinks() {
return links;
}
}
REST服务中的HATEOAS支持
该StudentResource#getById
和StudentResource#getAll
方法转换的内容, Student
被从返回的实体bean StudentService
EJB来HateoasStudentDto
他们回到REST客户端之前的对象。 HateoasStudentDto
对象除包含Student
实体bean提供的属性外,还包含JAX-RS 2.0 Link
对象的集合。 JAX-RS 2.0 Link
对象为应用程序提供了一种以编程方式定义到REST资源的链接的方法,以便该应用程序可以更轻松地实现HATEOAS支持。
HateoasStudentDto
类是一个具有明确定义的getter的POJO。 因此,在WebSphere Application Server JAX-RS 2.0运行时间使用杰克逊JSON处理器自动序列化HateoasStudentDto
类,所述的任一单个实例HateoasStudentDto
类或的实例的集合HateoasStudentDto
类。 甚至Jackson JSON处理器也可以正确处理Student
实体bean使用的Gender
枚举。
HateoasStudentDto#getLinks
方法上的@JsonProperty
和@JsonSerialize
注释告诉Jackson JSON处理器如何序列化Link
对象的集合。 Link
对象的序列化通过使用一个称为LinksSerializer
的自定义序列化类LinksSerializer
。
通过针对/demo/rest/students
输入GET
测试应用程序时,应用程序将返回以下JSON。
清单5.测试应用程序时返回的JSON
[ {
"id": "0b031e9b-6933-4a10-9ffc-c7fd2c604331",
"firstName": "Aaron",
"lastName": "Aaronson",
"gender": "MALE",
"email": "aaaronson@domain.net",
"registeredOn": 1359417600000,
"studentNumber": "823-934",
"_links": [
{
"rel": "self",
"href": "http://localhost:9080/demo/rest/students/0b031e9b-6933-4a10-9ffc-c7fd2c604331",
"type": "application/json"
}
]
}, …
{
"id": "864eb556-053a-42d7-a757-e24928fb19a3",
"firstName": "Pamela",
"lastName": "Peterson",
"gender": "FEMALE",
"email": "ppeterson@domain.net",
"registeredOn": 1358467200000,
"studentNumber": "826-660",
"_links": [
{
"rel": "self",
"href": "http://localhost:9080/demo/rest/students/864eb556-053a-42d7-a757-e24928fb19a3",
"type": "application/json"
}
]
} ]
每个JSON学生记录中包含的_ links
集合提供了一个链接对象,以便REST客户端可以确定用于访问特定学生记录的URI。
输入验证和参数汇总
StudentResource#createFromForm
方法允许客户端直接提交表单以创建新的学生记录,而不是提交包含新学生信息的JSON Blob。 createFromForm
方法在BaseStudentDto
参数上有两个重要的注释: @Valid
和@BeanParam
。
Java Validation API的@Valid
参数部分)指示JAX-RS 2.0感知Web容器对提供的BaseStudentDto
参数执行Bean验证。
JAX-RS 2.0 @BeanParam
批注指示容器创建BaseStudentDto
类的实例,并使用REST客户端的表单提交提供的值注入新对象。 新的@BeanParam
批注意味着开发人员可以将提交的值聚合到POJO,而不必在其输入资源方法中列出所有值。
BaseStudentDto
类字段先前已用JAX-RS @FormParam
注释和@NotNull
验证注释进行注释(请参见清单3)。 考虑一下REST客户端何时将以下POST提交到/demo/rest/students
资源。
清单6.对/ demo / rest / students URL的POST请求
POST /demo/rest/students HTTP/1.1
Host: localhost:9080
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
firstName=Norm&lastName=Smith&email=nsmith%40nowhere.com&gender=MALE
在这种情况下,容器使用URL编码的表单值来创建和注入BaseStudentDto
实例,并对新对象执行验证(任何字段都不能为null)。 然后,如果通过验证,则使用新的BaseStudentDto
对象启动createFromForm
方法。 如果验证未通过,则意味着REST客户端未提供一个或多个值,则会引发ConstraintViolationException
并导致Server 500错误。
提供增强的序列化
默认情况下,Jackson JSON处理器将java.util.Calendar
对象序列化为数字时间戳(自格林尼治标准时间1970年1月1日00:00:00以来的毫秒数)。 对于此应用程序,我们希望将学生记录的registeredOn
值序列化为符合ISO8601的日期字符串(即YYYY-MM-DD)。
为了控制序列化,SRA包含一个名为CalendarSerializer
的Jackson序列化类,如下面的清单所示。
清单7. CalendarSerializer类
public class CalendarSerializer extends SerializerBase<Calendar> {
protected CalendarSerializer() {
super(Calendar.class, true);
}
@Override
public JsonNode getSchema(SerializerProvider arg0, Type arg1) throws JsonMappingException {
return null;
}
@Override
public void serialize(Calendar cal, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
jgen.writeString(new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime()));
}
}
创建CalendarSerializer
类之后,使用@JsonSerialize(using=CalendarSerializer.class)
注释HatesoasStudentDto#getRegisterdOn
@JsonSerialize(using=CalendarSerializer.class)
方法。 这样,当HateoasStudentDto
对象被序列化时, registeredOn
日期将以YYYY-MM-DD格式作为字符串输出,如下表所示。
清单8.将registeredOn日期输出为字符串
[ {
"id": "0b031e9b-6933-4a10-9ffc-c7fd2c604331",
"firstName": "Aaron",
"lastName": "Aaronson",
"gender": "MALE",
"email": "aaaronson@domain.net",
"registeredOn": "2013-01-28",
"studentNumber": "823-934",
"_links": [
{
"rel": "self",
"href": "http://localhost:9080/demo/rest/students/0b031e9b-6933-4a10-9ffc-c7fd2c604331",
"type": "application/json"
}
]
}, …
{
"id": "864eb556-053a-42d7-a757-e24928fb19a3",
"firstName": "Pamela",
"lastName": "Peterson",
"gender": "FEMALE",
"email": "ppeterson@domain.net",
"registeredOn": "2013-01-17",
"studentNumber": "826-660",
"_links": [
{
"rel": "self",
"href": "http://localhost:9080/demo/rest/students/864eb556-053a-42d7-a757-e24928fb19a3",
"type": "application/json"
}
]
} ]
错误处理
如果提供了无效的JSON Blob或POST表单具有一个值或缺少值,则可能导致服务器错误。 以下各节介绍发生这些错误时会发生什么以及如何处理它们。
无效的JSON
如果将无效的JSON Blob提供给应用程序,则会导致服务器错误(HTTP 500)。 无效的JSON可能由于以下原因而导致:
- JSON格式错误。 例如,它缺少属性名称或值定界符,或者括号不平衡。
- JSON包含在
BaseStudentDto
类中没有相应属性的属性。
例如,如果/demo/rest/students
URL的POST
方法的请求主体为:
{ "bad":"property" }
在这种情况下,服务器将返回以下响应。
清单9.服务器对错误属性的响应
HTTP/1.1 500 Internal Server Error
X-Powered-By: Servlet/3.1
Content-Length: 0
Date: Mon, 26 Sep 2016 18:10:31 GMT
Content-Language: en-US
Connection: Close
WebSphere Application Server中的SystemOut.log文件显示已发出HTTP 500,因为Student类没有名为bad
的属性。
如果应用程序提供了无效的JSON,我们希望服务器返回带有诊断文本的HTTP 400(错误请求)响应。 在这种情况下,您将创建一个新的提供程序类,如下面的清单所示。
清单10. JsonMappingExceptionMapper类
@Provider
public class JsonMappingExceptionMapper implements
ExceptionMapper<JsonMappingException> {
@Override
public Response toResponse( JsonMappingException e ) {
// Get the mapping path at which the object mapper detected the error
List<Reference> references = e.getPath();
// Get the LAST reference from the list
String message = references.get(references.size() - 1).getFieldName();
return Response.status(Status.BAD_REQUEST).type(MediaType.TEXT_PLAIN)
.entity("The supplied JSON contained an invalid property: " + message).build();
}
}
JsonMappingExceptionMapper
类实现JAX-RS ExceptionMapper
接口,该接口具有一种方法toResponse
。 toResponse
方法采用Jackson JsonMappingException
并创建带有纯文本有效负载的HTTP 400响应。
现在,考虑到/demo/rest/students
URL的POST具有以下请求正文:
{ "bad":"property" }
服务器在HTTP 400响应中返回以下文本:
The supplied JSON contained an invalid property: bad
验证错误
如果将一个或多个缺少值的表单过帐到/demo/rest/students
URL,则会导致服务器错误(HTTP 500)。
例如,如果POST到/demo/rest/students
URL的表单缺少firstName
参数,则默认情况下服务器将返回以下响应。
清单11.缺少firstName参数的服务器响应
HTTP/1.1 500 Internal Server Error
X-Powered-By: Servlet/3.1
Content-Type: text/html;charset=ISO-8859-1
$WSEP:
Content-Language: en-US
Content-Length: 405
Connection: Close
Date: Mon, 12 Dec 2016 02:43:22 GMT
Error 500: java.lang.RuntimeException: org.apache.cxf.interceptor.Fault: null while invoking public javax.ws.rs.core.Response com.dw.demo.rest.resources.StudentResource.createFromForm(javax.ws.rs.core.UriInfo,com.dw.demo.rest.dto.BaseStudentDto) with params [org.apache.cxf.jaxrs.impl.UriInfoImpl@8b20c7ca, BaseStudentDto [firstName=null, lastName=Smith, email=nsmith@nowhere.com, gender=MALE]].
如果应用程序提供的表单无效,我们希望服务器返回HTTP 400(错误请求)响应以及一些诊断文本。 为此,我们创建了一个新的提供程序类,如下面的清单所示。
清单12. ValidationExceptionMapper类
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
@Override
public Response toResponse(ValidationException ex) {
if (ex instanceof ConstraintViolationException) {
return handleConstraintViolationException((ConstraintViolationException) ex);
}
return Response.status(Status.BAD_REQUEST).type(MediaType.TEXT_PLAIN).entity("bad: " + unwrapException(ex)).build();
}
private Response handleConstraintViolationException(ConstraintViolationException ex) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<?> v : ex.getConstraintViolations()) {
String lastName = "";
for (Node n : v.getPropertyPath()) {
lastName = n.getName();
}
sb.append("Error: '" + lastName + "' " + v.getMessage() + "\n");
}
return Response.status(Status.BAD_REQUEST)
.type(MediaType.TEXT_PLAIN).entity(sb.toString()).build();
}
protected String unwrapException(Throwable t) {
StringBuffer sb = new StringBuffer();
unwrapException(sb, t);
return sb.toString();
}
private void unwrapException(StringBuffer sb, Throwable t) {
if (t == null) {
return;
}
sb.append(t.getMessage());
if (t.getCause() != null && t != t.getCause()) {
sb.append('[');
unwrapException(sb, t.getCause());
sb.append(']');
}
}
}
ValidationExceptionMapping
类实现JAX-RS ExceptionMapper
接口,并实现toResponse
方法以创建具有纯文本有效载荷的HTTP 400响应,该有效载荷标识未通过验证的表单字段。
现在,考虑到/demo/rest/students
URL的POST具有以下请求。
清单13.对/ demo / rest / students URL的POST请求
POST /demo/rest/students HTTP/1.1
Host: localhost:9080
Content-Type: application/x-www-form-urlencoded
Cache-Control: no-cache
Postman-Token: 6237ff3d-0eab-4e44-3b4f-ea49b3985547
lastName=Smith&email=nsmith%40nowhere.com&gender=MALE
服务器在HTTP 400响应中返回以下文本:
Error: 'firstName' may not be null
结论
本教程说明了如何通过使用WebSphere Application Server的Java EE 7版本的JAX-RS 2.0功能来创建RESTful服务来管理学生注册。 该服务使用自定义序列化程序来确保以JSON表示的学生记录包含正确格式的日期,并且每个学生记录都包含一个兼容HATEOAS的链接,用于访问和操作记录。 自定义异常映射器用于通过HTTP 400错误代码(而不是HTTP 500错误代码)报告无效输入。
翻译自: https://www.ibm.com/developerworks/library/mw-1612-gunderson-trs/index.html