告诉老默我想学Spring Cloud了(新手篇):从0到1搭建Spring Cloud项目(实际项目开发的浓缩精华版)

告诉老默我想学Spring Cloud了(新手篇):从0到1搭建Spring Cloud项目

一、前言

 
  Spring官方网址:https://spring.io/
 
  电视剧《狂飙》有多火,看我博客的标题就知道了,哈哈,蹭了个热度。同样也是系列博客,后面还会有更多这个系列的博客发布。这篇博客主要介绍如何从0到1搭建一个Spring Cloud项目,版本如何选型,项目结构如何搭建,适合新手入门。别人不教的技术,我教你!全文已超 10 万字,写文不易,希望能得到大家的肯定。
 
帮我找到老墨,告诉他,我想学SpringCloud了(新手篇)
 
  第一次写Spring相关的博客,希望是个好的开端,能力有限,如果有误,还请大家多多指教。项目源代码下载地址在文章末尾第四章介绍了一些强大好用的插件,可以先去看看。
 
  【搭建Spring Cloud项目必备前置环境】

1、Windows系统,Win7、Win10或Win11。MacOS系统没用过,不过原理差不多。笔者我用的是Win10专业版。
 
2、安装JDK8。传送门最新JDK8(jdk-8u341)在Win10安装部署(超详细)
 
3、安装Maven。传送门最新Maven(apache-maven-3.8.6)在Win10安装部署(超详细)
 
4、安装 IDEA 或 Ecplise 开发工具。我安装的是 IntelliJ IDEA 2021.1.3 (Ultimate Edition) 版,并没有使用最新版,更不会更新最新版,因为这个版本可以使用一个插件无限重置试用期,新版限制多。传送门支持 IDEA 2021.1.3 版无限重置试用期插件 百度网盘
 
5、安装MySQL,MySQL 8.x 或 MySQL 5.7.x 都可以。传送门
 
(1)最新 MySQL 8.0.32 在Win10安装部署(详细)
(2)最新MySQL-5.7.40在云服务器Centos7.9安装部署
(3)Docker安装最新版MySQL5.7(mysql-5.7.40)教程(参考Docker Hub)
(4)写最好的Docker安装最新版MySQL8(mysql-8.0.31)教程(参考Docker Hub和MySQL官方文档)
 
6、安装Postman。传送门https://www.postman.com/downloads/

 
  本文由 CSDN@大白有点菜 原创,如需转载,请说明出处。如果觉得文章还不错,可以 点赞+收藏+关注 ,你们的肯定是我创作优质博客的最大的动力。
 

二、如何选择版本

 
  对于新手来说,学习搭建Spring Cloud项目,基本都是参考网上的博客教程,吧啦吧啦地复制一通,却没有想过,版本是如何选型的。Spring Boot 和 Spring Cloud 的版本那么多,是随意组合版本都可以吗? 答案是,不可以这么做!
 
  版本选型很重要,有些组件可以选择最新的版本,有些是不允许这么做。在实际工作中,对于 Spring Boot 和 Spring Cloud 的版本选型,一般做法是:选旧不选新,选稳定的最好。意思是,不要认为最新的版本就一定是最好的,问题会非常多,反而旧版本更好,稳定性更高。例如 Spring Framework 最新是版本 6.x ,但是大部分公司用的是 Spring Framework 5.x(主流),还有公司用 Spring Framework 4.x(这个版本太旧了,建议不要使用),除非有必要,不然很少会使用新版本。
 
  Spring官网有介绍到如何进行版本选型。接下来,我就带大家探究 Spring 官方是如何建议版本选型的。
 

2.1 SpringCloud 和 Spring Boot 版本选型

 

2.1.1 Spring Cloud 版本与 Spring Boot 版本关系

 
  我们浏览 Spring 的官网:https://spring.io/ ,在“Projects”菜单栏中点击“Spring Cloud”进入到 Spring Cloud 的介绍页面,在“OVERVIEW”标签中描述到,Spring Cloud 版本应该和哪个 Spring Boot 版本对应。如图所示,笔者我使用了“划词翻译”这款浏览器插件,使用谷歌翻译(需要梯子才能正常使用),官方网页都是英文,对英语差的人不友好,目前所有的谷歌翻译插件在正常情况下都无法使用,因为谷歌翻译也退出了中国市场。
 
在“Projects”菜单栏中点击“Spring Cloud”进入到 Spring Cloud 的介绍页面
 
在“OVERVIEW”标签中描述到,Spring Cloud 版本应该和哪个 Spring Boot 版本对应。
 
  按官方建议,我若选用 Spring Cloud 2021.0.x 这个版本,那么 Spring Boot 就要选择 2.6.x 或者 2.7.x 版本,对应的 Spring Framework 是 5.x 版本。如果是 Spring Cloud 2021.0.3 或更高版本,那么就可以选择 Spring Boot 2.7.x。具体是哪个版本呢?“x”只是一系列版本代号。
 
  随便提一下最新的 Spring Cloud 2022.0.x 版本,对应的 Spring Boot 版本是 3.0.x,对应的 Spring Framework 版本是 6.x ,都是跨了大版本,新增了不少功能。兼容性如何没研究过,要尝试最新的版本需要掂量掂量后果,不建议用来部署工作环境,除非有必要这么做。
 

2.1.2 选择具体的合适版本

 
  如何选择具体的合适版本呢?Spring官方给出了答案。我们切换到“LEARN”标签,点开“2021.0.6 GA”右边的“Reference DOC.”链接。GA 是 General Availability 的缩写,中文翻译:一般可用性。代表稳定发布版。 千万不能使用快照版(SNAPSHOT),不稳定
 
切换到“LEARN”标签,点开“2021.0.6 GA”右边的“Reference DOC.链接
 
  明显看到,如下图所示,官方建议在使用 Spring Cloud 2021.0.6 版本的情况下,使用的 Spring Boot 版本是 2.6.14 ! 但是这个版本不是 2.x 最新的版本,最新稳定的 Spring Boot 2.x 是 2.7.10 版本。就是说,官方并没有使用最新的版本去搭配,我们不要随意去使用对应的最新版本,按官方文档去选型,起码不会出现太大问题。虽然目前官方文档建议 Spring Boot 2.6.14 搭配 Spring Cloud 2021.0.6 使用,但是可能过一段时间就会更新更加稳定的版本组合,旧版本组合只要用着没问题就不用更换新版本。
 
官方建议在使用 Spring Cloud 2021.0.6 版本的情况下,使用的 Spring Boot 版本是 2.6.14 !
 
最新稳定的 Spring Boot 2.x 是 2.7.10 版本。
 
  官方建议的版本组合是可以变换的,只要在一定的范围内就行。网址:https://start.spring.io/actuator/info 可以查看所有 Spring Cloud 版本对应的 Spring Boot 版本范围,如图所示。要使用 Spring Cloud 2021.0.6 ,那 Spring Boot 版本的范围就要满足"2.6.1 <= Spring Boot < 3.0.0-M1"
 
查看所有 Spring Cloud 版本对应的 Spring Boot 版本范围
 

2.2 第三方组件的版本选型

 
  在项目开发中,我们会大量使用到第三方组件,该如何选择版本呢?那就得看看该组件的介绍,对依赖的组件(很多组件都依赖其它组件进行开发)版本有没有要求,如果没有太大要求,可以选用最新的版本。如果有要求,那就按要求去选择合适的版本。新版本的发布要么是新增功能,要么是修复 bug ,新增功能可能会产生新的 bug ,又得去修复 bug ,如此反复,因此版本迭代才这么频繁。
 
  注意是否是大版本还是小版本,可以这么理解:Spring Boot 2.x 到 Spring Boot 3.x 跨了一个大版本,功能有变化(新增或减少)。Spring Boot 2.6.13 到 Spring Boot 2.6.14 只跨了一个小版本,都属于 Spring Boot 2.x 系列,这种版本主要以修复 bug 为主,功能可能没变化
 

三、从0到1搭建 Spring Cloud 项目

 
  使用哪种工具去搭建Spring Cloud项目都可以,个人认为还是 IDEA 更好用,插件多,也是大家首选Java开发工具。有很多方式去创建Spring Cloud项目,我只介绍其中两种,采用哪种方式看个人意愿,没有最优的创建方式。
 

3.1 方式一创建Spring Cloud项目:官方网站或IDEA的Srping Initializr

 

3.1.1 创建步骤

 
  1、在Spring官网的“Projects”菜单栏中点击“Spring Initializr”进入到项目初始化页面。或者直接跳转到 https://start.spring.io/ 。也可以使用 IDEA 进行创建,效果是一样的。
 
在Spring官网的“Projects”菜单栏中点击“Spring Initializr”进入到项目初始化页面。
 
使用 IDEA 创建Spring Cloud项目并设置参数

 
使用 IDEA 创建Spring Cloud项目并添加依赖包

 
  2、“Project” 项勾选 “Maven” 。“Spring Boot” 项勾选 “2.7.10”(此处没有 2.6.14 版本选择,官方使用的是最新的 2.7.10 稳定版)。
 
  项目元数据 “Project Metadata” 项很重要,要填写的内容很多。

Group”填写域名,可真实也可虚构。如 com.dbydc
 
Artifact” 项输入项目名称,“Name”会自动填充同样的名称。如 SpringCloudZero2One
 
Description” 项是对项目的一些简单描述,可以写也可以不写。
 
Package name” 项是项目的包名,默认由 Group + Artifact 构成,也可以修改为指定的包名。如 com.dbydc.zero2one 。建议全部小写。
 
Packaging” 项默认“Jar” 即可。
 
Java” 项可选择 JDK 版本,此处选择 JDK 8

 
  可以添加其它依赖,也可以后期手动添加。必选组件:“Spring Web(Spring Framework Runtime架构之一)”。常用组件(可选)“Lombok(使用注解,不用写Setter和Getter方法,减少代码量,功能强大)”、“MySQL Driver(MySQL数据库连接驱动)”、“OpenFeign(微服务之间远程调用)”
 
  点 “GENERATE” 会下载打包好的压缩包。
 
Spring Initializr参数填写
 
3、解压压缩包,使用 IDEA 导入Spring Cloud项目。pom.xml 文件列出了所有依赖包。
 
解压Spring Cloud项目压缩包

 
pom.xml 文件列出了所有依赖包。
 

3.1.2 忽略指定后缀名的文件

 
  为了使项目结构更加简洁,我们可以设置忽略某些文件,也可以删除意义不大的文件。例如 *.iml 结尾的文件可以忽略
 
*.iml 结尾的文件可以忽略。

 
  打开“设置”面板:“File” -> “Settings”
 
打开“设置”面板

 
  选择“Editor”下的“File Types”,切换到“Ignored Files and Folders”,再点“+”
 
选择“Editor”下的“File Types”,切换到“Ignored Files and Folders”,再点“+”。
 
  输入“*.iml”,注意,一定要按回车键添加! 不然无法生效。
 
输入“*.iml”,注意,一定要按回车键添加!不然无法生效。
 
  先点“Apply”,再点“OK”,那么配置就设置好了,再去看项目结构,发现 xxx.iml 文件已经消失。
 
先点“Apply”,再点“OK”,那么配置就设置好了,再去看项目结构,发现 xxx.iml 文件已经消失。
 

3.1.3 删除对开发无影响的文件或文件夹(如 .mvn 、mvnw 和 mvnw.cmd)

 
  使用 Srping Initializr 生成的项目有“.mvn”、“mvnw”、“mvnw.cmd”等文件夹或文件,可以删除,对开发并没有多大影响。选中,右键,点“Delete”即可删除。你也可以保留这些文件,不一定需要删除。
 
  【知识拓展】:

【Spring实战 第5版】Page 10
 
mvnw 和 mvnw.cmd 是 Maven 包装器(wrapper)脚本,借助这些脚本,即使系统上没有安装 Maven ,也可以构建项目。

 
删除多余的文件或文件夹

 

3.1.4 自动生成的项目结构

 
  自动生成了一个 SpringCloudZero2OneApplication 引导类和 application.properties 配置文件。
 
  【知识拓展】:

【Spring实战 第5版】Page 10
 
(1)“static”目录:为浏览器提供服务的静态内容(如图片、样式表、JavaScript等),文件夹初始为空。
 
(2)“templates”目录:用来存放渲染内容到浏览器的模板文件(如 Thymeleaf 模板),文件夹初始为空。
 
(3)“application.properties”文件:配置文件,用来配置属性的,例如设置 Server 的端口号。目前使用最多的还是 application.ymlapplication.yaml 这两种写法。

 
自动生成了一个 SpringCloudZero2OneApplication 入口类和 application.properties 配置文件。
 

3.2 方式二创建Spring Cloud项目:IDEA工具创建Spring Cloud项目

 

3.2.1 创建步骤

 
  1、新建Spring Cloud项目:“File” -> “New” -> “Project”
 
新建Spring Cloud项目:“File” -> “New” -> “Project”。
 
  2、选择“Maven”项目,“Project SDK”默认识别出来,显示的是安装在Win10系统的 JDK 1.8.0_281 。点“Next”。
 
选择“Maven”项目

 
  3、填写“项目名称(Name)”,选择“项目保存路径(Location)”,可修改“版本号(Version)”。最后点“Finish”即可新建项目。
 
填写“项目名称(Name)”,选择“项目保存路径(Location)”,可修改“版本号(Version)”。
 

3.2.2 父模块 pom.xml 文件设置依赖管理

 
  打开 SpringCloudZero2One 项目根目录下的 pom.xml 文件,这是父模块的 pom.xml 文件。我们需要 设置依赖管理(Dependency Management),后面会添加很多子模块(Module),子模块也有各自的 pom.xml 文件。根目录下父模块的 pom.xml 会统筹管理整个项目的依赖,子模块添加某些依赖时可以不用写版本号,版本号从父模块继承(如果暂时不能理解,看后面的样例就能更好地理解了)。
 
  【Maven官方文档关于依赖机制的介绍】
  https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
 
  依赖范围(Dependency Scope):

谷歌翻译官方原文:
 
1、compile编译。这是默认范围,如果未指定则使用。编译依赖项在项目的所有类路径中都可用。此外,这些依赖关系会传播到依赖项目。
 
2、provided提供。这很像 compile ,但表示您希望 JDK 或容器在运行时提供依赖项。例如,在为 Java Enterprise Edition 构建 Web 应用程序时,您可以将对 Servlet API 和相关 Java EE API 的依赖设置为 provided 的范围,因为 Web 容器提供了这些类。具有此范围的依赖项将添加到用于编译和测试的类路径,但不会添加到运行时类路径。它不是传递性的。
 
3、runtime运行时。这个作用域表明依赖不是编译所必需的,而是执行所必需的。 Maven 在运行时和测试类路径中包含具有此范围的依赖项,但不包含编译类路径。
 
4、test测试。该作用域表示该依赖对于应用程序的正常使用不是必需的,仅在测试编译和执行阶段可用。这个范围是不可传递的。通常此范围用于测试库,例如 JUnit 和 Mockito。它也用于非测试库,例如 Apache Commons IO,如果这些库用于单元测试 (src/test/java) 但不用于模型代码 (src/main/java)。
 
5、system系统。此范围类似于提供的范围,只是您必须显式提供包含它的 JAR。工件始终可用,不会在存储库中查找。
 
6、import导入。此范围仅在 <dependencyManagement> 部分中的类型 pom 的依赖项上受支持。它表示依赖项将被替换为指定 POM 的 <dependencyManagement> 部分中的有效依赖项列表。由于它们被替换,具有导入范围的依赖项实际上并不参与限制依赖项的传递性。

 
  【父模块 pom.xml 修改内容】

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

    <groupId>com.dbydc.zero2one</groupId>
    <artifactId>SpringCloudZero2One</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring-boot-starter-parent.version>2.6.14</spring-boot-starter-parent.version>
        <spring-cloud.version>2021.0.6</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>${spring-boot-starter-parent.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.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>

 
父模块 pom.xml 修改内容

 
  spring-boot-starter-parentspring-cloud-dependencies 是Spring Cloud项目中必不可少的一员,spring-cloud-alibaba-dependencies 虽然是可选的,但是使用 Spring Cloud Alibaba 全家桶是 Java 界的主流,需要重点学习和掌握。
 
  依赖引入和管理的规范写法如下,依赖包的<version>标签使用格式“${xxx.version}”,然后在<properties>标签写上依赖的版本号

<properties>
    <spring-boot-starter-parent.version>2.6.14</spring-boot-starter-parent.version>
</properties>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>${spring-boot-starter-parent.version}</version>
</dependency>

 
  每次添加新的依赖包,都可以点击刷新图标。或者右键打开一个面板,点 “Maven” -> “Reload project” 效果是一样的。
 
每次添加新的依赖包,都可以点击刷新图标。
 
或者右键打开一个面板,点 “Maven” -> “Reload project” 效果是一样的。
 
  手动刷新依赖比较麻烦,还可以设置自动刷新依赖。设置:“Files” -> “Settings” -> “Build, Execution, Deployment” -> “Build Tools” -> “Maven”,勾选“Always update snapshots”,点“Apply”,最后点“OK”
 
IDEA设置自动刷新依赖
 
  每个 IDEA 版本可能都不一样,我用的是 IDEA 2021.1.3 Ultimate 版本,如果找不到相同的参数,那就自个查询对应版本如何设置,网上教程很多。其中一个是:Setting面板中搜索“maven”,在“maven”的“Importing”功能中勾选“Import Maven projects automatically”。还有一个是:“Files” -> “Settings” -> “Build, Execution, Deployment” -> “Build Tools”,勾选“Any changes”
 

3.2.3 删除父模块下的 src 目录

 
  新建项目时,会新建 src 目录(包含 main 和 test 目录),父模块不需要写业务代码,只需留一个 pom.xml 文件即可,我们需要手动删除 src 目录。
 
手动删除父模块下的 src 目录

 

3.2.4 Spring Cloud Alibaba 版本与 Spring Boot 版本的映射关系

 
  在前面说到,Spring Cloud 版本与 Spring Boot 版本存在一种映射关系,不能随意搭配,Spring Cloud Alibaba 版本也是一样,依赖 Spring Cloud 和 Spring Boot 版本。
 
  找到 Spring Cloud Alibaba 对应的页面,官方网址:https://spring.io/projects/spring-cloud-alibaba ,官方最新的版本是 2021.0.4.0 ,到底选择哪个版本呢,最新版 OR 旧版本?我们去 Github(国内访问很慢,想要访问快需要使用梯子,通俗说就是 FanQiang)查看其版本有哪些。
 
找到 Spring Cloud Alibaba 对应的页面
 
Spring Cloud Alibaba选择最新版 OR 旧版本?
 
  阿里巴巴出品,必有中文文档,那就点开“中文文档”呗。
 
阿里巴巴出品,必有中文文档,那就点开“中文文档”呗
 
  如何构建 中说到“2021.x 分支对应的是 Spring Cloud 2021 与 Spring Boot 2.6.x,最低支持 JDK 1.8。”,我们再点“版本说明”查看更详细的版本依赖关系。
 
“如何构建”中说到“2021.x 分支对应的是 Spring Cloud 2021 与 Spring Boot 2.6.x,最低支持 JDK 1.8。”
 
“版本说明”查看更详细的版本依赖关系。
 
  从“版本说明”可以看出,如果使用 Spring Cloud Alibaba 2021.0.4.0 版本,那么 Spring Cloud 版本为 2021.0.4 ,Spring Boot 版本为 2.6.11 。 我们搭建的Spring Cloud项目使用官方推荐的 Spring Cloud 2021.0.6 + Spring Boot 2.6.14 组合,当然是可以兼容的 Spring Cloud Alibaba 2021.0.4.0 版本的,以Spring官方推荐的版本为主。
 

3.3 公共API(cloud-common-api)子模块

 
  我们可以将一些依赖(如 MySQL 驱动、Lombok、OpenFeign等)放到一个公共的API子模块中,其它子模块引用公共API子模块依赖,就可以引入了想要的依赖,避免每个子模块都写一遍相同的依赖引入(类似将工具类封装起来,其它类共享)。
 

3.3.1 新建公共API(cloud-common-api)子模块

 
  选中“SpringCloudZero2One”项目,鼠标右键,“New” -> “Module” 新建子项目。
 
选中“SpringCloudZero2One”项目,鼠标右键,“New” -> “Module” 新建子项目。
 
  选择“Maven”项目,“Module SDK”选择本机安装的 JDK8(例如 jdk 1.8.0_281),点“Next”下一步。
 
选择“Maven”项目,“Module SDK”选择本机安装的 JDK8(例如 jdk 1.8.0_281),点“Next”下一步。
 
  注意,“Parent”一定是父模块“SpringCloudZero2One”,此处默认选中,不用修改。“Name”填写项目名称为“cloud-common-api”。其它的默认就行,不用修改。最后点“Finish”完成即可。
 
注意,“Parent”一定是父模块“SpringCloudZero2One”,此处默认选中,不用修改。“Name”填写项目名称为“cloud-common-api”。其它的默认就行,不用修改。
 
  新建公共API子模块后,会在父模块的 pom.xml 文件生成<modules>标签。
 
新建公共API子模块后,会在父模块的 pom.xml 文件生成标签。
 

3.3.2 公共API(cloud-common-api)子模块 pom.xml 文件添加依赖

 
  公共API(cloud-common-api)子模块主要添加以下依赖:

1、spring-boot-starter-web:Spring Framework 框架 Web 模块。
2、spring-boot-starter-test:Spring Framework 框架 Test 模块。
3、mysql-connector-j:MySQL驱动,mysql-connector-java 的新版本(8.0.31或以上)已迁移到 mysql-connector-j 。
4、lombok:使用注解,避免写 Setter 和 Getter ,减少代码量,功能强大。
5、druid-spring-boot-starter:德鲁伊数据库连接池。
6、mybatis-plus-boot-starter:mybatis的增强工具。

  【公共API(cloud-common-api)子模块 pom.xml 内容】

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

    <artifactId>cloud-common-api</artifactId>
    <description>公共模块API</description>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <mysql-connector-j.version>8.0.32</mysql-connector-j.version>
        <lombok.version>1.18.26</lombok.version>
        <druid-spring-boot-starter.version>1.2.16</druid-spring-boot-starter.version>
        <mybatis-plus-boot-starter.verion>3.5.3.1</mybatis-plus-boot-starter.verion>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <!--MySQL驱动,mysql-connector-java 的新版本(8.0.31或以上)已迁移到 mysql-connector-j -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>${mysql-connector-j.version}</version>
        </dependency>
        <!-- Lombok https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>
        <!-- Druid连接池 https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>${druid-spring-boot-starter.version}</version>
        </dependency>
        <!-- Mybatis-Plus https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus-boot-starter.verion}</version>
        </dependency>
    </dependencies>

