教學課程:在 Service Fabric 上使用 Java Web API 前端服務和具狀態後端服務建立應用程式Tutorial: Create an application with a Java web API front-end service and a stateful back-end service on Service Fabric

本教學課程是一個系列的第一部分。This tutorial is part one of a series. 當您完成時,您會有一個 Voting 應用程式,其 Java Web 前端會將投票結果儲存在叢集中具狀態的後端服務。When you are finished, you have a Voting application with a Java web front end that saves voting results in a stateful back-end service in the cluster. 此教學課程系列要求您具備一部運作中的 Mac OSX 或 Linux 開發人員電腦。This tutorial series requires that you have a working Mac OSX or Linux developer machine. 如果您不需要以手動建立 Voting 應用程式,可以下載已完成應用程式的原始程式碼並直接前往逐步解說投票範例應用程式If you don't want to manually create the voting application, you can download the source code for the completed application and skip ahead to Walk through the voting sample application. 此外,請考慮遵循 Java 可靠服務快速入門Also, consider following the Quickstart for Java reliable services.

本機 Voting 應用程式

在本教學課程系列中,您將了解如何:In this tutorial series you learn how to:

在系列的第一部分中,您將了解如何:In part one of the series, you learn how to:

  • 建立 Java 具狀態可靠服務Create a Java stateful reliable service
  • 建立 Java 無狀態 Web 應用程式服務Create a Java stateless web application service
  • 使用服務遠端處理來與具狀態服務進行通訊Use service remoting to communicate with the stateful service
  • 在本機 Service Fabric 叢集上部署應用程式Deploy application on a local Service Fabric cluster

必要條件Prerequisites

開始進行本教學課程之前:Before you begin this tutorial:

  • 如果您沒有 Azure 訂用帳戶,請建立免費帳戶If you don't have an Azure subscription, create a free account.
  • MacLinux 上設定開發環境。Set up your development environment for Mac or Linux. 請遵循指示來安裝 Eclipse 外掛程式、 Gradle、Service Fabric SDK 和 Service Fabric CLI (sfctl)。Follow the instructions to install the Eclipse plug-in, Gradle, the Service Fabric SDK, and the Service Fabric CLI (sfctl).

建立前端 Java 無狀態服務Create the front-end Java stateless service

首先,建立 Voting 應用程式的 Web 前端。First, create the web front end of the Voting application. 由 AngularJS 支援的 Web UI 會將要求傳送至 Java 無狀態服務,該服務會執行輕量型 HTTP 伺服器。A web UI powered by AngularJS sends requests to the Java stateless service, which runs a lightweight HTTP server. 此服務會處理每個要求,並將遠端程序呼叫傳送給具狀態服務以儲存選票。This service processes each request and sends a remote procedure call to the stateful service to store the votes.

  1. 啟動 Eclipse。Launch Eclipse.

  2. 透過 [檔案]->[新增]->[其他]->[Service Fabric]->[Service Fabric 專案] 來建立專案。Create a project with File->New->Other->Service Fabric->Service Fabric Project.

    Eclipse 中的 [新增專案] 對話方塊

  3. 在 [ServiceFabric 專案精靈] 對話方塊中,將專案命名為 Voting,然後按 [下一步]。In the ServiceFabric Project Wizard dialog, name the Project Voting and press Next.

    在新增服務對話方塊中選擇 Java 具狀態服務

  4. 在 [新增服務] 頁面上,選擇 [具狀態服務],並將服務命名為 VotingWebOn the Add Service page, choose Stateless Service, and name your service VotingWeb. 按一下 [完成] 以建立專案。Click Finish to create the project.

    建立無狀態服務

    Eclipse 會建立應用程式和服務專案並顯示在 [套件總管] 中。Eclipse creates an application and a service project and displays them in Package Explorer.

    建立應用程式後的套件總管

下表提供前一個螢幕擷取畫面之套件總管中每個項目的簡短描述。The table gives a short description of each item in the package explorer from the previous screenshot.

套件總管項目Package Explorer Item 說明Description
PublishProfilesPublishProfiles 包含 JSON 檔案,可描述本機和 Azure Service Fabric 叢集的設定檔詳細資料。Contains JSON files describing profile details of local and Azure Service Fabric clusters. 部署應用程式時,外掛程式會使用這些檔案的內容。The contents of these files is used by the plugin when deploying the application.
指令碼Scripts 包含協助程式指令碼,可從命令列用來快速管理您的應用程式和叢集。Contains helper scripts that can be used from the command line to quickly manage your application with a cluster.
VotingApplicationVotingApplication 包含已推送至 Service Fabric 叢集的 Service Fabric 應用程式。Contains the Service Fabric application that is pushed to the Service Fabric cluster.
VotingWebVotingWeb 包含前端無狀態服務原始程式檔,以及相關的 gradle 組建檔案。Contains the front-end stateless service source files along with the related gradle build file.
build.gradlebuild.gradle 用來管理專案的 Gradle 檔案。Gradle file used to manage the project.
settings.gradlesettings.gradle 包含此資料夾中的 Gradle 專案名稱。Contains names of Gradle projects in this folder.

