曹旭辉 发表于 2024-9-12 23:07:33

客户端负载均衡Ribbon实例

一,概述

一样平常来说,提到负载均衡,大家一样平常很轻易想到欣赏器 -> NGINX -> 反向代理多个Tomcat这样的架构图——业界管这种负载均衡模式叫“服务器端负载均衡”,因为此种模式下,负载均衡算法是NGINX提供的,而NGINX部署在服务器端。
本文所讲的Ribbon则是一个客户端侧负载均衡组件——通俗地说,就是集成在客户端(服务消耗者一侧),并提供负载均衡算法的一个组件。Ribbon是Netflix发布的负载均衡器,它可以帮我们控制HTTP和TCP客户端的行为。只需为Ribbon配置服务提供者地址列表,Ribbon就可基于负载均衡算法计算出要请求的目的服务地址。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机、响应时间加权等。
二,实现过程

一样平常环境下,负载均衡组件Ribbon和微服务注册中心Eureka是配合使用的。
为了在非springCloud微服务项目中,使用Ribbon的客户端负载均衡能力,我们可以按如下步骤实现:

[*]定义服务的被调用方Client,编写webApi接口
[*]定义服务的调用方Cousumer,调用webApi接口
[*]定义网关模块Gateway,通过定义路由的方式重新定义webApi接口路径,并引入Ribbon客户端负载均衡
[*]将步骤2中的WebApi地址切换为网关模块路由接口地址,从而使原来的webApi地址具有了客户端负载均衡功能
[*]使用docker部署Client(多实例)、Gateway,并在编排文件中使用服务名代替客户端列表地址,解耦Cousumer与Client之间的代码接口。
三,项目源码

https://i-blog.csdnimg.cn/direct/36ff0c51c7a64a758fade9f56cd9d7a3.png#pic_center
1. 源码放送:

https://gitee.com/00fly/microservice-all-in-one/tree/master/ribbon-demo
或者使用下面的备份文件恢复成原始的项目代码
怎样恢复,请移步查阅:神奇代码恢复工具
//goto docker-auto-ip\docker-compose.yml
version: '3.8'
services:

#gateway
ribbon-gateway:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-gateway:0.0.1
    container_name: ribbon-gateway
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    ports:
    - 8085:8080
    environment:
      USER_SERVERS: ribbon-user-0:8081,ribbon-user-1:8081
      MOVIE_SERVERS: ribbon-movie:8082
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'
      
#client01
ribbon-user-0:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-user:0.0.1
    container_name: ribbon-user-0
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'

#client02
ribbon-user-1:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-user:0.0.1
    container_name: ribbon-user-1
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'

ribbon-movie:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-movie:0.0.1
    container_name: ribbon-movie
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    environment:
      USER_API_URL: ribbon-gateway:8080/user/
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'
//goto docker-auto-ip\restart.sh
#!/bin/bash
docker-compose down && \
docker-compose --compatibility up -d && \
docker stats
//goto docker-auto-ip\stop.sh
#!/bin/bash
docker-compose down
//goto docker-fix-ip\docker-compose.yml
version: '3.8'
networks:
default:
    name: devops
    driver: bridge
    ipam:
      config:
      - subnet: 172.88.88.0/24
services:

#gateway
ribbon-gateway:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-gateway:0.0.1
    container_name: ribbon-gateway
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    ports:
    - 8085:8080
    environment:
      MOVIE_SERVERS: 172.88.88.101:8082
      USER_SERVERS: 172.88.88.200:8081,172.88.88.201:8081
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'
    networks:
      default:
      ipv4_address: 172.88.88.100
      
#client01
ribbon-user-0:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-user:0.0.1
    container_name: ribbon-user-0
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'
    networks:
      default:
      ipv4_address: 172.88.88.200
#client02
ribbon-user-1:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-user:0.0.1
    container_name: ribbon-user-1
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'
    networks:
      default:
      ipv4_address: 172.88.88.201

ribbon-movie:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-movie:0.0.1
    container_name: ribbon-movie
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    environment:
      USER_API_URL: http://172.88.88.100:8080/user/
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'
    networks:
      default:
      ipv4_address: 172.88.88.101
//goto docker-fix-ip\restart.sh
#!/bin/bash
docker-compose down && \
docker-compose --compatibility up -d && \
docker stats
//goto docker-fix-ip\stop.sh
#!/bin/bash
docker-compose down
//goto docker-scale\docker-compose.yml
version: '3.8'
services:

#gateway
ribbon-gateway:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-gateway:0.0.1
    container_name: ribbon-gateway
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    ports:
    - 8085:8080
    environment:
      USER_SERVERS: ribbon-user:8081
      MOVIE_SERVERS: ribbon-movie:8082
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'
      
#client
ribbon-user:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-user:0.0.1
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'

ribbon-movie:
    image: registry.cn-shanghai.aliyuncs.com/00fly/ribbon-movie:0.0.1
    deploy:
      resources:
      limits:
          cpus: '1'
          memory: 300M
      reservations:
          memory: 200M
    environment:
      USER_API_URL: ribbon-gateway:8080/user/
    restart: on-failure
    logging:
      driver: json-file
      options:
      max-size: 5m
      max-file: '1'
