springcloud搭建

背景

动手搭建springcCloud完整框架,以及了解springcloud的各个组件

spring Cloud了解

Spring Cloud 是什么?

Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的开发便利性简化了分布式系统的开发,比如服务发现、服务网关、服务路由、链路追踪等。Spring Cloud 并不重复造轮子,而是将市面上开发得比较好的模块集成进去,进行封装,从而减少了各模块的开发成本。

说人话:Spring Cloud 提供了构建分布式系统所需的“全家桶”。spring Cloud 不是一个新的框架,它是对现有技术整合之后提出实现的一种全面的解决方案。也可以说是一个生态

Spring Cloud 优缺点

  • 优点
    • 集大成者,Spring Cloud 包含了微服务架构的方方面面。
    • 约定优于配置,基于注解,没有配置文件。
    • 轻量级组件,Spring Cloud 整合的组件大多比较轻量级,且都是各自领域的佼佼者
    • 开发简便,Spring Cloud 对各个组件进行了大量的封装,从而简化了开发
    • 开发灵活,Spring Cloud 的组件都是解耦的,开发人员可以灵活按需选择组件。
  • 缺点
    • 项目结构复杂,每一个组件或者每一个服务都需要创建一个项目。
    • 部署门槛高,项目部署需要配合 Docker 等容器技术进行集群部署,而要想深入了解 Docker,学习成本高docker相关知识学习

常见组件

  • Eureka 服务注册与发现
  • Ribbon 客户端负载均衡
  • Feign 声明式服务调用
  • Hystrix 断路器
  • Config 分布式配置
  • gateway 服务网关

源码地址

springCloud-case

spring Cloud 项目

实践是检验知识的唯一标准,只有通过了反复的实践才能真正的学会一个东西

spring Cloud整体结构:

image-20220729105103277

spring Cloud 父项目

父子项目,一般,父项目创建是进行对maven依赖进行统一的版本管理,项目打包部署等,当然也可以对单个子项目单独打包部署等

  1. 父项目搭建

    image-20220729105944626

    image-20220729110937186

    image-20220729111123236

POM文件

<?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.zy.cloud</groupId>
    <artifactId>cloud-case</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules></modules>
    <!-- 首先修改打包方式 -->
    <packaging>pom</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <java.version>1.8</java.version>

        <spring.boot.version>2.1.4.RELEASE</spring.boot.version>
        <lombok.version>1.18.20</lombok.version>
        <orika.core.version>1.5.4</orika.core.version>
        <hutool.all.version>5.6.3</hutool.all.version>
        <fastjson.version>2.0.8</fastjson.version>
        <druid.starter.version>1.1.10</druid.starter.version>
        <mysql.connector.java.version>8.0.20</mysql.connector.java.version>
        <!-- mybatis -->
        <mybatis.plus.starter.version>3.4.2</mybatis.plus.starter.version>
        <mybatis.plus.generator.version>3.3.0</mybatis.plus.generator.version>
        <freemarker.version>2.3.28</freemarker.version>
        <velocity.version>2.0</velocity.version>
    </properties>

    <!--版本管理-->
    <dependencyManagement>
        <dependencies>

            <!-- ====================  spring cloud boot ==================== -->
            <!--springCloud-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- springboot的依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- ====================  springboot starter ==================== -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${spring.boot.version}</version>
            </dependency>
            <!-- 连接池druid-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.starter.version}</version>
            </dependency>

            <!-- ====================  lombok ==================== -->
            <!--实体类注解工具包-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
            <!-- ====================  工具 ==================== -->
            <!--对象转化工具http://orika-mapper.github.io/orika-docs/intro.html-->
            <dependency>
                <groupId>ma.glasnost.orika</groupId>
                <artifactId>orika-core</artifactId>
                <version>${orika.core.version}</version>
            </dependency>
            <!-- 糊涂工具库 https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool.all.version}</version>
            </dependency>
            <!-- json 转换工具 https://mvnrepository.com/artifact/com.alibaba/fastjson -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>

            <!-- mysql 连接驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.connector.java.version}</version>
            </dependency>
            <!--mybatis plus-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis.plus.starter.version}</version>
            </dependency>
            <!-- 代码生成器依赖-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
                <version>${mybatis.plus.generator.version}</version>
            </dependency>
            <!-- mybatis plus 代码生成器模板  freemarker/velocity -->
            <dependency>
                <groupId>org.freemarker</groupId>
                <artifactId>freemarker</artifactId>
                <version>${freemarker.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity-engine-core</artifactId>
                <version>${velocity.version}</version>
            </dependency>

        </dependencies>

    </dependencyManagement>


</project>

注意:

1.  父模块加载完成,将`src`下目录删除,只是需要`pom`对依赖进行统一管理
1.  `pom`文件修改打包方式为`pom`

公共组件cloud-case-common

  1. 在父模块cloud-case下添加module和需要注意parent选择父模块,包结构保持一致

    image-20220729112702686

  2. 等待common加载完会发现,父模块的pom文件

       <modules>
              <!-- 新增的子模块 -->
            <module>cloud-case-common</module> 
        </modules>
    
  3. 删除common启动类,和测试类,无需,修改pom文件

    <?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>cloud-case</artifactId>
            <groupId>com.zy.cloud</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>cloud-case-common</artifactId>
        <name>cloud-case-common</name>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <!-- mysql 连接驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <!--mybatis plus-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
            </dependency>
            <!-- 代码生成器依赖-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-generator</artifactId>
            </dependency>
            <!-- mybatis plus 代码生成器模板  freemarker/velocity -->
            <dependency>
                <groupId>org.freemarker</groupId>
                <artifactId>freemarker</artifactId>
            </dependency>
            <dependency>
                <groupId>org.apache.velocity</groupId>
                <artifactId>velocity-engine-core</artifactId>
            </dependency>
            <!--格式化-->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-annotations</artifactId>
            </dependency>
        </dependencies>
    </project>
    
    
  4. Mysql创建测试用表payment

    -- auto-generated definition  测试表
    create table payment
    (
        id     bigint auto_increment comment 'ID'
            primary key,
        serial varchar(200) null
    )
        collate = utf8_unicode_ci;
    
  5. common项目中整合生成实体类,controllerservice代码

    image-20220729131159951

    package com.zy.cloud.common.codeGenerator;
    
    import com.baomidou.mybatisplus.annotation.DbType;
    import com.baomidou.mybatisplus.core.toolkit.StringPool;
    import com.baomidou.mybatisplus.generator.AutoGenerator;
    import com.baomidou.mybatisplus.generator.InjectionConfig;
    import com.baomidou.mybatisplus.generator.config.*;
    import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder;
    import com.baomidou.mybatisplus.generator.config.rules.DateType;
    import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
    import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
    
    import java.io.File;
    import java.lang.reflect.Field;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * Description: mybatis plus 代码生成器
     */
    public class MybatisPlusGenerator {
    
        public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
            String[] tableNames = new String[]{}; //需要生成的表
            String[] excludeTableNames = new String[]{};//需要排除的表
            String author = "zy";//用户名
            String dataSourceUrl = "jdbc:mysql://127.0.0.1:3306/cloud?userUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC";//数据源地址
            String dataSourceDriver = "com.mysql.cj.jdbc.Driver";
            String dataSourceUsername = "root";
            String dataSourcePassword = "123456";
            String tablePrefix = ""; //表前缀
    
    
            AutoGenerator mpg = new AutoGenerator();// 代码生成器
            GlobalConfig gc = new GlobalConfig();// 全局配置
            gc.setAuthor(author);//用户名
            gc.setOpen(false); //是否打开输出的目录
            gc.setFileOverride(true);//是否重写  覆盖已经生成的文件
            gc.setActiveRecord(true);// 开启 activeRecord 模式
            gc.setDateType(DateType.ONLY_DATE);//设置生成实体日期属性的类型,我这里用的是util中的Date
            //gc.setSwagger2(true);  //带Swagger2注释
            gc.setEnableCache(false);// XML 二级缓存
            gc.setBaseResultMap(true);// mapper.xml生成查询映射结果
            gc.setBaseColumnList(true);//mapper.xml生成查询结果列
            gc.setMapperName("%sMapper");
            gc.setControllerName("%sController");
            gc.setServiceName("%sService");
            gc.setServiceImplName("%sServiceImpl");
            gc.setEntityName("%sPojo");
            mpg.setGlobalConfig(gc);
    
            // 数据源配置
            DataSourceConfig dsc = new DataSourceConfig();
            dsc.setDbType(DbType.MYSQL);//数据源类型
            dsc.setUrl(dataSourceUrl);
            dsc.setDriverName(dataSourceDriver);
            dsc.setUsername(dataSourceUsername);
            dsc.setPassword(dataSourcePassword);
            mpg.setDataSource(dsc);
    
            PackageConfig pc = new PackageConfig(); // 包配置
            pc.setParent("com.zy.cloud");
            pc.setController("produce.controller");
            pc.setEntity("common.pojo");
            pc.setMapper("produce.mapper");
            pc.setService("produce.service");
            pc.setServiceImpl("produce.service.impl");
            mpg.setPackageInfo(pc);
    
            // 配置模板
            TemplateConfig templateConfig = new TemplateConfig();
    
            //  设置为null时,则不会生成xml文件,controller、service等Java类
            templateConfig
                    .setXml("/templates/mapper.xml")
                    .setEntity("/templates/entity.java")                                // 设置生成entity的模板
                    .setMapper("/templates/mapper.java")                                // 设置生成mapper的模板
                    .setController("/templates/controller.java")                    // 设置生成service的模板
                    .setService("/templates/service.java")                            // 设置生成serviceImpl的模板
                    .setServiceImpl("/templates/serviceImpl.java");                    // 设置生成controller的模板
            mpg.setTemplate(templateConfig);
    
            // 策略配置
            StrategyConfig strategy = new StrategyConfig();
            strategy.setNaming(NamingStrategy.underline_to_camel);
            strategy.setColumnNaming(NamingStrategy.underline_to_camel);
    
    
            //实体共同继承的基础类, 里面抽取了共同的一些属性
    //        strategy.setSuperEntityClass("com.example.open.entity.BaseEntity");
            // 写于父类中的公共字段,一下为生成的实体所忽略的字段
    //        strategy.setSuperEntityColumns("ID", "CREATE_TIME", "MODIFY_TIME", "DELETED");
            //逻辑删除属性名称
    //        strategy.setLogicDeleteFieldName("DELETED");
    
            //是否使用lombok注解
            strategy.setEntityLombokModel(true);
            //是否定义输出的实体类有builder模式的调用链
            strategy.setEntityBuilderModel(true);
            //是否生成字段注解:读取表中的注释
            strategy.setEntityTableFieldAnnotationEnable(true);
            //是否生成@RestController注解的controller
            strategy.setRestControllerStyle(true);
            strategy.setEntitySerialVersionUID(true);
            // controller共同继承的公共父类
    //        strategy.setSuperControllerClass("com.zy.website.controller.BaseController");
            //驼峰转连接字符- , 例如PlatformUser -->  platform-user
            strategy.setControllerMappingHyphenStyle(true);
            //去除表名的前缀的固定生成实体
    //        strategy.setTablePrefix(tablePrefix);
    
            //自定义注入属性,在使用单个生成时,可以使用 自定义实体类名称
            InjectionConfig injectionConfig = new InjectionConfig() {
                //自定义属性注入:abc
                //在.ftl(或者是.vm)模板中,通过${cfg.abc}获取属性
                @Override
                public void initMap() {
    //                List<TableInfo> list = this.getConfig().getTableInfoList();
    //                if (list != null && !list.isEmpty()) {
    //                    TableInfo info = list.get(0);
    //                    Map<String, Object> map = new HashMap<>();
    //                    map.put("serviceImplNamePath", info.getEntityPath() + "Service");
    //                    super.setMap(map);
    //                }
                }
            };
    
            mpg.setCfg(injectionConfig);
            strategy.setExclude(excludeTableNames);//不需要生成的表
            strategy.setInclude(tableNames);
            mpg.setStrategy(strategy);
    //        mpg.setTemplateEngine(new VelocityTemplateEngine());
            mpg.setTemplateEngine(new FreemarkerTemplateEngine());
            //自定义文件生成路径,包路径
            //这里调用customPackagePath方法,使用可以自己在内部灵活配置路径
            //如果不调用该方法、就会使用MyBatis-Plus默认的文件生成路径和包路径生成文件、但可以使用上面的PackageConfig做一些简单的配置
            customPackagePath(pc, mpg);
    
            mpg.execute();
        }
    
        /**
         * 自定义包路径,文件生成路径,这边配置更灵活
         * 虽然也可以使用InjectionConfig设置FileOutConfig的方式设置路径
         * 这里直接使用Map方式注入ConfigBuilder配置对象更加直观
         *
         * @param pc
         * @param mpg
         * @throws NoSuchFieldException
         * @throws IllegalAccessException
         */
        public static void customPackagePath(PackageConfig pc, AutoGenerator mpg) throws NoSuchFieldException, IllegalAccessException {
    
            String projectPath = System.getProperty("user.dir");
            String mavenPath = "\\src\\main\\java\\";
            /**
             * packageInfo配置controller、service、serviceImpl、entity、mapper等文件的包路径
             * 这里包路径可以根据实际情况灵活配置
             */
            Map<String, String> packageInfo = new HashMap<>();
            packageInfo.put(ConstVal.CONTROLLER, "com.zy.cloud.produce.controller");
            packageInfo.put(ConstVal.SERVICE, "com.zy.cloud.produce.service");
            packageInfo.put(ConstVal.SERVICE_IMPL, "com.zy.cloud.produce.service.impl");
            packageInfo.put(ConstVal.MAPPER, "com.zy.cloud.produce.mapper");
            packageInfo.put(ConstVal.ENTITY, "com.zy.cloud.common.pojo");
    
            /**
             * pathInfo配置controller、service、serviceImpl、entity、mapper、mapper.xml等文件的生成路径
             * srcPath也可以更具实际情况灵活配置
             * 后面部分的路径是和上面packageInfo包路径对应的源码文件夹路径
             * 这里你可以选择注释其中某些路径,可忽略生成该类型的文件,例如:注释掉下面pathInfo中Controller的路径,就不会生成Controller文件
             */
            Map pathInfo = new HashMap<>();
            // 路径 父地址 +子
            pathInfo.put(ConstVal.CONTROLLER_PATH, projectPath + "\\cloud-case\\cloud-case-produce\\" + mavenPath + packageInfo.get(ConstVal.CONTROLLER).replaceAll("\\.", StringPool.BACK_SLASH + File.separator));
            pathInfo.put(ConstVal.SERVICE_PATH, projectPath + "\\cloud-case\\cloud-case-produce\\" + mavenPath + packageInfo.get(ConstVal.SERVICE).replaceAll("\\.", StringPool.BACK_SLASH + File.separator));
            pathInfo.put(ConstVal.SERVICE_IMPL_PATH, projectPath + "\\cloud-case\\cloud-case-produce\\" + mavenPath + packageInfo.get(ConstVal.SERVICE_IMPL).replaceAll("\\.", StringPool.BACK_SLASH + File.separator));
            pathInfo.put(ConstVal.ENTITY_PATH, projectPath + "\\cloud-case\\cloud-case-common\\" + mavenPath + packageInfo.get(ConstVal.ENTITY).replaceAll("\\.", StringPool.BACK_SLASH + File.separator));
            pathInfo.put(ConstVal.MAPPER_PATH, projectPath + "\\cloud-case\\cloud-case-produce\\" + mavenPath + packageInfo.get(ConstVal.MAPPER).replaceAll("\\.", StringPool.BACK_SLASH + File.separator));
            pathInfo.put(ConstVal.XML_PATH, projectPath + "\\cloud-case\\cloud-case-produce\\src\\main\\resources\\mapper\\");
            pc.setPathInfo(pathInfo);
    
            /**
             * 创建configBuilder对象,传入必要的参数
             * 将以上的定义的包路径packageInfo配置到赋值到configBuilder对象的packageInfo属性上
             * 因为packageInfo是私有成员变量,也没有提交提供公共的方法,所以使用反射注入
             * 为啥要这么干,看源码去吧
             */
            ConfigBuilder configBuilder = new ConfigBuilder(mpg.getPackageInfo(), mpg.getDataSource(), mpg.getStrategy(), mpg.getTemplate(), mpg.getGlobalConfig());
            Field packageInfoField = configBuilder.getClass().getDeclaredField("packageInfo");
            packageInfoField.setAccessible(true);
            packageInfoField.set(configBuilder, packageInfo);
    
            /**
             * 设置配置对象
             */
            mpg.setConfig(configBuilder);
        }
    
    
    }
    
    

    这里模板就不贴出来了,有需要源码项目中找,最后对common项目打包,并添加到父项目的pom文件管理

