讓 Smart WebService 插件支持 REST 服務

本文是《輕量級 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 方法:

  • 在 Smart WebService 插件中會調用 publishService 方法來發布 REST 服務。
  • 若是您須要調用 REST 服務,能夠調用 createClient 方法來建立 REST 客戶端,這一樣也是一個 Proxy 對象,固然這只是實現 REST Client 的方法之一。

說明:

如今工具都準備好了,下面要作的就是調用這個它,來發布 REST 服務。

第四步:發佈 REST 服務

咱們須要擴展一下 WebServiceServlet,由於只有它才能發佈 WebService。須要在裏面增長一個邏輯判斷:

  • 若 @WebService 的 type 爲 Type.SOAP,則發佈 SOAP 服務。
  • 若 @WebService 的 type 爲 Type.REST,則發佈 REST 服務。

只需作如下簡單改進便可實現:

@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 註解,其中定義了兩個屬性:

  • value 屬性用於指定 WebService 的路徑,實際上是一個後綴而已,叫什麼都無所謂,若是不指定,那麼就用類的簡單名稱。
  • type 屬性用於指定 WebService 的類型,這裏指定爲 REST,因此用到了枚舉類型 WebService.Type.REST。

隨後,須要使用 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 操做,能夠這樣理解:

  • GET -- Read
  • POST -- Create
  • PUT -- Update
  • DELETE -- Delete

看到了 URL 就知道是什麼類型的操做,這樣不是更清晰了嗎?

2. 簡化 URL 表達方式

同一個 URL,使用不一樣的動詞,可表達不一樣的語義,好比:

  • GET /product/1 -- 獲取 id 爲 1 的 Product
  • PUT /product/1 -- 更新 id 爲 1 的 Product(可在 Request Body 中放入具體更新的字段)
  • DELETE /product/1 -- 刪除 id 爲 1 的 Product

這樣的 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 連接,能夠查看 WADL 文檔,它與 WSDL 相似,只不過它是用於描述 REST 服務的。

看到了這個 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 對象的序列化呢?下回分解,敬請期待!

相關文章
相關標籤/搜索