</project>

 
  在子模块中,spring-boot-starter-web 和 spring-boot-starter-test 都不用写版本号,都由父模块的 pom.xml 去管理依赖版本号。注意:如果在Maven仓库复制,lombok 依赖需要删除“ <scope>provided</scope> ”这部分内容。
 
  我们要添加依赖和查询依赖的版本,可以进入Maven仓库去查询:https://mvnrepository.com/
 
  特别要说一下MySQL驱动的最新依赖:mysql-connector-j 。这是新版的写法,旧写法是:mysql-connector-java 。如果使用 Srping Initializr 创建项目,添加 MySQL Driver 依赖,它就是使用最新的写法 mysql-connector-j
 
  在 Maven 仓库中,MySQL 有两种版本的驱动,该怎么选择呢?
 
在 Maven 仓库中,MySQL 有两种版本的驱动,该怎么选择呢?
 
  进入到 MySQL Connector Java 页面,可以看到,Maven官方提到,mysql-connector-java 已经迁移到 mysql-connector-j 了!
 
mysql-connector-java 已经迁移到 mysql-connector-j
 
  只是 8.0.31 或以上版本才迁移到 mysql-connector-j ,低版本还依旧在 mysql-connector-java 依赖中。
 
只是 8.0.31 或以上版本才迁移到 mysql-connector-j ,低版本还依旧在 mysql-connector-java 依赖中。
 
  目前大家还是喜欢 mysql-connector-java 这种依赖的写法,也是主流的写法,支持的驱动版本多。不知道官方最新的MySQL驱动版本还会不会更新到 mysql-connector-java 依赖里面,如果不更新了,想要使用最新的驱动,那就使用 mysql-connector-j 依赖吧。
 
mysql-connector-java 依赖版本
 
  点一下MySQL版本号(8.0.32),进入到对应的maven页面。只需点一下“Maven”里面的依赖内容,就已经复制好,不用按 Ctrl + C 快捷键进行复制,可以直接粘贴了。其它依赖复制同理,不再详说。
 

对应的Maven仓库:
 
MySQL Connector/J:https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
 
Project Lombok:https://mvnrepository.com/artifact/org.projectlombok/lombok
 
Druid Spring Boot Starter:https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter
 
MyBatis Plus:https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter

 
点一下MySQL版本号(8.0.32)版本号,进入到对应的maven页面。
 
复制MySQL驱动依赖
 
复制lombok依赖
 
复制druid连接池依赖
 
复制mybatis-plus依赖
 

3.3.3 创建一个数据返回的公共类

 
  在实际开发中,后端和前端需要进行数据交互,一般的数据格式是 JSON 。后端需要定义一个公共类,用来返回数据给前端。我们就在 cloud-common-api 子模块中新建一个数据返回的的公共类,那么整个 SpringCloudZero2One 的其它子模块都可以使用这个公共类。
 
  选中“java”目录,右键,“New” -> “Java Class”,输入“com.dbydc.zero2one.common.utils.ResponseData”,新建 ResponseData.java 文件。这样写可以快速地新建包路径,例如“com.dbydc.zero2one.common.utils”。
 
选中“java”目录,右键,“New” -> “Java Class”
 
输入“com.dbydc.zero2one.common.utils.ResponseData”,新建 ResponseData.java 文件。
 
新建好的ResponseData.java文件
 
  数据返回公共类 ResponseData.java 代码修改如下
 

package com.dbydc.zero2one.common.utils;

import com.dbydc.zero2one.common.enums.ResponseCodeEnum;

import java.io.Serializable;

/**
 * 数据返回响应类
 * @author 大白有点菜
 * @className ResponseData
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
public class ResponseData implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 响应码
     */
    private Integer code;
    /**
     * 响应数据
     */
    private Object data;
    /**
     * 响应信息
     */
    private String message;

    public ResponseData() {

    }

    /**
     * 构造函数
     * @param code 响应码
     * @param data 响应数据
     * @param message 响应信息
     */
    public ResponseData(Integer code, Object data, String message) {
        this.code = code;
        this.data = data;
        this.message = message;
    }

    /**
     * Success,无入参
     * @return
     */
    public static ResponseData success() {
        return new ResponseData(200, null, "返回空数据");
    }

    /**
     * Success,入参:code
     * @param code 响应码
     * @return
     */
    public static ResponseData success(Integer code) {
        return new ResponseData(code, null, "返回空数据");
    }

    /**
     * Success,入参:code、data
     * @param code 响应码
     * @param data 响应数据
     * @return
     */
    public static ResponseData success(Integer code, Object data) {
        return new ResponseData(code, data, "返回数据成功");
    }

    /**
     * Success,入参:code、message
     * @param code 响应码
     * @param message 响应信息
     * @return
     */
    public static ResponseData success(Integer code, String message) {
        return new ResponseData(code, null, message);
    }

    /**
     * Success,入参:code、data、message
     * @param code 响应码
     * @param data 响应数据
     * @param message 响应信息
     * @return
     */
    public static ResponseData success(Integer code, Object data, String message) {
        return new ResponseData(code, data, message);
    }

    /**
     * Error,入参:code
     * @param code 响应码
     * @return
     */
    public static ResponseData error(Integer code) {
        return new ResponseData(code, null, "返回数据错误");
    }

    /**
     * Error,入参:code、message
     * @param code 响应码
     * @param message 响应信息
     * @return
     */
    public static ResponseData error(Integer code, String message) {
        return new ResponseData(code, null, message);
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

 
  ResponseData 类中写了硬代码(例如 return new ResponseData(200, null, “返回空数据”);),其实不好。应该定义一个枚举类(enum),枚举类中定义统一的响应码和响应信息,尽可能地避免写硬代码。
 
  好吧,改造一下。同样选中“java”目录,右键新建“Java Class”,输入“com.dbydc.zero2one.common.enums.ResponseCodeEnum”,新建 ResponseCodeEnum 枚举类注意:包路径中不能包含 enum ,不然会报错,因此使用 enums 代替。 代码修改如下:
 
新建 ResponseCodeEnum 枚举类

 
ResponseCodeEnum 枚举类代码修改。
 

package com.dbydc.zero2one.common.enums;

/**
 * 响应码枚举类
 * @author 大白有点菜
 * @className ResponseCodeEnum
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
public enum ResponseCodeEnum {
    /**
     * 成功-1000
     */
    SUCCESS(1000, "返回数据成功"),
    /**
     * 空数据-1001
     */
    NULL_DATA(1001, "返回空数据"),
    /**
     * 失败-1002
     */
    ERROR(1002, "返回数据错误");

    /**
     * 响应码
     */
    private final Integer code;
    /**
     * 响应信息
     */
    private final String message;

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    ResponseCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * 通过响应码获取响应信息
     * @param code 响应码
     * @return
     */
    public static String getMessageByCode(Integer code) {
        ResponseCodeEnum responseCodeEnum = getEnumByCode(code);
        return responseCodeEnum == null ? null : responseCodeEnum.getMessage();
    }

    /**
     * 通过响应码获取ResponseCodeEnum对象
     * @param code 响应码
     * @return
     */
    public static ResponseCodeEnum getEnumByCode(Integer code) {
        if (code == null) {
            return null;
        }
        ResponseCodeEnum[] values = ResponseCodeEnum.values();
        for (ResponseCodeEnum value : values) {
            if (value.getCode().equals(code)) {
                return value;
            }
        }
        return null;
    }
}

 
  ResponseCodeEnum 枚举类自定义三个整型的响应码(1000、1002、1003)和对应的响应信息,同时有两个 static 方法,getMessageByCode() 方法通过响应码获取响应信息,getEnumByCode() 方法通过响应码获取ResponseCodeEnum对象。后端需要和前端沟通响应码的定义,如何定义还得看项目实际需求。
 
  ResponseData 数据返回响应类改造后的代码如下:
 

package com.dbydc.zero2one.common.utils;

import com.dbydc.zero2one.common.enums.ResponseCodeEnum;

import java.io.Serializable;

/**
 * 数据返回响应类
 * @author 大白有点菜
 * @className ResponseData
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
public class ResponseData implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 响应码
     */
    private Integer code;
    /**
     * 响应数据
     */
    private Object data;
    /**
     * 响应信息
     */
    private String message;

    public ResponseData() {

    }

    /**
     * 构造函数
     * @param code 响应码
     * @param data 响应数据
     * @param message 响应信息
     */
    public ResponseData(Integer code, Object data, String message) {
        this.code = code;
        this.data = data;
        this.message = message;
    }

    /**
     * Success,无入参
     * @return
     */
    public static ResponseData success() {
        return new ResponseData(ResponseCodeEnum.NULL_DATA.getCode(), null, ResponseCodeEnum.NULL_DATA.getMessage());
    }

    /**
     * Success,入参:code
     * @param code 响应码
     * @return
     */
    public static ResponseData success(Integer code) {
        return new ResponseData(code, null, ResponseCodeEnum.NULL_DATA.getMessage());
    }

    /**
     * Success,入参:code、data
     * @param code 响应码
     * @param data 响应数据
     * @return
     */
    public static ResponseData success(Integer code, Object data) {
        return new ResponseData(code, data, ResponseCodeEnum.SUCCESS.getMessage());
    }

    /**
     * Success,入参:code、message
     * @param code 响应码
     * @param message 响应信息
     * @return
     */
    public static ResponseData success(Integer code, String message) {
        return new ResponseData(code, null, message);
    }

    /**
     * Success,入参:code、data、message
     * @param code 响应码
     * @param data 响应数据
     * @param message 响应信息
     * @return
     */
    public static ResponseData success(Integer code, Object data, String message) {
        return new ResponseData(code, data, message);
    }

    /**
     * Error,入参:code
     * @param code 响应码
     * @return
     */
    public static ResponseData error(Integer code) {
        return new ResponseData(code, null, ResponseCodeEnum.ERROR.getMessage());
    }

    /**
     * Error,入参:code、message
     * @param code 响应码
     * @param message 响应信息
     * @return
     */
    public static ResponseData error(Integer code, String message) {
        return new ResponseData(code, null, message);
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

 

3.4 创建 zero2one 数据库

 
  可以使用 Navicat 等工具在 MySQL创建 zero2one 数据库,更加方便。我直接连接 MySQL Server,使用命令去创建 zero2one 数据库,并创建 tb_payment 和 tb_order 两张表。
 

3.4.1、创建 zero2one 数据库

 
(1)创建数据库。

create database zero2one;

(2)查看所有数据库。

show databases;

 
创建 zero2one 数据库
 

3.4.2、新建表 tb_payment 和 tb_order

 
(1)切换到 zero2one 数据库,新建表 tb_payment

use zero2one;
CREATE TABLE `tb_payment` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `order_num` varchar(64) NOT NULL COMMENT '订单号',
  `payment_amount` decimal(10,2) DEFAULT NULL COMMENT '支付金额',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

 
新建表 tb_payment
 
(2)新建表 tb_order

CREATE TABLE `tb_order` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `order_num` varchar(64) NOT NULL COMMENT '订单号',
  `user_name` varchar(128) DEFAULT NULL COMMENT '用户名',
  `user_phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户手机号',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

 
新建表 tb_order
 

3.4.3、汇总的 zero2one.sql 文件创建表 tb_payment 和 tb_order

 

/*
 Navicat Premium Data Transfer

 Source Server         : Local
 Source Server Type    : MySQL
 Source Server Version : 80032
 Source Host           : localhost:3306
 Source Schema         : zero2one

 Target Server Type    : MySQL
 Target Server Version : 80032
 File Encoding         : 65001

 Date: 31/03/2023
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order`  (
  `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `order_num` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '订单号',
  `user_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
  `user_phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户手机号',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_order
-- ----------------------------

-- ----------------------------
-- Table structure for tb_payment
-- ----------------------------
DROP TABLE IF EXISTS `tb_payment`;
CREATE TABLE `tb_payment`  (
  `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `order_num` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '订单号',
  `payment_amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '支付金额',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_payment
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;

 

3.5 新建 cloud-payment-service6001 子模块

 
  我们新建一个 cloud-payment-service6001 子模块,这是互联网上博客写烂了的支付服务模块,具体的新建过程我就不再多说,和 cloud-common-api 子模块新建过程是一样的。为什么后面带个 6001 呢?这做的目的是,6001 代表这个服务的端口号,后期会创建更多的子模块,使用到不同的端口号,子项目名称中带端口号更加容易辨认。
 

3.5.1 修改 cloud-payment-service6001 子模块的 pom.xml 文件

 
  修改 cloud-payment-service6001 子模块的 pom.xml 文件,如下所示,只需引入公共API cloud-common-api 即可完事。
 

<?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>SpringCloudZero2One</artifactId>
        <groupId>com.dbydc.zero2one</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-payment-service6001</artifactId>

    <dependencies>
        <!-- 引入公共API cloud-common-api 依赖即可 -->
        <dependency>
            <groupId>com.dbydc.zero2one</groupId>
            <artifactId>cloud-common-api</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

</project>

 
修改 cloud-payment-service6001 子模块的 pom.xml 文件。
 

3.5.2 新增 application.yml 配置文件

 
  新建 application.yml 配置文件,“resources ” -> “New” -> “File”,输入 application.yml 并回车。为什么是 .yml 后缀呢?其实还有 .yaml.properties 这两种写法,等后面再去比较这几种不同后缀文件的异同。在以后的开发中,还会见到 bootstrap.yml 或 bootstrap.properties 这种配置文件,在此先提一下,此处不和 application.yml 比较异同。
 
新增 application.yml 配置文件
 

3.5.3 application.yml 配置文件参数设置

 
  application.yml 配置文件参数设置如下:
 

#服务端口号
server:
  port: 6001

spring:
  application:
    #服务名称
    name: cloud-payment-service
  #数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/zero2one?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai

mybatis-plus:
  #扫描资源(resources)路径下,mapper目录的所有以 .xml 结尾的mapper文件
  mapper-locations: classpath*:mapper/*.xml
  configuration:
    #下划线转驼峰
    map-underscore-to-camel-case: true

 

3.5.4 创建 PaymentServiceMain6001 引导类

 
  我们在包路径“com.dbydc.zero2one.payment”下新建 PaymentServiceMain6001 引导类
 
创建 PaymentServiceMain6001 引导类

 

package com.dbydc.zero2one.payment;

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

/**
 * Payment支付服务引导类
 * @author 大白有点菜
 * @className PaymentServiceMain6001
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
@SpringBootApplication
@MapperScan("com.dbydc.zero2one.payment.dao")
public class PaymentServiceMain6001 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentServiceMain6001.class, args);
    }
}

 
  注解 @SpringBootApplication 是必须要添加的。@MapperScan 注解的作用是扫描指定包路径(com.dbydc.zero2one.payment.dao)下所有的 dao 接口,如果不在启动类添加注解扫描,那么就要在每个 dao 接口上添加 @Mapper 注解,稍后会做说明。
 
  现在可以直接启动 PaymentServiceMain6001 引导类,正常的话是可以运行的。可以点绿色的三角形图标运行,或者按快捷键 Shift + F10 。正常运行如下图所示。
 
绿色的三角形图标或快捷键 Shift + F10 启动 PaymentServiceMain6001 引导类
 
PaymentServiceMain6001 引导类正常运行
 

3.5.5 创建常用的包(Package)

 
  主要创建这几个常用的包:controllerdao(也有人创建为 mapper)entityservice(此包下还多创建一个 impl 包)。controller 负责后端和前端的数据交互。dao 负责和数据库打交道。entity 是实体层。service 负责处理数据。
 
创建常用的包名
 
创建controller、dao、entity、service等常用包

 

3.5.6 创建 Payment 实体类

 
  在 entity 包路径下新建实体类 Payment
 
entity 包路径下新建实体类 Payment 。

 
  Payment 实体类代码修改如下,使用了 lombok 的 @Data 、@AllArgsConstructor 、@NoArgsConstructor 三个注解,不用写字段的 Getter 和 Setter 属性,代码量大大减少。
 

package com.dbydc.zero2one.payment.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.lang.NonNull;

import java.math.BigDecimal;
import java.util.Date;

/**
 * 支付服务 实体类
 * @author 大白有点菜
 * @className Payment
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment {
    /**
     * CREATE TABLE `tb_payment` (
     *   `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
     *   `order_num` varchar(64) NOT NULL COMMENT '订单号',
     *   `payment_amount` decimal(10,2) DEFAULT NULL COMMENT '支付金额',
     *   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     *   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
     *   PRIMARY KEY (`id`)
     * ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
     */

    /**
     * 自增主键
     */
    private Long id;

    /**
     * 订单号
     */
    @NonNull
    private String orderNum;

    /**
     * 支付金额
     */
    private BigDecimal paymentAmount;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    //@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    /**
     * 修改时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    //@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;
}

 
Payment 实体类代码修改
 

3.5.7 创建 IPaymentService 接口

 
  在 service 包路径下创建接口 IPaymentService 。注意包路径是“com.dbydc.zero2one.payment.service”。
 
在 service 包路径下创建接口 IPaymentService

 
  IPaymentService 接口代码如下,主要包含5个方法:
 

package com.dbydc.zero2one.payment.service;

import com.dbydc.zero2one.payment.entity.Payment;

/**
 * 支付服务 Service接口
 * @author 大白有点菜
 * @className IPaymentService
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
public interface IPaymentService {
    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    Payment getByOrderNum(String orderNum);

    /**
     * 通过主键ID获取数据
     * @param id 主键ID
     * @return
     */
    Payment getById(Long id);

    /**
     * 新增
     * @param payment 支付服务实体对象
     * @return
     */
    int add(Payment payment);

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    int deleteByOrderNum(String orderNum);

    /**
     * 通过主键ID删除数据
     * @param id 主键ID
     * @return
     */
    int deleteById(Long id);
}

 
IPaymentService 接口代码主要包含5个方法
 

