由于Spring Cloud基于Spring Boot构建,而Spring Cloud Alibaba又基于Spring Cloud Common的规范实现,所以当我们使用Spring Cloud Alibaba来构建微服务应用的时候,需要知道这三者之间的版本关系。
目前Spring Cloud Alibaba的版本与Spring Boot、Spring Cloud版本的兼容关系:
Spring Boot | Spring Cloud | Spring Cloud Alibaba |
---|---|---|
2.1.x | Greenwich | 0.9.x |
2.0.x | Finchley | 0.2.x |
1.5.x | Edgware | 0.1.x |
1.5.x | Dalston | 0.1.x |
以上版本对应内容根据当前情况实时调整修改,以方便用户了解他们的对应关系变化情况
YML配置文件缺陷
注意:YAML目前还有一些不足,它无法通过
@PropertySource
注解来加载配置。但是,YAML加载属性到内存中保存的时候是有序的,所以当配置文件中的信息需要具备顺序含义时,YAML的配置方式比起properties配置文件更有优势。
@Value
注解加载属性值的时候可以支持两种表达式来进行配置:
- 一种是我们上面介绍的PlaceHolder方式,格式为
${...}
,大括号内为PlaceHolder - 另外还可以使用SpEL表达式(Spring Expression Language), 格式为
#{...}
,大括号内为SpEL表达式
单体架构缺陷
eg:当访问http://127.0.0.1:8080/products时页面展示为(视图服务)
id | 名称 | 价格 |
---|---|---|
1 | 苹果 | 12.8 |
2 | 香蕉 | 12.5 |
3 | 橘子 | 12.2 |
product-service
代码实现为(数据服务):
controller:
return productService.listProducts();
service:
return productMapper.listProducts();
public class Product {
private int id;
private String name;
private int price;
}
这个项目很简单,就做两件事: 1. 提供数据 2. 展示数据。 这就是一个典型的单体结构。
它把两个服务 提供数据和展示数据 放在了一起,这就会出现固有的缺点。
- 如果要修改数据部分的代码, 那么必须把整个项目重新编译打包部署。 虽然展示部分,什么都没变但是也会因为重新部署而暂时不能使用,要部署完了,才能使用。
- 如果提供数据部分出现了问题,比如抛出了异常,会导致整个项目不能使用,展示数据部分也因此受到影响。
- 性能瓶颈难以突破
以上就是单体结构的问题,接下来把这个单体结构的项目,改造成为springcloud 微服务分布式架构,通过观察和参与改造过程,掌握和理解 springcloud。
接下来,通过分布式和集群的思路,对其改造。
微服务介绍
微服务概念
简单来说,一个 springboot 就是一个 微服务,并且这个 springboot 做的事情很单纯。 比如
product-service
这个项目,就可以拆成两个微服务,分别是 数据微服务,和视图微服务,其实就是俩 springboot, 只是各自做的事情都更单纯~
服务注册
注册中心可以使用:Eureka、Nacos…
有了微服务,就存在如何管理这个微服务,以及这两个微服务之间如何通信的问题,所以就要引入一个 微服务注册中心概念,这个微服务注册中心在 springcloud 里就叫做 eureka server, 通过它把就可以把微服务注册起来,以供将来调用。
服务访问
在业务逻辑上, 视图微服务 需要 数据微服务 的数据,所以就存在一个微服务访问另一个微服务的需要。
而这俩微服务已经被注册中心管理起来了,所以 视图微服务 就可以通过 注册中心定位并访问 数据微服务了。
分布式概念
按所述步骤改造后,就是分布式啦~ 简单说,原来是在一个 springboot里就完成的事情,现在分布在多个 springboot里做,这就是初步具备 分布式雏形了。
那么分布式有什么好处呢?
- 如果我要更新数据微服务,视图微服务是不受影响的
- 可以让不同的团队开发不同的微服务,他们之间只要约定好接口,彼此之间是低耦合的。
- 如果视图微服务挂了,数据微服务依然可以继续使用等等
服务集群
原来数据微服务只有这一个springboot, 现在做同样数据微服务的,有两个 springboot, 他们提供的功能一模一样,只是端口不一样,这样就形成了集群。
那么集群有什么好处呢?
- 比起一个 springboot, 两个springboot 可以分别部署在两个不同的机器上,那么理论上来说,能够承受的负载就是 x 2. 这样系统就具备通过横向扩展而提高性能的机制。
- 如果 8001 挂了,还有 8002 继续提供微服务,这就叫做高可用 。
分布式和集群周边服务
以上是很简单的分布式结构,围绕这个结构,我们有时候还需要做如下事情:
- 那些微服务是如何彼此调用的? sleuth 服务链路追踪
- 如何在微服务间共享配置信息?配置服务 Config Server
- 如何让配置信息在多个微服务之间自动刷新? RabbitMQ 总线 Bus
- 如果数据微服务集群都不能使用了, 视图微服务如何去处理? 断路器 Hystrix
- 视图微服务的断路器什么时候开启了?什么时候关闭了? 断路器监控 Hystrix Dashboard
- 如果视图微服务本身是个集群,那么如何进行对他们进行聚合监控? 断路器聚合监控 Turbine Hystrix Dashboard
- 如何不暴露微服务名称,并提供服务? Zuul 网关
搭建项目环境
项目目录
springcloud(父模块:删除没用的src目录)
— eureka-server(子模块)
springcloud父模块依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.how2j.springcloud</groupId>
<artifactId>springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springcloud</name>
<description>springcloud</description>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.3.1</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
创建子项目
eureka-server子模块依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.how2j.springcloud</groupId>
<artifactId>springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>eurekaServer</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
EurekaServerApplication(eureka-server子模块启动类)
import cn.hutool.core.util.NetUtil;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
//8761 这个端口是默认的,就不要修改了,后面的子项目,都会访问这个端口。
int port = 8761;
if(!NetUtil.isUsableLocalPort(port)) {
System.err.printf("端口%d被占用了,无法启动%n", port );
System.exit(1);
}
new SpringApplicationBuilder(EurekaServerApplication.class).properties("server.port=" + port).run(args);
}
}
EurekaServer 启动类。
这是一个 EurekaServer ,它扮演的角色是注册中心,用于注册各种微服务,以便于其他微服务找到和访问。
EurekaServer 本身就是个 Springboot 微服务, 所以它有 @SpringBootApplication 注解。
@EnableEurekaServer 表示这是个 EurekaServer 。
application.yml(eureka-server子模块配置信息)
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
spring:
application:
name: eureka-server
配置文件,提供 eureka 的相关信息。
hostname: localhost
表示主机名称。
registerWithEureka:false
. 表示是否注册到服务器。 因为它本身就是服务器,所以就无需把自己注册到服务器了。
fetchRegistry: false
. 表示是否获取服务器的注册信息,和上面同理,这里也设置为 false。
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
自己作为服务器,公布出来的地址。 比如后续某个微服务要把自己注册到 eureka server, 那么就要使用这个地址:http://localhost:8761/eureka/
name: eurka-server
表示这个微服务本身的名称是 eureka-server
启动并访问
运行 EurekaServerApplication,并访问: http://127.0.0.1:8761/
主要看 :Instances currently registered with Eureka, 可以发现信息是:No instances available。
这表示 暂时还没有微服务注册进来。
创建子项目
springcloud(父模块:删除没用的src目录)
— eureka-server(子模块)
— product-data-service(子模块)
product-data-service子模块依赖
修改 pom.xml 为如下:
spring-cloud-starter-netflix-eureka-client 表示这是个 eureka 客户端。
spring-boot-starter-web: 表示这是个web服务,会提供控制层
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.how2j.springcloud</groupId>
<artifactId>springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>productDataservice</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
</dependencies>
</project>
Product(product-data-service子模块实体类)
@Data
public class Product {
private int id;
private String name;
private int price;
}
ProductService(product-data-service子模块数据服务类)
服务类提供一个 Product 集合。
需要注意的是,这里把 端口号 放进了产品信息里。 这个数据服务会做成集群,那么访问者为了分辨到底是从哪个数据微服务取的数据,就需要提供个端口号,才能意识到是从不同的微服务得到的数据。
@Service
public class ProductService {
@Value("${server.port}")
String port;
public List<Product> listProducts(){
List<Product> ps = new ArrayList<>();
ps.add(new Product(1,"product a from port:"+port, 50));
ps.add(new Product(2,"product b from port:"+port, 150));
ps.add(new Product(3,"product c from port:"+port, 250));
return ps;
}
}
ProductController(product-data-service子模块控制器)
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@RequestMapping("/products")
public Object products() {
return productService.listProducts();
}
}
ProductDataServiceApplication(product-data-service子模块启动类)
启动类, 考虑到要做集群。 所以让用户自己输入端口,推荐 8001,8002,8003.
但是每次测试都要输入端口号又很麻烦,所以做了个 Future类,如果5秒不输入,那么就默认使用 8001端口。
@SpringBootApplication
@EnableEurekaClient
public class ProductDataServiceApplication {
public static void main(String[] args) {
int port = 0;
int defaultPort = 8001;
Future<Integer> future = ThreadUtil.execAsync(() ->{
int p = 0;
System.out.println("请于5秒钟内输入端口号, 推荐 8001 、 8002 或者 8003,超过5秒将默认使用 " + defaultPort);
Scanner scanner = new Scanner(System.in);
while(true) {
String strPort = scanner.nextLine();
if(!NumberUtil.isInteger(strPort)) {
System.err.println("只能是数字");
continue;
}
else {
p = Convert.toInt(strPort);
scanner.close();
break;
}
}
return p;
});
try{
port=future.get(5,TimeUnit.SECONDS);
}
catch (InterruptedException | ExecutionException | TimeoutException e){
port = defaultPort;
}
if(!NetUtil.isUsableLocalPort(port)) {
System.err.printf("端口%d被占用了,无法启动%n", port );
System.exit(1);
}
new SpringApplicationBuilder(ProductDataServiceApplication.class).properties("server.port=" + port).run(args);
}
/*@Bean
public Sampler defaultSampler() {
return Sampler.ALWAYS_SAMPLE;
} */
}
application.yml(product-data-service子模块配置信息)
设置微服务的名称: product-data-service
设置注册中心的地址: http://localhost:8761/eureka/ , 与 eureka-server 中的配置 application.yml 遥相呼应
# server:
# port: 因为会启动多个 product-data-service, 所以端口号由用户自动设置,推荐 8001,8002,8003
spring:
application:
name: product-data-service
zipkin:
base-url: http://localhost:9411
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
启动两个 微服务
启动两次 ProductDataServiceApplication, 分别输入 8001和8002.
可以在注册中心 http://127.0.0.1:8761/ 看到, product-data-service 这个微服务,有两个实例,分别是8001和8002端口。
服务访问
可以如此访问: http://127.0.0.1:8001/products ,http://127.0.0.1:8002/products,并看到如图所示的数据。
但是这种方式是通过 http 协议 访问微服务本身,和注册中心没有关系,也观察不到集群的效果,接下来我们就会讲如何用微服务,访问另一个微服务。
创建子项目
springcloud(父模块:删除没用的src目录)
— eureka-server(子模块)
— product-data-service(子模块)
— product-view-service-ribbon(子模块*后续不在使用)
product-view-service-ribbon子模块依赖
包含以下jar:
spring-cloud-starter-netflix-eureka-client
: eureka 客户端
spring-boot-starter-web
: springmvc
spring-boot-starter-thymeleaf
: thymeleaf 做服务端渲染
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.how2j.springcloud</groupId>
<artifactId>springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-view-service-ribbon</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
</project>
Product(product-view-service-ribbon子模块实体类)
@Data
public class Product {
private int id;
private String name;
private int price;
}
Ribbon 客户端
Ribbon 客户端, 通过 restTemplate 访问 http://PRODUCT-DATA-SERVICE/products , 而 product-data-service 既不是域名也不是ip地址,而是 数据服务在 eureka 注册中心的名称。
这里只是指定了要访问的 微服务名称,但是并没有指定端口号到底是 8001, 还是 8002.
@Component
public class ProductClientRibbon {
@Autowired
private RestTemplate restTemplate;
public List<Product> listProdcuts() {
return restTemplate.getForObject("http://PRODUCT-DATA-SERVICE/products",List.class);
}
}
ProductClientRibbon(product-view-service-ribbon子模块服务类)
服务类,数据从 ProductClientRibbon 中获取
@Service
public class ProductService {
@Autowired ProductClientRibbon productClientRibbon;
public List<Product> listProducts(){
return productClientRibbon.listProdcuts();
}
}
ProductController(product-view-service-ribbon子模块控制器)
@Controller
public class ProductController {
@Autowired
private ProductService productService;
@RequestMapping("/products")
public Object products(Model m) {
List<Product> ps = productService.listProducts();
m.addAttribute("ps", ps);
return "products";
}
}
products.html(product-view-service-ribbon子模块数据渲染)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>products</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style>
table {
border-collapse:collapse;
width:400px;
margin:20px auto;
}
td,th{
border:1px solid gray;
}
</style>
</head>
<body>
<div class="workingArea">
<table>
<thead>
<tr>
<th>id</th>
<th>产品名称</th>
<th>价格</th>
</tr>
</thead>
<tbody>
<tr th:each="p: ${ps}">
<td th:text="${p.id}"></td>
<td th:text="${p.name}"></td>
<td th:text="${p.price}"></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
ProductViewServiceRibbonApplication(product-view-service-ribbon子模块启动类)
启动类, 注解多了个 @EnableDiscoveryClient, 表示用于发现eureka 注册中心的微服务。
还多了个 RestTemplate,就表示用 restTemplate 这个工具来做负载均衡
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class ProductViewServiceRibbonApplication {
public static void main(String[] args) {
int port = 0;
int defaultPort = 8010;
Future<Integer> future = ThreadUtil.execAsync(() ->{
int p = 0;
System.out.println("请于5秒钟内输入端口号, 推荐 8010 超过5秒将默认使用 " + defaultPort);
Scanner scanner = new Scanner(System.in);
while(true) {
String strPort = scanner.nextLine();
if(!NumberUtil.isInteger(strPort)) {
System.err.println("只能是数字");
continue;
}
else {
p = Convert.toInt(strPort);
scanner.close();
break;
}
}
return p;
});
try{
port=future.get(5,TimeUnit.SECONDS);
}
catch (InterruptedException | ExecutionException | TimeoutException e){
port = defaultPort;
}
if(!NetUtil.isUsableLocalPort(port)) {
System.err.printf("端口%d被占用了,无法启动%n", port );
System.exit(1);
}
new SpringApplicationBuilder(ProductViewServiceRibbonApplication.class).properties("server.port=" + port).run(args);
}
@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
}
application.yml(product-view-service-ribbon子模块配置信息)
配置类,指定了 eureka server 的地址,以及自己的名称。 另外是一些 thymeleaf 的默认配置。
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: product-view-service-ribbon
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
content-type: text/html
mode: HTML5
启动并访问
运行 ProductViewServiceRibbonApplication 以启动 微服务,然后访问地址:
http://127.0.0.1:8010/products多刷新几遍,会发现这个端口有时候是 8001,有时候是8002. 从而观察到访问 数据服务集群,客户端负载均衡的效果。
调用用例图
如图所示:
- 首先数据微服务和视图微服务都被 eureka 管理起来了。
- 数据服务是由两个实例的集群组成的,端口分别是 8001 , 8002
- 视图微服务通过 注册中心调用微服务, 然后负载均衡到 8001 或者 8002 端口的应用上。
Feign 概念
Feign 是什么呢? Feign 是对 Ribbon的封装,使用注解的方式,调用起来更简单。。。 也是主流的方式~
Ribbon方式调用
public List<Product> listProdcuts() {
return restTemplate.getForObject("http://PRODUCT-DATA-SERVICE/products",List.class);
}
Feign方式调用
@FeignClient(value = "PRODUCT-DATA-SERVICE")
public interface ProductClientFeign {
@GetMapping("/products")
public List<Product> listProdcuts();
}
创建子项目
springcloud(父模块:删除没用的src目录)
— eureka-server(子模块)
— product-data-service(子模块)
— product-view-service-ribbon(子模块*后续不在使用)
— product-view-service-feign(子模块)
product-view-service-feign子模块依赖
jar 包多了个 spring-cloud-starter-openfeign,就是用来支持 Feign 方式的。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.how2j.springcloud</groupId>
<artifactId>springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-view-service-feign</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
</dependencies>
</project>
Product(product-view-service-feign子模块实体类)
Feign 客户端
Feign 客户端, 通过 注解方式 访问 访问PRODUCT-DATA-SERVICE服务的 products路径, product-data-service 既不是域名也不是ip地址,而是 数据服务在 eureka 注册中心的名称。
注意看,这里只是指定了要访问的 微服务名称,但是并没有指定端口号到底是 8001, 还是 8002.
@FeignClient(value = "PRODUCT-DATA-SERVICE")
//@FeignClient(value = "PRODUCT-DATA-SERVICE",fallback = ProductClientFeignHystrix.class)
public interface ProductClientFeign {
@GetMapping("/products")
public List<Product> listProdcuts();
}
服务类获取数据
服务类,数据从 productClientFeign 中获取数据
@Service
public class ProductService {
@Autowired ProductClientFeign productClientFeign;
public List<Product> listProducts(){
return productClientFeign.listProdcuts();
}
}
控制器
控制器,把数据取出来放在 product.html 中
@Controller
public class ProductController {
@Autowired ProductService productService;
@RequestMapping("/products")
public Object products(Model m) {
List<Product> ps = productService.listProducts();
m.addAttribute("ps", ps);
return "products";
}
}
启动类
启动类, 注解多了个 @EnableFeignClients, 表示用于使用 Feign 方式。
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableFeignClients
public class ProductViewServiceFeignApplication {
public static void main(String[] args) {
int port = 0;
int defaultPort = 8012;
Future<Integer> future = ThreadUtil.execAsync(() ->{
int p = 0;
System.out.println("请于5秒钟内输入端口号, 推荐 8012 、 8013 或者 8014,超过5秒将默认使用"+defaultPort);
Scanner scanner = new Scanner(System.in);
while(true) {
String strPort = scanner.nextLine();
if(!NumberUtil.isInteger(strPort)) {
System.err.println("只能是数字");
continue;
}
else {
p = Convert.toInt(strPort);
scanner.close();
break;
}
}
return p;
});
try{
port=future.get(5,TimeUnit.SECONDS);
}
catch (InterruptedException | ExecutionException | TimeoutException e){
port = defaultPort;
}
if(!NetUtil.isUsableLocalPort(port)) {
System.err.printf("端口%d被占用了,无法启动%n", port );
System.exit(1);
}
new SpringApplicationBuilder(ProductViewServiceFeignApplication.class).properties("server.port=" + port).run(args);
}
/*@Bean
public Sampler defaultSampler() {
return Sampler.ALWAYS_SAMPLE;
}*/
}
配置信息
配置类,指定了 eureka server 的地址,以及自己的名称。 另外是一些 thymeleaf 的默认配置。
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: product-view-service-feign
zipkin:
base-url: http://localhost:9411
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
content-type: text/html
mode: HTML5
# 断路器配置
# feign.hystrix.enabled: true
启动并访问
运行 ProductViewServiceFeignApplication以启动 微服务,然后访问地址:
http://127.0.0.1:8012/products多刷新几遍,会发现这个端口有时候是 8001,有时候是8002. 从而观察到访问 数据服务集群,客户端负载均衡的效果。
服务链路追踪
product-data-service和product-view-service 需要进行改造以使其可以被追踪到。
添加依赖
product-data-service和product-view-service添加以下依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
添加配置
product-data-service和product-view-service添加配置
spring:
zipkin:
base-url: http://localhost:9411
修改启动类
product-data-service和product-view-service的启动类上添加配置
@Bean
public Sampler defaultSampler() {
return Sampler.ALWAYS_SAMPLE;
}
- 需要启动链路追踪服务器,这个启动办法是下载 zipkin-server-2.10.1-exec.jar, 然后用如下命令启动:java -jar zipkin-server-2.10.1-exec.jar
- 挨个启动 eureka-server, 改造后的 product-data-service 和 product-view-service-feign 。 ( product-view-service-ribbon 后续不再使用)
- 访问一次 http://127.0.0.1:8012/products 通过 视图微服务去访问数据微服务,这样链路追踪服务器才知道有这事儿发生~
- 然后打开链路追踪服务器 http://localhost:9411/zipkin/dependency/ 就可以看到如图所示的 视图微服务调用数据微服务 的图形了。
断路器 HYSTRIX
所谓的断路器,就是当被访问的微服务无法使用的时候,当前服务能够感知这个现象,并且提供一个备用的方案出来。
当数据微服务无法使用了,如果有了断路器,那么视图微服务就能够知道此事,并且展示给用户相关的信息。 而不会报错或者一直卡在那里~
添加依赖
product-view-service-feign添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
修改客户端
注解由原来的 @FeignClient(value = “PRODUCT-DATA-SERVICE”)
修改为 @FeignClient(value = “PRODUCT-DATA-SERVICE”,fallback = ProductClientFeignHystrix.class)这就表示,如果访问的 PRODUCT-DATA-SERVICE 不可用的话,就调用 ProductClientFeignHystrix 来进行反馈信息。
ProductClientFeign
@FeignClient(value = "PRODUCT-DATA-SERVICE",fallback = ProductClientFeignHystrix.class)
public interface ProductClientFeign {
@GetMapping("/products")
public List<Product> listProdcuts();
}
配置消息
ProductClientFeignHystrix 实现了 ProductClientFeign 接口,并提供了 listProdcuts() 方法。
这个方法就会固定返回包含一条信息的集合~
ProductClientFeignHystrix
@Component
public class ProductClientFeignHystrix implements ProductClientFeign{
public List<Product> listProdcuts(){
List<Product> result = new ArrayList<>();
result.add(new Product(0,"产品数据微服务不可用",0));
return result;
}
}
网关 ZUUL
我们现在有两种微服务,分别是数据微服务和视图微服务。
他们有可能放在不同的 ip 地址上,有可能是不同的端口。
为了访问他们,就需要记录这些地址和端口。 而地址和端口都可能会变化,这就增加了访问者的负担。
所以这个时候,我们就可以用网关来解决这个问题。
如图所示,我们只需要记住网关的地址和端口号就行了。
如果要访问数据服务,访问地址 http://ip:port/api-data/products 即可。
如果要访问视图服务,访问地址 http://ip:port/api-view/products 即可。
创建子项目 zuul
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.how2j.springcloud</groupId>
<artifactId>springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>productServiceZuul</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
</project>
配置启动类
启动类,主要是 @EnableZuulProxy
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
@EnableDiscoveryClient
public class ProductServiceZuulApplication {
public static void main(String[] args) {
int port = 8040;
if(!NetUtil.isUsableLocalPort(port)) {
System.err.printf("端口%d被占用了,无法启动%n", port );
System.exit(1);
}
new SpringApplicationBuilder(ProductServiceZuulApplication.class).properties("server.port=" + port).run(args);
}
}
配置文件信息
配置文件,进行了路由映射
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: product-service-zuul
zuul:
routes:
api-a:
path: /api-data/**
serviceId: PRODUCT-DATA-SERVICE
api-b:
path: /api-view/**
serviceId: PRODUCT-VIEW-SERVICE-FEIGN
- 首先挨个运行 EurekaServerApplication, ProductDataServiceApplication, ProductViewServiceFeignApplication。
- 然后启动 ProductServiceZuulApplication
- 接着访问地址:http://localhost:8040/api-data/products、http://localhost:8040/api-view/products
这样就可以访问数据微服务和视微服务集群了,并且无需去记住那么多ip地址和端口号了。 ( 虽然这里集群只有一个实例。。。,有兴趣的同学可以多启动几个不同端口号的实例)
…。。。