將 HTML 和 Javascript 新增至 VotingWeb 服務Add HTML and Javascript to the VotingWeb service

若要新增可由無狀態服務轉譯的 UI,請新增 HTML 檔案。To add a UI that can be rendered by the stateless service, add an HTML file. 然後,無狀態 Java 服務中嵌入的輕量型 HTTP 伺服器會轉譯此 HTML 檔案。This HTML file is then rendered by the lightweight HTTP server embedded into the stateless Java service.

  1. 展開 VotingApplication 目錄以到達 VotingApplication/VotingWebPkg/Code 目錄。Expand the VotingApplication directory to reach the VotingApplication/VotingWebPkg/Code directory.

  2. 以滑鼠右鍵按一下 [Code] 目錄,然後按一下 [新增]->[資料夾]。Right-click on the Code directory and click New->Folder.

  3. 將資料夾命名為 wwwroot,然後按一下 [完成]。Name the folder wwwroot and click Finish.

    Eclipse 建立 wwwroot 資料夾

  4. 將檔案新增至名為 index.htmlwwwroot,並將下列內容貼到這個檔案中。Add a file to the wwwroot called index.html and paste the following contents into this file.

<!DOCTYPE html>
<html>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.4/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.13.4/ui-bootstrap-tpls.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<body>

<script>
var app = angular.module('VotingApp', ['ui.bootstrap']);
app.controller("VotingAppController", ['$rootScope', '$scope', '$http', '$timeout', function ($rootScope, $scope, $http, $timeout) {
    $scope.votes = [];
    
    $scope.refresh = function () {
        $http.get('getStatelessList')
            .then(function successCallback(response) {
        $scope.votes = Object.assign(
            {},
            ...Object.keys(response.data) .
            map(key => ({[decodeURI(key)]: response.data[key]}))
        )
        },
        function errorCallback(response) {
            alert(response);
        });
    };

    $scope.remove = function (item) {
       $http.get("removeItem", {params: { item: encodeURI(item) }})
            .then(function successCallback(response) {
                $scope.refresh();
            },
            function errorCallback(response) {
                alert(response);
            });
    };

    $scope.add = function (item) {
        if (!item) {return;}
        $http.get("addItem", {params: { item: encodeURI(item) }})
            .then(function successCallback(response) {
                $scope.refresh();
            },
            function errorCallback(response) {
                alert(response);
            });
    };
}]);
</script>

<div ng-app="VotingApp" ng-controller="VotingAppController" ng-init="refresh()">
    <div class="container-fluid">
        <div class="row">
            <div class="col-xs-8 col-xs-offset-2 text-center">
                <h2>Service Fabric Voting Sample</h2>
            </div>
        </div>

        <div class="row">
            <div class="col-xs-offset-2">
                <form style="width:50% ! important;" class="center-block">
                    <div class="col-xs-6 form-group">
                        <input id="txtAdd" type="text" class="form-control" placeholder="Add voting option" ng-model="item" />
                    </div>
                    <button id="btnAdd" class="btn btn-default" ng-click="add(item)">
                        <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
                        Add
                    </button>
                </form>
            </div>
        </div>

        <hr />

        <div class="row">
            <div class="col-xs-8 col-xs-offset-2">
                <div class="row">
                    <div class="col-xs-4">
                        Click to vote
                    </div>
                </div>
                <div class="row top-buffer" ng-repeat="(key, value)  in votes">
                    <div class="col-xs-8">
                        <button class="btn btn-success text-left btn-block" ng-click="add(key)">
                            <span class="pull-left">
                                {{key}}
                            </span>
                            <span class="badge pull-right">
                                {{value}} Votes
                            </span>
                        </button>
                    </div>
                    <div class="col-xs-4">
                        <button class="btn btn-danger pull-right btn-block" ng-click="remove(key)">
                            <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
                            Remove
                        </button>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

</body>
</html>

更新 VotingWeb.java 檔案Update the VotingWeb.java file

VotingWeb 子專案中,開啟 VotingWeb/src/statelessservice/VotingWeb.java 檔案。In the VotingWeb subproject, open the VotingWeb/src/statelessservice/VotingWeb.java file. VotingWebService 服務是進入無狀態服務的閘道,負責設定前端 API 的通訊接聽程式。The VotingWeb service is the gateway into the stateless service and is responsible for setting up the communication listener for the front-end API.

以下列內容取代檔案中的現有 createServiceInstanceListeners 方法並儲存變更。Replace the existing createServiceInstanceListeners method in the file with the following and save your changes.

