admin管理员组

文章数量:1650793

1. 微服务的介绍

1.1 系统架构演变

        随着互联网的发展,网站应用的规模也在不断的扩大,进而导致系统架构也在不断的进行变化。   从互联网早起到现在,系统架构大体经历了下面几个过程: 单体应用架构--->垂直应用架构--->分布式架构--->SOA架构--->微服务架构,当然还有悄然兴起的Service Mesh(服务网格化)

        接下来我们就来了解一下每种系统架构是什么样子的, 以及各有什么优缺点。

1.1.1 单体应用架构

        互联网早期,一般的网站应用流量较小,只需一个应用,将所有功能代码都部署在一起就可以,这样可以减少开发、部署和维护的成本。

        比如说一个电商系统,里面会包含很多用户管理,商品管理,订单管理,物流管理等等很多模块,  我们会把它们做成一个web项目,然后部署到一台tomcat服务器上。

优点:

  1. 项目架构简单,小型项目的话,开发成本低。
  2. 项目部署在一个节点上,维护方便

缺点:

  1. 全部功能集成在一个工程中,对于大型项目来讲不易开发和维护[修改代码]。
  2. 项目模块之间紧密耦合,单点容错率低。
  3. 无法针对不同模块进行针对性优化和水平扩展

1.1.2 垂直应用架构

        随着访问量的逐渐增大,单一应用只能依靠增加节点来应对,但是这时候会发现并不是所有的模块  都会有比较大的访问量.

        还是以上面的电商为例子, 用户访问量的增加可能影响的只是用户和订单模块, 但是对消息模块的影响就比较小. 那么此时我们希望只多增加几个订单模块, 而不增加消息模块.  此时单体应用就做不到了, 垂直应用就应运而生了.

        所谓的垂直应用架构,就是将原来的一个应用拆成互不相干的几个应用,以提升效率。比如我们可  以将上面电商的单体应用拆分成:

        电商系统(用户管理 商品管理 订单管理)

        后台系统(用户管理 订单管理 客户管理)

        CMS系统(广告管理 营销管理)

        这样拆分完毕之后,一旦用户访问量变大,只需要增加电商系统的节点就可以了,而无需增加后台  和CMS的节点。建立三个工程。

 优点:

  1. 系统拆分实现了流量分担,解决了并发问题,可以针对不同模块进行优化和水平扩展
  2. 一个系统的问题不会影响到其他系统,提高容错率

缺点:

  1. 系统之间相互独立, 无法进行相互调用
  2. 系统之间相互独立, 会有重复的开发任务

1.1.3 分布式架构

        当垂直应用越来越多,重复的业务代码就会越来越多。这时候,我们就思考可不可以将重复的代码  抽取出来,做成统一的业务层作为独立的服务(service),然后由前端控制层(controller)调用不同的业务层服务呢?

        这就产生了新的分布式系统架构。它将把工程拆分成表现层controller和服务层service两个部分,服务层中包含业务  逻辑。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。

优点:

  1. 抽取公共的功能为服务层,提高代码复用性

缺点:

  1. 调用关系错综复杂,难以维护

1.1.4 SOA架构----阿里dubbo

        在分布式架构下,当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心集群进行实时管理。此时,用于资源调度和治理中心(SOA Service Oriented Architecture,面向服务的架构)是关键。

 优点:

  1. 使用注册中心解决了服务间调用关系的自动调节

缺点:

  1. 服务间会有依赖关系,一旦某个环节出错会影响较大( 服务雪崩 )
  2. 服务关系复杂,运维、测试部署困难。

 1.1.5 微服务架构

        微服务架构在某种程度上是面向服务的架构SOA继续发展的下一步,它更加强调服务的"彻底拆分"---->必须要springboot(独立的系统) 必须依赖于springboot技术。Springcloud如果没有springboot 那么springcloud也无法使用。 springboot可以独立使用。因为springboot里面内置了tomcat   独立运行。Java -jar xxx.jar

只适合大工程。移动互联网 电商项目 p2p 等,

系统管理--->系统微服务   入库管理 -----入库微服务 

 优点:

  1. 服务原子化拆分,独立打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展
  2. 微服务之间采用Restful等轻量级http协议相互调用

缺点:   小型项目----微服务架构不合适。仓库系统---微服务。

  1. 微服务系统开发的技术成本高《高》(容错、分布式事务等)

1.2 微服务架构介绍

        微服务架构, 简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。

1.2.1 微服务架构的常见问题。

针对每个问题使用不同的组件来讲解。

一旦采用微服务系统架构,就势必会遇到这样几个问题:

  1. 这么多小服务,如何管理他们?  
  2. 这么多小服务,他们之间如何通讯?调用
  3. 这么多小服务,客户端怎么访问他们?前端
  4. 这么多小服务,一旦出现问题了,应该如何自处理?
  5. 这么多小服务,一旦出现问题了,应该如何排错?

Springcloud微服务就是针对上面这些问题 提出的解决方案。

对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。---学习如何解决上面的问题==组件。

1.2.2 微服务架构的常见概念

1.2.2.1 服务治理

服务治理就是进行服务的自动化管理,其核心是服务的自动注册与发现。

服务注册:服务实例将自身服务信息注册到注册中心。

服务发现:服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。

服务剔除:服务注册中心将出问题的服务自动剔除到可用列表之外,使其不会被调用到。

 1.2.2.2 服务调用

在微服务架构中,通常存在多个服务之间的远程调用的需求。目前主流的远程调用技术有基于HTTP的RESTful接口以及基于TCP的RPC协议。

REST(Representational State Transfer)

这是一种HTTP调用的格式,更标准,更通用,无论哪种语言都支持http协议。 

RPC(Remote Promote Call)

Rpc  @Autowire Bservice  bservice.方法()

一种进程间通信方式。允许像调用本地服务一样调用远程服务。RPC框架的主要目标就是让远程服务调用更简单、透明。RPC框架负责屏蔽底层的传输方式、序列化方式和通信细节。开发人员在使用的时候只需要了解谁在什么位置提供了什么样的远程服务接口即可,并不需要关心底层通信细节和调用过程。

区别与联系

 1.2.2.3 服务网关

随着微服务的不断增多,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:

  1. 客户端需要调用不同的url地址,增加难度
  2. 在一定的场景下,存在跨域请求的问题
  3. 每个微服务都需要进行单独的身份认证

针对这些问题,API网关顺势而生。

API网关直面意思是将所有API调用统一接入到API网关层,由网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。

 1.2.2.4 服务容错

在微服务当中,一个请求经常会涉及到调用几个服务,如果其中某个服务不可用,没有做服务容错的话,极有可能会造成一连串的服务不可用,这就是雪崩效应

我们没法预防雪崩效应的发生,只能尽可能去做好容错。服务容错的三个核心思想是:

  1. 不被外界环境影响
  2. 不被上游请求压垮
  3. 不被下游响应拖垮

 1.2.2.5 链路追踪

随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要对一次请求涉及的多个服务链路进行日志记录,性能监控即链路追踪

1.2.3 微服务架构的常见解决方案

1.2.3.1 ServiceComb

Apache ServiceComb,前身是华为云的微服务引擎 CSE (Cloud Service Engine) 云服务,是全球首个Apache微服务顶级项目。它提供了一站式的微服务开源解决方案,致力于帮助企业、用户和开发者将企业应用轻松微服务化上云,并实现对微服务应用的高效运维管理.

1.2.3.2 SpringCloud springcloud 很多组件都是拿的是Netflix公司,这家公司这些组件停止维护和更新。)

Spring Cloud是一系列框架的集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。

Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。

已经停更了。Euruka(注册中),feign远程调用,hystrix 容错,zuul网关 

1.2.3.3 SpringCloud Alibaba(引入springcloud很多组件都更新了。) springcloud alibaba

        Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

1.3 SpringCloud Alibaba介绍

        Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

        依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

1.3.1 主要功能

服务限流降级:默认支持 WebServletWebFlux OpenFeignRestTemplateSpring Cloud

Gateway Zuul Dubbo RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修

改限流降级规则,还支持查看限流降级 Metrics 监控。

服务注册与发现:适配 Spring Cloud 服务注册与发现标准nacos,默认集成了 Ribbon 的支持。

分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。

消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。

分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。

阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任

何时间、任何地点存储和访问任意类型的数据。

分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。

同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有

Workerschedulerx-client)上执行。

阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建

客户触达通道。

1.3.2 组件

Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳

定性。

Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。

RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠

