RIBBON是什么?
前面已经做好了数据微服务,现在是时候整个视图微服务访问前面注册好的数据微服务了。 springcloud 提供了两种方式,一种是 Ribbon,一种是 Feign。
Ribbon 是使用 restTemplate 进行调用,并进行客户端负载均衡。 什么是客户端负载均衡呢? 在前面 注册数据微服务 里,注册了8001和8002两个微服务, Ribbon 会从注册中心获知这个信息,然后由 Ribbon 这个客户端自己决定是调用哪个,这个就叫做客户端负载均衡。
Feign 是什么呢? Feign 是对 Ribbon的封装,调用起来更简单。
建立子项目
创建子项目 product-view-service-ribbon:
pom.xml
spring-cloud-starter-netflix-eureka-client: eureka 客户端
spring-boot-starter-web: springmvc
spring-boot-starter-thymeleaf: thymeleaf 做服务端渲染
<?xml version="1.0" encoding="UTF-8"?> <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"> <parent> <artifactId>springcloud</artifactId> <groupId>edu.hpu.springcloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>product-view-service-ribbon</artifactId> <name>product-view-service-ribbon</name> <!-- FIXME change it to the project's website --> <url>http://www.example.com</url> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <!--eureka 客户端--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!--SpringMvc--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> </project>
实体类
package edu.hpu.springcloud.pojo; public class Product { private int id; private String name; private int price; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public Product(int id, String name, int price) { this.id = id; this.name = name; this.price = price; } public Product() { } }
Ribbon 客户端
Ribbon 客户端, 通过 restTemplate 访问 http://PRODUCT-DATA-SERVICE/products , 而 product-data-service 既不是域名也不是ip地址,而是 数据服务在 eureka 注册中心的名称。
看一下注册中心:
上面那个地址只指定了服务,没有指定端口号。
package edu.hpu.springcloud.client; import edu.hpu.springcloud.pojo.Product; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import java.util.List; //Ribbon 客户端 @Component public class ProductClientRibbon { @Autowired RestTemplate restTemplate; public List<Product> listProcucts(){ return restTemplate.getForObject("http://PRODUCT-DATA-SERVICE/products",List.class); } }
服务层
服务层数据从客户端获取
package edu.hpu.springcloud.service; import edu.hpu.springcloud.client.ProductClientRibbon; import edu.hpu.springcloud.pojo.Product; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class ProductService { @Autowired ProductClientRibbon productClientRibbon; public List<Product> listProducts(){ return productClientRibbon.listProcucts(); //从productClientRibbon中获得数据服务 } }
控制层
package edu.hpu.springcloud.controller; import edu.hpu.springcloud.pojo.Product; import edu.hpu.springcloud.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import java.util.List; @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"; } }
视图
视图采用thymeleaf:
<!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>
配置
application.yml,eureka server 的地址,以及自己的名称。 另外是一些 thymeleaf 的默认配置
eureka: clg: UTF-8 content-type: text/html mode: HTML5ient: serviceUrl: defaultZone: http://localhost:8761/eureka/ spring: application: name: product-view-service-ribbon thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html encodin
启动类
ProductDataServiceApplication,
启动类, 注解多了个 @EnableDiscoveryClient, 表示用于发现eureka 注册中心的微服务。
还多了个 RestTemplate,就表示用 restTemplate 这个工具来做负载均衡
@Bean @LoadBalanced RestTemplate restTemplate() { return new RestTemplate(); }
在ribbon客户端中使用了这个RestTemplate.
package edu.hpu.springcloud; import cn.hutool.core.convert.Convert; import cn.hutool.core.thread.ThreadUtil; import cn.hutool.core.util.NetUtil; import cn.hutool.core.util.NumberUtil; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import java.util.Scanner; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @SpringBootApplication @EnableEurekaClient public class ProductDataServiceApplication { public static void main(String[] args) { int port = 0; int defaultPort = 8003; 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); } }
运行访问
调用图示
这里面三个角色,注册中心、服务提供者、服务消费者,数据微服务作为服务提供者,视图微服务作为服务消费者。
1、 首先数据微服务和视图微服务都被 eureka 管理起来了。
2、 数据服务是由三个实例的集群组成的,端口分别是 8001 , 8002,8003。
3、视图微服务通过 注册中心调用微服务, 然后负载均衡到 8001 、8002或者8003端口的应用上。
错误
1、springCloud com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused: connect
在默认设置下,Eureka服务注册中心也会将自己作为客户端来尝试注册它自己,所以我们需要禁用它的客户端注册行为。
禁止方式如下:在application.properties配置文件中增加以下内容
eureka.client.register-with-eureka=false eureka.client.fetch-registry=false
.yml也差不多,写的时候注意级别就行了。
2、低级错误:注册中心和数据微服务没启动
中间关了IJ,直接启动视图微服务,爆了一堆错,一想,是注册中心和数据微服务没启动,它向谁请求、调用谁去。
3、java.lang.IllegalStateException: No instances available for PRODUCT-DATA-SERVICE
找不到这个实例,我们看一下注册中心,看看数据微服务叫什么名字。
似乎也没什么错,不算了,直接给它粘贴到客户端。OK。
4、java.net.ConnectException: Connection refused: connect
刷新页面的时候发现报了这么个错误,负载均衡一到数据微服务8002这个端口的时候就报错,似乎是被占用了吧,我又注册个端口为8003的数据微服务,这个端口倒是没有问题。
参考:
【1】、http://how2j.cn/k/springcloud/springcloud-ribbon/2040.html#nowhere
【2】、https://blog.csdn.net/qq_39930369/article/details/87616077
【3】、https://www.cnblogs.com/zmblog/p/8777878.html