CategoryResourceRepost/极客时间专栏/geek/从0开始学微服务/模块一 入门微服务/04 | 如何发布和引用服务?.md
louzefeng bf99793fd0 del
2024-07-09 18:38:56 +00:00

14 KiB
Raw Blame History

从这期开始,我将陆续给你讲解微服务各个基本组件的原理和实现方式。

今天我要与你分享的第一个组件是服务发布和引用。我在前面说过,想要构建微服务,首先要解决的问题是,服务提供者如何发布一个服务,服务消费者如何引用这个服务。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。

我前面说过,最常见的服务发布和引用的方式有三种:

  • 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方式则更加通用。

    思考题

    针对你的业务场景思考一下,假如要进行服务化,你觉得使用哪种服务描述最合适?为什么?

    欢迎你在留言区写下自己的思考,与我一起讨论。