的消息发布与订阅服务。

DubboApache Dubbo™ 是一款高性能 Java RPC 框架。

Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。

Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心

产品。

Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提

供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和

访问任意类型的数据。

Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精

准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。

Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速

搭建客户触达通道。

理论好好读读。

微服务拆分的原则

        之前你维护的一体化架构就像是一个大的蜘蛛网,不同功能模块错综复杂地交织在一起,方法之间调用关系非常的复杂,导致你修复了一个 Bug 可能会引起另外多个 Bug,整体的维护成本非常高。同时,数据库较弱的扩展性也限制了服务的扩展能力

出于上述考虑,你要对架构做拆分。但拆分并不像听上去那么简单,这其实就是将整体工程重构甚至重写的过程。你需要将代码拆分到若干个子工程里面,再将这些子工程通过一些通信方式组装起来,这对架构是很大的调整,需要跨多个团队协作完成。

所以在开始拆分之前你需要明确几个拆分的原则,否则就会事倍功半甚至对整体项目产生不利的影响。

 原则一做到单一服务内部功能的高内聚和低耦合

也就是说每个服务只完成自己职责之内的任务,对于不是自己职责的功能交给其它服务来完成。说起来你可能觉得理所当然对这一点不屑一顾,但很多人在实际开发中,经常会出现一些问题。

比如,我之前的项目中有用户服务和内容服务,用户信息中有“是否为认证用户”字段。组内有个同事在内容服务里有这么一段逻辑:如果用户认证字段等于 1,代表是认证用户,那么就把内容权重提升。问题是判断用户是否为认证用户的逻辑应该内聚在用户服务内部,而不应该由内容服务判断,否则认证的逻辑一旦变更内容服务也需要一同跟着变更,这就不满足高内聚、低耦合的要求了。所幸我们在 Review 代码时及时发现了这个问题,并在服务上线之前修复了它。

原则二,你需要关注服务拆分的粒度,先粗略拆分再逐渐细化

在服务拆分的初期,你其实很难确定服务究竟要拆分成什么样。但是从“微服务”这几个字来看,服务的粒度貌似应该足够小,甚至有“一方法一服务”的说法。不过服务多了也会带来问题,像是服务个数的增加会增加运维的成本。再比如原本一次请求只需要调用进程内的多个方法,现在则需要跨网络调用多个 RPC 服务,在性能上肯定会有所下降。

所以我推荐的做法是:拆分初期可以把服务粒度拆得粗一些,后面随着团队对于业务和微服务理解的加深,再考虑把服务粒度细化。比如对于一个社区系统来说,你可以先把和用户关系相关的业务逻辑,都拆分到用户关系服务中,之后,再把比如黑名单的逻辑独立成黑名单服务。

原则三,拆分的过程,要尽量避免影响产品的日常功能迭代

也就是说,要一边做产品功能迭代,一边完成服务化拆分。

还是拿我之前维护的一个项目举例。我曾经在竞品对手快速发展的时期做了服务的拆分,拆分的方式是停掉所有业务开发全盘推翻重构,结果错失了产品发展的最佳机会,最终败给了竞争对手。

因此,我们的拆分只能在现有一体化系统的基础上不断剥离业务独立部署,剥离的顺序你可以参考以下几点:

    1. 优先剥离比较独立的边界服务(比如短信服务、地理位置服务),从非核心的服务出发减少拆分对现有业务的影响,也给团队一个练习、试错的机会;

    2. 当两个服务存在依赖关系时优先拆分被依赖的服务。比如内容服务依赖于用户服务获取用户的基本信息,那么如果先把内容服务拆分出来,内容服务就会依赖于一体化架构中的用户模块,这样还是无法保证内容服务的快速部署能力。

    所以正确的做法是理清服务之间的调用关系,比如内容服务会依赖用户服务获取用户信息,互动服务会依赖内容服务,所以要按照先用户服务再内容服务,最后互动服务的顺序来进行拆分。

原则四,服务接口的定义要具备可扩展性

服务拆分之后,由于服务是以独立进程的方式部署,所以服务之间通信就不再是进程内部的方法调用而是跨进程的网络通信了。在这种通信模型下服务接口的定义要具备可扩展性,否则在服务变更时会造成意想不到的错误。

在之前的项目中,某一个微服务的接口有三个参数,在一次业务需求开发中,组内的一个同学将这个接口的参数调整为了四个,接口被调用的地方也做了修改,结果上线这个服务后却不断报错,无奈只能回滚。

这是因为这个接口先上线后参数变更成了四个,但是调用方还未变更还是在调用三个参数的接口,那就肯定会报错了。所以服务接口的参数类型最好是封装类,这样如果增加参数就不必变更接口的签名,而只需要在类中添加字段就可以了。

2. 微服务环境搭建

我们本次是使用的电商项目中的商品微服务订单微服务为案例进行讲解。

2.1 案例准备

2.1.1 技术选型

maven3.5.0+

数据库:MySQL 5.7 以上

持久层: Mybatis-plus 《Mybatis  Mapper Mybatis-plus》

其他: SpringCloud Alibaba 技术栈  druid

2.1.2 模块设计 

springcloud-alibaba 父工程 ----jar的版本管理  公共jar的引入

shop-common 公共模块【实体类】 《实体类,公共依赖,工具类。》

shop-product 商品微服务 【端口: 8080~8089  搭建集群

shop-order 订单微服务 【端口: 8090~8099  搭建集群

2.1.3 微服务调用

在微服务架构中,最常见的场景就是微服务之间的相互调用。我们以电商系统中常见的用户下单

例来演示微服务的调用:客户向订单微服务发起一个下单的请求,在进行保存订单之前需要调用商品微服务查询商品的信息。

我们一般把服务的主动调用方称为服务消费者,把服务的被调用方称为服务提供者

 在这种场景下,订单微服务就是一个服务消费者, 商品微服务就是一个服务提供者。

2.2 创建父工程

创建一个springboot工程,然后在pom.xml文件中添加下面内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache/POM/4.0.0" xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache/POM/4.0.0 https://maven.apache/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <!--第一步这里把springboot的版本号改为2.3.12-->
        <version>2.3.12.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wzh</groupId>
    <artifactId>springcloud-wzh-parent</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <!--第二步如果你的工程为父工程那么它的打包方式为pom-->
    <packaging>pom</packaging>

    <!--定义版本号-->
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF- 8</project.reporting.outputEncoding>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.3.RELEASE</spring-cloud-alibaba.version>
    </properties>

    <!--dependencyManagement:它只负责jar的版本号管理,不负责jar的下载,交于子模块,子模块在使用时无需指定版本号
          :springboot springcloud  springcloudalibaba之间版本一定要匹配
    -->
    <dependencyManagement>
        <dependencies>
            <!--springcloud的版本管理-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--springcloudalibaba的版本管理-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

查看springcloud springboot  springcloudalibaba版本对应说明:

https://github/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E

版本对应:

 2.3 创建基础模块---公共模块

1.创建 springcloud-wzh-common 模块,在pom.xml中添加依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache/POM/4.0.0"
         xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-wzh-parent</artifactId>
        <groupId>com.wzh</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-wzh-common</artifactId>

    <properties>
        <mavenpiler.source>8</mavenpiler.source>
        <mavenpiler.target>8</mavenpiler.target>
        <mybatis-plus-version>3.5.1</mybatis-plus-version>
        <fastjson-version>1.2.56</fastjson-version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus-version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson-version}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>


</project>

2.创建实体类

package com.wzh.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

//商品
@Data
@TableName(value="shop_product")
public class Product {
    @TableId(type= IdType.AUTO)
    private Integer pid;
    private String pname;//商品名称
    private Double pprice;//商品价格
    private Integer stock;//库存
}




package com.wzh.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;


@Data
@TableName("shop_order")
public class Order {
    @TableId(type = IdType.AUTO)
    private Long oid; //订单id
    private Integer uid;//用户id
    private String username;//用户名
    private Integer pid;//商品id
    private String pname;//商品名称
    private Double pprice;//商品价格
    private Integer number;//购买数量
}

2.4 创建商品微服务

步骤:

1 创建模块 导入依赖

2 创建SpringBoot主启动类

3 加入配置文件

4 创建必要的接口和实现类(controller service dao)

 新建一个 springcloud-wzh-product 模块,然后进行下面操作