3.5.8 创建 PaymentDao 接口

 
  在 dao 包路径下创建接口 PaymentDao
 
dao 包路径下创建接口 PaymentDao
 
  PaymentDao 接口代码如下,注意,如果在启动类已经添加 @MapperScan 注解扫描包路径“com.dbydc.zero2one.payment.dao”,那就不用再添加 @Mapper 注解。否则,一定要添加 @Mapper 注解,不然程序编译会报错!
 
  PaymentDao 接口的方法、参数和 IPaymentService 接口的方法、参数是一致的,只不过多了 @Param 注解,有几个参数,就写多少个@Param注解,假设有个方法 getAll(@Param(“orderNum”) String orderNum, @Param(“id”) Long id) 。
 

package com.dbydc.zero2one.payment.dao;

import com.dbydc.zero2one.payment.entity.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * 支付服务 Dao接口
 * @author 大白有点菜
 * @className PaymentDao
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
//@Mapper //如果不在启动类添加 @MapperScan 注解进行dao包扫描,那么每个Dao接口都需要加上这个 @Mapper 注解
public interface PaymentDao {
    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    Payment getByOrderNum(@Param("orderNum") String orderNum);

    /**
     * 通过主键ID获取数据
     * @param id 主键ID
     * @return
     */
    Payment getById(@Param("id") Long id);

    /**
     * 新增
     * @param payment 支付服务实体对象
     * @return
     */
    int add(@Param("payment") Payment payment);

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    int deleteByOrderNum(@Param("orderNum") String orderNum);

    /**
     * 通过主键ID删除数据
     * @param id 主键ID
     * @return
     */
    int deleteById(@Param("id") Long id);
}

 
PaymentDao 接口代码
 

3.5.9 创建 PaymentServiceImpl 实现类

 
  在 impl 包路径下创建实现类 PaymentServiceImpl 。注意包路径是“com.dbydc.zero2one.payment.service.impl”。
 
impl 包路径下创建实现类 PaymentServiceImpl 。

 
  PaymentServiceImpl 实现类代码如下,实现 IPaymentService 接口实例化 PaymentDao 接口对象有两种方式,添加 @Autowire@Resource 注解,具体使用看代码注意:一定不能遗漏注解 @Service !!!
 

package com.dbydc.zero2one.payment.service.impl;

import com.dbydc.zero2one.payment.dao.PaymentDao;
import com.dbydc.zero2one.payment.entity.Payment;
import com.dbydc.zero2one.payment.service.IPaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;

/**
 * 支付服务 Service实现类
 * @author 大白有点菜
 * @className PaymentServiceImpl
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
@Service
public class PaymentServiceImpl implements IPaymentService {

    /**
     * 写法一:Spring的注解。将对象交由Spring去管理,官方不推荐这种写法。
     */
    //@Autowired
    //private PaymentDao paymentDao;

    private PaymentDao paymentDao;
    /**
     * 写法二:Spring的注解。Setter方式注入对象,官方推荐这种写法。
     * @param paymentDao
     */
    @Autowired
    public void setPaymentDao(PaymentDao paymentDao) {
        this.paymentDao = paymentDao;
    }

    /**
     * 写法三:Java的注解。全称javax.annotation.Resource,它属于JSR-250规范的一个注解,包含Jakarta EE(J2EE)中。
     */
    //@Resource
    //private PaymentDao paymentDao;

    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    @Override
    public Payment getByOrderNum(String orderNum) {
        return paymentDao.getByOrderNum(orderNum);
    }

    /**
     * 通过主键ID获取数据
     * @param id 主键ID
     * @return
     */
    @Override
    public Payment getById(Long id) {
        return paymentDao.getById(id);
    }

    /**
     * 新增
     * @param payment 支付服务实体对象
     * @return
     */
    @Override
    public int add(Payment payment) {
        Date currentTime = new Date();
        payment.setCreateTime(currentTime);
        payment.setUpdateTime(currentTime);
        return paymentDao.add(payment);
    }

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    @Override
    public int deleteByOrderNum(String orderNum) {
        return paymentDao.deleteByOrderNum(orderNum);
    }

    /**
     * 通过主键ID删除数据
     * @param id 主键ID
     * @return
     */
    @Override
    public int deleteById(Long id) {
        return paymentDao.deleteById(id);
    }
}

 
PaymentServiceImpl 实现类代码
 
  顺便说一下,IDEA 工具会对 paymentDao 对象提示这种错误:Could not autowire. No beans of 'PaymentDao' type found. 为什么会这样呢?其实是没有在 Dao 接口类中添加 @Mapper 注解导致,只要在启动类加了 @MapperScan 注解,编译时就不会报错,只是 IDEA 工具的语法检查识别问题。
 
paymentDao 对象提示错误
 

3.5.10 创建 PaymentMapper.xml 文件

 
  先在 resources 资源目录下新建目录 mapper,再创建xml文件 PaymentMapper.xml
 
先在 resources 资源目录下新建目录 mapper,再创建xml文件 PaymentMapper.xml 。
 
  再将以下模板内容复制到 PaymentMapper.xml 文件中。
 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="">

</mapper>

 
  修改 PaymentMapper.xml 文件的内容。首先复制 PaymentDao 接口的路径引用,选中 PaymentDao 并右键,点“Copy Path”,选择最后的“Copy Reference”,然后粘贴到 mapper 标签的 namespace 属性里
 
复制 PaymentDao 接口的路径引用,选中 PaymentDao 并右键,点“Copy Path”
 
选择最后的“Copy Reference”,然后粘贴到 mapper 标签的 namespace 属性里
 
mapper 标签的 namespace 属性的值
 
  PaymentMapper.xml 文件代码如下:
 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dbydc.zero2one.payment.dao.PaymentDao">

    <resultMap id="paymentMap" type="com.dbydc.zero2one.payment.entity.Payment">
        <id property="id" column="id" jdbcType="BIGINT"/>
        <result property="orderNum" column="order_num" jdbcType="VARCHAR"/>
        <result property="paymentAmount" column="payment_amount" jdbcType="DECIMAL"/>
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
        <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
    </resultMap>

    <!-- 通过订单号获取数据 -->
    <select id="getByOrderNum" parameterType="string" resultMap="paymentMap">
        SELECT id, order_num, payment_amount, create_time, update_time
        FROM tb_payment
        <where>
            <if test="orderNum != null">
                order_num = #{orderNum}
            </if>
        </where>
    </select>

    <!-- 通过主键ID获取数据 -->
    <select id="getById" parameterType="long" resultMap="paymentMap">
        SELECT id, order_num, payment_amount, create_time, update_time
        FROM tb_payment
        <where>
            <if test="id != null">
                id = #{id}
            </if>
        </where>
    </select>

    <!-- 新增 -->
    <insert id="add" parameterType="com.dbydc.zero2one.payment.entity.Payment">
        INSERT INTO tb_payment (
            order_num,
            payment_amount,
            create_time,
            update_time
        )
        VALUES (
            #{payment.orderNum},
            #{payment.paymentAmount},
            #{payment.createTime},
            #{payment.updateTime}
        )
    </insert>

    <!-- 通过订单号删除数据 -->
    <delete id="deleteByOrderNum" parameterType="string">
        DELETE FROM tb_payment
        <where>
            <if test="orderNum != null">
                order_num = #{orderNum}
            </if>
        </where>
    </delete>

    <!-- 通过主键ID删除数据 -->
    <delete id="deleteById" parameterType="long">
        DELETE FROM tb_payment
        <where>
            <if test="id != null">
                id = #{id}
            </if>
        </where>
    </delete>
</mapper>

 

3.5.11 创建 PaymentController 业务类

 
  在 controller 包路径下创建业务类 PaymentController
 
在 controller 包路径下创建业务类 PaymentController 。
 
  PaymentController 业务类代码如下:
 

package com.dbydc.zero2one.payment.controller;

import com.dbydc.zero2one.common.enums.ResponseCodeEnum;
import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.payment.entity.Payment;
import com.dbydc.zero2one.payment.service.IPaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * 支付服务 Controller业务类
 * @author 大白有点菜
 * @className PaymentController
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
@RestController
@RequestMapping("payment")
public class PaymentController {

    @Autowired
    private IPaymentService paymentService;

    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/queryByOrderNum/{orderNum}")
    public ResponseData queryByOrderNum(@PathVariable String orderNum) {
        Payment payment = paymentService.getByOrderNum(orderNum);
        return payment == null ? ResponseData.success(ResponseCodeEnum.NULL_DATA.getCode())
                : ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), payment);
    }

    /**
     * 通过主键ID获取数据
     * @param id 主键ID
     * @return
     */
    @GetMapping("/queryById/{id}")
    public ResponseData queryById(@PathVariable Long id) {
        Payment payment = paymentService.getById(id);
        return payment == null ? ResponseData.success(ResponseCodeEnum.NULL_DATA.getCode())
                : ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), payment);
    }

    /**
     * 新增
     * @param payment 支付服务实体对象
     * @return
     */
    @PostMapping("/add")
    public ResponseData add(@RequestBody @Validated Payment payment) {
        int addResult = paymentService.add(payment);
        return addResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), null, "新增数据成功")
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), "新增数据失败");
    }

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/deleteByOrderNum/{orderNum}")
    public ResponseData deleteByOrderNum(@PathVariable String orderNum) {
        int deleteResult = paymentService.deleteByOrderNum(orderNum);
        return deleteResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), null, "删除数据成功")
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), "删除数据失败");
    }

    /**
     * 通过主键ID删除数据
     * @param id 主键ID
     * @return
     */
    @GetMapping("/deleteById/{id}")
    public ResponseData deleteById(@PathVariable Long id) {
        int deleteResult = paymentService.deleteById(id);
        return deleteResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), null, "删除数据成功")
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), "删除数据失败");
    }
}

 
  PaymentController 业务类大概说一下其中的注解:

1、@RestController:@Controller + @ResponseBody 。一个注解顶两个注解,以前就是写两个注解。@Controller 注解允许通过类路径扫描自动检测实现类。@ResponseBody 注解指示方法返回值的批注应绑定到 Web 响应正文。
 
2、@GetMapping:用于将 HTTP GET 请求映射到特定处理程序方法的注释。等价于 @RequestMapping(method = RequestMethod.GET)。
 
3、@PostMapping:用于将 HTTP POST 请求映射到特定处理程序方法的注释。等价于 @RequestMapping(method = RequestMethod.POST)。
 
4、@PathVariable:指示方法参数应绑定到 URI 模板变量的注释。请求地址中的参数变量需要用“{}”大括号括起来。
 
5、@RequestBody:指示方法参数的注释应绑定到 Web 请求的正文。请求的正文通过 以 HttpMessageConverter 解析方法参数,具体取决于请求的内容类型。或者,可以通过使用 对 @Valid 参数进行批注来应用自动验证。这个注解使用很灵活,也很常用,支持同时使用 @Validated 注解进行校验。方法入参支持实体类,支持 Map 类等等,主要在 POST 方式中使用较多,Get 方式也支持。
 
6、@Validated:JSR-303的 javax.validation.Valid的变体,支持验证组的规范。

 
  我们在实体类指定字段添加符合 JSR-303 校验规范的注解,如 @NonNull 注解,在 Controller 业务类方法的形式参数中添加 @Validated 注解,即可实现检验的功能。有些第三方工具的检验注解是 @NotNull@NotEmpty@NotBlank ,在工作中会接触很多,而且很好用,可以设置字符串信息,例如一些中文提示。如图,若传过来的 orderNum 为 null ,那么就会报错。
 
Payment实体类的 orderNum 字段添加 @NonNull 校验注解
 
PaymentController业务类的 add 方法形式参数中添加 @Validated 注解
 

3.5.12 使用 Postman 或 浏览器 测试接口是否正常

 
1、测试接口1新增数据。对应的方法:public ResponseData add(@RequestBody @Validated Payment payment);

接口:localhost:6001/payment/add
提交方式:post
使用工具:Postman

 
  可以看到,加了 @Validated 和 @NonNull 注解后,如果字段 orderNum 的传值为 null ,返回的的是错误提示。此处请求体内容使用 JSON 格式
 
加了 @Validated 和 @NonNull 注解后,如果字段 orderNum 的传值为 null ,返回的的是错误提示
 
  正常的请求体内容和成功返回结果:
 
正常的请求体内容和成功返回结果

 
2、测试接口2通过订单号获取数据。对应的方法:public ResponseData queryByOrderNum(@PathVariable String orderNum);

接口:localhost:6001/payment/queryByOrderNum/dbydc666
提交方式:get
使用工具:Postman 或 浏览器

 
  使用 Postman 或 浏览器都可以,不需要使用 JSON 格式的请求体,只需在请求地址中输入正确的订单号即可。
 
使用 GET 请求,通过订单号获取数据

 
使用 浏览器 请求,通过订单号获取数据

 
3、测试接口3通过主键ID获取数据。对应的方法:public ResponseData queryById(@PathVariable Long id);

接口:localhost:6001/payment/queryById/1
提交方式:get
使用工具:Postman 或 浏览器

 
  使用 Postman 或 浏览器都可以,不需要使用 JSON 格式的请求体,只需在请求地址中输入正确的主键ID即可。
 
使用 GET 请求,通过主键ID获取数据

 
使用 浏览器 请求,通过主键ID获取数据

 
4、测试接口4通过订单号删除数据。对应的方法:public ResponseData deleteByOrderNum(@PathVariable String orderNum);

接口:localhost:6001/payment/deleteByOrderNum/a123
提交方式:get
使用工具:Postman 或 浏览器

 
  使用 Postman 或 浏览器都可以,不需要使用 JSON 格式的请求体,只需在请求地址中输入正确的订单号即可。数据库存在 order_num = “a123” 这条数据,当提交删除请求后,只剩下两条数据,order_num = “a123” 这条数据被删除了。
 
数据库存在 order_num = "a123" 这条数据

 
使用 GET 请求,通过订单号删除数据

 
只剩下两条数据,order_num = "a123" 这条数据被删除了

 
5、测试接口5通过主键ID删除数据。对应的方法:public ResponseData deleteById(@PathVariable Long id);

接口:localhost:6001/payment/deleteById/3
提交方式:get
使用工具:Postman 或 浏览器

 
  使用 Postman 或 浏览器都可以,不需要使用 JSON 格式的请求体,只需在请求地址中输入正确的主键ID即可。数据库存在 id = 3 这条数据,当提交删除请求后,只剩下一条数据,id = 3 这条数据被删除了。
 
使用 GET 请求,通过主键ID删除数据
 
只剩下一条数据,id = 3 这条数据被删除了

 
  至此,所有接口都是正常的,结束了吗?当然没有啦,只有一个服务不能称之为微服务,还得创建另外一个服务,服务之间远程调用接口还没说呢!除了使用 Postman 或 浏览器 直接请求地址进行测试接口外,还可以使用 @Test 注解进行测试。
 

3.5.13 使用 MockMvc 类 和 @Test 注解测试 Controller 的接口是否正常

 
  在“src”目录下,有两个目录:main 和 test 。我们在 main 目录里面写业务代码,而 test 目录可以写一些代码进行测试。
 
在“src”目录下,有两个目录:main 和 test 。

 
  新建 Zero2OneTest 类,包路径是“com.dbydc.zero2one.test”。
 
新建 Zero2OneTest 类,包路径是“com.dbydc.zero2one.test”。
 
  Zero2OneTest 测试类的简单代码,主要先看看 @Test 注解是否生效:
 

package com.dbydc.zero2one.test;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

/**
 * 测试类
 * @author 大白有点菜
 * @className Zero2OneTest
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class Zero2OneTest {

    @Test
    public void contextLoads() {
        System.out.println("大白有点菜");
    }
}

 
  一运行,发现立马报错了:java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test。经过排查,原来是包名不一致所致(main 目录是“com.dbydc.zero2one.payment”,test 目录写的是“com.dbydc.zero2one.test”),经过修改 test 目录的包路径,最后能正常运行。
 
包名不一致导致Test运行报错
 
修改 test 目录的包路径,最后能正常运行
 
  Spring框架的 Test 模块是很强大的,如果有兴趣,读者可以去研究一下:https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html
 
  注意:有些博客是添加 @RunWith 和 @SpringBootTest 这两个注解,@RunWith 是 JUnit 4 的写法。但我们使用的是 Spring Boot 2.6.14,若还这样写,是识别不出来的,因为已经不支持 JUnit 4 了,仅支持更高版本的 JUnit 5。
 
  有些博客写到同时添加 @ExtendWith(SpringExtension.class) 和 @SpringBootTest 这两个注解。 在高版本(如 Spring Boot 2.6.14)中,只需使用一个注解 @SpringBootTest 就行! 在 Zero2OneTest 测试类中,我添加的 @ExtendWith(SpringExtension.class) 是多余的。
 
请添加图片描述

 
  此篇博客 《Spring Boot Test单元测试——Junit4、Junit5区别与@ExtendWith不识别生效问题解析》 介绍得比较详细,里面涉及的版本说法不能保证一定正确,我还没找到官方的权威文档去验证。
 
  重新创建了两个测试类:Zero2OneTest1Zero2OneTest2 。两个类的代码写法不太一样,但结果是一样的。
 

package com.dbydc.zero2one.payment;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;


/**
 * 测试类1
 * @author 大白有点菜
 * @className Zero2OneTest1
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
@SpringBootTest
@AutoConfigureMockMvc
public class Zero2OneTest1 {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void contextLoads() throws Exception {
        //System.out.println("大白有点菜");
        MvcResult result = mockMvc.perform(
                MockMvcRequestBuilders.get("/payment/queryByOrderNum/{orderNum}", "dbydc666")
//                MockMvcRequestBuilders.get("/payment/queryByOrderNum")
//                    .param("orderNum", "dbydc666")
                    .contentType(MediaType.APPLICATION_JSON))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
}

 

package com.dbydc.zero2one.payment;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

/**
 * 测试类2
 * @author 大白有点菜
 * @className Zero2OneTest2
 * @date 2023-04-01 14:42
 * @description
 * @since 1.0
 **/
@SpringBootTest
public class Zero2OneTest2 {

    private MockMvc mockMvc;

    @Autowired
    WebApplicationContext wac;

    @BeforeEach
    public void beforeSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    public void contextLoads() throws Exception {
        //System.out.println("大白有点菜");
        MvcResult result = mockMvc.perform(
                MockMvcRequestBuilders.get("/payment/queryByOrderNum/{orderNum}", "dbydc666")
//                MockMvcRequestBuilders.get("/payment/queryByOrderNum")
//                    .param("orderNum", "dbydc666")
                    .contentType(MediaType.APPLICATION_JSON))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        System.out.println(result.getResponse().getContentAsString());
    }
}

 
  一定要先运行 PaymentServiceMain6001 引导类,不运行无法调用接口,再运行测试方法。运行测试方法很简单,在方法上鼠标右键,选择“Run xxx()”即可。如图所示,MockHttpServletResponse 的 “Status” 是 200 ,“Body”有具体的内容。
 
运行测试方法

 
使用MockMvc调用 Controller 的接口正常
 
  特别要注意!!!,GET方式请求 “localhost:6001/payment/queryByOrderNum/dbydc666” 这种带参数值的地址,正确写法是:MockMvcRequestBuilders.get("/payment/queryByOrderNum/{orderNum}", "dbydc666")不能写成 MockMvcRequestBuilders.get(“/payment/queryByOrderNum”).param(“orderNum”, “dbydc666”) 这种形式,不然返回的MockHttpServletResponse 中的“Status”会一直是 404 。404,是一种HTTP状态码,指网页或文件未找到。
 
MockHttpServletResponse 中的“Status”会一直是 404 。
 

3.6 新建 cloud-order-service8001 子模块

 
  cloud-order-service8001 子模块创建过程和 cloud-payment-service6001 子模块是一样的,这里不再详说,直接贴出主要文件的代码,某些方法的返回值也不一样,可以好好体会一下功能实现的多变性。创建好的子模块如图所示:
 
cloud-order-service8001 子模块新建

 
cloud-order-service8001 子模块结构

 

3.6.1 修改 cloud-order-service8001 子模块的 pom.xml 文件

 
  修改 cloud-order-service8001 子模块的 pom.xml 文件,如下所示,只需引入公共API cloud-common-api 即可完事。
 