生产者和消费者

子服务的创建过程,不在叙述,和公共组件一致

生产者 cloud-case-produce

整合mybatisPlus实现简单正删改查,用以测试

  1. 导入依赖包,修改pom文件

    <?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>cloud-case</artifactId>
            <groupId>com.zy.cloud</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>cloud-case-produce</artifactId>
        <name>cloud-case-produce</name>
    
        <dependencies>
            <!--引入实体的包-->
            <dependency>
                <groupId>com.zy.cloud</groupId>
                <artifactId>cloud-case-common</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--监控监控-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!-- 连接池druid-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
            </dependency>
            <!--mybatis plus-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
            </dependency>
        </dependencies>
    </project>
    
  2. 通过代码生成器,生成controller,service,mapper以及xml文件

  3. 添加application.yml文件

    server:
      port: 8090 #服务端口号
    
    #spring相关配置
    spring:
      application:
        name: cloud-case-produce #服务名
      datasource:
        type: com.alibaba.druid.pool.DruidDataSource  #当前数据源操作类型
        driver-class-name: com.mysql.cj.jdbc.Driver  #数据库驱动包
        url: jdbc:mysql://localhost:3306/cloud?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
    #mybatis plus
    mybatis-plus:
      #  config-location: classpath:/mybatis/mybatis-config.xml
      mapper-locations: classpath*:/mapper/*.xml
      type-aliases-package: com.zy.cloud.common.pojo
      global-config:
        db-config:
          id-type: auto  #自动生成填充主键id       
            
    
  4. controller

    import com.zy.cloud.common.commonCode.ApiReturnCode;
    import com.zy.cloud.common.commonResult.ApiReturn;
    import com.zy.cloud.common.pojo.PaymentPojo;
    import com.zy.cloud.produce.service.PaymentService;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.web.bind.annotation.*;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    /**
     * PaymentController
     */
    @RestController
    public class PaymentController {
    
        private static Logger logger = LogManager.getLogger(PaymentController.class);
    
        @Resource
        private PaymentService paymentPojoService;
        
        @Value("${spring.application.name}")
        private String serviceName;
    
    
        @PostMapping("/payment/create")
        public ApiReturn create(@RequestBody PaymentPojo dept) {
            int i = paymentPojoService.create(dept);
            if (i > 0) {
                return new ApiReturn(ApiReturnCode.SUCCESSFUL);
            } else {
                return new ApiReturn(ApiReturnCode.HTTP_ERROR);
            }
        }
    
        @GetMapping("/payment/get/{id}")
        public ApiReturn queryById(@PathVariable("id") Long id) {
            PaymentPojo payment = paymentPojoService.queryById(id);
            if (payment != null) {
                ApiReturn apiReturn = new ApiReturn();//ApiReturnCode统一返回类 仓库中找
                apiReturn.setData(payment);
                apiReturn.setApiReturnCode(ApiReturnCode.SUCCESSFUL);//ApiReturnCode统一返回code码 仓库中找
                return apiReturn;
            } else {
                return new ApiReturn(ApiReturnCode.HTTP_ERROR);
            }
        }
    
    }
    

    image-20220729134718021

  5. OK启动生产者,新增一条数据并访问

消费者 cloud-case-consumer

  1. 修改pom文件

    <?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>cloud-case</artifactId>
            <groupId>com.zy.cloud</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>cloud-case-consumer</artifactId>
        <name>cloud-case-consumer</name>
    
        <dependencies>
            <dependency>
                <groupId>com.zy.cloud</groupId>
                <artifactId>cloud-case-common</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </project>
    
    
  2. 添加application.yml配置文件

    server:
      port: 8070
    spring:
      application:
        name: cloud-case-consumer
    
  3. 添加controller层通过RestTemplate调用生产者

    package com.zy.cloud.consumer.controller;
    
    import com.zy.cloud.common.commonResult.ApiReturn;
    import com.zy.cloud.common.pojo.PaymentPojo;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.client.RestTemplate;
    
    @RestController
    @RequestMapping("/consumer")
    public class OrderController {
        private static Logger logger = LogManager.getLogger(OrderController.class);
    
        //调用生产者
        public static final String PAYMENT_URL = "http://127.0.0.1:8090";
    
        @Value("${server.port}")
        private String port;
        @Value("${spring.application.name}")
        private String applicationName;
    
        @Autowired
        private RestTemplate restTemplate;
    
        //创建支付订单的接口
        @GetMapping("/payment/create")
        public ApiReturn<PaymentPojo> create(PaymentPojo payment) {
            return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, ApiReturn.class);
        }
    
        //获取id获取支付订单
        @GetMapping("/payment/get/{id}")
        public ApiReturn<PaymentPojo> getPayment(@PathVariable("id") Long id) {
            logger.info("执行服务", applicationName, ":", port);
            return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, ApiReturn.class);
        }
    
    }
    
    
  4. 修改启动类

    
    //由于在common公共组件中导入了mysql连接包,所有的服务又要导入公共组件所有在启东时排除DataSourceAutoConfiguration
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    public class CloudCaseConsumerApplication {
        public static void main(String[] args) {
            SpringApplication.run(CloudCaseConsumerApplication.class, args);
            System.out.println("springCloud Consumer start !!!");
        }
        /**
         *  启动:Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured
         *     导入数据库连接包,未配置信息
         *    解决:
         *     @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
         *     启动不加连接数据库
         *
         */
    }
    
  5. 启动消费者调用查询接口,获取数据

