喜欢的朋友可以关注下专栏: Java 架构技术进阶. 里面有大量 batj 面试题集锦, 还有各种技术分享, 如有好文章也欢迎投稿哦.
在本期将学习以下知识点:
什么是服务注册和发现?
基于 Eureka 的注册服务器
服务生产者
结合 Ribbon 服务消费者
结合 Feign 的服务生产者和消费者
什么是服务注册和发现
假设有 2 个微服务 A 和 B 分别在端点 http:// localhost:8181 / 和 http:// localhost:8282 / 上运行, 如果想要在 A 服务中调用 B 服务, 那么我们需要在 A 服务中键入 B 服务的 url, 这个 url 是负载均衡器分配给我们的, 包括负载平衡后的 IP 地址, 那么很显然, B 服务与这个 URL 硬编码耦合在一起了, 如果我们使用了服务自动注册机制, 就可以使用 B 服务的逻辑 ID, 而不是使用特定 IP 地址和端口号来调用服务.
我们可以使用 Netflix Eureka Server 创建 Service Registry 服务器, 并将我们的微服务同时作为 Eureka 客户端, 这样一旦我们启动微服务, 它将自动使用逻辑服务 ID 向 Eureka Server 注册. 然后, 其他微服务 (同样也是 Eureka 客户端) 就可以使用服逻辑务 ID 来调用 REST 端点服务了.
Spring Cloud 使用 Load Balanced RestTemplate 创建 Service Registry 并发现其他服务变得非常容易.
除了使用 Netflix Eureka Server 作为服务发现, 也可以使用 Zookeeper, 但是根据 CAP 定理, 在需要 P 网络分区容忍性情况下, 强一致性 C 和高可用性 A 只能选择一个, Zookeeper 是属于 CP, 而 Eureka 是属于 AP, 在服务发现方面, 高可用性才是更重要, 否则无法完成服务之间调用, 而服务信息是否一致则不是最重要, A 服务发现 B 服务时, B 服务信息没有及时更新, 可能发生调用错误, 但是调用错误总比无法连接到服务注册中心要强. 否则, 服务注册中心就成为整个系统的单点故障, 存在极大的单点风险, 这是我们为什么需要分布式系统的首要原因.
基于 Eureka 的注册服务器
让我们使用 Netflix Eureka 创建一个 Service Registry, 它只是一个带有 Eureka Server 启动器的 SpringBoot 应用程序.
使用 Intellij 的 Idea 开发工具是非常容易启动 Spring cloud 的:
可以从 https://start.spring.io / 网址, 选择相应组件即可.
由于我们需要建立一个注册服务器, 因此选择 Eureka Server 组件即可, 通过这些自动工具实际上是能自动生成 Maven 的配置:
- <dependency>
- <groupId>
- org.springframework.cloud
- </groupId>
- <artifactId>
- spring-cloud-starter-netflix-eureka-server
- </artifactId>
- </dependency>
我们需要给 SpringBoot 启动类添加 @EnableEurekaServer 注释, 以使我们的 SpringBoot 应用程序成为基于 Eureka Server 的 Service Registry.
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
- @EnableEurekaServer
- @SpringBootApplication
- public class ServiceRegistryApplication {
- public static void main(String[] args) {
- SpringApplication.run(ServiceRegistryApplication.class, args);
- }
- }
默认情况下, 每个 Eureka 服务器也是 Eureka 客户端, 客户端一定会需要一个服务器 URL 来定位, 否则就会不断报错, 由于我们只有一个 Eureka Server 节点(独立模式), 我们将通过在 application.properties 文件中配置以下属性来禁用此客户端行为.
SpringCloud 有 properties 和 YAML 两种配置方式, 这两种配置方式其实只是形式不同, properties 配置信息格式是 a.b.c, 而 YAML 则是 a:b:c:, 两者本质是一样的, 只需要其中一个即可, 这里以 properties 为案例:
- spring.application.name=jdon-eureka-server
- server.port=1111
- eureka.instance.hostname=localhost
- eureka.client.register-with-eureka=false
- eureka.client.fetch-registry=false
现在运行 ServiceRegistryApplication 并访问 http:// localhost:1111, 如果不能访问, 说明没有正常启动, 请检查三个环节: pom.xml 是否配置正确? 需要 Eureka 和配置
SpringBoot 的注释 @EnableEurekaServer 是否增加了?
最后, application.properties 是否配置?
SpringCloud 其实非常简单, 约定大于配置, 默认只要配置服务器端口就可以了, 然后是一条注释 @EnableEurekaServer, 就能启动 Eurek 服务器了.
服务器准备好后, 我们就要准备服务生产者, 向服务器里面注册自己, 服务消费者则是从服务器中发现注册的服务然后调用.
服务生产者
服务生产者其实首先是 Eureka 的客户端, 生产者将自己注册到前面启动的服务器当中, 引如果是 idea 的导航, 选择 CloudDiscovery 的 EurekaDiscovery, 如果是 Maven 则引入包依赖是:
- <dependency>
- <groupId>
- org.springframework.cloud
- </groupId>
- <artifactId>
- spring-cloud-starter-netflix-eureka-client
- </artifactId>
- </dependency>
这样, spring-cloud-starter-netflix-eureka-client 这个 jar 包就放入我们系统的 classpath, 为了能够正常使用这个 jar 包, 还需要配置, 只需要在 application.properties 中配置 eureka.client.service-url.defaultZone 属性即可自动注册 Eureka Server:
eureka.client.service-url.defaultZone=http://localhost:1111/eureka/
当我们的服务在 Eureka Server 注册时, 它会持续发送一定时间间隔的心跳. 如果 Eureka 服务器没有从任何服务的实例接收到心跳, 它将认为这个服务实例已经关闭并从自己的池中剔除它.
以上是服务生产者注册服务的过程, 比较简单, 为了使我们的服务生产者能的演示代码够运行起来, 我们还需要新建一个服务生产者代码:
- @RestController
- public class ProducerService {
- @GetMapping("/pengproducer")
- public String sayHello(){
- return "hello world";
- }
- }
这段代码是将服务暴露成 RESTful 接口,@RestController 是声明 REST 接口,/pengproducer 是 REST 的访问 url, 通过 get 方式能够获得字符串: hello world
因为 REST 属于 web 的一种接口, 因此需要在 pom.xml 中引入 Web 包:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-Web</artifactId>
- </dependency>
然后在 application.properties 中加入有关 REST 接口的配置:
- spring.application.name=PengProducerService
- server.port=2111
指定我们的生产者服务的名称是 PengProducerService,REST 端口开在 2111.
现在可以在 idea 中启动我们的应用了, 这样我们启动这个项目, 就可以在 http://127.0.0.1:2111/ 访问这个 REST 服务. 同时, 因为我们之前已经启动了注册服务器, 访问 http://localhost:1111 / 你会发现 PengProducerService 出现在服务列表中:
上面启动应用服务是在 idea 编辑器中, 我们还可以通过命令行启动我们的服务生产者:
java -jar -Dserver.port=2112 producer-0.0.1-SNAPSHOT.jar
这个是在端口 2112 开启我们的服务端点了. 现在再问 http://localhost:1111/, 你会看到可用节点 Availability Zones 下面已经从 (1) 变为(2), 现在我们的服务生产者已经有两个实例在运行, 当服务的消费者访问这个两个实例时, 它可以根据负载平衡策略比如轮询访问其中一个服务生产者实例.
总结一下, 为了让服务生产者注册到 Euraka 服务器中, 只需要两个步骤:
1. 引入 spring-cloud-starter-netflix-eureka-client 包
2. 配置 Eurake 服务器的地址
请注意, spring-cloud-starter-netflix-eureka-client 包是 Spring Cloud 升级后最新的包名, 原来是 spring-cloud-starter-eureka, 里面没有 netflix, 这是过去版本, Spring Boot 1.5 以后都是加入了 netflix 的, 见 Spring Cloud Edgware Release Notes
另外, 这里不需要在 SpringBoot 主代码中再加入 @enablediscoveryclient 或 @enableeurekaclient, 只要 eureka 的 client 包在 maven 中配置, 也就会出现在系统的 classpath 中, 这样就会默认自动注册到 eureka 服务器中了.
这部分源码下载: 百度网盘.
下面我们准备访问这个服务生产者 PengProducerService 的消费者服务:
结合 Ribbon 的服务消费者
上个章节我们已经启动了两个服务生产者实例, 如何通过负载平衡从两个中选择一个访问呢? 这时就需要 Ribbon, 为了使用 Ribbon, 我们需要使用 @LoadBalanced 元注解, 那么这个注解放在哪里呢? 一般有两个 DiscoveryClient 和 RestTemplate, 这两个的区别是:
1. DiscoveryClient 可以获得服务提供者 (生产者) 的多个实例集合, 能让你手工决定选择哪个实例, 这里负载平衡的策略比如 round robin 轮询就不会派上, 实际就没有使用 Ribbon:
- List<ServiceInstance> instances=discoveryClient.getInstances("PengProducerService");
- ServiceInstance serviceInstance=instances.get(0);
2.RestTemplate 则是使用 Ribbon 的负载平衡策略, 使用 @LoadBalanced 注释 resttemplate 并使用 zuul 代理服务器作为边缘服务器. 那么对 zuul 边缘服务器的任何请求将默认使用 Ribbon 进行负载平衡, 而 resttemplate 将以循环方式路由请求. 这部分代码如下:
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.ResponseEntity;
- import org.springframework.stereotype.Controller;
- import org.springframework.Web.client.RestTemplate;
- @Controller
- public class ConsumerService {
- @Autowired
- private RestTemplate restTemplate;
- public String callProducer() {
- ResponseEntity<String> result =
- this.restTemplate.getForEntity(
- "http://PengProducerService/pengproducer",
- String.class,
- "");
- if (result.getStatusCode() == HttpStatus.OK) {
- System.out.printf(result.getBody() + "called in callProducer");
- return result.getBody();
- } else {
- System.out.printf("is it empty");
- return "empty";
- }
- }
- }
RestTemplate 是自动注射进这个控制器, 在这控制器, 我们调用了服务生产者 http://PengProducerService/pengproducer, 然后获得其结构.
这个控制器的调用我们可以在 SpringBoot 启动函数里调用:
- @SpringBootApplication
- public class ConsumerApplication {
- @Bean
- @LoadBalanced
- public RestTemplate restTemplate() {
- return new RestTemplate();
- }
- public static void main(String[] args) {
- ApplicationContext ctx = SpringApplication.run(ConsumerApplication
- .class, args);
- ConsumerService consumerService = ctx.getBean(ConsumerService.class);
- System.out.printf("final result RestTemplate=" + consumerService
- .callProducer() + "\n");
- }
- }
注意到 @LoadBalanced 是标注在 RestTemplate 上, 而 RestTemplate 是被注入到 ConsumerService 中的, 这样通过调用 RestTemplate 对象实际就是获得负载平衡后的服务实例. 这个可以通过我们的服务提供者里面输出 hashcode 来分辨出来, 启动两个服务提供者实例, 每次运行 ConsumerService, 应该是依次打印出不同的 hashcode:
hello world1246528978 called in callProducerfinal result RestTemplate=hello world1246528978
再次运行结果:
hello world1179769159 called in callProducerfinal result RestTemplate=hello world1179769159
hellow world 后面的哈希值不同, 可见是来自不同的服务提供者实例.
如果系统基于 https 进行负载平衡, 那么只需要两个步骤:
1.application.properties 中激活 ribbon 的 https:
ribbon.IsSecure=true
2. 代码中 RestTemplate 初始化时传入 ClientHttpRequestFactory 对象:
- @Bean
- @LoadBalanced
- public RestTemplate restTemplate() {
- CloseableHttpClient httpClient = HttpClientUtil.getHttpClient();
- HttpComponentsClientHttpRequestFactory clientrequestFactory = new HttpComponentsClientHttpRequestFactory();
- clientrequestFactory.setHttpClient(httpClient);
- RestTemplate restTemplate = new RestTemplate(clientrequestFactory);
- return restTemplate;
- }
这部分源码下载: 百度网盘
结合 Feign 的服务生产者和消费者
上篇是使用 Ribbon 实现对多个服务生产者实例使用负载平衡的方式进行消费, 在调用服务生产者时, 返回的是字符串类型, 如果返回是各种自己定义的对象, 这些对象传递到消费端是通过 JSON 方式, 那么我们的消费者需要使用 Feign 来访问各种 JSON 对象.
需要注意的是: Feign = Eureka +Ribbon + RestTemplate, 也就是说, 使用 Feign 访问服务生产者, 无需前面章节那么关于负载平衡的代码了, 前面我们使用 RestTemplate 进行负载平衡访问, 代码还是挺复杂
现在我们开始 Feign 的实现: 首先我们在服务的生产者那边进行修改, 让我们生产者项目变得接近实战中项目, 增加领域层, 服务层和持久层.
假设新增 Article 领域模型对象, 我们就需要仓储保存, 这里我们使用 Spring 默认约定, 使用 JPA 访问 h2 数据库, 将 Article 通过 JPA 保存到 h2 数据库中:
要启用 JPA 和 h2 数据库, 首先只要配置 pom.xml:
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-jpa</artifactId>
- </dependency>
- <dependency>
- <groupId>com.h2database</groupId>
- <artifactId>h2</artifactId>
- <scope>runtime</scope>
- </dependency>
Article 领域模型对象作为需要持久的实体对象: 配置实体 @Entity 和 @Id 主键即可:
- @Entity
- public class Article {
- @Id
- @GeneratedValue(strategy = GenerationType.AUTO)
- private Long id;
- private String title;
- private String body;
- private Date startDate;
然后我们建立一个空的 Article 仓储接口即可:
- @Repository
- public interface ArticleRep extends JpaRepository<Article,Long> {
- }
这样, 关于 Article 的 CRUD 实现就已经有了, 不需要自己再编写任何 SQL 语句. 这样我们编写一个 Service 就可以提供 Article 对象的 CRUD 方法, 这里只编写插入和查询批量两个方法:
- @Service
- public class ArticleService {
- @Autowired
- ArticleRep articleRep;
- public List<Article> getAllArticles(){
- return articleRep.findAll();
- }
- public void insertArticle(Article article){
- articleRep.save(article);
- }
- }
我们在 REST 接口中暴露这两种方法:
1. get /articles 是查询所有对象
2. post /article 是新增
- @RestController
- public class ProducerService {
- @Autowired
- ArticleService articleService;
- @GetMapping("/articles")
- public List<Article> getAllArticles(){
- return articleService.getAllArticles();
- }
- @GetMapping("/article")
- public void publishArticle(@RequestBody Article article){
- articleService.insertArticle(article);
- }
上面服务的生产者提供了两个 REST url, 我们在消费者这边使用 / articles 以获得所有文章:
- @FeignClient(name="PengProducerService")
- public interface ConsumerService {
- @GetMapping("/articles")
- List<Article> getAllArticles();
- }
这是我们消费者的服务, 调用生产者 /articles, 这是一个接口, 无需实现, 注意需要标注 FeignClient, 其中写入 name 或 value 微服务生产者的 application.properties 配置:
spring.application.name=PengProducerService
当然, 这里会直接耦合 PengProducerService 这个名称, 我们以后可以通过配置服务器更改, 这是后话.
然后需要在应用 Application 代码加入 @EnableFeignClients:
- @SpringBootApplication
- @EnableFeignClients
- public class FeignconsumerApplication {
- public static void main(String[] args) {
- ApplicationContext context = SpringApplication.run(FeignconsumerApplication
- .class, args);
- ConsumerService consumerService = context.getBean(ConsumerService
- .class);
- System.out.printf("#############all articles ok" + consumerService
- .getAllArticles());
- }
在 FeignconsumerApplication 我们调用了前面接口 ConsumerService, 而 ConsumerService 则通过负载平衡调用另外一个生产者微服务, 如果我们给那个生产者服务加入一些 Articles 数据, 则这里就能返回这些数据:
#############all articles ok[com.example.feignconsumer.domain.Article@62b475e2, com.example.feignconsumer.domain.Article@e9474f]
说明调用成功.
在调试过程中, 曾经出现错误:
Load balancer does not have available server for client:PengProducerService
经常排查是由于生产者项目中 pom.xml 导入的是 spring-cloud-starter-netflix-eureka-client, 改为 pring-cloud-starter-netflix-eureka-server 就可以了, 这是 SpringBoot 2.0 发现的一个问题.
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
- </dependency>
本章的代码下载: 百度网盘
总结
通过这个项目学习, 我们如同蚕丝剥茧层层搞清楚了 Spring Cloud 的微服务之间同步调用方式, 发现基于 REST/JSON 的调用代码最少, 也是最方便, Feign 封装了 Ribbon 负载平衡和 Eureka 服务器访问以及 REST 格式处理.
喜欢的朋友可以关注下专栏: Java 架构技术进阶. 里面有大量 batj 面试题集锦, 还有各种技术分享, 如有好文章也欢迎投稿哦.
如果你对技术提升很感兴趣, 欢迎 1~5 年的工程师可以加入我的 Java 进阶之路来交流学习: 908676731. 里面都是同行, 有资源共享, 还有大量面试题以及解析. 欢迎一到五年的工程师加入, 合理利用自己每一分每一秒的时间来学习提升自己, 不要再用 "没有时间" 来掩饰自己思想上的懒惰! 趁年轻, 使劲拼, 给未来的自己一个交代!
来源: http://www.jianshu.com/p/ad02e8051303