<?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>SpringCloudZero2One</artifactId>
        <groupId>com.dbydc.zero2one</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-order-service8001</artifactId>

    <dependencies>
        <!-- 引入公共API cloud-common-api 依赖即可 -->
        <dependency>
            <groupId>com.dbydc.zero2one</groupId>
            <artifactId>cloud-common-api</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

</project>

 

3.6.2 新建 application.yml 配置文件并设置参数

 
  application.yml 配置文件参数设置如下:
 

#服务端口号
server:
  port: 8001

spring:
  application:
    #服务名称
    name: cloud-order-service
  #数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/zero2one?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai

mybatis-plus:
  #扫描资源(resources)路径下,mapper目录的所有以 .xml 结尾的mapper文件
  mapper-locations: classpath*:mapper/*.xml
  configuration:
    #下划线转驼峰
    map-underscore-to-camel-case: true

 

3.6.3 创建 OrderServiceMain8001 引导类

 
  我们在包路径“com.dbydc.zero2one.order”下新建 OrderServiceMain8001 引导类
 

package com.dbydc.zero2one.order;

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

/**
 * Order订单服务引导类
 * @author 大白有点菜
 * @className OrderServiceMain8001
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
@SpringBootApplication
@MapperScan("com.dbydc.zero2one.order.dao")
public class OrderServiceMain8001 {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceMain8001.class, args);
    }
}

 

3.6.4 创建 Order 实体类

 
  在 entity 包路径下新建实体类 Order
 

package com.dbydc.zero2one.order.entity;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.lang.NonNull;

import java.util.Date;

/**
 * 订单服务 实体类
 * @author 大白有点菜
 * @className Order
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {

    /**
     * CREATE TABLE `tb_order` (
     *   `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
     *   `order_num` varchar(64) NOT NULL COMMENT '订单号',
     *   `user_name` varchar(128) DEFAULT NULL COMMENT '用户名',
     *   `user_phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户手机号',
     *   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     *   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
     *   PRIMARY KEY (`id`)
     * ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
     */

    /**
     * 自增主键
     */
    private Long id;

    /**
     * 订单号
     */
    @NonNull
    private String orderNum;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 用户手机号
     */
    private String userPhone;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;

    /**
     * 修改时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
}

 

3.6.5 创建 IOrderService 接口

 
  在 service 包路径下创建接口 IOrderService 。注意包路径是“com.dbydc.zero2one.order.service”。
 

package com.dbydc.zero2one.order.service;

import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.order.entity.Order;

/**
 * 订单服务 Service接口
 * @author 大白有点菜
 * @className IOrderService
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
public interface IOrderService {
    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    ResponseData getByOrderNum(String orderNum);

    /**
     * 通过用户手机号获取批量数据
     * @param userPhone 用户手机号
     * @return
     */
    ResponseData getListByPhone(String userPhone);

    /**
     * 新增
     * @param order 订单服务实体对象
     * @return
     */
    ResponseData add(Order order);

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    ResponseData deleteByOrderNum(String orderNum);

    /**
     * 通过订单号修改数据
     * @param order 订单服务实体对象
     * @return
     */
    ResponseData updateByOrderNum(Order order);
}

 

3.6.6 创建 OrderDao 接口

 
  在 dao 包路径下创建接口 OrderDao
 

package com.dbydc.zero2one.order.dao;

import com.dbydc.zero2one.order.entity.Order;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 订单服务 Dao接口
 * @author 大白有点菜
 * @className OrderDao
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
//@Mapper //如果不在启动类添加 @MapperScan 注解进行dao包扫描,那么每个Dao接口都需要加上这个 @Mapper 注解
public interface OrderDao {

    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    Order getByOrderNum(@Param("orderNum") String orderNum);

    /**
     * 通过用户手机号获取批量数据
     * @param userPhone 用户手机号
     * @return
     */
    List<Order> getListByPhone(@Param("userPhone") String userPhone);

    /**
     * 新增
     * @param order 订单服务实体对象
     * @return
     */
    int add(@Param("order") Order order);

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    int deleteByOrderNum(@Param("orderNum") String orderNum);

    /**
     * 通过订单号修改数据
     * @param order 订单服务实体对象
     * @return
     */
    int updateByOrderNum(@Param("order") Order order);
}

 

3.6.7 创建 OrderServiceImpl 实现类

 
  在 impl 包路径下创建实现类 OrderServiceImpl 。注意包路径是“com.dbydc.zero2one.order.service.impl”。一定不能遗漏注解 @Service !
 

package com.dbydc.zero2one.order.service.impl;

import com.dbydc.zero2one.common.enums.ResponseCodeEnum;
import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.order.dao.OrderDao;
import com.dbydc.zero2one.order.entity.Order;
import com.dbydc.zero2one.order.service.IOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;
import java.util.Objects;

/**
 * 订单服务 Service实现类
 * @author 大白有点菜
 * @className OrderServiceImpl
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
@Service
public class OrderServiceImpl implements IOrderService {

    /**
     * 写法一:Spring的注解。将对象交由Spring去管理,官方不推荐这种写法。
     */
    @Autowired
    private OrderDao orderDao;

//    private OrderDao orderDao;
//    /**
//     * 写法二:Spring的注解。Setter方式注入对象,官方推荐这种写法。
//     * @param orderDao
//     */
//    @Autowired
//    public void setOrderDao(OrderDao orderDao) {
//        this.orderDao = orderDao;
//    }

    /**
     * 写法三:Java的注解。全称javax.annotation.Resource,它属于JSR-250规范的一个注解,包含Jakarta EE(J2EE)中。
     */
    //@Resource
    //private OrderDao orderDao;

    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    @Override
    public ResponseData getByOrderNum(String orderNum) {
        Order order = orderDao.getByOrderNum(orderNum);
        return order == null ? ResponseData.success(ResponseCodeEnum.NULL_DATA.getCode())
                : ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), order);
    }

    /**
     * 通过用户手机号获取批量数据
     * @param userPhone 用户手机号
     * @return
     */
    @Override
    public ResponseData getListByPhone(String userPhone) {
        List<Order> orderList = orderDao.getListByPhone(userPhone);
        return (orderList == null || orderList.size() == 0) ? ResponseData.success(ResponseCodeEnum.NULL_DATA.getCode())
                : ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), orderList);
    }

    /**
     * 新增
     * @param order 订单服务实体对象
     * @return
     */
    @Override
    public ResponseData add(Order order) {
        Date date = new Date();
        order.setCreateTime(date);
        order.setUpdateTime(date);
        int addResult = orderDao.add(order);
        return addResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), "新增数据成功")
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), "新增数据失败");
    }

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    @Override
    public ResponseData deleteByOrderNum(String orderNum) {
        int deleteResult = orderDao.deleteByOrderNum(orderNum);
        return deleteResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), "删除数据成功")
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), "删除数据失败");
    }

    /**
     * 通过订单号修改数据
     * @param order 订单服务实体对象
     * @return
     */
    @Override
    public ResponseData updateByOrderNum(Order order) {
        if (Objects.nonNull(order)) {
            order.setUpdateTime(new Date());
        }
        int updateResult = orderDao.updateByOrderNum(order);
        return updateResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), "修改数据成功")
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), "修改数据失败");
    }
}

 

3.6.8 创建 OrderMapper.xml 文件

 
  先在 resources 资源目录下新建目录 mapper,再创建xml文件 OrderMapper.xml
 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dbydc.zero2one.order.dao.OrderDao">

    <resultMap id="orderMap" type="com.dbydc.zero2one.order.entity.Order">
        <id property="id" column="id" jdbcType="BIGINT"/>
        <result property="orderNum" column="order_num" jdbcType="VARCHAR"/>
        <result property="userName" column="user_name" jdbcType="VARCHAR"/>
        <result property="userPhone" column="user_phone" jdbcType="VARCHAR"/>
        <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
        <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
    </resultMap>

    <!-- 通过订单号获取数据 -->
    <select id="getByOrderNum" parameterType="string" resultMap="orderMap">
        SELECT id, order_num, user_name, user_phone, create_time, update_time
        FROM tb_order
        <where>
            <if test="orderNum != null">
                order_num = #{orderNum}
            </if>
        </where>
    </select>

    <!-- 通过用户手机号获取批量数据 -->
    <select id="getListByPhone" parameterType="string" resultMap="orderMap">
        SELECT id, order_num, user_name, user_phone, create_time, update_time
        FROM tb_order
        <where>
            <if test="userPhone != null and userPhone != ''">
                user_phone = #{userPhone}
            </if>
        </where>
    </select>

    <!-- 新增 -->
    <insert id="add" parameterType="com.dbydc.zero2one.order.entity.Order">
        INSERT INTO tb_order (
            order_num,
            user_name,
            user_phone,
            create_time,
            update_time
        )
        VALUES (
            #{order.orderNum},
            #{order.userName},
            #{order.userPhone},
            #{order.createTime},
            #{order.updateTime}
        )
    </insert>

    <!-- 通过订单号删除数据 -->
    <delete id="deleteByOrderNum" parameterType="string">
        DELETE FROM tb_order
        <where>
            <if test="orderNum != null">
                order_num = #{orderNum}
            </if>
        </where>
    </delete>

    <!-- 通过订单号修改数据 -->
    <update id="updateByOrderNum" parameterType="com.dbydc.zero2one.order.entity.Order">
        UPDATE tb_order
        SET user_name = #{order.userName}
        <where>
            <if test="order.OrderNum != null">
                order_num = #{order.orderNum}
            </if>
        </where>
    </update>

</mapper>

 

3.6.9 创建 OrderController 业务类

 
  在 controller 包路径下创建业务类 OrderController
 

package com.dbydc.zero2one.order.controller;

import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.order.entity.Order;
import com.dbydc.zero2one.order.service.IOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * 订单服务 Controller业务类
 * @author 大白有点菜
 * @className OrderController
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
@RestController
@RequestMapping("order")
public class OrderController {

    @Autowired
    private IOrderService orderService;

    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/query/{orderNum}")
    public ResponseData queryByOrderNum(@PathVariable("orderNum") String orderNum) {
        return orderService.getByOrderNum(orderNum);
    }

    /**
     * 通过用户手机号获取批量数据
     * @param userPhone 用户手机号
     * @return
     */
    @GetMapping("/queryList/{userPhone}")
    public ResponseData queryListByPhone(@PathVariable("userPhone") String userPhone) {
        return orderService.getListByPhone(userPhone);
    }

    /**
     * 新增
     * @param order 订单服务实体对象
     * @return
     */
    @PostMapping("/add")
    public ResponseData add(@RequestBody @Validated Order order) {
        return orderService.add(order);
    }

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/delete/{orderNum}")
    public ResponseData deleteByOrderNum(@PathVariable("orderNum") String orderNum) {
        return orderService.deleteByOrderNum(orderNum);
    }

    /**
     * 通过订单号修改数据
     * @param order 订单服务实体对象
     * @return
     */
    @PostMapping("/update")
    public ResponseData updateByOrderNum(@RequestBody Order order) {
        return orderService.updateByOrderNum(order);
    }
}

 

3.6.10 使用 Postman 测试接口是否正常

 
1、测试接口1新增数据。对应的方法:public ResponseData queryByOrderNum(@PathVariable(“orderNum”) String orderNum);

接口:localhost:8001/order/add
提交方式:post
使用工具:Postman

 
  使用四组不同的数据进行新增操作:
 

{
    "orderNum" : "a111",
    "userName" : "大白真不菜",
    "userPhone" : "111111"
}
{
    "orderNum" : "b222",
    "userName" : "大白真不菜",
    "userPhone" : "111111"
}
{
    "orderNum" : "c333",
    "userName" : "大白好菜啊",
    "userPhone" : "222222"
}
{
    "orderNum" : "d444",
    "userName" : "大白好菜啊",
    "userPhone" : "222222"
}

 
  测试结果如下。
 
新增数据成功

 
2、测试接口2通过订单号获取数据。对应的方法:public ResponseData queryByOrderNum(@PathVariable(“orderNum”) String orderNum);

接口:localhost:8001/order/query/a111
提交方式:get
使用工具:Postman 或 浏览器

 
  使用 Postman 或 浏览器都可以,不需要使用 JSON 格式的请求体,只需在请求地址中输入正确的订单号即可。
 
使用 GET 请求,通过订单号获取数据

 
3、测试接口3通过用户手机号获取批量数据。对应的方法:public ResponseData queryListByPhone(@PathVariable(“userPhone”) String userPhone);

接口:localhost:8001/order/queryList/111111
提交方式:get
使用工具:Postman 或 浏览器

 
  使用 Postman 或 浏览器都可以,不需要使用 JSON 格式的请求体,只需在请求地址中输入正确的手机号即可。
 
使用 GET 请求,通过用户手机号获取批量数据

 
4、测试接口4通过订单号删除数据。对应的方法:public ResponseData deleteByOrderNum(@PathVariable(“orderNum”) String orderNum);

接口:localhost:8001/order/delete/d444
提交方式:get
使用工具:Postman 或 浏览器

 
  使用 Postman 或 浏览器都可以,不需要使用 JSON 格式的请求体,只需在请求地址中输入正确的订单号即可。
 
使用 GET 请求,通过订单号删除数据

 
5、测试接口5通过订单号修改数据。对应的方法:public ResponseData updateByOrderNum(@RequestBody Order order);

接口:localhost:8001/order/update
提交方式:post
使用工具:Postman

 
使用 POST 请求,通过订单号修改数据

 

3.7 使用 RestTemplate 进行同步地远程调用 HTTP 接口

 
  如果还有公司使用 RestTemplate 来进行远程调用接口,那可以确定那公司来自远古时代(说笑的,并不过时),主流基本都是使用 OpenFeign。我们来玩玩微服务之间如何使用 RestTemplate 进行同步的远程调用。OpenFeign 底层使用的是 RestTemplate ,经过封装后,使用更加方便。
 
  Spring官方API文档介绍 RestTemplate 类:https://docs.spring.io/spring-framework/docs/5.3.26/javadoc-api/
 

  Synchronous client to perform HTTP requests, exposing a simple, template method API over underlying HTTP client libraries such as the JDK HttpURLConnection, Apache HttpComponents, and others. RestTemplate offers templates for common scenarios by HTTP method, in addition to the generalized exchange and execute methods that support of less frequent cases.
  执行 HTTP 请求的同步客户端,通过底层 HTTP 客户端库(例如 JDK HttpURLConnection、Apache HttpComponents 等)公开一个简单的模板方法 API。 RestTemplate 除了支持不太频繁的情况的通用交换和执行方法之外,还通过 HTTP 方法为常见场景提供模板。【谷歌翻译】
 
  RestTemplate is typically used as a shared component. However, its configuration does not support concurrent modification, and as such its configuration is typically prepared on startup. If necessary, you can create multiple, differently configured RestTemplate instances on startup. Such instances may use the same the underlying ClientHttpRequestFactory if they need to share HTTP client resources.
  RestTemplate 通常用作共享组件。但是,它的配置不支持并发修改,因此它的配置通常是在启动时准备的。如有必要,您可以在启动时创建多个不同配置的 RestTemplate 实例。如果这些实例需要共享 HTTP 客户端资源,则它们可以使用相同的底层 ClientHttpRequestFactory。【谷歌翻译】
 
  NOTE: As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.
  注意:从 5.0 开始,这个类处于维护模式,只有少量的更改请求和错误被接受。请考虑使用 org.springframework.web.reactive.client.WebClient,它具有更现代的 API 并支持同步、异步和流式场景。【谷歌翻译】

 
  新建一个 config 目录,创建 RestTemplateConfig 配置类。
 
新建一个 config 目录,创建 RestTemplateConfig 配置类。
 
  RestTemplateConfig 配置类主要代码:
 

package com.dbydc.zero2one.order.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
 * RestTemplate配置类
 * @author 大白有点菜
 * @className RestTemplateConfig
 * @date 2023-04-01
 * @description
 * @since 1.0
 **/
@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

 
  修改 OrderController 的代码,新增方法 remoteQueryByOrderNumPAYMENT_URL 静态变量,用来进行远程调用 Payment 服务
 

public static final String PAYMENT_URL = "http://localhost:6001";

@Autowired
private RestTemplate restTemplate;
/**
 * 通过订单号远程调用支付服务的接口并获取数据
 * @param orderNum 订单号
 * @return
 */
@GetMapping("/remoteQuery/{orderNum}")
public ResponseData remoteQueryByOrderNum(@PathVariable("orderNum") String orderNum) {
    return restTemplate.getForObject(PAYMENT_URL + "/payment/queryByOrderNum/" + orderNum, ResponseData.class);
}

 
新增 PAYMENT_URL 静态变量和定义 RestTemplate 变量
 
新增 remoteQueryByOrderNum 方法
 
  Order 服务 和 Payment 服务同时运行起来,使用 Postman 以 GET 方式调用接口“localhost:8001/order/remoteQuery/dbydc666”,数据能正常获取。
 
使用 Postman 调用接口“localhost:8001/order/remoteQuery/dbydc666”,数据能正常获取
 

3.8 使用 OpenFeign 进行服务之间接口调用

 
  Feign 是一个声明式的 Web 服务客户端,它使编写 Web 服务客户端变得更加容易。OpenFeign 进一步封装了 RestTemplate ,只需要使用注解就可以实现 RestTemplate 的功能。
 
  【Spring Cloud OpenFeign官方文档】:
  https://docs.spring.io/spring-cloud-openfeign/docs/3.1.6/reference/html/
 

3.8.1 新建服务端子模块 cloud-payment-service6002 并改造代码

 
  新建一个服务端的子模块 cloud-payment-service6002 ,用于测试 Feign 的远程调用。项目结构和 cloud-payment-service6001 子模块基本一致,有几个类的代码需要改造优化,cloud-payment-service6001 设计上有些小问题。
 
  几个内容不变或改动很小的文件:Payment(支付实体类)、PaymentDao(支付Dao接口)、PaymentMapper.xml(Mybatis操作SQL的映射文件)、application.yml(配置文件)。
 
  配置文件 application.yml 只需将 server.port 修改为 6002 即可,其它内容不变。
 
  IPaymentService 接口所有抽象方法的返回值都全部改为 ResponseData ,这是优化之一,为什么这么改呢?因为复杂的逻辑处理都在 PaymentServiceImpl 实现类中完成,不要在 Controller 业务类去做逻辑处理。IPaymentService 接口代码如下:
 

package com.dbydc.zero2one.payment.service;

import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.payment.entity.Payment;

/**
 * 支付服务 Service接口
 * @author 大白有点菜
 * @className IPaymentService
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
public interface IPaymentService {
    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    ResponseData getByOrderNum(String orderNum);

    /**
     * 通过主键ID获取数据
     * @param id 主键ID
     * @return
     */
    ResponseData getById(Long id);

    /**
     * 新增
     * @param payment 支付服务实体对象
     * @return
     */
    ResponseData add(Payment payment);

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    ResponseData deleteByOrderNum(String orderNum);

    /**
     * 通过主键ID删除数据
     * @param id 主键ID
     * @return
     */
    ResponseData deleteById(Long id);
}

 
  在公共模块 cloud-common-api 中的 enums 包下新增一个操作状态枚举类 OperateStatusEnum 。代码如下:
 

package com.dbydc.zero2one.common.enums;

/**
 * 操作状态 枚举类
 * @author 大白有点菜
 * @className OperateStatusEnum
 * @date 2023-04-02
 * @description
 * @since 1.0
 **/
public enum OperateStatusEnum {
    /**
     * 新增成功:1
     */
    ADD_SUCCESS(1, "新增数据成功"),
    /**
     * 新增失败:-1
     */
    ADD_FAIL(-1, "新增数据失败"),
    /**
     * 删除成功:2
     */
    DELETE_SUCCESS(2, "删除数据成功"),
    /**
     * 删除失败:-2
     */
    DELETE_FAIL(-2, "删除数据失败"),
    /**
     * 修改成功:3
     */
    UPDATE_SUCCESS(3, "修改数据成功"),
    /**
     * 修改失败:-3
     */
    UPDATE_FAIL(-3, "修改数据失败");

    /**
     * 操作状态码
     */
    private final Integer code;
    /**
     * 操作信息
     */
    private final String message;

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    OperateStatusEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    /**
     * 通过操作状态码获取操作信息
     * @param code 操作状态码
     * @return
     */
    public static String getMessageByCode(Integer code) {
        OperateStatusEnum operateStatusEnum = getEnumByCode(code);
        return operateStatusEnum == null ? null : operateStatusEnum.getMessage();
    }

