Spring Cloud Alibaba微服务从入门到进阶·笔记

目录
整合Spring-Cloud-Alibaba
服务发现nacos
实现负载均衡Ribbon
声明式HTTP客户端Feign
服务容错Sentinel
消息驱动的微服务SpringcloudAlibabaRocketMQ
API网关SpringCloudGateway

版本对齐

整合Spring-Cloud-Alibaba

  • 整合Spring Cloud

参考官方文档

  1. 在pom.xml加入
<properties>
    <spring.cloud-version>Hoxton.SR8</spring.cloud-version>
</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>
</dependencyManagement>
  • 整合Spring Cloud Alibaba

根据使用的spring-cloud版本查询对应的Spring Cloud Alibaba的版本spring-cloud-alibaba组件版本关系

<properties>
    <spring.cloud-version>Hoxton.SR8</spring.cloud-version>
</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>
    <!--整合spring-cloud-alibaba-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>2.2.3.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
</dependencies>
</dependencyManagement>

服务发现 nacos

  1. 搭建Nacos Server

下载spring-cloud-alibaba对应的nacos版本

  1. 加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>0.9.0.RELEASE</version>
</dependency>
  1. 加配置yml
spring:
  cloud:
    nacos:
      discovery:
        # 指定nacos server地址,不要加http/https
        server-addr: 47.98.37.74:8848
  application:
    # 服务名称尽量用一,不要用_,不要用特殊字符
    name: cloud-horizon-user
/**
* 测试: 服务发现,证明内容中心总能找到用户中心
* @return 商城中心所有实例的地址信息
*/
@GetMapping("/findAllInstance")
public Result findAllInstance(){
//查询指定服务的所有实例信息
List<ServiceInstance> instances = discoveryClient.getInstances("cloud-horizon-store");
return Result.success(instances);
}

RestTemplate 请求服务调用

  • 整合RestTemplate
/**
* @author rp
*/
@Configuration
public class RestTemplateConfig {
/**
* @SentinelRestTemplate RestTemplate整合Sentinel
* */
@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
return new RestTemplate(factory);
}

@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(15000);
factory.setReadTimeout(10000);
return factory;
}
}
  • 案例
@GetMapping("/restTemplateStore")
public Result restTemplateStore(){
    String url = discoveryClient.getInstances("cloud-horizon-store").stream()
.map(instance -> instance.getUri().toString() + "cloud_horizon_store/userShop/test")
.findFirst()
.orElseThrow(() -> new CloudHorizonException(ResultEnum.NOT_FIND_INSTANCE));
    JSONObject forObject = restTemplate.getForObject(url, JSONObject.class);
    assert forObject != null;
return Result.successJsonToList(forObject,UserShopDTO.class);
}
  1. 服务发现的领域模型
spring:
  cloud:
    nacos:
      discovery:
        # 指定nacos server地址
        server-addr: 47.98.37.74:8848
        # 指定namespace
        namespace: c01f509a-f12d-45ba-ad18-4a4ee6f65ad1
        # 指定集群名称
        cluster-name: CD
        # 元数据
        metadata:
          auth: Mr培
          vaersion: 1

实现负载均衡 Ribbon

  • 随机负载均衡编码实现
/**
* 随机负载均衡
* */
@GetMapping("/randomRestTemplateStore")
public Result randomRestTemplateStore(){
    List<String> urls = discoveryClient.getInstances("cloud-horizon-store").stream()
        .map(instance -> instance.getUri().toString() + "cloud_horizon_store/userShop/test").collect(Collectors.toList());
    int i = ThreadLocalRandom.current().nextInt(urls.size());
    JSONObject forObject = restTemplate.getForObject(urls.get(i), JSONObject.class);
    assert forObject != null;
return Result.successJsonToList(forObject,UserShopDTO.class);
}
  • Ribbon 实现负载均衡
  1. 为RestTemplate整合Ribbon
  /**
  * @LoadBalanced RestTemplate整合Sentinel
  * */
  @Bean
  @LoadBalanced
  public RestTemplate restTemplate(ClientHttpRequestFactory factory){
      return new RestTemplate(factory);
  }
  1. 重构代码
/**
* ribbon重构 随机负载均衡
* */
@GetMapping("/ribbonRestTemplateStore")
public Result ribbonRestTemplateStore(){
JSONObject forObject = restTemplate.getForObject("http://cloud-horizon-store/cloud_horizon_store/userShop/test", JSONObject.class);
assert forObject != null;
 return Result.successJsonToList(forObject,UserShopDTO.class);
}
截屏2020-12-27 下午10.40.30.png
  • 细粒度配置自定义
    1. Java代码配置 方式

创建configuration包,创建CloudHorizonStoreRibbonConfiguration类

/**
 * ribbon规则代码配置
 * RibbonClients 全局配置方式
 * @author rp
 */
@Configuration
@RibbonClient(name = "cloud-horizon-store",configuration = RibbonConfiguration.class)
public class CloudHorizonStoreRibbonConfiguration {   
}

创建ribbonconfiguration包,必须在启动类所在包之外,创建RibbonConfiguration

/**
 * nacos调用规则
 * @author rp
 */
@Configuration
public class RibbonConfiguration {
//    随机规则
//    @Bean
//    public IRule ribbonRule(){
//        return new RandomRule();
//    }
//    权重规则
//    @Bean
//    public IRule ribbonRule(){
//        return new NacosWeightedRule();
//    }
    @Bean
    public IRule ribbonRule(){
        return new NacosSameClusterWeightedRule();
    }
}

父子上下文重叠会造成事物不生效,重复扫描
2. 配置属性 方式

# ribbon规则属性配置
cloud-horizon-user:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

截屏2020-12-27 下午10.41.47.png

3. 全局配置 方式

/**
 * ribbon规则代码配置
 * RibbonClients 全局配置方式
 * @author rp
 */
@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class CloudHorizonStoreRibbonConfiguration {
}
  • Ribbon 饥饿加载
    第一次请求非常慢,开启饥饿加载
# 饥饿加载 
# clients 请求哪些微服务的时候饥饿加载 
ribbon:
  eager-load:
    enabled: true
    clients: cloud-horizon-user,cloud-horizon-store
  • 扩展Ribbon-权重支持

创建 NacosWeightedRule 类