@Override
protected List<ServiceInstanceListener> createServiceInstanceListeners() {

    EndpointResourceDescription endpoint = this.getServiceContext().getCodePackageActivationContext().getEndpoint(webEndpointName);
    int port = endpoint.getPort();

    List<ServiceInstanceListener> listeners = new ArrayList<ServiceInstanceListener>();
    listeners.add(new ServiceInstanceListener((context) -> new HttpCommunicationListener(context, port)));
    return listeners;
}

新增 HTTPCommunicationListener.java 檔案Add the HTTPCommunicationListener.java file

HTTP 通訊接聽程式可作為控制器,以設定 HTTP 伺服器及公開用來定義投票動作的 API。The HTTP communication listener acts as a controller that sets up the HTTP server and exposes the APIs defining voting actions. 以滑鼠右鍵按一下 VotingWeb/src/statelessservice 資料夾中的 [statelessservice] 套件,然後選取 [新增] -> [檔案]。Right-click on the statelessservice package in the VotingWeb/src/statelessservice folder, then select New->File. 將檔案命名為 HttpCommunicationListener.java,然後按一下 [完成]。Name the file HttpCommunicationListener.java and click Finish.

將檔案內容取代為下列項目,然後儲存變更。Replace the file contents with the following, then save your changes. 稍後,在「更新 HttpCommunicationListener.java 檔案」中,這個檔案會經過修改,以從後端服務轉譯、讀取和寫入投票資料。Later, in Update the HttpCommunicationListener.java file, this file is modified to render, read, and write voting data from the back-end service. 現在,接聽程式只會傳回 Voting 應用程式的靜態 HTML。For now, the listener simply returns the static HTML for the Voting app.

// ------------------------------------------------------------
//  Copyright (c) Microsoft Corporation.  All rights reserved.
//  Licensed under the MIT License (MIT). See License.txt in the repo root for license information.
// ------------------------------------------------------------

package statelessservice;

import com.google.gson.Gson;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.Headers;

import java.io.File;
import java.io.OutputStream;
import java.io.FileInputStream;

import java.net.InetSocketAddress;
import java.net.URI;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;

import microsoft.servicefabric.services.communication.runtime.CommunicationListener;
import microsoft.servicefabric.services.runtime.StatelessServiceContext;
import microsoft.servicefabric.services.client.ServicePartitionKey;
import microsoft.servicefabric.services.remoting.client.ServiceProxyBase;
import microsoft.servicefabric.services.communication.client.TargetReplicaSelector;
import system.fabric.CancellationToken;

public class HttpCommunicationListener implements CommunicationListener {

    private static final Logger logger = Logger.getLogger(HttpCommunicationListener.class.getName());

    private static final String HEADER_CONTENT_TYPE = "Content-Type";
    private static final int STATUS_OK = 200;
    private static final int STATUS_NOT_FOUND = 404;
    private static final int STATUS_ERROR = 500;
    private static final String RESPONSE_NOT_FOUND = "404 (Not Found) \n";
    private static final String MIME = "text/html";
    private static final String ENCODING = "UTF-8";

    private static final String ROOT = "wwwroot/";
    private static final String FILE_NAME = "index.html";
    private StatelessServiceContext context;
    private com.sun.net.httpserver.HttpServer server;
    private ServicePartitionKey partitionKey;
    private final int port;

    public HttpCommunicationListener(StatelessServiceContext context, int port) {
        this.partitionKey = new ServicePartitionKey(0);
        this.context = context;
        this.port = port;
    }

    // Called by openAsync when the class is instantiated
    public void start() {
        try {
            logger.log(Level.INFO, "Starting Server");
            server = com.sun.net.httpserver.HttpServer.create(new InetSocketAddress(this.port), 0);
        } catch (Exception ex) {
            logger.log(Level.SEVERE, null, ex);
            throw new RuntimeException(ex);
        }

        // Responsible for rendering the HTML layout described in the previous step
        server.createContext("/", new HttpHandler() {
            @Override
            public void handle(HttpExchange t) {
                try {
                    File file = new File(ROOT + FILE_NAME).getCanonicalFile();

                    if (!file.isFile()) {
                      // Object does not exist or is not a file: reject with 404 error.
                      t.sendResponseHeaders(STATUS_NOT_FOUND, RESPONSE_NOT_FOUND.length());
                      OutputStream os = t.getResponseBody();
                      os.write(RESPONSE_NOT_FOUND.getBytes());
                      os.close();
                    } else {
                      Headers h = t.getResponseHeaders();
                      h.set(HEADER_CONTENT_TYPE, MIME);
                      t.sendResponseHeaders(STATUS_OK, 0);
    
                      OutputStream os = t.getResponseBody();
                      FileInputStream fs = new FileInputStream(file);
                      final byte[] buffer = new byte[0x10000];
                      int count = 0;
                      while ((count = fs.read(buffer)) >= 0) {
                        os.write(buffer,0,count);
                      }

                      fs.close();
                      os.close();
                    }
                } catch (Exception e) {
                    logger.log(Level.WARNING, null, e);
                }
            }
        });

        /*
        [Replace this entire comment block in the 'Connect the services' section]
        */

        server.setExecutor(null);
        server.start();
    }