    /**
     * 通过操作状态码获取ResponseCodeEnum对象
     * @param code 操作状态码
     * @return
     */
    public static OperateStatusEnum getEnumByCode(Integer code) {
        if (code == null) {
            return null;
        }
        OperateStatusEnum[] values = OperateStatusEnum.values();
        for (OperateStatusEnum value : values) {
            if (value.getCode().equals(code)) {
                return value;
            }
        }
        return null;
    }
}

 
  PaymentServiceImpl 实现类代码
 

package com.dbydc.zero2one.payment.service.impl;

import com.dbydc.zero2one.common.enums.OperateStatusEnum;
import com.dbydc.zero2one.common.enums.ResponseCodeEnum;
import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.payment.dao.PaymentDao;
import com.dbydc.zero2one.payment.entity.Payment;
import com.dbydc.zero2one.payment.service.IPaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.Date;

/**
 * 支付服务 Service实现类
 * @author 大白有点菜
 * @className PaymentServiceImpl
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
@Service
public class PaymentServiceImpl implements IPaymentService {

    /**
     * 写法一:Spring的注解。将对象交由Spring去管理,官方不推荐这种写法。
     */
    //@Autowired
    //private PaymentDao paymentDao;

    //private PaymentDao paymentDao;
    /**
     * 写法二:Spring的注解。Setter方式注入对象,官方推荐这种写法。
     * @param paymentDao
     */
    //@Autowired
    //public void setPaymentDao(PaymentDao paymentDao) {
        //this.paymentDao = paymentDao;
    //}

    /**
     * 写法三:Java的注解。全称javax.annotation.Resource,它属于JSR-250规范的一个注解,包含Jakarta EE(J2EE)中。
     */
    @Resource
    private PaymentDao paymentDao;

    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    @Override
    public ResponseData getByOrderNum(String orderNum) {
        Payment payment = paymentDao.getByOrderNum(orderNum);
        return payment == null ? ResponseData.success(ResponseCodeEnum.NULL_DATA.getCode())
                : ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), payment);
    }

    /**
     * 通过主键ID获取数据
     * @param id 主键ID
     * @return
     */
    @Override
    public ResponseData getById(Long id) {
        Payment payment = paymentDao.getById(id);
        return payment == null ? ResponseData.success(ResponseCodeEnum.NULL_DATA.getCode())
                : ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), payment);
    }

    /**
     * 新增
     * @param payment 支付服务实体对象
     * @return
     */
    @Override
    public ResponseData add(Payment payment) {
        Date date = new Date();
        payment.setCreateTime(date);
        payment.setUpdateTime(date);
        int addResult = paymentDao.add(payment);
        return addResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), null, OperateStatusEnum.ADD_SUCCESS.getMessage())
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), OperateStatusEnum.ADD_FAIL.getMessage());
    }

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    @Override
    public ResponseData deleteByOrderNum(String orderNum) {
        int deleteResult = paymentDao.deleteByOrderNum(orderNum);
        return deleteResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), null, OperateStatusEnum.DELETE_SUCCESS.getMessage())
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), OperateStatusEnum.DELETE_FAIL.getMessage());
    }

    /**
     * 通过主键ID删除数据
     * @param id 主键ID
     * @return
     */
    @Override
    public ResponseData deleteById(Long id) {
        int deleteResult = paymentDao.deleteById(id);
        return deleteResult > 0 ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), null, OperateStatusEnum.DELETE_SUCCESS.getMessage())
                : ResponseData.error(ResponseCodeEnum.ERROR.getCode(), OperateStatusEnum.DELETE_FAIL.getMessage());
    }
}

 
  PaymentController 业务类新增 6 个接口方法

//Feign - 通过订单号获取数据(Configuration配置类)
@GetMapping("/feign/queryByOrderNum/{orderNum}")
public ResponseData queryFeignByOrderNum(@PathVariable(“orderNum”) String orderNum);
 
//Feign - 通过订单号获取数据(application.yml)
@GetMapping("/feign/query/{orderNum}")
public ResponseData queryFeign(@PathVariable(“orderNum”) String orderNum);
 
//Feign - 演示ReadTimeout超时
@GetMapping(value = "/feign/timeout")
public String paymentFeignTimeOut();
 
//Feign - 演示契约(Contract)配置
@GetMapping(value = "/feign/contract/{id}")
public String paymentFeignContract(@PathVariable(“id”) int id);
 
//Feign - 演示请求拦截器(RequestInterceptor)
@GetMapping(value = "/feign/interceptor")
public String paymentFeignInterceptor();
 
//Feign - 演示Fallback
@GetMapping(value = "/feign/fallback")
public String paymentFeignFallback();

 

package com.dbydc.zero2one.payment.controller;

import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.payment.entity.Payment;
import com.dbydc.zero2one.payment.service.IPaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;

/**
 * 支付服务 Controller业务类
 * @author 大白有点菜
 * @className PaymentController
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
@RestController
@RequestMapping("payment")
public class PaymentController {

    @Autowired
    private IPaymentService paymentService;

    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/queryByOrderNum/{orderNum}")
    public ResponseData queryByOrderNum(@PathVariable String orderNum) {
        return paymentService.getByOrderNum(orderNum);
    }

    /**
     * 通过主键ID获取数据
     * @param id 主键ID
     * @return
     */
    @GetMapping("/queryById/{id}")
    public ResponseData queryById(@PathVariable Long id) {
        return paymentService.getById(id);
    }

    /**
     * 新增
     * @param payment 支付服务实体对象
     * @return
     */
    @PostMapping("/add")
    public ResponseData add(@RequestBody @Validated Payment payment) {
        return paymentService.add(payment);
    }

    /**
     * 通过订单号删除数据
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/deleteByOrderNum/{orderNum}")
    public ResponseData deleteByOrderNum(@PathVariable String orderNum) {
        return paymentService.deleteByOrderNum(orderNum);
    }

    /**
     * 通过主键ID删除数据
     * @param id 主键ID
     * @return
     */
    @GetMapping("/deleteById/{id}")
    public ResponseData deleteById(@PathVariable Long id) {
        return paymentService.deleteById(id);
    }

    /**
     * Feign - 通过订单号获取数据(Configuration配置类)
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/feign/queryByOrderNum/{orderNum}")
    public ResponseData queryFeignByOrderNum(@PathVariable("orderNum") String orderNum) {
        long time = 6L;
        //暂停指定秒钟
        try {
            TimeUnit.SECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return paymentService.getByOrderNum(orderNum);
    }

    /**
     * Feign - 通过订单号获取数据(application.yml)
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/feign/query/{orderNum}")
    public ResponseData queryFeign(@PathVariable("orderNum") String orderNum) {
        long time = 6L;
        //暂停指定秒钟
        try {
            TimeUnit.SECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return paymentService.getByOrderNum(orderNum);
    }

    /**
     * Feign - 演示ReadTimeout超时
     * @return
     */
    @GetMapping(value = "/feign/timeout")
    public String paymentFeignTimeOut() {
//        long time = 59L;
        long time = 6L;
        //暂停指定秒钟
        try {
            TimeUnit.SECONDS.sleep(time);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String msg = new StringBuilder().append("【大白有点菜】等到花儿也谢了,总共等待:").append(time).append(" 秒").toString();
        System.out.println(msg);
        return msg;
    }

    /**
     * Feign - 演示契约(Contract)配置
     * @return
     */
    @GetMapping(value = "/feign/contract/{id}")
    public String paymentFeignContract(@PathVariable("id") int id) {
        return "大白有点菜" + id + "啊";
    }

    /**
     * Feign - 演示请求拦截器(RequestInterceptor)
     * @return
     */
//    @GetMapping(value = "/feign/interceptor", produces = "text/html;charset=UTF-8")
    @GetMapping(value = "/feign/interceptor")
    public String paymentFeignInterceptor() {
        return new StringJoiner(",", "‘(*>﹏<*)′", "‘(*>﹏<*)′").add("大白有点菜被拦截了呀").toString();
    }

    /**
     * Feign - 演示Fallback
     * @return
     */
    @GetMapping(value = "/feign/fallback")
    public String paymentFeignFallback() {
        return "演示Fallback";
    }
}

 
  PaymentServiceMain6002 引导类代码
 

package com.dbydc.zero2one.payment;

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

/**
 * Payment支付服务引导类
 * @author 大白有点菜
 * @className PaymentServiceMain6002
 * @date 2023-03-31
 * @description
 * @since 1.0
 **/
@SpringBootApplication
@MapperScan("com.dbydc.zero2one.payment.dao")
public class PaymentServiceMain6002 {
    public static void main(String[] args) {
        SpringApplication.run(PaymentServiceMain6002.class, args);
    }
}

 

3.8.2 子模块 cloud-order-service8001 添加 Feign 客户端

 
  考虑到以后远程调用都使用 Feign 客户端,我们在公共模块 cloud-common-apipom.xml 文件中引入 spring-cloud-starter-openfeign 依赖。
 

<!-- OpenFeign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

 
cloud-common-api 的 pom.xml 文件中引入 spring-cloud-starter-openfeign 依赖
 
  OrderServiceMain8001 引导类添加 @EnableFeignClients 注解。
 
OrderServiceMain8001 引导类添加 @EnableFeignClients 注解。
 

3.8.3 创建 PaymentFeignService 接口用来进行 Feign 远程调用

 
  在 cloud-order-service8001 模块中新建包 feign ,再新建接口 PaymentFeignService ,如图所示。只有一个抽象方法 queryFeign() ,返回值是 ResponseData注意,需要添加注解 @Service 和 @FeignClient 。
 
  “value”是服务端的名称。
 
  “url”是调用地址,绝对 URL 或可解析的主机名(协议是可选的),会拼接注解 @GetMapping 里面的内容组成完整地址:localhost:6002/payment/feign/query/{orderNum} ,相当于浏览器访问这个地址。
 
  “contextId”写这个接口 PaymentFeignService 的名称,用作 Bean 名称,一定不能缺少这部分,缺少会导致编译报错,只有一个接口不会,多个接口会编译报错。
 
  注意,PaymentFeignService 接口代码里,ResponseData queryFeign(@PathVariable("orderNum") String orderNum); 如果注解 @PathVariable 后面不加 (“orderNum”) ,运行会报错! 后面会验证这一点。
 
在 cloud-order-service8001 模块中新建包 feign ,再新建接口 PaymentFeignService
 

package com.dbydc.zero2one.order.feign;

import com.dbydc.zero2one.common.utils.ResponseData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * Feign远程调用Payment服务接口(使用application.yml配置文件定义的feign属性)
 * @author 大白有点菜
 * @className PaymentFeignService
 * @date 2023-04-02
 * @description
 * @since 1.0
 **/
@Service
@FeignClient(value = "cloud-payment-service", url = "localhost:6002/payment", contextId = "PaymentFeignService")
public interface PaymentFeignService {
    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/feign/query/{orderNum}")
    ResponseData queryFeign(@PathVariable("orderNum") String orderNum);
}

 

3.8.4 创建 OrderFeignController 业务类

 
  OrderFeignController 业务类主要提供 Feign 的远程调用接口,主要代码:
 

package com.dbydc.zero2one.order.controller;

import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.order.feign.*;
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;

/**
 * 订单服务 Controller业务类 Feign远程调用
 * @author 大白有点菜
 * @className OrderFeignController
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@RestController
@RequestMapping("order")
public class OrderFeignController {

    @Autowired
    private PaymentFeignService paymentFeignService;

    /**
     * 通过订单号获取数据(application.yml)
     * @param orderNum
     * @return
     */
    @GetMapping("/feign/query/{orderNum}")
    public ResponseData queryFeign(@PathVariable("orderNum") String orderNum) {
        return paymentFeignService.queryFeign(orderNum);
    }
}

 

3.8.5 验证 Feign 接口抽象方法添加 @PathVariable 注解而不加别名 (“xxx”) 导致报错

 
  上面贴的代码是没有问题的。为了验证报错,我这里稍微处理一下代码:OrderFeignController 类的 @PathVariable 注解不加别名。PaymentFeignService 接口的抽象方法 queryFeign() 入参的 @PathVariable 注解也不加别名。 运行 OrderServiceMain8001 引导类,报错如下:PathVariable annotation was empty on param 0.
 
OrderFeignController 类的 @PathVariable 注解不加别名。
 
PaymentFeignService接口的抽象方法 queryFeign() 入参的 @PathVariable 注解也不加别名。
 
运行报错:PathVariable annotation was empty on param 0.
 
  PaymentFeignService 接口的抽象方法 queryFeign() 入参的 @PathVariable 注解加上别名,而OrderFeignController 类的 @PathVariable 注解不加别名,运行会报错吗?
 
PaymentFeignService 接口的抽象方法 queryFeign() 入参的 @PathVariable 注解加上别名,而OrderFeignController 类的 @PathVariable 注解不加别名,运行不会报错
 
  由此可见,如果以后报这种 PathVariable annotation was empty on param 0. 错误,记得在注解 @PathVariable 加别名,即 @PathVariable(“xxx”) ,Controller 类可以不加,但建议统一加上去,以免出问题
 

3.8.6 使用 Postman 测试 PaymentFeignService 的 Feign 接口是否正常

 
  先启动 Payment 服务(PaymentServiceMain6002 引导类),再启动 Order 服务(OrderServiceMain8001 引导类)。
 
  测试接口通过订单号获取数据。对应的方法:public ResponseData queryByOrderNum(@PathVariable(“orderNum”) String orderNum);

接口:localhost:8001/order/feign/query/dbydc666
提交方式:get
使用工具:Postman

 
  测试结果如下。
 
feign 远程查询数据成功

 

3.8.7 覆盖 Feign 默认值:超时配置和日志记录级别

 
  常见覆盖 Feign 默认值有两种方式:

(1)application.yml 配置 feign 属性值。
 
(2)使用 @FeignClient 声明额外的配置(在 FeignClientsConfiguration 之上)来完全控制 feign 客户端。注解的 configuration() 方法支持一个自定义配置类列表。格式@FeignClient(value = "cloud-payment-service", configuration = {FeignConfig.class, FeignContractConfig.class})

 
  1、application.yml 配置 feign 属性值,并监控 com.dbydc.zero2one.order.feign.* 路径所有接口的远程调用日志,模式是 debug
 

feign:
  client:
    config:
      default:
        #connectTimeout:防止由于服务器处理时间长而阻塞调用者。
        connectTimeout: 5000
        #readTimeout:从建立连接开始应用,当返回响应时间过长时触发。
        readTimeout: 8000
        #日志级别:none、basic、headers、full
        loggerLevel: full

logging:
  level:
    com.dbydc.zero2one.order.feign.*: debug

 
  2、使用 @FeignClient 声明额外的配置。先在 cloud-common-api 模块中新建包 config ,再新建配置类 FeignConfig 。这是一个公共的自定义 Feign 配置类,主要设置 超时处理连接时间(connectTimeout)读取时间(readTimeout)】和 日志记录日志级别(loggerLevel)】参数。注意:FeignConfig 配置类不需要添加注解 @Configuration 注解,添加了是全局配置,不添加是局部配置。强烈建议不要配置为全局! 使用方式:@FeignClient(configuration = {FeignConfig.class})
 
  (1)超时处理配置
 

超时参数功能默认值(单位:秒-s)
connectTimeout防止由于服务器处理时间长而阻塞调用者。10
readTimeout从建立连接开始应用,当返回响应时间过长时触发。60

 
connectTimeout和readTimeout默认值
 
  (2)日志记录级别:生产使用 BASIC 级别,开发使用 FULL 级别
 

日志级别(Logger.Level)描述
NONE无日志记录(默认)。
BASIC只记录请求方法(request method)、URL、响应状态码(response status code)和执行时间(execution time)。生产环境使用
HEADERS记录基本信息(在BASIC级别基础上)以及请求和响应标头(headers)。
FULL记录请求和响应的标头(headers)、正文(body)和元数据(metadata)。开发环境使用

 
  Logger.Level 级别为 FULL 的效果如下,打印出来的信息很多。
 
Logger.Level 级别为 FULL 的效果如下,打印出来的信息很多。
 

package com.dbydc.zero2one.common.config;

import feign.Logger;
import feign.Request;
import org.springframework.context.annotation.Bean;

import java.util.concurrent.TimeUnit;

/**
 * 公共OpenFeign远程调用配置类
 * @author 大白有点菜
 * @className FeignConfig
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
public class FeignConfig {

    long connectTimeout = 5L;
    long readTimeout = 5L;

    @Bean
    public Request.Options options() {
        return new Request.Options(connectTimeout, TimeUnit.SECONDS, readTimeout, TimeUnit.SECONDS, true);
    }

    /**
     * 日志级别
     * 1、NONE:无日志记录(默认)。
     * 2、BASIC:只记录请求方法(request method)、URL、响应状态码(response status code)和执行时间(execution time)。
     * 3、HEADERS:记录基本信息(在BASIC级别基础上)以及请求和响应标头(headers)。
     * 4、FULL:记录请求和响应的标头(headers)、正文(body)和元数据(metadata)。
     * @return
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

 
  3、application.yml 配置 feign 属性和使用 @FeignClient 声明自定义配置类,两者能共存吗?答案是,可以!默认优先级最高是 application.yml 配置的属性。通俗地说,Feign 客户端优先读取 application.yml 配置的 feign 属性值,再读取 FeignConfig 配置类设置的属性值(如果有声明到指定的配置类),先读取到的属性值存在就直接采用,覆盖另一个属性值。 两者的属性值是互补的,application.yml 不配置 Logger.Level 属性值,那就使用 FeignConfig 配置类设置的 Logger.Level 属性值,同时存在,读取的是优先级高的那个
 
  4、如果要设置 FeignConfig 配置类的优先级最高,如何设置呢feign.client.default-to-properties 设置为 false注意层级关系,default-to-properties 是在 feign.client 下的,层级不对可能会报错
 
设置 FeignConfig 配置类的优先级最高,将 feign.client.default-to-properties 设置为 false
 

feign:
  client:
    config:
      default:
        #connectTimeout:防止由于服务器处理时间长而阻塞调用者。
        connectTimeout: 5000
        #readTimeout:从建立连接开始应用,当返回响应时间过长时触发。
        readTimeout: 8000
        #日志级别:none、basic、headers、full
        loggerLevel: basic
    #默认是 true ,设置为 false 代表以自定义的 @Configuration 配置类的值为最高优先级,配置文件(*.yml)的默认值次之。
    default-to-properties: false

 

3.8.8 测试 Feign 客户端使用 application.yml 的 feign 属性值

 
  对两处地方进行临时性的修改,application.yml 的 feign.client.default-to-properties 属性先注释掉,服务端的 PaymentController 业务类的 queryFeign() 方法添加一个暂停线程指定秒数的操作。
 

feign:
  client:
    config:
      default:
        #connectTimeout:防止由于服务器处理时间长而阻塞调用者。
        connectTimeout: 5000
        #readTimeout:从建立连接开始应用,当返回响应时间过长时触发。
        readTimeout: 8000
        #日志级别:none、basic、headers、full
        loggerLevel: full
    #默认是 true ,设置为 false 代表以自定义的 @Configuration 配置类的值为最高优先级,配置文件(*.yml)的默认值次之。
#    default-to-properties: false
/**
 * Feign - 通过订单号获取数据
 * @param orderNum 订单号
 * @return
 */