/**
 * ribbon支持nacos权重
 * @author rp
 */
public class NacosWeightedRule implements IRule {
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    private ILoadBalancer lb;
    
    @Override
    public Server choose(Object key) {
        BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
//        想要请求的微服务的名称
        String name = loadBalancer.getName();
        //拿到服务发现的相关API
        NamingService namingService =  nacosDiscoveryProperties.namingServiceInstance();
        // nacos client自动通过基 于权重的负载均衡算法,给我们选择一个实例
        try {
            Instance instance = namingService.selectOneHealthyInstance(name);
            return new NacosServer(instance);
        } catch (NacosException e) {
            e.printStackTrace();
            return null;
        }
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
        this.lb = lb;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return lb;
    }
}

修改规则配置 RibbonConfiguration

/**
 * nacos调用规则
 * @author rp
 */
@Configuration
public class RibbonConfiguration {
    @Bean
    public IRule ribbonRule(){
        return new NacosWeightedRule();
    }
}
  • 扩展Ribbon-同集群优先

属性配置集群名称

spring:
  cloud:
    nacos:
      discovery:
        # 指定nacos server地址
        server-addr: 47.98.37.74:8848
        #指定集群名称
        cluster-name: CD

在configuration包下创建 NacosSameClusterWeightedRule类

/**
 * 相同集群调用
 * @author rp
 */
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;

@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {

}

@Override
public Server choose(Object key) {
    try {
        #拿到配置文件中的集群名称CD
        String clusterName = nacosDiscoveryProperties.getClusterName();
        BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
        //想要请求的微服务的名称
        String name = loadBalancer.getName();
        //拿到服务发现的相关API
        NamingService namingService =  nacosDiscoveryProperties.namingServiceInstance();
        // 1.找到指定服务的所有实例A
        List<Instance> instances = namingService.selectInstances(name, true);
        // 2.过滤出相同集群下的所有实例B
        List<Instance> sameClusterInstances = instances.stream().filter(instance -> Objects.equals(instance.getClusterName(), clusterName)).collect(Collectors.toList());
        // 3.如果B是空,就用A
        List<Instance> instancesToBeChosen = new ArrayList<>();
        if (CollectionUtil.isEmpty(sameClusterInstances)){
            instancesToBeChosen = instances;
        }else{
            instancesToBeChosen = sameClusterInstances;
        }
        // 4.基于权重的负载均衡算法,返回1个实例
        Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
        return new NacosServer(instance);
    } catch (NacosException e) {
        e.printStackTrace();
        return null;
    }
}

}

class ExtendBalancer extends Balancer {
    public static Instance getHostByRandomWeight2(List<Instance> hosts){
        return getHostByRandomWeight(hosts) ;
    }
}

修改nacos调用规则

/**
 * nacos调用规则
 * @author rp
 */
@Configuration
public class RibbonConfiguration {
    @Bean
    public IRule ribbonRule(){
        return new NacosSameClusterWeightedRule();
    }
}
  • 扩展Ribbon-基于元数据的版本控制

属性文件配置元数据

spring:
  cloud:
    nacos:
      discovery:
        metadata:
          auth: Mr培
          vaersion: 1
  • 深入理解Nacos的Namespace

属性文件配置Namespace

spring:
  cloud:
    nacos:
      discovery:
        # 指定namespace
        namespace: c01f509a-f12d-45ba-ad18-4a4ee6f65ad1

服务之间不能跨Namespace调用

声明式HTTP客户端-Feign

  • 使用Feign实现远程HTTP调用
  1. 加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 在启动类上加注解 @EnableFeignClients
/**
 * @author rp
 */
@EnableAsync
@SpringBootApplication
@EnableFeignClients
public class ServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }

}
  1. 编写HTTP请求
    创建feignclient包,创建interface接口 CloudHorizonStoreFeignClient类
/** 
 * name 请求微服务的名称
 * @author rp
 */
@FeignClient(name = "cloud-horizon-store")
public interface CloudHorizonStoreFeignClient {

    /**
     * http://cloud-horizon-store/cloud_horizon_store/userShop/test
     * 获取所有用户店铺信息
     * @return
     */
    @GetMapping("/cloud_horizon_store/userShop/test")
    @SentinelResource("findUserShopAll")
    JSONObject findUserShopAll();
}
  • Feign的组成
    截屏2020-12-27 下午10.42.41.png

    自定义Feign日志级别
  1. Java代码方式
    修改feign的interface接口文件添加
    onfiguration = CloudHorizonStoreFeighConfiguration.class
/**
 * @author rp
 */
@FeignClient(name = "cloud-horizon-store",configuration = CloudHorizonStoreFeighConfiguration.class)
public interface CloudHorizonStoreFeignClient {
    /**
     * 获取所有用户店铺信息
     * @return
     */
    @GetMapping("/cloud_horizon_store/userShop/test")
    @SentinelResource("findUserShopAll")
    JSONObject findUserShopAll();
}

在configuration包下编写 CloudHorizonStoreFeighConfiguration 配置类

/**
 * feign的配置类
 * 这个类别加@Configuration注解了,否则必须挪到@ComponentScan能扫描的包以外
 * 父子上下文重复扫描问题
 * @author rp
 */
public class CloudHorizonStoreFeighConfiguration {

    @Bean
    public Logger.Level level(){
//        让feign打印所有请求的细节
        return Logger.Level.FULL;
    }
}

将CloudHorizonStoreFeighConfiguration配置类通过属性文件配置logging

logging:
  level:
    com:
      cloud:
        horizon:
          server:
            domain:
              mapper: DEBUG                              
  com.cloud.horizon.user.server.feignclient.CloudHorizonStoreFeignClient: DEBUG
  1. 属性配置方式


    截屏2020-12-27 下午10.43.22.png
feign:
  client:
    config:
      #想要调用的微服务的名称
      #cloud-horizon-store:
        #loggerLevel: full
      #全局配置
      default:
        loggerLevel: full

通过代码配置全局日志,在启动类上@EnableFeignClients注解添加defaultConfiguration属性

/**
 * @author rp
 */
@EnableAsync
@SpringBootApplication
//全局配置feign日志级别
@EnableFeignClients(defaultConfiguration = CloudHorizonStoreFeighConfiguration.class)
public class ServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }

}
  • 总结