1.创建pox.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache/POM/4.0.0"
         xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-wzh-parent</artifactId>
        <groupId>com.wzh</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-wzh-product</artifactId>

    <properties>
        <mavenpiler.source>8</mavenpiler.source>
        <mavenpiler.target>8</mavenpiler.target>
    </properties>
    <dependencies>
        <!--引入公共模块-->
        <dependency>
            <groupId>com.wzh</groupId>
            <artifactId>springcloud-wzh-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

2. 创建工程的主类

package com.wzh;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh
 * @ClassName: ProductApplication
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 20:54
 * @Version: 1.0
 */
@SpringBootApplication
@MapperScan(basePackages = "com.wzh.mapper")
public class ProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class,args);
    }
}

3 创建配置文件application.properties

# 为了后期扩展方便商品微服务的端口设置为8080~8089之间
server.port=8081

#数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springcloud?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

#sql日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

4 创建ProductMapper接口

package com.wzh.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wzh.entity.Product;


public interface ProductMapper extends BaseMapper<Product> {
}

5 创建ProductService接口

package com.wzh.service;

import com.wzh.entity.Product;


public interface ProductService {
    public Product findById(Integer pid);
}

6 创建ProductServiceImpl实现类

package com.wzh.service.impl;

import com.wzh.entity.Product;
import com.wzh.mapper.ProductMapper;
import com.wzh.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.service.impl
 * @ClassName: ProductServiceImpl
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:03
 * @Version: 1.0
 */
@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    private ProductMapper productMapper;
    @Override
    public Product findById(Integer pid) {
        Product product = productMapper.selectById(pid);
        return product;
    }
}

7 创建ProductController类

package com.wzh.controller;

import com.alibaba.fastjson.JSON;
import com.wzh.entity.Product;
import com.wzh.mapper.ProductMapper;
import com.wzh.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.controller
 * @ClassName: ProductController
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:05
 * @Version: 1.0
 */
@RestController
@RequestMapping("product")
@Slf4j
public class ProductController {
    @Autowired
    private ProductService productService;
    
    @GetMapping("getById/{pid}")
    public Product getById(@PathVariable Integer pid){
        Product product = productService.findById(pid);
        log.info("查询到商品:"+ JSON.toJSONString(product));
        return  product;
    }
}

8 启动工程,等到数据库表创建完毕之后,加入测试数据

INSERT INTO shop_product VALUE(NULL,'小米','1000','5000'); 
INSERT INTO shop_product VALUE(NULL,'华为','2000','5000'); 
INSERT INTO shop_product VALUE(NULL,'苹果','3000','5000'); 
INSERT INTO shop_product VALUE(NULL,'OPPO','4000','5000');

9.通过浏览器访问服务

 2.5 创建订单微服务

1 创建一个名为 springcloud-wzh-order 的模块,并添加springboot依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache/POM/4.0.0"
         xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-wzh-parent</artifactId>
        <groupId>com.wzh</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>springcloud-wzh-order</artifactId>

    <properties>
        <mavenpiler.source>8</mavenpiler.source>
        <mavenpiler.target>8</mavenpiler.target>
    </properties>
    <dependencies>
        <!--引入公共模块-->
        <dependency>
            <groupId>com.wzh</groupId>
            <artifactId>springcloud-wzh-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
</project>

2 创建启动类

package com.wzh;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh
 * @ClassName: OrderApplication
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:13
 * @Version: 1.0
 */
@SpringBootApplication
@MapperScan(basePackages = "com.wzh.mapper")
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
    
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

3 创建配置文件application.properties

# 为了后期扩展方便订单微服务的端口设置为8090~8099之间
server.port=8091

#数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springcloud?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

#sql日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

4 创建OrderMapper接口

package com.wzh.mapper;


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wzh.entity.Order;


public interface OrderMapper extends BaseMapper<Order> {
}

5 创建OrderService接口

package com.wzh.service;

import com.wzh.entity.Order;
import com.wzh.vo.CommonResult;

public interface OrderService {
    public CommonResult save(Order order);
}

6 创建OrderServiceImpl实现类

package com.wzh.service.impl;

import com.wzh.entity.Order;
import com.wzh.mapper.OrderMapper;
import com.wzh.service.OrderService;
import com.wzh.vo.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    
    @Override
    public CommonResult save(Order order) {
        int insert = orderMapper.insert(order);
        return new CommonResult(2000,"下单成功",null);
    }
}

7 创建OrderController类

需要设置订单对象中商品的信息
商品操作都在商品微服务---订单微服务远程调用商品微服务即可拿到商品信息,远程调用:http协议的restFul风格调用适合微服务。基于TCP协议的RPC调用时和SOA分布式
一定采用的为http协议:
(1)自己写代码完成调用[httpclient]微信支付---适合调用第三方网址 
(2)spring提供了一个工具类RestTemplate,该类也是基于http协议完成的调用
package com.wzh.controller;

import com.alibaba.fastjson.JSON;
import com.wzh.entity.Order;
import com.wzh.entity.Product;
import com.wzh.service.OrderService;
import com.wzh.vo.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.controller
 * @ClassName: OrderController
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:23
 * @Version: 1.0
 */
@RestController
@RequestMapping("order")
@Slf4j
public class OrderController {
    @Autowired
    private OrderService orderService;
    //该类默认没有交于spring管理---->需要在主启动类中创建bean
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("buy/{pid}/{number}")
    public CommonResult buy(@PathVariable Integer pid,@PathVariable Integer number){
        log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
        
        Order order = new Order();
        //用户的信息可以根据token从redis中获取用户信息
        order.setUid(1);
        order.setUsername("赵六");

        //需要设置订单对象中商品的信息
        //商品操作都在商品微服务---订单微服务远程调用商品微服务即可拿到商品信息,远程调用:http协议的restFul风格调用适合微服务。基于TCP协议的RPC调用时和SOA分布式
        //一定采用的为http协议:(1)自己写代码完成调用[httpclient]微信支付---适合调用第三方网址 (2)spring提供了一个工具类RestTemplate,该类也是基于http协议完成的调用
        Product product = restTemplate.getForObject("http://localhost:8081/product/getById/" + pid, Product.class);
        log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));

        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(number);
        
        return orderService.save(order);
    }
}

8 启动工程并在浏览器上测试

3. Nacos Discovery--服务治理

3.1 服务治理介绍

先来思考一个问题

通过上一章的操作,我们已经可以实现微服务之间的调用。但是我们把服务提供者的网络地址

ip,端口)等硬编码到了代码中,这种做法存在许多问题:

  • 一旦服务提供者地址变化,就需要手工修改代码
  • 一旦是多个服务提供者,无法实现负载均衡功能
  • 一旦服务变得越来越多,人工维护调用关系困难

那么应该怎么解决呢, 这时候就需要通过注册中心动态的实现服务治理

什么是服务治理

服务治理是微服务架构中最核心最基本的模块。用于实现各个微服务的自动化注册与发现

服务注册:在服务治理框架中,都会构建一个注册中心,每个服务单元向注册中心登记自己提供服

务的详细信息。并在注册中心形成一张服务的清单,服务注册中心需要以心跳30s  90s的方式去监测清单中 的服务是否可用,如果不可用,需要在服务清单中剔除不可用的服务。

服务发现:服务调用方向服务注册中心咨询服务,并获取所有服务的实例清单,实现对具体服务实

例的访问。

通过上面的调用图会发现,除了微服务,还有一个组件是服务注册中心,它是微服务架构非常重要

的一个组件,在微服务架构里主要起到了协调者的一个作用。注册中心一般包含如下几个功能:

1. 服务发现:

服务注册:保存服务提供者和服务调用者的信息

服务订阅:服务调用者订阅服务提供者的信息,注册中心向订阅者推送提供者的信息

2. 服务配置:

配置订阅:服务提供者和服务调用者订阅微服务相关的配置

配置下发:主动将配置推送给服务提供者和服务调用者

3. 服务健康检测

检测服务提供者的健康情况,如果发现异常,执行服务剔除

常见的注册中心

Zookeeper

zookeeper是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式

应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用

配置项的管理等。

Eureka

EurekaSpringcloud Netflix中的重要组件,主要作用就是做服务注册和发现。但是现在已经

,停更不停用。

Consul

Consul是基于GO语言开发的开源工具,主要面向分布式,服务化的系统提供服务注册、服务发现

和配置管理的功能。Consul的功能都很实用,其中包括:服务注册/发现、健康检查、Key/Value

存储、多数据中心和分布式一致性保证等特性。Consul本身只是一个二进制的可执行文件,所以