服务注册中心 Eureka

Eureka简介

EurekaNetflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能

Eureka架构了解

Eureka采用了CS的设计架构,Eureka Server服务端作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka Client客户端连接到Eureka Server服务端并维持心跳的连接,这样系统的维护人员就可以通过Eureka Server来监控系统中的各个微服务是否正常运行。

在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的的信息,比如:服务地址通讯等以别名方式注册到注册中心上,另一方(消费者|生产者)以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用和RPC远程调用框架核心设计思想。这里的重点就是注册中心,因为注册中心管理每个服务于服务之间的依赖关系。在任何的RPC框架中,都会有一个注册中心,存放服务地址相关信息,也就是接口地址。

img

  • 服务注册(register):Eureka Client会通过发送REST请求的方式向Eureka Server注册自己的服务,提供自身的元数据,比如ip地址、端口、运行状况指标的url、主页地址等信息。Eureka Server接收到注册请求后,就会把这些元数据信息存储在一个双层的Map中。
  • 服务续约(renew):在服务注册后,Eureka Client会维护一个心跳来持续通知Eureka Server,说明服务一直处于可用状态,防止被剔除。Eureka Client在默认的情况下会每隔30秒(eureka.instance.leaseRenewallIntervalInSeconds)发送一次心跳来进行服务续约。
  • 服务同步(replicate):Eureka Server之间会互相进行注册,构建Eureka Server集群,不同Eureka Server之间会进行服务同步,用来保证服务信息的一致性。
  • 获取服务(get registry):服务消费者(Eureka Client)在启动的时候,会发送一个REST请求给Eureka Server,获取上面注册的服务清单,并且缓存在Eureka Client本地,默认缓存30秒(eureka.client.registryFetchIntervalSeconds)同时,为了性能虑,Eureka Server也会维护一份只读的服务清单缓存,该缓存每隔30秒更新一次。
  • 服务调用:服务消费者在获取到服务清单后,就可以根据清单中的服务列表信息,查找到其他服务的地址,从而进行远程调用。EurekaRegionZone的概念,一个Region可以包含多个Zone,在进行服务调用时,优先访问处于同一个Zone中的服务提供者。
  • 服务下线(cancel):Eureka Client需要关闭或重启时,就不希望在这个时间段内再有请求进来,所以,就需要提前先发送REST请求给Eureka Server,告诉Eureka Server自己要下线了,Eureka Server在收到请求后,就会把该服务状态置为下线(DOWN),并把该下线事件传播出去。
  • 服务剔除(evict):服务实例可能会因为网络故障等原因导致不能提供服务,而此时该实例也没有发送请求给Eureka Server来进行服务下线,所以,还需要有服务剔除的机制。Eureka Server在启动的时候会创建一个定时任务,每隔一段时间(默认60秒),从当前服务清单中把超时没有续约(默认90秒,eureka.instance.leaseExpirationDurationInSeconds)的服务剔除。
  • 自我保护:既然Eureka Server会定时剔除超时没有续约的服务,那就有可能出现一种场景,网络一段时间内发生了异常,所有的服务都没能够进行续约,Eureka Server就把所有的服务都剔除了,这样显然不太合理。所以,就有了自我保护机制,当短时间内,统计续约失败的比例,如果达到一定阈值,则会触发自我保护的机制,在该机制下,Eureka Server不会剔除任何的微服务,等到正常后,再退出自我保护机制。自我保护开关(eureka.server.enable-self-preservation: false)

Eureka 服务端 cloud-case-server[集群方式]

eureka server即是一个springboot项目也是一个注册中心,这点和zk等不同,项目搭建步骤与生产者消费者相同

  1. 修改win本机host文件映射

    127.0.0.1       eureka7001.com
    127.0.0.1 		eureka7002.com
    #模仿域名请求
    
  2. 修改pom文件,导入依赖

    <?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>cloud-case</artifactId>
            <groupId>com.zy.cloud</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>cloud-case-eureka-server</artifactId>
        <name>cloud-case-eureka-server</name>
    
        <dependencies>
            <!--eureka-server-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            </dependency>
            <dependency>
                <groupId>com.zy.cloud</groupId>
                <artifactId>cloud-case-common</artifactId>
            </dependency>
            <!--boot web actuator-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
        </dependencies>
    </project>
    
  3. 修改yml文件

    server:
      port: 7001
    eureka:
      instance:
        hostname: eureka${server.port:7001}.com #eureka服务端的实例名称  注意端口号 启动时添加
      client:
        register-with-eureka: false     #false表示不向注册中心注册自己。
        fetch-registry: false     #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
        service-url:
          #设置与eureka  server交互的地址和注册服务都需要依赖这个地址
          defaultZone: http://eureka${defaultZone:7001}.com:${defaultZone:7001}/eureka/  #集群就是指向其他的eureka  defaultZone项目启动时加载jvm上的参数
    spring:
      application:
        name: cloud-case-eureka-server
    
  4. 修改启动类

    添加eureka注册中心服务端注解@EnableEurekaServer

    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    @EnableEurekaServer //eureka Server激活注解
    public class CloudCaseEurekaServerApplication {
        public static void main(String[] args) {
            SpringApplication.run(CloudCaseEurekaServerApplication.class, args);
            System.out.println("springCloud Eureka start !!!");
        }
    }
    
  5. 利用idea启动两台服务,形成简单的Erurka集群

    将参数server.portdefaultZone对调改动,即可启动两个服务注册中心,并且相互注册

    image-20220729143802680

启动成功页面:

image-20220729144234221

image-20220729144432962

Eureka 客户端

Eureka 客户端,就是,所有注入服务注册中心的服务,公共common无需,我们修改生产者和消费者,注册到服务注册中心

  1. 添加pom依赖

            <!--eureka-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
    
  2. 修改application.yml文件,添加eureka配置

    #添加注册中心,eureka
    eureka:
      client:
        #表示是否将自己注册进eureka  默认为true
        register-with-eureka: true
        #是否从EurekaServer中抓取已有的注册信息,默认为true,单点无所谓,集群必须设置true才能和ribbon使用负载均衡
        fetch-registry: true
        service-url:
          #集群配置
          defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
      instance:
        #服务名称修改
        instance-id: produce${server.port}
        #访问路径可以显示ip地址
        prefer-ip-address: true
        lease-renewal-interval-in-seconds: 2 # 设置心跳间隔【默认30s】
        lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔(默认是90秒)
    
  3. 添加服务注解,启动生产者和消费者

    在启动类上添加@EnableEurekaClient注解

    image-20220729145849776

  4. 此时,生产者和消费者都已经注册到服务注册中心,那么消费者就可以通过服务名调用生产者

    image-20220729145446269

负载均衡器 Ribbon

Ribbon 简介

Ribbon是Neflix发布的开源项目,后由Spring Cloud开发团队封装于Spring Cloud中,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。功能是提供客户端的软件负载均衡算法和服务调用。Ribbon是一个基于HTTP和CP的客户端负载均衡工具,Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。Ribbon客户端提供一系列的完善的配置项,如连接超时,重试等。简单的说,就是配置文件中列出Load Balancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机等)去连接这些机器。我们很容器使用Ribbon实现自定义负载均衡算法。

Ribbon参考

Load Balancer

简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡的软件有:Nginx,LVS,硬件F5等。

Ribbon本地负载均衡与Nginx服务端负载均衡的区别

Nginx是服务器的负载均衡,客户端所有的请求都会交给Nginx,然后由Nginx实现转发请求,即负载均衡是由服务端实现的。
Ribbon本地负载均衡,在调用服务接口的时候,会在注册中心上获取注册信息服务列表之后缓冲到JVM本地,从而在本地实现RPC远程服务调用技术。

LB负载均衡分类

