jersey spring
想要在Java中使用REST? 然后您来对地方了,因为在博客文章中,我将向您介绍如何“美丽”地设计REST API,以及如何使用Jersey框架在Java中实现它。 本教程中开发的RESTful API将演示针对存储在MySql数据库中的播客资源的完整的Create__read,_update_and_delete(CRUD)功能。
1.例子
为什么?
在开始之前,让我告诉你为什么写这篇文章–好吧,我的意图是将来为Podcastpedia.org提供REST API。 当然,我可以像现在对AJAX调用那样使用Spring自己的REST实现 ,但是我还想看看“正式”实现的样子。 因此,了解该技术的最佳方法是使用该技术构建原型。 这就是我所做的,也是我在这里介绍的,我可以说我对泽西岛非常满意。 继续阅读以了解原因!!!
注意:您可以使用jQuery和Spring MVC访问我的帖子自动完成搜索框,以了解Spring如何处理REST请求。
它有什么作用?
本教程中管理的资源是播客。 REST API将允许创建,检索,更新和删除此类资源。
建筑与技术
该演示应用程序基于“德米特法则(LoD)或最少知识原理” [16]使用了多层体系结构:
- 第一层是通过Jersey实施的REST支持,具有立面的作用并将逻辑委托给业务层
- 业务层是逻辑发生的地方
- 数据访问层是与持久性存储(在本例中为MySql数据库)进行通信的地方
关于所用技术/框架的几句话:
1.3.1。 泽西岛(门面)
Jersey RESTful Web服务框架是开源,生产质量的,用于在Java中开发RESTful Web服务的框架,该框架提供对JAX-RS API的支持,并充当JAX-RS (JSR 311和JSR 339)参考实现。
1.3.2。 Spring(业务层)
我喜欢将东西与Spring粘在一起,这个示例也不例外。 我认为没有更好的方法来制作具有不同功能的POJO。 您将在本教程中找到将Jersey 2与Spring集成所需的内容。
1.3.3。 JPA 2 / Hibernate(持久层)
对于持久层,我仍然使用DAO模式,尽管要实现它,我使用的是JPA 2,正如某些人所说,它应该使DAO变得多余(我不喜欢EntityManager / JPA专用代码)。 JPA 2的AS支持框架我正在使用Hibernate。
有关Java中的持久性主题的有趣讨论,请参见我的Spring,JPA2和Hibernate的Java持久性示例 。
1.3.4。 网络容器
一切都与Maven打包为.war
文件,并且可以部署在任何Web容器上–我使用Tomcat和Jetty ,但也可以是Glassfih,Weblogic,JBoss或WebSphere。
1.3.5。 MySQL
示例数据存储在MySQL表中:
1.3.6。 技术版本
- 泽西2.9
- Spring4.0.3
- Hibernate4
- Maven的3
- Tomcat7
- 码头9
- MySQL的5.6
注意:帖子中的主要焦点将是REST api设计及其通过Jersey JAX-RS实现的实现,所有其他技术/层均被视为实现因素。
源代码
此处提供的项目的源代码可在GitHub上获得,其中包含有关如何安装和运行项目的完整说明:
- Codingpedia / Demo-rest-jersey-spring
2.配置
在开始介绍REST API的设计和实现之前,我们需要做一些配置,以便所有这些奇妙的技术可以一起发挥作用
项目依赖
Jersey Spring扩展名必须出现在项目的类路径中。 如果使用的是Maven,请将其添加到项目的pom.xml
文件中:
pom.xml中的Jersey-spring依赖项
<dependency><groupId>org.glassfish.jersey.ext</groupId><artifactId>jersey-spring3</artifactId><version>${jersey.version}</version><exclusions><exclusion><groupId>org.springframework</groupId><artifactId>spring-core</artifactId></exclusion> <exclusion><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></exclusion><exclusion><groupId>org.springframework</groupId><artifactId>spring-beans</artifactId></exclusion></exclusions>
</dependency>
<dependency><groupId>org.glassfish.jersey.media</groupId><artifactId>jersey-media-json-jackson</artifactId><version>2.4.1</version>
</dependency>
注意: jersey-spring3.jar为Spring库使用其自己的版本,因此要使用所需的库(在本例中为Spring 4.0.3,请释放),您需要手动排除这些库。
代码警报:如果您想查看项目中还需要哪些其他依赖项(例如Spring,Hibernate,Jetty maven插件,测试等),则可以查看GitHub上完整的pom.xml文件。
web.xml
Web应用程序部署描述符
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"><display-name>Demo - Restful Web Application</display-name><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring/applicationContext.xml</param-value></context-param><servlet><servlet-name>jersey-serlvet</servlet-name><servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class><init-param><param-name>javax.ws.rs.Application</param-name><param-value>org.codingpedia.demo.rest.RestDemoJaxRsApplication</param-value> </init-param> <load-on-startup>1</load-on-startup></servlet><servlet-mapping><servlet-name>jersey-serlvet</servlet-name><url-pattern>/*</url-pattern></servlet-mapping><resource-ref><description>Database resource rest demo web application </description><res-ref-name>jdbc/restDemoDB</res-ref-name><res-type>javax.sql.DataSource</res-type><res-auth>Container</res-auth></resource-ref>
</web-app>
泽西小服务器
注意Jersey Servlet配置[行18-33]。 javax.ws.rs.core.Application
类定义JAX-RS应用程序的组件(根资源和提供程序类)。 我使用了ResourceConfig,
它是Jersey类Application
自己的实现,并且提供了高级功能来简化JAX-RS组件的注册。 请查阅文档中的JAX-RS应用程序模型 ,以了解更多可能性。
我对ResourceConfig
类的实现org.codingpedia.demo.rest.RestDemoJaxRsApplication ,
注册了应用程序资源,过滤器,异常映射器和功能:
org.codingpedia.demo.rest.service.MyDemoApplication
package org.codingpedia.demo.rest.service;//imports omitted for brevity /*** Registers the components to be used by the JAX-RS application* * @author ama* */
public class RestDemoJaxRsApplication extends ResourceConfig {/*** Register JAX-RS application components.*/public RestDemoJaxRsApplication() {// register application resourcesregister(PodcastResource.class);register(PodcastLegacyResource.class);// register filtersregister(RequestContextFilter.class);register(LoggingResponseFilter.class);register(CORSResponseFilter.class);// register exception mappersregister(GenericExceptionMapper.class);register(AppExceptionMapper.class);register(NotFoundExceptionMapper.class);// register featuresregister(JacksonFeature.class);register(MultiPartFeature.class);}
}
请注意:
-
org.glassfish.jersey.server.spring.scope.RequestContextFilter
,它是一个Spring过滤器,提供了JAX-RS和Spring请求属性之间的桥梁 -
org.codingpedia.demo.rest.resource.PodcastsResource
,这是一个“外观”组件,它通过注释公开了REST API,并将在稍后的文章中进行详尽介绍。 -
org.glassfish.jersey.jackson.JacksonFeature
,它是注册Jackson JSON提供程序的功能-应用程序需要它才能理解JSON数据
2.1.2.2。 Spring应用程序上下文配置
Spring应用程序上下文配置位于spring/applicationContext.xml
下的类路径中:
Spring应用程序上下文配置
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="org.codingpedia.demo.rest.*" /><!-- ************ JPA configuration *********** --><tx:annotation-driven transaction-manager="transactionManager" /> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"><property name="entityManagerFactory" ref="entityManagerFactory" /></bean><bean id="transactionManagerLegacy" class="org.springframework.orm.jpa.JpaTransactionManager"><property name="entityManagerFactory" ref="entityManagerFactoryLegacy" /></bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"><property name="persistenceXmlLocation" value="classpath:config/persistence-demo.xml" /><property name="persistenceUnitName" value="demoRestPersistence" /> <property name="dataSource" ref="restDemoDS" /><property name="packagesToScan" value="org.codingpedia.demo.*" /><property name="jpaVendorAdapter"><bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"><property name="showSql" value="true" /><property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" /></bean></property></bean> <bean id="entityManagerFactoryLegacy" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"><property name="persistenceXmlLocation" value="classpath:config/persistence-demo.xml" /><property name="persistenceUnitName" value="demoRestPersistenceLegacy" /><property name="dataSource" ref="restDemoLegacyDS" /><property name="packagesToScan" value="org.codingpedia.demo.*" /><property name="jpaVendorAdapter"><bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"><property name="showSql" value="true" /><property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect" /></bean></property></bean> <bean id="podcastDao" class="org.codingpedia.demo.rest.dao.PodcastDaoJPA2Impl"/> <bean id="podcastService" class="org.codingpedia.demo.rest.service.PodcastServiceDbAccessImpl" /> <bean id="podcastsResource" class="org.codingpedia.demo.rest.resource.PodcastsResource" /><bean id="podcastLegacyResource" class="org.codingpedia.demo.rest.resource.PodcastLegacyResource" /><bean id="restDemoDS" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton"><property name="jndiName" value="java:comp/env/jdbc/restDemoDB" /><property name="resourceRef" value="true" /> </bean><bean id="restDemoLegacyDS" class="org.springframework.jndi.JndiObjectFactoryBean" scope="singleton"><property name="jndiName" value="java:comp/env/jdbc/restDemoLegacyDB" /><property name="resourceRef" value="true" /> </bean>
</beans>
这里没什么特别的,它只是定义了整个演示应用程序所需的bean(例如podcastsResource
,这是我们REST API的入口点类)。
3. REST API(设计和实现)
资源资源
3.1.1。 设计
如前所述,演示应用程序管理播客,播客代表了我们REST API中的资源 。 资源是REST中的中心概念,其特点是有两点:
- 每个引用都有一个全局标识符(例如HTTP中的URI )。
- 具有一个或多个表示形式,它们公开给外部世界并且可以使用(在此示例中,我们将主要使用JSON表示形式)进行操作
在REST中,资源通常用名词(播客,客户,用户,帐户等)而不是动词(getPodcast,deleteUser等)表示。
本教程中使用的端点是:
-
/podcasts
– (注意复数) URI标识表示播客集合的资源 -
/podcasts/{id}
–通过播客ID标识播客资源的URI
3.1.2。 实作
为了简单起见,播客将仅具有以下属性:
-
id
–唯一标识播客 -
feed
-播客的URL饲料 -
title
–播客标题 -
linkOnPodcastpedia
–您可以在Podcastpedia.org上找到播客 -
description
-播客的简短描述
我本可以只使用一个Java类来表示代码中的播客资源,但是在那种情况下,该类及其属性/方法可能会因JPA和XML / JAXB / JSON注释而变得混乱不堪。 我想避免这种情况,而是使用了两个具有几乎相同属性的表示形式:
- PodcastEntity.java –在DB和业务层中使用的JPA注释类
- Podcast.java –在正面和业务层中使用的带有 JAXB / JSON注释的类
注意:我仍在努力使自己相信这是更好的方法,因此,如果对此有任何建议,请发表评论。
Podcast.java类如下所示:
播客
package org.codingpedia.demo.rest.resource;//imports omitted for brevity/*** Podcast resource placeholder for json/xml representation * * @author ama**/
@SuppressWarnings("restriction")
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Podcast implements Serializable {private static final long serialVersionUID = -8039686696076337053L;/** id of the podcast */@XmlElement(name = "id") private Long id;/** title of the podcast */@XmlElement(name = "title") private String title;/** link of the podcast on Podcastpedia.org */@XmlElement(name = "linkOnPodcastpedia") private String linkOnPodcastpedia;/** url of the feed */@XmlElement(name = "feed") private String feed;/** description of the podcast */@XmlElement(name = "description")private String description; /** insertion date in the database */@XmlElement(name = "insertionDate")@XmlJavaTypeAdapter(DateISO8601Adapter.class) @PodcastDetailedViewprivate Date insertionDate;public Podcast(PodcastEntity podcastEntity){try {BeanUtils.copyProperties(this, podcastEntity);} catch (IllegalAccessException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (InvocationTargetException e) {// TODO Auto-generated catch blocke.printStackTrace();}}public Podcast(String title, String linkOnPodcastpedia, String feed,String description) {this.title = title;this.linkOnPodcastpedia = linkOnPodcastpedia;this.feed = feed;this.description = description;}public Podcast(){}//getters and setters now shown for brevity
}
并转换为以下JSON表示形式,它实际上是当今与REST一起使用的事实上的媒体类型:
{"id":1,"title":"Quarks & Co - zum Mitnehmen-modified","linkOnPodcastpedia":"http://www.podcastpedia.org/podcasts/1/Quarks-Co-zum-Mitnehmen","feed":"http://podcast.wdr.de/quarks.xml","description":"Quarks & Co: Das Wissenschaftsmagazin","insertionDate":"2014-05-30T10:26:12.00+0200"
}
即使JSON越来越成为REST API中的首选表示形式,您也不应忽略XML表示形式,因为大多数系统仍使用XML格式与其他方进行通信。
好消息是,在泽西岛,您可以一枪杀死两只兔子–使用JAXB bean(如上所述),您将能够使用相同的Java模型生成JSON和XML表示形式。 另一个优势是使用这种模型的简单性以及Java SE Platform中API的可用性。
注意:本教程中定义的大多数方法都将产生和使用application / xml媒体类型,其中application / json是首选方式。
方法
在向您介绍API之前,让我告诉您
- 创建= POST
- 读取=获取
- 更新= PUT
- 删除=删除
并且不是严格的1:1映射。 为什么? 因为您还可以将PUT用于创建,将POST用于更新。 在接下来的段落中将对此进行解释和演示。
注意:对于“读取和删除”非常清楚,它们确实使用GET和DELETE HTTP操作一对一映射。 无论如何,REST是一种体系结构风格,而不是一种规范,您应该使体系结构适应您的需求,但是如果您想公开自己的API并希望有人使用它,则应该遵循一些“最佳实践”。
如前所述, PodcastRestResource
类是处理所有其余请求的类:
package org.codingpedia.demo.rest.resource;
//imports
......................
@Component
@Path("/podcasts")
public class PodcastResource {@Autowiredprivate PodcastService podcastService;.....................
}
注意类定义之前的@Path("/podcasts")
–与播客资源相关的所有内容都将在此路径下发生。 @Path批注的值是相对URI路径。 在上面的示例中,Java类将托管在URI路径/podcasts
。 PodcastService
接口将业务逻辑公开给REST外观层。
代码警报:您可以在GitHub – PodcastResource.java上找到该类的全部内容。 我们将逐步介绍该文件,并说明与不同操作对应的不同方法。
3.2.1。 创建播客
3.2.1.1。 设计
尽管资源创建的“最著名”方法是使用POST,但是如前所述,要创建新资源,我可以同时使用POST和PUT方法,而我做到了:
描述 | URI | HTTP方法 | HTTP状态响应 |
添加新的播客 | /播客/ | 开机自检 | 创建了201 |
添加新的播客(必须发送所有值) | / podcasts / {id} | 放 | 创建了201 |
使用POST(非幂等)之间的最大区别
“ POST方法用于请求源服务器接受请求中包含的实体作为请求行中由Request-URI标识的资源的新下级[…]如果在源服务器上创建了资源,响应应为201(已创建),并包含描述请求状态并引用新资源的实体以及位置标头” [1]
和PUT(幂等)
“ PUT方法请求将封闭的实体存储在提供的Request-URI […]下,如果Request-URI没有指向现有资源,并且请求用户代理能够将该URI定义为新资源,原始服务器可以使用该URI创建资源。 如果创建了新资源,则原始服务器务必通过201(已创建)响应通知用户代理。” [1]
是对于PUT,您应该事先知道将在其中创建资源的位置,并发送该条目的所有可能值。
3.2.1.2。 实作
3.2.1.2.1。 使用POST创建单个资源
从JSON创建单个播客资源
/*** Adds a new resource (podcast) from the given json format (at least title* and feed elements are required at the DB level)* * @param podcast* @return* @throws AppException*/
@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response createPodcast(Podcast podcast) throws AppException {Long createPodcastId = podcastService.createPodcast(podcast);return Response.status(Response.Status.CREATED)// 201.entity("A new podcast has been created").header("Location","http://localhost:8888/demo-rest-jersey-spring/podcasts/"+ String.valueOf(createPodcastId)).build();
}
注解
-
–表示该方法响应HTTP POST请求@POST
-
@Consumes({MediaType.APPLICATION_JSON})
–定义方法接受的媒体类型,在这种情况下为"application/json"
-
@Produces({MediaType.TEXT_HTML})
–定义方法可以产生的媒体类型,在这种情况下为"text/html"
。
响应
- 成功:文本/ html文档,HTTP状态为
201 Created
,位置标头指定在何处创建资源 - 错误时:
400 Bad request
如果没有提供足够的数据
3.2.1.2.2。 使用PUT创建单个资源(“播客”)
这将在下面的“更新播客”部分中进行处理。
3.2.1.2.3。 奖励–从表单创建单个资源(“播客”)
从表单创建单个播客资源
/*** Adds a new podcast (resource) from "form" (at least title and feed* elements are required at the DB level)* * @param title* @param linkOnPodcastpedia* @param feed* @param description* @return* @throws AppException*/
@POST
@Consumes({ MediaType.APPLICATION_FORM_URLENCODED })
@Produces({ MediaType.TEXT_HTML })
@Transactional
public Response createPodcastFromApplicationFormURLencoded(@FormParam("title") String title,@FormParam("linkOnPodcastpedia") String linkOnPodcastpedia,@FormParam("feed") String feed,@FormParam("description") String description) throws AppException {Podcast podcast = new Podcast(title, linkOnPodcastpedia, feed,description);Long createPodcastid = podcastService.createPodcast(podcast);return Response.status(Response.Status.CREATED)// 201.entity("A new podcast/resource has been created at /demo-rest-jersey-spring/podcasts/"+ createPodcastid).header("Location","http://localhost:8888/demo-rest-jersey-spring/podcasts/"+ String.valueOf(createPodcastid)).build();
}
注解
-
@POST
–表示该方法响应HTTP POST请求 -
@Consumes({MediaType.APPLICATION_FORM_URLENCODED})
–定义该方法接受的媒体类型,在这种情况下为"application/x-www-form-urlencoded"
-
@FormParam
–在方法的输入参数之前,此注释将请求实体主体中包含的表单参数的值绑定到资源方法参数。 除非使用“Encoded
注释”将其禁用,否则将对值进行URL解码
-
-
-
@Produces({MediaType.TEXT_HTML})
–定义该方法可以产生的媒体类型,在这种情况下为“ text / html”。 响应将是状态为201的html文档,它向调用方指示请求已得到满足并导致创建了新资源。
响应
- 成功:文本/ html文档,HTTP状态为
201 Created
,位置标头指定在何处创建资源 - 错误时:
400 Bad request
如果没有提供足够的数据
3.2.2。 阅读播客
3.2.2.1。 设计
该API支持两种读取操作:
- 返回播客的集合
- 返回由ID标识的播客
描述 | URI | HTTP方法 | HTTP状态响应 |
返回所有播客 | / podcasts /?orderByInsertionDate = {ASC | DESC}&numberDaysToLookBack = {val} | 得到 | 200 OK |
添加新的播客(必须发送所有值) | / podcasts / {id} | 得到 | 200 OK |
注意收集资源的查询参数– orderByInsertionDate和numberDaysToLookBack。 将过滤器添加为URI中的查询参数而不是路径的一部分是很有意义的。
3.2.2.2。 实作
3.2.2.2.1。 阅读所有播客(“ /”)
阅读所有资源
/*** Returns all resources (podcasts) from the database* * @return* @throws IOException* @throws JsonMappingException* @throws JsonGenerationException* @throws AppException*/
@GET
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public List<Podcast> getPodcasts(@QueryParam("orderByInsertionDate") String orderByInsertionDate,@QueryParam("numberDaysToLookBack") Integer numberDaysToLookBack)throws JsonGenerationException, JsonMappingException, IOException,AppException {List<Podcast> podcasts = podcastService.getPodcasts(orderByInsertionDate, numberDaysToLookBack);return podcasts;
}
注解
-
@GET
–表示该方法响应HTTP GET请求 -
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
–定义该方法可以产生的媒体类型),在这种情况下为"application/json"
或"application/xml"
(您需要在@XmlRootElement
前面Podcast
类)。 响应将是JSON或XML格式的播客列表。
响应
- 数据库中的播客列表和HTTP状态200 OK
3.2.2.2.1。 阅读一个播客
按ID读取一种资源
@GET
@Path("{id}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public Response getPodcastById(@PathParam("id") Long id)throws JsonGenerationException, JsonMappingException, IOException,AppException {Podcast podcastById = podcastService.getPodcastById(id);return Response.status(200).entity(podcastById).header("Access-Control-Allow-Headers", "X-extra-header").allow("OPTIONS").build();
}
注解
-
@GET
–表示该方法响应HTTP GET请求 -
@Path("{id}")
–标识类方法将为其请求服务的URI路径。 “ id”值是构成URI路径模板的嵌入式变量。 它与@PathParam
变量结合使用。@PathParam("id")
–将URI模板参数(“ id”)的值绑定到资源方法参数。
-
@Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
–定义方法可以产生的媒体类型,在这种情况下为"application/json"
或"application/xml"
(您需要在Podcast前面的@XmlRootElement
课)。
响应
- 成功:请求的播客的HTTP状态为
200 OK
。 格式为xml或JSON,具体取决于客户端发送的Accept-header值(可能押于application / xml或application / json) - 错误:
404 Not found
如果数据库中不存在具有给定ID的播客,则找不到
3.2.3。 更新播客
3.2.3.1。 设计
描述 | URI | HTTP方法 | HTTP状态响应 |
更新播客( 完整 ) | / podcasts / {id} | 放 | 200 OK |
更新播客( 部分 ) | / podcasts / {id} | 开机自检 | 200 OK |
在REST领域,您将进行两种更新:
- 全面更新–您将在其中提供所有
- 部分更新–当仅一些属性通过网络发送以进行更新时
要进行完整的更新,很明显可以使用PUT方法,并且符合RFC 2616中该方法的规范。
现在,对于部分更新,有很多关于使用什么的建议/辩论:
- 通过PUT
- 通过POST
- 通过PATCH
让我告诉我为什么我认为第一种选择(使用PUT)是行不通的。 好吧,根据规格
“如果请求URI引用了已经存在的资源,则应将封闭的实体视为驻留在源服务器上的实体的修改版本。” [1]
如果我只想更新ID为2的播客的title属性
PUT命令进行部分更新
PUT http://localhost:8888/demo-rest-jersey-spring/podcasts/2 HTTP/1.1
Accept-Encoding: gzip,deflate
Content-Type: application/json
Content-Length: 155
Host: localhost:8888
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.1.1 (java 1.5){"title":"New Title"
}
然后,根据规范,“存储”在该位置的资源应仅具有ID和标题,显然我的意图不是那样。
通过POST的第二个选项……好吧,我们可以“滥用”此选项 ,这恰恰是我在实现中所做的,但是它似乎与我不符,因为POST的规范指出:
“发布的实体从属于该URI,就像文件从属于包含它的目录,新闻文章从属于发布它的新闻组或记录从属于数据库一样。” [1 ]
在我看来,这似乎不是部分更新案例……
第三种选择是使用PATCH,我想这是该方法成功的主要原因:
“几个扩展超文本传输协议(HTTP)的应用程序
需要功能来进行部分资源修改。 现有的
HTTP PUT方法仅允许完全替换文档。 该提案添加了新的HTTP方法PATCH,以修改现有的 HTTP资源。” [2]
我很确定将来会使用它进行部分更新,但是由于它还不是规范的一部分,并且尚未在Jersey中实现,因此我选择在演示中使用POST的第二个选项。 如果您真的想用PATCH在Java中实现部分更新,请查看这篇文章– JAX-RS 2.0中的透明PATCH支持
3.2.3.1。 实作
3.2.3.1.1。 完整更新
创建或完全更新资源实现方法
@PUT
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response putPodcastById(@PathParam("id") Long id, Podcast podcast)throws AppException {Podcast podcastById = podcastService.verifyPodcastExistenceById(id);if (podcastById == null) {// resource not existent yet, and should be created under the// specified URILong createPodcastId = podcastService.createPodcast(podcast);return Response.status(Response.Status.CREATED)// 201.entity("A new podcast has been created AT THE LOCATION you specified").header("Location","http://localhost:8888/demo-rest-jersey-spring/podcasts/"+ String.valueOf(createPodcastId)).build();} else {// resource is existent and a full update should occurpodcastService.updateFullyPodcast(podcast);return Response.status(Response.Status.OK)// 200.entity("The podcast you specified has been fully updated created AT THE LOCATION you specified").header("Location","http://localhost:8888/demo-rest-jersey-spring/podcasts/"+ String.valueOf(id)).build();}
}
注解
-
@PUT
–表示该方法响应HTTP PUT请求 -
@Path("{id}")
–标识类方法将为其请求服务的URI路径。 “ id”值是构成URI路径模板的嵌入式变量。 它与@PathParam
变量结合使用。@PathParam("id")
–将URI模板参数(“ id”)的值绑定到资源方法参数。
-
@Consumes({MediaType.APPLICATION_JSON})
–定义方法接受的媒体类型,在这种情况下为"application/json"
-
@Produces({MediaType.TEXT_HTML})
–定义方法可以产生的媒体类型,在这种情况下为“ text / html”。
将是一个html文档,其中包含不同的消息和状态,具体取决于已采取的措施
回应
- 在创作时
- 成功时:
201 Created
并且在Location标头中指定了创建资源的指定位置 - 错误时:
400 Bad request
如果未提供用于插入的最低要求的属性)
- 成功时:
- 完整更新
- 成功:
200 OK
- 成功:
3.2.3.1.2。 部分更新
部分更新
//PARTIAL update
@POST
@Path("{id}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.TEXT_HTML })
public Response partialUpdatePodcast(@PathParam("id") Long id, Podcast podcast) throws AppException {podcast.setId(id);podcastService.updatePartiallyPodcast(podcast);return Response.status(Response.Status.OK)// 200.entity("The podcast you specified has been successfully updated").build();
}
注解
-
–表示该方法响应HTTP POST请求@POST
-
@Path("{id}")
–标识类方法将为其请求服务的URI路径。 “ id”值是构成URI路径模板的嵌入式变量。 它与@PathParam
变量结合使用。@PathParam("id")
–将URI模板参数(“ id”)的值绑定到资源方法参数。
-
@Consumes({MediaType.APPLICATION_JSON})
–定义方法接受的媒体类型,在这种情况下为"application/json"
-
@Produces({MediaType.TEXT_HTML})
–定义方法可以产生的媒体类型,在这种情况下为"text/html"
。
响应
- 成功:
200 OK
- 错误:
404 Not Found
,如果在提供的位置没有资源可用
3.2.4。 删除播客
3.2.4.1。 设计
描述 | URI | HTTP方法 | HTTP状态响应 |
删除所有播客 | /播客/ | 删除 | 204没有内容 |
删除指定位置的播客 | / podcasts / {id} | 删除 | 204没有内容 |
3.2.4.2。 实作
3.2.4.2.1。 删除所有资源
删除所有资源
@DELETE
@Produces({ MediaType.TEXT_HTML })
public Response deletePodcasts() {podcastService.deletePodcasts();return Response.status(Response.Status.NO_CONTENT)// 204.entity("All podcasts have been successfully removed").build();
}
注解
-
@DELETE
–表示该方法响应HTTP DELETE请求 -
@Produces({MediaType.TEXT_HTML})
–定义该方法可以产生的媒体类型,在这种情况下为“ text / html”。
响应
- 响应将是一个html文档,状态为204 No content,向调用方指示请求已完成。
3.2.4.2.2。 删除一项资源
删除一项资源
@DELETE
@Path("{id}")
@Produces({ MediaType.TEXT_HTML })
public Response deletePodcastById(@PathParam("id") Long id) {podcastService.deletePodcastById(id);return Response.status(Response.Status.NO_CONTENT)// 204.entity("Podcast successfully removed from database").build();
}
注解
-
@DELETE
–表示该方法响应HTTP DELETE请求 -
@Path("{id}")
–标识类方法将为其请求服务的URI路径。 “ id”值是构成URI路径模板的嵌入式变量。 它与@PathParam
变量结合使用。@PathParam("id")
–将URI模板参数(“ id”)的值绑定到资源方法参数。
-
@Produces({MediaType.TEXT_HTML})
–定义该方法可以产生的媒体类型,在这种情况下为“ text / html”。
响应
- 成功时:如果删除播客,则返回
204 No Content
成功状态 - 出现错误:播客不再可用,并且返回
404 Not found
状态
4.记录
当日志记录级别设置为DEBUG时,将记录每个请求的路径和响应的实体。 在Jetty过滤器的帮助下,它像包装器一样具有AOP样式的功能。
有关此问题的更多详细信息,请参见我的文章“ 如何使用SLF4J和Logback登录Spring” 。
5.异常处理
如果出现错误,我决定使用统一的错误消息结构进行响应。 这是一个错误响应的示例:
示例–错误消息响应
{"status": 400,"code": 400,"message": "Provided data not sufficient for insertion","link": "http://www.codingpedia.org/ama/tutorial-rest-api-design-and-implementation-with-jersey-and-spring","developerMessage": "Please verify that the feed is properly generated/set"
}
注意:请继续关注,因为以下文章将提供有关使用Jersey的REST中的错误处理的更多详细信息。
6.在服务器端添加CORS支持
我扩展了为教程开发的API的功能,以支持服务器端的跨源资源共享(CORS) 。
有关此问题的更多详细信息,请参阅我的文章“ 如何使用Jersey向Java的服务器端添加CORS支持” 。
7.测试
Java集成测试
为了测试该应用程序,我将使用Jersey Client
并针对正在运行的Jetty服务器和部署了该应用程序的服务器执行请求。 为此,我将使用Maven故障安全插件 。
7.1.1。 组态
7.1.1.1 Jersey客户端依赖性
要构建Jersey客户端,必须在类路径中提供jersey-client
jar。 使用Maven,您可以将其作为依赖项添加到pom.xml
文件中:
Jersey Client Maven依赖项
<dependency><groupId>org.glassfish.jersey.core</groupId><artifactId>jersey-client</artifactId><version>${jersey.version}</version><scope>test</scope>
</dependency>
7.1.1.2。 故障安全插件
Failsafe插件用于构建生命周期的集成测试和验证阶段,以执行应用程序的集成测试。 Failsafe插件在集成测试阶段不会使构建失败,从而使集成后测试阶段能够执行。
要使用故障安全插件,您需要将以下配置添加到pom.xml
Maven故障安全插件配置
<plugins>[...]<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-failsafe-plugin</artifactId><version>2.16</version><executions><execution><id>integration-test</id><goals><goal>integration-test</goal></goals></execution><execution><id>verify</id><goals><goal>verify</goal></goals></execution></executions></plugin>[...]
</plugins>
7.1.1.2。 Jetty Maven插件
集成测试将针对正在运行的码头服务器执行,该服务器仅在执行测试时启动。 为此,您必须在jetty-maven-plugin
配置以下执行:
用于集成测试的Jetty Maven插件配置
<plugins><plugin><groupId>org.eclipse.jetty</groupId><artifactId>jetty-maven-plugin</artifactId><version>${jetty.version}</version><configuration><jettyConfig>${project.basedir}/src/main/resources/config/jetty9.xml</jettyConfig><stopKey>STOP</stopKey><stopPort>9999</stopPort><stopWait>5</stopWait><scanIntervalSeconds>5</scanIntervalSeconds>[...]</configuration><executions><execution><id>start-jetty</id><phase>pre-integration-test</phase><goals><!-- stop any previous instance to free up the port --><goal>stop</goal> <goal>run-exploded</goal></goals><configuration><scanIntervalSeconds>0</scanIntervalSeconds><daemon>true</daemon></configuration></execution><execution><id>stop-jetty</id><phase>post-integration-test</phase><goals><goal>stop</goal></goals></execution></executions></plugin>[...]
</plugins>
注意:在pre-integration-test
阶段,在停止任何正在运行的实例以释放端口之后,将启动Jetty服务器,在post-integration-phase
,它将停止。 必须将scanIntervalSeconds
设置为0,并将daemon
为true。
代码警报:在GitHub上找到完整的pom.xml文件
7.1.2。 建立整合测试
我正在使用JUnit作为测试框架。 默认情况下,故障安全插件将自动包含具有以下通配符模式的所有测试类:
-
–包括其所有子目录以及以“ IT”开头的所有Java文件名。"**/IT*.java"
-
–包括其所有子目录以及所有以“ IT”结尾的java文件名。"**/*IT.java"
-
–包括其所有子目录以及所有以“ ITCase”结尾的java文件名。"**/*ITCase.java"
我已经创建了一个测试类RestDemoServiceIT
,它将测试读取(GET)方法,但是所有其他过程均应相同:
public class RestDemoServiceIT {[....]@Testpublic void testGetPodcast() throws JsonGenerationException,JsonMappingException, IOException {ClientConfig clientConfig = new ClientConfig();clientConfig.register(JacksonFeature.class);Client client = ClientBuilder.newClient(clientConfig);WebTarget webTarget = client.target("http://localhost:8888/demo-rest-jersey-spring/podcasts/2");Builder request = webTarget.request(MediaType.APPLICATION_JSON);Response response = request.get();Assert.assertTrue(response.getStatus() == 200);Podcast podcast = response.readEntity(Podcast.class);ObjectMapper mapper = new ObjectMapper();System.out.print("Received podcast from database *************************** "+ mapper.writerWithDefaultPrettyPrinter().writeValueAsString(podcast));}
}
注意:
- 我也必须为客户端注册JacksonFeature,以便可以以JSON格式编组播客响应– response.readEntity(Podcast.class)
- 我正在针对端口8888上正在运行的Jetty进行测试–下一节将向您展示如何在所需的端口上启动Jetty
- 我希望我的要求为200
- 借助帮助
org.codehaus.jackson.map.ObjectMapper
我正在以相当格式显示JSON响应
7.1.3。 运行集成测试
可以通过调用构建生命周期的
阶段来调用故障安全插件。 verify
Maven命令调用集成测试
mvn verify
要在端口8888上启动码头,您需要将jetty.port
属性设置为8888。在Eclipse中,我使用以下配置:
与SoapUI的集成测试
最近,在大量使用SoapUI测试基于SOAP的Web服务之后,我重新发现了SoapUI 。 使用最新版本(在撰写本文时,最新版本是5.0.0),它提供了很好的功能来测试基于REST的Web服务,并且以后的版本应该对此进行改进。 因此,除非您开发自己的框架/基础结构来测试REST服务,否则为什么不尝试使用SoapUI。 我做到了,到目前为止,我对结果感到满意,因此我决定制作一个视频教程,现在您可以在我们的频道的YouTube上找到该视频教程:
8.版本控制
有三种主要可能性
- 网址 :“ / v1 / podcasts / {id}”
- 接受/内容类型标头 :application / json; 版本= 1
因为我是开发人员而不是RESTafarian ,所以我会选择URL选项。 在本例中,我在实现方面要做的就是将PodcastResource
类上的@Path
的值批注从修改为
路径中的版本控制
@Component
@Path("/v1/podcasts")
public class PodcastResource {...}
当然,在生产应用程序上,您不希望每个资源类都以版本号为前缀,而是希望以某种方式通过AOP过滤器对版本进行处理。 也许这样的事情会出现在下面的帖子中……
以下是一些对事情有更好理解的人们的宝贵资源:
- [视频] REST + JSON API设计–开发人员最佳实践
- 您的API版本错误,这就是为什么我决定通过@troyhunt用3种不同的错误方式进行操作
- REST服务版本控制
- API版本控制的最佳做法? –关于Stackoverflow的有趣讨论
9.总结
好,就是这样。 如果您走了这么远,我必须向您表示祝贺,但是我希望您可以从本教程中了解有关REST的知识,例如设计REST API,用Java实现REST API,测试REST API等。 如果您愿意,我将不胜感激,如果您通过发表评论或在Twitter,Google +或Facebook上分享评论来帮助其传播。 谢谢! 不要忘记也查看Podcastpedia.org ,您肯定会找到有趣的Podcast和情节。 感谢您的支持。
如果您喜欢这篇文章,我们将非常感谢您为我们的工作做出的一点贡献! 立即向Paypal捐款。
10.资源
源代码
- GitHub – Codingpedia / demo-rest-jersey-spring (有关如何安装和运行项目的说明)
网络资源
- HTTP –超文本传输协议— HTTP / 1.1 – RFC2616
- rfc5789 – HTTP的PATCH方法
- 泽西岛用户指南
- HTTP状态码定义
- REST – http://en.wikipedia.org/wiki/Representational_State_Transfer
- CRUD – http://en.wikipedia.org/wiki/Create,_read,_update_and_delete
- RESTful服务的Java API(JAX-RS)
- 泽西岛-Java中的RESTful Web服务
- HTTP PUT,PATCH或POST –是部分更新还是完全替换?
- JAX-RS 2.0中的透明PATCH支持
- Maven故障安全插件
- Maven故障安全插件用法
- SoapUI 5.0今天发布了!
- SoapUI –使用脚本断言
- [视频] REST + JSON API设计–开发人员最佳实践
- [视频] RESTful API设计–第二版
- 得墨meter耳定律
Codingpedia相关资源
- 带有Spring,JPA2和Hibernate的Java持久性示例
- http://www.codingpedia.org/ama/spring-mybatis-integration-example/
- http://www.codingpedia.org/ama/tomcat-jdbc-connection-pool-configuration-for-production-and-development/
- http://www.codingpedia.org/ama/error-when-executing-jettyrun-with-jetty-maven-plugin-version-9-java-lang-unsupportedclassversionerror-unsupported-major-minor-version-51-0/
- http://www.codingpedia.org/ama/autocomplete-search-box-with-jquery-and-spring-mvc/
翻译自: https://www.javacodegeeks.com/2014/08/tutorial-rest-api-design-and-implementation-in-java-with-jersey-and-spring.html
jersey spring