安装和部署都非常简单,只需要从官网下载后,在执行对应的启动脚本即可。

Nacos (服务治理  配置中心)

Nacos是一个更易于构建云原生应用的动态服务发现配置管理服务管理平台。它是 Spring

Cloud Alibaba 组件之一,负责服务注册发现服务配置,可以这样认为nacos=eureka+config

3.2 nacos简介

Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速

实现动态服务发现、服务配置、服务元数据及流量管理。

从上面的介绍就可以看出,nacos的作用就是一个注册中心,用来管理注册上来的各个微服务。

3.3 nacos实战入门

nacos所有路径不要有中文

接下来,我们就在现有的环境中加入nacos,并将我们的两个微服务注册上去。

Releases · alibaba/nacos · GitHub

3.3.1 搭建nacos环境

1: 安装nacos

下载地址: https://github/alibaba/nacos/releases

下载zip格式的安装包,然后进行解压缩操作   

务必选用与springcloud springcloudalibaba对应的版本

2: 启动nacos

#切换目录

cd nacos/bin

windows环境下请下载nacos-server-2.0.3.zip(与父模块的springcloud版本匹配)包到本地后解压缩后zip包,在解压缩的nacos文件夹下的盘符目录中输入cmd后回车进入cmd命令控制台,然后输入如下命令启动nacos单机服务

#命令启动

startup.cmd -m standalone

3: 访问nacos

打开浏览器输入http://localhost:8848/nacos,即可访问服务, 默认密码是nacos/nacos

 3.3.2 将商品微服务注册到nacos

接下来开始修改 springcloud-wzh-product 模块的代码, 将其注册到nacos服务上

1. pom.xml中添加nacos的依赖

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

2. 在主启动类上添加nacos的开启注解

package com.wzh;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

 
@SpringBootApplication
@MapperScan(basePackages = "com.wzh.mapper")
@EnableDiscoveryClient  //开启nacos注解
public class ProductApp {
    public static void main(String[] args) {
        SpringApplication.run(ProductApp.class,args);
    }
}

3.在application.properties添加nacos的配置

#配置nacos注册中心的地址
spring.cloud.nacos.discovery.server-addr=localhost:8848
#指定是否把该服务注册到注册中心
#spring.cloud.nacos.discovery.register-enabled=false
#指定微服务的名称
spring.application.name=springcloud-wzh-product

4. 启动服务, 观察nacos的控制面板中是否有注册上来的商品微服务

 3.3.3 将订单微服务注册到nacos

接下来开始修改 springcloud_wzh_order 模块的代码, 将其注册到nacos服务上

1. pom.xml中添加nacos的依赖

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

2. 在主启动类上添加nacos的开启注解

package com.wzh;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@MapperScan(basePackages = "com.wzh.mapper")
@EnableDiscoveryClient  //开启nacos注解
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

3. application.properties中添加nacos服务的配置

#配置nacos注册中心的地址
spring.cloud.nacos.discovery.server-addr=localhost:8848
#指定是否把该服务注册到注册中心
#spring.cloud.nacos.discovery.register-enabled=false
#指定微服务的名称   切记不能用_分隔
spring.application.name=springcloud-wzh-order

4. 重新启动服务, 观察nacos的控制面板中是否有注册上来的订单微服务

 5 修改OrderController中的代码

package com.wzh.controller;

import com.alibaba.fastjson.JSON;
import com.wzh.entity.Order;
import com.wzh.entity.Product;
import com.wzh.service.OrderService;
import com.wzh.vo.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
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;

import java.util.List;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.controller
 * @ClassName: OrderController
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:23
 * @Version: 1.0
 */
@RestController
@RequestMapping("order")
@Slf4j
public class OrderController {
    @Autowired
    private OrderService orderService;
    //该类默认没有交于spring管理
    @Autowired
    private RestTemplate restTemplate;

    //spring中提供了一个工具类,该类可以从注册中心拉取微服务信息。
    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("buy/{pid}/{number}")
    public CommonResult buy(@PathVariable Integer pid, @PathVariable Integer number){
        log.info(">>客户下单,这时候要调用商品微服务查询商品信息");

        Order order = new Order();
        //用户的信息可以根据token从redis中获取用户信息
        order.setUid(1);
        order.setUsername("赵六");

        //根据服务名称获取对应的所有实例对象      这里要与product微服务的名称一致
        List<ServiceInstance> instances = discoveryClient.getInstances("springcloud-wzh-product");
        //获取第一个实例对象
        ServiceInstance instance = instances.get(0);
        String path = instance.getUri().toString();
        Product product = restTemplate.getForObject(path + "/product/getById/" + pid, Product.class);
        
        log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));

        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(number);

        return orderService.save(order);
    }
}

DiscoveryClient是专门负责服务注册和发现的,我们可以通过它获取到注册到注册中心的所有服

务。

6.启动服务, 观察nacos的控制面板中是否有注册上来的订单微服务,然后通过访问消费者服务验证调用是否成功

3.4 实现服务调用的负载均衡

3.4.1 什么是负载均衡 nginx

通俗的讲, 负载均衡就是将负载(工作任务,访问请求)进行分摊到多个操作单元(服务器,组件)上进行执行。

根据负载均衡发生位置的不同,一般分为服务端负载均衡客户端负载均衡

服务端负载均衡指的是发生在服务提供者一方,比如常见的nginx负载均衡

而客户端负载均衡指的是发生在服务请求的一方,也就是在发送请求之前已经选好了由哪个实例处理请求

 我们在微服务调用关系中一般会选择客户端负载均衡,也就是在服务调用的一方来决定服务由哪个提供者执行.

3.4.2 自定义实现负载均衡

1 通过idea再启动一个 springcloud-wzh-product 微服务,设置其端口为8082

 2 通过nacos查看微服务的启动情况

3 修改 springcloud-wzh-order 的代码,实现负载均衡 

package com.wzh.controller;

import com.alibaba.fastjson.JSON;
import com.wzh.entity.Order;
import com.wzh.entity.Product;
import com.wzh.service.OrderService;
import com.wzh.vo.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.A;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
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;

import java.util.List;
import java.util.Random;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.controller
 * @ClassName: OrderController
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:23
 * @Version: 1.0
 */
@RestController
@RequestMapping("order")
@Slf4j
public class OrderController01 {
    @Autowired
    private OrderService orderService;
    //该类默认没有交于spring管理
    @Autowired
    private RestTemplate restTemplate;

    //spring中提供了一个工具类,该类可以从注册中心拉取微服务信息。
    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("buy/{pid}/{number}")
    public CommonResult buy(@PathVariable Integer pid, @PathVariable Integer number){
        log.info(">>客户下单,这时候要调用商品微服务查询商品信息");

        Order order = new Order();
        //用户的信息可以根据token从redis中获取用户信息
        order.setUid(1);
        order.setUsername("赵六");

        //根据服务名称获取对应的所有实例对象      这里要与product微服务的名称一致
        List<ServiceInstance> instances = discoveryClient.getInstances("springcloud-wzh-product");
        //获取第一个实例对象
        int index = new Random().nextInt(instances.size());
        ServiceInstance instance = instances.get(index);
        log.info("从nacos获取springcloud-wzh-product服务的ip地址以及端口号{}",instance.getUri());
        
        String path = instance.getUri().toString();
        Product product = restTemplate.getForObject(path + "/product/getById/" + pid, Product.class);

        log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));

        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(number);

        return orderService.save(order);
    }
}

4. 启动两个服务提供者和一个服务消费者,多访问几次消费者测试效果

3.4.3 基于Ribbon实现负载均衡 ---组件

1.什么是Ribbon

        是 Netflix 发布的一个负载均衡器,有助于控制 HTTP TCP客户端行为。在 SpringCloud 中, nacos一般配合Ribbon进行使用,Ribbon提供了客户端负载均衡的功能,Ribbon利用从nacos中读 取到的服务信息,在调用服务节点提供的服务时,会合理(策略)的进行负载 SpringCloud中可以将注册中心和Ribbon配合使用,Ribbon自动的从注册中心中获取服务提供者的 列表信息,并基于内置的负载均衡算法,请求服务。

        是 Netflix 发布的一个负载均衡器,Ribbon自动的从注册中心中获取服务提供者的 列表信息,并基于内置的负载均衡算法,请求服务。

2.Ribbon的主要作用

1)服务调用

基于Ribbon实现服务调用, 是通过拉取到的所有服务列表组成(服务名-请求路径的)映射关系。借助 RestTemplate 最终进行调用