集中式LB:即在服务的消费方和提供方之间是有独立的LB设施(可以是硬件,如F5,也可以是软件,如Nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方。

进程内LB:将LB逻辑集成于消费方,消费方从注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个 合适的服务器。Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

Ribbon就是实现服务端的负载均衡加RestTemplate的调用。是一个软负载均衡的客户端组件,他可以和其他所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。

Ribbon负载均衡策略

image-20220729151026997

Ribbon架构图

image-20220729153331032

Ribbon在工作时分成两步:

  • 第一步先选择 EurekaServer ,它优先选择在同一个区域内负载较少的server.
  • 第二步再根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。
    其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权。

整合Ribbon 消费者端

  1. 启动两台生产者8090/8091,一台消费者8070

    image-20220729151903491

  2. 消费者端 新增Ribbon依赖,修改pom

    由于spring-cloud-starter-netflix-eureka-client自带了spring-cloud-starter-ribbon引用,所以就不用引入ribbon的包

    image-20220729152146932

  3. 新增config目录,MyselfRule.java配置类

    
    
    import com.netflix.loadbalancer.IRule;
    import com.netflix.loadbalancer.RoundRobinRule;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * Ribbon 负载均衡的算法
     **/
    @Configuration
    public class MyselfRule {
    
        @Bean
        public IRule myRule() {
            //默认轮询算法
            RoundRobinRule rule = new RoundRobinRule();
            return rule;
        }
    
    }
    
    
  4. 消费者端请求查询数据,观察两台生产者日志

自定义负载策略算法

  1. 新建MyCustomRule

    package com.zy.cloud.consumer.config;
    
    import com.netflix.client.config.IClientConfig;
    import com.netflix.loadbalancer.AbstractLoadBalancerRule;
    import com.netflix.loadbalancer.ILoadBalancer;
    import com.netflix.loadbalancer.Server;
    
    import java.util.List;
    import java.util.concurrent.ThreadLocalRandom;
    
    /**
     * Ribbon 简单自定义路由规则策略
     * 要求每台服务器被调用5次才能轮询下一个,也就是说以前每台机器一次,现在每台机器5次。
     */
    public class MyCustomRule extends AbstractLoadBalancerRule {
        @Override
        public void initWithNiwsConfig(IClientConfig iClientConfig) {
    
        }
    
        @Override
        public Server choose(Object key) {
            return choose(getLoadBalancer(), key);
        }
    
        /**
         * Randomly choose from all living servers
         */
        private int total = 0; // 总共被调用的次数,目前要求每台被调用5次
        private int currentIndex = 0; // 当前提供服务的机器号
    
        //@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
        //从服务清单中随机选择一个服务实例
        @SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE")
        public Server choose(ILoadBalancer lb, Object key) {
            if (lb == null) {
                return null;
            }
            Server server = null;
    
            while (server == null) {
                if (Thread.interrupted()) {
                    return null;
                }
                //负载均衡来获得可用实例列表upList和allList
                List<Server> upList = lb.getReachableServers();
                List<Server> allList = lb.getAllServers();
                int serverCount = allList.size();
                if (serverCount == 0) {
                    /*
                     * No servers. End regardless of pass, because subsequent passes
                     * only get more restrictive.
                     */
                    return null;
                }
                if (total < 5) {
                    server = upList.get(currentIndex);
                    total += 1;
                } else {
                    total = 0;
                    currentIndex++;
                    if (currentIndex >= upList.size()) {
                        currentIndex = 0;
                    }
                }
                if (server == null) {
                    /*
                     * The only time this should happen is if the server list were
                     * somehow trimmed. This is a transient condition. Retry after
                     * yielding.
                     */
                    Thread.yield();
                    continue;
                }
                if (server.isAlive()) {
                    return (server);
                }
                // Shouldn't actually happen.. but must be transient or a bug.
                server = null;
                Thread.yield();
            }
            //正常情况下,每次都应该可以选择一个服务实例
            return server;
        }
    
    
        //随机获取一个随机数
        protected int chooseRandomInt(int serverCount) {
            return ThreadLocalRandom.current().nextInt(serverCount);
        }
    
    }
    
  2. 修改配置类,重启消费者

    image-20220729153045999

  3. 请求消费者观察生产者

服务接口调用 OpenFeign

OpenFeign简介

  1. 什么是Feign?

Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用支持负载均衡。

  1. Feign是能干什么?

Feign旨在使编写Java Http客户端变得更容易。前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模板化的调用方法。但是在实际开发过程中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需要创建一个接口并使用注解的方式来配置它,(以前是Dao接口上面标准Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可)即可完成对服务提供方的接口绑定,简化了Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。
Feign集成了Ribbon,利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡,而与Ribbon不同的是,通过Feign值需要定义服务绑定接口且一声明式的方法,优雅而简单的实现了服务调用。

Feign和OpenFeign的区别

img

Openfeign实现负载均衡

这里为了和之前的消费者端区分,新建一个子服务,消费者 cloud-case-consumer-openFegin

  1. 导入pom依赖

    <?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>cloud-case</artifactId>
            <groupId>com.zy.cloud</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>cloud-case-consumer-openFeign</artifactId>
        <name>cloud-case-consumer-openFeign</name>
    
        <dependencies>
            <dependency>
                <groupId>com.zy.cloud</groupId>
                <artifactId>cloud-case-common</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!--eureka 消费者其实可以不注册到eureka -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <!--openfeign-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
    
        </dependencies>
    
    </project>
    
  2. 新建yml配置文件

    server:
      port: 8060
    spring:
      application:
        name: cloud-case-consumer-openFeign
      profiles:
        active: prod
    eureka:
      client:
        register-with-eureka: true
        fetch-registry: true
        service-url:
          #集群配置
          defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
      instance:
        #服务名称修改
        instance-id: consumerOpenFeign${server.port}
        #访问路径可以显示ip地址
        prefer-ip-address: true
    
    # 设置feign客户端超时时间(OpenFeign默认支持ribbon)
    ribbon:
      # 指的是建立连接所用的时间,适用于网络状态正常的情况下,两端连接所用的时间,设置等待5000为5秒时间
      ReadTimeout: 5000
      # 指的是建立连接后从服务器读取到可用资源所用的时间
      ConnectTimeout: 5000
    logging:
      level:
        # feign日志以什么级别监控哪个接口
        com.zy.cloud.openFeign.service.PaymentFeginService: info
    
  3. controller、 service Fegin 接口类

    @RestController
    @RequestMapping("consumerFegin")
    public class OrderFeginController {
    
        @Resource
        PaymentFeginService paymentFeginService;
    
        @GetMapping("/payment/get/{id}")
        public ApiReturn getPaymentById(@PathVariable("id") Long id) {
            ApiReturn apiReturn = paymentFeginService.queryById(id);
            return apiReturn;
        }
    }
    
    import com.zy.cloud.common.commonResult.ApiReturn;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    
    @ComponentScan
    @FeignClient(value = "CLOUD-CASE-PRODUCE")//使用Feign对某一个服务,调用哪个服务,服务名
    public interface PaymentFeginService {
    
        @GetMapping("/payment/get/{id}") //这个请求地址是和生产者的controller一致
        public ApiReturn queryById(@PathVariable("id") Long id);
    }
    
    
  4. 主启动类 添加Feign激活注解

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    @EnableEurekaClient
    @EnableFeignClients//激活Feign的注解  需要加到主启动类上
    public class CloudCaseConsumerOpenFeignApplication {
        public static void main(String[] args) {
            SpringApplication.run(CloudCaseConsumerOpenFeignApplication.class, args);
            System.out.println("springCloud Consumer OpenFegin start !!!");
        }
    }
    
  5. 启动cloud-case-consumer-openFegin 观察生产者

  6. 开启日志打印功能,新建FeginConfig配置类

    import feign.Logger;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class FeginConfig {
    
        /**
         *Openfeign提供了日志打印功能
         *  比如我们消费者服务调用生产者的服务的时候在接口调用的时候,我们可能需要更详细的信息,如信息头、状态码、时间、接口等等。
         *  就可以使用Openfeign日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。也就是对Feign接口的调用情况进行监控和输出。
         *
         *  Logger有四种类型:
         *
         * NONE:默认的,不显示任何日志。
         *
         * BASIC:仅记录请求方法、URL、响应状态及执行时间。
         *
         * HEADERS:除了BASIC中定义的信息之外,还有请求和响应的有信息。
         *
         * FULL:除了BASIC中定义的信息之外,还有请求和响应的正文及元数据。
         *
         * 通过注册Bean来设置日志记录级别!
         *
         */
        /**
         * feignClient配置日志级别
         *  在yml文件中需要开启日志的Feign客户端,要写PaymentService业务类的全限定类名
         * @return
         */
        @Bean
        public Logger.Level feignLoggerLevel() {
            // 请求和响应的头信息,请求和响应的正文及元数据
            return Logger.Level.FULL;
        }
    }
    

服务降级熔断 Hystrix

服务雪崩

多个为服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其他的微服务,这就是所谓的“扇出”。
如果扇出的链路上某一个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所以的“雪崩效应”

对于高流量的应用来说,单一的后端依赖可能会导致所有的服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延时进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序和系统。所以,通常当你发现一个模块下的某一个实例失败后,这时候这个模块依然还会接受流量,然后这个问题的模块该调用了其他的模块,这样就会发生级联故障,或者叫雪崩。

img

复杂的分布式体系结构中的应用程序,有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败

Hystrix简介

为了解决这一问题,就出现了Hystrix的技术,Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能保证在一个依赖出问题的情况下,不会导致整个服务失败,避免出现级联故障,以提高分布式系统的弹性。

“断路器”本身是一种开发装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

在springcloud中断路器组件就是Hystrix。Hystrix也是Netflix套件的一部分。他的功能是,当对某个服务的调用在一定的时间内(默认10s),有超过一定次数(默认20次)并且失败率超过一定值(默认50%),该服务的断路器会打开。返回一个由开发者设定的fallback。fallback可以是另一个由Hystrix保护的服务调用,也可以是固定的值。fallback也可以设计成链式调用,先执行某些逻辑,再返回fallback。

Hystrix三大作用

服务降级(Fallback):比如当服务器忙,请稍后再试,不让客户端等待并立刻返回一个友好提示,Fallback,会发生降级的几种情况:程序运行异常、超时、服务熔断触发服务降级。

服务熔断(Break):类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示。三个步骤先进行服务的降级、进而熔断、恢复调用链路。

实时的监控:会持续地记录所有通过Hystrix发起的请求执行信息,并以统计报表和图形的形式展示给用户,包括没秒执行多少成功,多少失败等。

了解服务限流(Flowlimit):比如秒杀高并发等操作,严禁一窝蜂的过来拥挤、安排大家排队,一秒钟N个请求,有序进行。

Hystrix代码实现

创建单独子服务,生产者消费者

生产者创建
  1. 导入依赖

    cloud-case-produce生产者相比多两个包

    	<!-- hystrix-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            </dependency>
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
            </dependency>
    
  2. 新建yml配置文件

    cloud-case-produce生产者yml相比,服务名修改为cloud-case-produce-hystrix

  3. controller

    package com.zy.cloud.produceHystrix.controller;
    
    
    import com.netflix.hystrix.contrib.javanica.annotation.DefaultProperties;
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
    import com.zy.cloud.common.commonCode.ApiReturnCode;
    import com.zy.cloud.common.commonResult.ApiReturn;
    import com.zy.cloud.common.pojo.PaymentPojo;
    import com.zy.cloud.produceHystrix.service.PaymentHystrixService;
    import org.apache.logging.log4j.LogManager;
    import org.apache.logging.log4j.Logger;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.client.ServiceInstance;
    import org.springframework.web.bind.annotation.*;
    
    import javax.annotation.Resource;
    import java.util.List;
    
    /**
     * PaymentController
     *
     * @author zy
     * @since 2022-07-21
     */
    @RestController
    @DefaultProperties(defaultFallback = "payment_Global_FallbackMethod") //全局默认异常回调方法
    public class PaymentHystrixController {
    
        private static Logger logger = LogManager.getLogger(PaymentHystrixController.class);
    
        @Resource
        private PaymentHystrixService paymentPojoService;
        /**
         * 正常访问
         */
        @GetMapping("/payment/hystrix/ok/{id}")
        @HystrixCommand
        public String paymentInfo_OK(@PathVariable("id") Integer id) {
            String result = paymentPojoService.paymentInfo_OK(id);
            logger.info("*******************result:" + result);
            return result;
        }
    
        /**
         * 超时访问
         */
        @GetMapping("/payment/hystrix/timeout/{id}/{timeNumber}")
    //    @HystrixCommand//默认的fallback注解
        public String paymentInfo_TimeOut(@PathVariable("id") Integer id, @PathVariable("timeNumber") Integer timeNumber) {
            String result = paymentPojoService.paymentInfo_TimeOut(id, timeNumber);
            logger.info("*********************result:" + result);
            return result;
    
        }
    
        @GetMapping("/payment/hystrix/paymentCircuitBreaker/{id}")
        public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
            String result = paymentPojoService.paymentCircuitBreaker(id);
            return result;
        }
    
        /**
         * 全局fallback
         */
        public String payment_Global_FallbackMethod() {
            return "Global异常处理信息,请稍后重试.o(╥﹏╥)o";
        }
    }
    
  4. service

    public interface PaymentHystrixService extends IService<PaymentPojo> {
    
        String paymentInfo_TimeOut(Integer id, Integer timeNumber);
    
        String paymentCircuitBreaker(Integer id);
    }
    
    import cn.hutool.core.util.IdUtil;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
    import com.zy.cloud.common.pojo.PaymentPojo;
    import com.zy.cloud.produceHystrix.mapper.PaymentMapper;
    import com.zy.cloud.produceHystrix.service.PaymentHystrixService;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.util.concurrent.TimeUnit;
    
    @Service
    public class PaymentServiceImpl extends ServiceImpl<PaymentMapper, PaymentPojo> implements PaymentHystrixService {
    
        @Resource
        private PaymentMapper paymentPojoMapper;
    
        /**
         * 正常访问
         *
         * @param id
         * @return
         */
        @Override
        public String paymentInfo_OK(Integer id) {
            return "线程池:" + Thread.currentThread().getName() + " cloud-case-produce-hystrix_OK,id:" + id + "\t" + "O(∩_∩)O哈哈~";
        }
    
        /**
         * 超时访问
         *
         * @param id
         * @return
         */
        //服务降级,对特殊接口 单独处理
        @HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
                @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",
                        value = "1000")})
        @Override
        public String paymentInfo_TimeOut(Integer id, Integer timeNumber) {
            try {
                TimeUnit.SECONDS.sleep(timeNumber);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //int age = 10/0; 错误代码也会触发降级
            return "线程池:" + Thread.currentThread().getName() + " cloud-case-produce-hystrix_TimeOut,id:" + id + "\t" +
                    "O(∩_∩)O哈哈~  耗时(秒)" + timeNumber;
        }
    
        //服务降级的兜底的方法
        public String paymentTimeOutFallbackMethod(Integer id, Integer timeNumber) {
            return "cloud-case-produce-hystrix 支付系统繁忙,o(╥﹏╥)o";
        }
    
        /*
      服务熔断
      	什么是断路器?
    		“断路器”本身是一种开关装置,当某个服务单元发生故障监控(类似熔断保险丝),向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延。乃至雪崩。
    	什么是熔断机制?
    		熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某一个微服务出错不可用或者响应时间太长,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息,当检测到该节点微服务调用响应正常后,恢复调用链路。
    		在Spring Cloud框架里,熔断机制通过Hystrix实现。Hystrix会监控微服务调用的状况,当失败的调用到一定的阈值,缺省是5秒内20次调用失败,就会启动熔断机制。熔断机制的注解是@HystrixCommand。
        重要参数:
        	circuitBreaker.sleepWindowInMilliseconds:
        		断路器的快照时间窗,也叫做窗口期。可以理解为一个触发断路器的周期时间值,默认为10秒(10000)。
    		circuitBreaker.requestVolumeThreshold:
    			断路器的窗口期内触发断路的请求阈值,默认为20。换句话说,假如某个窗口期内的请求总数都不到该配置值,那么断路器连发生的资格都没有。断路器在该窗口期内将不会被打开。
    		circuitBreaker.errorThresholdPercentage:
    			断路器的窗口期内能够容忍的错误百分比阈值,默认为50(也就是说默认容忍50%的错误率)。打个比方,假如一个窗口期内,发生了100次服务请求,其中50次出现了错误。在这样的情况下,断路器将会被打开。在该窗口期结束之前,即使第51次请求没有发生异常,也将被执行fallback逻辑。
        */
        ///熔断
        //在10秒内,发生20次以上的请求时,假如错误率达到50%以上,则断路器将被打开。(当一个窗口期过去的时候,断路器将变成半开(HALF-OPEN)状态,如果这时候发生的请求正常,则关闭,否则又打开)
        @HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {
                @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),// 是否开启断路器
                @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),// 请求次数
                @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 时间窗口期
                @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),// 失败率达到多少后跳闸
        })
        @Override
        public String paymentCircuitBreaker(Integer id) {
            if (id < 0) {
                throw new RuntimeException("******id 不能负数");
            }
            String serialNumber = IdUtil.simpleUUID();
            return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
        }
        
        //兜底降级的方法
        public String paymentCircuitBreaker_fallback(Integer id) {
            return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~   id: " + id;
        }
        /*
        * 断路器的窗口期内能够容忍的错误百分比阈值,默认为50(也就是说默认容忍50%的错误率)。打个比方,假如一个窗口期内,发生了100次服务请求,其中50次出现了错误。
        * 在这样的情况下,断路器将会被打开。在该窗口期结束之前,即使第51次请求没有发生异常,也将被执行fallback逻辑。
        * */
    }
    
    
    
  5. 修改主启动类开启EnableCircuitBreaker启动生产者

    import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.web.servlet.ServletRegistrationBean;
    import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    import org.springframework.context.annotation.Bean;
    
    @SpringBootApplication
    @EnableEurekaClient //eureka 客户端
    @EnableDiscoveryClient //服务发现
    @EnableCircuitBreaker //降级处理 开启
    public class CloudCaseProduceHystrixApplication {
        public static void main(String[] args) {
            SpringApplication.run(CloudCaseProduceHystrixApplication.class, args);
            System.out.println("springCloud Produce  Hystrix start !!!");
        }
    }
    