截屏2020-12-27 下午10.46.31.png

截屏2020-12-27 下午10.45.22.png

截屏2020-12-27 下午10.45.45.png
  • Feign的继承
  • Feign多参数请求组构造

跟springMVC用法一样

GET(其它传参方式百度)
  1. 提供服务方
@GetMapping("/findUserShop")
public Result findUserShop(ShopDTO shopDTO){
    return Result.success(shopDTO);
}
  1. 调用方
@Autowired
private StoreCloudHorizonStoreFeignClient storeCloudHorizonStoreFeignClient;

@GetMapping("/findUserShop")
public Result findUserShop(ShopDTO shopDTO){
    JSONObject forObject = storeCloudHorizonStoreFeignClient.findUserShop(shopDTO);
    return Result.successJsonToList(forObject,UserShopDTO.class);
}
  1. feign接口
/**
 * @author rp
 */
@FeignClient(name = "cloud-horizon-store")
public interface StoreCloudHorizonStoreFeignClient {
    /**
     * 获取所有用户店铺信息
     * @return
     */
    @GetMapping("/cloud_horizon_store/userShop/findUserShop")
    JSONObject findUserShop(@SpringQueryMap ShopDTO shopDTO);
}
  1. 配置文件加注解,解决FeignClient重名问题(@FeignClient(name = "cloud-horizon-store"))
spring:
  main:
   allow-bean-definition-overriding: true
POST(自行百度)

跟springMVC用法一样

  • Feign脱离Ribbon使用
@FeignClient(name = "baidu",url="http://www.baidu.com")
public interface TestBaiduFeignClient {
    @GetMapping("/baidu")
    public String index();
}

@FeignClient(name = "baidu",url="http://www.baidu.com") name必须存在

  • RestTemplate和Feign对比


    截屏2020-12-27 下午10.47.18.png
  • Feign性能优化
    连接池

  1. 加依赖
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
  1. 写配置
feign:
  client:
    config:
      #全局配置
      default:
        loggerLevel: full
  httpclient:
    #让feign使用apache httpclient做请求;而不是默认的urlconnection
    enabled: true
    #feign的最大连接数
    max-connections: 200
    #feign单个路径的最大连接数
    max-connections-per-route: 50

日志级别

不建议设置日志级别为full

  • Feign其它问题,百度学习

服务容错-Sentinel

  • 整合Sentinel
  1. 加依赖为应用整合sentinel
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  1. 加依赖为应用整合actuator
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 加配置开启actuator
management:
  #endpoint:
    #health:
      #show-details: always
  #激活所有的actuator端点
  endpoints:
    web:
      exposure:
        include: '*'

访问 localhost:8010/actuator/sentinel

  • Sentinel控制台
  1. 搭建Sentinel控制台
    下载地址
    下载与项目相同版本Sentinel
    通过命令启动
java -jar sentinel-dashboard-1.6.2.jar
  1. 为应用整合控制台
spring:
  cloud:
   sentinel:
     filter:
       #关闭掉对Spring MVC端点的保护
       enabled: false
   transport:
     #指定sentinel控制台的地址
     dashboard: localhost:8085

相关文章
sentinel 控制台监控空白的问题

  1. 触点链路暴露 @SentinelResource("findUserShopAll")
/**
 * @author rp
 */
@FeignClient(name = "cloud-horizon-store")
public interface CloudHorizonStoreFeignClient {
    /**
     * 获取所有用户店铺信息
     * @return
     */
    @GetMapping("/cloud_horizon_store/userShop/test")
    @SentinelResource("findUserShopAll")
    JSONObject findUserShopAll();
}
  • Sentinel具体用法百度


    截屏2020-12-27 下午10.47.55.png
  • RestTemplate整合Sentinel

为RestTemplate加上@SentinelRestTemplate注解

/**
 * @SentinelRestTemplate RestTemplate整合Sentinel
 * */
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate restTemplate(ClientHttpRequestFactory factory){
    return new RestTemplate(factory);
}

关闭@SentinelRestTemplate注解

resttemplate :
  sentinel :
    #关闭@SentinelRestTemplate注解
    enabled: false

限流友好提示 百度
@SentinelResource(value = "/blockTemplateStore",blockHandler = "block",fallback = "fallback")
blockHandler 限流或者降级,fallback降级

/**
 * 发生降级时,定制处理
 * */
@GetMapping("/blockTemplateStore")
@SentinelResource(value = "/blockTemplateStore",blockHandler = "block",fallback = "fallback")
public Result blockTemplateStore(){
    JSONObject forObject = restTemplate.getForObject("http://cloud-horizon-store/cloud_horizon_store/userShop/test", JSONObject.class);
    assert forObject != null;
    return Result.successJsonToList(forObject,UserShopDTO.class);
}
/**
 * 处理限流或降级
 * 用户相同的返回值,参数
 * */
public Result block(BlockException e) {
    log.warn("限流,或者降级了",e);
    return Result.success();
}
/**
 * 处理降级 sentinel 1.6以上 可以处理Throwable(所有异常)
 * 用户相同的返回值,参数 ,不能抛异常
 * */
public Result fallback() {
    log.warn("降级了");
    return Result.success();
}

限流或降级 独立处理 blockHandlerClass 属性

@SentinelResource(value = "/blockTemplateStore",
        fallback = "fallback",
        blockHandlerClass = SentinelBlockHandlerClass.class)

创建 SentinelBlockHandlerClass类

/**
 * @author rp
 */
@Slf4j
public class SentinelBlockHandlerClass {
    /**
     * 用户相同的返回值,参数
     * */
    public static Result block(BlockException e) {
        log.warn("限流,或者降级了",e);
        return Result.success();
    }
}

sentinel 1.6 以上支持 fallbackClass

  • Feign整合Sentinel

添加配置属性

feign:
  sentinel:
    # 为feign整合sentinel
    enabled: true

限流降级发生时,如何定制自己的逻辑处理?

  1. 修改CloudHorizonStoreFeignClient类,添加属性 fallback
/**
 * @author rp
 */
