1. 程式人生 > 實用技巧 >/04-如何釋出和引用服務?

/04-如何釋出和引用服務?

今天我要與你分享的第一個元件是服務釋出和引用。我在前面說過,想要構建微服務,首先要解決的問題是,服務提供者如何釋出一個服務,服務消費者如何引用這個服務。具體來說,就是這個服務的介面名是什麼?呼叫這個服務需要傳遞哪些引數?介面的返回值是什麼型別?以及一些其他介面描述資訊。

我前面說過,最常見的服務釋出和引用的方式有三種:

  • RESTful API

  • XML配置

  • IDL檔案

下面我就結合具體的例項,逐個講解每一種方式的具體使用方法以及各自的應用場景,以便你在選型時作參考。

RESTful API

首先來說說RESTful API的方式,主要被用作HTTP或者HTTPS協議的介面定義,即使在非微服務架構體系下,也被廣泛採用。

下面是開源服務化框架Motan釋出RESTful API的例子,它釋出了三個RESTful格式的API,介面宣告如下:

@Path("/rest")
 public interface RestfulService {
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     List<User> getUsers(@QueryParam("uid") int uid);
 
     @GET
     @Path("/primitive")
     @Produces(MediaType.TEXT_PLAIN)
     String testPrimitiveType();
 
     @POST
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
     @Produces(MediaType.APPLICATION_JSON)
     Response add(@FormParam("id") int id, @FormParam("name") String name);

具體的服務實現如下:

public class RestfulServerDemo implements RestfulService {
        
     @Override
     public List<User> getUsers(@CookieParam("uid") int uid) {
         return Arrays.asList(new User(uid, "name" + uid));
     }
 
     @Override
     public String testPrimitiveType() {
         return "helloworld!";
     }
 
     @Override
     public Response add(@FormParam("id") int id, @FormParam("name") String name) {
         return Response.ok().cookie(new NewCookie("ck", String.valueOf(id))).entity(new User(id, name)).build();
     }

服務提供者這一端通過部署程式碼到Tomcat中,並配置Tomcat中如下的web.xml,就可以通過servlet的方式對外提供RESTful API。

<listener>
     <listener-class>com.weibo.api.motan.protocol.restful.support.servlet.RestfulServletContainerListener</listener-class>
 </listener>

 <servlet>
     <servlet-name>dispatcher</servlet-name>
     <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
     <load-on-startup>1</load-on-startup>
     <init-param>
         <param-name>resteasy.servlet.mapping.prefix</param-name>
         <param-value>/servlet</param-value>  <!-- 此處實際為servlet-mapping的url-pattern,具體配置見resteasy文件-->
     </init-param>
 </servlet>

 <servlet-mapping>
     <servlet-name>dispatcher</servlet-name>
     <url-pattern>/servlet/*</url-pattern>
 </servlet-mapping>

這樣服務消費者就可以通過HTTP協議呼叫服務了,因為HTTP協議本身是一個公開的協議,對於服務消費者來說幾乎沒有學習成本,所以比較適合用作跨業務平臺之間的服務協議。比如你有一個服務,不僅需要在業務部門內部提供服務,還需要向其他業務部門提供服務,甚至開放給外網提供服務,這時候採用HTTP協議就比較合適,也省去了溝通服務協議的成本。

XML配置

接下來再來給你講下XML配置方式,這種方式的服務釋出和引用主要分三個步驟:

  • 服務提供者定義介面,並實現介面。

  • 服務提供者程序啟動時,通過載入server.xml配置檔案將介面暴露出去。

  • 服務消費者程序啟動時,通過載入client.xml配置檔案來引入要呼叫的介面。

我繼續以服務化框架Motan為例,它還支援以XML配置的方式來發布和引用服務。

首先,服務提供者定義介面。

public interface FooService {
    public String hello(String name);
}

然後服務提供者實現介面。

public class FooServiceImpl implements FooService {

    public String hello(String name) {
        System.out.println(name + " invoked rpc service");
        return "hello " + name;
    }
}

最後服務提供者程序啟動時,載入server.xml配置檔案,開啟8002埠監聽。

server.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:motan="http://api.weibo.com/schema/motan"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
   http://api.weibo.com/schema/motan http://api.weibo.com/schema/motan.xsd">

    <!-- service implemention bean -->
    <bean id="serviceImpl" class="quickstart.FooServiceImpl" />
    <!-- exporting service by Motan -->
    <motan:service interface="quickstart.FooService" ref="serviceImpl" export="8002" />
</beans>

服務提供者載入server.xml的程式碼如下:

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Server {

    public static void main(String[] args) throws InterruptedException {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:motan_server.xml");
        System.out.println("server start...");
    }
}

服務消費者要想呼叫服務,就必須在程序啟動時,載入配置client.xml,引用介面定義,然後發起呼叫。

client.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:motan="http://api.weibo.com/schema/motan"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
   http://api.weibo.com/schema/motan http://api.weibo.com/schema/motan.xsd">

    <!-- reference to the remote service -->
    <motan:referer id="remoteService" interface="quickstart.FooService" directUrl="localhost:8002"/>
</beans>

服務消費者啟動時,載入client.xml的程式碼如下。

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class Client {

    public static void main(String[] args) throws InterruptedException {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:motan_client.xml");
        FooService service = (FooService) ctx.getBean("remoteService");
        System.out.println(service.hello("motan"));
    }
}

就這樣,通過在服務提供者和服務消費者之間維持一份對等的XML配置檔案,來保證服務消費者按照服務提供者的約定來進行服務呼叫。在這種方式下,如果服務提供者變更了介面定義,不僅需要更新服務提供者載入的介面描述檔案server.xml,還需要同時更新服務消費者載入的介面描述檔案client.xml。

一般是私有RPC框架會選擇XML配置這種方式來描述介面,因為私有RPC協議的效能要比HTTP協議高,所以在對效能要求比較高的場景下,採用XML配置的方式比較合適。但這種方式對業務程式碼侵入性比較高,XML配置有變更的時候,服務消費者和服務提供者都要更新,所以適合公司內部聯絡比較緊密的業務之間採用。如果要應用到跨部門之間的業務呼叫,一旦有XML配置變更,需要花費大量精力去協調不同部門做升級工作。在我經歷的實際專案裡,就遇到過一次底層服務的介面升級,需要所有相關的呼叫方都升級,為此花費了大量時間去協調溝通不同部門之間的升級工作,最後經歷了大半年才最終完成。所以對於XML配置方式的服務描述,一旦應用到多個部門之間的介面格式約定,如果有變更,最好是新增介面,不到萬不得已不要對原有的介面格式做變更。

IDL檔案

IDL就是介面描述語言(interface description language)的縮寫,通過一種中立的方式來描述介面,使得在不同的平臺上執行的物件和不同語言編寫的程式可以相互通訊交流。比如你用Java語言實現提供的一個服務,也能被PHP語言呼叫。

也就是說IDL主要是用作跨語言平臺的服務之間的呼叫,有兩種最常用的IDL:一個是Facebook開源的Thrift協議,另一個是Google開源的gRPC協議。無論是Thrift協議還是gRPC協議,它們的工作原理都是類似的。

接下來,我以gRPC協議為例,給你講講如何使用IDL檔案方式來描述介面。

gRPC協議使用Protobuf簡稱proto檔案來定義介面名、呼叫引數以及返回值型別。

比如檔案helloword.proto定義了一個介面SayHello方法,它的請求引數是HelloRequest,它的返回值是HelloReply。

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}

}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}  

假如服務提供者使用的是Java語言,那麼利用protoc外掛即可自動生成Server端的Java程式碼。

private class GreeterImpl extends GreeterGrpc.GreeterImplBase {

  @Override
  public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
    HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + req.getName()).build();
    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }

  @Override
  public void sayHelloAgain(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
    HelloReply reply = HelloReply.newBuilder().setMessage("Hello again " + req.getName()).build();
    responseObserver.onNext(reply);
    responseObserver.onCompleted();
  }
}

假如服務消費者使用的也是Java語言,那麼利用protoc外掛即可自動生成Client端的Java程式碼。

public void greet(String name) {
  logger.info("Will try to greet " + name + " ...");
  HelloRequest request = HelloRequest.newBuilder().setName(name).build();
  HelloReply response;
  try {
    response = blockingStub.sayHello(request);
  } catch (StatusRuntimeException e) {
    logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
    return;
  }
  logger.info("Greeting: " + response.getMessage());
  try {
    response = blockingStub.sayHelloAgain(request);
  } catch (StatusRuntimeException e) {
    logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
    return;
  }
  logger.info("Greeting: " + response.getMessage());
}  

假如服務消費者使用的是PHP語言,那麼利用protoc外掛即可自動生成Client端的PHP程式碼。

    $request = new Helloworld\HelloRequest();
    $request->setName($name);
    list($reply, $status) = $client->SayHello($request)->wait();
    $message = $reply->getMessage();
    list($reply, $status) = $client->SayHelloAgain($request)->wait();
    $message = $reply->getMessage(); 

由此可見,gRPC協議的服務描述是通過proto檔案來定義介面的,然後再使用protoc來生成不同語言平臺的客戶端和服務端程式碼,從而具備跨語言服務呼叫能力。

有一點特別需要注意的是,在描述介面定義時,IDL檔案需要對介面返回值進行詳細定義。如果介面返回值的欄位比較多,並且經常變化時,採用IDL檔案方式的介面定義就不太合適了。一方面可能會造成IDL檔案過大難以維護,另一方面只要IDL檔案中定義的介面返回值有變更,都需要同步所有的服務消費者都更新,管理成本就太高了。

我在專案實踐過程中,曾經考慮過採用Protobuf檔案來描述微博內容介面,但微博內容返回的欄位有幾百個,並且有些欄位不固定,返回什麼欄位是業務方自定義的,這種情況採用Protobuf檔案來描述的話會十分麻煩,所以最終不得不放棄這種方式。

總結

今天我給你介紹了服務描述最常見的三種方式:RESTful API、XML配置以及IDL檔案。

具體採用哪種服務描述方式是根據實際情況決定的,通常情況下,如果只是企業內部之間的服務呼叫,並且都是Java語言的話,選擇XML配置方式是最簡單的。如果企業內部存在多個服務,並且服務採用的是不同語言平臺,建議使用IDL檔案方式進行描述服務。如果還存在對外開放服務呼叫的情形的話,使用RESTful API方式則更加通用。