消费者创建

创建单独子服务,消费者 cloud-case-consumer-hystrix

  1. 导入pom依赖

    cloud-case-consumer-openFeign生产者相同

  2. yml文件

    修改服务名(cloud-case-consumer-hystrix)和端口号(8050) 其它与cloud-case-consumer-openFeign生产者

  3. controller新增两个方法

        /**
         * 正常访问
         *
         * @param id
         * @return
         */
        @GetMapping("/consumerHystrix/payment/hystrix/ok/{id}")
        public String paymentInfo_OK(@PathVariable("id") Integer id) {
            String result = paymentHystrixService.paymentInfo_OK(id);
            return result;
        }
    
        /**
         * 超时访问
         * @param id
         * @return
         */
        @GetMapping("/consumerHystrix/payment/hystrix/timeout/{id}/{timeNumber}")
        public String paymentInfo_TimeOut(@PathVariable("id") Integer id, @PathVariable("timeNumber") Integer timeNumber) {
            String result = paymentHystrixService.paymentInfo_TimeOut(id, timeNumber);
            return result;
        }
      @GetMapping("/payment/circuit/{id}")
        public String paymentCircuitBreaker(@PathVariable("id") Integer id)
        {
            String result = paymentHystrixService.paymentCircuitBreaker(id);
            return result;
        }
    
    
  4. 新增PaymentFallbackService/PaymentFallbackServiceFactory

    /**
     * 对接口降级处理
     **/
    @Component
    public class PaymentFallbackService implements PaymentHystrixService {
        @Override
        public ApiReturn queryById(Long id) {
            return new ApiReturn(ApiReturnCode.HTTP_ERROR);
        }
    
        @Override
        public String paymentInfo_TimeOut(Integer id, Integer timeNumber) {
            return "PaymentFallbackService 降级 fall  TimeOut 服务器出现故障,o(╥﹏╥)o";
        }
    
        @Override
        public String paymentInfo_OK(Integer id) {
            return "PaymentFallbackService 降级 fall  OK 服务器出现故障,o(╥﹏╥)o";
        }
    
        @Override
        public String paymentCircuitBreaker(Integer id) {
            return "PaymentFallbackService 降级 fall  paymentCircuitBreaker 服务器出现故障,o(╥﹏╥)o";
        }
    }
    
    
    public class PaymentFallbackServiceFactory implements FallbackFactory {
        @Override
        public Object create(Throwable throwable) {
            System.out.println("fallback; reason was: {}" + throwable.getMessage());
            return new PaymentHystrixService() {
                @Override
                public ApiReturn queryById(Long id) {
                    return new ApiReturn(ApiReturnCode.HTTP_ERROR);
                }
    
                @Override
                public String paymentInfo_TimeOut(Integer id, Integer timeNumber) {
                    return "PaymentFallbackServiceFactory 降级 fall  TimeOut 服务器出现故障,o(╥﹏╥)o";
                }
    
                @Override
                public String paymentInfo_OK(Integer id) {
                    return "PaymentFallbackServiceFactory 降级 fall  OK 服务器出现故障,o(╥﹏╥)o";
                }
    
                @Override
                public String paymentCircuitBreaker(Integer id) {
                    return "PaymentFallbackServiceFactory 降级 fall  paymentCircuitBreaker 服务器出现故障,o(╥﹏╥)o";
                }
            };
        }
    }
    
    
  5. service

    @ComponentScan
    //使用Feign对某一个服务  fallback 降级处理类  fallbackFactory降级处理工程 二选一
    //@FeignClient(value = "CLOUD-CASE-PRODUCE-HYSTRIX", fallback = PaymentFallbackService.class)
    @FeignClient(value = "CLOUD-CASE-PRODUCE-HYSTRIX", fallbackFactory = PaymentFallbackServiceFactory.class)
    public interface PaymentHystrixService {
    
        @GetMapping("/payment/get/{id}")
        public ApiReturn queryById(@PathVariable("id") Long id);
    
    
        @GetMapping("/payment/hystrix/timeout/{id}/{timeNumber}")
        String paymentInfo_TimeOut(@PathVariable("id") Integer id, @PathVariable("timeNumber") Integer timeNumber);
    
        @GetMapping("/payment/hystrix/ok/{id}")
        String paymentInfo_OK(@PathVariable("id") Integer id);
    
        @GetMapping("/payment/hystrix/paymentCircuitBreaker/{id}")
        String paymentCircuitBreaker(@PathVariable("id") Integer id);
    }
    
  6. 主启动类,启动消费者

    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    @EnableEurekaClient
    @EnableFeignClients
    public class ColudCaseConsumerHystrixApplocation {
        public static void main(String[] args) {
            SpringApplication.run(ColudCaseConsumerHystrixApplocation.class, args);
            System.out.println("springCloud Consumer Hystrix start !!!");
        }
    }
    
    
  7. 请求接口,可以看到当参数错误,或发生异常时,会返回回调方法中的结果,保证页面不崩

    image-20220729163849291

  8. 使用压测工具也可以发现/consumerHystrix/payment/hystrix/ok/1