@GetMapping("/feign/query/{orderNum}")
public ResponseData queryFeign(@PathVariable String orderNum) {
    long time = 6L;
    //暂停指定秒钟
    try {
        TimeUnit.SECONDS.sleep(time);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return paymentService.getByOrderNum(orderNum);
}

 
服务端的 PaymentController 业务类的 queryFeign() 方法添加一个暂停线程指定秒数的操作。
 
  使用 Postman 进行测试,GET 方式提交,接口为:localhost:8001/order/feign/query/dbydc666 。服务端 Payment 的接口暂停了 6 秒,但客户端 Order 设置的 readTimeout 为 8 秒,不会报超时错误,数据正常接收。
 
使用 Postman 进行测试,GET 方式提交,接口为:localhost:8001/order/feign/query/dbydc666 。
 

3.8.9 测试 Feign 客户端使用自定义 FeignConfig 配置类的属性值

 
  注释掉 application.yml 里面 feign 的属性配置,在 feign 包下创建一个新的 PaymentFeignConfigurationService 接口,申明 FeignConfig 配置类。
 
注释掉 application.yml 里面 feign 的属性配置
 

package com.dbydc.zero2one.order.feign;

import com.dbydc.zero2one.common.config.FeignConfig;
import com.dbydc.zero2one.common.utils.ResponseData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * Feign远程调用Payment服务接口(使用自定义的配置类)
 * @author 大白有点菜
 * @className PaymentFeignConfigurationService
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Service
@FeignClient(value = "cloud-payment-service", url = "localhost:6002/payment",
        contextId = "PaymentFeignConfigurationService", configuration = {FeignConfig.class})
public interface PaymentFeignConfigurationService {
    /**
     * 通过订单号获取数据
     * @param orderNum 订单号
     * @return
     */
    @GetMapping("/feign/queryByOrderNum/{orderNum}")
    ResponseData queryByOrderNum(@PathVariable("orderNum") String orderNum);
}

 
  模块 cloud-order-service8001 的业务类 OrderFeignController 添加 queryByOrderNum() 方法。代码如下:
 

@Autowired
private PaymentFeignConfigurationService configurationService;
/**
 * 通过订单号获取数据(Configuration配置类)
 * @param orderNum
 * @return
 */
@GetMapping("/feign/queryByOrderNum/{orderNum}")
public ResponseData queryByOrderNum(@PathVariable("orderNum") String orderNum) {
    return configurationService.queryByOrderNum(orderNum);
}

 
  FeignConfig 配置类里的 connectTimeout 和 readTimeout 都设置为 5 秒,而服务端 PaymentController 业务类的 queryFeignByOrderNum() 设置暂停线程时间为 6 秒。同时启动 OrderServiceMain8001PaymentServiceMain6002 引导类,还是用Postman进行测试。
 
服务端 PaymentController 业务类的 queryFeignByOrderNum() 设置暂停线程时间为 6 秒
 
  使用 GET 方式请求接口:localhost:8001/order/feign/queryByOrderNum/dbydc666 此时发现报错 Read timed out ,说明使用自定义的 FeignConfig 配置类时生效的,readTimeout 设置的 5 秒小于服务端延迟的 6 秒。如图所示,Postman 返回错误信息和 Order 服务错误日志。
 
使用 GET 方式请求接口:localhost:8001/order/feign/queryByOrderNum/dbydc666
 
Order服务错误日志
 
  将 Payment 服务端接口的延迟时间修改为 2 秒,重新启动 PaymentServiceMain6002 引导类。这时 Postman 成功返回数据,Order 服务也能打印正常的调用接口日志。如图所示:
 
Postman 成功返回数据

 
Order 服务也能打印正常的调用接口日志
 

3.8.10 演示超时处理

 
  在前面一小节已经演示过了超时报错,这部分内容其实可以省略了,读者可自行选择。我有单独写一个 PaymentFeignTimeoutService 接口去演示超时的情景。PaymentFeignTimeoutService 接口代码如下:
 

package com.dbydc.zero2one.order.feign;

import com.dbydc.zero2one.common.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * Feign远程调用Payment服务接口(主要是 演示ReadTimeout超时)
 * @author 大白有点菜
 * @className PaymentFeignTimeoutService
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Service
@FeignClient(value = "cloud-payment-service", url = "localhost:6002/payment",
        contextId = "PaymentFeignTimeoutService", configuration = {FeignConfig.class})
public interface PaymentFeignTimeoutService {
    /**
     * 演示ReadTimeout超时
     * @return
     */
    @GetMapping(value = "/feign/timeout", produces = "application/json;charset=UTF-8")
    String paymentFeignTimeOut();
}

 
  cloud-order-service8001 模块中的 OrderFeignController 业务类添加 paymentFeignTimeOut() 方法。
 

@Autowired
private PaymentFeignTimeoutService timeoutService;
/***
 * 演示ReadTimeout超时
 * @return
 */
@GetMapping(value = "/feign/timeout")
public String paymentFeignTimeOut() {
    String value = timeoutService.paymentFeignTimeOut();
    System.out.println(value);
    return value;
}

 
  PaymentController 业务类的 paymentFeignTimeOut() 方法的线程暂停时间设置为 10 秒,同时启动 Order 服务和 Payment 服务。GET 方式请求接口:localhost:8001/order/feign/timeout 。结果不出意外,还是 Read timed out 报错。
 
GET 方式请求接口:localhost:8001/order/feign/timeout 返回错误信息
 
PaymentController 业务类的 paymentFeignTimeOut() 方法的线程暂停时间设置为 10 秒
 
  重新将 PaymentController 业务类的 paymentFeignTimeOut() 方法的线程暂停时间设置为 2 秒,再次请求接口,Order 服务打印正常信息:【大白有点菜】等到花儿也谢了,总共等待:2 秒 。
 
Order 服务打印正常信息
 

3.8.11 演示契约(Contract)配置

 
  Feign 有它自己的契约,OpenFeign 在此基础上支持 SpringMVC 注解。例如在方法上写注解 @GetMapping(“/feign/query/{orderNum}”) 就是 SpringMVC 的注解方式,是一种契约(Contract)。那么 Feign 原生的契约是怎样的呢?
 
  要使用 Feign 原生的契约,可以在 cloud-common-api 模块的 config 包下新建 FeignContractConfig 配置类,将 Contract 类交给 Spring Bean 管理。代码如下,new Contract.Default();使 Feign 原生的契约生效new SpringMvcContract();使 SpringMvc 的契约生效
 

package com.dbydc.zero2one.common.config;

import feign.Contract;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;

/**
 * 公共Feign契约配置类
 * @author 大白有点菜
 * @className FeignContractConfig
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
public class FeignContractConfig {

    /**
     * 契约配置
     * @return
     */
    @Bean
    Contract feignContract() {
        return new Contract.Default();  //不支持 SpringMvc 注解
//        return new SpringMvcContract(); //支持 SpringMvc 注解
    }
}

 
  在 cloud-order-service8001 模块的 feign 包下新建接口 PaymentFeignContractService 。主要代码如下,@FeignClient 注解的 configuration 属性同时声明 FeignConfig.class 和 FeignContractConfig.class
 

package com.dbydc.zero2one.order.feign;

import com.dbydc.zero2one.common.config.FeignConfig;
import com.dbydc.zero2one.common.config.FeignContractConfig;
import feign.Headers;
import feign.Param;
import feign.RequestLine;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;

/**
 * Feign远程调用Payment服务接口(主要是演示契约配置)
 * @author 大白有点菜
 * @className PaymentFeignContractService
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Service
@FeignClient(value = "cloud-payment-service", url = "localhost:6002/payment",
        contextId = "PaymentFeignContractService", configuration = {FeignConfig.class, FeignContractConfig.class})
public interface PaymentFeignContractService {
    /**
     * 演示契约配置(FeignContractConfig配置文件)
     * 1、Contract.Default():Feign 原生默认契约,不支持SpringMvc注解。
     * 2、SpringMvcContract():OpenFeign 支持SpringMvc注解。
     * @return
     */
    @RequestLine("GET /feign/contract/{id}")
    @Headers({"Content-Type: application/json", "Accept: application/json"})
    String paymentFeignContract(@Param("id") int id);
}

 
  Feign 原生的契约使用的是 @RequestLine 注解,值的写法和 SpringMvc 的 @GetMapping 注解的值写法不一样。添加 @Headers 注解目的是解决返回的中文乱码注意啦,入参使用的注解是 @Param ,千万别用错了,SpringMvc 契约使用的是 @PathVariable 注解!
 
  cloud-order-service8001 模块中的 OrderFeignController 业务类添加 paymentFeignContract() 方法。
 

@Autowired
private PaymentFeignContractService contractService;
/**
 * 演示契约配置(FeignContractConfig配置文件)
 * 1、Contract.Default():Feign原生默认契约,不支持SpringMvc注解。
 * 2、SpringMvcContract():OpenFeign 支持SpringMvc注解。
 * @return
 */
@GetMapping(value = "/feign/contract/{id}")
public String paymentFeignContract(@PathVariable("id") int id) {
    String value = contractService.paymentFeignContract(id);
    System.out.println(value);
    return value;
}

 
  又来套路了,使用 Postman 测试,以 GET 方式提交请求:localhost:8001/order/feign/contract/666 。结果是 Order 服务接收到数据:大白有点菜666啊 。
 
以 GET 方式提交请求:localhost:8001/order/feign/contract/666 。

 
结果是 Order 服务接收到数据:大白有点菜666啊 。
 

3.8.12 演示请求拦截器(RequestInterceptor)

 
  Feign 支持 请求拦截器(RequestInterceptor),可以拦截请求头和请求参数,例如 Token ,实现身份验证和各微服务之间传递一些请求参数。
 
  cloud-order-service8001 模块中新建包 interceptor ,再创建Feign认证请求拦截器类 FeignAuthRequestInterceptor实现 RequestInterceptor 接口,重写 apply() 方法。 。主要代码如下:
 

package com.dbydc.zero2one.order.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;

/**
 * Feign认证请求拦截器
 * @author 大白有点菜
 * @className FeignAuthRequestInterceptor
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
public class FeignAuthRequestInterceptor implements RequestInterceptor {

    private String tokenId;

    public FeignAuthRequestInterceptor(String tokenId) {
        this.tokenId = tokenId;
    }

    @Override
    public void apply(RequestTemplate template) {
        template.header("Authorization", tokenId);
    }
}

 
  cloud-payment-service6002 模块中新建包 interceptor ,再创建认证请求拦截器类 AuthInterceptor实现 HandlerInterceptor 接口,重写 preHandle() 方法。 。主要代码如下,判断字符串对象 authorization 是否为空,不为空并且等于“666”,才返回 true ,否则返回 false 。
 

package com.dbydc.zero2one.payment.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 认证请求拦截器
 * @author 大白有点菜
 * @className AuthInterceptor
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Slf4j
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //逻辑认证
        String authorization = request.getHeader("Authorization");
        log.error("获取的认证信息 Authorization:{}", authorization);
//        response.setCharacterEncoding("UTF-8");
//        response.setContentType("application/json;charset=UTF-8");
        if (StringUtils.hasText(authorization) && authorization.equals("666")) {
            return true;
        }
        return false;
    }
}

 
  cloud-payment-service6002 模块中新建包 config ,再创建WebMvc拦截器配置类 WebMvcConfig继承 WebMvcConfigurationSupport 类,重写 addInterceptors() 方法。 。主要代码如下:
 

package com.dbydc.zero2one.payment.config;

import com.dbydc.zero2one.payment.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

/**
 * WebMvc拦截器配置类
 * @author 大白有点菜
 * @className WebMvcConfig
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor());
    }
}

 
  cloud-order-service8001 模块中 config 包下创建拦截器配置类 OrderInterceptorConfig。主要代码如下,使用到了 @Value 注解,需要在 application.yml 定义 feign.tokenId 这个属性(注意,tokenId 存在的意义只是测试需要)。以后我们都可以这样在 application.yml 中自定义一个属性,使用 @Value 注解获取到这个属性值。
 

package com.dbydc.zero2one.order.config;

import com.dbydc.zero2one.order.interceptor.FeignAuthRequestInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 拦截器配置类
 * @author 大白有点菜
 * @className OrderInterceptorConfig
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Configuration
public class OrderInterceptorConfig {

    @Value("${feign.tokenId}")
    private String tokenId;

    /**
     * 自定义拦截器
     * @return
     */
    @Bean
    FeignAuthRequestInterceptor feignAuthRequestInterceptor() {
        return new FeignAuthRequestInterceptor(tokenId);
    }
}

 

feign:
  tokenId: 666
  client:
    config:
      default:
        #connectTimeout:防止由于服务器处理时间长而阻塞调用者。
        connectTimeout: 5000
        #readTimeout:从建立连接开始应用,当返回响应时间过长时触发。
        readTimeout: 8000
        #日志级别:none、basic、headers、full
        loggerLevel: full
    #默认是 true ,设置为 false 代表以自定义的 @Configuration 配置类的值为最高优先级,配置文件(*.yml)的默认值次之。
    default-to-properties: false

 
  cloud-order-service8001 模块中 feign 包下创建请求拦截器接口 PaymentFeignInterceptorService@GetMapping 注解中的 produces() 方法定义了 Content-Type 为“application/json;charset=UTF-8”,以处理返回的中文乱码。主要代码如下:
 

package com.dbydc.zero2one.order.feign;

import com.dbydc.zero2one.common.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * Feign远程调用Payment服务接口(请求拦截器(RequestInterceptor))
 * @author 大白有点菜
 * @className PaymentFeignInterceptorService
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Service
@FeignClient(value = "cloud-payment-service", url = "localhost:6002/payment",
        contextId = "PaymentFeignInterceptorService", configuration = {FeignConfig.class})
public interface PaymentFeignInterceptorService {
    /**
     * 演示请求拦截器(RequestInterceptor)
     * @return
     */
    @GetMapping(value = "/feign/interceptor", produces = "application/json;charset=UTF-8")
    String paymentFeignInterceptor();
}

 
  cloud-order-service8001 模块中的 OrderFeignController 业务类添加 paymentFeignInterceptor() 方法。
 

@Autowired
private PaymentFeignInterceptorService interceptorService;
/**
 * 演示请求拦截器(RequestInterceptor)
 * @return
 */
@GetMapping(value = "/feign/interceptor")
public String paymentFeignInterceptor() {
    String value = interceptorService.paymentFeignInterceptor();
    System.out.println(value);
    return value;
}

 
  同时启动 OrderServiceMain8001 和 PaymentServiceMain6002 引导类。使用 Postman 以 GET 方式请求接口:localhost:8001/order/feign/interceptor 。Order 服务正常接收到数据。
 
使用 Postman 以 GET 方式请求接口:localhost:8001/order/feign/interceptor
 
Order 服务正常接收到数据。
 
  修改 application.yml 中 feign.tokenId 的值为 665 ,重新运行 Order 服务,再请求接口,得到的返回结果是 null
 
修改 application.yml 中 feign.tokenId 的值为 665
 
重新运行 Order 服务,再请求接口,得到的返回结果是 null
 

3.8.13 演示Fallback

 
  如果服务器未运行或不可用,或者超时,则数据包会导致连接被拒绝。通信以错误消息或回退(fallback)结束。Spring Cloud 断路器(CircuitBreaker)支持 Fallback(回退),自定义一个 Fallback 类,当接口调用超时或者不可用时,可以自定义异常的回退原因。就好比手机关闭上网功能,微信会提示当前网络不可用。
 
  如果不引入支持熔断机制的组件,Fallback 功能不会生效。支持熔断降级的组件主要有 HystrixResilience4JSentinel(Spring Cloud Alibaba 全家桶之一)。引入 Hystrix 组件,但不介绍这个组件的使用,这小节主要演示 Fallback 功能。
 
  cloud-order-service8001 模块的 pom.xml 文件引入 hystrix 依赖。
 

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-hystrix -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    <version>2.2.10.RELEASE</version>
</dependency>

 
cloud-order-service8001 模块的 pom.xml 文件引入 hystrix 依赖。
 
  cloud-order-service8001 模块的 application.yml 中配置 feign.circuitbreaker.enabledtrue 。如果 Spring Cloud CircuitBreaker 在类路径上并且 feign.circuitbreaker.enabled=true,Feign 将使用断路器包装所有方法。
 

feign:
  tokenId: 666
  circuitbreaker:
    enabled: true
  client:
    config:
      default:
        #connectTimeout:防止由于服务器处理时间长而阻塞调用者。
        connectTimeout: 5000
        #readTimeout:从建立连接开始应用,当返回响应时间过长时触发。
        readTimeout: 8000
        #日志级别:none、basic、headers、full
        loggerLevel: full
    #默认是 true ,设置为 false 代表以自定义的 @Configuration 配置类的值为最高优先级,配置文件(*.yml)的默认值次之。
    default-to-properties: false

 
  cloud-order-service8001 模块中 feign 包下创建回退接口 PaymentFeignFallbackService@GetMapping 注解中的 produces() 方法定义了 Content-Type 为“application/json;charset=UTF-8”,以处理返回的中文乱码。主要代码如下:
 

package com.dbydc.zero2one.order.feign;

import com.dbydc.zero2one.common.config.FeignConfig;
import com.dbydc.zero2one.order.feign.fallback.PaymentFeignFallBackServiceFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * Feign远程调用Payment服务接口(使用自定义的配置类 和 Fallback)
 * @author 大白有点菜
 * @className PaymentFeignFallbackService
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Service
@FeignClient(value = "cloud-payment-service", url = "localhost:6002/payment", contextId = "PaymentFeignFallbackService",
        configuration = FeignConfig.class, fallbackFactory = PaymentFeignFallBackServiceFallbackFactory.class)
public interface PaymentFeignFallbackService {
    /**
     * 演示Fallback
     * @return
     */
    @GetMapping(value = "/feign/fallback", produces = "application/json;charset=UTF-8")
    String paymentFeignFallback();
}

 
  cloud-order-service8001 模块中 feign 包下新建 fallback 包,创建自定义的FallbackFactory类 PaymentFeignFallBackServiceFallbackFactory实现 FallbackFactory<T> 接口,重写 create() 方法记得添加 @Component 注解。【温馨提醒】,如果 PaymentFeignFallbackService 接口是有多个方法的,那在重写的 create() 方法里面这么写 return new PaymentFeignFallbackService(){};,而不是直接 throw 出一个异常,IDEA 会提示自动补全,我这么写的格式不完全正确。主要代码如下。
 

package com.dbydc.zero2one.order.feign.fallback;

import com.dbydc.zero2one.order.feign.PaymentFeignFallbackService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

/**
 * FallbackFactory实现远程调用Payment服务访问异常触发回退原因
 * @author 大白有点菜
 * @className PaymentFeignFallBackServiceFallbackFactory
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Component
@Slf4j
public class PaymentFeignFallBackServiceFallbackFactory implements FallbackFactory<PaymentFeignFallbackService> {
    @Override
    public PaymentFeignFallbackService create(Throwable cause) {
        log.error("哎哟,大白有点菜掉粉了呢,原因是Fallback了:", cause);
        throw new NoFallbackAvailableException("哎哟,大白有点菜掉粉了呢", cause);
    }
}

 
  cloud-order-service8001 模块中的 OrderFeignController 业务类添加 paymentFeignFallback() 方法。
 

@Autowired
private PaymentFeignFallbackService fallbackService;
/**
 * 演示Fallback
 * @return
 */
@GetMapping(value = "/feign/fallback")
public String paymentFeignFallback() {
    String value = fallbackService.paymentFeignFallback();
    System.out.println(value);
    return value;
}

 
  使用 Postman 以 GET 方式请求接口:localhost:8001/order/feign/fallback ,如果接口正常,返回的数据应该是:演示Fallback。如图所示:
 
使用 Postman 以 GET 方式请求接口:localhost:8001/order/feign/fallback
 
如果接口正常,返回的数据应该是:演示Fallback。
 
  如何验证 Fallback 功能呢?只停止 Payment 服务,再重新请求接口。由结果可以看出,create() 是被调用了的, Fallback 功能正常。
 
由结果可以看出,create() 是被调用了的, Fallback 功能正常。
 

3.8.14 完整的 OrderFeignController 业务类代码

 

package com.dbydc.zero2one.order.controller;

import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.order.feign.*;
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;