@FeignClient(name = "cloud-horizon-store",fallback = CloudHorizonStoreFeignClientFallback.class)
public interface CloudHorizonStoreFeignClient {
    /**
     * 获取所有用户店铺信息
     * @return
     */
    @GetMapping("/cloud_horizon_store/userShop/test")
    @SentinelResource(value = "findUserShopAll")
    JSONObject findUserShopAll();
}
  1. 创建 CloudHorizonStoreFeignClientFallback类
/**
 * @author rp
 */
@Component
public class CloudHorizonStoreFeignClientFallback implements CloudHorizonStoreFeignClient{
    @Override
    public JSONObject findUserShopAll() {
        //处理逻辑,比如返回一个默认对象 JSONObject
        return null;
    }
}
  1. 添加属性fallbackFactory,fallbackFactory 比 fallback强大,二者存其一
    fallbackFactory可以拿到异常
/**
 * @author rp
 */
//@FeignClient(name = "cloud-horizon-store",configuration = CloudHorizonStoreFeighConfiguration.class)
@FeignClient(name = "cloud-horizon-store",
//        fallback = CloudHorizonStoreFeignClientFallback.class,
fallbackFactory = CloudHorizonStoreFeignClientFallbackFactory.class)
public interface CloudHorizonStoreFeignClient {

    /**
     * 获取所有用户店铺信息
     * @return
     */
    @GetMapping("/cloud_horizon_store/userShop/test")
    @SentinelResource(value = "findUserShopAll")
    JSONObject findUserShopAll();
}
  1. 创建CloudHorizonStoreFeignClientFallbackFactory类
/**
 * @author rp
 */
@Slf4j
@Component
public class CloudHorizonStoreFeignClientFallbackFactory implements FallbackFactory<CloudHorizonStoreFeignClient> {
    @Override
    public CloudHorizonStoreFeignClient create(Throwable throwable) {
        return new CloudHorizonStoreFeignClient(){
            @Override
            public JSONObject findUserShopAll() {
                log.warn("远程调用被限流/降级了",throwable);
                //处理逻辑,比如返回一个默认对象 JSONObject
                return null;
            }
        };
    }

}
  • Sentinel使用姿势总结


    截屏2020-12-27 下午10.49.14.png
  • 规则持久化-拉模式

Alibaba Sentinel规则持久化-拉模式-手把手教程【基于文件】

  1. 加依赖
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-extension</artifactId>
</dependency>
  1. 写代码
/**
 * 拉模式规则持久化
 *
 * @author itmuch.com
 */
public class FileDataSourceInit implements InitFunc {
    @Override
    public void init() throws Exception {
        // TIPS: 如果你对这个路径不喜欢,可修改为你喜欢的路径
        String ruleDir = System.getProperty("user.home") + "/sentinel/rules";
        String flowRulePath = ruleDir + "/flow-rule.json";
        String degradeRulePath = ruleDir + "/degrade-rule.json";
        String systemRulePath = ruleDir + "/system-rule.json";
        String authorityRulePath = ruleDir + "/authority-rule.json";
        String paramFlowRulePath = ruleDir + "/param-flow-rule.json";

        this.mkdirIfNotExits(ruleDir);
        this.createFileIfNotExits(flowRulePath);
        this.createFileIfNotExits(degradeRulePath);
        this.createFileIfNotExits(systemRulePath);
        this.createFileIfNotExits(authorityRulePath);
        this.createFileIfNotExits(paramFlowRulePath);

        // 流控规则
        ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
            flowRulePath,
            flowRuleListParser
        );
        // 将可读数据源注册至FlowRuleManager
        // 这样当规则文件发生变化时,就会更新规则到内存
        FlowRuleManager.register2Property(flowRuleRDS.getProperty());
        WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
            flowRulePath,
            this::encodeJson
        );
        // 将可写数据源注册至transport模块的WritableDataSourceRegistry中
        // 这样收到控制台推送的规则时,Sentinel会先更新到内存,然后将规则写入到文件中
        WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);

        // 降级规则
        ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
            degradeRulePath,
            degradeRuleListParser
        );
        DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
        WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
            degradeRulePath,
            this::encodeJson
        );
        WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);

        // 系统规则
        ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
            systemRulePath,
            systemRuleListParser
        );
        SystemRuleManager.register2Property(systemRuleRDS.getProperty());
        WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
            systemRulePath,
            this::encodeJson
        );
        WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);

        // 授权规则
        ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
            authorityRulePath,
            authorityRuleListParser
        );
        AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
        WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
            authorityRulePath,
            this::encodeJson
        );
        WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);

        // 热点参数规则
        ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
            paramFlowRulePath,
            paramFlowRuleListParser
        );
        ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
        WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
            paramFlowRulePath,
            this::encodeJson
        );
        ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
    }

    private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
        source,
        new TypeReference<List<FlowRule>>() {
        }
    );
    private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
        source,
        new TypeReference<List<DegradeRule>>() {
        }
    );
    private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
        source,
        new TypeReference<List<SystemRule>>() {
        }
    );

    private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
        source,
        new TypeReference<List<AuthorityRule>>() {
        }
    );

    private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
        source,
        new TypeReference<List<ParamFlowRule>>() {
        }
    );

    private void mkdirIfNotExits(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            file.mkdirs();
        }
    }

    private void createFileIfNotExits(String filePath) throws IOException {
        File file = new File(filePath);
        if (!file.exists()) {
            file.createNewFile();
        }
    }

    private <T> String encodeJson(T t) {
        return JSON.toJSONString(t);
    }
}
  1. 配置
    在resources下创建目录META-INF/services,创建文件
    com.alibaba.csp.sentinel.init.InitFunc
    内容改成上面FileDataSourceInit的包名类名全路径即可。
    com.itmuch.contentcenter.FileDataSourceInit
  • 规则持久化-推模式

Alibaba Sentinel规则持久化-推模式-手把手教程【基于Nacos】

截屏2020-12-27 下午10.50.10.png

  • 集群流控


    截屏2020-12-27 下午10.50.52.png
  • 扩展Sentinel-错误页优化

