最常见的服务发布和引用的方式有三种:
- RESTful API
- XML 配置
- IDL 文件
下面我就结合具体的实例,逐个讲解每一种方式的具体使用方法以及各自的应用场景,以便你在选型时作参考。
RESTful API
首先来说说 RESTful API 的方式,主要被用作 HTTP 或者 HTTPS 协议的接口定义,即使在非微服务架构体系下,也被广泛采用。
下面是开源服务化框架Motan发布 RESTful API 的例子,它发布了三个 RESTful 格式的 API,接口声明如下:
复制代码
| @Path(“/rest”) | @Path(“/rest”) | ||
|---|---|---|---|
| public interface RestfulService { | public interface RestfulService { | ||
| @GET | @GET | ||
| @Produces(MediaType.APPLICATION_JSON) | @Produces(MediaType.APPLICATION_JSON) | ||
| List |
List |
||
| @GET | @GET | ||
| @Path(“/primitive”) | @Path(“/primitive”) | ||
| @Produces(MediaType.TEXT_PLAIN) | @Produces(MediaType.TEXT_PLAIN) | ||
| String testPrimitiveType(); | String testPrimitiveType(); | ||
| @POST | @POST | ||
| @Consumes(MediaType.APPLICATION_FORM_URLENCODED) | @Consumes(MediaType.APPLICATION_FORM_URLENCODED) | ||
| @Produces(MediaType.APPLICATION_JSON) | @Produces(MediaType.APPLICATION_JSON) | ||
| Response add(@FormParam(“id”) int id, @FormParam(“name”) String name); | Response add(@FormParam(“id”) int id, @FormParam(“name”) String name); |
具体的服务实现如下:
复制代码
| public class RestfulServerDemo implements RestfulService { | public class RestfulServerDemo implements RestfulService { | ||
|---|---|---|---|
| @Override | @Override | ||
| public List |
public List |
||
| return Arrays.asList(new User(uid, “name” + uid)); | return Arrays.asList(new User(uid, “name” + uid)); | ||
| } | } | ||
| @Override | @Override | ||
| public String testPrimitiveType() { | public String testPrimitiveType() { | ||
| return “helloworld!”; | return “helloworld!”; | ||
| } | } | ||
| @Override | @Override | ||
| public Response add(@FormParam(“id”) int id, @FormParam(“name”) String name) { | 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(); | return Response.ok().cookie(new NewCookie(“ck”, String.valueOf(id))).entity(new User(id, name)).build(); | ||
| } | } |
服务提供者这一端通过部署代码到 Tomcat 中,并配置 Tomcat 中如下的 web.xml,就可以通过 servlet 的方式对外提供 RESTful API。
复制代码
这样服务消费者就可以通过 HTTP 协议调用服务了,因为 HTTP 协议本身是一个公开的协议,对于服务消费者来说几乎没有学习成本,所以比较适合用作跨业务平台之间的服务协议。比如你有一个服务,不仅需要在业务部门内部提供服务,还需要向其他业务部门提供服务,甚至开放给外网提供服务,这时候采用 HTTP 协议就比较合适,也省去了沟通服务协议的成本。
XML 配置
接下来再来给你讲下 XML 配置方式,这种方式的服务发布和引用主要分三个步骤:
- 服务提供者定义接口,并实现接口。
- 服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去。
- 服务消费者进程启动时,通过加载 client.xml 配置文件来引入要调用的接口。
我继续以服务化框架 Motan 为例,它还支持以 XML 配置的方式来发布和引用服务。
首先,服务提供者定义接口。
复制代码
| public interface FooService { | public interface FooService { | ||
|---|---|---|---|
| public String hello(String name); | public String hello(String name); | ||
| } | } |
然后服务提供者实现接口。
复制代码
| public class FooServiceImpl implements FooService { | public class FooServiceImpl implements FooService { | ||
|---|---|---|---|
| public String hello(String name) { | public String hello(String name) { | ||
| System.out.println(name + “ invoked rpc service”); | System.out.println(name + “ invoked rpc service”); | ||
| return “hello “ + name; | return “hello “ + name; | ||
| } | } | ||
| } | } |
最后服务提供者进程启动时,加载 server.xml 配置文件,开启 8002 端口监听。
server.xml 配置如下:
复制代码
| <?xml version=”1.0” encoding=”UTF-8”?> | <?xml version=”1.0” encoding=”UTF-8”?> | ||
|---|---|---|---|
| <beans xmlns=”http://www.springframework.org/schema/beans“ | <beans xmlns=”http://www.springframework.org/schema/beans“ | ||
| xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“ | xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“ | ||
| xmlns:motan=”http://api.weibo.com/schema/motan“ | 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 | 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"> | http://api.weibo.com/schema/motan http://api.weibo.com/schema/motan.xsd"> | ||
服务提供者加载 server.xml 的代码如下:
复制代码
| import org.springframework.context.ApplicationContext; | import org.springframework.context.ApplicationContext; | ||
|---|---|---|---|
| import org.springframework.context.support.ClassPathXmlApplicationContext; | import org.springframework.context.support.ClassPathXmlApplicationContext; | ||
| public class Server { | public class Server { | ||
| public static void main(String[] args) throws InterruptedException { | public static void main(String[] args) throws InterruptedException { | ||
| ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“classpath:motan_server.xml”); | ApplicationContext applicationContext = new ClassPathXmlApplicationContext(“classpath:motan_server.xml”); | ||
| System.out.println(“server start…”); | System.out.println(“server start…”); | ||
| } | } | ||
| } | } |
服务消费者要想调用服务,就必须在进程启动时,加载配置 client.xml,引用接口定义,然后发起调用。
client.xml 配置如下:
复制代码
| <?xml version=”1.0” encoding=”UTF-8”?> | <?xml version=”1.0” encoding=”UTF-8”?> | ||
|---|---|---|---|
| <beans xmlns=”http://www.springframework.org/schema/beans“ | <beans xmlns=”http://www.springframework.org/schema/beans“ | ||
| xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“ | xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance“ | ||
| xmlns:motan=”http://api.weibo.com/schema/motan“ | 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 | 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"> | http://api.weibo.com/schema/motan http://api.weibo.com/schema/motan.xsd"> | ||
服务消费者启动时,加载 client.xml 的代码如下。
复制代码
| import org.springframework.context.ApplicationContext; | import org.springframework.context.ApplicationContext; | ||
|---|---|---|---|
| import org.springframework.context.support.ClassPathXmlApplicationContext; | import org.springframework.context.support.ClassPathXmlApplicationContext; | ||
| public class Client { | public class Client { | ||
| public static void main(String[] args) throws InterruptedException { | public static void main(String[] args) throws InterruptedException { | ||
| ApplicationContext ctx = new ClassPathXmlApplicationContext(“classpath:motan_client.xml”); | ApplicationContext ctx = new ClassPathXmlApplicationContext(“classpath:motan_client.xml”); | ||
| FooService service = (FooService) ctx.getBean(“remoteService”); | FooService service = (FooService) ctx.getBean(“remoteService”); | ||
| System.out.println(service.hello(“motan”)); | 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. | // The greeter service definition. | ||
|---|---|---|---|
| service Greeter { | service Greeter { | ||
| // Sends a greeting | // Sends a greeting | ||
| rpc SayHello (HelloRequest) returns (HelloReply) {} | rpc SayHello (HelloRequest) returns (HelloReply) {} | ||
| rpc SayHelloAgain (HelloRequest) returns (HelloReply) {} | rpc SayHelloAgain (HelloRequest) returns (HelloReply) {} | ||
| } | } | ||
| // The request message containing the user’s name. | // The request message containing the user’s name. | ||
| message HelloRequest { | message HelloRequest { | ||
| string name = 1; | string name = 1; | ||
| } | } | ||
| // The response message containing the greetings | // The response message containing the greetings | ||
| message HelloReply { | message HelloReply { | ||
| string message = 1; | string message = 1; | ||
| } | } |
假如服务提供者使用的是 Java 语言,那么利用 protoc 插件即可自动生成 Server 端的 Java 代码。
复制代码
| private class GreeterImpl extends GreeterGrpc.GreeterImplBase { | private class GreeterImpl extends GreeterGrpc.GreeterImplBase { | ||
|---|---|---|---|
| @Override | @Override | ||
| public void sayHello(HelloRequest req, StreamObserver |
public void sayHello(HelloRequest req, StreamObserver |
||
| HelloReply reply = HelloReply.newBuilder().setMessage(“Hello “ + req.getName()).build(); | HelloReply reply = HelloReply.newBuilder().setMessage(“Hello “ + req.getName()).build(); | ||
| responseObserver.onNext(reply); | responseObserver.onNext(reply); | ||
| responseObserver.onCompleted(); | responseObserver.onCompleted(); | ||
| } | } | ||
| @Override | @Override | ||
| public void sayHelloAgain(HelloRequest req, StreamObserver |
public void sayHelloAgain(HelloRequest req, StreamObserver |
||
| HelloReply reply = HelloReply.newBuilder().setMessage(“Hello again “ + req.getName()).build(); | HelloReply reply = HelloReply.newBuilder().setMessage(“Hello again “ + req.getName()).build(); | ||
| responseObserver.onNext(reply); | responseObserver.onNext(reply); | ||
| responseObserver.onCompleted(); | responseObserver.onCompleted(); | ||
| } | } | ||
| } | } |
假如服务消费者使用的也是 Java 语言,那么利用 protoc 插件即可自动生成 Client 端的 Java 代码。
复制代码
| public void greet(String name) { | public void greet(String name) { | ||
|---|---|---|---|
| logger.info(“Will try to greet “ + name + “ …”); | logger.info(“Will try to greet “ + name + “ …”); | ||
| HelloRequest request = HelloRequest.newBuilder().setName(name).build(); | HelloRequest request = HelloRequest.newBuilder().setName(name).build(); | ||
| HelloReply response; | HelloReply response; | ||
| try { | try { | ||
| response = blockingStub.sayHello(request); | response = blockingStub.sayHello(request); | ||
| } catch (StatusRuntimeException e) { | } catch (StatusRuntimeException e) { | ||
| logger.log(Level.WARNING, “RPC failed: {0}”, e.getStatus()); | logger.log(Level.WARNING, “RPC failed: {0}”, e.getStatus()); | ||
| return; | return; | ||
| } | } | ||
| logger.info(“Greeting: “ + response.getMessage()); | logger.info(“Greeting: “ + response.getMessage()); | ||
| try { | try { | ||
| response = blockingStub.sayHelloAgain(request); | response = blockingStub.sayHelloAgain(request); | ||
| } catch (StatusRuntimeException e) { | } catch (StatusRuntimeException e) { | ||
| logger.log(Level.WARNING, “RPC failed: {0}”, e.getStatus()); | logger.log(Level.WARNING, “RPC failed: {0}”, e.getStatus()); | ||
| return; | return; | ||
| } | } | ||
| logger.info(“Greeting: “ + response.getMessage()); | logger.info(“Greeting: “ + response.getMessage()); | ||
| } | } |
假如服务消费者使用的是 PHP 语言,那么利用 protoc 插件即可自动生成 Client 端的 PHP 代码。
复制代码
| $request = new Helloworld\HelloRequest(); | $request = new Helloworld\HelloRequest(); | ||
|---|---|---|---|
| $request->setName($name); | $request->setName($name); | ||
| list($reply, $status) = $client->SayHello($request)->wait(); | list($reply, $status) = $client->SayHello($request)->wait(); | ||
| $message = $reply->getMessage(); | $message = $reply->getMessage(); | ||
| list($reply, $status) = $client->SayHelloAgain($request)->wait(); | list($reply, $status) = $client->SayHelloAgain($request)->wait(); | ||
| $message = $reply->getMessage(); | $message = $reply->getMessage(); |
由此可见,gRPC 协议的服务描述是通过 proto 文件来定义接口的,然后再使用 protoc 来生成不同语言平台的客户端和服务端代码,从而具备跨语言服务调用能力。
有一点特别需要注意的是,在描述接口定义时,IDL 文件需要对接口返回值进行详细定义。如果接口返回值的字段比较多,并且经常变化时,采用 IDL 文件方式的接口定义就不太合适了。一方面可能会造成 IDL 文件过大难以维护,另一方面只要 IDL 文件中定义的接口返回值有变更,都需要同步所有的服务消费者都更新,管理成本就太高了。
我在项目实践过程中,曾经考虑过采用 Protobuf 文件来描述微博内容接口,但微博内容返回的字段有几百个,并且有些字段不固定,返回什么字段是业务方自定义的,这种情况采用 Protobuf 文件来描述的话会十分麻烦,所以最终不得不放弃这种方式。