/**
 * 订单服务 Controller业务类 Feign远程调用
 * @author 大白有点菜
 * @className OrderFeignController
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@RestController
@RequestMapping("order")
public class OrderFeignController {

    @Autowired
    private PaymentFeignService paymentFeignService;

    @Autowired
    private PaymentFeignConfigurationService configurationService;

    @Autowired
    private PaymentFeignContractService contractService;

    @Autowired
    private PaymentFeignTimeoutService timeoutService;

    @Autowired
    private PaymentFeignInterceptorService interceptorService;

    @Autowired
    private PaymentFeignFallbackService fallbackService;

    /**
     * 通过订单号获取数据(Configuration配置类)
     * @param orderNum
     * @return
     */
    @GetMapping("/feign/queryByOrderNum/{orderNum}")
    public ResponseData queryByOrderNum(@PathVariable("orderNum") String orderNum) {
        return configurationService.queryByOrderNum(orderNum);
    }

    /**
     * 通过订单号获取数据(application.yml)
     * @param orderNum
     * @return
     */
    @GetMapping("/feign/query/{orderNum}")
    public ResponseData queryFeign(@PathVariable("orderNum") String orderNum) {
        return paymentFeignService.queryFeign(orderNum);
    }

    /***
     * 演示ReadTimeout超时
     * @return
     */
    @GetMapping(value = "/feign/timeout")
    public String paymentFeignTimeOut() {
        String value = timeoutService.paymentFeignTimeOut();
        System.out.println(value);
        return value;
    }

    /**
     * 演示契约配置(FeignContractConfig配置文件)
     * 1、Contract.Default():Feign原生默认契约,不支持SpringMvc注解。
     * 2、SpringMvcContract():OpenFeign 支持SpringMvc注解。
     * @return
     */
    @GetMapping(value = "/feign/contract/{id}")
    public String paymentFeignContract(@PathVariable("id") int id) {
        String value = contractService.paymentFeignContract(id);
        System.out.println(value);
        return value;
    }

    /**
     * 演示请求拦截器(RequestInterceptor)
     * @return
     */
    @GetMapping(value = "/feign/interceptor")
    public String paymentFeignInterceptor() {
        String value = interceptorService.paymentFeignInterceptor();
        System.out.println(value);
        return value;
    }

    /**
     * 演示Fallback
     * @return
     */
    @GetMapping(value = "/feign/fallback")
    public String paymentFeignFallback() {
        String value = fallbackService.paymentFeignFallback();
        System.out.println(value);
        return value;
    }
}

 

3.9 使用 MyBatis-Plus 替代 MyBatis 进行数据库交互

 
  实际开发中会大量使用 MyBatis-Plus 组件,只需写少量的代码,就可以实现单表的CRUD操作,也可以自己写方法和SQL,和使用 MyBatis 一样,功能更强大。官方以三段话去介绍 MyBatis-Plus :(1)只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。(2)只需简单配置,即可快速进行单表 CRUD 操作,从而节省大量时间。(3)代码生成、自动分页、逻辑删除、自动填充等功能一应俱全。
 
  【Mybatis-plus官方网址】:
  https://baomidou.com/
 

3.9.1 新建 cloud-order-service8002 子模块

 
  cloud-order-service8002 子模块也是要引入 cloud-common-api 依赖的,不再详说。cloud-order-service8002 子模块结构如图所示:
 
cloud-order-service8002子模块结构

 
  主要包含 controller、dao、entity、request、service(子目录 impl)、mapper 这几个目录。公共模块 cloud-common-api 的 pom.xml 已经引入 mybatis-plus-boot-starter 依赖。
 
公共模块 cloud-common-api 已经引入 mybatis-plus-boot-starter 依赖。
 

3.9.2 application.yml 配置文件参数设置

 
  application.yml 配置文件参数设置如下:
 

#服务端口号
server:
  port: 8002

spring:
  application:
    #服务名称
    name: cloud-order-service
  #数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/zero2one?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai

mybatis-plus:
  #扫描资源(resources)路径下,mapper目录的所有以 .xml 结尾的mapper文件
  mapper-locations: classpath*:mapper/*.xml
  configuration:
    #下划线转驼峰
    map-underscore-to-camel-case: true

 

3.9.3 创建 OrderServiceMain8002 引导类

 
  我们在包路径“com.dbydc.zero2one.order”下新建 OrderServiceMain8002 引导类
 

package com.dbydc.zero2one.order;

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

/**
 * Order订单服务引导类
 * @author 大白有点菜
 * @className OrderServiceMain8002
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@SpringBootApplication
public class OrderServiceMain8002 {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceMain8002.class, args);
    }
}

 

3.9.4 创建 Order 实体类

 
  在 entity 包路径下新建实体类 Order 。和前面的 cloud-order-service8001 子模块的 Order 实体类不同,主要体现在:

1、类上添加注解 @TableName("tb_order") ,"tb_order"是 表名
 
2、自增主键 id 添加注解 @TableId(value = "id", type = IdType.AUTO) ,会自动插入主键。
 
3、Date 类型的 createTime 添加注解 @TableField(fill = FieldFill.INSERT) ,代表新增数据时,自动插入当前时间。
 
4、Date 类型的 updateTime 添加注解 @TableField(fill = FieldFill.INSERT_UPDATE) ,代表新增数据时,自动插入当前时间,修改数据时,自动更新为当前时间。

 

package com.dbydc.zero2one.order.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.lang.NonNull;

import java.util.Date;

/**
 * 订单服务 实体类
 * @author 大白有点菜
 * @className Order
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_order")
public class Order {

    /**
     * CREATE TABLE `tb_order` (
     *   `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
     *   `order_num` varchar(64) NOT NULL COMMENT '订单号',
     *   `user_name` varchar(128) DEFAULT NULL COMMENT '用户名',
     *   `user_phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户手机号',
     *   `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     *   `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
     *   PRIMARY KEY (`id`)
     * ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
     */

    /**
     * 自增主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 订单号
     */
    //@NonNull
    private String orderNum;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 用户手机号
     */
    private String userPhone;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    /**
     * 修改时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
}

 

3.9.5 创建 IOrderService 接口

 
  在 service 包路径下创建接口 IOrderService 。注意包路径是“com.dbydc.zero2one.order.service”。继承 IService<T> 接口
 

import com.baomidou.mybatisplus.extension.service.IService;
import com.dbydc.zero2one.order.entity.Order;

/**
 * 订单服务 Service接口
 * @author 大白有点菜
 * @className IOrderService
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
public interface IOrderService extends IService<Order> {

}

 
  逗我?抽象方法呢?怎么空空如也?先别急躁,等着看好戏吧。
 

3.9.6 创建 OrderDao 接口

 
  在 dao 包路径下创建接口 OrderDao继承 BaseMapper<T> 类
 

package com.dbydc.zero2one.order.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dbydc.zero2one.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;

/**
 * 订单服务 Dao接口
 * @author 大白有点菜
 * @className OrderDao
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Mapper
public interface OrderDao extends BaseMapper<Order> {

}

 
  哈?又逗我呢?也是空的,没有抽象方法?操作数据库个锤子哦!
 

3.9.7 创建 OrderServiceImpl 实现类

 
  在 impl 包路径下创建实现类 OrderServiceImpl 。注意包路径是“com.dbydc.zero2one.order.service.impl”。一定不能遗漏注解 @Service !继承 ServiceImpl<M extends BaseMapper<T>, T> 类,同时实现 IOrderService 接口
 

package com.dbydc.zero2one.order.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.dbydc.zero2one.order.dao.OrderDao;
import com.dbydc.zero2one.order.entity.Order;
import com.dbydc.zero2one.order.service.IOrderService;
import org.springframework.stereotype.Service;

/**
 * 订单服务 Service实现类
 * @author 大白有点菜
 * @className OrderServiceImpl
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Service
public class OrderServiceImpl extends ServiceImpl<OrderDao, Order> implements IOrderService {

}

 
  天呐!肯定是写《告诉老默我想学Spring Cloud了(新手篇):从0到1搭建Spring Cloud项目(实际项目开发的浓缩精华版)》这篇博客写到眼花,得买瓶珍珠明目液来润润,缓解眼疲劳,现在都出现幻觉了。
 

3.9.8 创建 OrderMapper.xml 文件

 
  先在 resources 资源目录下新建目录 mapper,再创建xml文件 OrderMapper.xml
 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dbydc.zero2one.order.dao.OrderDao">

</mapper>

 
  这?这!确定没写错?你似乎在逗我笑!连个 SQL 语句都没有,怀疑你整这个是来搞笑的。
 

3.9.9 创建 OrderRequest 请求实体类

 
  在 request 包路径下创建业务类 OrderRequest 。这个请求实体类映射传过来的 Json 格式数据。
 

package com.dbydc.zero2one.order.request;

import lombok.Data;

/**
 * 订单服务 请求实体类
 * @author 大白有点菜
 * @className OrderRequest
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Data
public class OrderRequest {
    /**
     * 订单号
     */
    private String orderNum;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 用户手机号
     */
    private String userPhone;
}

 

3.9.10 创建 OrderController 业务类

 
  在 controller 包路径下创建业务类 OrderController 。可以使用 LambdaQueryWrapperQueryWrapper 处理接收到的请求数据,注入 IOrderService 对象 orderService ,可以直接调用 MyBatis-Plus 的 list(Wrapper<T>) 方法查询满足查询条件的所有列表数据。
 

package com.dbydc.zero2one.order.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.dbydc.zero2one.common.enums.ResponseCodeEnum;
import com.dbydc.zero2one.common.utils.ResponseData;
import com.dbydc.zero2one.order.entity.Order;
import com.dbydc.zero2one.order.request.OrderRequest;
import com.dbydc.zero2one.order.service.IOrderService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 订单服务 Controller业务类
 * @author 大白有点菜
 * @className OrderController
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@RestController
@RequestMapping("order")
public class OrderController {

    @Autowired
    private IOrderService orderService;

    /**
     * 通过手机号查询批量数据
     * @param orderRequest 订单请求实体
     * @return
     */
    @PostMapping("/findList")
    public ResponseData findList(@RequestBody OrderRequest orderRequest) {

        //Order order = new Order();
        //BeanUtils.copyProperties(orderRequest, order);
        //LambdaQueryWrapper<Order> queryWrapper = new LambdaQueryWrapper<>(order);
        //List<Order> orders = orderService.list(queryWrapper);

        //写法一(LambdaQueryWrapper):通过手机号查询批量数据
        LambdaQueryWrapper<Order> queryWrapper1 = new LambdaQueryWrapper<>();
        queryWrapper1.eq(Order::getUserPhone, orderRequest.getUserPhone());

        //写法二(QueryWrapper):通过手机号查询批量数据
        QueryWrapper<Order> queryWrapper2 = new QueryWrapper<>();
        queryWrapper2.eq("user_phone", orderRequest.getUserPhone());

        //List<Order> orders = orderService.list(queryWrapper1);
        List<Order> orders = orderService.list(queryWrapper2);

        return CollectionUtils.isEmpty(orders) ? ResponseData.success(ResponseCodeEnum.NULL_DATA.getCode())
                : ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), orders);
    }
}

 

3.9.11 使用 Postman 测试接口是否正常

 
  测试接口通过手机号查询批量数据。对应的方法:public ResponseData findList(@RequestBody OrderRequest orderRequest);

接口:localhost:8002/order/findList
提交方式:post
使用工具:Postman

 
  使用一组 Json 格式数据进行查询操作:
 

{
    "userPhone" : "222222"
}

 
  测试结果如下。
 
使用 Postman 测试接口:通过手机号查询批量数据

 
  What!?无论是 IOrderService 接口、OrderDao 接口、OrderServiceImpl 实现类,都没有写抽象方法和实现方法,只需继承 MyBatis-Plus 的 IService 接口和 ServiceImpl 实现类,就完成了简单的查询操作!冷静下来没有啊,真没逗你,MyBatis-Plus 就是这么玩的,写啥方法呢,单表操作还要写方法吗?
 
  就是为什么人们喜欢 MyBatis-Plus 的原因,代码不用写多少,功能却不差,上班可以摸鱼的时间也就多了,正如开头介绍的那样,如丝般顺滑。还有很多功能需要读者自己在网上或工作中慢慢探索了,三言两语说不完。
 

3.9.12 补充:自动填充功能实现

 
  在前面的实体类 Order 中,字段 createTime(创建时间) 添加注解 @TableField(fill = FieldFill.INSERT) ,字段 updateTime(更新时间) 添加注解 @TableField(fill = FieldFill.INSERT_UPDATE) ,目的是自动填充当前时间,那么,注解生效了吗?其实呢,需要写一个实现类,不然不会生效。
 
  【MyBatis-Plus官方自动填充功能说明】:
  https://baomidou.com/pages/4c6bcf/
 
  【MyBatis-Plus官方关于自动填充功能例子 - Gitee开源库】:
  https://gitee.com/baomidou/mybatis-plus-samples/tree/master/mybatis-plus-sample-auto-fill-metainfo
 
  首先,改造一下 Order 实体类代码,将 createTime 和 updateTime 字段的属性由 Date 类型修改为 LocalDateTime 类型。Date 类型是个老古董了,从JDK 1.0 开始就存在,而 LocalDateTime 是从 JDK 1.8 才出现的。LocalDateTime 是一个不可变的日期时间对象,表示日期时间,通常被视为年-月-日-小时-分钟-秒。 MyBatis-Plus官方例子中用到的就是 LocalDateTime ,其实使用 Date 也不会报错,但建议使用新的时间类,新的类出现肯定是为了解决旧的类在某些方面的缺陷。
 

package com.dbydc.zero2one.order.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.lang.NonNull;

import java.time.LocalDateTime;
import java.util.Date;

/**
 * 订单服务 实体类
 * @author 大白有点菜
 * @className Order
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("tb_order")
public class Order {

    /**
     * CREATE TABLE `tb_order` (
     *   `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
     *   `order_num` varchar(64) NOT NULL COMMENT '订单号',
     *   `user_name` varchar(128) DEFAULT NULL COMMENT '用户名',
     *   `user_phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户手机号',
     *   `create_time` datetime DEFAULT NULL COMMENT '创建时间',
     *   `update_time` datetime DEFAULT NULL COMMENT '修改时间',
     *   PRIMARY KEY (`id`)
     * ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
     */

    /**
     * 自增主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 订单号
     */
    //@NonNull
    private String orderNum;

    /**
     * 用户名
     */
    private String userName;

    /**
     * 用户手机号
     */
    private String userPhone;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT)
//    private Date createTime;
    private LocalDateTime createTime;

    /**
     * 修改时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @TableField(fill = FieldFill.INSERT_UPDATE)
//    private Date updateTime;
    private LocalDateTime updateTime;
}

 
  其次,新建一个包 handler ,再创建 MyMetaObjectHandler 自动填充功能实现类,并实现 MetaObjectHandler 接口,重写 insertFill(MetaObject metaObject) 和 updateFill(MetaObject metaObject) 两个方法。记得添加注解 @Component 。代码如下:
 

package com.dbydc.zero2one.order.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * MyBatis-Plus自动填充功能实现类
 * @author 大白有点菜
 * @className MyMetaObjectHandler
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        //log.info("start insert fill ....");
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
//        this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); // 起始版本 3.3.0(推荐使用)
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
//        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date()); // 起始版本 3.3.0(推荐)
        // 或者
        //this.strictInsertFill(metaObject, "createTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
        // 或者
        //this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        //log.info("start update fill ....");
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐)
        // 或者
        //this.strictUpdateFill(metaObject, "updateTime", () -> LocalDateTime.now(), LocalDateTime.class); // 起始版本 3.3.3(推荐)
        // 或者
        //this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug)
    }
}

 
  之前创建其它模块的时候,创建表 tb_order 的 create_time 和 update_time 字段,已经处理为自动插入当前时间:(CURRENT_TIMESTAMP)和(CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP)。现在为了更好地看到当前时间是由MyBatis-Plus的自动填充功能来实现的,需要修改一下 tb_order 表的 create_time 和 update_time 的默认值,默认值修改为 NULL ,即插入数据时,默认填充 NULL 值。如下:
 

CREATE TABLE `tb_order` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `order_num` varchar(64) NOT NULL COMMENT '订单号',
  `user_name` varchar(128) DEFAULT NULL COMMENT '用户名',
  `user_phone` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '用户手机号',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_time` datetime DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

 
  OrderController 业务类新增 add() 方法。此处使用到 org.springframework.beans 包下的 BeanUtils 工具类的静态方法 copyProperties() 来从 OrderRequest 请求实体类复制属性值到新建的 Order 类对象,以代替 Setter 和 Getter 这种写法。新增代码如下:
 

/**
 * 新增数据
 * @param orderRequest 订单请求实体
 * @return
 */
@PostMapping("/add")
public ResponseData add(@RequestBody OrderRequest orderRequest) {
    Order order = new Order();
    BeanUtils.copyProperties(orderRequest, order);
    boolean saveResult = orderService.save(order);

    return saveResult == true ? ResponseData.success(ResponseCodeEnum.SUCCESS.getCode(), OperateStatusEnum.ADD_SUCCESS.getMessage())
            : ResponseData.success(ResponseCodeEnum.ERROR.getCode(), OperateStatusEnum.ADD_FAIL.getMessage());
}

 
  启动 OrderServiceMain8002 服务,使用 Postman 以 POST 方式请求接口:localhost:8002/order/add ,请求的数据格式为 JSON。数据能正常插入,而且时间也能自动填充。
 

{
    "orderNum" : "e555",
    "userName" : "大白有点菜",
    "userPhone" : "333333"
}

 

3.10 不使用数据库时,需移除数据源自动配置或移除组件依赖

 

3.10.1 新建 cloud-order-service8003 子模块

 
  cloud-order-service8003 子模块要引入 cloud-common-api 依赖。cloud-order-service8003 子模块结构如图所示,只有一个 OrderServiceMain8003 引导类和一个 application.yml 配置文件。
 
cloud-order-service8003 子模块结构

 

3.10.2 新建 application.yml 配置文件

 
  application.yml 配置文件内容如下,假设不需要使用到数据库,把数据源配置这部分内容注释掉
 

#服务端口号
server:
  port: 8003

spring:
  application:
    #服务名称
    name: cloud-order-service
  #数据源
#  datasource:
#    driver-class-name: com.mysql.cj.jdbc.Driver
#    username: root
#    password: 123456
#    url: jdbc:mysql://localhost:3306/zero2one?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai

 

3.10.3 创建 OrderServiceMain8003 引导类

 
  OrderServiceMain8003 引导类代码如下:
 

package com.dbydc.zero2one.order;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;

/**
 * Order订单服务引导类
 * @author 大白有点菜
 * @className OrderServiceMain8003
 * @date 2023-04-03
 * @description
 * @since 1.0
 **/
@SpringBootApplication
//@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
//@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})
public class OrderServiceMain8003 {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceMain8003.class, args);
    }
}

 

3.10.4 运行 OrderServiceMain8003 引导类报错及解决方法

 
  正常来说,之前创建的这么多模块中,引导类运行基本不会报错,但是这次运行,OrderServiceMain8003 运行报错如下:
 

***************************
APPLICATION FAILED TO START
***************************

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class


Action:

Consider the following:
	If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
	If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).


Process finished with exit code 1

 
  分析报错日志,明显看出来是配置数据源失败引起报错:Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. 如何解决呢?有两种方法。
 
  方法一,也是网上一搜就能找到答案的方法:OrderServiceMain8003 引导类的 @SpringBootApplication 注解中移除 DataSourceAutoConfiguration 数据源自动配置类。如图所示,运行一下看看效果如何。
 

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})

 
@SpringBootApplication 注解中移除 DataSourceAutoConfiguration 数据源自动配置类
 
  怎么还报错呀?不是移除 DataSourceAutoConfiguration 数据源自动配置类就能解决问题了吗,网上也是这么写的,哪里写错了吗?先说结论,没有写错,只是遗漏了一个 Druid 数据源自动配置类,我们在 cloud-common-api 模块的 pom.xml 中引入 druid-spring-boot-starter 依赖,必须同时把 Druid 数据源自动配置类移除才行! OrderServiceMain8003 引导类修改代码后重新运行,再看效果如何。
 

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})

 
OrderServiceMain8003 引导类同时移除 DataSourceAutoConfiguration 和 DruidDataSourceAutoConfigure 自动配置类
 
  完美! OrderServiceMain8003 引导类正常运行,不再报错了,移除相关数据源自动配置类能解决问题。
 
  再来说说方法二,我们在 cloud-order-service8003 模块的 pom.xml 中移除 Druid 数据连接池依赖(druid-spring-boot-starter)MyBatis-Plus 依赖(mybatis-plus-boot-starter)。写法如下:
 

<?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>SpringCloudZero2One</artifactId>
        <groupId>com.dbydc.zero2one</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-order-service8003</artifactId>

    <dependencies>
        <!-- 引入公共API cloud-common-api 依赖即可 -->
        <dependency>
            <groupId>com.dbydc.zero2one</groupId>
            <artifactId>cloud-common-api</artifactId>
            <version>${project.version}</version>
            <exclusions>
                <!-- 移除Druid数据库连接池依赖 -->
                <exclusion>
                    <groupId>com.alibaba</groupId>
                    <artifactId>druid-spring-boot-starter</artifactId>
                </exclusion>
                <!-- 移除MyBatis-Plus依赖 -->
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