2)负载均衡

当有多个服务提供者时,Ribbon可以根据负载均衡的算法自动的选择需要调用的服务地址

Ribbon是Spring Cloud的一个组件, 它可以让我们使用一个注解就能轻松的搞定负载均衡,

nacos依赖中包含了Ribbon,所以不需要再导包

1步:在RestTemplate 的生成方法上添加@LoadBalanced注解

package com.wzh;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@MapperScan(basePackages = "com.wzh.mapper")
@EnableDiscoveryClient  //开启nacos注解
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
    @Bean
    @LoadBalanced   //ribbon借助RestTemplate完成负载均衡的调用  默认策略轮询
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

2步:修改OrderController服务调用的方法

package com.wzh.controller;

import com.alibaba.fastjson.JSON;
import com.wzh.entity.Order;
import com.wzh.entity.Product;
import com.wzh.service.OrderService;
import com.wzh.vo.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
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;

import java.util.List;
import java.util.Random;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.controller
 * @ClassName: OrderController
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:23
 * @Version: 1.0
 */
@RestController
@RequestMapping("order")
@Slf4j
public class OrderController02 {
    @Autowired
    private OrderService orderService;
    //该类默认没有交于spring管理
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("buy/{pid}/{number}")
    public CommonResult buy(@PathVariable Integer pid, @PathVariable Integer number){
        log.info(">>客户下单,这时候要调用商品微服务查询商品信息");

        Order order = new Order();
        //用户的信息可以根据token从redis中获取用户信息
        order.setUid(1);
        order.setUsername("赵六");
        //1.在RestTemplate获取方法上@LoadBalanced注解 使用Ribbon完成负载均衡
        //2.修改RestTemplate调用代码 http://服务名称/资源路径
        //restTemplate调用处必须 http://服务提供者的名称/服务提供资源路径
        Product product = restTemplate.getForObject("http://springcloud-wzh-product/product/getById/" + pid, Product.class);

        log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));

        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(number);

        return orderService.save(order);
    }
}

Ribbon支持的负载均衡策略

Ribbon内置了多种负载均衡策略,内部负载均衡的顶级接口为

comflix.loadbalancer.IRule , 具体的负载策略如下图所示:

 我们可以通过修改application.properties配置来调整Ribbon的负载均衡策略,具体代码如下

#指定ribbon负载均衡的策略: springcloud-wzh-product微服务名称.ribbon.NFLoadBalancerRuleClassName=负载均衡策略类
springcloud-wzh-product.ribbon.NFLoadBalancerRuleClassName=comflix.loadbalancer.RandomRule

思考:上面使用ribbon完成负载均衡有什么缺点:  restTemplate---url地址。

//1.代码可读性较差. 

//2.编码风格不一样. 
习惯的编码风格service 调用dao  service中注入dao,dao对象调用相应的方法

3.5 基于OpenFeign实现服务调用

3.5.1 什么是OpenFeign  

        OpenFeign是Spring Cloud提供的一个声明式的伪Http客户端, 它使得调用远程服务就像调用本地服务一样简单, 只需要创建一个接口并添加一个注解即可。

        Nacos很好的兼容了Feign Feign负载均衡默认集成了 Ribbon, 所以在Nacos下使用Fegin默认就实现了负载均衡的效果。

3.5.2 OpenFeign的使用

1 加入OpenFegin的依赖

        <!--openfeign的jar文件-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

2 在主启动类上加入开启openfeign的注解

package com.wzh;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@MapperScan(basePackages = "com.wzh.mapper")
@EnableDiscoveryClient  //开启nacos注解
@EnableFeignClients //开启openfeign的注解
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
    @Bean
    @LoadBalanced   //ribbon借助RestTemplate完成负载均衡的调用  默认策略轮询
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

3 创建feign的接口

直接从ProductController复制过来,别忘了路径上加/product

package com.wzh.feign;

import com.wzh.entity.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;


// 被调用的微服务名称 springcloud-wzh-product
// openfeign--->生成代理实现类---->url:http://springcloud-wzh-product/product/getById/{pid}
@FeignClient(value = "springcloud-wzh-product")
public interface ProductFeign {

    //调用微服务接口方法:必须和微服务提供者的方法参数必须一致
    @GetMapping("/product/getById/{pid}")
    public Product getById(@PathVariable Integer pid);

}

4 修改OrderController的代码

package com.wzh.controller;

import com.alibaba.fastjson.JSON;
import com.wzh.entity.Order;
import com.wzh.entity.Product;
import com.wzh.feign.ProductFeign;
import com.wzh.service.OrderService;
import com.wzh.vo.CommonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.controller
 * @ClassName: OrderController
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:23
 * @Version: 1.0
 */
@RestController
@RequestMapping("order")
@Slf4j
public class OrderController03 {
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private ProductFeign productFeign;

    @GetMapping("buy/{pid}/{number}")
    public CommonResult buy(@PathVariable Integer pid, @PathVariable Integer number){
        log.info(">>客户下单,这时候要调用商品微服务查询商品信息");

        Order order = new Order();
        //用户的信息可以根据token从redis中获取用户信息
        order.setUid(1);
        order.setUsername("赵六");
        log.info("openfeign的代理类生成的类:{}"+productFeign);
        Product product = productFeign.getById(pid);
        //这里直接使用productFeign调用接口中的方法,和我们原来controller调用service一样啊
        log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));

        order.setPid(pid);
        order.setPname(product.getPname());
        order.setPprice(product.getPprice());
        order.setNumber(number);

        return orderService.save(order);
    }
}

5 重启order微服务,查看效果

3.6. 使用eureka作为注册中心

3.6.1 什么是eureka

Eureka是Netflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflix中,实现SpringCloud的服务发现功能。它没有客户端,需要我们自己创建。

1.创建一个eureka服务端

它可以是一个独立的springboot项目,也可以在其他服务器上,这里我们为了方便就部署到这个springcloud项目中了

 2. 在eureka服务端引入相关的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache/POM/4.0.0"
         xmlns:xsi="http://www.w3/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache/POM/4.0.0 http://maven.apache/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-wzh-parent</artifactId>
        <groupId>com.wzh</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-server</artifactId>

    <properties>
        <mavenpiler.source>8</mavenpiler.source>
        <mavenpiler.target>8</mavenpiler.target>
    </properties>

    <dependencies>
        <!--eureka-server-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</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-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>-->
    </dependencies>


</project>

3.修改application.properties配置文件

server.port=7000

spring.application.name=eureka-server

#实例的主机名  所在的主机ip
eureka.instance.hostname=localhost

#是否把eureka服务端注册到注册中心
eureka.client.register-with-eureka=false

#该服务是否从注册中心拉取服务
eureka.client.fetch-registry=false

#提供类微服务的地址
eureka.client.service-url.defaultZone=http://localhost:7000/eureka


4. 添加主启动类

package com.wzh;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloudflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer//开启eureka服务端注解
public class EurekaApp {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApp.class,args);
    }
}

5. 修改springcloud-wzh-product和springcloud-wzh-order客户端配置文件

这里先把之前的nacos依赖注释掉,引入eureka依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

6. 分别修改application.properties配置文件,注释掉nacos的地址,添加eureka的地址

product的配置文件:

# 为了后期扩展方便商品微服务的端口设置为8080~8089之间
server.port=8081

#数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springcloud?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

#sql日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#配置eureka注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:7000/eureka
#配置nacos注册中心的地址
#spring.cloud.nacos.discovery.server-addr=localhost:8848
#指定是否把该服务注册到注册中心
#spring.cloud.nacos.discovery.register-enabled=false
#指定微服务的名称   务必不能用_分隔
spring.application.name=springcloud-wzh-product

order的配置文件:

# 为了后期扩展方便订单微服务的端口设置为8090~8099之间
server.port=8091

#数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springcloud?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

#sql日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

#配置eureka注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:7000/eureka
#配置nacos注册中心的地址
#spring.cloud.nacos.discovery.server-addr=localhost:8848
#指定是否把该服务注册到注册中心
#spring.cloud.nacos.discovery.register-enabled=false
#指定微服务的名称   务必不能用_分隔
spring.application.name=springcloud-wzh-order

#指定ribbon负载均衡的策略: springcloud-wzh-product微服务名称.ribbon.NFLoadBalancerRuleClassName=负载均衡策略类
springcloud-wzh-product.ribbon.NFLoadBalancerRuleClassName=comflix.loadbalancer.RandomRule