image-20220729164126429

image-20220729164157111

此时线程池全部被占用,我们访问页面,不会等待,会直接返回回调方法中的友好提示

image-20220729164256071


Gateway API网关服务

Gateway 网关简介

在微服务架构中,一个系统往往由多个微服务组成,而这些服务可能部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。

这种客户端直接请求服务的方式存在以下问题:

  • 当服务数量众多时,客户端需要维护大量的服务地址,这对于客户端来说,是非常繁琐复杂的。
  • 在某些场景下可能会存在跨域请求的问题。
  • 身份认证的难度大,每个微服务需要独立认证。

我们可以通过 API 网关来解决这些问题,下面就让我们来看看什么是 API 网关

1. 简介:

  • API 网关是一个搭建在客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。
  • API 网关就像整个微服务系统的门面一样,是系统对外的唯一入口。有了它,客户端会先将请求发送到 API 网关,然后由 API 网关根据请求的标识信息将请求转发到微服务实例

2. API网关的职能:

  • 请求接入:是整个微服务系统的接口点,所有的API接口服务请求的接入点
  • 业务聚合:作为所有后端业务服务的聚合点
  • 中介策略:实现安全、验证、路由、过滤、限流等策略
  • 统一管理:对所有API服务和策略进行统一管理

3. API 网关的分类与功能?

img

4. Gateway是什么

Spring Cloud GatewaySpring官方基于Spring 5.0,Spring Boot 2.0Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。

5. 为什么用Gateway

Spring Cloud Gateway 可以看做是一个 Zuul 1.x 的升级版和代替品,比 Zuul 2 更早的使用 Netty 实现异步 IO,从而实现了一个简单、比Zuul 1.x更高效的、与Spring Cloud紧密配合的 API 网关。
Spring Cloud Gateway 里明确的区分了 RouterFilter,并且一个很大的特点是内置了非常多的开箱即用功能,并且都可以通过 SpringBoot 配置或者手工编码链式调用来使用。
比如内置了 10 种 Router,使得我们可以直接配置一下就可以随心所欲的根据 Header、或者 Path、或者 Host、或者 Query 来做路由。
比如区分了一般的 Filter 和全局 Filter,内置了 20 种 Filter 和 9 种全局 Filter,也都可以直接用。当然自定义 Filter 也非常方便。

6. 最重概念

  • Route(路由):这是网关的基本构建块,它由一个ID,一个URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。

  • Predicate(断言):输入类类是一个ServerWebExchange。我们可以使用它来匹配来自HTTP请求的任何内容,例如headers。如果请求与断言相匹配则进行路由。

  • Filter(过滤器)Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者后对请求进行修改。

7. 工作流程

image-20220801101942712

客户端向Spring Cloud Gateway发出请求,然后在Gateway Hander Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler

Handler在通过指定的过滤器链来将请求发送到我们实际的服务业务逻辑,然后返回。过滤器之间用虚线分是因为过滤器可能会在发送代理请求之间(“pre”)或之后(“post”)执行业务逻辑。

Filterpre类型的过滤器可以做参数校验、权限校验、流量监控、协议转换等。在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

主要核心:是路由转发+执行过滤器链

Gateway 实现

新建子服务项目,实现网关路由功能 cloud-case-gateway

  1. 修改pom文件

    <?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>cloud-case</artifactId>
            <groupId>com.zy.cloud</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>cloud-case-gateway</artifactId>
        <name>cloud-case-gateway</name>
    
        <dependencies>
            <dependency>
                <groupId>com.zy.cloud</groupId>
                <artifactId>cloud-case-common</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
            <!--gateway-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-gateway</artifactId>
            </dependency>
        </dependencies>
    </project>
    
    
  2. 修改yml配置文件(使用)

    server:
      port: 8010
    
    spring:
      application:
        name: cloud-case-gateway
      profiles:
        active: prod
    # 路由断言 配置文件格式 还可以使用编码式,两者二选一
      cloud:
        gateway:
          routes:
            - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
              #匹配后提供服务的路由地址
              uri: http://localhost:8096 # 可用服务名
              predicates:
                - Path=/payment/hystrix/** # 断言,路径相匹配的进行路由 根据路由正则匹配
            - id: payment_route2
              uri: http://localhost:8092
              predicates:
                Path=/payment/get/** #断言,路径相匹配的进行路由
    
    #注册中心,eureka
    eureka:
      client:
        #表示是否将自己注册进eureka  默认为true
        register-with-eureka: true
        #是否从EurekaServer中抓取已有的注册信息,默认为true,单点无所谓,集群必须设置true才能和ribbon使用负载均衡
        fetch-registry: true
        service-url:
          #集群配置
          defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
      instance:
        #服务名称修改
        instance-id: cloudCaseGateway${server.port}
        #访问路径可以显示ip地址
        prefer-ip-address: true
    
  3. 修改启动类

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    @EnableEurekaClient
    public class CloudCaseGatewayApplication {
        public static void main(String[] args) {
            SpringApplication.run(CloudCaseGatewayApplication.class, args);
            System.out.println("SpringCloud Gateway start !!! ");
        }
    }
    
  4. 实现编码式路由

    新建config目录,新增配置类:

    import org.springframework.cloud.gateway.route.RouteLocator;
    import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * 配置了一个id为routr-name的路由规则
     */
    @Configuration
    public class GatewyConfig {
        /*我们现在配置的是YML进行配置的,还有一种配置方案就是通过硬编码的方式。就是代码中注入RouteLocator的Bean,
        是为了解决YML文件配置太多,文件太大的问题。那就开始撸起来吧!*/
        @Bean
        public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
            RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
            //uri 中 lb://服务名  代表从注册中心动态获取请求服务的地址
            routes.route("cloud-case-produce-hystrix", r -> r.path("/payment/hystrix/**").uri("lb://cloud-case-produce-hystrix")).build();
            routes.route("cloud-case-produce", r -> r.path("/payment/get/**").uri("lb://cloud-case-produce")).build();
            routes.route("cloud-case-consumer", r -> r.path("/consumer/**").uri("lb://cloud-case-consumer")).build();
            routes.route("cloud-case-consumer-openFeign", r -> r.path("/consumerFegin/**").uri("lb://cloud-case-consumer-openFeign")).build();
            return routes.build();
        }
    }
    
  5. 自定义Filter过滤

    新增TokenFilter类:

    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    /*
     * 自定义全局file  校验请求是否带token
     * */
    public class TokenFilter implements GlobalFilter, Ordered {
    
        Logger logger = LoggerFactory.getLogger(TokenFilter.class);
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String token = exchange.getRequest().getQueryParams().getFirst("token");
            if (token == null || token.isEmpty()) {
                logger.info("token is empty...");
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
            return chain.filter(exchange);
        }
    
        @Override
        public int getOrder() {
            return -100;
        }
    }
    

    修改GatewyConfig注入:

      @Bean
        public TokenFilter tokenFilter() {
            return new TokenFilter();
        }
    
  6. 启动服务,浏览器访问网关,即可路由到真正处理业务的微服务中

config 分布式配置中心

为什么用分布式配置中心

微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务,由于每个服务都需要必要的配置信息才能运行,所以一套集中式、动态的配置管理设施是必不可少的。我们每个服务都有自己的配置文件,如果有上百个微服务的话就有上百个配置文件,如果要改某一个配置的参数,有可能就需要修改好多个微服务的配置文件,那样岂不是又费时又费力,Spring Cloud提供了ConfigServer配置中心来解决这个问题,实现了一处修改,处处运行。

什么是分布式配置中心

Spring Cloud Config为微服务架构中的微服务提供了集中化的外部配置支持,配置服务器为各个不同的微服务应用的所有环境提供了中心的化额外部配置。如下图:

image-20220801112818537

Spring Cloud Config分为服务端和客户端两部分,服务端也成为分布式配置中心,他是一个独立的微服务应用,用来连接配置服务器并未客户端提供获取配置信息,加密、解密信息等访问接口。

客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

Spring Cloud Config都有什么作用?

  1. 集中管理配置文件,便于管理
  2. 不同的环境不同的配置,动态化的配置更新,分环境比如:dev/test/prod/bata/release
  3. 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统计拉去配置自己的信息。
  4. 当配置发生改变时,服务不需要重启即可感知到配置的变化并应用新的配置
  5. 将配置信息以Rest接口暴露,post/curi访问刷新即可

代码实现

gitee上新建配置文件仓库,也可以使用已经准备好的:https://gitee.com/Sir-yuChen/springcloud-config

配置中心

img

  1. 修改pom文件

    不要引入springweb模块

    <?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>cloud-case</artifactId>
            <groupId>com.zy.cloud</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>cloud-case-config-center</artifactId>
        <name>cloud-case-config-center</name>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-config-server</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-actuator</artifactId>
            </dependency>
        </dependencies>
    </project>
    
  2. 修改yml配置文件

    spring:
      application:
        name: cloud-case-config-center #注册进Eureka服务器的服务名称
      cloud:
        config:
          server:
            git:
              uri: https://gitee.com/Sir-yuChen/springcloud-config/  #GitHub上复制的项目地址
              username: xxxx
              password: xxxx
              skipSslValidation: true # 跳过ssl认证
              force-pull: true
              search-paths: cloud-case
              default-label: main
    #注册中心,eureka
    eureka:
      client:
        #表示是否将自己注册进eureka  默认为true
        register-with-eureka: true
        #是否从EurekaServer中抓取已有的注册信息,默认为true,单点无所谓,集群必须设置true才能和ribbon使用负载均衡
        fetch-registry: true
        service-url:
          #集群配置
          defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
      instance:
        #服务名称修改
        instance-id: ConfigCenter${server.port}
        #访问路径可以显示ip地址
        prefer-ip-address: true
    
    server:
      port: 8020
    
  3. 修改启动类

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.config.server.EnableConfigServer;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    
    
    @SpringBootApplication
    @EnableEurekaClient
    @EnableConfigServer // 配置中心注解
    @EnableDiscoveryClient
    public class CloudCaseConfigCenterApplication {
        public static void main(String[] args) {
            SpringApplication.run(CloudCaseConfigCenterApplication.class, args);
            System.out.println("springCloud Cofig Center start !!! ");
        }
    }
    
  4. 启动项目

