首先展示一下源自网络的springCloud组件架构图:
结合这张图来说一下springCloud的应用过程:
从上面这张图中也可以看出SpringCloud主要的组件,解释一下这些组件都具体是做什么用的:
整个项目分为四个模块consumer、provider、euraka、gateway
consumer结构
eureka结构
gateway文件结构
provider文件结构
<?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>org.example</groupId>
<artifactId>springCloud</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<gson.version>2.8.0</gson.version>
<jackson.version>2.9.5</jackson.version>
<swagger.version>2.9.2</swagger.version>
<mybatis.plus.boot.version>3.1.0</mybatis.plus.boot.version>
<hutool.version>5.7.19</hutool.version>
<druid.version>1.1.10</druid.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.8.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
主要的部分就是Properties和dependency两部分,本次测试项目中并没有使用到数据库,但实际业务需要使用到,所以记得添加
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springCloud</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>provider</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
@RestController
@RequestMapping("provider")
public class ProviderController {
//使用Map模拟数据库
private Map<Integer, User> map;
{
map = new HashMap<>();
map.put(1, new User(1, "Johnny"));
map.put(2, new User(2, "Timmy"));
}
@GetMapping("/findAll")
public List<User> findAll() {
System.out.println("Provider::findAll");
List<User> list = new ArrayList<>();
for (Map.Entry<Integer, User> users : map.entrySet()) {
list.add(users.getValue());
}
return list;
}
@GetMapping("/findById/{id}")
public User findById(@PathVariable("id") Integer id) {
return map.get(id);
}
}
pojo层代码如下
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String name;
}
ProviderApplication启动类代码如下
@SpringBootApplication
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springCloud</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>consumer</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
Controller层代码如下:
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/findAll")
public List<User> findAll() {
/*因为我们是消费者,我们只能通过访问生产者的方式来获取数据
SpringCloud 基于springBoot的restFul(http)协议*/
//provider的请求url
System.out.println("Consumer:: findAll");
String baseUrl = "http://localhost:8081/provider/findAll";
List list = restTemplate.getForObject(baseUrl, List.class);
return list;
}
@GetMapping("/findById/{id}")
public User findById(@PathVariable("id") Integer id) {
String baseUrl = "http://localhost:8081/provider/findById/" + id;
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
pojo层代码如下
和provider的完全一致
Consumer启动类代码如下
@SpringBootApplication
public class ConsumerApplication {
//springBoot没有主动注入RestTemplate,需要我们手动注入
@Autowired
private RestTemplateBuilder templateBuilder;
@Bean
public RestTemplate restTemplate() {
return templateBuilder.build();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class);
}
}
<component name="RunDashboard">
<option name="configurationTypes">
<set>
<option value="SpringBootApplicationConfigurationType" />
set>
option>
component>
后重启IDEA使之生效
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springCloud</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
@SpringBootApplication
@EnableEurekaServer //声明当前模块是一个Eureka服务端
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class);
}
}
配置文件
server:
# 实际业务不要用这个端口号,易被攻击
port: 8761
#声明注册中心的url
eureka:
client:
service-url:
defaultZone: http://0.0.0.0:8761/eureka
# 关闭注册自己
fetch-registry: false
register-with-eureka: false
Done!! 启动程序,url访问localhost:8761
我们看到Instance currently registered with Eureka中空空如也,这是因为我们没有进行对微服务的注册,重申一遍:所有的微服务都需要注册到注册中心
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
2、启动类上加注解
@EnableDiscoveryClient
这里想说一下@EnableDiscoveryClient和@EnableEurekaClient
从表面意思上看,似乎是@EnableEurekaClient更应该用在这里,但实际上这个注解已经是老版了,应用场景单一
server:
port: 8081
# 服务名称
spring:
application:
name: provider
#eureka配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/findAll")
public List<User> findAll() {
/*在provider和consumer都注册到注册中心后,可以通过注册的名称来获取Ip地址*/
//通过discoveryClient获得的是一个实例列表,因为通常我们会做负载均衡,所以不止一个实例
System.out.println("Eureka:: Consumer");
List<ServiceInstance> providers = discoveryClient.getInstances("provider");
ServiceInstance serviceInstance = providers.get(0);
URI uri = serviceInstance.getUri();
System.out.println(uri);
String scheme = serviceInstance.getScheme();
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
//拼接url
String eurekaUrl = scheme + "://" + host + ":" + port + "/provider/findAll";
System.out.println(eurekaUrl);
List list = restTemplate.getForObject(eurekaUrl, List.class);
return list;
}
@GetMapping("/findById/{id}")
public User findById(@PathVariable("id") Integer id) {
String baseUrl = "http://localhost:8081/provider/findById/" + id;
User user = restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
运行Consumer后使用postman调用findAll接口,控制台显示如下
@SpringBootApplication
@EnableDiscoveryClient
public class ConsumerApplication {
//springBoot没有主动注入RestTemplate,需要我们手动注入
@Autowired
private RestTemplateBuilder templateBuilder;
@Bean
@LoadBalanced //开启负载均衡 只增加了这里请注意,其余都不变
public RestTemplate restTemplate() {
return templateBuilder.build();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class);
}
}
以下仅显示Conusmer的findAll的更改,其余模块没有更改
@GetMapping("/findAll")
public List<User> findAll() {
/*方案3:开启负载均衡后,通过discoveryClient获得的是provider的列表*/
//第一个provider是实例名称,即在provider配置文件中的name
String balancedUrl = "http://provider/provider/findAll";
List list = restTemplate.getForObject(balancedUrl, List.class);
return list;
}
#RandomRule 随机
#RoundRobinRule 轮询
#provider是服务名,记得切换称自己的
provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
如果设置全局负载均衡策略,就需要在consumer的配置类中返回一个配置
像这样
@Bean
public IRule defaultLBStrategy(){
return new RandomRule();
}
!!!需要注意的是,两者不能同时用,用其中一个的时候,记得注释另一个
插播! 插播! 紧急插播 Hystrix原理
先上手操作,再了解一下原理,还是蛮重要的
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
# 配置熔断策略:
hystrix:
command:
default:
circuitBreaker:
# 原理分析中解释配置含义
# 强制打开熔断器 默认false关闭的。测试配置是否生效
forceOpen: false
# 触发熔断错误比例阈值,默认值50%
errorThresholdPercentage: 50
# 熔断后休眠时长,默认值5秒
sleepWindowInMilliseconds: 5000
# 熔断触发最小请求次数,默认值是20
requestVolumeThreshold: 10
execution:
isolation:
thread:
# 熔断超时设置,默认为1秒
timeoutInMilliseconds: 2000
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@HystrixCommand(fallbackMethod = "findAllFallBack")
@GetMapping("/findAll")
public List<User> findAll() {
/* 方案2: 在provider和consumer都注册到注册中心后,可以通过注册的名称来获取Ip地址*/
//通过discoveryClient获得的是一个实例列表,因为通常我们会做负载均衡,所以不止一个实例
// System.out.println("Eureka:: Consumer");
List<ServiceInstance> providers = discoveryClient.getInstances("provider");
ServiceInstance serviceInstance = providers.get(0);
URI uri = serviceInstance.getUri();
System.out.println(uri);
String scheme = serviceInstance.getScheme();
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
//拼接url
String eurekaUrl = scheme + "://" + host + ":" + port + "/provider/findAll";
System.out.println(eurekaUrl);
List list = restTemplate.getForObject(eurekaUrl, List.class);
return list;
}
public List<User> findAllFallBack() {
System.out.println("Ops!! find All Fall Back");
return null;
}
}
!! 注意:FallBack方法的返回值必须和FindAll方法一致
在Provider的findAll方法中让线程睡几秒,触发熔断
因为我们配置的触发熔断时间为1秒,所以当请求返回超过1秒时就会触发熔断
PS:当我们需要所有的请求都共用一个FallBack方法时,可以配置全局熔断器
只需要两步
1、在Controller类(注意这里是类不是方法)上添加注解 @DefaultProperties(defaultFallback = “defaultFallback”)
2、在目标方法上添加注解 @HystrixCommand (注意不需要再次指定fallbackMethod)
ps:暗戳戳的说一句,感觉也没有省事到哪里去
老样子,先操作后理解
插播! 插播! Feign的插播
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
@EnableFeignClients //开启feign客户端
@FeignClient(value = "provider",path = "/provider")
public interface ConsumerClient {
//伪装成ProviderController中的findAll
@GetMapping("/findAll")
List<User> findAll() throws Exception;
}
@RestController
@RequestMapping("consumer")
public class ConsumerController {
//注入Feign客户端
@Qualifier("com.dean.feign.ConsumerClient")
@Autowired
private ConsumerClient consumerClient;
@GetMapping("/findAll")
public List<User> findAll() throws Exception {
/*方案4:feign客户端方式访问*/
return consumerClient.findAll();
}
}
是的,Feign里面也结合了熔断器,让熔断器的实现更加容易
如果需要进一步配置feign的熔断器,需要添加依赖
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-hystrixartifactId>
<version>9.7.0version>
dependency>
然后在配置文件里进行配置
feign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 15000
threadpool:
default:
coreSize: 40
maximumSize: 100
maxQueueSize: 100
如不需要这些高级配置只需要按下列步骤进行即可
@Component
public class ConsumerFallBack implements ConsumerClient {
//feign Hystrix 服务降级方法
@Override
public List<User> findAll() throws Exception {
System.out.println("feign Hystrix");
return null;
}
}
# feign 熔断器
feign:
hystrix:
enabled: true
@FeignClient(value = "provider",path = "/provider",fallback = ConsumerFallBack.class)
public interface ConsumerClient {
//伪装成ProviderController中的findAll
@GetMapping("/findAll")
List<User> findAll() throws Exception;
}
Gateway的pom文件中添加依赖
注意这里有个大坑,因为我们之前在父工程的中添加了spring-boot-starter-web的依赖,但是网关使用时不能有这个依赖,所以需要我们从父工程中移除,分别添加到使用到这个依赖的子工程中(在本项目里就是consumer、provider、eureka)
一定要移除,移除完成后在gateway的pom文件中添加如下依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
@EnableDiscoveryClient
@SpringBootApplication
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class);
}
}
server:
port: 9000
# 服务名称
spring:
application:
name: gateway
# 网关配置
cloud:
gateway:
# 全局配置
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 *表示通配 允许所有的域 实际可以填写ip
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
# 网关路由配置
routes:
- id: provider-router # 路由id 唯一标识
uri: lb://provider # 路由地址,动态路由 lb是一个网关的协议
predicates: # 断言
- Path=/provider/** # 请求url 多个使用逗号 即所有的/provider/**请求会交由uri中写的服务provider来处理
#eureka配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class TokenFliter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("There is GateWay");
return chain.filter(exchange);
}
//可能有多个网关,执行顺序由getOrder决定
@Override
public int getOrder() {
return 0;
}
}
predicates:
- Path=/**
filters:
- PrefixPath=/provider
测试
可以看到通过该配置,即隐藏了实际服务的端口号也隐藏了请求的前缀
predicates:
- Path=/api/**
filters:
- StripPrefix=1
predicates: # 断言
- Path=/api/** #
filters:
- StripPrefix=1
- PrefixPath=/provider
但一定要注意StripPrefix和PrefixPath的顺序问题
写成现在这样的意思就是先去掉/api,再添加/provider,所以请求的时候可以直接用/api/findAll请求
过滤器是网关的一个重要功能,实现了请求的鉴权,前面路由前缀的功能也是使用过滤器实现的
过滤器分为全局过滤器和局部过滤器
可以同时配置全局过滤器和局部过滤器
全局过滤器(实现GlobalFilter及Ordered)
例如上面所说的TokenFilter
@Component
public class TokenFliter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("There is GateWay");
return chain.filter(exchange);
}
//设置过滤器执行顺序,值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
自定义局部过滤器
有两种实现方式
package com.dean.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
//类名的格式必须为XXXGatewayFilterFactory
@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> {
public static final String AUTH_NAME = "name";
//构造函数
public AuthGatewayFilterFactory() {
super(Config.class);
}
//如果要获得配置的-Auth的值,必须重写这个方法,否则拿不到
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(AUTH_NAME);
}
//过滤逻辑
@Override
public GatewayFilter apply(AuthGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
System.out.println("AuthGateWay Filter");
System.out.println(config.name);
//可以通过exchange拿到uri,进行拼接或处理 参考PrefixPathGateWayFilterFactory
return chain.filter(exchange);
};
}
//用于接收配置文件中的参数
public static class Config {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
主要有几个重点:
1、类名必须是XXXGatewayFilterFactory
2、配置文件里的配置就是这个XXX
3、shortcutFieldOrder()方法必须重写,否则拿不到配置的值
gateway的yaml中路由处进行配置
# 网关路由配置
routes:
#id是个列表 有多个路由配置时就写多个id及下面的配置,以下是完整的一个
- id: provider-router # 路由id 唯一标识
uri: lb://provider # 路由地址,动态路由 lb是一个网关的协议
predicates: # 断言
- Path=/**
# - Path=/api/** # 请求url 多个使用逗号 即所有的/provider/**请求会交由uri中写的服务provider来处理
# 添加filter后,会在predicates上的所有请求上添加/provider
# 所以实际请求就不需要再写/provider了
filters:
# - StripPrefix=1
- PrefixPath=/provider
- Auth=name #过滤器的名称取得是AuthGatewayFilterFactory去掉GatewayFilterFactory的部分
其实只是多了一个我们自己增加的-Auth
PS:当我们想要配置多个路由及每个路由配置自己的过滤器时
从配置中可以看出route下的-id是个列表的格式
彩蛋:GateWay自带的过滤器
gateway自带的过滤器有几十个,常见的有
过滤器名称 | 说明 |
---|---|
AddRequestHeader | 对匹配上的请求加上Header |
AddRequestParameters | 对匹配上的请求路由 |
AddResponseHeader | 对从网关返回的响应添加Header |
StripPrefifix | 对匹配上的请求路径去除前缀 |
PrefifixPath | 对匹配上的请求路径添加前缀 |
例如配置文件中配置
spring:
application:
name: gateway
cloud:
gateway:
default-filters:
- AddResponseHeader=ilove,web
请求后就可以看到响应头添加了ilove=web了
感兴趣的话,可以去官方文档看看
以上。