7. 重新启动所有微服务,测试是否正常使用

 

一定要启动eureka服务端,如果报500,可以看看。

3.7 nacos搭建集群

在实际开发过程中,如果使用 Nacos 的话,为了确保高可用,我们一般都 会对其进行集群的部署。Nacos 规定集群中 Nacos 节点的数量需要 大于等 3 ;同时,单机模式下 Nacos 的数据默认保存在其内嵌数据库中 deby ,不方便观察数据存储的基本情况。而且如果集群中启动多个默认配置下的 Nacos 节点,数据存储是存在一致性问题的。为了解决这个问题, Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL 的存储;此外,我们还需要借助Nginx 实现负载均衡。这一过程的部署架构图如下所示:

 准备条件:

1. 准备三个 nacos 服务器(复制三份) --- 为了操作方便统一部署到同一台电脑  8860.8870.8880

2. 准备 mysql--- 创建一个 nacos-mysql-- 创建相应的表

3.7.1 创建数据库

1. 复制sql到nacos_mysql数据库中,完成创建表

 3.7.2 指定使用mysql作为数据存储

修改nacos8860\nacos-server-2.0.3\nacos\conf的application.properties配置文件

3.7.3 配置集群文件

修改nacos8860\nacos-server-2.0.3\nacos\conf下的 cluster.conf.example文件,并把后缀名去掉

3.7.4 复制三份nacos目录

如果有正在运行的nacos服务,先关闭所有nacos服务 并修改每一个nacos\conf下的application.properties中的对应端口号

3.7.5 启动三个nacos服务

双击打开bin目录下的startup.cmd

 

如果某一个启动失败请参考 

Nacos2.0.1数据持久化和伪集群学习--集群启动报错问题(Error creating bean with name ‘grpcSdkServer‘)_hy_luckyyang的博客-CSDN博客 如果全部启动失败请参考 nacos连接数据库,数据库必须要设置远程连接
Mysql设置远程连接 - Mr_伍先生 - 博客园 (cnblogs)

nacos集群全部启动失败报 Failed to obtain JDBC Connection; nested exception is java.sql.SQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up. 

打开navicat连接数据库再启动nacos集群即可

访问其中一个nacos服务器查看是否搭建成功 我这里访问的是 http://localhost:8860/nacos

3.7.6 搭建nginx代理上面三个nacos服务

我这里为了方便都在window下操作。 1.修改conf目录下的nginx.conf配置文件

2.启动nginx

 任务管理器可以查看是否开启成功

 3. 访问http://localhost:81/nacos查看是否代理成功

3.7.7 微服务注册到nacos集群上

1.修改微服务中的application.properties配置文件,把nacos注册中心的地址改为nginx所在ip和端口

2.重启微服务项目,查看是否成功

这里一定不要去访问nginx的ip+port+资源路径,因为nacos是服务注册中心,不要误解了,这里我卡了半天才反应过来。 

如果出现这个问题,出现微服务注册不到nacos上报错

需要将nacos中配置的ip地址换为我们真实的ip地址  或者关闭虚拟机的虚拟网卡

4. Gateway--服务网关

4.1 网关简介

        大家都都知道在微服务架构中,一个系统会被拆分为很多个微服务。那么作为客户端(pc androud ios 平板)要如何去调用这么多的微服务呢?如果没有网关的存在,我们只能在客户端记录每个微服务的地址,然后分别去调用。 axios.get(ip:port/url)   axios.get(ip:port/url)

这样的架构,会存在着诸多的问题:

  1. 客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
  2. 认证复杂,每个服务都需要独立认证。
  3. 存在跨域请求,在一定场景下处理相对复杂。

(跨域: 浏览器的ajax从一个地址访问另一个地址:

   协议://ip:port  如果三则有一个不同,则会出现跨域问题。

    http://192.168.10.11:8080 ----->https://192.168.10.11:8080

    http://127.0.0.1:8080--->http://localhost:8080  跨域

上面的这些问题可以借助API网关来解决。

所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服 务,一些与业务本身功能无关的公共逻辑可以在这里实现,诸如认证、鉴权、监控(黑白名单)、路由转发等等。 添加上API网关之后,系统的架构图变成了如下所示:

在业界比较流行的网关,有下面这些:

1.Ngnix+lua

使用nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用

lua是一种脚本语言,可以来编写一些简单的逻辑, nginx支持lua脚本

2.Kong

基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。 问题:

只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式。

3.Zuul 1.0(慢 servlet 2.0 ) zuul2.0 没出来。

Netflix开源的网关,功能丰富,使用JAVA开发,易于二次开发 问题:缺乏管控,无法动态配

置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx

4.Spring Cloud Gateway

Spring公司为了替换Zuul而开发的网关服务,将在下面具体介绍。

注意:SpringCloud alibaba技术栈中并没有提供自己的网关,我们可以采用Spring Cloud Gateway来做网关

4.2 Gateway 简介

Spring Cloud GatewaySpring公司基于Spring 5.0Spring Boot 2.0 Project Reactor 等术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。它的目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 的方式提供了网关基本的功能,例如:安全,监控和限流。

优点:

  1. 性能强劲:是第一代网关Zuul1.6
  2. 功能强大:内置了很多实用的功能,例如转发、监控、限流等
  3. 设计优雅,容易扩展.

缺点:  

  1. 其实现依赖NettyWebFlux,不是传统的Servlet编程模型,学习成本高
  2. 不能将其部署在TomcatJettyServlet容器里,只能打成jar包执行 web.Jar
  3. 需要Spring Boot 2.0及以上的版本,才支持

gateway内置了服务器 netty服务器。千万不要再使用tomcat作为服务器。

4.3 Gateway快速入门---如何使用gateway网关

要求: 通过浏览器访问api网关,然后通过网关将请求转发到商品微服务,订单微服务

1 创建一个api-gateway的微服务工程并加入依赖

    <dependencies>
        <!--只需要添加该依赖,不要使用spring-boot-starter- web依赖。
        因为gateway内置了服务器netty.而web的jar内置 tomcat
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
    </dependencies>

2 创建application.yml配置文件

# gateway服务网关端口
server:
  port: 7000

#指定微服务名称
spring:
  application:
    name: api-gateway

# 代理的springcloud-wzh-product微服务
# 路由转发  List<RouteDefinition> routes
  cloud:
    gateway:
      routes:
        - id: springcloud-wzh-product  # 路由的唯一标识,只要不重复都可以,如果不写默认会通过UUID产生,一般写成被路由的服务名称
          uri: http://localhost:8081 #路由转发的真实地址   被路由的地址 这里是product微服务的ip
          order: 1                  #表示优先级  数字越小优先级越高
          # predicates:断言: 执行路由的判断条件   当满足断言时,才会转发到真实的uri地址
          predicates:
            - Path=/product/**
        #  filters:   # 过滤器: 可以在请求前或请求后作一些手脚
         #   - StripPrefix=1


        # 代理的springcloud-wzh-product微服务
  # 路由转发  List<RouteDefinition> routes
        - id: springcloud-wzh-order  # 不写则随机生成 必须唯一
          uri: http://localhost:8091 #路由转发的真实地址
          # predicates:当满足断言时,才会转发到真实的uri地 址
          predicates:
            - Path=/order/**

3 创建主启动类

package com.wzh;

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

import javax.swing.*;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh
 * @ClassName: GatewayApplication
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/21 11:26
 * @Version: 1.0
 */
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class,args);
    }
}

4 启动项目,并通过网关去访问微服务

发现 : 路由配置时 uri 的地址还是一个固定的网络地址,如果后期微服务地址发送改变,也需要修改网关的配置。 不能完成负载均衡。 解决: 我们知道, gateway 他也是一个微服务,它可以从注册中心拉取服务信息。

 4.4  让gateway网关从注册中心拉取服务信息--增强版

        现在在配置文件中写死了转发路径的地址, 前面我们已经分析过地址写死带来的问题, 接下来我们从注册中心获取此地址

1 gateway微服务加入nacos依赖

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

2 在启动类上加入服务发现的注解

package com.wzh;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import javax.swing.*;


@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class,args);
    }
}

3 修改application.yml配置文件,指定注册中心的地址

# gateway服务网关端口
server:
  port: 7000

#指定微服务名称
spring:
  application:
    name: api-gateway