//goto docker-scale\restart.sh
#!/bin/bash
docker-compose down && \
docker-compose --compatibility up -d --scale ribbon-user=2 --scale ribbon-movie=2 && \
docker stats
//goto docker-scale\stop.sh
#!/bin/bash
docker-compose down
//goto pom.xml
<?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">
        <modelVersion>4.0.0</modelVersion>
        <groupId>com.itmuch.cloud</groupId>
        <artifactId>ribbon-all-in-one</artifactId>
        <version>0.0.1</version>
        <packaging>pom</packaging>

        <!-- 引入spring boot的依赖 -->
        <parent>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>2.2.6.RELEASE</version>
                <relativePath />
        </parent>

        <modules>
                <module>ribbon-gateway</module>
                <module>ribbon-movie</module>
                <module>ribbon-user</module>
        </modules>

        <properties>
                <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
                <maven.build.timestamp.format>yyyyMMdd-HH</maven.build.timestamp.format>
                <docker.hub>registry.cn-shanghai.aliyuncs.com</docker.hub>
                <java.version>1.8</java.version>
                <docker.plugin.version>1.2.2</docker.plugin.version>
                <docker.image.prefix>00fly</docker.image.prefix>
                <spring.cloud.version>Hoxton.SR6</spring.cloud.version>
                <knife4j.version>2.0.8</knife4j.version>
                <skipTests>true</skipTests>
        </properties>

        <dependencyManagement>
                <dependencies>
                        <!-- 引入spring cloud的依赖 -->
                        <dependency>
                                <groupId>org.springframework.cloud</groupId>
                                <artifactId>spring-cloud-dependencies</artifactId>
                                <version>${spring.cloud.version}</version>
                                <type>pom</type>
                                <scope>import</scope>
                        </dependency>
                        <dependency>
                                <groupId>com.github.xiaoymin</groupId>
                                <artifactId>knife4j-spring-boot-starter</artifactId>
                                <version>${knife4j.version}</version>
                        </dependency>
                        <dependency>
                                <groupId>com.github.xiaoymin</groupId>
                                <artifactId>knife4j-micro-spring-boot-starter</artifactId>
                                <version>${knife4j.version}</version>
                        </dependency>
                </dependencies>
        </dependencyManagement>

        <build>
                <pluginManagement>
                        <plugins>
                                <plugin>
                                        <groupId>org.springframework.boot</groupId>
                                        <artifactId>spring-boot-maven-plugin</artifactId>
                                        <executions>
                                                <execution>
                                                        <goals>
                                                                <goal>repackage</goal>
                                                        </goals>
                                                </execution>
                                        </executions>
                                </plugin>

                                <!-- 添加docker-maven插件 -->
                                <plugin>
                                        <groupId>io.fabric8</groupId>
                                        <artifactId>docker-maven-plugin</artifactId>
                                        <version>0.40.3</version>
                                        <executions>
                                                <execution>
                                                        <phase>package</phase>
                                                        <goals>
                                                                <goal>build</goal>
                                                                <!--<goal>push</goal>-->
                                                                <!--<goal>remove</goal>-->
                                                        </goals>
                                                </execution>
                                        </executions>
                                        <configuration>
                                                <!-- 连接到带docker环境的linux服务器编译image -->
                                                <!--<dockerHost>http://192.168.182.10:2375</dockerHost>-->
                                                <!-- Docker 推送镜像仓库地址 -->
                                                <pushRegistry>${docker.hub}</pushRegistry>
                                                <images>
                                                        <image>
                                                                <!--推送到私有镜像仓库,镜像名需要添加仓库地址 -->
                                                                <name>${docker.hub}/00fly/${project.artifactId}:${project.version}</name>
                                                                <!--定义镜像构建行为 -->
                                                                <build>
                                                                        <dockerFileDir>${project.basedir}</dockerFileDir>
                                                                </build>
                                                        </image>
                                                </images>
                                                <authConfig>
                                                        <!--认证配置,用于私有镜像仓库registry认证 -->
                                                        <username>${docker.username}</username>
                                                        <password>${docker.password}</password>
                                                </authConfig>
                                        </configuration>
                                </plugin>
                        </plugins>
                </pluginManagement>
        </build>
</project>
//goto ribbon-gateway\Dockerfile
#基础镜像
FROM adoptopenjdk/openjdk8-openj9:alpine-slim

COPY wait-for.sh /
RUN chmod +x /wait-for.sh && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

#引入运行包
COPY target/*.jar /app.jar

#指定交互端口
EXPOSE 8080

CMD ["--server.port=8080"]

#项目的启动方式
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-Xshareclasses", "-Xquickstart", "-jar", "/app.jar"]
//goto ribbon-gateway\pom.xml
<?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>
                <groupId>com.itmuch.cloud</groupId>
                <artifactId>ribbon-all-in-one</artifactId>
                <version>0.0.1</version>
        </parent>

        <modelVersion>4.0.0</modelVersion>
        <artifactId>ribbon-gateway</artifactId>
        <packaging>jar</packaging>

        <dependencies>
                <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-starter-gateway</artifactId>
                        <exclusions>
                                <exclusion>
                                        <groupId>org.springframework.boot</groupId>
                                        <artifactId>spring-boot-starter-logging</artifactId>
                                </exclusion>
                        </exclusions>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-log4j2</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </dependency>

                <!-- 集成 knife4j -->
                <dependency>
                        <groupId>com.github.xiaoymin</groupId>
                        <artifactId>knife4j-spring-boot-starter</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                </dependency>
        </dependencies>

        <build>
                <finalName>${project.artifactId}</finalName>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                        <plugin>
                                <groupId>io.fabric8</groupId>
                                <artifactId>docker-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>
</project>
//goto ribbon-gateway\src\main\java\com\fly\gateway\config\SwaggerHeaderFilter.java
package com.fly.gateway.config;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;

/**
* @author fsl
* @description: SwaggerHeaderFilter
* @date 2019-06-0310:47
*/
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory<Object>
{
    private static final String HEADER_NAME = "X-Forwarded-Prefix";
   
    private static final String URI = "/v2/api-docs";
   
    /**
   * 网关过滤器
   */
    @Override
    public GatewayFilter apply(Object config)
    {
      return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();
            if (!StringUtils.endsWithIgnoreCase(path, URI))
            {
                return chain.filter(exchange);
            }
            String basePath = path.substring(0, path.lastIndexOf(URI));
            ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
            ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
            return chain.filter(newExchange);
      };
    }
}
//goto ribbon-gateway\src\main\java\com\fly\gateway\config\SwaggerResourceConfig.java
package com.fly.gateway.config;