</project>

 
在 cloud-order-service8003 模块的 pom.xml 中移除 Druid 数据连接池依赖和 MyBatis-Plus 依赖。
 
  完美! OrderServiceMain8003 引导类正常运行,不再报错了,移除相关依赖组件也能解决问题。
 

3.11 application.yml 配置文件的多种形式

 

3.11.1 application.yml 在实际开发中的多种形式

 
  除了应用程序属性文件,Spring Boot 还将尝试使用命名约定 application-{profile} 加载特定于配置文件的文件。
 
  在实际开发中,除了看到 application.yml 这么一个配置文件,还有 application-dev.ymlapplication-test.ymlapplication-prod.yml 这三个配置文件。其实看后缀名就知道,这三个配置文件的使用环境分别是开发环境、测试环境和生产环境,通过主导的 application.yml 即可轻松完成切换。通过将属性 spring.profiles.active 配置为 devtestprod 值,就可以加载对应的配置文件,例如 application-{profile}.ymlprofile 为 dev、test、prod
 
  【Spring Boot - 配置文件特定文件】:
  https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-profile-specific-properties
 
请添加图片描述
 

3.11.2 新建 cloud-order-service8004 子模块并移除数据源依赖

 
  cloud-order-service8004 模块的 pom.xml 内容如下,同样引入公共的 cloud-common-api 依赖,同时移除 Druid 和 MyBatis-Plus 依赖。
 

<?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>SpringCloudZero2One</artifactId>
        <groupId>com.dbydc.zero2one</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-order-service8004</artifactId>

    <dependencies>
        <!-- 引入公共API cloud-common-api 依赖即可 -->
        <dependency>
            <groupId>com.dbydc.zero2one</groupId>
            <artifactId>cloud-common-api</artifactId>
            <version>${project.version}</version>
            <exclusions>
                <!-- 移除Druid数据库连接池依赖 -->
                <exclusion>
                    <groupId>com.alibaba</groupId>
                    <artifactId>druid-spring-boot-starter</artifactId>
                </exclusion>
                <!-- 移除MyBatis-Plus依赖 -->
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

</project>

 

3.11.3 新建配置文件

 
  在 resources 资源目录下新建 application.yml、applicaiton-dev.yml、applicaiton-test.yml 和 applicaiton-prod.yml 四个配置文件。说明:只在 application.yml 中定义一个属性 blogger ,其它三个配置文件定义的属性是 use.configurationFile ,属性值不同定义的目的验证程序启动会同时加载 application.yml 和指定的 applicaiton-{profile}.yml,同时 applicaiton-{profile}.yml 的 server.port 属性值会覆盖 application.yml 的 server.port 属性值
 
  (1)application.yml 配置文件
 

#服务端口号
server:
  port: 8004

blogger: 大白有点菜

spring:
  application:
    #服务名称
    name: cloud-order-service
  profiles:
    #开发环境
    active: dev
    #测试环境
#    active: test
    #生产环境
#    active: prod

 
  (2)application-dev.yml 配置文件
 

#服务端口号
server:
  port: 9001

use:
  configurationFile: application-dev.yml 和 application.yml

 
  (2)application-test.yml 配置文件
 

#服务端口号
server:
  port: 9002

use:
  configurationFile: application-test.yml 和 application.yml

 
  (3)application-prod.yml 配置文件
 

#服务端口号
server:
  port: 9003

use:
  configurationFile: application-test.yml 和 application.yml

 

3.11.4 创建 OrderTest 测试类

 
  OrderTest 测试类代码如下:
 
创建 OrderTest 测试类
 

package com.dbydc.zero2one.order;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * Order测试类
 * @author 大白有点菜
 * @className OrderTest
 * @date 2023-04-06
 * @description
 * @since 1.0
 **/
@SpringBootTest
@Slf4j
public class OrderTest {

    @Value("${server.port}")
    private String serverPort;

    @Value("${blogger}")
    private String blogger;

    @Value("${use.configurationFile}")
    private String configurationFile;

    @Test
    public void contextLoads() {
        //log.warn("嘿," + blogger + ",应用程序的端口号:" + serverPort + " ,优先使用的配置文件:" + configurationFile);
        log.warn("嘿,{},应用程序的端口号:{},优先使用的配置文件:{}", blogger, serverPort, configurationFile);
    }
}

 

3.11.5 验证

 
  当application.yml 的 spring.profiles.active 的属性值设置为 dev ,运行 OrderTest 测试类的 contextLoads() 方法,结果如下,使用的服务端口是 9001 而不是 8004,use.configurationFile 属性值内容是 application-dev.yml 的
 
使用 application-dev.yml并运行 OrderTest 测试类的 contextLoads() 方法
 
  当application.yml 的 spring.profiles.active 的属性值设置为 test ,运行 OrderTest 测试类的 contextLoads() 方法,结果如下,使用的服务端口是 9002 而不是 8004,use.configurationFile 属性值内容是 application-test.yml 的
 
使用 application-test.yml并运行 OrderTest 测试类的 contextLoads() 方法
 
  当application.yml 的 spring.profiles.active 的属性值设置为 prod ,运行 OrderTest 测试类的 contextLoads() 方法,结果如下,使用的服务端口是 9003 而不是 8004,use.configurationFile 属性值内容是 application-prod.yml 的
 
使用 application-prod.yml并运行 OrderTest 测试类的 contextLoads() 方法
 
  实际开发就是通过切换配置文件,然后部署在不同环境中,各个伙伴进行接口联调或自测,互不影响。开发中还会使用到灰度功能,此处不展开讨论。
 

3.12 .properties 和 .yml 格式的配置文件的优先级

 
  Spring 官方文档说到,如果在同一位置具有同时具有 .properties 和 .yml 格式的配置文件,则 .properties 优先
 
  【Spring Boot - 外部化配置】:
  https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config
 
.properties 和 .yml 格式的配置文件加载顺序
 

3.12.1 新建 LoadedConfigFileListener 类记录加载的属性源

 
  添加监听 ApplicationReadyEvent 的组件来记录加载的属性源,新建类 LoadedConfigFileListener 。代码参考自 Stack Overflow 的一个问题:Spring Boot: When application.properties and application.yml are loaded by a spring boot application
 
LoadedConfigFileListener 新建
 
LoadedConfigFileListener 类代码参考
 

package com.dbydc.zero2one.order;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.config.ConfigFileApplicationListener;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.stereotype.Component;

import java.util.Iterator;

/**
 * 添加监听 ApplicationReadyEvent 的组件来记录加载的属性源
 * @author 大白有点菜
 * @className LoadedConfigFileListener
 * @date 2023-04-06
 * @description
 * @since 1.0
 **/
@Component
@Slf4j
public class LoadedConfigFileListener implements ApplicationListener<ApplicationReadyEvent>, Ordered {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        MutablePropertySources propertySources = event.getApplicationContext().getEnvironment().getPropertySources();
        Iterator<PropertySource<?>> propertySourceIterator = propertySources.iterator();
        propertySourceIterator.forEachRemaining(propertySource -> log.error("Successfully loaded: \"{}\" into application context", propertySource.getName()));
    }

    @Override
    public int getOrder() {
        return ConfigFileApplicationListener.DEFAULT_ORDER + 1;
    }
}

 

3.12.2 配置文件加入日志记录级别

 
  LoadedConfigFileListener 类记录加载的属性源,需要开启日志记录级别。在配置文件中加入以下内容,DEBUG 模式监控 Spring Framework Core 运行的属性源记录,TRACE 模式监控 Spring Boot 上下文(Context)配置的属性源记录。所有配置文件都添加上,例如 application.yml
 

logging:
  level:
    org:
      springframework:
        core:
          env: DEBUG
        boot:
          context:
            config: TRACE

 

3.12.3 创建 application.properties 配置文件

 
  配置文件除了支持 *.yml 的写法,还支持 *.properties 的写法,同时存在的话,application.properties 一定会被加载,application.yml 也会被加到列表中,优先级 application.properties 更高。
 

#服务端口号
server.port=8104

blogger=application.properties-大白有点菜

use.configurationFile=application.properties

#服务名称
spring.application.name=cloud-order-service

logging.level.org.springframework.core.env=DEBUG

logging.level.org.springframework.boot.context.config=TRACE

 
  强烈建议不要同时存在 application.properties 和 application.yml ,使用 application.yml 是主流。如果配置文件的某个属性需要配置为中文的值,那么 application.properties 读取是乱码,而 application.yml 读取是正常的。同时存在并不会报错,如果搞混属性配置,可能导致严重的程序运行事故。
 

3.12.4 验证 application.properties 和 application.yml 的加载顺序和优先级

 
  application.yml 的内容如下:
 

#服务端口号
server:
  port: 8004

blogger: 大白有点菜

use:
  configurationFile: application.yml

spring:
  application:
    #服务名称
    name: cloud-order-service

logging:
  level:
    org:
      springframework:
        core:
          env: DEBUG
        boot:
          context:
            config: TRACE

 
  运行 OrderTest 测试类的 contextLoads() 方法,从日志可以看出,日志从上到下顺序是先 application.properties 后 application.yml ,这打印的是配置文件的加载顺序吗?打印的内容确实是来自 application.properties 文件(blogger 属性的值是:application.properties-大白有点菜,打印出来的中文是乱码),这是覆盖了 application.yml 文件的属性值吗?
 
请添加图片描述

 
  按 Spring 官方文档的说法,Spring Boot 使用一种非常特殊的 PropertySource 顺序,旨在允许合理地覆盖值。后面的属性源可以覆盖前面定义的值【出处】https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.external-config
 
Spring Boot 使用一种非常特殊的 PropertySource 顺序,旨在允许合理地覆盖值。后面的属性源可以覆盖前面定义的值
 
  如果按官方的说法,配置文件的属性值是存在覆盖的,那么配置文件的加载顺序应该是:先加载 application.yml ,后加载 application.properties 。application.properties 的属性值优先,并覆盖了 application.yml 的属性值。那日志输出的:先 application.properties 后 application.yml 又是个什么情况呢?网上的博客说法各式各样,是对是错很难判断,官方文档也没详细说明,很难找到一种权威的正确结论,我不知道如何去验证这些。
 
  如果 application.yml 配置了 spring.profiles.active=dev ,那 application.properties 的属性值还会优先读取吗? 猜想没有意义,“实践”才是最有效的戳破谬论的武器。application.yml 文件修改如下:
 

#服务端口号
server:
  port: 8004

blogger: 大白有点菜

use:
  configurationFile: application.yml

spring:
  application:
    #服务名称
    name: cloud-order-service
  profiles:
    #开发环境
    active: dev
    #测试环境
#    active: test
    #生产环境
#    active: prod

logging:
  level:
    org:
      springframework:
        core:
          env: DEBUG
        boot:
          context:
            config: TRACE

 
  application-dev.yml 内容如下:
 

#服务端口号
server:
  port: 9001

blogger: application-dev.yml-大白有点菜

use:
  configurationFile: application-dev.yml 和 application.yml

 
  运行 contextLoads() 方法,结果很意外,居然打印的是 application-dev.yml 文件的内容!得出结论:优先级 application-dev.yml > application.properties > application.yml
 
运行 contextLoads() 方法,结果很意外,居然打印的是 application-dev.yml 文件的内容
 
  两个测试,我对日志输出中的红色部分的内容(即上面提到的配置文件加载顺序):Config resource 'class path resource [xxx.yml]' via location 'optional:classpath:/' ,提出一个更合理结论:日志打印出来的是配置文件的读取优先级,并不是加载顺序!
 

3.12.5 application.yml 和 application.yaml 哪个优先级高

 
  配置文件还支持 *.yaml 的写法,哪个优先级高呢?还是直接验证吧,更有说服力。需要注释掉 application.properties 文件内容,不然会造成干扰
 

##服务端口号
#server.port=8104
#
#blogger=application.properties-大白有点菜
#
#use.configurationFile=application.properties
#
##服务名称
#spring.application.name=cloud-order-service
#
#logging.level.org.springframework.core.env=DEBUG
#
#logging.level.org.springframework.boot.context.config=TRACE

 
  application.yml 文件内容如下,端口号为8004
 

#服务端口号
server:
  port: 8004

blogger: application.yml-大白有点菜

use:
  configurationFile: application.yml

spring:
  application:
    #服务名称
    name: cloud-order-service

logging:
  level:
    org:
      springframework:
        core:
          env: DEBUG
        boot:
          context:
            config: TRACE

 
  application.yaml 文件内容如下,端口号为8204
 

#服务端口号
server:
  port: 8204

blogger: application.yaml-大白有点菜

use:
  configurationFile: application.yaml

spring:
  application:
    #服务名称
    name: cloud-order-service

logging:
  level:
    org:
      springframework:
        core:
          env: DEBUG
        boot:
          context:
            config: TRACE

 
  运行结果可以看出,application.yml 优先级比 application.yaml 更高!
 
运行结果可以看出,application.yml 优先级比 application.yaml 更高
 

3.12.6 配置文件优先级总结

 
  application-{profile}.yml > application.properties > application.yml > application.yaml
 

3.13 bootstrap.yml 配置文件

 
  除了 application.yml 和 application.properties 这种应用级别的配置文件,还有一种系统级别的配置文件:bootstrap.ymlbootstrap.propertiesbootstrap.yml 需要在 Spring Cloud 框架下才会生效,Spring Boot 框架只识别 application.yml 这种配置文件
 
  使用 Spring Cloud Alibaba 的 Nacos 的 Config 功能时,网址:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html,就需要创建一个 bootstrap.properties 文件用来配置 Nacos server 的地址和应用名
 
  那 bootstrap.yml 和 application.yml 有什么区别呢?
 

3.13.1 bootstrap.yml 和 application.yml 的区别

 
  Stack Overflow 上有个答案,还算是比较有权威性的说法,问题如下:What is the difference between putting a property on application.yml or bootstrap.yml in spring boot? 。国外的牛人就是不一样,直接去问了 Spring Cloud 团队的人,答案如下(是否有误,自行斟酌):
 

1、bootstrap.yml is loaded before application.yml.
1、bootstrap.yml 在 application.yml 之前加载。
 
2、It is typically used for the following:
2、它通常用于以下情况:
 

  • When using Spring Cloud Config Server, you should specify spring.application.name and spring.cloud.config.server.git.uri inside bootstrap.yml.
  • 使用 Spring Cloud Config Server 时,您应该在 bootstrap.yml 中指定 spring.application.name 和 spring.cloud.config.server.git.uri
     
  • Some encryption/decryption information.
  • 一些加密/解密信息。

 
3、Technically, bootstrap.yml is loaded by a parent Spring ApplicationContext. That parent ApplicationContext is loaded before the one that uses application.yml.
3、从技术上讲,bootstrap.yml 由父 Spring ApplicationContext 加载。该父 ApplicationContext 在使用 application.yml 的父应用程序上下文之前加载。

 
  为什么Config Server 需要将这些参数放入bootstrap.yml 中? 答案如下:
 

  When using Spring Cloud, the ‘real’ configuration data is usually loaded from a server. In order to get the URL (and other connection configuration, such as passwords, etc.), you need an earlier or “bootstrap” configuration. Thus, you put the config server attributes in the bootstrap.yml, which is used to load the real configuration data (which generally overrides what’s in an application.yml [if present]).
  使用 Spring Cloud 时,“真实”配置数据通常是从服务器加载的。为了获取 URL(和其他连接配置,例如密码等),您需要更早的或“引导程序”配置。因此,您将配置服务器属性放在 bootstrap.yml 中,它用于加载真正的配置数据(通常会覆盖 application.yml [如果存在] 中的内容)。

 
Stack Overflow关于bootstrap.yml的问题描述
 

3.13.2 bootstrap.yml 配置文件生效

 
  要想配置文件 bootstrap.yml 生效,需要在 pom.xml 引入依赖:
 

<!-- 使 bootstrap.yml 生效 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

 
  测试时,需要注释掉所有的 application 配置文件,不然看不到加载 boostrap.yml 读取内容的效果。运行结果如下:
 
加载并读取bootstrap.yml配置文件
 

3.14 application.properties 配置文件存在中文,加载出来是乱码

 
  在前面的测试例子中演示过,application.properties 配置文件存在中文,加载出来的是乱码,使用 application.yml 配置文件不会有这种问题。
 

四、强大的浏览器网页全文翻译插件、IDEA日志输出美化插件、浏览器JSON格式化插件

 
  本文章中的很多截图,绿色背景是中文翻译,中文上面是英文原文,什么网页全文翻译插件这么强大?当然是 划词翻译 啦!官方网址:https://hcfy.app/秒杀“Google 翻译”插件。支持谷歌浏览器和火狐浏览器,支持的翻译服务超多,我都是默认使用谷歌进行翻译(需要梯子,梯子也是收费的,自行解决,不提供方式),免费试用三次翻译,超过需要购买。其实费用也很便宜,使用Wechat支付只要 60RMB 一年(升价了,我购买时才 45 RMB),128RMB 永久,需要创建一个账号,使用邮箱绑定即可。
 
浏览器网页全文翻译插件“划词翻译”
 
“划词翻译”支持的翻译服务
 
  前后端联调都是使用 JSON 这种数据格式的,浏览器安装一款 JSON 数据格式化插件很有必有,从此告别复制 JSON 数据到在线JSON格式化网站进行处理的麻烦骚操作。我用的是:JSON Formatter
 
JSON Formatter谷歌浏览器插件
 
  文章中程序运行后的日志以各种颜色显示出来,如此好看,又是哪个 IDEA 的插件呢?我的最爱:Grep Console 。效果如下,如看美丽的风景一样舒畅。需要在 Settings -> Plugins -> Marketplace 搜索“Grep Console”并安装。设置很简单,右键弹出“Grep Console”设置面板,一般颜色设置建议是:ERROR 使用红色,WARN 使用黄色,INFO 使用浅绿色,DEBUG 和 TRACE 使用哪种颜色看个人兴趣,建议使用颜色较淡的。最后,尽情享受多彩的日志吧!
 
文章中程序运行后的日志以各种颜色显示出来
 
IDEA安装 Grep Console插件
 
右键弹出“Grep Console”设置面板
 
颜色设置建议
 

五、结语

 
  终于完工!耗费这么多精力和时间教大家如何从0到1搭建一个 Spring Cloud 项目,字数已突破10万字。何为0,何为1,这篇文章能很好地诠释这一点吧!笔者我能力有限,在文章中的某些词组或语句可能存在描述性错误,大概理解意思就行。在文章开头也说了,这会是一个系列博客,后面继续有优质的文章发布,敬请期待。
 
  如果觉得对您有帮助,请动动小指头,点赞+收藏+关注 吧,我的粉丝还很少啊!最喜欢阳哥说的一句口头禅:天上飞的理念,要有落地的实现!
 
  授“鱼”不如授“渔”,面试背的Java八股文都是虚的,不如实操来得实际,面试要求求职者啥都会,啥都精通,哪来那么多精通?面试造火箭,入职拧螺丝,这就是互联网行业的现状。
 
  没有985/211学历光环庇护,也没有大公司工作经历闪耀,别人写的就是好文,内容就不会有问题吗,末流学历写的就不值一看吗?
 
  每一篇博客,都是倾注十成的精力,百分之两百的时间,配合官方文档或书籍或国外专业技术论坛,力求做到内容更准确,更全面。别人一周写3到4篇文章,我可能一周只出1篇,因为:只求“质”,不追“量”!
 
  遇到问题多百度、Google,多上Stack Overflow(大牛多,答案都比较专业,国内百度的答案很杂,对错不好判断),搜关键错误语句,总能找到解决方法。
 
  项目 SpringCloudZero2One 代码我会提交到 Github、Gitee上,同时也放到百度网盘,多种方式供大家下载,下载地址:
 

百度网盘:https://pan.baidu.com/s/1EMNJR9D3N7ptAFjFbfLIZw?pwd=yyds
 
Gitee:https://gitee.com/zyt2021/SpringCloudZero2One
 
Github:https://github.com/dbydc/SpringCloudZero2One

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大白有点菜

你的鼓励决定文章的质量

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值