本文是《轻量级 Java Web 框架架构设计》的系列博文。 html
前几天咱们已基本实现 Smart WebService 插件,该插件可无缝集成到 Smart Framework 中,可发布基于 SOAP 的 WebService。 java
目前咱们已经自定义了一个 @WebService 注解,直接将其配置在某个接口上,即可将该接口发布为 WebService,无需再作任何的配置。 git
这一切彷佛都那么的简单而优雅,但又彷佛缺乏了一点什么? apache
没错!只能发布基于 SOAP 的 WebService,却不能发布基于 REST 的 WebService(如下简称“REST 服务”)。这确实有些遗憾! 编程
本文即将揭晓如何发布并调用 REST 服务,请您继续往下阅读。 json
第一步:在 Maven 中添加相关依赖包 浏览器
咱们选择了 CXF,看来是明智的,由于它不只仅能够提供 SOAP 支持,同时还提供了 REST 支持,并且它的功能远远不止这些。 架构
... <dependency> <groupId>org.apache.cxf</groupId> <artifactId>cxf-rt-frontend-jaxrs</artifactId> <version>2.7.7</version> </dependency> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-jaxrs</artifactId> <version>1.9.13</version> </dependency> ...
注意,要使用 CXF 的 cxf-rt-frontend-jaxrs,而在 SOAP 中,咱们使用的是 cxf-rt-frontend-jaxws,一个是 jaxws,另外一个是 jaxrs,一个字母只差,差别却千千万。 oracle
这还要依赖一个 Jackson 的 jackson-jaxrs 包,它是干吗的?别着急,立刻您就知道了。 app
第二步:扩展 @WebService 注解
还记得以前我提到过,为何要自定义一个 @WebService 注解吗?为何不用 JDK 给咱们提供的 javax.jws.WebService 呢?
其实就是为了干今天这件大事 —— 实现 REST 服务。
现将 @WebService 注解作以下扩展:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface WebService { String value() default ""; Type type() default Type.SOAP; public enum Type { SOAP, REST } }
增长了一个 type 属性,默认值是 SOAP,这里用到了 Java 枚举,方便咱们定义不一样类型的 WebService(其实目前主流也就这两种:SOAP 与 REST)。
第三步:封装 CXF API
仍是用之前的套路,将 CXF API 作一个封装。还记得上次编写了一个 WebServiceHelper 吗?它能够发布 WebService 并获取 WebService 客户端。若是将 REST 服务的发布与调用也一并加入该类中,或许不是最好的选择,倒不如将该类重命名为 SOAPHelper,而后再提供一个 RESTHelper,这样也许更加符合设计原则中的“单一职责原则”,它们俩的职责更加清晰,也便于维护。
public class RESTHelper { private static final JacksonJsonProvider jsonProvider = new JacksonJsonProvider(); // 发布 REST 服务 public static void publishService(String wadl, Class<?> resourceClass) { JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean(); factory.setAddress(wadl); factory.setResourceClasses(resourceClass); factory.setProviders(Arrays.asList(jsonProvider)); factory.setResourceProvider(resourceClass, new SingletonResourceProvider(BeanHelper.getBean(resourceClass))); factory.create(); } // 建立 REST 客户端 public static <T> T createClient(String wadl, Class<? extends T> resourceClass) { return JAXRSClientFactory.create(wadl, resourceClass, Arrays.asList(jsonProvider)); } }
以上首先定义了一个 jsonProvider(JacksonJsonProvider),它是 Jackson JSON 库给咱们提供的基于 JAX-RS 的序列化与反序列化工具。该对象只需加载一次便可,因此将其定义为 static 的了。
随后提供了两个 static 方法:
说明:
如今工具都准备好了,下面要作的就是调用这个它,来发布 REST 服务。
第四步:发布 REST 服务
咱们须要扩展一下 WebServiceServlet,由于只有它才能发布 WebService。须要在里面增长一个逻辑判断:
只需作如下简单改进便可实现:
@WebServlet(urlPatterns = WebServiceConstant.SERVLET_URL, loadOnStartup = 0) public class WebServiceServlet extends CXFNonSpringServlet { ... private void publishWebService() { // 遍历全部标注了 @WebService 注解的接口 List<Class<?>> interfaceClassList = ClassHelper.getClassListByAnnotation(WebService.class); if (CollectionUtil.isNotEmpty(interfaceClassList)) { for (Class<?> interfaceClass : interfaceClassList) { // 获取 @WebService 注解及其相关属性 WebService ws = interfaceClass.getAnnotation(WebService.class); String wsValue = ws.value(); WebService.Type wsType = ws.type(); // 获取 WebService 地址 String address = getAddress(wsValue, interfaceClass); // 判断 WebService 类型(SOAP 或 REST) if (wsType == WebService.Type.SOAP) { doPublishForSOAP(address, interfaceClass); } else if (wsType == WebService.Type.REST) { doPublishForREST(address, interfaceClass); } } } } private void doPublishForSOAP(String wsdl, Class<?> interfaceClass) { // 获取 WebService 实现类(找到惟一的实现类) Class<?> implementClass = IOCHelper.findImplementClass(interfaceClass); // 获取实现类的实例 Object implementInstance = BeanHelper.getBean(implementClass); // 发布 SOAP Service SOAPHelper.publishService(wsdl, interfaceClass, implementInstance); } private void doPublishForREST(String wadl, Class<?> resourceClass) { // 发布 REST Service RESTHelper.publishService(wadl, resourceClass); } ... }
是否是 so easy?尽管 if else 有不少人反对,但我仍是以为它够简单、够直接,并不是全部状况都须要用多态来替换 if else 的,要具体状况具体分析,固然这只是个人我的的编程习惯问题了。
下面,咱们不妨配置一个 REST 服务吧,看看 Smart WebService 插件可否将其成功地发布出来。
第五步:配置 REST 服务
REST 推荐咱们直接面向类进行发布,而无需面向接口。其实 REST 也能够定义接口的,只不过意义不太大,我我的也是这么以为的,而 SOAP 彷佛必需要有一个接口才行,搞得跟 EJB 有一拼了。
不妨以 Smart Sample 中的 Product 为例,咱们为它发布一个 REST 服务吧。
@Bean @WebService(value = "/rest/ProductService", type = WebService.Type.REST) @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class ProductService extends BaseService { @GET @Path("/products") public List<Product> getProductList() { return DataSet.selectList(Product.class, "", "id asc"); } @GET @Path("/product/{productId}") public Product getProduct(@PathParam("productId") long productId) { return DataSet.select(Product.class, "id = ?", productId); } @POST @Path("/product") @Transaction public boolean createProduct(Map<String, Object> productFieldMap) { return DataSet.insert(Product.class, productFieldMap); } @PUT @Path("/product/{productId}") @Transaction public boolean updateProduct(@PathParam("productId") long productId, Map<String, Object> productFieldMap) { return DataSet.update(Product.class, productFieldMap, "id = ?", productId); } @DELETE @Path("/product/{productId}") @Transaction public boolean deleteProduct(@PathParam("productId") long productId) { return DataSet.delete(Product.class, "id = ?", productId); } }
首先,在类的头上咱们使用了 @WebService 注解,其中定义了两个属性:
随后,须要使用 JAX-RS 规范提供的两个很是重要的注解:@Consumes 与 @Produces,前者用于序列化方法中参数,后者用于序列化方法返回值。
你们必定要明确,无论使用 SOAP 仍是 REST,他们都是 WebService,都是须要作序列化与反序列化的,只不过 REST 更加轻量级一些罢了,咱们可使用 JSON 来做为对象序列化工具,还记得 RESTHelper 中的 jsonProvider 的吗?它就是干这个活的。因此咱们在这里使用了 JAX-RS 规范的 javax.ws.rs.core.MediaType 常量类来指定 JSON 类型,实际上就是 application/json。
固然,也可使用 XML 做为对象序列化工具,可是我我的更加倾向于 JSON,由于它更加简洁,更加轻量级,也是如今的主流。不相信的话,您能够看看许多互联网公司(好比:淘宝、百度、新浪等)开放的 Open API,多半都是基于 JSON 的,其实它们本质上就是 REST 服务,只不过加上了一些权限控制机制,好比使用了 OAuth 规范。
这里的 ProductService 其实与普通的 Service 没多大区别,也可使用事务控制(能够在须要事务控制的方法上使用 @Transaction 注解),只不过能够对外发布 WebService 罢了。初看一下该类中的方法,是否是与 Smart Action 或 Spring MVC Controller 有殊途同归之妙呢?这就是 JAX-RS 规范教咱们如何发布 REST 服务的方法。
咱们这里展示了 REST 中经常使用的四种动做:GET、POST、PUT、DELETE,他们能够分别对应 CRUD 操做,并且还能够简化 URL 的表现形式,这彷佛太妙了。
有些朋友问我:有 GET 与 POST 不就够了吗?为什么还要有 PUT、DELETE 呢?缘由以下:
1. 语义更加清晰
这四个动词分别对应咱们的 CRUD 操做,能够这样理解:
看到了 URL 就知道是什么类型的操做,这样不是更清晰了吗?
2. 简化 URL 表达方式
同一个 URL,使用不一样的动词,可表达不一样的语义,好比:
这样的 URL 是否优雅呢?
发布 REST 服务再也不是咱们同年的梦想了,并且 Smart 还能够同时发布 SOAP 与 REST 这两种 WebService,启动 Tomcat 后将自动发布。
第六步:启动 Tomcat
可经过 CXF 提供的 WebService 控制台查看已发布的 WebService,只需输入如下地址:
http://localhost:8080/smart-sample/ws
这里有一个 WADL,全称是 Web Application Description Language(Web 应用描述语言),REST 就用 WADL 来描述本身的。
如下两个资源方便您了解一下 WADL 到底是什么?
看到了这个 WADL 链接,也就证实 REST 服务发布成功了,咱们能够随时经过 REST 客户端进行调用。
最后一步:调用 REST 服务
REST 有一个特性确实比 SOAP 要好不少,那就是便于测试。咱们可使用浏览器,或 REST 客户端软件,或使用 Chrome、Firefox 的相关 REST 客户端插件,这些均可以让咱们轻松调用 REST 服务。咱们不妨使用浏览器来调用一下 REST 服务吧。
在浏览器地址栏中输入:http://localhost:8080/smart-sample/ws/rest/ProductService/product/1
是否是很爽呢?但使用浏览器咱们只能发送 GET 请求,其它类型的请求,咱们仍是经过客户端软件来调用比较好。
那么,如何在 Java 中来调用 REST 服务呢?
调用 REST 服务,必须知道 WADL 地址,这就像调用 SOAP 服务,必需要知道 WSDL 地址同样。咱们首先来一个简单的调用吧:
public class ProductServiceRESTTest { private String wadl = "http://localhost:8080/smart-sample/ws/rest/ProductService"; private ProductService productService = RESTHelper.createClient(wadl, ProductService.class); @Test public void getProductTest() { long productId = 1; Product product = productService.getProduct(productId); Assert.assertNotNull(product); } }
这是一个 JUnit 单元测试类,咱们经过 WADL 并使用 RESTHelper 来建立 REST 客户端(代理),直接经过这个代理对象来调用目标方法。其实 CXF 底层也使用了 CGLib 做为动态代理工具,看来这个工具的使用范围还真广,由于它确实好用!
调用结果如咱们所愿,一切正常。可是这彷佛太简单,咱们要再也不来一个更复杂一点的调用吧:
... @Test public void createProductTest() { Map<String, Object> productFieldMap = new HashMap<String, Object>(); productFieldMap.put("productTypeId", 1); productFieldMap.put("productName", "1"); productFieldMap.put("productCode", "1"); productFieldMap.put("price", 1); productFieldMap.put("description", "1"); boolean result = productService.createProduct(productFieldMap); Assert.assertTrue(result); } ...
调用 REST 服务并传递一个 Map 对象,其结果也是正确的。看来 JSON 对象序列化起效果了,若是您不使用 jsonProvider 确定会报错,告诉您没法序列化 Map 对象。这偏偏是 SOAP 服务的硬伤!
想让 SOAP 来序列化 Map 对象,咱们恐怕不会这样简单了。那么,如何经过 SOAP 来实现 Map 对象的序列化呢?下回分解,敬请期待!