客户端
  1. 修改pom文件

    <?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>cloud-case</artifactId>
            <groupId>com.zy.cloud</groupId>
            <version>1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
        <artifactId>cloud-case-config-client</artifactId>
        <name>cloud-case-config-client</name>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    <!--        springcloud config client 注意一定不要引错包了-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </dependency>
        </dependencies>
    </project>
    
    
  2. 修改配置文件

    bootstrap.yml

    • bootstrap.yml 是系统级别的,加载优先级高于 application.yml ,负责从外部加载配置并解析

    • application.yml是用户级的资源配置项,而bootstrap.yml是系统级的资源配置项,bootstrap.yml的优先级更高,SpringCloud会创建一个"Bootstrap Context",作为Spring应用的“Application Context"的父上下文。

    • 初始化的时候,“Bootstrap Context"负责从外部源加载配置属性并解析配置,这两个上下文共享一个从外部获取的"Environment”。”Bootstrap“属性有高优先级,默认情况系,它们不会被本地配置覆盖。

    • "Bootstrap Context"和"Application Context"这两个上下文有不同的约定,所以新增一个bootstrap.yml文件,保证这两个上下文的配置分离。

    spring:
      application:
        name: cloud-case-config-client
      cloud:
        #Config客户端配置
        config:
          label: main #分支名称
    #      uri: http://localhost:8020/ #配置中心地址     service-id 服务发现二选一配置即可【建议使用服务名】
          profile: dev  # config-dev.yml 中的 dev 环境名
          name: cloud-case-config-client
          discovery:
            enabled: true
            service-id: CLOUD-CASE-CONFIG-CENTER # 指定配置中心的service-id, 便于扩展为高可用配置集群,不区分大小写
          server:
            git:
              search-paths: cloud-case
    
    #注册中心,eureka
    eureka:
      client:
        #表示是否将自己注册进eureka  默认为true
        register-with-eureka: true
        #是否从EurekaServer中抓取已有的注册信息,默认为true,单点无所谓,集群必须设置true才能和ribbon使用负载均衡
        fetch-registry: true
        service-url:
          #集群配置
          defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
      instance:
        #服务名称修改
        instance-id: ConfigClient${server.port}
        #访问路径可以显示ip地址
        prefer-ip-address: true
    

    application.yml

    server:
      port: 8021
    
  3. 主启动类

    @SpringBootApplication
    @EnableEurekaClient
    @EnableDiscoveryClient
    public class CloudCaseConfigClientApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(CloudCaseConfigClientApplication.class, args);
            System.out.println("springCloud Cofig Client start !!! ");
        }
    }
    
  4. 获取远程配置

    远程配置文件:

    image-20220801132010700

    获取远程配置:

    @RestController
    public class ConfigClientController {
    
        @Value("${config.info}")
        private String configInfo;
    
        @Value("${config.version}")
        private String configVersion;
    
        @GetMapping("/servicePort")
        public String getConfigInfo() {
            System.out.println("================》 获取远程配置");
            return "远程获取到的配置:" + configInfo + "-----" + configVersion;
        }
    }
    
    
  5. actuator监控动态刷新配置

    1. 导jar包

      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
      
    2. 加配置bootstrap.yml

      # 暴露监控端点
      management:
        endpoints:
          web:
            exposure:
              include: "*"
      
    3. 加注解

      在业务类ConfigClientController上添加@RefreshScope注解使客户端服务具有刷新功能

    4. 刷新配置

      发送POST请求刷新客户端Client该刷新请求必须发送后,客户端才能获得刷新后的信息,刷新客户端的请求必须是POST请求

      curl -X POST "http://127.0.0.1:xxxx/actuator/refresh"
      

Bus消息总线

用SpringCloud Config时,我们可以实现配置信息手动的动态刷新,也就是远端配置信息发生改变后,需要告诉服务端配置信息发生变化后,服务端才会更新配置信息,而现在我们想要实现分布式自动刷新配置信息功能,这就需要我们使用SpringCloud Bus消息总线配合SpringCloud Config实现配置信息的动态刷新。SpringCloud Bus是用来将分布式系统的节点与轻量级消息系统连接起来的框架,整合了Java的事件处理机制和消息中间件的功能,SpringCloud Bus目前支持两种消息代理:RabbitMQ和Kafka。SpringCloud Bus能管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等, 也可以当做微服务间的通信通道。

什么是总线?

在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。

SpringCloud Config客户端的实例都监听消息队列中的同一个主题(topic)(默认是SpringCloud Bus),当一个服务器刷新数据的时候,它会把这个信息放入到主题中,这样其他监听了同一主题的服务就能得到通知,然后去更新自身的配置。

SpringCloud Bus动态刷新全局广播

消息总线的两种设计思想:

  • 方案一:利用消息总线触发一个客户端/bus/refresh端点,而后刷新所有客户端的配置:

在这里插入图片描述

  • 方案二:利用消息总线触发一个服务端ConfigServer的/bus/refresh端点,而后刷新所有客户端的配置:

在这里插入图片描述

  • 明显第二种架构更加合适,第一种架构不合适的原因主要有:
  • 打破了微服务的职责单一性,因为微服务本身是业务模块,它本不应该承担配置刷新的职责;
  • 破坏了微服务各节点的对等性;
  • 有一定的局限性,比如在微服务迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,那就会增加更多的修改。

所以虽然从技术上两种方案都可以实现,但是无疑在技术选型上我们应该选择第二种方案。

bus整合到config

  1. cloud-case-config-center 配置中心

    1. center导出依赖

      <!--添加消息总线RabbitMQ支持-->
      <dependency>
      	<groupId>org.springframework.cloud</groupId>
      	<artifactId>spring-cloud-starter-bus-amqp</artifactId>
      </dependency>
      
    2. 修改application.yml配置

      # RabbitMQ相关配置
      spring:
        rabbitmq:
          host: localhost
          port: 5672
          username: guest
          password: guest
      # 暴露总线刷新配置的端点  
      management:
        endpoints:
          web:
            exposure:
              include: 'bus-refresh'
      
  2. cloud-case-config-client 客户端

    1. pom

      <!--添加消息总线RabbitMQ支持-->
      <dependency>
      	<groupId>org.springframework.cloud</groupId>
      	<artifactId>spring-cloud-starter-bus-amqp</artifactId>
      </dependency>
      
    2. application.yml

      # RabbitMQ相关配置
      spring:
        rabbitmq:
          host: localhost
          port: 5672
          username: guest
          password: guest
      
  3. 我们将远程配置中心的配置文件修改为如下:

    image-20220801135940436

  4. 启动两个配置中心的客户端8021/8022 请求获取配置文件,查看是否变化

  5. 动态刷新定点通知

    再全局广播中,我们更新配置文件的信息对所有服务都进行了通知,但是假设我们只想通知8070,而不想通知3366又该怎么办呢,这就需要我们用定点通知的方法通知3355,也就是说我们进行消息通知时只指定具体某个实例生效而不是全部生效,这种情况下就需要修改我们发送的POST请求:

    curl -X POST "http://{配置中心的地址}/actuator/bus-refresh/{destination}"
    

    这样的话,/bus/refresh请求就不再发送到具体的服务实例上,而是发给ConfigServer配置中心并通过destination参数指定需要更新配置的服务或实例。其中destination具体为微服务+端口号

    比如现在我们修改远端配置文件,但是我们只想通知8021的配置更新信息,而不想通知8022,此时我们发送的POST请求就应该是:

    curl -X POST "http://localhost:8020/actuator/bus-refresh/config-client:8021"
    

    此时我们再访问配置中心及其客户端的配置中心,我们发现8021的配置信息得到了更新,而8022的配置中心仍然是之前的配置

Stream消息驱动

消息驱动是什么?

在实际应用中有很多消息中间件,比如现在企业里常用的有ActiveMQ、RabbitMQ、RocketMQ、Kafka等,学习所有这些消息中间件无疑需要大量时间经历成本,那有没有一种技术,使我们不再需要关注具体的消息中间件的细节,而只需要用一种适配绑定的方式,自动的在各种消息中间件内切换呢?消息驱动就是这样的技术,它能屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型

SpringCloud Stream是一个构件消息驱动微服务的框架。应用程序通过inputs和outputs来与SpringCloud Stream中的绑定器(binder)对象交互,通过配置来绑定,而SpringCloud Stream的绑定器对象负责与消息中间件交互,所以,我们只需要搞清楚如何与SpringCloud Stream交互就可以方便使用消息驱动的方式。但是截至到目前时间,SpringCloud Stream目前仅支持RabbitMQ和Kafka

设计思想

在经典的消息队列中,生产者/消费者之间靠消息媒介传递信息内容,消息必须走特定的通道Message Channel,消息通道里的子接口Subscribable Channel消费消息,然后MessageHandler负责收发处理。

在SpringCloud Stream中,通过定义绑定器(binder)作为中间层,实现了应用程序与消息中间件细节之间的隔离。在消息绑定器中,INPUT对应于消费者,OUTPUT对应于生产者,Stream中的消息通信方式遵循了发布—订阅模式:用Topic(主题)进行广播(RabbitMQ中对应于Exchange交换机,Kafka中就是Topic)。

Stream编码API和常用注解

组成 说明
Middleware 中间件,目前只支持RabbitMQ和Kaf
Binder Binder是应用与消息中间件之间的封装,目前实行了RabbitMQ和Kafka的Binder,通过Binder可以很方便的连接中间件,可以动态的改变消息类型(对应于Kafka的topic,RabbitMQ的exchange),这些都可以通过配置文件来实现
@Input 注解标识输入通道,通过该输入通道接收到的消息进入应用程序
@Output 注解标识输出通道,发布的消息将通过该通道离开应用程序
@StreamListner 监听队列,用于消费者的队列的消息接收
@EnableBinding 使信道Channel和交换机/主题(Exchange/Topic)绑定在一起