@Component
public class MyUrlBlockHandler implements UrlBlockHandler {
    @Override
    public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
        ErrorMsg msg = null;
        if (ex instanceof FlowException) {
            msg = ErrorMsg.builder()
                .status(100)
                .msg("限流了")
                .build();
        } else if (ex instanceof DegradeException) {
            msg = ErrorMsg.builder()
                .status(101)
                .msg("降级了")
                .build();
        } else if (ex instanceof ParamFlowException) {
            msg = ErrorMsg.builder()
                .status(102)
                .msg("热点参数限流")
                .build();
        } else if (ex instanceof SystemBlockException) {
            msg = ErrorMsg.builder()
                .status(103)
                .msg("系统规则(负载/...不满足要求)")
                .build();
        } else if (ex instanceof AuthorityException) {
            msg = ErrorMsg.builder()
                .status(104)
                .msg("授权规则不通过")
                .build();
        }
        // http状态码
        response.setStatus(500);
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type", "application/json;charset=utf-8");
        response.setContentType("application/json;charset=utf-8");
        // spring mvc自带的json操作工具,叫jackson
        new ObjectMapper()
            .writeValue(
                response.getWriter(),
                msg
            );
    }
}

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
class ErrorMsg {
    private Integer status;
    private String msg;
}
  • 扩展Sentinel-实现区分来源
@Component
public class MyRequestOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 从请求参数中获取名为 origin 的参数并返回
        // 如果获取不到origin参数,那么就抛异常
        String origin = request.getParameter("origin");
        if (StringUtils.isBlank(origin)) {
            throw new IllegalArgumentException("origin must be specified");
        }
        return origin;
    }
}
  • 扩展Sentinel-RESTfulURL支持
@Component
@Slf4j
public class MyUrlCleaner implements UrlCleaner {
    @Override
    public String clean(String originUrl) {
        // 让 /shares/1 与 /shares/2 的返回值相同
        // 返回/shares/{number}

        String[] split = originUrl.split("/");

        return Arrays.stream(split)
            .map(string -> {
                if (NumberUtils.isNumber(string)) {
                    return "{number}";
                }
                return string;
            })
            .reduce((a, b) -> a + "/" + b)
            .orElse("");
    }
}

消息驱动的微服务-SpringCloudAlibabaRocketMQ

  • 搭建RocketMQ

官网
RocketMq在Ubuntu安装
启动报JDK环境错误

  • 搭建RocketMQ控制台

rocketmq控制台搭建(rocketmq-console)

  • RocketMQ的详细使用教程

官方文档

  • Spring消息编程模型-生产者
  1. 加依赖,注意版本兼容
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.3</version>
</dependency>
  1. 写配置
rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    #小坑:必須指定group
    group: test-group
  1. 写代码 RocketMQTemplate 发送消息
private final RocketMQTemplate rocketMQTemplate;
    
public ShareDTO update() {
    # 发送消息
    rocketMQTemplate.converAndSend("add-integarl",UserAddBonusMsgDTO.builder()
                                .userId(share.getUserId())
                                .bonus(50)
                                .build());
}
截屏2020-12-27 下午10.51.29.png
  • Spring消息编程模型-消费者
  1. 加依赖,注意版本兼容
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.3</version>
</dependency>
  1. 写配置
rocketmq:
  name-server: 127.0.0.1:9876
  1. 写代码 Listener
    生产者的Group通过配置文件编写,消费者的Group通过consumerGroup属性配置,topic 要与生产者的一致
@Service
@RocketMQMessageListener(consumerGroup = "consumer-group", topic = "add-integral")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusListener implements RocketMQListener<UserAddBonusMsgDTO> {
   
    @Override
    public void onMessage(UserAddBonusMsgDTO message) {
       //为用户加积分
       //记录日志
    }
   
}
截屏2020-12-27 下午10.52.03.png

其它 MQ

⚠️⚠️⚠️
  • 实现分布式事物消息


    截屏2020-12-27 下午10.52.32.png

编码实现

  1. 创建rocketMq事务表 rocketmq_transaction_log
USE `content_center`;

create table rocketmq_transaction_log
(
id int auto_increment comment 'id' primary key,
transaction_id varchar(45) not null comment '事努id',
log varchar(45) not null comment '日志'
)
comment 'RocketMQ事务日志表';
  1. 写代码,发送半消息(Spring Cloud Stream)
    rocketMQTemplate.sendMessageInTransaction("tx-add-bonus-group","add-integral",MessageBuilder
    .withPayload(
    UserAddBonusMsgDTO.builder()
    .userId(share.getUserId())
    .bonus(50)
    .build()
    )
    // header也有妙用...
    .setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId)
    .setHeader("share_id", id)
    .setHeader("dto", JSON.toJSONString(auditDTO))
    .build());
    private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;
    private final Source source;
    
    public Share auditById(Integer id, ShareAuditDTO auditDTO) {
        // 1. 查询share是否存在,不存在或者当前的audit_status != NOT_YET,那么抛异常
        Share share = this.shareMapper.selectByPrimaryKey(id);
        if (share == null) {
            throw new IllegalArgumentException("参数非法!该分享不存在!");
        }
        if (!Objects.equals("NOT_YET", share.getAuditStatus())) {
            throw new IllegalArgumentException("参数非法!该分享已审核通过或审核不通过!");
        }

        // 3. 如果是PASS,那么发送消息给rocketmq,让用户中心去消费,并为发布人添加积分
        if (AuditStatusEnum.PASS.equals(auditDTO.getAuditStatusEnum())) {
            // 发送半消息。。
            String transactionId = UUID.randomUUID().toString();
            this.source.output()
                .send(
                    MessageBuilder
                        .withPayload(
                            UserAddBonusMsgDTO.builder()
                                .userId(share.getUserId())
                                .bonus(50)
                                .build()
                        )
                        // header也有妙用...
                        .setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId)
                        .setHeader("share_id", id)
                        .setHeader("dto", JSON.toJSONString(auditDTO))
                        .build()
                );
        }
        else {
            this.auditByIdInDB(id, auditDTO);
        }
        return share;
    }

    @Transactional(rollbackFor = Exception.class)
    public void auditByIdInDB(Integer id, ShareAuditDTO auditDTO) {
        Share share = Share.builder()
            .id(id)
            .auditStatus(auditDTO.getAuditStatusEnum().toString())
            .reason(auditDTO.getReason())
            .build();
        this.shareMapper.updateByPrimaryKeySelective(share);

        // 4. 把share写到缓存
    }

    @Transactional(rollbackFor = Exception.class)
    public void auditByIdWithRocketMqLog(Integer id, ShareAuditDTO auditDTO, String transactionId) {
        this.auditByIdInDB(id, auditDTO);

        this.rocketmqTransactionLogMapper.insertSelective(
            RocketmqTransactionLog.builder()
                .transactionId(transactionId)
                .log("审核分享...")
                .build()
        );
    }
  1. 事务的检查
