technology-note/java/springcloud实战/2.springCloud服务发现.md
2019-02-28 13:58:17 +08:00

453 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
id: "2018-11-22-15-57"
date: "2018/11/22 15:57"
title: "springCloud学习2(服务发现)"
tags: ["spring-boot", "spring-cloud","eureka"]
categories:
- "java"
- "springCloud实战"
---
**本篇原创发布于:**[FleyX 的个人博客](http://tapme.top/blog/detail/2018-11-22-15-57)
本篇代码存放于:[github](https://github.com/FleyX/demo-project/tree/master/springcloud/spring-cloud%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0)
# 一、服务发现架构
  服务发现架构通常具有下面 4 个概念:
1. 服务注册:服务如何使用服务发现代理进行注册?
2. 服务地址的客户端查找:服务客户端查找服务信息的方法是什么?
3. 信息共享:如何跨节点共享服务信息?
4. 健康监测:服务如何将它的健康信息传回给服务发现代理?
下图展示了这 4 个概念的流程,以及在服务发现模式实现中通常发生的情况:
![服务发现架构](https://raw.githubusercontent.com/FleyX/files/master/blogImg/springcloud%E5%AE%9E%E6%88%98/20190107102247.png)
  通常服务实例都只向一个服务发现实例注册,服务发现实例之间再通过数据传输,让每个服务实例注册到所有的服务发现实例中。
  服务在向服务发现实例注册后,这个服务就能被服务消费者调用了。服务消费者可以使用多种模型来"发现"服务。
1. 每次调用服务时,通过服务发现层来获取目标服务地址并进行调用。这种用的比较少,弊端较多。首先是每次服务调用都通过服务发现层来完成,耗时会比直接调用高。最主要的是这种方法很脆弱,消费端完全依赖于服务发现层来查找和调用服务。
2. 更健壮的方法是使用所谓的客户端负载均衡。
  如下图所示:
![客户端负载均衡](https://raw.githubusercontent.com/FleyX/files/master/blogImg/springcloud%E5%AE%9E%E6%88%98/20190114104818.png)
  在这个模型中,当服务消费者需要调用一个服务时:
  (1)联系服务发现层,获取所请求服务的所有服务实例,然后放到本地缓存中。
  (2)每次调用该服务时,服务消费者从缓存中取出一个服务实例的位置,通常这个'取出'使用简单的复制均衡算法,如“轮询”,“随机",以确保服务调用分布在所有实例之间。
  (3)客户端将定期与服务发现层进行通信,并刷新服务实例的缓存。
  (4)如果在调用服务的过程中,服务调用失败,那么本地缓存将从服务发现层中刷新数据,再次尝试。
<!-- more -->
# 二、spring cloud 实战
&emsp;&emsp;使用 spring cloud 和 Netflix Eureka 搭建服务发现实例。
## 1、构建 Spring Eureka 服务
&emsp;&emsp;eurekasvr POM 主要配置如下:
```xml
<!-- 其他依赖省略 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
```
&emsp;&emsp;applicaiton.yml 配置如下:
```yaml
server:
port: 8761
eureka:
client:
#不注册自己
register-with-eureka: false
#不在本地缓存注册表信息
fetch-registry: false
server:
#接受请求前的等待实际,开发模式下不要开启
#wait-time-in-ms-when-sync-empty: 5
```
&emsp;&emsp;最后在启动类上加入注释`@SpringBootApplication`即可启动服务中心。服务中心管理页面:[http://localhost:8761](http://localhost:8761)
## 2、将服务注册到服务中心
&emsp;&emsp;这里我们编写一个新服务注册到服务中心,organizationservice组织服务。并将上一篇的两个服务confsvr:配置中心服务,licensingservice:授权服务注册到服务中心。
### a、confvr 注册
&emsp;&emsp;首先修改 POM 文件:
```XML
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
```
&emsp;&emsp;然后修改配置文件 application.yml:
```yaml
server:
port: 8888
eureka:
instance:
#注册服务的IP而不是服务器名
prefer-ip-address: true
client:
#向eureka注册服务
register-with-eureka: true
#拉取注册表的本地副本
fetch-registry: true
service-url:
#Eureka服务的位置(如果有多个注册中心,使用,分隔)
defaultZone: http://localhost:8761/eureka/
spring:
profiles:
# 使用文件系统来存储配置信息需要设置为native
active: native
application:
name: confsvr
cloud:
config:
server:
native:
# 使用文件来存放配置文件,为每个应用程序提供用逗号分隔的文件夹列表
searchLocations: file:///D:/configFolder/licensingservice,file:///D:/configFolder/organizationservice
```
&emsp;&emsp;最后在启动类加入注解`@EnableDiscoveryClient`,启动即可在 eureka 管理页面发现。
### b、licensingservice 注册
&emsp;&emsp;首先修改 POM
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
```
&emsp;&emsp;然后修改配置文件 bootstrap.yml
```yaml
spring:
application:
#指定名称以便spring cloud config客户端知道查找哪个配置
name: licensingservice
profiles:
#指定环境
active: dev
cloud:
config:
#设为true便会自动获取从配置中心获取配置文件
enabled: true
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
```
&emsp;&emsp;最后在启动类加入注解`@EnableDiscoveryClient`,启动即可在 eureka 管理页面发现本服务实例。
### c、创建 organizationservice
&emsp;&emsp;首先在文件夹**file:///D:/configFolder/organizationservice**下创建两个配置文件organizationservice.yml,organizationservice-dev.yml,内容分别为:
```yaml
#organizationservice-dev.yml
server:
port: 10012
```
```yaml
#organizationservice.yml
spring:
application:
name: organizationservice
```
&emsp;&emsp;主要 POM 配置如下:
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
```
&emsp;&emsp;然后修改配置文件bootstrap.yml
```yaml
spring:
application:
#指定名称以便spring cloud config客户端知道查找哪个配置
name: organizationservice
profiles:
#指定环境
active: dev
cloud:
config:
enabled: true
eureka:
instance:
prefer-ip-address: true
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:8761/eureka/
```
&emsp;&emsp;最后在启动类加入注解`@EnableDiscoveryClient`,启动。
## 3、使用服务发现来查找服务
&emsp;&emsp;现在已经有两个注册服务了,现在来让许可证服务调用组织服务,获取组织信息。首先在 organizationservice 服务中的 controller 包中加入一个 controller 类,让它能够响应请求:
```java
//OrganizationController.java
@RestController
public class OrganizationController {
@GetMapping(value = "/organization/{orgId}")
public Object getOrganizationInfo(@PathVariable("orgId") String orgId) {
Map<String, String> data = new HashMap<>(2);
data.put("id", orgId);
data.put("name", orgId + "公司");
return data;
}
}
```
&emsp;&emsp;接下来让许可证服务通过 Eureka 来找到组织服务的实际位置,然后调用该接口。为了达成目的,我们将要学习使用 3 个不同的 Spring/Netflix 客户端库,服务消费者可以使用它们来和 Ribbon 进行交互。从最低级别到最高级别,这些库包含了不同的与 Ribbon 进行交互的抽象封装层次:
- Spring DiscoveryClient
- 启用了 RestTemplate 的 Spring DiscoveryClient
- Neflix Feign 客户端
### a、使用 Spring DiscoveryClient
&emsp;&emsp;该工具提供了对 Ribbon 和 Ribbon 中缓存的注册服务最低层次的访问,可以查询通过 Eureka 注册的所有服务以及这些服务对应的 URL。
&emsp;&emsp;首先在 licensingservice 的启动类中加入`@EnableDiscoveryClient`注解来启用 DiscoveryClient 和 Ribbon 库。
&emsp;&emsp;然后在 service 包下创建 OrganizationService.java
```java
@Service
public class OrganizationService {
private static final String SERVICE_NAME = "organizationservice";
private DiscoveryClient discoveryClient;
@Autowired
public OrganizationService(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
/**
* 使用Spring DiscoveryClient查询
*
* @param id
* @return
*/
public Organization getOrganization(String id) {
RestTemplate restTemplate = new RestTemplate();
List<ServiceInstance> instances = discoveryClient.getInstances(SERVICE_NAME);
if (instances.size() == 0) {
throw new RuntimeException("无可用的服务");
}
String serviceUri = String.format("%s/organization/%s", instances.get(0).getUri().toString(), id);
ResponseEntity<Organization> responseEntity = restTemplate.exchange(serviceUri, HttpMethod.GET
, null, Organization.class, id);
return responseEntity.getBody();
}
}
```
&emsp;&emsp;接着在 controller 包中新建 LicensingController.java
```java
@RestController
public class LicensingController {
private OrganizationService organizationService;
@Autowired
public LicensingController(OrganizationService organizationService) {
this.organizationService = organizationService;
}
@GetMapping("/licensing/{orgId}")
public Licensing getLicensing(@PathVariable("orgId") String orgId) {
Licensing licensing = new Licensing();
licensing.setValid(false);
licensing.setOrganization(organizationService.getOrganization(orgId));
return licensing;
}
}
```
&emsp;&emsp;启动所有项目,访问[localhost:10011/licensing/12](localhost:10011/licensing/12),可以看到返回如下结果:
```json
{
"organization": {
"id": "12",
"name": "12公司"
},
"valid": false
}
```
&emsp;&emsp;在实际开发中,基本上是用不到这个的,除非是为了查询 Ribbon 以获取某个服务的所有实例信息,才会直接使用。如果直接使用它存在以下两个问题:
1. 没有利用 Ribbon 的客户端负载均衡
2. 和业务无关的代码写得太多
### b、使用带 Ribbon 功能的 Spring RestTemplate 调用服务
&emsp;&emsp;这种方法是较为常用的微服务通信机制之一。要启动该功能,需要使用 Spring Cloud 注解@LoadBanced 来定义 RestTemplate bean 的构造方法。方便起见直接在启动类中定义 bean:
```java
#LicensingserviceApplication.java
@SpringBootApplication
@EnableDiscoveryClient //使用不带Ribbon功能的Spring RestTemplate,其他情况下可删除
public class LicensingserviceApplication {
/**
* 使用带有Ribbon 功能的Spring RestTemplate其他情况可删除
*/
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(LicensingserviceApplication.class, args);
}
}
```
&emsp;&emsp;接着 service 包下增加一个类OrganizationByRibbonService.java
```java
@Component
public class OrganizationByRibbonService {
private RestTemplate restTemplate;
@Autowired
public OrganizationByRibbonService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public Organization getOrganizationWithRibbon(String id) {
ResponseEntity<Organization> responseEntity = restTemplate.exchange("http://organizationservice/organization/{id}",
HttpMethod.GET, null, Organization.class, id);
return responseEntity.getBody();
}
}
```
&emsp;&emsp;最后就是在 LicensingController.js 中加一个访问路径:
```java
//不要忘记注入OrganizationByRibbonService服务
@GetMapping("/licensingByRibbon/{orgId}")
public Licensing getLicensingByRibbon(@PathVariable("orgId") String orgId) {
Licensing licensing = new Licensing();
licensing.setValid(false);
licensing.setOrganization(organizationService.getOrganization(orgId));
return licensing;
}
}
```
&emsp;&emsp;访问[localhost:10011/licensingByRibbon/113](localhost:10011/licensingByRibbon/113),即可看到结果。
### c、使用 Netflix Feign 客户端调用
&emsp;&emsp;Feign 客户端是 Spring 启用 Ribbon 的 RestTemplate 类的替代方案。开发人员只需定义一个接口,然后使用 Spring 注解来标注接口,即可调用目标服务。除了编写接口定义无需编写其他辅助代码。
&emsp;&emsp;首先启动类上加一个`@EnableFeignClients`注解启用 feign 客户端。然后在 POM 中加入 Feign 的依赖
```xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
```
&emsp;&emsp;然后在 client 包下新建 OrganizationFeignClient.java
```java
@FeignClient("organizationservice")//使用FeignClient注解指定目标服务
public interface OrganizationFeignClient {
/**
* 获取组织信息
*
* @param orgId 组织id
* @return Organization
*/
@RequestMapping(method = RequestMethod.GET, value = "/organization/{orgId}", consumes = "application/json")
Organization getOrganization(@PathVariable("orgId") String orgId);
}
```
&emsp;&emsp;最后修改`LicensingController.java`,加入一个路由调用 Feign。
```java
//注入OrganizationFeignClient,使用构造注入
@GetMapping("/licensingByFeign/{orgId}")
public Licensing getLicensingByFeign(@PathVariable("orgId") String orgId) {
Licensing licensing = new Licensing();
licensing.setValid(false);
licensing.setOrganization(organizationFeignClient.getOrganization(orgId));
return licensing;
}
```
访问[localhost:10011/licensingByFeign/11313](localhost:10011/licensingByFeign/11313),即可看到结果。
# 总结
&emsp;&emsp;这一节磨磨蹭蹭写了好几天,虽然例子很简单,但是相信应该是能够看懂的。由于篇幅原因代码没有全部贴上,想要查看完整代码,可以访问这个链接:[点击跳转](https://github.com/FleyX/demo-project/tree/master/springcloud/spring-cloud%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0)。
**本篇原创发布于:**[FleyX 的个人博客](http://tapme.top/blog/detail/2018-11-22-15-57)