本文是《輕量級 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 對象的序列化呢?下回分解,敬請期待!