@RocketMQTransactionListener(txProducerGroup = "tx-add-bonus-group")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusTransactionListener implements RocketMQLocalTransactionListener {
    private final ShareService shareService;
    private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        MessageHeaders headers = msg.getHeaders();

        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
        Integer shareId = Integer.valueOf((String) headers.get("share_id"));

        String dtoString = (String) headers.get("dto");
        ShareAuditDTO auditDTO = JSON.parseObject(dtoString, ShareAuditDTO.class);

        try {
            this.shareService.auditByIdWithRocketMqLog(shareId, auditDTO, transactionId);
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        MessageHeaders headers = msg.getHeaders();
        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);

        // select * from xxx where transaction_id = xxx
        RocketmqTransactionLog transactionLog = this.rocketmqTransactionLogMapper.selectOne(
            RocketmqTransactionLog.builder()
                .transactionId(transactionId)
                .build()
        );
        if (transactionLog != null) {
            return RocketMQLocalTransactionState.COMMIT;
        }
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}
  • Spring Cloud Stream 框架

  • Spring Cloud Stream编写生产者

  1. 加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
  1. 启动类加注解 @EnableBinding({Source.class})
  2. 写配置
spring:
  cloud:
      stream:
          rocketmq:
            binder:
              name-server: 127.0.0.1:9876
          bindings:
            output:
              # 用来指定topic
              destination: add-bonus
  1. 发送消息
private final Source source;
    
@GetMapping("/test-stream")
public String testStream() {
    source.output()
    .send(
        MessageBuilder
               .withPayload("消息体")
               .build()
     );
return "success";
}
  1. 关闭nacos心跳日志,设计日志级别为error
logging:
  level:
    com.alibaba.nacos: error
  • Spring Cloud Stream编写消费者
  1. 加依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
  1. 加依赖
  2. 启动类加注解 @EnableBinding({Sink.class})
  3. 写配置
spring:
  cloud:
      stream:
          rocketmq:
            binder:
              name-server: 127.0.0.1:9876
          bindings:
            input:
              # 与内容中心一致
              destination: stream-test-topic
              # 如果用的是RocketMq,一定要设置
              # 如果用的是其它MQ,可留空
              group: binder-group
  1. 消费消息
@Service
@Slf4j
public class TestStreamConsumer {
    private final UserService userService;

    @StreamListener(Sink.INPUT)
    public void receive(String message) {
       log.info("通过stream收到了消息:{}",message);
    }
}
  • Spring Cloud Stream接口自定义
    • 自定义接口实现消息生产者
  1. 创建interface
public interface MySource{

 //my-output 要与配置文件一致
 String MY_OUTPUT = ”my-output“;
 
 @Output(MY_OUTPUT)
 MessageChannel output();
}
  1. 在启动类引用 MySource.class
@EnableBinding({Source.class,MySource.class})
  1. 加配置
spring:
  cloud:
      stream:
          rocketmq:
            binder:
              name-server: 127.0.0.1:9876
          bindings:
            output:
              # 用来指定topic
              destination: add-bonus
            my-output:
              destination: stream-my-topic
  1. 测试代码
private final MySource mySource;
    
@GetMapping("/test-stream-2")
public String testStream() {
    mySource.output()
    .send(
        MessageBuilder
               .withPayload("消息体")
               .build()
     );
return "success";
}
    • 自定义接口实现消息消费者
  1. 创建interface
public interface MySink{

 //my-input 要与配置文件一致
 String MY_INPUT = ”my-input“;
 
 @Input(MY_INPUT)
 SubscribableChannel input();
}
  1. 在启动类引用 MySink.class
@EnableBinding({Sink.class,MySink.class})
  1. 加配置
spring:
  cloud:
      stream:
          rocketmq:
            binder:
              name-server: 127.0.0.1:9876
          bindings:
            input:
              # 与内容中心一致
              destination: stream-test-topic
              # 如果用的是RocketMq,一定要设置
              # 如果用的是其它MQ,可留空
              group: binder-group
            my-input:
             destination: stream-my-topic
             group: my-group
  1. 消费消息
@Service
@Slf4j
public class MyTestStreamConsumer {
    private final UserService userService;

    @StreamListener(MySink.MY_INPUT)
    public void receive(String message) {
       log.info("自定义接口消费,通过stream收到了消息:{}",message);
    }
}
截屏2020-12-27 下午10.53.28.png

SpringCloudStream异常处理

@Service
@Slf4j
public class MyTestStreamConsumer {
    private final UserService userService;

    @StreamListener(MySink.MY_INPUT)
    public void receive(String message) {
       log.info("自定义接口消费,通过stream收到了消息:{}",message);
       throw new RuntimeException("x");
    }
    
    
    @StreamListener("errorChannel")
    public void error(Message<?> message) {
      ErrorMessage errorMessage = (ErrorMessage) message;
     log.warn("发生异常:{}",errorMessage);
   }

}
  • Spring Cloud Stream + RocketMq实现分布式事务

删除自定义代码(重构)生产者

@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ShareService {
    private final Source source;
     public Share auditById(Integer id, ShareAuditDTO auditDTO) {
        // 1. 查询share是否存在,不存在或者当前的audit_status != NOT_YET,那么抛异常
        Share share = this.shareMapper.selectByPrimaryKey(id);
        if (share == null) {
            throw new IllegalArgumentException("参数非法!该分享不存在!");
        }
        if (!Objects.equals("NOT_YET", share.getAuditStatus())) {
            throw new IllegalArgumentException("参数非法!该分享已审核通过或审核不通过!");
        }

        // 3. 如果是PASS,那么发送消息给rocketmq,让用户中心去消费,并为发布人添加积分
        if (AuditStatusEnum.PASS.equals(auditDTO.getAuditStatusEnum())) {
            // 发送半消息。。
            String transactionId = UUID.randomUUID().toString();
            this.source.output()
                .send(
                    MessageBuilder
                        .withPayload(
                            UserAddBonusMsgDTO.builder()
                                .userId(share.getUserId())
                                .bonus(50)
                                .build()
                        )
                        // header也有妙用...
                        .setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId)
                        .setHeader("share_id", id)
                        .setHeader("dto", JSON.toJSONString(auditDTO))
                        .build()
                );
        }
        else {
            this.auditByIdInDB(id, auditDTO);
        }
        return share;
    }

    @Transactional(rollbackFor = Exception.class)
    public void auditByIdInDB(Integer id, ShareAuditDTO auditDTO) {
        Share share = Share.builder()
            .id(id)
            .auditStatus(auditDTO.getAuditStatusEnum().toString())
            .reason(auditDTO.getReason())
            .build();
        this.shareMapper.updateByPrimaryKeySelective(share);

        // 4. 把share写到缓存
    }   
    
}    