import java.util.ArrayList;
import java.util.List;

import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;

/***
* 聚合各个服务的swagger接口
*/
@Slf4j
@Component
@Primary
@AllArgsConstructor
public class SwaggerResourceConfig implements SwaggerResourcesProvider
{
    /**
   * 网关路由
   */
    private final RouteLocator routeLocator;
   
    private final GatewayProperties gatewayProperties;
   
    @Override
    public List<SwaggerResource> get()
    {
      List<SwaggerResource> resources = new ArrayList<>();
      List<String> routes = new ArrayList<>();
      routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
      gatewayProperties.getRoutes().stream().filter(routeDefinition -> routes.contains(routeDefinition.getId())).forEach(route -> {
            route.getPredicates()
                .stream()
                .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                .forEach(predicateDefinition -> resources.add(swaggerResource(route.getId(), predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0").replace("**", "v2/api-docs"))));
      });
      return resources;
    }
   
    private SwaggerResource swaggerResource(String name, String location)
    {
      log.info("name:{},location:{}", name, location);
      SwaggerResource swaggerResource = new SwaggerResource();
      swaggerResource.setName(name);
      swaggerResource.setLocation(location);
      swaggerResource.setSwaggerVersion("2.0");
      return swaggerResource;
    }
}
//goto ribbon-gateway\src\main\java\com\fly\gateway\GateWayApplication.java
package com.fly.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GateWayApplication
{
    public static void main(String[] args)
    {
      SpringApplication.run(GateWayApplication.class, args);
    }
}
//goto ribbon-gateway\src\main\java\com\fly\gateway\handler\SwaggerHandler.java
package com.fly.gateway.handler;

import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;

/**
* swagger聚合接口
*
*/
@RestController
public class SwaggerHandler
{
    @Autowired(required = false)
    private SecurityConfiguration securityConfiguration;
   
    @Autowired(required = false)
    private UiConfiguration uiConfiguration;
   
    private final SwaggerResourcesProvider swaggerResources;
   
    public SwaggerHandler(SwaggerResourcesProvider swaggerResources)
    {
      this.swaggerResources = swaggerResources;
    }
   
    @GetMapping("/swagger-resources/configuration/security")
    public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration()
    {
      return Mono.just(new ResponseEntity<>(Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
    }
   
    @GetMapping("/swagger-resources/configuration/ui")
    public Mono<ResponseEntity<UiConfiguration>> uiConfiguration()
    {
      return Mono.just(new ResponseEntity<>(Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
    }
   
    @GetMapping("/swagger-resources")
    public Mono<ResponseEntity<?>> swaggerResources()
    {
      return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
    }
}
//goto ribbon-gateway\src\main\resources\application.yml
server:
port: 8080
spring:
application:
    name: ribbon-gateway
cloud:
    gateway:
      discovery:
      locator:
          lowerCaseServiceId: true
      routes:
      - id: ribbon-user
          uri: lb://ribbon-user
          predicates:
            - Path=/user/**
          filters:
            - StripPrefix=1
      - id:ribbon-movie
          uri: lb://ribbon-movie
          predicates:
            - Path=/movie/**
          filters:
            - StripPrefix=1

ribbon-movie:
ribbon:
    listOfServers: ${MOVIE_SERVERS:127.0.0.1:8082}

ribbon-user:
ribbon:
    listOfServers: ${USER_SERVERS:127.0.0.1:8081}
//goto ribbon-gateway\wait-for.sh
#!/bin/sh

TIMEOUT=15
QUIET=0

echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}

usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet                        Do not output any status messages
-t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
-- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
exit "$exitcode"
}

wait_for() {
for i in `seq $TIMEOUT` ; do
    nc -z "$HOST" "$PORT" > /dev/null 2>&1

    result=$?
    if [ $result -eq 0 ] ; then
      if [ $# -gt 0 ] ; then
      exec "$@"
      fi
      exit 0
    fi
    sleep 1
done
echo "Operation timed out" >&2
exit 1
}

while [ $# -gt 0 ]
do
case "$1" in
    *:* )
    HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
    PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
    shift 1
    ;;
    -q | --quiet)
    QUIET=1
    shift 1
    ;;
    -t)
    TIMEOUT="$2"
    if [ "$TIMEOUT" = "" ]; then break; fi
    shift 2
    ;;
    --timeout=*)
    TIMEOUT="${1#*=}"
    shift 1
    ;;
    --)
    shift
    break
    ;;
    --help)
    usage 0
    ;;
    *)
    echoerr "Unknown argument: $1"
    usage 1
    ;;
esac
done

if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi

wait_for "$@"
//goto ribbon-movie\Dockerfile
#基础镜像
FROM adoptopenjdk/openjdk8-openj9:alpine-slim

COPY wait-for.sh /
RUN chmod +x /wait-for.sh && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

#引入运行包
COPY target/*.jar /app.jar

#指定交互端口
EXPOSE 8082

CMD ["--server.port=8082"]

#项目的启动方式
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-Xshareclasses", "-Xquickstart", "-jar", "/app.jar"]
//goto ribbon-movie\pom.xml
<?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>
                <groupId>com.itmuch.cloud</groupId>
                <artifactId>ribbon-all-in-one</artifactId>
                <version>0.0.1</version>
        </parent>

        <modelVersion>4.0.0</modelVersion>
        <artifactId>ribbon-movie</artifactId>
        <packaging>jar</packaging>

        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                        <exclusions>
                                <exclusion>
                                        <groupId>org.springframework.boot</groupId>
                                        <artifactId>spring-boot-starter-logging</artifactId>
                                </exclusion>
                        </exclusions>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-log4j2</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-webflux</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-starter-openfeign</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.cloud</groupId>
                        <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.apache.commons</groupId>
                        <artifactId>commons-lang3</artifactId>
                </dependency>
                <!-- 使用Apache HttpClient替换Feign原生httpclient -->
                <dependency>
                        <groupId>io.github.openfeign</groupId>
                        <artifactId>feign-okhttp</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <scope>provided</scope>
                </dependency>

                <!-- 集成 knife4j -->
                <dependency>
                        <groupId>com.github.xiaoymin</groupId>
                        <artifactId>knife4j-spring-boot-starter</artifactId>
                </dependency>
        </dependencies>

        <build>
                <finalName>${project.artifactId}</finalName>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                        <plugin>
                                <groupId>io.fabric8</groupId>
                                <artifactId>docker-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>
</project>
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\ConsumerMovieApplication.java
package com.itmuch.cloud.study;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableCaching
@EnableFeignClients
@SpringBootApplication
public class ConsumerMovieApplication
{
    public static void main(String[] args)
    {
      SpringApplication.run(ConsumerMovieApplication.class, args);
    }
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\core\config\Knife4jConfig.java
package com.itmuch.cloud.study.core.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;

import io.swagger.annotations.ApiOperation;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/**
* Knife4j配置
*
*/
@Configuration
@EnableKnife4j
@EnableSwagger2WebMvc
public class Knife4jConfig
{
    @Value("${knife4j.enable:true}")
    private boolean enable;
   
    /**
   * 开发、测试环境接口文档打开
   *
   * @return
   * @see [类、类#方法、类#成员]
   */
    @Bean
    Docket createRestApi()
    {
      return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
            .enable(enable)
            .select()
            .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
            .paths(PathSelectors.any()) // 包下的类,生成接口文档
            .build();
    }
   
    private ApiInfo apiInfo()
    {
      return new ApiInfoBuilder().title("movie模块API").description("接口文档").termsOfServiceUrl("http://00fly.online/").version("1.0.0").build();
    }
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\core\config\RibbonConfiguration.java
//package com.itmuch.cloud.study.core.config;
//
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//
//import com.netflix.loadbalancer.IRule;
//import com.netflix.loadbalancer.RandomRule;
//
///**
// * 该类为Ribbon的配置类 注意:该类不应该在主应用程序上下文的@ComponentScan 中。
// *
// */
//@Configuration
//public class RibbonConfiguration
//{
//    @Bean
//    IRule ribbonRule()
//    {
//      // 负载均衡规则,改为随机
//      return new RandomRule();
//    }
//}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\core\config\TestConfiguration.java
//package com.itmuch.cloud.study.core.config;
//
//import org.springframework.cloud.netflix.ribbon.RibbonClient;
//import org.springframework.context.annotation.Configuration;
//
///**
// * 使用RibbonClient,为特定name的Ribbon Client自定义配置. 使用@RibbonClient的configuration属性,指定Ribbon的配置类. <br>
// * 可参考的示例: http://spring.io/guides/gs/client-side-load-balancing/
// *
// */
//@Configuration
//@RibbonClient(name = "microservice-ribbon-user", configuration = RibbonConfiguration.class)
//public class TestConfiguration
//{
//}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\core\config\WebMvcConfig.java
package com.itmuch.cloud.study.core.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
*
* mvc配置
*
* @author 00fly
* @version [版本号, 2021年4月23日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer
{
    /**
   * @param registry
   */
    @Override
    public void addViewControllers(final ViewControllerRegistry registry)
    {
      registry.addViewController("/").setViewName("doc.html");
    }
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\core\utils\JsonBeanUtils.java
package com.itmuch.cloud.study.core.utils;

import java.io.IOException;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
* JsonBean转换工具
*
* @author 00fly
*
*/
public class JsonBeanUtils
{
    private static ObjectMapper objectMapper = new ObjectMapper();
   
    /**
   * bean转json字符串
   *
   * @param bean
   * @return
   * @throws IOException
   */
    public static String beanToJson(Object bean)
      throws IOException
    {
      String jsonText = objectMapper.writeValueAsString(bean);
      return objectMapper.readTree(jsonText).toPrettyString();
    }
   
    /**
   * bean转json字符串
   *
   * @param bean
   * @param pretty 是否格式美化
   * @return
   * @throws IOException
   */
    public static String beanToJson(Object bean, boolean pretty)
      throws IOException
    {
      if (pretty)
      {
            return beanToJson(bean);
      }
      String jsonText = objectMapper.writeValueAsString(bean);
      return objectMapper.readTree(jsonText).toString();
    }
   
    /**
   * json字符串转bean
   *
   * @param jsonText
   * @return
   * @throws IOException
   */
    public static <T> T jsonToBean(String jsonText, Class<T> clazz)
      throws IOException
    {
      return objectMapper.readValue(jsonText, clazz);
    }
   
    /**
   * json字符串转bean
   *
   * @param jsonText
   * @return
   * @throws IOException
   */
    public static <T> T jsonToBean(String jsonText, JavaType javaType)
      throws IOException
    {
      return objectMapper.readValue(jsonText, javaType);
    }
   
    /**
   * json字符串转bean
   *
   * @param jsonText
   * @param clazz
   * @param ingoreError 是否忽略无法识别字段
   * @return
   * @throws IOException
   */
    public static <T> T jsonToBean(String jsonText, Class<T> clazz, boolean ingoreError)
      throws IOException
    {
      objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, !ingoreError);
      return objectMapper.readValue(jsonText, clazz);
    }
   
    /**
   * json字符串转bean
   *
   * @param jsonText
   * @return
   * @throws IOException
   */
    public static <T> T jsonToBean(String jsonText, TypeReference<T> typeRef)
      throws IOException
    {
      return objectMapper.readValue(jsonText, typeRef);
    }
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\controller\DataPushController.java
package com.itmuch.cloud.study.user.controller;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;

import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import com.itmuch.cloud.study.core.utils.JsonBeanUtils;
import com.itmuch.cloud.study.user.entity.Article;
import com.itmuch.cloud.study.user.service.DataService;
import com.itmuch.cloud.study.user.service.SSEServer;

import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Api(tags = "DataPush模块")
@RestController
public class DataPushController
{
    long init = 0L;
   
    @Autowired
    DataService dataService;
   
    @PostConstruct
    private void init()
    {
      log.info("Server-Sent Events start");
      new ScheduledThreadPoolExecutor(2).scheduleAtFixedRate(() -> {
            long now = (init + RandomUtils.nextInt(5, 10)) % 101;
            SSEServer.batchSendMessage(String.valueOf(init));
            if (now < init)
            {
                try
                {
                  // 随机选择2个,返回访问量小的
                  List<Article> articles = dataService.getArticles();
                  int length = articles.size();
                  Article article001 = articles.get(RandomUtils.nextInt(0, length));
                  Article article002 = articles.get(RandomUtils.nextInt(0, length));
                  SSEServer.batchSendMessage("json", JsonBeanUtils.beanToJson(article001.getViewCount() > article002.getViewCount() ? article002 : article001, false));
                }
                catch (IOException e)
                {
                }
            }
            init = now;
      }, 2000, 1000, TimeUnit.MILLISECONDS);
    }
   
    @CrossOrigin
    @GetMapping("/sse/connect/{userId}")
    public SseEmitter connect(@PathVariable String userId)
    {
      return SSEServer.connect();
    }
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\controller\MovieController.java
package com.itmuch.cloud.study.user.controller;

import java.net.InetAddress;
import java.net.UnknownHostException;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.itmuch.cloud.study.user.entity.User;
import com.itmuch.cloud.study.user.feign.UserFeignClient;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@Api(tags = "movie模块")
@RestController
public class MovieController
{
    String serverIp;
   
    @Autowired
    private UserFeignClient userFeignClient;
   
    @PostConstruct
    private void init()
    {
      try
      {
            serverIp = InetAddress.getLocalHost().getHostAddress();
      }
      catch (UnknownHostException e)
      {
      }
    }
   
    @ApiOperation("查询用户")
    @GetMapping("/user/{id}")
    public User findById(@PathVariable Long id)
    {
      // 带出serverIp方便判断数据来源容器
      User user = this.userFeignClient.findById(id);
      if (user.getId() > 0)
      {
            user.setName(user.getName() + " === in server:" + serverIp);
      }
      return user;
    }
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\entity\Article.java
package com.itmuch.cloud.study.user.entity;

import lombok.Data;

@Data
public class Article
{
    String title;
   
    String url;
   
    Long viewCount;
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\entity\BlogData.java
package com.itmuch.cloud.study.user.entity;

import lombok.Data;

@Data
public class BlogData
{
    private Record data;
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\entity\Record.java
package com.itmuch.cloud.study.user.entity;

import java.util.List;

import lombok.Data;

@Data
public class Record
{
    private List<Article> list;
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\entity\User.java
package com.itmuch.cloud.study.user.entity;

import java.math.BigDecimal;

import lombok.Data;

@Data
public class User
{
    private Long id;
   
    private String username;
   
    private String name;
   
    private Integer age;
   
    private BigDecimal balance;
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\feign\UserFeignClient.java
package com.itmuch.cloud.study.user.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import com.itmuch.cloud.study.user.entity.User;

@FeignClient(name = "microservice-ribbon-user", url = "${user.api.url:127.0.0.1:8081}", fallback = FeignClientFallback.class)
public interface UserFeignClient
{
    @GetMapping("/{id}")
    public User findById(@PathVariable("id") Long id);
}

/**
* 回退类FeignClientFallback需实现Feign Client接口,FeignClientFallback也可以是public class,没有区别
*
*/
@Component
class FeignClientFallback implements UserFeignClient
{
    @Override
    public User findById(Long id)
    {
      User user = new User();
      user.setId(-1L);
      user.setUsername("默认用户");
      return user;
    }
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\service\DataService.java
package com.itmuch.cloud.study.user.service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import com.itmuch.cloud.study.core.utils.JsonBeanUtils;
import com.itmuch.cloud.study.user.entity.Article;
import com.itmuch.cloud.study.user.entity.BlogData;

import lombok.extern.slf4j.Slf4j;

/**
* DataService
*/
@Slf4j
@Service
public class DataService
{
    WebClient webClient = WebClient.builder().codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(-1)).build();
   
    /**
   * 获取Article数据列表
   *
   * @return
   * @throws IOException
   */
    @Cacheable(cacheNames = "data", key = "'articles'", sync = true)
    public List<Article> getArticles()
      throws IOException
    {
      log.info("★★★★★★★★ getData from webApi ★★★★★★★★");
      String resp = webClient.get().uri("https://00fly.online/upload/data.json").acceptCharset(StandardCharsets.UTF_8).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(String.class).block();
      return JsonBeanUtils.jsonToBean(resp, BlogData.class, true).getData().getList();
    }
}
//goto ribbon-movie\src\main\java\com\itmuch\cloud\study\user\service\SSEServer.java
package com.itmuch.cloud.study.user.service;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import lombok.extern.slf4j.Slf4j;

/**
* Server-Sent Events <BR>
* https://blog.csdn.net/hhl18730252820/article/details/126244274
*/
@Slf4j
public class SSEServer
{
    private static List<SseEmitter> sseEmitters = new CopyOnWriteArrayList<>();
   
    public static SseEmitter connect()
    {
      SseEmitter sseEmitter = new SseEmitter(0L); // 设置超时时间,0表示不过期,默认是30秒,超过时间未完成会抛出异常
      
      // 注册回调
      sseEmitter.onCompletion(completionCallBack(sseEmitter));
      sseEmitter.onError(errorCallBack(sseEmitter));
      sseEmitter.onTimeout(timeOutCallBack(sseEmitter));
      sseEmitters.add(sseEmitter);
      log.info("###### create new sse connect, count: {}", sseEmitters.size());
      return sseEmitter;
    }
   
    public static void batchSendMessage(String message)
    {
      sseEmitters.forEach(it -> {
            try
            {
                it.send(message, MediaType.APPLICATION_JSON);
            }
            catch (IOException e)
            {
                log.error("send message error: {}", e.getMessage());
                remove(it);
            }
      });
    }
   
    /**
   * 指定name,发送message
   *
   * @param name
   * @param message 普通字符串或json数据
   */
    public static void batchSendMessage(String name, String message)
    {
      sseEmitters.forEach(it -> {
            try
            {
                it.send(SseEmitter.event().name(name).data(message));
            }
            catch (IOException e)
            {
                log.error("send message error: {}", e.getMessage());
                remove(it);
            }
      });
    }
   
    public static void remove(SseEmitter s)
    {
      if (sseEmitters.contains(s))
      {
            sseEmitters.remove(s);
            log.info("###### remove SseEmitter, count: {}", sseEmitters.size());
      }
    }
   
    private static Runnable completionCallBack(SseEmitter s)
    {
      return () -> {
            log.info("结束连接");
            remove(s);
      };
    }
   
    private static Runnable timeOutCallBack(SseEmitter s)
    {
      return () -> {
            log.info("连接超时");
            remove(s);
      };
    }
   
    private static Consumer<Throwable> errorCallBack(SseEmitter s)
    {
      return throwable -> {
            log.error("连接异常");
            remove(s);
      };
    }
}
//goto ribbon-movie\src\main\resources\application-ribbon.yml
server:
port: 8082
spring:
application:
    name: ribbon-movie
cache:
    type: simple
   
#设置负载均衡参数
microservice-ribbon-user:
ribbon:
    #配置规则
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
   
    #配置地址:宿主机ip+映射端口或docker自定义网络指定地址
    #listOfServers: 172.22.208.1:8081,172.22.208.1:8091
    listOfServers: 172.88.88.200:8081,172.88.88.201:8081
feign:
httpclient:
    enabled: true
ribbon:
ReadTimeout: 30000
ConnectTimeout: 30000
logging:
level:
    root: INFO
//goto ribbon-movie\src\main\resources\application.yml
server:
port: 8082
spring:
application:
    name: ribbon-movie
cache:
    type: simple

#feign.okhttp.enabled默认不开启
#从Spring Cloud Dalston开始,Feign默认是不开启Hystrix的。
feign:
okhttp:
    enabled: true
hystrix:
    enabled: true
logging:
level:
    root: INFO
//goto ribbon-movie\wait-for.sh
#!/bin/sh

TIMEOUT=15
QUIET=0

echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}

usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet                        Do not output any status messages
-t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
-- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
exit "$exitcode"
}

wait_for() {
for i in `seq $TIMEOUT` ; do
    nc -z "$HOST" "$PORT" > /dev/null 2>&1

    result=$?
    if [ $result -eq 0 ] ; then
      if [ $# -gt 0 ] ; then
      exec "$@"
      fi
      exit 0
    fi
    sleep 1
done
echo "Operation timed out" >&2
exit 1
}

while [ $# -gt 0 ]
do
case "$1" in
    *:* )
    HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
    PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
    shift 1
    ;;
    -q | --quiet)
    QUIET=1
    shift 1
    ;;
    -t)
    TIMEOUT="$2"
    if [ "$TIMEOUT" = "" ]; then break; fi
    shift 2
    ;;
    --timeout=*)
    TIMEOUT="${1#*=}"
    shift 1
    ;;
    --)
    shift
    break
    ;;
    --help)
    usage 0
    ;;
    *)
    echoerr "Unknown argument: $1"
    usage 1
    ;;
esac
done

if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi

wait_for "$@"
//goto ribbon-user\Dockerfile
#基础镜像
FROM adoptopenjdk/openjdk8-openj9:alpine-slim

COPY wait-for.sh /
RUN chmod +x /wait-for.sh && \
    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo 'Asia/Shanghai' >/etc/timezone

#引入运行包
COPY target/*.jar /app.jar

#指定交互端口
EXPOSE 8081

CMD ["--server.port=8081"]

#项目的启动方式
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-Xshareclasses", "-Xquickstart", "-jar", "/app.jar"]
//goto ribbon-user\pom.xml
<?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>
                <groupId>com.itmuch.cloud</groupId>
                <artifactId>ribbon-all-in-one</artifactId>
                <version>0.0.1</version>
        </parent>

        <modelVersion>4.0.0</modelVersion>
        <artifactId>ribbon-user</artifactId>
        <packaging>jar</packaging>

        <dependencies>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-web</artifactId>
                        <exclusions>
                                <exclusion>
                                        <groupId>org.springframework.boot</groupId>
                                        <artifactId>spring-boot-starter-logging</artifactId>
                                </exclusion>
                        </exclusions>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-log4j2</artifactId>
                </dependency>
                <dependency>
                        <groupId>org.springframework.boot</groupId>
                        <artifactId>spring-boot-starter-data-jpa</artifactId>
                </dependency>
                <dependency>
                        <groupId>com.h2database</groupId>
                        <artifactId>h2</artifactId>
                </dependency>
                <!-- 集成 knife4j -->
                <dependency>
                        <groupId>com.github.xiaoymin</groupId>
                        <artifactId>knife4j-spring-boot-starter</artifactId>
                </dependency>
        </dependencies>

        <build>
                <finalName>${project.artifactId}</finalName>
                <plugins>
                        <plugin>
                                <groupId>org.springframework.boot</groupId>
                                <artifactId>spring-boot-maven-plugin</artifactId>
                        </plugin>
                        <plugin>
                                <groupId>io.fabric8</groupId>
                                <artifactId>docker-maven-plugin</artifactId>
                        </plugin>
                </plugins>
        </build>
</project>
//goto ribbon-user\src\main\java\com\itmuch\cloud\study\controller\UserController.java
package com.itmuch.cloud.study.controller;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.itmuch.cloud.study.entity.User;
import com.itmuch.cloud.study.repository.UserRepository;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;

@Api(tags = "user模块")
@RestController
public class UserController
{
    @Autowired
    private UserRepository userRepository;
   
    @ApiOperation("查询用户")
    @GetMapping("/{id:\\d+}")
    public Optional<User> findById(@PathVariable Long id)
    {
      return this.userRepository.findById(id);
    }
   
    @ApiOperation("查询全部用户")
    @GetMapping("getAll")
    public List<User> getAll()
    {
      return this.userRepository.findAll();
    }
}
//goto ribbon-user\src\main\java\com\itmuch\cloud\study\core\config\Knife4jConfig.java
package com.itmuch.cloud.study.core.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;

import io.swagger.annotations.ApiOperation;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/**
* Knife4j配置
*
*/
@Configuration
@EnableKnife4j
@EnableSwagger2WebMvc
public class Knife4jConfig
{
    @Value("${knife4j.enable:true}")
    private boolean enable;
   
    /**
   * 开发、测试环境接口文档打开
   *
   * @return
   * @see [类、类#方法、类#成员]
   */
    @Bean
    Docket createRestApi()
    {
      return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
            .enable(enable)
            .select()
            .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
            .paths(PathSelectors.any()) // 包下的类,生成接口文档
            .build();
    }
   
    private ApiInfo apiInfo()
    {
      return new ApiInfoBuilder().title("user模块API").description("接口文档").termsOfServiceUrl("http://00fly.online/").version("1.0.0").build();
    }
}
//goto ribbon-user\src\main\java\com\itmuch\cloud\study\core\config\WebMvcConfig.java
package com.itmuch.cloud.study.core.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
*
* mvc配置
*
* @author 00fly
* @version [版本号, 2021年4月23日]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
@Configuration
public class WebMvcConfig implements WebMvcConfigurer
{
    /**
   * @param registry
   */
    @Override
    public void addViewControllers(final ViewControllerRegistry registry)
    {
      registry.addViewController("/").setViewName("doc.html");
    }
}
//goto ribbon-user\src\main\java\com\itmuch\cloud\study\entity\User.java
package com.itmuch.cloud.study.entity;

import java.math.BigDecimal;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User
{
    public User()
    {
    }

    public User(Long id, String username, String name, Integer age, BigDecimal balance)
    {
      this.id = id;
      this.username = username;
      this.name = name;
      this.age = age;
      this.balance = balance;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String username;

    @Column
    private String name;

    @Column
    private Integer age;

    @Column
    private BigDecimal balance;

    public Long getId()
    {
      return this.id;
    }

    public void setId(Long id)
    {
      this.id = id;
    }

    public String getUsername()
    {
      return this.username;
    }

    public void setUsername(String username)
    {
      this.username = username;
    }

    public String getName()
    {
      return this.name;
    }

    public void setName(String name)
    {
      this.name = name;
    }

    public Integer getAge()
    {
      return this.age;
    }

    public void setAge(Integer age)
    {
      this.age = age;
    }

    public BigDecimal getBalance()
    {
      return this.balance;
    }

    public void setBalance(BigDecimal balance)
    {
      this.balance = balance;
    }

}
//goto ribbon-user\src\main\java\com\itmuch\cloud\study\ProviderUserApplication.java
package com.itmuch.cloud.study;

import java.math.BigDecimal;
import java.net.InetAddress;
import java.util.stream.Stream;

import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import com.itmuch.cloud.study.entity.User;
import com.itmuch.cloud.study.repository.UserRepository;

@SpringBootApplication
public class ProviderUserApplication
{
    public static void main(String[] args)
    {
      SpringApplication.run(ProviderUserApplication.class, args);
    }
   
    /**
   * 初始化用户信息 注:Spring Boot2不能像1.x一样,用spring.datasource.schema/data指定初始化SQL脚本,否则与actuator不能共存<br>
   * 原因:https://github.com/spring-projects/spring-boot/issues/13042<br>
   * https://github.com/spring-projects/spring-boot/issues/13539
   *
   * @param repository repo
   * @return runner
   */
    @Bean
    ApplicationRunner init(UserRepository repository)
    {
      return args -> {
            String ip = InetAddress.getLocalHost().getHostAddress();
            int init = (int)(System.currentTimeMillis() % 10);
            User user1 = new User(1L, "account1", "张三 from " + ip, init + 20, new BigDecimal(100.00));
            User user2 = new User(2L, "account2", "李四 from " + ip, init + 30, new BigDecimal(180.00));
            User user3 = new User(3L, "account3", "王五 from " + ip, init + 40, new BigDecimal(280.00));
            Stream.of(user1, user2, user3).forEach(repository::save);
      };
    }
}
//goto ribbon-user\src\main\java\com\itmuch\cloud\study\repository\UserRepository.java
package com.itmuch.cloud.study.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.itmuch.cloud.study.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long>
{
}
//goto ribbon-user\src\main\resources\application.yml
server:
port: 8081
spring:
application:
    name: ribbon-user
jpa:
    generate-ddl: false
    show-sql: true
    hibernate:
      ddl-auto: create-drop
logging:
level:
    root: INFO
//goto ribbon-user\wait-for.sh
#!/bin/sh

TIMEOUT=15
QUIET=0

echoerr() {
if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
}

usage() {
exitcode="$1"
cat << USAGE >&2
Usage:
$cmdname host:port [-t timeout] [-- command args]
-q | --quiet                        Do not output any status messages
-t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
-- COMMAND ARGS                     Execute command with args after the test finishes
USAGE
exit "$exitcode"
}

wait_for() {
for i in `seq $TIMEOUT` ; do
    nc -z "$HOST" "$PORT" > /dev/null 2>&1

    result=$?
    if [ $result -eq 0 ] ; then
      if [ $# -gt 0 ] ; then
      exec "$@"
      fi
      exit 0
    fi
    sleep 1
done
echo "Operation timed out" >&2
exit 1
}

while [ $# -gt 0 ]
do
case "$1" in
    *:* )
    HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
    PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
    shift 1
    ;;
    -q | --quiet)
    QUIET=1
    shift 1
    ;;
    -t)
    TIMEOUT="$2"
    if [ "$TIMEOUT" = "" ]; then break; fi
    shift 2
    ;;
    --timeout=*)
    TIMEOUT="${1#*=}"
    shift 1
    ;;
    --)
    shift
    break
    ;;
    --help)
    usage 0
    ;;
    *)
    echoerr "Unknown argument: $1"
    usage 1
    ;;
esac
done

if [ "$HOST" = "" -o "$PORT" = "" ]; then
echoerr "Error: you need to provide a host and port to test."
usage 2
fi

wait_for "$@"

2. 部署方式

这边提供了3种docker部署方式


[*]自动ip(推荐)
[*]固定ip
[*]docker scale 水平扩展
分别对应上图的docker-auto-ip、docker-fix-ip、docker-scale 目录,有兴趣的同砚,可以研究研究!
四,功能演示

http://124.71.129.204:8085/doc.html
https://i-blog.csdnimg.cn/direct/710185515d794666b87da90b155ecb99.png#pic_center
https://i-blog.csdnimg.cn/direct/5c915db6c15e4f8a90a9e60c9c1e102c.png#pic_center
https://i-blog.csdnimg.cn/direct/29aa83c46a1a4821a75cc6a60b2e6cd6.png#pic_center
五,其他

此实例整合了gateway、ribbon、feign、hystrix、swagger,
大家会发现hystrix熔断器起作用时并不从负载均衡中移除故障节点,大家可以思考比较下hystrix和ribbon 异同!
有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 客户端负载均衡Ribbon实例