1. 程式人生 > >用函式計算搭建微服務——雲客服訪客名片

用函式計算搭建微服務——雲客服訪客名片

雲客服可以方便快捷整合到使用者的官網、APP、公眾號等任意位置;依託阿里雲智慧演算法,機器人能準確的理解使用者的意圖,並準確的回答使用者的問題;提供完整的熱線、線上服務功能,並能輕鬆連線企業的其他系統,如 CRM 等;動態管理客戶和坐席使用的統一知識庫、知識文章;實時彙總、實時分析服務中心的資料,幫助業務決策者從全域性視角瞭解熱門問題和當前的服務瓶頸;雲客服是一套完整的智慧服務體系。

訪客名片是雲客服上一個功能,用於關聯雲客服和客戶 CRM 系統之間的使用者,方便客服人員瞭解提問使用者的基本資訊,更好的支援使用者。

目前雲客服提供的訪客名片整合指南是一個基於 Spring MVC 實現的 Web 專案,nodejs 語言背景的使用者提出希望將 java 實現移植到函式計算服務,以服務的形式提供給 nodejs 實現的核心業務呼叫。

困難點

使用者自己嘗試過移植,如下幾個技術點讓其受挫:

  1. 雲客服提供的 jar 是 maven 私有倉庫,外網無法訪問,所以需要以拷貝 jar 的方式整合進 maven
  2. 如何正確的配置 maven 外掛打包成函式計算支援的 jar 包。

依賴本地 Jar 包

fccsplatform-common-crypto-1.0.0.20161108.jar 是一個阿里內網 maven 倉庫的包,外網客戶需要問雲客服索要。下面 xml片段是 maven 依賴本地 jar 包的通用方法,但是由於 maven 依賴本地 jar 並不常見,需要查些資料。

<dependency>
    <groupId>com.alipay.fc.csplatform</groupId>
    <artifactId>fccsplatform-common-crypto</artifactId>
    <version>1.0.0.20161108</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/lib/fccsplatform-common-crypto-1.0.0.20161108.jar</systemPath>
</dependency>

<scope>system</scope>表示從系統路徑查詢 jar 包,而不是倉庫,要配合 <systemPath/> 一起使用。<systemPath/> 指定 jar 的真實路徑,一般需要配合${project.basedir}一起使用,表示相對於專案目錄。

封裝 Handler 函式

Spring MVC 以 Controller 的形式對外暴露服務,而對應於函式計算實現 Handler 介面即可。

/**
 * This is the interface for any none event based function handler
 */
public interface StreamRequestHandler {

    /**
     * The interface to handle a function compute invoke request
     *
     * @param input The function compute input data wrapped in a stream
     * @param output The function compute output data wrapped in a stream
     * @param context The function compute execution environment context object.
     * @throws IOException IOException during I/O handling
     */
    void handleRequest(InputStream input, OutputStream output, Context context) throws IOException;
}

訪客名片的業務較為簡單隻要實現加密和解碼兩個介面,不涉及到第三方服務呼叫

public class Encryptor implements StreamRequestHandler {

    private CustomerInfo customerInfo = new CustomerInfo();

    public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException {
        String cInfo = CharStreams.toString(new InputStreamReader(
                input, Charset.defaultCharset()));
        try {
            output.write(customerInfo.encrypt(cInfo).getBytes(Charset.defaultCharset()));
        } catch (GeneralSecurityException e) {
            e.printStackTrace();
        }
    }
}
public class Decryptor implements StreamRequestHandler {

    private CustomerInfo customerInfo = new CustomerInfo();

    @Override
    public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException {
        String jsonString = CharStreams.toString(new InputStreamReader(
                input, Charset.defaultCharset()));

        JSONObject jsonObject = JSON.parseObject(jsonString);
        try {
            String cInfo = customerInfo.decrypt(jsonObject.getString("params"), jsonObject.getString("key"));
            output.write(cInfo.getBytes(Charset.defaultCharset()));

        } catch (GeneralSecurityException e) {
            e.printStackTrace();
        }

    }
}

上面 Encryptor.java 和 Decryptor.java 負責實現 StreamRequestHandler 介面,而 CustomerInfo.java 類封裝了真正的業務邏輯。

public class CustomerInfo {

    # 問雲客服要 PUB_KEY
    private static String PUB_KEY = "your PUB_KEY";


    public String encrypt(String cInfo) throws GeneralSecurityException, UnsupportedEncodingException {
        PublicKey publicKey = getPubKey(PUB_KEY);
        Map<String, String> res = CustomerInfoCryptoUtil.encryptByPublicKey(cInfo, publicKey);

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("cinfo", res.get("text"));
        jsonObject.put("key", res.get("key"));

        return jsonObject.toJSONString();
    }

    public String decrypt(String params, String key) throws UnsupportedEncodingException, GeneralSecurityException {
        PublicKey publicKey = getPubKey(PUB_KEY);
        return CustomerInfoCryptoUtil.decryptByPublicKey(params, key, publicKey);
    }

    private PublicKey getPubKey(String pubKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64Util.decode(pubKey));
        KeyFactory keyFactory;
        keyFactory = KeyFactory.getInstance("RSA");
        PublicKey key = keyFactory.generatePublic(keySpec);
        return key;
    }

打包

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>copy-dependencies</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.directory}/classes/lib</outputDirectory>
                <includeScope>compile</includeScope>
            </configuration>
        </execution>
    </executions>
</plugin>

函式計算官方文件推薦了兩種打包方式 maven-assembly-plugin 和 maven-dependency-plugin。

  • maven-assembly-plugin 會提取所有被依賴的 class 檔案,重新打包成一個 jar 包,體積更小,但可能會產生一些額外的問題
  • maven-dependency-plugin 將所有依賴 jar 包都打包那一個內部的 lib/ 目錄下,然後打包成巢狀 jar 包,函式計算執行時會解壓頂層 jar 包。

關於 java 打包更多的細節可以參考個人的另外一篇文章《Java 打包 FatJar 方法小結》 。本例中實測使用 maven-assembly-plugin 外掛打包是不行的,因為 systemPath 指定依賴的傳遞依賴沒法被打包進去,導致執行時缺少類,所以選用了 maven-dependency-plugin 的方式。

fun 部署

最後通過一個 fun 的 template.yml 檔案進行描述, 然後執行 mvn package && fun deploy

ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
  YunkefuSign:
    Type: 'Aliyun::Serverless::Service'
    Properties:
      Description: 'yun ke fu Sign'
    Encryptor:
      Type: 'Aliyun::Serverless::Function'
      Properties:
        Handler: com.aliyun.fc.Encryptor::handleRequest
        Runtime: java8
        CodeUri: './target/yunkefu-sign-1.0-SNAPSHOT.jar'
        Timeout: 60
    Decryptor:
      Type: 'Aliyun::Serverless::Function'
      Properties:
        Handler: com.aliyun.fc.Decryptor::handleRequest
        Runtime: java8
        CodeUri: './target/yunkefu-sign-1.0-SNAPSHOT.jar'
        Timeout: 60

小結

將語言作為服務的邊界是服務架構常見的劃分邊界方式之一。函式計算支援多語言環境,具備可擴充套件和高可用特性,使其適於以服務的形式支援多語言的架構。通常情況下,開發商在熟悉語言領域有足夠的積累,構建了一整套完備的架構和部署運維體系,而為了擴大業務的邊界,有時候會面臨被迫選擇熟悉語言領域之外的程式語言和架構,對於這一挑戰,函式計算只需要用語言簡單地寫業務,無需關注架構和運維,可以減輕開發商的壓力。