txProducerGroup与配置文件一致

@RocketMQTransactionListener(txProducerGroup = "tx-add-bonus-group")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusTransactionListener implements RocketMQLocalTransactionListener {
    private final ShareService shareService;
    private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        MessageHeaders headers = msg.getHeaders();

        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
        Integer shareId = Integer.valueOf((String) headers.get("share_id"));

        String dtoString = (String) headers.get("dto");
        ShareAuditDTO auditDTO = JSON.parseObject(dtoString, ShareAuditDTO.class);

        try {
            this.shareService.auditByIdWithRocketMqLog(shareId, auditDTO, transactionId);
            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        MessageHeaders headers = msg.getHeaders();
        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);

        // select * from xxx where transaction_id = xxx
        RocketmqTransactionLog transactionLog = this.rocketmqTransactionLogMapper.selectOne(
            RocketmqTransactionLog.builder()
                .transactionId(transactionId)
                .build()
        );
        if (transactionLog != null) {
            return RocketMQLocalTransactionState.COMMIT;
        }
        return RocketMQLocalTransactionState.ROLLBACK;
    }
}

加配置

spring:
  cloud:
    stream:
      rocketmq:
        binder:
          name-server: 127.0.0.1:9876
        bindings:
          output:
            producer:
              transactional: true
              group: tx-add-bonus-group
      bindings:
        output:
          # 用来指定topic
          destination: add-bonus

删除自定义代码(重构)消费者

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class AddBonusStreamConsumer {
    private final UserService userService;

    @StreamListener(Sink.INPUT)
    public void receive(UserAddBonusMsgDTO message) {
        message.setEvent("CONTRIBUTE");
        message.setDescription("投稿加积分..");
        userService.addBonus(message);
    }
}

本地事务方法 addBonus

@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserService {
    private final UserMapper userMapper;
    private final BonusEventLogMapper bonusEventLogMapper;

    @Transactional(rollbackFor = Exception.class)
    public void addBonus(UserAddBonusMsgDTO msgDTO) {
        // 1. 为用户加积分
        Integer userId = msgDTO.getUserId();
        Integer bonus = msgDTO.getBonus();
        User user = this.userMapper.selectByPrimaryKey(userId);

        user.setBonus(user.getBonus() + bonus);
        this.userMapper.updateByPrimaryKeySelective(user);

        // 2. 记录日志到bonus_event_log表里面
        this.bonusEventLogMapper.insert(
            BonusEventLog.builder()
                .userId(userId)
                .value(bonus)
                .event(msgDTO.getEvent())
                .createTime(new Date())
                .description(msgDTO.getDescription())
                .build()
        );
        log.info("积分添加完毕...");
    }
}

编写SpringCloudGateway

新建一个springboot项目

  1. 修改版本,和其它微服务版本一致
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
  1. 引入依赖
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
        
<dependencyManagement>
 <dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-alibaba-dependencies</artifactId>
        <version>0.9.0.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
 </dependencies>
</dependencyManagement>
  1. 写配置
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
截屏2020-12-27 下午10.55.43.png

Spring Cloud Gateway-路由谓词工厂详解(Route Predicate Factories)

  • 路由谓词配置predicates
  1. after
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: after_route
          uri: lb://user-center
          predicates:
            # 当且仅当请求时的时间After配置的时间时,才会转发到用户微服务
            # 目前配置不会进该路由配置,所以返回404
            # 将时间改成 < now的时间,则访问localhost:8040/** -> user-center/**
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - After=2030-01-20T17:42:47.789-07:00[America/Denver]
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. Before
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: before_route
          uri: lb://user-center
          predicates:
            # 当且仅当请求时的时间Before配置的时间时,才会转发到用户微服务
            # 目前配置不会进该路由配置,所以返回404
            # 将时间改成 > now的时间,则访问localhost:8040/** -> user-center/**
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - Before=2018-01-20T17:42:47.789-07:00[America/Denver]
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. Between
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: between_route
          uri: lb://user-center
          predicates:
            # 当且仅当请求时的时间Between配置的时间时,才会转发到用户微服务
            # 因此,访问localhost:8040/** -> user-center/**
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2027-01-21T17:42:47.789-07:00[America/Denver]
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. cookie
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: cookie_route
          uri: lb://user-center
          predicates:
            # 当且仅当带有名为somecookie,并且值符合正则ch.p的Cookie时,才会转发到用户微服务
            # 如Cookie满足条件,则访问http://localhost:8040/** -> user-center/**
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - Cookie=somecookie, ch.p
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. Header
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: header_route
          uri: lb://user-center
          predicates:
            # 当且仅当带有名为X-Request-Id,并且值符合正则\d+的Header时,才会转发到用户微服务
            # 如Header满足条件,则访问http://localhost:8040/** -> user-center/**
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - Header=X-Request-Id, \d+
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. host
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: host_route
          uri: lb://user-center
          predicates:
            # 当且仅当名为Host的Header符合**.somehost.org或**.anotherhost.org时,才会转发用户微服务
            # 如Host满足条件,则访问http://localhost:8040/** -> user-center/**
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - Host=**.somehost.org,**.anotherhost.org
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. Method
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: method_route
          uri: lb://user-center
          predicates:
            # 当且仅当HTTP请求方法是GET时,才会转发用户微服务
            # 如请求方法满足条件,访问http://localhost:8040/** -> user-center/**
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - Method=GET
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. Path
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: path_route
          uri: lb://user-center
          predicates:
            # 当且仅当访问路径是/users/*或者/some-path/**,才会转发用户微服务
            # segment是一个特殊的占位符,单层路径匹配
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - Path=/users/{segment},/some-path/**
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. query-1
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: query_route
          uri: lb://user-center
          predicates:
            # 当且仅当请求带有baz的参数,才会转发到用户微服务
            # eg. 访问http://localhost:8040/users/1?baz=xx -> user-center的/users/1
            - Query=baz
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. query-2
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: query_route
          uri: lb://user-center
          predicates:
            # 当且仅当请求带有名为foo的参数,且参数值符合正则ba.,才会转发到用户微服务
            # eg. 访问http://localhost:8040/users/1?baz=baz -> user-center的/users/1?baz=baz
            - Query=foo, ba.
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  1. RemoteAddr
server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: remoteaddr_route
          uri: lb://user-center
          predicates:
            # 当且仅当请求IP是192.168.1.1/24网段,例如192.168.1.10,才会转发到用户微服务
            # eg. 访问http://localhost:8040/users/1 -> user-center的/users/1
            - RemoteAddr=192.168.1.1/24
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
  • 自定义路由谓词工厂
/**
 * 路由工厂必须以RoutePredicateFactory结尾
 * @author renpei
 */