# 代理的springcloud-wzh-product微服务
# 路由转发  List<RouteDefinition> routes
  cloud:
    gateway:
      routes:
        - id: springcloud-wzh-product  # 路由的唯一标识,只要不重复都可以,如果不写默认会通过UUID产生,一般写成被路由的服务名称
          uri: lb://springcloud-wzh-product #路由转发的真实地址   被路由的地址 这里是product微服务的ip
          order: 1                  #表示优先级  数字越小优先级越高
          # predicates:断言: 执行路由的判断条件   当满足断言时,才会转发到真实的uri地址
          predicates:
            - Path=/product/**
        #  filters:   # 过滤器: 可以在请求前或请求后作一些手脚
         #   - StripPrefix=1


        # 代理的springcloud-wzh-product微服务
  # 路由转发  List<RouteDefinition> routes
        - id: springcloud-wzh-order  # 不写则随机生成 必须唯一
          uri: lb://springcloud-wzh-order #路由转发的真实地址
          # predicates:当满足断言时,才会转发到真实的uri地 址
          predicates:
            - Path=/order/**
    # 指定注册中心的地址
    nacos:
      discovery:
        server-addr: localhost:81
        register-enabled: false   #不注册到注册中心上

 这里如果报failed to req API:/nacos/v1/ns/instance after all servers([localhost:8848]) tried: java.ConnectException: Connection refused: connect

首先看看顺序和我的是否一致,他有顺序要求

 4 重启微服务,测试是否成功

 这里只是实现了不用修改微服务的IP地址和负载均衡。

发现 : 有一个微服务需要配置一个路由,如果这时增加一个新的微服务,则需要在配置文件中增加一个新的路由配置。 能不能自动路由转发微服务呢 !====

4.5 gateway自动路由---简写版

使用自动路由就不能设置断言和过滤器等

1 修改applciation.yml配置文件,去掉关于路由的配置,添加新的定位发现功能

# gateway服务网关端口
server:
  port: 7000

#指定微服务名称
spring:
  application:
    name: api-gateway


# 是否开启gateway的定位发现功能
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
# 指定注册中心的地址
    nacos:
      server-addr: localhost:81   # nacos.discovery.server-addr一样的效果

2 重新启动项目,并通过网关去访问微服务

访问时 :       http://网关的 ip: 网关的 port/ 微服务名称 / 资源路径

4.6 Gateway核心架构

4.6.1  基本概念

路由(Route) gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个信息:

  1. id,路由标识符,区别于其他 Route。默认生成一个
  2. uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
  3. order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
  4. predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。
  5. filter,过滤器用于修改请求响应信息

4.6.2 执行流程

执行流程大体如下:

1. Gateway ClientGateway Server发送请求

2. 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文

3. 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给 RoutePredicateHandlerMapping

4. RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用

5. 如果过断言成功,由FilteringWebHandler创建过滤器链并调用

6. 请求会一次经过PreFilter--微服务--PostFilter的方法,最终返回响应

4.7 介绍Gateway断言

Predicate(断言, 谓词) 用于进行条件判断,只有断言都返回真,才会真正的执行路由。

断言就是说: 什么条件下 才能进行路由转发

4.7.1 内置路由断言工厂

SpringCloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配体如下:

  • 基于Datetime类型的断言工厂

此类型的断言根据时间做判断,主要有三个:

AfterRoutePredicateFactory: 接收一个日期参数,判断请求日期是否晚于指定日期

BeforeRoutePredicateFactory: 接收一个日期参数,判断请求日期是否早于指定日期

BetweenRoutePredicateFactory: 接收两个日期参数,判断请求日期是否在指定时间段内

-After=2019-12-31T23:59:59.789+08:00[Asia/Shanghai]

  • 基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory

接收一个IP地址段,判断请求主机地址是否在地址段中

-RemoteAddr=192.168.1.1/24

  • 基于Cookie的断言工厂

CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。 判断请求

cookie是否具有给定名称且值与正则表达式匹配。

-Cookie=chocolate, ch.

  • 基于Header的断言工厂

HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。 判断请求Header是否

具有给定名称且值与正则表达式匹配。 key value

-Header=X-Request-Id, \d+

  • 基于Host的断言工厂

HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的Host是否满足匹配规则。

-Host=**.testhost  

  • 基于Method请求方法的断言工厂

MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。

-Method=GET

  • 基于Path请求路径的断言工厂

PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。

-Path=/foo/{segment}基于Query请求参数的断言工厂

QueryRoutePredicateFactory :接收两个参数,请求param和正则表达式, 判断请求参数是否具

有给定名称且值与正则表达式匹配。

-Query=baz, ba.   

  • 基于路由权重的断言工厂

WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发

routes:

-id: weight_route1 uri: host1 predicates:

-Path=/product/**

-Weight=group3, 1

-id: weight_route2 uri: host2 predicates:

-Path=/product/**

-Weight= group3, 9

  • 内置路由断言工厂的使用

接下来我们验证几个内置断言的使用:

# gateway服务网关端口
server:
  port: 7000

#指定微服务名称
spring:
  application:
    name: api-gateway

# 代理的springcloud-wzh-product微服务
# 路由转发  List<RouteDefinition> routes
  cloud:
    gateway:
      routes:
        - id: springcloud-wzh-product  # 路由的唯一标识,只要不重复都可以,如果不写默认会通过UUID产生,一般写成被路由的服务名称
          uri: lb://springcloud-wzh-product #路由转发的真实地址   被路由的地址 这里是product微服务的ip
          order: 1                  #表示优先级  数字越小优先级越高
          # predicates:断言: 执行路由的判断条件   当满足断言时,才会转发到真实的uri地址
          predicates:
            - Path=/product/**
            - Method=GET #请求方式必须是GET
            - RemoteAddr=192.168.31.121 # 判断请求主机地址是否在地址段中
        #  filters:   # 过滤器: 可以在请求前或请求后作一些手脚
         #   - StripPrefix=1


        # 代理的springcloud-wzh-product微服务
# 路由转发  List<RouteDefinition> routes
        - id: springcloud-wzh-order  # 不写则随机生成 必须唯一
          uri: lb://springcloud-wzh-order #路由转发的真实地址
          # predicates:当满足断言时,才会转发到真实的uri地 址
          predicates:
            - Path=/order/**
# 指定注册中心的地址
    nacos:
      discovery:
        server-addr: localhost:81
        register-enabled: false   #不注册到注册中心上

 只有31.121能访问

如果上面内置的断言不能满足你的需求:可以自定义断言。 注意: 名称必须为XXXRoutePredicateFactory并且继承 AbstractRoutePredicateFactory

使用时 XXX=

4.7.2 自定义路由断言工厂

我们来设定一个场景: 假设我们的应用仅仅让age(min,max)之间的人来访问。

1 在配置文件中,添加一个Age的断言配置

自定义一个断言工厂, 实现断言方法

package com.wzh.config;


import org.apachemons.lang.StringUtils;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
//泛型为自定义的一个配置类,该配置类用来存放配置文件中的值
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
    public AgeRoutePredicateFactory() {
        super(AgeRoutePredicateFactory.Config.class);
    }
    //读取配置文件中的内容并配置给配置类中的属性
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("minAge","maxAge");
    }

    public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
        return (exchange) -> {
            String age = exchange.getRequest().getQueryParams().getFirst("age");
            if(StringUtils.isNotEmpty(age)){
                int a = Integer.parseInt(age);
                return a>=config.minAge && a<=config.maxAge;
            }
            return true;
        };
    }

    public static class Config {
        private Integer minAge;
        private Integer maxAge;

        public Config(Integer minAge, Integer maxAge) {
            this.minAge = minAge;
            this.maxAge = maxAge;
        }

        public Config() {
        }

        public Integer getMinAge() {
            return minAge;
        }

        public void setMinAge(Integer minAge) {
            this.minAge = minAge;
        }

        public Integer getMaxAge() {
            return maxAge;
        }

        public void setMaxAge(Integer maxAge) {
            this.maxAge = maxAge;
        }
    }
}

3 启动测试

#测试发现当age在(18,60)可以访问,其它范围不能访问

localhost:7000/product/getById/1?age=17

localhost:7000/product/getById/1?age=30

4.8 过滤器

三个知识点:

1 作用: 过滤器就是在请求的传递过程中,对请求和响应做一些手脚

2 生命周期: Pre Post

3 分类: 局部过滤器(作用在某一个路由上) 全局过滤器(作用全部路由上)

Gateway, Filter的生命周期只有两个:“pre” “post”

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

Gateway Filter从作用范围可分为两种: GatewayFilterGlobalFilter

  • GatewayFilter:应用到单个路由或者一个分组的路由上。
  • GlobalFilter:应用到所有的路由上。

4.8.1 局部过滤器

局部过滤器是针对单个路由的过滤器。

  1. 1.内置局部过滤器
  2. SpringCloud Gateway中内置了很多不同类型的网关路由过滤器。

    Spring Cloud Gateway 内置的过滤器工厂 - 小菜鸟攻城狮 - 博客园

 内置局部过滤器的使用

# gateway服务网关端口
server:
  port: 7000

#指定微服务名称
spring:
  application:
    name: api-gateway

# 代理的springcloud-wzh-product微服务
# 路由转发  List<RouteDefinition> routes
  cloud:
    gateway:
      routes:
        - id: springcloud-wzh-product  # 路由的唯一标识,只要不重复都可以,如果不写默认会通过UUID产生,一般写成被路由的服务名称
          uri: lb://springcloud-wzh-product #路由转发的真实地址   被路由的地址 这里是product微服务的ip
          order: 1                  #表示优先级  数字越小优先级越高
          # predicates:断言: 执行路由的判断条件   当满足断言时,才会转发到真实的uri地址
          predicates:
            - Path=/product/**
            - Method=GET #请求方式必须是GET
            #- RemoteAddr=192.168.31.121 # 判断请求主机地址是否在地址段中
            - Age=18,60
          filters:   # 过滤器: 可以在请求前或请求后作一些手脚
            - SetStatus=2000    #设置返回状态码2000  原来是200


        # 代理的springcloud-wzh-product微服务
# 路由转发  List<RouteDefinition> routes
        - id: springcloud-wzh-order  # 不写则随机生成 必须唯一
          uri: lb://springcloud-wzh-order #路由转发的真实地址
          # predicates:当满足断言时,才会转发到真实的uri地 址
          predicates:
            - Path=/order/**
# 指定注册中心的地址
    nacos:
      discovery:
        server-addr: localhost:81
        register-enabled: false   #不注册到注册中心上

2 重启服务,测试 

  1. 2.自定义局部过滤器

1 在配置文件中,添加一个Log的过滤器配置

自定义一个过滤器工厂,实现方法

package com.wzh.config;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory;
import org.springframework.cloud.gateway.support.HttpStatusHolder;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;
@Component
public class LogGatewayFilterFactory  extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {

    public LogGatewayFilterFactory() {
        super(LogGatewayFilterFactory.Config.class);
    }

    public List<String> shortcutFieldOrder() {
        return Arrays.asList("consoleLog","cacheLog");
    }


    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                if(config.cacheLog){
                    System.out.println("开启缓存日志");
                }
                if(config.consoleLog){
                    System.out.println("开启控制台日志");
                }
                return chain.filter(exchange);
            }
        };
    }

    public static class Config {
        private Boolean consoleLog;
        private Boolean cacheLog;

        public Config() {
        }

        public Boolean getConsoleLog() {
            return consoleLog;
        }

        public void setConsoleLog(Boolean consoleLog) {
            this.consoleLog = consoleLog;
        }

        public Boolean getCacheLog() {
            return cacheLog;
        }

        public void setCacheLog(Boolean cacheLog) {
            this.cacheLog = cacheLog;
        }
    }
}

3 启动测试

4.8.2 全局过滤器

全局过滤器作用于所有路由, 无需配置。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。

作用: 认证校验 黑白名单 敏感词

1.内置全局过滤器

SpringCloud Gateway内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:

2.自定义全局过滤器

内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现的,那么我们一起通过代码的形式自定义一个过滤器,去完成统一的权限校验。

开发中的鉴权逻辑:

  • 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  • 认证通过,将用户信息进行加密形成token,返回给客户端aaaa,作为登录凭证
  • 以后每次请求,客户端都携带认证的token
  • 服务端对token进行解密,判断是否有效。

作用: 认证校验 黑白名单 敏感词

如上图,对于验证用户是否已经登录鉴权的过程可以在网关统一检验。

检验的标准就是请求中是否携带token凭证以及token的正确性。

下面的我们自定义一个GlobalFilter,去校验所有请求的请求参数中是否包含“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑

自定义全局过滤器 要求:必须实现GlobalFilter,Ordered接口

Spring Cloud Gateway全局过滤器GlobalFilter:返回消息和重定向_weixin_33701617的博客-CSDN博客

登陆过滤器:

1 applicaiton.yml配置文件设置自定义放行路径

# 自定义配置  放行路径
anon:
  url:
    - /product/getById/*
    - /product/getMsg

 2 创建实体类

package com.wzh.entity;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;


@Component
@ConfigurationProperties(prefix = "anon")
public class Anon {
    private List<String> url;

    public Anon() {
    }

    public Anon(List<String> url) {
        this.url = url;
    }

    public List<String> getUrl() {
        return url;
    }

    public void setUrl(List<String> url) {
        this.url = url;
    }
}

3 引入fastjson包

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.80</version>
        </dependency>

4 在ProductController中添加方法getMsg 

package com.wzh.controller;

import com.alibaba.fastjson.JSON;

import com.wzh.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.wzh.entity.Product;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.controller
 * @ClassName: ProductController
 * @Author: 王振华
 * @Description:
 * @Date: 2022/8/18 21:05
 * @Version: 1.0
 */