Stream 集成

  1. 消息生产者

    在原cloud-case-produce 生产者子服务基础下集成stream消息驱动

    1. 依赖

      <dependency>
      	<groupId>org.springframework.cloud</groupId>
      	<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
      </dependency>
      
    2. application.yml

        # springcloud stream 整合
      spring:
        cloud:
          stream:
            # 绑定mq信息,这里我们绑定的是rabbitmq
            binders:
              # 给这个binder起个名字
              spring-clould-stream-binder:
                # mq的类型,如果是kafka的话就是kafka
                type: rabbit
                # 配置mq的信息
                environment:
                  spring:
                    rabbitmq:
                      host: localhost
                      port: 5672
                      username: guest
                      password: guest
            # 这里就是将通道与binder进行绑定
            bindings:
              # 定义output,因为我们是消息生产者,需要将消息写到channel中
              output:
                # 使用消息队列名字,在kafka就是topic的名字,然后rabbitmq的话就是Exchange 的名字
                destination: springCloudStreamExchange
                # 传输内容的格式,也就是消息的格式,如果是json的话 application/json
                content-type: text/plain
                # 绑定的binder
                binder: spring-clould-stream-binder
                # 将消息发送固定组 保证消息的幂等性,避免消息重复消费
                group: A
      
    3. 写业务类实现消费发送接口service

      // springcloud stream 发送消息接口
      public interface IMessageProvider {
          boolean send();
      }
      
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.cloud.stream.annotation.EnableBinding;
      import org.springframework.cloud.stream.messaging.Source;
      import org.springframework.messaging.MessageChannel;
      import org.springframework.messaging.support.MessageBuilder;
      
      import java.util.UUID;
      
      //注意service的实现类中不再需要@Service注解,因为这个service不再是传统意义上的和Controller、DAO数据等进行交互的service,而是要绑定绑定器打交道的service。
      @EnableBinding(Source.class) //定义消息的推送管道
      public class MessageProviderImpl implements IMessageProvider {
      
          @Autowired
          private MessageChannel output; //消息发送管道
      
          @Override
          public boolean send() {
              String serial = UUID.randomUUID().toString();
              boolean send = output.send(MessageBuilder.withPayload(serial).build());//发送消息
              System.out.println("====Produce springCloud stream 发送消息:====>:" + serial);
              return send;
          }
      }
      
    4. controller层

      @RestController
      public class MqController {
      
          private static Logger logger = LogManager.getLogger(MqController.class);
      
          @Resource
          private IMessageProvider iMessageProvider;
      
          @GetMapping("/sendMessage")
          public boolean sendMessage() {
              logger.error("error 发送消息 elk test~~~~~~");
              logger.info("info 发送消息 elk test~~~~~~");
              logger.warn("warn 发送消息 elk test~~~~~~");
              return iMessageProvider.send();
          }
      }
      
  2. 消息消费者

    在原cloud-case-consumer 生产者子服务基础下集成stream消息驱动

    1. pom依赖

       <!--springcloud stream 消息驱动 整合-->
              <dependency>
                  <groupId>org.springframework.cloud</groupId>
                  <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
              </dependency>
      
    2. application.yml

      spring:
        cloud:
          stream:
            # 绑定mq信息,这里我们绑定的是rabbitmq
            binders:
              # 给这个binder起个名字
              spring-clould-stream-binder:
                # mq的类型,如果是kafka的话就是kafka
                type: rabbit
                # 配置mq的信息
                environment:
                  spring:
                    rabbitmq:
                      host: localhost
                      port: 5672
                      username: guest
                      password: guest
            # 这里就是将通道与binder进行绑定
            bindings:
              # 定义output,因为我们是消息生产者,需要将消息写到channel中
              input:
                # 使用消息队列名字,在kafka就是topic的名字,然后rabbitmq的话就是Exchange 的名字
                destination: springCloudStreamExchange
                # 传输内容的格式,也就是消息的格式,如果是json的话 application/json
                content-type: text/plain
                # 绑定的binder
                binder: spring-clould-stream-binder
                # 自定义组 解决重复消费
                group: A
      
    3. service业务代码

      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.cloud.stream.annotation.EnableBinding;
      import org.springframework.cloud.stream.annotation.StreamListener;
      import org.springframework.cloud.stream.messaging.Sink;
      import org.springframework.messaging.Message;
      
      //这里是消息消费者,所以就就是接收者,所以使用Sink
      @EnableBinding(Sink.class)
      public class MessageConsumerService {
      
          @Value("${server.port}")
          private String port;
      
          //监听消息
          @StreamListener(Sink.INPUT)
          public void recevieMes(Message<String> message) {
              //监听MQ但是不会消费堆积的消息 所有的消费者都会接到相同的消息,消费,消息重复 原因:组流水号不一样 【自定义组名即可解决】
              /*
              *   在RabbitMQ中,默认分组是不同的,组流水号不一样,被认为不同组,我们查看testExchange交换机,可以发现8802和8803两个消息消费者处于不同的组,
              *       所以8801消息生产者发送的消息可以被这两个消费者重复消费:
              * */
              System.out.println("--" + port + "---Consumer springcloud Stream 消费消息:" + message.getPayload());
          }
      }
      
  3. 启动一个生产者8090两个消费者8070 & 8071,调用生产在发送消息消费者consumer这会监听MQ中的消费,并消费

logstash+elk 日志收集 skywalking链式追踪

  1. logstash+elk 安装

    https://www.cnblogs.com/zsql/p/13143445.html
    skywalking: https://skywalking.apache.org/zh/2020-04-19-skywalking-quick-start/
    
  2. pom

         <!-- skywalking   logstash-->
            <dependency>
                <groupId>org.apache.skywalking</groupId>
                <artifactId>apm-toolkit-trace</artifactId>
             	<version>8.11.0</version>
            </dependency>
            <dependency>
                <groupId>org.apache.skywalking</groupId>
                <artifactId>apm-toolkit-logback-1.x</artifactId>
             	<version>8.11.0</version>
            </dependency>
            <dependency>
                <groupId>net.logstash.logback</groupId>
                <artifactId>logstash-logback-encoder</artifactId>
             	<version>6.1</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </dependency>
    
  3. logback-spring.xml

    <?xml version="1.0" encoding="utf-8" ?>
    
    <configuration>
    
        <!-- logback-spring加载早于application.yml,如果直接通过${参数key}的形式是无法获取到对应的参数值-->
        <!-- source指定的是application.yml配置文件中key,其它地方直接用${log.path}引用这个值 -->
        <!-- 解决在相对路径下生成log.path_IS_UNDEFINED的问题,增加defaultValue -->
        <springProperty scope="context" name="base.path" source="logging.file.path" defaultValue="${user.home}/logs"/>
        <!-- app.name根据你的应用名称修改 -->
        <springProperty scope="context" name="app.name" source="spring.application.name" defaultValue="applog"/>
        <property name="log.path" value="${base.path}/${app.name}/${app.name}"/>
        <!--配置规则类的位置-->
        <conversionRule conversionWord="ip" converterClass="com.zy.cloud.common.utils.IPLogConfig"/>
        <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符-->
        <property name="log.pattern"
                  value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%tid] [%thread] %-5level  %logger{36}  -%msg%n"/>
        <property name="log.file.pattern"
                  value='{"appName": "${app.name}","time": "%date{yyyy-MM-dd HH:mm:ss.SSS}","level": "%level","traceId":"%tid","ip": "%ip","pid": "${PID:-}","thread": "%thread","class": "%logger","method": "%method","line": "%line","message": "%message","statck_trace":"%xEx"}'/>
    
    
        <!-- 控制台日志输出配置 -->
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
                <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
                    <pattern>${log.pattern}</pattern>
                </layout>
            </encoder>
        </appender>
        <!-- 配置异步记录 AsyncAppender 上报日志 -->
        <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
            <discardingThreshold>0</discardingThreshold>
            <queueSize>1024</queueSize>
            <neverBlock>true</neverBlock>
            <appender-ref ref="STDOUT"/>
        </appender>
        <!-- skywalking grpc 日志收集 8.4.0版本开始支持 -->
        <appender name="grpc-log" class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppender">
            <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
                <layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCPatternLogbackLayout">
                    <pattern>${log.pattern}</pattern>
                </layout>
            </encoder>
        </appender>
        <!-- 文件输出日志配置,按照每天生成日志文件 -->
        <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
                <!-- 日志文件输出的文件名称 -->
                <FileNamePattern>${log.path}-%d{yyyy-MM-dd}.%i.log</FileNamePattern>
                <!-- 日志保留天数 -->
                <MaxHistory>30</MaxHistory>
                <MaxFileSize>10MB</MaxFileSize>
            </rollingPolicy>
    
            <encoder charset="UTF-8" class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder"
                     layout="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout"
            >
                <providers>
                    <pattern>
                        <pattern>${log.file.pattern}</pattern>
                    </pattern>
                </providers>
            </encoder>
        </appender>
    
        <!-- mybatis日志配置 -->
        <!--    <logger name="java.sql.Connection" level="DEBUG"/>-->
        <!--    <logger name="java.sql.Statement" level="DEBUG"/>-->
        <!--    <logger name="java.sql.PreparedStatement" level="DEBUG"/>-->
    
        <!-- 配置开发环境,多个使用逗号隔开(例如:dev,sit) -->
        <springProfile name="dev">
            <!--定义日志输出级别-->
            <root level="INFO">
                <appender-ref ref="STDOUT"/>
                <appender-ref ref="file"/>
            </root>
        </springProfile>
    
        <!-- 配置测试环境,多个使用逗号隔开 -->
        <springProfile name="sit">
            <!--定义日志输出级别-->
            <root level="INFO">
                <appender-ref ref="ASYNC"/> 
                <appender-ref ref="STDOUT"/>
                <appender-ref ref="grpc-log"/>
                <appender-ref ref="file"/>
            </root>
        </springProfile>
    
        <!-- 配置生产环境,多个使用逗号隔开 -->
        <springProfile name="prod">
            <!--定义日志输出级别-->
            <root level="INFO">
                <appender-ref ref="ASYNC"/> 
                <appender-ref ref="STDOUT"/>
                <appender-ref ref="grpc-log"/>
                <appender-ref ref="file"/>
            </root>
        </springProfile>
    
    </configuration>
    
  4. application.yml

    spring:  
      profiles:
        active: prod
    logging:
      file:
        path: D:/elasticsearch/logs
      config: classpath:logback-spring.xml
    
  5. logstash.conf

    input {
    	file {
    		path => "D:/elasticsearch/logs/cloud-case-*/*.log" # 应用日志文件存放路径
    		start_position => "beginning"
    		codec => multiline {
    			pattern => "^(20)[0-9]{2}-[0-9]{2}-[0-9]{2}" # 正则表达式,匹配开头为 "年月日" 的为一条日志的开始
    			negate => true # true表示若pattern正则匹配失败,则执行合并;false表示若pattern正则匹配失败,则执行合并。默认为false
    			what => "previous" # previous表示跟前面的行合并;next表示跟后面的行合并
    		}
    	}
    }
    
    output {
    	elasticsearch {
    		hosts => ["127.0.0.1:9200"] # ES地址
    		#index => "-%{+YYYY.MM.dd}" # 索引
    	}
    	stdout {
    		codec => rubydebug
    	}
    }
    # 启动方式
    #./bin/logstash -f /etc/logstash/conf.d/message.conf
    # https://www.cnblogs.com/zbzSH/p/15727583.html
    

至此手动搭建了所有常见的springcloud 常用组件。

posted @ 2022-08-02 10:17  Mr*宇晨  阅读(125)  评论(0编辑  收藏  举报