    //Helper method to parse raw HTTP requests
    private Map<String, String> queryToMap(String query){
        Map<String, String> result = new HashMap<String, String>();
        for (String param : query.split("&")) {
            String pair[] = param.split("=");
            if (pair.length>1) {
                result.put(pair[0], pair[1]);
            }else{
                result.put(pair[0], "");
            }
        }
        return result;
    }

    //Called closeAsync when the service is shut down
    private void stop() {
        if (null != server)
            server.stop(0);
    }

    //Called by the Service Fabric runtime when this service is created on a node
    @Override
    public CompletableFuture<String> openAsync(CancellationToken cancellationToken) {
        this.start();
                    logger.log(Level.INFO, "Opened Server");
        String publishUri = String.format("http://%s:%d/", this.context.getNodeContext().getIpAddressOrFQDN(), port);
        return CompletableFuture.completedFuture(publishUri);
    }

    //Called by the Service Fabric runtime when the service is shut down
    @Override
    public CompletableFuture<?> closeAsync(CancellationToken cancellationToken) {
        this.stop();
        return CompletableFuture.completedFuture(true);
    }

    //Called by the Service Fabric runtime to forcibly shut this listener down
    @Override
    public void abort() {
        this.stop();
    }
}

設定接聽連接埠Configure the listening port

建立 VotingWebService 前端服務時,Service Fabric 會選取供服務接聽的連接埠。When the VotingWeb service front-end service is created, Service Fabric selects a port for the service to listen on. VotingWeb 服務會充當此應用程式的前端,可接受外部流量,因此讓我們將該服務繫結至固定的已知連接埠。The VotingWeb service acts as the front end for this application and accepts external traffic, so let's bind that service to a fixed and well-know port. 在 [套件總管] 中,開啟 VotingApplication/VotingWebPkg/ServiceManifest.xml。In Package Explorer, open VotingApplication/VotingWebPkg/ServiceManifest.xml. 在 [資源] 區段中尋找 [端點] 資源,並將 [連接埠] 的值變更為 8080 (我們將在教學課程中繼續使用此連接埠)。Find the Endpoint resource in the Resources section and change the Port value to 8080 (we'll continue using this port throughout the tutorial). 若要在本機上部署並執行應用程式,則必須在您的電腦上開啟應用程式接聽連接埠,並讓此連接埠可供使用。To deploy and run the application locally, the application listening port must be open and available on your computer. 將下列程式碼片段貼在 ServiceManifest 元素內 (也就是 <DataPackage> 元素的正下方)。Paste the following code snippet within the ServiceManifest element (i.e. just below the <DataPackage> element).

<Resources>
    <Endpoints>
        <!-- This endpoint is used by the communication listener to obtain the port on which to
            listen. Please note that if your service is partitioned, this port is shared with
            replicas of different partitions that are placed in your code. -->
        <Endpoint Name="WebEndpoint" Protocol="http" Port="8080" />
    </Endpoints>
  </Resources>

將具狀態後端服務新增到應用程式Add a stateful back-end service to your application

現在已完成 Java Web API 服務的基本架構,讓我們繼續完成具狀態後端服務。Now that the skeleton of the Java Web API service is complete, let's go ahead and complete the stateful back-end service.

Service Fabric 可讓您使用可靠集合,直接在服務內以一致且可靠的方式儲存資料。Service Fabric allows you to consistently and reliably store your data right inside your service by using reliable collections. 可靠集合是一組高可用性和可靠的集合類別。Reliable collections are a set of highly available and reliable collection classes. 任何用過 Java 集合的人都很熟悉這些類別的使用方式。The usage of these classes is familiar to anyone who has used Java collections.

  1. 在 [套件總管] 中,以滑鼠右鍵按一下應用程式專案中的 [Voting],然後選擇 [Service Fabric] > [新增 Service Fabric 服務]。In Package Explorer, right-click Voting within the application project and choose Service Fabric > Add Service Fabric Service.

  2. 在 [新增服務] 對話方塊中,選擇 [具狀態服務],並將服務命名為 VotingDataService,然後按一下 [新增服務]。In the Add Service dialog, choose Stateful Service and name the service VotingDataService and click Add Service.

    建立服務專案後,您的應用程式中會有兩個服務。Once your service project is created, you have two services in your application. 隨著您繼續組建應用程式,您可以用相同的方式新增更多服務。As you continue to build your application, you can add more services in the same way. 每個服務都可以獨立設定版本和升級。Each can be independently versioned and upgraded.

  3. Eclipse 會建立服務專案並顯示在 [套件總管] 中。Eclipse creates a service project and displays it in Package Explorer.

    Controllers\HomeController.cs

新增 VotingDataService.java 檔案Add the VotingDataService.java file