@RestController
@RequestMapping("/product")
@Slf4j
public class ProductController {
    @Autowired
    private ProductService productService;

    @GetMapping("/getById/{pid}")
    public Product getById(@PathVariable Integer pid){
        Product product = productService.findById(pid);
        log.info("查询到商品:"+ JSON.toJSONString(product));
        return  product;
    }

    @GetMapping("/getMsg")
    public String getMsg(){
        return "getMsg=========";
    }
}

5 编写登陆过滤器

package com.wzh.filter;


import com.alibaba.fastjson.JSON;
import com.wzh.entity.Anon;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * @ProjectName: springcloud-wzh-parent
 * @Package: com.wzh.filter
 * @ClassName: LoginFilter
 * @Date: 2022/8/21 15:26
 * @Version: 1.0
 */
@Component
public class LoginFilter implements GlobalFilter, Ordered {
    @Autowired
    private Anon anon;
    //过滤方法   过滤业务
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        //解决中文乱码
        response.getHeaders().add("Content-Type","application/json;charset=UTF-8");


        //1.  获取请求路径
        String path = request.getPath().toString();
        System.out.println(path);
        //2.  判断当前的请求路径是否为放行路径
        if(anon.getUrl().contains(path)){
            System.out.println("--------");
            return chain.filter(exchange);//放行
        }
        //3. 判断是否携带token
        String token = request.getHeaders().getFirst("token");
        //这里先指定token值为admin  后期是从redis中获取的
        if(StringUtils.hasText(token)&&token.equals("admin")){
            return chain.filter(exchange);//放行
        }

        //3.返回一个json数据。
        //3.1设置状态码
        response.setStatusCode(HttpStatus.UNAUTHORIZED);

        //3.2封装返回数据
        Map<String, Object> map = new HashMap<>();
        map.put("msg", "未登录");
        map.put("code", 4000);

        //3.3作JSON转换
        byte[] bytes = JSON.toJSONString(map).getBytes(StandardCharsets.UTF_8);

        //3.4调用bufferFactory方法,生成DataBuffer对象
        DataBuffer buffer = response.bufferFactory().wrap(bytes);

        //4.调用Mono中的just方法,返回要写给前端的JSON数据
        return response.writeWith(Mono.just(buffer));
    }

    //优先级 返回的值越小优先级越高
    @Override
    public int getOrder() {
        return 0;
    }
}

6 重启服务,测试是否成功

这里有个问题,带参数的也不会放行,比如product/getById/1,它在登录过滤器中
anon.getUrl().contains(path)比较的时候永远不会相等

4.9 gateway网关设置跨域

第一种,在解决之前在gateway中用了全局配置类

package com.wzh.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

第二种:配置文件添加

spring:
  cloud:
    gateway:
      globalcors:
        add-to-simple-url-handler-mapping: true
        cors-configurations:
          '[/**]':
            allowedOrigins:
              - "*"
            allowedMethods:
              - "GET"
              - "POST"
              - "DELETE"
              - "PUT"
              - "OPTIONS"
            allowedHeaders: "*"
            allowCredentials: true
            maxAge: 360000

gateway跨域问题解决方法_sky~的博客-CSDN博客_gateway跨域

本文标签: SpringCloud