使用WebSphere Application Server构建简单的,兼容JAX-RS 2.0的REST服务

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对象(PO​​JO)实体提供程序支持,由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对象

上一个清单中显示的方法可以接受或返回BaseStudentDtoHateoasStudentDto类的实例(或实例列表)。 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#getByIdStudentResource#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接口,该接口具有一种方法toResponsetoResponse方法采用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&#40;javax.ws.rs.core.UriInfo,com.dw.demo.rest.dto.BaseStudentDto&#41; 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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值