VotingDataService.java 檔案包含一些方法,其中包含用於從可靠集合中擷取、新增和移除選票的邏輯。The VotingDataService.java file contains the methods that contain logic to retrieve, add, and remove votes from the reliable collections. 將下列 VotingDataService 類別方法新增至 VotingDataService/src/statefulservice/VotingDataService.java 檔案。Add the following VotingDataService class methods to the VotingDataService/src/statefulservice/VotingDataService.java file.

package statefulservice;

import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;

import microsoft.servicefabric.services.communication.runtime.ServiceReplicaListener;

import microsoft.servicefabric.services.runtime.StatefulService;

import microsoft.servicefabric.services.remoting.fabrictransport.runtime.FabricTransportServiceRemotingListener;

import microsoft.servicefabric.data.ReliableStateManager;
import microsoft.servicefabric.data.Transaction;
import microsoft.servicefabric.data.collections.ReliableHashMap;
import microsoft.servicefabric.data.utilities.AsyncEnumeration;
import microsoft.servicefabric.data.utilities.KeyValuePair;

import system.fabric.StatefulServiceContext;

import rpcmethods.VotingRPC;

class VotingDataService extends StatefulService implements VotingRPC {
    private static final String MAP_NAME = "votesMap";
    private ReliableStateManager stateManager;

    protected VotingDataService (StatefulServiceContext statefulServiceContext) {
        super (statefulServiceContext);
    }

    @Override
    protected List<ServiceReplicaListener> createServiceReplicaListeners() {
        this.stateManager = this.getReliableStateManager();
        ArrayList<ServiceReplicaListener> listeners = new ArrayList<>();

        listeners.add(new ServiceReplicaListener((context) -> {
            return new FabricTransportServiceRemotingListener(context,this);
        }));

        return listeners;
    }

