客户端负载均衡Ribbon实例
一般来说,提到负载均衡,大家一般很容易想到浏览器 -> NGINX -> 反向代理多个Tomcat这样的架构图——业界管这种负载均衡模式叫“服务器端负载均衡”,因为此种模式下,负载均衡算法是NGINX提供的,而NGINX部署在服务器端。
一,概述
一般来说,提到负载均衡,大家一般很容易想到浏览器 -> 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之间的代码接口。
三,项目源码
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
五,其他
此实例整合了gateway、ribbon、feign、hystrix、swagger,
大家会发现hystrix熔断器起作用时并不从负载均衡中移除故障节点,大家可以思考比较下hystrix和ribbon 异同!
有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-
更多推荐
所有评论(0)