@Component
public class TimeBetweenRoutePredicateFactory
    extends AbstractRoutePredicateFactory<TimeBeweenConfig> {
    public TimeBetweenRoutePredicateFactory() {
        super(TimeBeweenConfig.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(TimeBeweenConfig config) {
        LocalTime start = config.getStart();
        LocalTime end = config.getEnd();
        return exchange -> {
            LocalTime now = LocalTime.now();
            return now.isAfter(start) && now.isBefore(end);
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("start", "end");
    }

}

@Data
public class TimeBeweenConfig {
    private LocalTime start;
    private LocalTime end;
}

截屏2020-12-27 下午10.56.27.png
  • 编写一个过滤器工厂

过滤器工厂必须GatewayFilterFactory结尾

@Slf4j
@Component
public class PreLogGatewayFilterFactory  extends AbstractNameValueGatewayFilterFactory {
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return ((exchange, chain) -> {
           //config.getName() = a,config.getValue() = b
            log.info("请求进来了...{},{}", config.getName(), config.getValue());
            ServerHttpRequest modifiedRequest = exchange.getRequest()
                .mutate()
                .build();
            ServerWebExchange modifiedExchange = exchange.mutate()
                .request(modifiedRequest)
                .build();

            return chain.filter(modifiedExchange);
        });
    }
}

配置filters

server:
  port: 8040
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: after_route
          uri: lb://user-center
          predicates:
            # 当且仅当请求时的时间After配置的时间时,才会转发到用户微服务
            # 目前配置不会进该路由配置,所以返回404
            # 将时间改成 < now的时间,则访问localhost:8040/** -> user-center/**
            # eg. 访问http://localhost:8040/users/1 -> user-center/users/1
            - After=2030-01-20T17:42:47.789-07:00[America/Denver]
          filters:
            - AddRequestHeader=X-Request-Foo,Bar
            - PreLog=a,b
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace

全局过滤器

百度SpringCloudGateway了解更多

生产环境配置文件

server:
  port: 8040
spring:
  application:
    name: gateway
  zipkin:
    base-url: http://localhost:9411/
    discoveryClientEnabled: false
  sleuth:
    sampler:
      # 抽样率,默认是0.1(10%)
      probability: 1.0
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          # 让gateway通过服务发现组件找到其他的微服务
          enabled: true
      routes:
        - id: user_route
          uri: lb://user-center
          predicates:
            - Path=/users/**
        - id: content_route
          uri: lb://content-center
          predicates:
            - Path=/shares/**,/admin/**
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always
logging:
  level:
    org.springframework.cloud.gateway: trace
截屏2020-12-27 下午10.57.01.png

截屏2020-12-27 下午10.57.27.png

网关限流

业务环节开发

  • Feign实现Token传递

方式一

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {
    /**
     * http://user-center/users/{id}
     * @param id
     * @return
     */
    @GetMapping("/users/{id}")
    UserDTO findById(@PathVariable Integer id,@RequestHeader("X-token") String token);
}

方式二

  1. 拦截器
public class TokenRelayRequestIntecepor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // 1. 获取到token
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("X-Token");

        // 2. 将token传递
        if (StringUtils.isNotBlank(token)) {
            template.header("X-Token", token);
        }

    }
}
  1. 配置拦截器
feign:
  client:
    config:
      # 全局配置
      default:
        loggerLevel: full
        requestInterceptors:
          - com.itmuch.contentcenter.feignclient.interceptor.TokenRelayRequestIntecepor
  • RestTemplate实现Token传递

方式一

    @Autowired
    private RestTemplate restTemplate;
    
    @GetMapping("/tokenRelay/{userId}")
    public ResponseEntity<UserDTO> tokenRelay(@PathVariable Integer userId, HttpServletRequest request) {
        String token = request.getHeader("X-Token");
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Token", token);

        return this.restTemplate
            .exchange(
                "http://user-center/users/{userId}",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                UserDTO.class,
                userId
            );
    }

方式二 RestTemplate的拦截器

  1. Interceptor
public class RestTemplateTokenRelayInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest httpRequest = attributes.getRequest();
        String token = httpRequest.getHeader("X-Token");

        HttpHeaders headers = request.getHeaders();
        headers.add("X-Token", token);

        // 保证请求继续执行
        return execution.execute(request, body);
    }
}
  1. 配置
// 在spring容器中,创建一个对象,类型RestTemplate;名称/ID是:restTemplate
// <bean id="restTemplate" class="xxx.RestTemplate"/>
@Bean
@LoadBalanced
@SentinelRestTemplate
public RestTemplate restTemplate() {
    RestTemplate template = new RestTemplate();
    template.setInterceptors(
        Collections.singletonList(
            new RestTemplateTokenRelayInterceptor()
        )
    );
    return template;
}
  • 自定义注解实现

配置服务器nacos

  • 使用nacos管理配置

百度

  • 搭建生产服可用的nacos集群环境

百度
搭建生产服可用的nacos集群环境

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容