    // Method that will be invoked via RPC from the front end to retrieve the complete set of votes in the map
    public CompletableFuture<HashMap<String,String>> getList() {
        HashMap<String, String> tempMap = new HashMap<String, String>();

        try {

            ReliableHashMap<String, String> votesMap = stateManager
                    .<String, String> getOrAddReliableHashMapAsync(MAP_NAME).get();

            Transaction tx = stateManager.createTransaction();
            AsyncEnumeration<KeyValuePair<String, String>> kv = votesMap.keyValuesAsync(tx).get();
            while (kv.hasMoreElementsAsync().get()) {
                KeyValuePair<String, String> k = kv.nextElementAsync().get();
                tempMap.put(k.getKey(), k.getValue());
            }

            tx.close();


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

        return CompletableFuture.completedFuture(tempMap);
    }

    // Method that will be invoked via RPC from the front end to add an item to the votes list or to increase the
    // vote count for a particular item
    public CompletableFuture<Integer> addItem(String itemToAdd) {
        AtomicInteger status = new AtomicInteger(-1);

        try {

            ReliableHashMap<String, String> votesMap = stateManager
                    .<String, String> getOrAddReliableHashMapAsync(MAP_NAME).get();

            Transaction tx = stateManager.createTransaction();
            votesMap.computeAsync(tx, itemToAdd, (k, v) -> {
                if (v == null) {
                    return "1";
                }
                else {
                    int numVotes = Integer.parseInt(v);
                    numVotes = numVotes + 1;
                    return Integer.toString(numVotes);
                }
            }).get();

            tx.commitAsync().get();
            tx.close();

            status.set(1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return CompletableFuture.completedFuture(new Integer(status.get()));
    }

    // Method that will be invoked via RPC from the front end to remove an item
    public CompletableFuture<Integer> removeItem(String itemToRemove) {
        AtomicInteger status = new AtomicInteger(-1);
        try {
            ReliableHashMap<String, String> votesMap = stateManager
                    .<String, String> getOrAddReliableHashMapAsync(MAP_NAME).get();

            Transaction tx = stateManager.createTransaction();
            votesMap.removeAsync(tx, itemToRemove).get();
            tx.commitAsync().get();
            tx.close();

            status.set(1);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return CompletableFuture.completedFuture(new Integer(status.get()));
    }

}

現在已建立無狀態前端服務和後端服務的基本架構。The skeleton for the front-end stateless service and the backend service is now created.

建立您的應用程式的通訊介面Create the communication interface to your application

下一步是連接前端無狀態服務與後端服務。The next step is connecting the front-end stateless service and the backend service. 這些服務都會利用稱為 VotingRPC 的介面,該介面可定義 Voting 應用程式的作業。Both the services utilize an interface called the VotingRPC that defines the operations of the Voting application. 前端和後端服務都會實作這個介面,以啟用這兩項服務之間的遠端程序呼叫 (RPC)。This interface is implemented by both the front-end and back-end services to enable remote procedure calls (RPC) between the two services. 不幸的是,Eclipse 不支援新增 Gradle 子專案,所以必須手動新增包含此介面的套件。Unfortunately, Eclipse does not support the adding of Gradle subprojects, so the package that contains this interface has to be added manually.

  1. 在 [套件總管] 中,以滑鼠右鍵按一下 Voting 專案,然後按一下 [新增] -> [資料夾]。Right click on the Voting project in the Package Explorer and click New -> Folder. 將資料夾命名為 VotingRPC/src/rpcmethodsName the folder VotingRPC/src/rpcmethods.

    建立 VotingRPC 套件

  2. Voting/VotingRPC/src/rpcmethods 之下建立一個名為 VotingRPC.java 的檔案,並將下列內容貼在 VotingRPC.java 檔案中。Create a file under Voting/VotingRPC/src/rpcmethods named VotingRPC.java and paste the following inside the VotingRPC.java file.

    package rpcmethods;
    
    import java.util.ArrayList;
    import java.util.concurrent.CompletableFuture;
    import java.util.List;
    import java.util.HashMap;
    
    import microsoft.servicefabric.services.remoting.Service;
    
    public interface VotingRPC extends Service {
        CompletableFuture<HashMap<String, String>> getList();
    
        CompletableFuture<Integer> addItem(String itemToAdd);
    
        CompletableFuture<Integer> removeItem(String itemToRemove);
    }
    
  3. 在 Voting/VotingRPC 目錄中建立名為 build.gradle 的空白檔案,並將下列內容貼在此檔案中。Create an empty file named build.gradle in the Voting/VotingRPC directory and paste the following inside it. 這個 gradle 檔案用來建置和建立由其他服務匯入的 jar 檔案。This gradle file is used to build and create the jar file that is imported by the other services.

    apply plugin: 'java'
    apply plugin: 'eclipse'
    
    sourceSets {
      main {
         java.srcDirs = ['src']
         output.classesDir = 'out/classes'
          resources {
           srcDirs = ['src']
         }
       }
    }
    
    clean.doFirst {
        delete "${projectDir}/out"
        delete "${projectDir}/VotingRPC.jar"
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        compile ('com.microsoft.servicefabric:sf-actors:1.0.0')
    }
    
    jar {
        from configurations.compile.collect {
            (it.isDirectory() && !it.getName().contains("native")) ? it : zipTree(it)}
    
        manifest {
                attributes(
                'Main-Class': 'rpcmethods.VotingRPC')
            baseName "VotingRPC"
            destinationDir = file('./')
        }
    
        exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
    }
    
    defaultTasks 'clean', 'jar'
    
  4. 在 Voting/settings.gradle 檔案中,新增以下一行以在組建中包含新建立的專案。In the Voting/settings.gradle file, add a line to include the newly created project in the build.

    include ':VotingRPC'
    
  5. 在 Voting/VotingWeb/src/statelessservice/HttpCommunicationListener.java 檔案中,以下列內容取代註解區塊。In the Voting/VotingWeb/src/statelessservice/HttpCommunicationListener.java file, replace the comment block with the following.

    server.createContext("/getStatelessList", new HttpHandler() {
        @Override
        public void handle(HttpExchange t) {
            try {
                t.sendResponseHeaders(STATUS_OK,0);
                OutputStream os = t.getResponseBody();
    
                HashMap<String,String> list = ServiceProxyBase.create(VotingRPC.class, new URI("fabric:/VotingApplication/VotingDataService"), partitionKey, TargetReplicaSelector.DEFAULT, "").getList().get();
                String json = new Gson().toJson(list);
                os.write(json.getBytes(ENCODING));
                os.close();
            } catch (Exception e) {
                logger.log(Level.WARNING, null, e);
            }
        }
    });
    
    server.createContext("/removeItem", new HttpHandler() {
        @Override
        public void handle(HttpExchange t) {
            try {
                OutputStream os = t.getResponseBody();
                URI r = t.getRequestURI();
    
                Map<String, String> params = queryToMap(r.getQuery());
                String itemToRemove = params.get("item");
    
                Integer num = ServiceProxyBase.create(VotingRPC.class, new URI("fabric:/VotingApplication/VotingDataService"), partitionKey, TargetReplicaSelector.DEFAULT, "").removeItem(itemToRemove).get();
    
                if (num != 1)
                {
                    t.sendResponseHeaders(STATUS_ERROR, 0);
                } else {
                    t.sendResponseHeaders(STATUS_OK,0);
                }
    
                String json = new Gson().toJson(num);
                os.write(json.getBytes(ENCODING));
                os.close();
            } catch (Exception e) {
                logger.log(Level.WARNING, null, e);
            }
    
        }
    });
    
    server.createContext("/addItem", new HttpHandler() {
        @Override
        public void handle(HttpExchange t) {
            try {
                URI r = t.getRequestURI();
                Map<String, String> params = queryToMap(r.getQuery());
                String itemToAdd = params.get("item");
    
                OutputStream os = t.getResponseBody();
                Integer num = ServiceProxyBase.create(VotingRPC.class, new URI("fabric:/VotingApplication/VotingDataService"), partitionKey, TargetReplicaSelector.DEFAULT, "").addItem(itemToAdd).get();
                if (num != 1)
                {
                    t.sendResponseHeaders(STATUS_ERROR, 0);
                } else {
                    t.sendResponseHeaders(STATUS_OK,0);
                }
    
                String json = new Gson().toJson(num);
                os.write(json.getBytes(ENCODING));
                os.close();
            } catch (Exception e) {
                logger.log(Level.WARNING, null, e);
            }
        }
    });
    
  6. 在 Voting/VotingWeb/src/statelessservice/HttpCommunicationListener.java 檔案的頂端新增適當的 import 陳述式。Add the appropriate import statement at the top of the Voting/VotingWeb/src/statelessservice/HttpCommunicationListener.java file.

    import rpcmethods.VotingRPC; 
    

在這個階段,前端、後端和 RPC 介面的功能都已完成。At this stage, the functionality for the front end, backend, and RPC interfaces are complete. 下一個階段是先適當地設定 Gradle 指令碼,再部署至 Service Fabric 叢集。The next stage is to configure the Gradle scripts appropriately before deploying to a Service Fabric cluster.

逐步解說投票範例應用程式Walk through the voting sample application

投票應用程式包含兩個服務:The voting application consists of two services:

  • Web 前端服務 (VotingWeb) - Java Web 前端服務,可提供網頁並公開 API 來與後端服務通訊。Web front-end service (VotingWeb)- A Java web front-end service that serves the web page and exposes APIs to communicate with the backend service.
  • 後端服務 (VotingDataService) - Java Web 服務,可定義透過遠端程序呼叫 (RPC) 叫用來保存選票的方法。Back-end service (VotingDataService) - A Java web service, which defines methods that are invoked via Remote Procedure Calls (RPC) to persist votes.

應用程式圖表

在應用程式中執行動作 (新增項目、投票、移除項目) 時,會發生下列事件:When you perform an action in the application (add item, vote, remove item) the following events occur:

  1. JavaScript 會將適當的要求當作 HTTP 要求,傳送至 Web 前端服務中的 Web API。A JavaScript sends the appropriate request to the web API in the web front-end service as an HTTP request.

  2. Web 前端服務會使用 Service Fabric 內建的服務遠端處理功能來尋找要求,並將要求轉送至後端服務。The web front-end service uses the built-in Service Remoting functionality of Service Fabric to locate and forward the request to the back-end service.

  3. 後端服務會定義一些方法,以更新可靠字典中的結果。The back-end service defines methods that update the result in a reliable dictionary. 此可靠字典的內容會複寫到叢集中的多個節點並保存在磁碟上。The contents of this reliable dictionary get replicated to multiple nodes within the cluster and persisted on disk. 應用程式的所有資料都會儲存在叢集中。All the application's data is stored in the cluster.

設定 Gradle 指令碼Configure Gradle scripts

在這一節中會設定專案的 Gradle 指令碼。In this section, the Gradle scripts for the project are configured.

  1. 以下列內容取代 Voting/build.gradle 檔案的內容。Replace the contents of the Voting/build.gradle file with the following.

    apply plugin: 'java'
    apply plugin: 'eclipse'
    
    subprojects {
        apply plugin: 'java'
    }
    
    defaultTasks 'clean', 'jar', 'copyDeps'
    
  2. 以下列內容取代 Voting/VotingWeb/build.gradle 檔案的內容。Replace the contents of Voting/VotingWeb/build.gradle file with the following.

    apply plugin: 'java'
    apply plugin: 'eclipse'
    
    sourceSets {
      main {
         java.srcDirs = ['src']
         output.classesDir = 'out/classes'
          resources {
           srcDirs = ['src']
         }
       }
    }
    
    clean.doFirst {
        delete "${projectDir}/../lib"
        delete "${projectDir}/out"
        delete "${projectDir}/../VotingApplication/VotingWebPkg/Code/lib"
        delete "${projectDir}/../VotingApplication/VotingWebPkg/Code/VotingWeb.jar"
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        compile ('com.microsoft.servicefabric:sf-actors:1.0.0-preview1')
        compile project(':VotingRPC')
    }
    
    task explodeDeps(type: Copy, dependsOn:configurations.compile) { task ->
        configurations.compile.filter {!it.toString().contains("native")}.each{
            from it
        }
    
        configurations.compile.filter {it.toString().contains("native")}.each{
            from zipTree(it)
        }
        into "../lib/"
        include "lib*.so", "*.jar"
    }
    
    task copyDeps<< {
        copy {
            from("../lib/")
            into("../VotingApplication/VotingWebPkg/Code/lib")
            include('lib*.so')
        }
    }
    
    compileJava.dependsOn(explodeDeps)
    
    jar {
        from configurations.compile.collect {(it.isDirectory() && !it.getName().contains("native")) ? it : zipTree(it)}
    
        manifest {
            attributes(
                'Main-Class': 'statelessservice.VotingWebServiceHost')
            baseName "VotingWeb"
            destinationDir = file('../VotingApplication/VotingWebPkg/Code/')
        }
    
        exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
    }
    
    defaultTasks 'clean', 'jar', 'copyDeps'
    
  3. 取代 Voting/VotingDataService/build.gradle 檔案的內容。Replace the contents of Voting/VotingDataService/build.gradle file.

    apply plugin: 'java'
    apply plugin: 'eclipse'
    
    sourceSets {
      main {
         java.srcDirs = ['src']
         output.classesDir = 'out/classes'
          resources {
           srcDirs = ['src']
         }
       }
    }
    
    clean.doFirst {
        delete "${projectDir}/../lib"
        delete "${projectDir}/out"
        delete "${projectDir}/../VotingApplication/VotingDataServicePkg/Code/lib"
        delete "${projectDir}/../VotingApplication/VotingDataServicePkg/Code/VotingDataService.jar"
    }
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        compile ('com.microsoft.servicefabric:sf-actors:1.0.0-preview1')
        compile project(':VotingRPC')
    }
    
    task explodeDeps(type: Copy, dependsOn:configurations.compile) { task ->
        configurations.compile.filter {!it.toString().contains("native")}.each{
            from it
        }
    
        configurations.compile.filter {it.toString().contains("native")}.each{
            from zipTree(it)
        }
        into "../lib/"
        include "lib*.so", "*.jar"
    }
    
    compileJava.dependsOn(explodeDeps)
    
    task copyDeps<< {
        copy {
            from("../lib/")
            into("../VotingApplication/VotingDataServicePkg/Code/lib")
            include('lib*.so')
        }
    }
    
    jar {
        from configurations.compile.collect {
            (it.isDirectory() && !it.getName().contains("native")) ? it : zipTree(it)}
    
        manifest {
            attributes('Main-Class': 'statefulservice.VotingDataServiceHost')
    
            baseName "VotingDataService"
            destinationDir = file('../VotingApplication/VotingDataServicePkg/Code/')
        }
    
        exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
    }
    
    defaultTasks 'clean', 'jar', 'copyDeps'
    

將應用程式部署至本機叢集Deploy application to local cluster

此時,應用程式已準備好部署到本機 Service Fabric 叢集。At this point, the application is ready to be deployed to a local Service Fabric cluster.

  1. 在 [套件總管] 中,以滑鼠右鍵按一下 Voting 專案,然後按一下 [Service Fabric] -> [建置應用程式] 以建置應用程式。Right click on the Voting project in the Package Explorer and click Service Fabric -> Build Application to build your application.

  2. 執行本機 Service Fabric 叢集。Run your local Service Fabric cluster. 此步驟取決於您的開發環境 (Mac 或 Linux)。This step depends on your development environment (Mac or Linux).

    如果您使用 Mac,請利用下列命令執行本機叢集:使用您自有工作區的路徑取代傳入 -v 參數的命令。If you are using a Mac, you run the local cluster with the following command: Replace the command passed into the -v parameter with the path to your own workspace.

    docker run -itd -p 19080:19080 -p 8080:8080 -p --name sfonebox servicefabricoss/service-fabric-onebox
    

    請參閱 OS X 安裝指南中的詳細指示。See more detailed instructions in the OS X setup guide.

    如果您在 Linux 機器上執行,可使用下列命令啟動本機叢集:If you are running on a Linux machine, you start the local cluster with the following command:

    sudo /opt/microsoft/sdk/servicefabric/common/clustersetup/devclustersetup.sh
    

    請參閱 Linux 安裝指南中的詳細指示。See more detailed instructions in the Linux setup guide.

  3. 在 Eclipse 的 [套件總管] 中,以滑鼠右鍵按一下 Voting 專案,然後按一下 [Service Fabric] -> [發佈應用程式...]。In the Package Explorer for Eclipse, right click on the Voting project and click Service Fabric -> Publish Application ...

  4. 在 [發佈應用程式] 視窗中,從下拉式清單中選取 Local.json,然後按一下 [發佈]。In the Publish Application window, select Local.json from the dropdown, and click Publish.

  5. 請移至您的網頁瀏覽器並存取 http://localhost:8080,以檢視本機 Service Fabric 叢集上執行中的應用程式。Go to your web browser and access http://localhost:8080 to view your running application on the local Service Fabric cluster.

後續步驟Next steps

在教學課程的這個部分中,您已了解如何:In this part of the tutorial, you learned how to:

  • 建立 Java 服務成為具狀態可靠服務Create Java service as a stateful reliable service
  • 建立 Java 服務成為具狀態 Web 服務Create Java service as a stateless web service
  • 新增 Java 介面,以處理您的服務之間的遠端程序呼叫 (RPC)Add a Java interface to handle the Remote Procedure Calls (RPC) between your services
  • 設定您的 Gradle 指令碼Configure your Gradle scripts
  • 建置應用程式並將它部署到本機 Service Fabric 叢集Build and deploy your application to a local Service Fabric cluster

前進到下一個教學課程:Advance to the next tutorial: