前言
前面讲了SSM框架,还讲了Spring Boot工程,学完这些,就可以算是一名初级Java工程师了,也就可以做Java相关的一些开发了,但这还远远不够。如果公司规模不大,只需要写一些接口,那么前面的课程算是绰绰有余的,如果对未来发展有很大的期望,希望能够进一些大厂,有更好的发展,那么接下来的课程你绝对不容错过。今天我们要讲的内容是“微服务”,相信你对Java有些了解的话都应该听过这个名字,微服务难吗?听名字似乎是挺难的,那么这篇博客的目的就是为你了解微服务打基础,我不敢说学完之后你能把微服务做的多好多好,起码你会有个方向,知道微服务是什么,然后再去有针对性的学习,就这么努力下去,相信不久的将来,你一定会成为一名高级Java工程师。用大锤的话说:出任CEO,迎娶白富美,走上人生巅峰。
服务器端的发展历程
要了解微服务,我们就要知道其出现的目的是什么?为了大家很好的理解这一目的,我们就要说说服务端的发展历程,从最初的服务端到微服务的诞生,究竟发生了什么?是什么催动着微服务的诞生。
早期的服务器
早期的服务器没有现在那么多的功能,仅仅是一些静态页面,那时候恐怕网吧和网络游戏还没出现,那时的电脑还没有现在那么多的功能(ps:虽然我也没经历过那个年代),那时的网站只能做一些信息的展示,看一些消息,看新闻已经是很奢侈的体验了,我们现在很多人并没有经历过这一历程,但你一定记得刚有手机时订阅的新闻短信,基本上只能看,不能互动。
动态的页面
页面可以根据数据库的数据动态变化,数据库登上了历史的舞台,用户已经能够简单的修改一些内容,比如用户名,密码。大家一定都在电脑上注册过QQ,那时候七位数,八位数的QQ还是很多的,现在都出到多少位了?博主印象里前些年有十一位的QQ号,现在就不知道了。
用户内容网站
这样的网站像论坛,贴吧,微博,这些网站的诞生基本都在21世纪,网站的内容都来自用户发布,现在多数社交性质网站都是如此,我们对此算是最了解的。这种类型的网站也更适用互联网的快速发展,满足人们的日常娱乐需求。
微服务
随着智能手机的普及,网民数量急剧增加,原来的服务器配置已不能支撑高并发,高访问量,此时就需要多台服务器共同来支撑网站的运行。我们试想,若是一个页面3s内无法显示,我们还会继续等待吗?所以要满足基本的高并发,高可用,高性能,服务器的革命也就此开启,所以传统的Java就是简单的写接口,甚至有些公司还会写一些web页面,相信已经很少了,前后端分离是趋势,微服务就算是另一个方向了。所以,我们经常把项目分为两大类:
企业级应用
如其名,一般是专门服务于某个企业的,但也包括了很多行业,如政府,企业,医疗等,他们有一个很好听的名字:门户网站。这些网站有个特点,他们是针对某一人群的,所以不会像双十一访问京东淘宝那样的高并发,也不会有很多的替代品。他们的功能一般都较单一,对权限的要求也不高。但随着多元化的发展和生存挑战,一些企业级应用也开始慢慢的出现了一些周边产品,导致项目内嵌的功能越来越多,纯粹的功能型应用也越来越少,微服务也被应用于此类应用,这里我就不点名了。
互联网应用
互联网型应用,一般都是面向不同的社会群里,比如购物型,外卖型,娱乐型,生活型,名字就不提了。各行各业的人都有这样的需求,这样的应用,他们的用户群体广也就导致了用户量庞大这一特点,在某些特定的时间段会出现高并发的情况,但这并不代服务器就可以懈怠,这更要求服务器能够承受这样的压力,就像是线程池,数据库连接池,需要的时候会进行资源的自动分配,这在微服务里是比较常见的。我们常听到一些比如主从,负载均衡等都是微服务里不可或缺的部分。
微服务介绍
什么是微服务?
说起微服务,你不得不知道一个人:Martin Fowler(马丁·福勒),微服务的概念是他在2014年提出的,在此之前,各厂商也是有自己的微服务概念的,但是并不统一,原理是也就是增加服务器,后来Martin Fowler提出这个概念后,行业内就开始遵守这个概念,统一了微服务,就像秦始皇统一度量衡一样,下面放上大佬的照片,来瞻仰一下:
大佬不愧是大佬,从发量就可窥探一二。言归正传,说说微服务的概念。微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通信。同时服务会使用最小的模的集中管理能力,服务可以用不同的编程语言与数据库等组件实现。
简单来说,微服务就是将一个大型项目的各个业务代码拆分成多个互不干扰的小项目,而这些小项目专心完成自己的功能,而且可以调用别的小项目的方法,从而完成整体功能。
听起来有点移动端组件化的概念,有木有?有木有?组建间相互调用,是不是?有没有做移动端转Java的老铁?你一定知道博主在说什么。如果你明白,那微服务你就懂了一大半了。
为什么使用微服务
我们假设一个单体的项目,没有使用微服务,一旦用户量激增,就无法快速响应,为了提高服务器运行的效率,只能往上使劲的堆服务器,堆内存,效果是有的,但并没有很好。
我们举例说明,这就好比一个小饭店,点餐是一个人,做饭是一个人,饭点到了,一堆人涌入,点餐还是一个人,做饭也还是一个人,相信在市区上班的童鞋都有过等餐一个小时的经历,这就是一个单体的项目,如果只增加厨师,虽然出餐快了,但是点餐还是一个人,效果并不明显。如果增加厨师的同时,也增加点餐服务员的数量,即使有人请假也不会影响出餐的速度,而且大点的餐馆分工明确,点餐的,收银的,做饭的,大家互相协作,又互不影响。这就是一个微服务项目。体现在现在的应用内就是:有的模块异常,但其他模块运行正常,各模块之间相互关联,但互不影响。
怎么使用微服务
Martin Fowler提出这个标准之后,各厂商都开始慢慢支持,到如今,行业内几乎统一,也由此诞生了一些很好的工具,可以帮助我们快速的搭建微服务的框架。否则我们自己编写支持这个标准的代码是不现实的,必须通过现成的框架或组件完成满足这个微服务标准的项目结构和格式。Spring Cloud就是其中的佼佼者,我们常叫做Spring Cloud全家桶。
Spring Cloud
什么是Spring Cloud
SpringCloud是由Spring提供的一套能够快速搭建微服务架构程序的框架集,我们上面也说了,他又叫做Spring Cloud全家桶,所以他不是一个框架,而是很多框架的统称。他的目的就是为了搭建微服务架构。
Spring Cloud的提供者主要有:
- Spring自己编写的框架和软件
- Netflix(奈非):早期提供了很多(全套)微服务架构组件
- alibaba(阿里巴巴):新版本SpringCloud,推荐使用(正在迅速占领市场)
我们目前使用的Spring Cloud大多是阿里巴巴的微服务组件,因为毕竟是中国人写的,就不需要我们再去翻译,而且由于阿里的双十一的并发量可以看出,这个框架很成熟,老外写的也很好,但没那么方便,阿里的这个依托于自己的系统,目前来说很成熟,毕竟双十一大家都是经历过的。从功能上微服务的功能可以细分为下面的内容:
- 微服务的注册中心
- 微服务间的调用
- 微服务的分布式事务
- 微服务的限流
- 微服务的网关
以上等等,还有其他,接下来,我们来慢慢介绍这些功能。在学习这些功能之前,我希望大家可以先去下载一个Nacos软件,如果安装过虚拟机则更好,你可以直接使用Docker来做,说起来这些东西就会比较头疼,能读到这里的童鞋想必都是有Java基础的,这些基础软件我就默认大家都是知道的,但本着这篇博客要上3w字的心态,咱们还是慢慢说吧,软件部分提到的,大家需要自行下载和配置。
Nacos注册中心
在开始学习之前,你需要先下载Nacos软件,不用多说了吧。下载好先放着,我们来说说这个软件和它该怎么用。
下载地址
就是下载比较慢,要有些耐心。
什么是Nacos
Nacos是Spring Cloud Alibaba提供的一个软件,主要有注册中心和配置中心的功能。微服务的统筹管理则依赖于这个注册中心,像组件化的注册那样,只有注册了才能成为微服务的一部分。
Nacos启动必须要保证当前系统配置了java环境变量,就是要环境变量中有JAVA_HOME的配置指向安装jdk的路径,这一步在学习Java之前你肯定是已经配置好了,没有的可以去百度哈,不再赘述。
解压后的软件包,进入bin一级目录,会看到如下文件:
cmd结尾是windows系统的启动/结束版本,sh结尾是linux和mac版本的,startup是启动,shutdown是停止,很明显是吧?
windows启动要先进入dos命令,进入bin下的文件,输入:
startup.cmd -m standalone
博主是mac电脑,输入命令如下:
sh startup.sh -m standalone
其实差不多,就是后缀不一样,解释下命令,standalone是单机运行模式,不带的话运行就会失败,默认8848端口,启动后可以验证nacos是否启动成功,在浏览器输入:
看到如下网页,则说明nacos服务启动成功:
如果启动后打不开网页,一定是启动失败,我直接说怎么做,用文本打开启动文件,看JAVA_HOME后面有没有下面这串路径,没有的话添加上,Java的版本要使用自己安装的版本。
保存后在此输入命令并回车:
这个问题博主就遇到了,不知道自己版本的可以这么做:
open /Library/Java/JavaVirtualMachines
接着就会弹出Java jdk所在目录,一级一级点进去,就知道完整路径了,注意,只写/Library/Java是不行的,这个博主也不知道原因,你在目录下也看不到Java文件,可能是隐藏文件吧。
成功运行后,打开登录页面,默认的登录账户和密码都是nacos。
登录成功进入nacos后台,页面如下:
可以切换中英文。这时要注意,启动nacos的命令窗口不能关闭,否则服务将可能关闭导致无法注册到nacos后台,微服务就无法成形。也许你已经觉得有些繁琐了,这时更要耐着性子,小心小心再小心,错一步都不行,微服务不难,难的是这些步骤,还有后面的一些配置,错一个符号都不行,都将导致注册失败。
创建微服务项目
为了便于大家理解微服务,这个项目不会特别复杂,但微服务该有的它基本都会有。后面如果项目中用到,可根据需求做出变更。
创建微服务的壳
本质还是一个boot工程:
依赖这里我们什么也不选:
接着删除src文件,就不贴图了,没错,删除src文件,大家不要怀疑。这时候我们其实要写pom文件了,这里先放一放,等下回给大家贴一个pom文件内容,我们接着进行下一步。
创建通用子模块
工程创建
选中cloud文件,右键新建一个工程:
选择Module,之后就是创建新工程的步骤:
这里大家最好按照我的命名方式来做,以免出错。下一步还是什么也不勾选,博主稍后给出pom文件应有的内容,大家贴进去。由于这个工程是一个用于存放通用内容的工程,所以这里不会进行测试相关的,也不太会有资源的使用,为了方便查看,删除test文件夹,删除resources文件夹,删除CloudCommonsApplication启动文件(一定要删除,否则会导致报错,刚刚发生,才回来补充的,真是造孽啊)。
到这里,我们就不得不开始pom文件的依赖添加了,否则将很难进行下去。
依赖管理
我们先在cloud项目下的pom文件中添加下面一大串依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.4</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.codingfire</groupId> <artifactId>cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud</name> <description>微服务框架练习</description> <packaging>pom</packaging> <modules> </modules> <properties> <java.version>1.8</java.version> <spring-cloud.version>2020.0.3</spring-cloud.version> <spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version> <spring-boot.version>2.5.4</spring-boot.version> <spring-boot-configuration-processor.version>2.3.0.RELEASE</spring-boot-configuration-processor.version> <spring-security-jwt.version>1.0.10.RELEASE</spring-security-jwt.version> <mybatis-spring-boot.version>2.2.0</mybatis-spring-boot.version> <mybaits-plus.version>3.4.1</mybaits-plus.version> <pagehelper-spring-boot.version>1.4.0</pagehelper-spring-boot.version> <mysql.version>8.0.26</mysql.version> <lombok.version>1.18.20</lombok.version> <knife4j-spring-boot.version>2.0.9</knife4j-spring-boot.version> <spring-rabbit-test.version>2.3.10</spring-rabbit-test.version> <spring-security-test.version>5.5.2</spring-security-test.version> <fastjson.version>1.2.45</fastjson.version> <druid.version>1.1.20</druid.version> <jjwt.version>0.9.0</jjwt.version> <seata-server.version>1.4.2</seata-server.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <!-- 依赖管理 --> <dependencyManagement> <dependencies> <!--seata-all--> <dependency> <groupId>io.seata</groupId> <artifactId>seata-all</artifactId> <version>${seata-server.version}</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <!-- MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> <scope>runtime</scope> </dependency> <!-- Alibaba Druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <!-- MyBatis Spring Boot:数据访问层MyBatis编程 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis-spring-boot.version}</version> </dependency> <!-- MyBatis Plus Spring Boot:MyBatis增强 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybaits-plus.version}</version> </dependency> <!-- MyBatis Plus Generator:代码生成器 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>${mybaits-plus.version}</version> </dependency> <!-- PageHelper Spring Boot:MyBatis分页 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pagehelper-spring-boot.version}</version> </dependency> <!-- Spring Boot:基础框架 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Web:WEB应用 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Freemarker:MyBaits Plus Generator的辅助项 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Validation:验证请求参数 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Security:认证授权 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Oauth2:认证授权 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot配置处理器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${spring-boot-configuration-processor.version}</version> </dependency> <!-- Spring Security JWT --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>${spring-security-jwt.version}</version> </dependency> <!-- Knife4j Spring Boot:在线API --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>${knife4j-spring-boot.version}</version> </dependency> <!-- Spring Boot Data Redis:缓存 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Data MongoDB:缓存 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Data Elasticsearch:文档搜索 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot AMQP:消息队列 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Actuator:健康监测 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Cloud家族 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- Spring Cloud Alibaba --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency> <!-- Alibaba FastJson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <!-- JJWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jjwt.version}</version> </dependency> <!-- Spring Boot Test:测试 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> <!-- Spring Rabbit Test:消息队列测试 --> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit-test</artifactId> <version>${spring-rabbit-test.version}</version> <scope>test</scope> </dependency> <!-- Spring Security Test:Security测试 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <version>${spring-security-test.version}</version> <scope>test</scope> </dependency> <!--seata整合springboot--> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>${seata-server.version}</version> </dependency> </dependencies> </dependencyManagement> </project>
贴到自己的项目里面,如果你的项目名和博主有出入,对照改下。
这里解释下properties中的内容,因为为了使微服务中使用依赖版本统一,便于管理,所以这里抽离了版本的内容,可以在下面dependencies中看到version中的version已经变成了properties中定义的名字。这个需要你记住,就需要这么做。
接着我们来到cloud-commons工程中的pom文件,这里就要涉及到微服务里面的父子工程的概念。因为cloud是父工程,所以commons工程中pom文件中的parent要改成cloud工程中的信息:
这里是已经修改过的,对照看下所处的位置,一定要这么写,否则将无法进行关联。
接着贴上commons工程中pom文件的内容:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.codingfire</groupId> <artifactId>cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.codingfire</groupId> <artifactId>cloud-commons</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-commons</name> <description>Demo project for Spring Boot</description> <properties> <java.version>17</java.version> </properties> <dependencies> <!--在线api文档--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency> <!-- Spring Boot Web:WEB应用 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </exclusion> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> </exclusion> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> </dependencies> </project>
做完之后,还有一个关键性的一步,需要在父工程cloud中的pom文件中增加子工程commons:
<modules> <module>cloud-commons</module> </modules>
在工程中找下modules,添加commons到里面。这时候微服务的关联部分就建立起来了,类似commons的工程在微服务中还有很多,下次再创建时将不再赘述。
做完之后,我们在此工程下创建一个类,用于后面的微服务的演示,这里以前文中提到的阿里命名规范的pojo来堆model类进行命名,打击以后在开发中尽量按照这种方式来做,形成一个规范。
pojo模型类创建
选中com.codingfire.cloud.commons包,右键新建类如下:
大家还记得model类第一步要干嘛吗?实现Serializable接口,接着创建属性:
@ApiModel("购物车新增DTO")
@Data
public class CartAddDTO implements Serializable {
@ApiModelProperty(value = "商品编号",name = "commodityCode",example = "PC100")
private String commodityCode;
@ApiModelProperty(value = "商品单价",name = "price",example = "188")
private Integer price;
@ApiModelProperty(value = "商品个数",name = "count",example = "5")
private Integer count;
@ApiModelProperty(value = "用户ID",name = "userId",example = "UU100")
private String userId;
}
@ApiModel注解是用在接口相关的实体类上的注解,它主要是用来对使用该注解的接口相关的实体类添加额外的描述信息,并且常常和@ApiModelProperty注解配合使用。而@ApiModelProperty注解则是作用在接口相关实体类的属性(字段)上的注解,用来对具体的接口相关实体类中的参数添加额外的描述信息,除了可以和 @ApiModel 注解关联使用,也会单独拿出来用。
@Data注解的主要作用是提高代码的简洁,使用这个注解可以省去代码中大量的get()、 set()、 toString()等方法,要使用 @Data注解要先引入lombok,我们已经在cloud中全局引入,这个省略的方法前文中有提到,但似乎没有说明,从前文看过来的童鞋注意啦,敲黑板了,可要记住了。
接着创建实体类Cart,即购物车model:
package com.codingfire.cloud.commons.pojo.cart.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class Cart implements Serializable {
private Integer id;
// 商品编号
private String commodityCode;
// 价格
private Integer price;
// 数量
private Integer count;
// 用户id
private Integer userId;
}
注意包的层级:
接着创建订单模块的类:
package com.codingfire.cloud.commons.pojo.order.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@ApiModel("新增订单的DTO")
@Data
public class OrderAddDTO implements Serializable {
@ApiModelProperty(value = "用户id",name="userId",example = "UU100")
private String userId;
@ApiModelProperty(value = "商品编号",name="commodityCode",example = "PC100")
private String commodityCode;
@ApiModelProperty(value = "商品数量",name="count",example = "5")
private Integer count;
@ApiModelProperty(value = "总金额",name="money",example = "50")
private Integer money;
}
还是要注意层级:
接着创建订单Order的实体类:
package com.codingfire.cloud.commons.pojo.order.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class Order implements Serializable {
private Integer id;
private String userId;
private String commodityCode;
private Integer count;
private Integer money;
}
层级其实在类中第一行已经可以体现了,大家要注意:
最后还需要一个类,库存类,和上面一样,一个DTO类,一个实体类:
package com.codingfire.cloud.commons.pojo.stock.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@ApiModel("商品减少库存DTO")
@Data
public class StockReduceCountDTO implements Serializable {
@ApiModelProperty(value = "商品编号",name="commodityCode",example = "PC100")
private String commodityCode;
@ApiModelProperty(value = "减库存数",name="reduceCount",example = "5")
private Integer reduceCount;
}
package com.codingfire.cloud.commons.pojo.stock.entity;
import lombok.Data;
import java.io.Serializable;
@Data
public class Stock implements Serializable {
private Integer id;
private String commodityCode;
private Integer reduceCount;
}
最终的层级结构如下:
在实际业务中还会用到一个vo,用于返回数据给前端,暂时先不用,但我们要先创建返回数据的模型。
返回数据格式类创建
在commons包下创建restful包,并新建一个类用于定义状态码,此类在前文中有提到,我印象里没有创建给大家看,这里创建下,非微服务项目也是需要这个类的。
项目整体结构如下:
ResponseCode类的枚举类型如下:
package com.codingfire.cloud.commons.restful;
/**
* 错误代码枚举类型
*/
public enum ResponseCode {
OK(200),
BAD_REQUEST(400),
UNAUTHORIZED(401),
FORBIDDEN(403),
NOT_FOUND(404),
NOT_ACCEPTABLE(406),
CONFLICT(409),
INTERNAL_SERVER_ERROR(500);
private Integer value;
ResponseCode(Integer value) {
this.value = value;
}
public Integer getValue() {
return value;
}
}
创建异常类,此类只是异常信息类,并不是全局异常捕获类,前文中讲解过全局异常类,微服务中也必不可少的:
CloudServiceException异常类代码如下:
package com.codingfire.cloud.commons.exceotion;
import com.codingfire.cloud.commons.restful.ResponseCode;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class CloudServiceException extends RuntimeException {
private ResponseCode responseCode;
public CloudServiceException(ResponseCode responseCode, String message) {
super(message);
setResponseCode(responseCode);
}
}
全局异常类如下,同样创建在exception包内,但在此之前需要先创建一个返回数据类型的类,创建在restful包下,名字叫JsonResult:
package com.codingfire.cloud.commons.restful;
import com.codingfire.cloud.commons.exceotion.CloudServiceException;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
@Data
public class JsonResult<T> implements Serializable {
/**
* 状态码
*/
@ApiModelProperty(value = "业务状态码", position = 1, example = "200, 400, 401, 403, 404, 409, 500")
private Integer state;
/**
* 消息
*/
@ApiModelProperty(value = "业务消息", position = 2, example = "登录失败!密码错误!")
private String message;
/**
* 数据
*/
@ApiModelProperty(value = "业务数据", position = 3)
private T data;
/**
* 创建响应结果对象,表示"成功",不封装其它任何数据
* @return 响应结果对象
*/
public static JsonResult<Void> ok() {
return ok("OK");
}
public static JsonResult ok(String message){
JsonResult jsonResult=new JsonResult();
jsonResult.setState(ResponseCode.OK.getValue());
jsonResult.setMessage(message);
jsonResult.setData(null);
return jsonResult;
}
/**
* 创建响应结果对象,表示"成功",且封装客户端期望响应的数据
* @param data 客户端期望响应的数据
* @return 响应结果对象
*/
public static <T> JsonResult<T> ok(String message,T data) {
JsonResult<T> jsonResult = new JsonResult<>();
jsonResult.setState(ResponseCode.OK.getValue());
jsonResult.setData(data);
return jsonResult;
}
/**
* 创建响应结果对象,表示"失败",且封装"失败"的描述
*
* @param e CoolSharkServiceException异常对象
* @return 响应结果对象
*/
public static JsonResult<Void> failed(CloudServiceException e) {
return failed(e.getResponseCode(), e);
}
/**
* 创建响应结果对象,表示"失败",且封装"失败"的描述
*
* @param responseCode "失败"的状态码
* @param e "失败"时抛出的异常对象
* @return 响应结果对象
*/
public static JsonResult<Void> failed(ResponseCode responseCode, Throwable e) {
return failed(responseCode, e.getMessage());
}
/**
* 创建响应结果对象,表示"失败",且封装"失败"的描述
*
* @param responseCode "失败"的状态码
* @param message "失败"的描述文本
* @return 响应结果对象
*/
public static JsonResult<Void> failed(ResponseCode responseCode, String message) {
JsonResult<Void> jsonResult = new JsonResult<>();
jsonResult.setState(responseCode.getValue());
jsonResult.setMessage(message);
return jsonResult;
}
}
接着就是全局异常捕获类,叫GlobalControllerExceptionHandler,创建在exception包下:
package com.codingfire.cloud.commons.exceotion;
import com.codingfire.cloud.commons.restful.JsonResult;
import com.codingfire.cloud.commons.restful.ResponseCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@RestControllerAdvice
@Slf4j
public class GlobalControllerExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler({CloudServiceException.class})
public JsonResult<Void> handleCoolSharkServiceException(CloudServiceException e) {
log.debug("出现业务异常,业务错误码={},描述文本={}", e.getResponseCode().getValue(), e.getMessage());
e.printStackTrace();
JsonResult<Void> result = JsonResult.failed(e);
log.debug("即将返回:{}", result);
return result;
}
/**
* 处理绑定异常(通过Validation框架验证请求参数时的异常)
*/
@ExceptionHandler(BindException.class)
public JsonResult<Void> handleBindException(BindException e) {
log.debug("验证请求数据时出现异常:{}", e.getClass().getName());
e.printStackTrace();
String message = e.getBindingResult().getFieldError().getDefaultMessage();
JsonResult<Void> result = JsonResult.failed(ResponseCode.BAD_REQUEST, message);
log.debug("即将返回:{}", result);
return result;
}
/**
* 处理系统(其它)异常
*/
@ExceptionHandler({Throwable.class})
public JsonResult<Void> handleSystemError(Throwable e) {
log.debug("出现系统异常,异常类型={},描述文本={}", e.getClass().getName(), e.getMessage());
e.printStackTrace();
JsonResult<Void> result = JsonResult.failed(ResponseCode.INTERNAL_SERVER_ERROR, e);
log.debug("即将返回:{}", result);
return result;
}
}
你会看到代码中用到了前文中学到的Slf4j框架,可见基础是很重要的。到此为止,commons工程模块创建完毕,这里面涉及的东西或许比前面几篇博客的东西还要多,也更复杂,但如果你能跟到现在,且项目还没有报错,恭喜你,你距离学会微服务又近了一步。微服务可以说就是把一个大的项目拆分成多个小的项目,到此你已经学会了怎么创建这些子项目,也就完成了一半,后面就是把这些子项目注册到nacos中,并进行相互调用,我们可以认为,这就达成了微服务。
但在不知不觉间,我已经掉入了自己挖的坑里,涉及到业务方面的内容也需要给大家写一部分,但此时全文已经2w+字数,这里应该可以写更长吧?只是希望正在学习的你千万别半途而废,真正的重头戏到现在依然还没有开始,我们所做的不过是前期的准备工作而已,考虑要不要拆分这一篇博客为多篇,想了想还是算了,目录给大家分好,也会更方便学习。
代码部分,博主也有参考一些好的案例,并给大家深入讲解,所以大家不要急,我们继续往下学习。
创建业务子模块
这个模块的任务是触发订单生成,我们来想想,订单生成的时候会发生什么?
- 首先订单创建时我们是在购物车内下单结算的
- 其次订单生成后需要删除购物车中已下单商品
- 接着,对应商品的库存要跟着锁定,直到付款后库存减少,
这是一个完整的业务流程,涉及到了多个模块间的关联,也就关系到了事务,关于事务,可以查看Java开发 - 数据库中的基本数据结构,了解其原理。
子工程创建
下面,我们来创建这个模块:
完成后项目结构如下:
你已经大概知道了微服务的基本项目结构,这很好,我们接着讲。
此时要删除src下的test测试文件夹,和commons中一样,因为会报错。这是因为我们并没有引入测试的框架。
依赖添加
项目创建完成后就需要父子关联,commons子工程中就已经说明了,我们再来一次:
首先,父工程中pom文件modules变为:
<modules> <module>cloud-commons</module> <module>cloud-bussiness</module> </modules>
bussiness子工程的pom文件直接贴出来,大家要知道,变化的是parent标签内的东西:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.codingfire</groupId> <artifactId>cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.codingfire</groupId> <artifactId>cloud-bussiness</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-bussiness</name> <description>Demo project for Spring Boot</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.codingfire</groupId> <artifactId>cloud-commons</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies> </project>
增加YAML配置
接下来到了一个关键的地方,增加配置,也就是YAML配置,我们先把properties文件改成yml结尾的,以后的项目中我们可能更多的使用yml结尾的格式,这种格式层次感更强,看起来更直观。
修改完后,同时创建一个dev配置的文件,名为:application-dev.yml,留作备用,接着将下面的配置贴入上面第一个yml结尾的文件:
server: port: 20000 #公共配置 mybatis: configuration: # 禁用缓存 cache-enabled: false # 配置映射驼峰命名法,数据库中user_name的字段,会映射在java的userName属性上 map-underscore-to-camel-case: true # 将运行的sql语句输出到控制台 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl knife4j: # 开启增强配置 enable: true # 生产环境屏蔽,开启将禁止访问在线API文档 production: false # Basic认证功能,即是否需要通过用户名、密码验证后才可以访问在线API文档 basic: # 是否开启Basic认证 enable: false # 用户名,如果开启Basic认证却未配置用户名与密码,默认是:admin/123321 username: root # 密码 password: root spring: profiles: active: dev
创建基础配置类
在此子工程下,我们需要添加一些必备的配置,和SSM框架下一样,需要创建config包,创建一个名为CommonsConfiguration的类:
package com.codingfire.cloud.bussiness.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
// 当前项目默认情况下不会扫描commons项目中的资源和内容,编写这个类,配置扫描commons
@Configuration // 所有配置Spring的配置类必须添加这个注解
@ComponentScan(basePackages = "com.codingfire.cloud.commons.exception")
public class CommonsConfiguration {
}
我才发现我前面exception包的名字写错了,p写成了o,和我一样的修改下。
接着创建Knife4jConfiguration,这是在线文档的配置:
package com.codingfire.cloud.bussiness.config;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
/**
* 【重要】指定Controller包路径
*/
private String basePackage = "com.codingfire.cloud.bussiness.controller";
/**
* 分组名称
*/
private String groupName = "base-bussiness";
/**
* 主机名
*/
private String host = "xxxxxxxxxx";
/**
* 标题
*/
private String title = "bussiness-web实例";
/**
* 简介
*/
private String description = "bussiness-web项目,实现购买";
/**
* 服务条款URL
*/
private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0";
/**
* 联系人
*/
private String contactName = "codingfire";
/**
* 联系网址
*/
private String contactUrl = "https://blog.csdn.net/CodingFire";
/**
* 联系邮箱
*/
private String contactEmail = "[email protected]";
/**
* 版本号
*/
private String version = "1.0-SNAPSHOT";
@Autowired
private OpenApiExtensionResolver openApiExtensionResolver;
@Bean
public Docket docket() {
String groupName = "1.0-SNAPSHOT";
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.host(host)
.apiInfo(apiInfo())
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build()
.extensions(openApiExtensionResolver.buildExtensions(groupName));
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title)
.description(description)
.termsOfServiceUrl(termsOfServiceUrl)
.contact(new Contact(contactName, contactUrl, contactEmail))
.version(version)
.build();
}
}
这个步骤依据各公司不同,可能不一定使用这种在线文档,很多都是使用的API工具,大家根据实际情况来。
业务代码
目前我们还不需要数据库介入,可直接编写业务逻辑代码,这里的业务分层和类的创建就比较讲究了,前文中曾提到一些,这里可要睁大眼睛好好看了,很可能你以后的项目结构就是这个样子的。
先创建一个service包,代表业务层,包里创建一个类IBusinessService,这个类只用于声明接口的调用方法,不用于实现业务,真正实现部分是在一个叫BusinessServiceImpl的实现类中。我们先来创建这两个类:
package com.codingfire.cloud.bussiness.service;
public interface IBusinessService {
// business业务触发购买下订单的方法声明
void buy();
}
要注意,这是一个接口,通过interface可以得知。
package com.codingfire.cloud.bussiness.service.impl;
import com.codingfire.cloud.bussiness.service.IBusinessService;
import com.codingfire.cloud.commons.pojo.order.dto.OrderAddDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class BusinessServiceImpl implements IBusinessService {
@Override
public void buy() {
// 暂时模拟一个下单业务
// 创建OrderAddDTO类,赋值并输出信息
OrderAddDTO orderAddDTO=new OrderAddDTO();
orderAddDTO.setCommodityCode("PC100");
orderAddDTO.setUserId("UU100");
orderAddDTO.setMoney(500);
orderAddDTO.setCount(5);
// 因为没有持久层,只能输出一下,表示运行正常
log.info("新增订单信息为:{}",orderAddDTO);
}
}
@Service注解用于类上,标记当前类是一个service类,加上该注解会将当前类自动注入到spring容器中进行统一管理。此类实现了接口IBusinessService。
接着是调用接口的那一方,必定是controller,创建一个controller包,此包必须和启动类同一层,前文中有提到。接着创建BusinessController类,到这一步,看过SSM框架的童鞋应该都比较熟悉了:
package com.codingfire.cloud.bussiness.controller;
import com.codingfire.cloud.bussiness.service.IBusinessService;
import com.codingfire.cloud.commons.restful.JsonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/base/business")
// knife4j介绍当前控制器作用
@Api(tags = "购买业务开始模块")
public class BusinessController {
@Autowired
private IBusinessService businessService;
@PostMapping("/buy") // localhost:20000/base/business/buy
@ApiOperation("发起购买")
public JsonResult buy(){
// 调用业务逻辑层方法即可
businessService.buy();
return JsonResult.ok("购买完成");
}
}
代码测试
现在,让我们运行项目,也就是运行启动文件,在学习这篇博客之前,我确信大家都是有Java基础的,所以像这种运行项目的问题,就不再细说了。
我们在CloudBussinessApplication类中,右键run,此时还没有配置注册到nacos的东西,暂时不用管nacos,我们这里测试的是刚刚创建的在线文档的类。
看到如图所示,就代表运行成功,这时候我们在浏览器输入:http://localhost:20000/doc.html
显示如下页面即代表在线文档工具引入成功:
如果你已经写了controller类,则会显示controller中的接口数据。 我们稍后会一起来写。
注册业务类到nacos
服务注册后,就可以在nacos中查看已注册的服务。这个很像是组网,而这也是微服务最最不可或缺的一步,在做配置之前,要确保nacos在运行状态,否则运行bussiness会导致报错。
要注册到nacos,我们还需要在bussiness的pom文件中添加如下依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
之前都没说过,添加玩依赖记得右上角有个刷新的按钮要点下,依赖才会被下载到工程中,否则运行将因为无法找到依赖而导致报错。
接下来在application-dev.yml文件中添加nacos注册的配置,这里说明下,实际开发中肯定不需要你搞这么复杂,会使用虚拟机来做这些服务,会更简单,比如博主这里就用的Docker,等你学会之后可以了解下。
配置如下:
spring: application: # 当前Springboot项目的名称,用于注册中心服务的名称 name: nacos-business cloud: nacos: discovery: # 定义nacos运行的路径 server-addr: localhost:8848
注意格式,格式不对,颜色就不会改变:
这时候,保证nacos服务是开启的,接着运行business项目,运行成功后,我们去nacos后台看看,在nacos的服务列表中能否看到nacos-business的名称。
发现没有?定睛一看,博主这里犯了个错,name应该在application层级之下,结果平级了,正确的配置应该是:
spring: application: # 当前Springboot项目的名称,用于注册中心服务的名称 name: nacos-business cloud: nacos: discovery: # 定义nacos运行的路径 server-addr: localhost:8848
此时运行成功后,不等查看nacos后台,已经在控制台看到:
nacos registry, DEFAULT_GROUP nacos-business
我们现在去nacos后台看一下:
在服务管理-服务列表下,能看到名为nacos-business的服务名,这代表我们的nacos服务注册已经成功,后续的子项目注册到nacos和这个的步骤是一样的,只是名字不一样,大家要留心记住。我可以说,你勉强算是微服务入了个门了。
但仅会注册到微服务还是远远不够的,还需要使用它们,所以,请继续跟着博主往下学习。
针对nacos,博主这里要给大家说明下nacos的心跳机制:
cloud:
nacos:
discovery:
# ephemeral设置当前项目启动时注册到nacos的类型 true(默认):临时实例 false:永久实例
ephemeral: true
- 临时实例:服务启动后,每5s向nacos后台发送一个心跳包,包含了当前服务的基本信息,nacos收到这个心跳包后,如果该服务未注册,就将该服务注册到nacos,若已注册,就表明该服务是活的。如果15s内nacos没有接收到这个心跳包,则将该服务标记为非正常状态,如果30秒内没有接收到这个服务的心跳包,Nacos会将这个服务从注册列表中剔除,我们可以尝试暂停运行的项目,然后去后台查看nacos列表中服务的状态。另外,这些时间是可以修改的,有兴趣的可自行查阅
- 永久实例:字面意思,注册的nacos服务将被持久化存储,即使项目运行停止,也仍可在服务列表中看到已注册的服务,其心跳包的规则和临时实例则是一致的
而一般来说,我们创建的多位临时实例,只有项目中的关键业务才会设置为永久实例。
创建购物车子模块
和先前创建的模块方式一样,名字叫cloud-cart,创建完成后,同样需要修改子项目的parents和在父项目中添加子项目。创建完成后删除test文件夹,修改application.properties文件为application.yml文件。
添加依赖
父项目:
<modules> <module>cloud-commons</module> <module>cloud-bussiness</module> <module>cloud-cart</module> </modules>
子项目:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.codingfire</groupId> <artifactId>cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.codingfire</groupId> <artifactId>cloud-cart</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-cart</name> <description>Demo project for Spring Boot</description> <dependencies> <!--web实例--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mybatis整合springboot--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!--alibaba 数据源德鲁伊--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--all-common依赖--> <dependency> <groupId>com.codingfire</groupId> <artifactId>cloud-commons</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!--在线api文档--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency> </dependencies> </project>
添加配置
server: port: 20001 #公共配置 mybatis: configuration: cache-enabled: false map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl knife4j: # 开启增强配置 enable: true # 生产环境屏蔽,开启将禁止访问在线API文档 production: false # Basic认证功能,即是否需要通过用户名、密码验证后才可以访问在线API文档 basic: # 是否开启Basic认证 enable: false # 用户名,如果开启Basic认证却未配置用户名与密码,默认是:admin/123321 username: root # 密码 password: root spring: profiles: active: dev
此处需要直连数据库,要额外创建一个application-dev.yml文件, 添加连接数据库的配置:
spring: datasource: url: jdbc:mysql://localhost:3306/cloud_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: root
账户密码记得要写成自己的数据库账号密码。这数据库配置添加好了,数据库我们还没有添加,下面就来添加数据库。
添加数据库
创建一个数据库,名字为cloud_db,接着还需要创建几张表,并添加数据,哦,对了,在做这些之前,大家需要先开启数据库服务,这个应该都知道的啊,就不再说明了。
运行如下SQL:
CREATE DATABASE `cloud_db`; USE `cloud_db`; /*Table structure for table `cart_tbl` */ DROP TABLE IF EXISTS `cart_tbl`; CREATE TABLE `cart_tbl` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '购物车id', `commodity_code` varchar(255) DEFAULT NULL COMMENT '商品编码', `price` int DEFAULT '0' COMMENT '商品单价', `count` int DEFAULT '0' COMMENT '购买数量', `user_id` varchar(255) DEFAULT NULL COMMENT '用户id', PRIMARY KEY (`id`), UNIQUE KEY `commodity_code` (`commodity_code`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3; /*Data for the table `cart_tbl` */ insert into `cart_tbl`(`id`,`commodity_code`,`price`,`count`,`user_id`) values (1,'PU201',500,10,'UU100'); /*Table structure for table `order_tbl` */ DROP TABLE IF EXISTS `order_tbl`; CREATE TABLE `order_tbl` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '订单id', `user_id` varchar(255) DEFAULT NULL COMMENT '用户id', `commodity_code` varchar(255) DEFAULT NULL COMMENT '商品编码,也可以是商品id', `count` int DEFAULT '0' COMMENT '购买这个商品的数量', `money` int DEFAULT '0' COMMENT '订单金额', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb3; /*Data for the table `order_tbl` */ insert into `order_tbl`(`id`,`user_id`,`commodity_code`,`count`,`money`) values (22,'UU100','PU201',10,200), (23,'UU100','PU201',10,200), (24,'UU100','PU201',10,200), (25,'UU100','PU201',10,200); /*Table structure for table `stock_tbl` */ DROP TABLE IF EXISTS `stock_tbl`; CREATE TABLE `stock_tbl` ( `id` int NOT NULL AUTO_INCREMENT COMMENT '商品id', `commodity_code` varchar(255) DEFAULT NULL COMMENT '商品编码', `count` int DEFAULT '0' COMMENT '商品库存', PRIMARY KEY (`id`), UNIQUE KEY `commodity_code` (`commodity_code`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3; /*Data for the table `stock_tbl` */ insert into `stock_tbl`(`id`,`commodity_code`,`count`) values (1,'PU201',990); /*Table structure for table `undo_log` */ DROP TABLE IF EXISTS `undo_log`; CREATE TABLE `undo_log` ( `id` bigint NOT NULL AUTO_INCREMENT, `branch_id` bigint NOT NULL, `xid` varchar(100) NOT NULL, `context` varchar(128) NOT NULL, `rollback_info` longblob NOT NULL, `log_status` int NOT NULL, `log_created` datetime NOT NULL, `log_modified` datetime NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) ) ENGINE=InnoDB AUTO_INCREMENT=68 DEFAULT CHARSET=utf8mb3;
运行成功后在数据库工具中可以看到如下:
一共四个表加少量的数据。
添加配置类
前面的bussines子模块中,也添加过配置类, 目的是扫描路径和配置在线文档,这里我们再做一遍。会的童鞋也可以自己主动尝试,不会的或不太熟悉的我们跟着博主一起做。
cart包下新建config包,config包下建此类:
package com.codingfire.cloud.cart.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration // 所有配置Spring的配置类必须添加这个注解
@ComponentScan(basePackages = "com.codingfire.cloud.commons.exception")
public class CartConfiguration {
}
接着是在线文档的配置类,和bussiness子项目里的配置类几乎一样,稍微改动:
package com.codingfire.cloud.cart.config;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
/**
* 【重要】指定Controller包路径
*/
private String basePackage = "com.codingfire.cloud.cart.controller";
/**
* 分组名称
*/
private String groupName = "base-cart";
/**
* 主机名
*/
private String host = "xxxxxx";
/**
* 标题
*/
private String title = "cart-web实例";
/**
* 简介
*/
private String description = "构建基础cart-web项目";
/**
* 服务条款URL
*/
private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0";
/**
* 联系人
*/
private String contactName = "codingfire";
/**
* 联系网址
*/
private String contactUrl = "https://blog.csdn.net/CodingFire";
/**
* 联系邮箱
*/
private String contactEmail = "[email protected]";
/**
* 版本号
*/
private String version = "1.0-SNAPSHOT";
@Autowired
private OpenApiExtensionResolver openApiExtensionResolver;
@Bean
public Docket docket() {
String groupName = "1.0-SNAPSHOT";
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.host(host)
.apiInfo(apiInfo())
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build()
.extensions(openApiExtensionResolver.buildExtensions(groupName));
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title)
.description(description)
.termsOfServiceUrl(termsOfServiceUrl)
.contact(new Contact(contactName, contactUrl, contactEmail))
.version(version)
.build();
}
}
添加nacos配置
这个前面做过了,我们再来一遍:
pom文件内添加依赖如下:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
yml的dev文件内 添加配置如下:
spring: datasource: url: jdbc:mysql://localhost:3306/cloud_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: 0 application: # 当前Springboot项目的名称,用于注册中心服务的名称 name: nacos-cart cloud: nacos: discovery: server-addr: localhost:8848
做完这些,cart工程启动后就可以注册到nacos服务中,就不再贴图了,大家自己运行后查看。有问题的再仔细查看博客,是否有漏掉的内容。
编写业务代码
购物车的逻辑,我们想想会有哪些功能?添加商品?删除商品?不外如是。业务层的分层我们前文中有提到,分别是持久层,业务逻辑层和控制器层。接下来的代码就围绕这三层展开。这里会给大家看类的层级和类的结构,这么做也是有原因的,所以大家要认真看接下来的代码,不能说很重要吧,就是决定了你的代码的整洁度和对逻辑理解的能力。
持久层
创建mapper包,在mapper包下创建一个接口类叫CartMapper:
package com.codingfire.cloud.cart.mapper;
import com.codingfire.cloud.commons.pojo.cart.entity.Cart;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
public interface CartMapper {
// 新增购物车中商品的方法
@Insert("insert into cart_tbl(commodity_code,user_id,price,count) " +
"values(#{commodityCode},#{userId},#{price},#{count})")
void insertCart(Cart cart);
// 删除购物车中商品的方法
@Delete("delete from cart_tbl where user_id=#{userId} and commodity_code=#{commodityCode}")
void deleteCartByUserIdAndCommodityCode(@Param("userId") String userid,
@Param("commodityCode") String commodityCode);
}
实际微服务中我们不会把SQL直接写在调用的地方,都是通过XML文件以映射的形式存在,这在前文中也是有提到的,不再赘述。此处仅为节省篇幅,方便大家理解。
业务逻辑层
创建service包,包下创建一个接口类叫ICartService,这里就到了业务分层的关键地方,第一个字母I代表接口的意思:
package com.codingfire.cloud.cart.service;
import com.codingfire.cloud.commons.pojo.cart.dto.CartAddDTO;
public interface ICartService {
// 新增购物车的业务逻辑层方法
void cartAdd(CartAddDTO cartAddDTO);
// 删除购物车的业务逻辑层方法
void deleteUserCart(String userId,String commodityCode);
}
这个接口类只做接口调用,不作他用,实现接口调用的方法写在单独的实现类中,service包下创建impl包, impl包下创建CartServiceImpl类:
package com.codingfire.cloud.cart.service.impl;
import com.codingfire.cloud.cart.mapper.CartMapper;
import com.codingfire.cloud.cart.service.ICartService;
import com.codingfire.cloud.commons.pojo.cart.dto.CartAddDTO;
import com.codingfire.cloud.commons.pojo.cart.entity.Cart;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class CartServiceImpl implements ICartService {
@Autowired
private CartMapper cartMapper;
@Override
public void cartAdd(CartAddDTO cartAddDTO) {
// 实例化一个Cart对象
Cart cart=new Cart();
// 利用工具类,将cartAddDTO中的属性值赋值到cart对象的同名属性中
BeanUtils.copyProperties(cartAddDTO,cart);
// 调用cartMapper对象实现新增功能
cartMapper.insertCart(cart);
log.info("新增购物车商品成功!{}",cart);
}
@Override
public void deleteUserCart(String userId, String commodityCode) {
// 直接调用mapper删除购物车商品的方法即可
cartMapper.deleteCartByUserIdAndCommodityCode(userId,commodityCode);
log.info("购物车商品删除成功");
}
}
提醒下,类上面的注解可不能少,否则无法引入log,博主刚刚就犯了这个错误。此类中基本不存在陌生的注解,我们在前文中SSM框架都是有用到的,所以基本功还是很重要的。
控制器层
控制器层我们说过,controller包要和启动类同级,所以创建一个和启动类同级的包controller,在controller包下创建CartController类:
package com.codingfire.cloud.cart.controller;
import com.codingfire.cloud.cart.service.ICartService;
import com.codingfire.cloud.commons.pojo.cart.dto.CartAddDTO;
import com.codingfire.cloud.commons.restful.JsonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/base/cart")
@Api(tags="购物车模块")
public class CartController {
@Autowired
private ICartService cartService;
@PostMapping("/add")
@ApiOperation("新增购物车商品")
public JsonResult cartAdd(CartAddDTO cartAddDTO){
cartService.cartAdd(cartAddDTO);
return JsonResult.ok("新增购物车商品完成");
}
@PostMapping("/delete")
@ApiOperation("删除购物车中商品")
@ApiImplicitParams({
@ApiImplicitParam(value = "用户id",name="userId",
example = "UU100",required = true),
@ApiImplicitParam(value = "商品编号",name="commodityCode",
example = "PC100",required = true)
})
public JsonResult deleteUserCart(String userId,String commodityCode){
// 调用业务逻辑层删除购物车商品的方法
cartService.deleteUserCart(userId,commodityCode);
return JsonResult.ok("删除购物车商品成功");
}
}
控制器中代码也不难,只要稍微熟悉Java代码都看得懂,唯一需要说明的就是注解,你可能已经注意到了一个@Api开头的注解,可能有些童鞋已经猜测到了,这就是在线API相关的配置,等项目运行起来后可以对照在线API看一下参数,你就明白每一个参数的意思了,不必多说。
此时如果启动服务,这几个接口就是可用的状态了,话不多说,我们现在就去启动项目来进行测试吧,是不是有些迫不及待了呢?
测试服务
这里可以先不用管bussiness子项目,单独运行cart子项目测试就可以,运行项目,运行失败,发现包了一个错:
Field cartMapper in com.codingfire.cloud.cart.service.impl.CartServiceImpl required a bean of type 'com.codingfire.cloud.cart.mapper.CartMapper' that could not be found.
原来是Spring框架找不到此路径下 com.codingfire.cloud.cart.mapper.CartMapper的类,哦,想起来了,这里少了一个配置类,用于将mapper包下的类交给Spring框架来管理。
我们在config包下新建一个配置类MybatisConfiguration:
package com.codingfire.cloud.cart.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
// Mybatis扫描必须指定到mapper包
@MapperScan("com.codingfire.cloud.cart.mapper")
public class MybatisConfiguration {
}
此配置类添加完后,mapper包下的文件就可以被Mybatis扫描到,就可以实现自动装配。
现在,我们重新运行项目,可以看到项目已经成功运行:
我们打开在线文档的地址:http://localhost:20001/doc.html
点开购物车模块,已经能看到两个接口,这正是我们在controller中定义的两个接口:
接着点击调试按钮,请求参数是我们在@Api注解里添加的:
直接点击发送,可以在响应内容中看到返回数据,state=200,请求成功,可以再去测试下删除的接口,确保要删除的数据是存在于数据库中的,否则将不能删除成功,同时,相同的数据也无法重复添加到数据库中,这点基础,大家想必还是知道的。
创建订单子模块
创建工程
首先是创建项目子工程:
这没啥好说的,熟能生巧,大家多练练,项目创建完成。
添加依赖
接着就是pom文件的配置:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.codingfire</groupId> <artifactId>cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.codingfire</groupId> <artifactId>cloud-order</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-order</name> <description>Demo project for Spring Boot</description> <dependencies> <!--web实例--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mybatis整合springboot--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!--alibaba 数据源德鲁伊--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--all-common依赖--> <dependency> <groupId>com.codingfire</groupId> <artifactId>cloud-commons</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!--在线api文档--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency> <!-- nacos注册中心依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> </project>
做完后,记得父工程pom文件添加子工程:
<modules> <module>cloud-commons</module> <module>cloud-bussiness</module> <module>cloud-cart</module> <module>cloud-order</module> </modules>
否则就无法建立连接,可不要忘了,最好都动动手跟着博主一起做,眼高手低不可取,虽然这已经是第四遍了。
完成后,删除test文件夹,删除application.properties文件,把cart里面的两个yml文件复制进来,省事!
复制完后,修改两处:
第一处是yml文件内server的port,改为20002;
第二处是dev.yml下的application的name改为cloud-order;
添加配置类
和cart一样,需要三个config类,你可以选择从cart直接复制过来,但是需要修改里面不同的内容。
三个类分别是:
package com.codingfire.cloud.order.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration // 所有配置Spring的配置类必须添加这个注解
@ComponentScan(basePackages = "com.codingfire.cloud.commons.exception")
public class OrderConfiguration {
}
package com.codingfire.cloud.order.config;
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {
/**
* 【重要】指定Controller包路径
*/
private String basePackage = "com.codingfire.cloud.order.controller";
/**
* 分组名称
*/
private String groupName = "base-order";
/**
* 主机名
*/
private String host = "xxxxxx";
/**
* 标题
*/
private String title = "order-web实例";
/**
* 简介
*/
private String description = "order-web项目";
/**
* 服务条款URL
*/
private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0";
/**
* 联系人
*/
private String contactName = "codingfire";
/**
* 联系网址
*/
private String contactUrl = "https://blog.csdn.net/CodingFire";
/**
* 联系邮箱
*/
private String contactEmail = "[email protected]";
/**
* 版本号
*/
private String version = "1.0-SNAPSHOT";
@Autowired
private OpenApiExtensionResolver openApiExtensionResolver;
@Bean
public Docket docket() {
String groupName = "1.0-SNAPSHOT";
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.host(host)
.apiInfo(apiInfo())
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build()
.extensions(openApiExtensionResolver.buildExtensions(groupName));
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title)
.description(description)
.termsOfServiceUrl(termsOfServiceUrl)
.contact(new Contact(contactName, contactUrl, contactEmail))
.version(version)
.build();
}
}
package com.codingfire.cloud.order.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
// Mybatis扫描必须指定到mapper包
@MapperScan("com.codingfire.cloud.order.mapper")
public class MybatisConfiguration {
}
只要你们不嫌内容多,我就给你们贴出来代码,看看代码结构:
这里我提一个问题,大家要留心。今天有个同学在学习我的博客时发生了报错,截图给我看,我一看,类的层级完全错了,和我博客里面的完全不一样,config包创建在test文件夹下,这是很不应该的,所以大家在学习时都要留意这个问题,我一般都会告诉大家创建在哪,或者截图给大家看,所以是很详细的,如果还是做错,就真的说不过去了。
开发持久层
这一部分代码和cart也是十分相似的,单位了熟悉起见,请大家紧跟博主的步骤,一步步来,尽量别出错误。
持久层是数据库相关,这里体现在mapper类中,所以我们要创建一个mapper包,同时创建一个类叫OrderMapper,对了,这是一个接口哦,不是class:
package com.codingfire.cloud.order.mapper;
import com.codingfire.cloud.commons.pojo.order.entity.Order;
import org.apache.ibatis.annotations.Insert;
public interface OrderMapper {
// 新增订单的mapper方法
@Insert("insert into order_tbl(user_id,commodity_code,count,money) " +
"values(#{userId},#{commodityCode},#{count},#{money})")
void insertOrder(Order order);
}
业务逻辑层
业务逻辑层的体现为service,所以要建一个service包,包下建一个接口类叫IOrderService,也是接口,负责接口的声明:
package com.codingfire.cloud.order.service;
import com.codingfire.cloud.commons.pojo.order.dto.OrderAddDTO;
public interface IOrderService {
// 新增订单到数据库的业务逻辑层方法
void orderAdd(OrderAddDTO orderAddDTO);
}
接着创建此类的实现类,用于实现接口的调用代码,在service包下创建新包impl,impl包下创建OrderServiceImpl类,并实现IOrderService接口:
package com.codingfire.cloud.order.service.impl;
import com.codingfire.cloud.commons.pojo.order.dto.OrderAddDTO;
import com.codingfire.cloud.commons.pojo.order.entity.Order;
import com.codingfire.cloud.order.mapper.OrderMapper;
import com.codingfire.cloud.order.service.IOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class OrderServiceImpl implements IOrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public void orderAdd(OrderAddDTO orderAddDTO) {
// 1.减少订单商品的库存(要调用stock模块的方法)
// 2.删除订单中购物车商品的信息(要调用cart模块的方法)
// 3.新增订单
// 实例化订单对象
Order order=new Order();
// 赋值同名属性
BeanUtils.copyProperties(orderAddDTO,order);
// 调用持久层方法
orderMapper.insertOrder(order);
log.info("新增订单完成:{}",order);
}
}
最后看一下目录结构:
看你有没有搞错,错了有可能就跟不上博主的步骤了。
控制器层
每次到控制器层是博主最开心的部分,因为这一部分是最不容易出错的一步,控制器层几乎只做业务的调用和上传参数的处理,所以是逻辑结构最清晰的一层。所以,你喜欢吗?
在order包下,和启动类同级别,也和config,mapper,service包同级创建controller包,包下再创建OrderController类:
package com.codingfire.cloud.order.controller;
import com.codingfire.cloud.commons.pojo.order.dto.OrderAddDTO;
import com.codingfire.cloud.commons.restful.JsonResult;
import com.codingfire.cloud.order.service.IOrderService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/base/order")
@Api(tags="订单模块")
public class OrderController {
@Autowired
private IOrderService orderService;
@ApiOperation("新增订单")
@PostMapping("/add")
public JsonResult orderAdd(OrderAddDTO orderAddDTO){
// 调用业务逻辑层方法
orderService.orderAdd(orderAddDTO);
return JsonResult.ok("新增订单完成");
}
}
这里要说明一点,如果你的包名和博主不一样,那么第一行的package肯定会报错的,之前的也一样,大家要知道,所以最好名字都和爆竹保持一致,等你熟练后再建属于自己的项目和包名类名。
测试代码
和之前一样,启动项目,因为nacos依赖已经添加过了,这里不再重复说明,不清楚的看看pom文件,是不是nacos以来已经在天际依赖部分直接加入了。
保证nacos处于活的状态,运行order启动文件,哎~好了,我们打开在线文档的地址:http://localhost:20002/doc.html
新增订单接口已经存在了,看看参数也都有,我们点击调试,然后用准备的临时数据发送,服务器返回给我们的数据说明已经创建了订单:
打开数据库软件,看看有没有这条数据:
已经添加了一条订单数据,说明我们的代码写的没有问题,你的成功了吗?如果OK了,我们就进行下一步了。
创建库存子模块
创建工程
接下来的步骤就要稍微快一点,但是和上面几乎是一样的:
创建工程时不勾选任何依赖。
添加依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.codingfire</groupId> <artifactId>cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.codingfire</groupId> <artifactId>cloud-stock</artifactId> <version>0.0.1-SNAPSHOT</version> <name>cloud-stock</name> <description>Demo project for Spring Boot</description> <dependencies> <!--web实例--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mybatis整合springboot--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!--alibaba 数据源德鲁伊--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!--all-common依赖--> <dependency> <groupId>com.codingfire</groupId> <artifactId>cloud-commons</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <!--在线api文档--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency> <!-- nacos注册中心依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> </dependencies> </project>
自己贴到pom文件内,右上角刷新按钮点下,加载依赖。父工程记得添加子项目:
<modules> <module>cloud-commons</module> <module>cloud-bussiness</module> <module>cloud-cart</module> <module>cloud-order</module> <module>cloud-stock</module> </modules>
添加config类
这个还需要再说吗?去把上面的类复制进去,该改的包名啥的的改成stock 的:
就不再贴代码了,几乎是一样的,细心一点就不会错,删除test测试文件夹,删除application.properties文件,把yml文件复制进来,不要忘了dev的,接着修改其端口号为20003,dev配置下application的name修改为nacos-stock。
持久层
在mapper包下创建StockMapper类:
package com.codingfire.cloud.stock.mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;
@Repository
public interface StockMapper {
// 减少指定商品库存的方法
@Update("update stock_tbl set count=count-#{reduceCount} " +
" where commodity_code=#{commodityCode} and count>=#{reduceCount}")
void updateStockByCommodityCode(@Param("commodityCode") String commodityCode,
@Param("reduceCount") Integer reduceCount);
}
增加哥小知识,@Repository,@Service,@Controller三个注解它们分别对应存储层Bean,业务层Bean,和控制器层Bean,前面的mapper类没添加*@Repository注解*的需要加上。就不带领大家一一添加了。
业务层
在service包下创建IStockService类:
package com.codingfire.cloud.stock.service;
import com.codingfire.cloud.commons.pojo.stock.dto.StockReduceCountDTO;
public interface IStockService {
// 减少库存数的业务逻辑层方法
void reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO);
}
然后在service包下建impl包,包下再创建StockServiceImpl实现类:
package com.codingfire.cloud.stock.service.impl;
import com.codingfire.cloud.commons.pojo.stock.dto.StockReduceCountDTO;
import com.codingfire.cloud.stock.mapper.StockMapper;
import com.codingfire.cloud.stock.service.IStockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class StockServiceImpl implements IStockService {
@Autowired
private StockMapper stockMapper;
@Override
public void reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO) {
stockMapper.updateStockByCommodityCode(
stockReduceCountDTO.getCommodityCode(), // 第一个参数是商品编号
stockReduceCountDTO.getReduceCount()); // 第二个参数是减少的库存数
log.info("库存减少完成!");
}
}
控制器层
创建controller包,包下建StockContrller类:
package com.codingfire.cloud.stock.controller;
import com.codingfire.cloud.commons.pojo.stock.dto.StockReduceCountDTO;
import com.codingfire.cloud.commons.restful.JsonResult;
import com.codingfire.cloud.stock.service.IStockService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/base/stock")
@Api(tags = "库存管理")
public class StockController {
@Autowired
private IStockService stockService;
@PostMapping("/reduce/count")
@ApiOperation("减少商品库存业务")
public JsonResult reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO){
stockService.reduceCommodityCount(stockReduceCountDTO);
return JsonResult.ok("商品库存减少完成!");
}
}
测试代码
接着运行此项目,博主就不演示了,和上面的步骤是一样的,大家尝试自己来试试看,能不能运行成功,查看在线文档,进行调试接口。如果成功,那就跟博主进行下一步,也是微服务的核心步骤了。
子项目总结
到此,所有的业务相关子项目就创建完毕了,后面还会有子项目,基本是偏功能性的。所有的子项目运行后,都可以去nacos后台查看是否注册到nacos里,后面两个子项目虽然没有显示带大家注册nacos服务,但是配置里面是已经存在的,这点大家要知悉,不要有疑问。大家也可以尝试bussiness,cart,order,stock,四个子项目一起运行,查看nacos后台注册情况,查看在线文档情况,这可能对电脑配置有些要求,配置低电脑可能就要开始卡顿了。
下面要讲的内容是微服务的核心所在,也是催动微服务工作的重心,这些子项目只是业务的体现,并不能算是真正的微服务,我们可以把它看作是业务的框架模式,真的核心是子项目见相互调用,负载均衡,网关,分布式事务,Sentinel,ES等等,不要着急,这些功能我们本篇内容均会涉及。到此已经5.5w字了,我们再接再厉,继续学习。
Dubbo
Dubbo是一套RPC框架,所在在讲解什么是Dubbo之前,需要先说明什么RPC。
RPC是什么
RPC是Remote Procedure Call的缩写,翻译后为:远程过程调用。其目的是实现两台/多台计算机/服务器之间的方法调用抑或是通信问题。它将中间步骤都封装起来,让我们进行远程方法调用的时候感觉到就像在本地调用一样。
实现RPC,需要解决两个主要问题,一个是序列化问题,一个是通信协议问题。
序列化问题,我们可以在项目中看到有方法,方法有方法名和参数,这些可能是字符串,可能是自定义引用类型,在网络传输或者保存的时候,我们知道计算机智能识别二进制数据,所以要要进行序列化,当我们要读取的时候,就进行反序列化。我们再创建Model类的时候是都实现了Serializable接口的,这个接口就负责序列化和反序列化工作。
通信协议,指序列化后怎么传递给服务器,其可以是最直接的tcp,也可以是http,甚至可以是消息中间件,总之,不论我用什么方式,需要把序列化的数据发送给需要的地方。你可能听说过三次握手和四次挥手,这就是信息传递的一种方式。
但RPC的只是还不止于此,甚至单独写一篇博客都是可以的,这里只做简单介绍,让大家知道它是干嘛的,想深入了解的童鞋可自行查阅。
什么是Dubbo
前面说过,Dubbo是一套RPC框架,是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和Spring框架无缝集成。它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
也就是说,2019年后,SpringCloud和Dubbo才能共同使用,时间不算很早,所以我们说dubbo是个新技术也说得过去。
Dubbo框架支持多种通信协议和序列化协议,可以通过配置文件进行修改。
支持的通信协议有:
- dubbo协议(默认)
- rmi协议
- hessian协议
- http协议
- webservice
- .....
支持的序列化协议
- hessian2(默认)
- java序列化
- compactedjava
- nativejava
- fastjson
- dubbo
- fst
- kryo
Dubbo默认情况下,协议的特征如下:
- 采用NIO单一长连接
- 优秀的并发性能,但是大型文件的处理差
- Dubbo开发简单,有助于提升开发效率
Dubbo在调用前也需要进行注册,像前面注册到nacos一样。Dubbo也可以注册到nacos,也可以注册到Redis,zookeeper等。注册后需要需要调用的地方发现,然后才能被调用。消费端自动发现服务地址列表的能力,是微服务框架需要具备的关键能力,借助于自动化的服务发现,微服务之间可以在无需感知被调用方的部署位置和 IP 地址的情况下实现通信。这就很神奇了!!注册中心则在这里面起到了至关重要的作用,而微服务恰恰是注册中心下各各注册进来的服务共同构建成的一个大型分布式项目。
此处必须有图,稍等,现画:
这里的服务调用方和服务提供方可以存在多个,图中仅画出一对,实际可以存在很多对,可以一对一,也可以是一对多。
Dubbo在本项目中的使用
在上文中,我们已经明确了,订单生成的过程需要删除购物车中的数据,减少订单商品的库存。这就是一个典型的一对多的服务。接下来,我们在原项目中添加一些东西以支持Dubbo的使用。
修改库存子模块
添加Dubbo依赖:
<!-- Dubbo依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency>
在application-dev.yml内添加Dubbo配置,最终的application-dev.yml文件为:
spring: datasource: url: jdbc:mysql://localhost:3306/cloud_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: 0 application: # 当前Springboot项目的名称,用于注册中心服务的名称 name: nacos-cart cloud: nacos: discovery: server-addr: localhost:8848 dubbo: protocol: port: -1 # 设置Dubbo服务调用的端口 设置-1能够实现动态自动设置合适端口,生成规则是从20880开始递增 name: dubbo # 设置端口名称,一般固定就叫dubbo registry: address: nacos://localhost:8848 # 配置当前Dubbo注册中心的类型和地址 consumer: check: false # 设置为false表示当前项目启动时,不检查要调用的远程服务是否可用,避免报错
修改StockServiceImpl类,最终代码如下:
package com.codingfire.cloud.stock.service.impl;
import com.codingfire.cloud.commons.pojo.stock.dto.StockReduceCountDTO;
import com.codingfire.cloud.stock.mapper.StockMapper;
import com.codingfire.cloud.stock.service.IStockService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/*
@DubboService注解表示当前业务逻辑层实现类中的所有方法
均会注册到Nacos,成为Dubbo可以被发现的业务逻辑层方法
*/
@DubboService
@Service
@Slf4j
public class StockServiceImpl implements IStockService {
@Autowired
private StockMapper stockMapper;
@Override
public void reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO) {
stockMapper.updateStockByCommodityCode(
stockReduceCountDTO.getCommodityCode(), // 第一个参数是商品编号
stockReduceCountDTO.getReduceCount()); // 第二个参数是减少的库存数
log.info("库存减少完成!");
}
}
添加了DubboService注解,别的没有了。
最后一步,当前项目的Spring启动类要添加一个注解,表示当前项目有Dubbo服务提供,在CloudStockApplication类前面添加注解:
// 如果当前项目是Dubbo服务的生产者,必须添加这个注解 @EnableDubbo
最终的类如下:
package com.codingfire.cloud.stock;
import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
// 如果当前项目是Dubbo服务的生产者,必须添加这个注解
@EnableDubbo
public class CloudStockApplication {
public static void main(String[] args) {
SpringApplication.run(CloudStockApplication.class, args);
}
}
修改购物车子模块
其操作和修改库存子模块一样,大家有样学样,照葫芦画瓢。博主就不带领大家操作了,因为是一样的,重复的内容就不放了。
修改订单子模块
修改订单子模块和上面稍有不同,因为此模块既是服务提供方,又是服务调用方,有些略为的差别,所以此模块的操作博主带领大家一起做。
添加依赖:
<!-- Dubbo依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-dubbo</artifactId> </dependency> <!-- 生成订单时,stock模块的减库存方法和cart模块删除购物车商品的方法都是需要的,所以引入这两个模块 --> <dependency> <groupId>com.codingfire</groupId> <artifactId>cloud-cart</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.codingfire</groupId> <artifactId>cloud-stock</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
yml的配置是一样的,启动类添加注解也是一样的,有一个大不同的类是OrderServiceImpl,此类中先前的一些方法还没有写,只是列出了步骤,这里需要补上,完整的类代码如下:
package com.codingfire.cloud.order.service.impl;
import com.codingfire.cloud.cart.service.ICartService;
import com.codingfire.cloud.commons.pojo.order.dto.OrderAddDTO;
import com.codingfire.cloud.commons.pojo.order.entity.Order;
import com.codingfire.cloud.commons.pojo.stock.dto.StockReduceCountDTO;
import com.codingfire.cloud.order.mapper.OrderMapper;
import com.codingfire.cloud.order.service.IOrderService;
import com.codingfire.cloud.stock.service.IStockService;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/*
因为bussiness模块要调用这个业务逻辑层中的方法
所以这个类中的方法也要注册到Nacos
*/
@DubboService
@Service
@Slf4j
public class OrderServiceImpl implements IOrderService {
@Autowired
private OrderMapper orderMapper;
// 当前order模块使用stock的业务逻辑层方法来以减少库存,使用cart模块的业务逻辑层方法来删除购物车中订单商品
// 因为stock模块的减库存方法在Nacos中注册,所以可以使用Dubbo调用
// 要想调用就必须使用@DubboReference,才能获得业务逻辑层实现类对象
@DubboReference
private IStockService dubboStockService;
// order还需要cart模块的删除购物车商品的方法
@DubboReference
private ICartService dubboCartService;
@Override
public void orderAdd(OrderAddDTO orderAddDTO) {
// 1.减少订单商品的库存(要调用stock模块的方法)
StockReduceCountDTO stockReduceCountDTO=new StockReduceCountDTO();
// 为减少库存的商品编号赋值
stockReduceCountDTO.setCommodityCode(orderAddDTO.getCommodityCode());
// 为减少库存的数量赋值
stockReduceCountDTO.setReduceCount(orderAddDTO.getCount());
dubboStockService.reduceCommodityCount(stockReduceCountDTO);
// 2.删除订单中购物车商品的信息(要调用cart模块的方法)
dubboCartService.deleteUserCart(orderAddDTO.getUserId(),
orderAddDTO.getCommodityCode());
// 3.新增订单
// 实例化订单对象
Order order=new Order();
// 赋值同名属性
BeanUtils.copyProperties(orderAddDTO,order);
// 调用持久层方法
orderMapper.insertOrder(order);
log.info("新增订单完成:{}",order);
}
}
大家看看代码,应该是可以理解的,从文字描述道代码的体现,过程变复杂了吧?但是不要懵,这已经是博主简化过后的结构了,大佬们还会在子模块中建子模块,将service和api调用单独分开来写,过程更加复杂,也许你以后会遇到,也许这已经是你的终点。此时此刻,你需要理解这里的代码的调用,而且是通过Dubbo调用存在于其他独立服务器的代码,他当然是需要注册到nacos的,这是前提。
测试代码
下面,我们来测试这一块的代码,这里要仔细了,和先前的测试都不一样,需要同时运行三个子项目,且要保证nacos是活的。下面看步骤一起来做:
第一步:启动nacos,如果是活的,就不用管;
第二步:启动cart和stock子项目;
第三步:启动order子项目;
三个子项目全启动之后,确保都是启动成功且不报错的:
再看nacos后台,三个服务都已注册:
可以看到上面有consumer(服务消费方),下面有providers (服务提供方)。
此时,我们打开order的在线文档:http://localhost:20002/doc.html
点到调试修改commodityCode对应的参数为PU201,点击发送按钮,返回成功信息,为了验证是否成功,我们打开数据库工具看下库存的数量,然后再次操作一遍,刷新数据库视图,看看数据库中库存表中库存数量是否变化。若变化则代表测试成功。
数据库中数据有限,大家可以手动增加数据,再修改调试数据,进而验证接口的正确与否。
若要测试bussiness模块调用生成订单的方法,不知道大家是否知道该怎么做?其实和上面的步骤几乎是一样的,博主不再带领大家一起操作,可自行尝试并测试。需要注意的是,bussiness是服务调用方,不是提供方,可以不用在启动类写@EnableDubbo注解。
Dubbo使用总结
写到这里,dubbo就算给大家介绍完了,关于dubbo如果还想要更深入了解的可自行查阅,博主以后有时间也可能会单独介绍dubbo。不过这里对dubbo的介绍已经能满足你日常所需,接下来,我们继续往后面讲。
负载均衡
什么是负载均衡?
负载均衡(Load Balance)的意思是将大量服务合理地分摊到多个操作单元上进行执行。主要用于解决互联网架构中的高并发和高可用的问题。实际开发中,我们的服务也都是集群式的,让多个相同的服务单独运行,以此来实现更高的并发。此处格外注意,必须是相通的服务,也就是做了主从的服务器才能做负载均衡,若是服务不同,根本不需要负载均衡。
为什么这么说呢?我们要先明白负载均衡解决的是什么问题?负载均衡用来解决多个相同的服务提供者存在时,一个请求进来后该访问哪个服务提供方的问题。假设有A,B,C三个相同的服务提供方,进来一个请求,请求A,或者B,或者C都是可以的,那么到底该访问谁?负载均解决的就是这个问题。
负载均衡怎么用
Dubbo框架内部支持负载均衡算法,能够尽可能的让请求在相对空闲的服务器上运行,设置好负载均衡的策略算法,并设置好每个服务器的运行权重,就可以实现负载均衡的效果。
Dubbo支持的负载均衡算法:
- 随机算法
- 轮询算法
- 加权轮询算法
- 哈希算法
我觉得在这里给你们讲清楚这几种算法必要性不大,所以不再详细介绍,你只要知道有这几种,原理方面可自行去了解,我觉得网上介绍的挺好的,比我写的好,我就偷个懒,使用上也不做介绍了,方向有了,就自行去了解吧。后续不出意外,会单独写一篇怎么配置负载均衡的博客,大家可以等,但别期待,我怕会太久。
Seata
Seata是什么?
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
它同时也是Spring Cloud Alibaba提供的组件,其官网:Seata
可以进去查看更多用法,我还是觉得官网介绍写的更详细,我就不写了,可以点击查看介绍:Seata 是什么
在我们的项目中使用Seata
添加依赖
cart/stock/order新增的依赖都是一样的:
<!-- seata和SpringBoot整合依赖 --> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </dependency> <!-- Seata完成分布式事务需要的两个相关依赖(Seata需要下面两个依赖中的资源) --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency>
添加配置
在application-dev.yml文件内添加如下配置:
seata: tx-service-group: cloud_group #定义分组名称 service: vgroup-mapping: cloud_group: default # 使用seata默认事务配置 grouplist: default: localhost:8091 # 8091是seata默认的地址
和dubbo是平级关系。注意,同一个事物的tx-service-group必须相等,且指定相同的seata地址和端口。
bussiness子模块配置
由于它是调用方,是事务管理器,所以略微有些差异。
依赖:
<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> </dependency>
前面提到bussiness的pom也需要添加dubbo依赖和添加order模块的依赖,大家应该记得吧?可不要忘了。
在application-dev.yml的配置则是和上面一样的,他们同属于一个事物,由于前面额外添加了配置没有说明,这里放上完全的配置:
spring: datasource: url: jdbc:mysql://localhost:3306/cloud_db?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true username: root password: 0 application: # 当前Springboot项目的名称,用作注册中心服务的名称 name: nacos-bussiness cloud: nacos: discovery: # 定义nacos运行的路径 server-addr: localhost:8848 # ephemeral设置当前项目启动时注册到nacos的类型 true(默认):临时实例 false:永久实例 ephemeral: true dubbo: protocol: port: -1 # 设置Dubbo服务调用的端口 设置-1能够实现动态自动设置合适端口,生成规则是从20880开始递增 name: dubbo # 设置端口名称,一般固定就叫dubbo registry: address: nacos://localhost:8848 # 配置当前Dubbo注册中心的类型和地址 consumer: check: false # 设置为false表示当前项目启动时,不检查要调用的远程服务是否可用,避免报错 seata: tx-service-group: cloud_group #定义分组名称 service: vgroup-mapping: cloud_group: default # 使用seata默认事务配置 grouplist: default: localhost:8091 # 8091是seata默认的地址
在业务逻辑层的buy方法上需要添加新的注解,完整类如下:
package com.codingfire.cloud.bussiness.service.impl;
import com.codingfire.cloud.bussiness.service.IBusinessService;
import com.codingfire.cloud.commons.exception.CloudServiceException;
import com.codingfire.cloud.commons.pojo.order.dto.OrderAddDTO;
import com.codingfire.cloud.commons.restful.ResponseCode;
import com.codingfire.cloud.order.service.IOrderService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class BusinessServiceImpl implements IBusinessService {
// Dubbo在获取order模块的业务逻辑层实现类
@DubboReference
private IOrderService dubboOrderService;
// 一旦编写这个注解@GlobalTransactional
// seata就会将这个方法当做一个分布式事务的起点
// 之后所有远程Dubbo调用的数据库操作要么都成功,要么都失败
@GlobalTransactional
@Override
public void buy() {
// 暂时模拟一个下单业务
// 创建OrderAddDTO类,赋值并输出信息
OrderAddDTO orderAddDTO=new OrderAddDTO();
orderAddDTO.setCommodityCode("PC100");
orderAddDTO.setUserId("UU100");
orderAddDTO.setMoney(500);
orderAddDTO.setCount(8);
// 因为没有持久层,只能输出一下,表示运行正常
log.info("新增订单信息为:{}",orderAddDTO);
// dubbo调用生成订单方法
dubboOrderService.orderAdd(orderAddDTO);
// 为了验证我们seata是有效果的
// 在当前业务逻辑层方法中随机发生异常
// 我们可以通过观察正常运行时数据是否提交和发生异常是数据是否回滚来判断seata是否工作
if(Math.random()<0.5){
throw new CloudServiceException(ResponseCode.INTERNAL_SERVER_ERROR,
"发生随机异常");
}
}
}
seata大家有吗?
seata下载地址
下载好启动,启动方法看和nacos相似:
mac/linux:
sh seata-server.sh
windows:
cmd seata-server.bat
测试seata
确保nacos和seata服务都启动了,接着按顺序启动此4个项目:cart/stock/order/business
访问bussiness在线文档,发起购买,点开调试界面,点击发送按钮:
如果发生如图所示随机异常,则表示seata测试成功,而在控制台,也可看到事物失败后回滚的提示:
senta总结
哎呦,各位看官,真是要了老命了,边写边校正错误,只是你们看不到博主的痛苦,每一步都是博主成功后的截图,到今天,利用业余时间写这边博客已经是第七天了,这块内容可真多,已经写了6w字了,微服务应该还差几个点就可以结束了,我快写吐了,虽然有参考一些代码架构,但还是做出了一些调整,着实费脑,不行不行,后续要准备收费了,先看先得。
Sentinel
什么是Sentinel?
官网地址
home | Sentinel
下载地址
https://github.com/alibaba/Sentinel
下载完后,双击启动,也可通过命令行启动:
介绍内容查看introduction | Sentinel
毕竟官网写的比博主写的好,就不浪费博主的烂笔头了。
要说明的是,Sentinel也是Spring Cloud Alibaba的组件,翻译过来的意思是哨兵,Sentinel以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。但是在微服务中,其配置下的sentinel.transport.port,每一个单独的子项目不可相同,因为限流等等针对的是某一个模块容器,而非一次针对全部,下面,我们来在此项目中做一个案例。
在我们的项目中使用Sentinel
添加依赖
以stock子项目为例,添加依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>
application-dev.yml文件添加新配置如下,必须和nacos同级:
sentinel: transport: dashboard: localhost:8080 # 配置Sentinel仪表台的位置 port: 8721 # 真正执行限流的端口也要设置一下,注意这个端口其他微服务项目不能相同
添加限流操作
接着来对接口做限流操作,说起来也很简单,通过添加注解的方式,在StockController中添加,完整类如下:
package com.codingfire.cloud.stock.controller;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.codingfire.cloud.commons.exception.CloudServiceException;
import com.codingfire.cloud.commons.pojo.stock.dto.StockReduceCountDTO;
import com.codingfire.cloud.commons.restful.JsonResult;
import com.codingfire.cloud.commons.restful.ResponseCode;
import com.codingfire.cloud.stock.service.IStockService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/base/stock")
@Api(tags = "库存管理")
public class StockController {
@Autowired
private IStockService stockService;
@PostMapping("/reduce/count")
@ApiOperation("减少商品库存业务")
// @SentinelResource标记的方法会被Sentinel监控
// value的值是这个监控的名称,我们可以在"仪表台"中看到
// blockHandler的值指定了请求被限流时运行的方法名称
// 此方法就是我们抢购商品时有时候会进入的服务器忙界面,真是万恶的限流,错失了多少瓶茅台啊
@SentinelResource(value = "减少库存方法(控制器)",blockHandler = "blockError",
fallback = "fallbackError")
public JsonResult reduceCommodityCount(StockReduceCountDTO stockReduceCountDTO){
stockService.reduceCommodityCount(stockReduceCountDTO);
return JsonResult.ok("商品库存减少完成!");
}
// 这个方法是Sentinel注解中fallback属性指定的降级方法
// 当前控制器方法运行发生异常时,Sentinel会运行下面的降级方法
// 降级方法中,可以不直接结束请求,而去运行一些代替代码或者补救措施
// 让用户获得最低限度的响应
public JsonResult fallbackError(StockReduceCountDTO stockReduceCountDTO){
return JsonResult.failed(ResponseCode.BAD_REQUEST,"因为运行异常,服务降级");
}
}
一旦达到限流条件,就会执行下面的限流方法,常常抢购失败进入的服务器忙页面就是拜他所赐。
Sentinel 限流方法应该满足如下要求
1.必须是public修改
2.返回值类型必须和控制方法一致
3.方法名称要和控制器方法限流注解中规定的名称一致
4.参数列表必须和控制器一致,可以在所以参数后声明BlockException来获得限流异常
fallbackError处理的是限流时的逻辑,俗称降级逻辑。当触发限流的条件后,就会触发限流的备用方法。明白了吗?下面,我们来实际操作下。
打开Sentinel后台
前面Sentinel已经运行,我们在浏览器输入:http://localhost:8080/#/login
默认账号密码都是sentinel,进入后发现页面是空的:
这是因为还没有运行我们配置了sentinel的项目,现在运行stock项目,然后打开stock的在线文档地址:http://localhost:20003/doc.html
打开调试界面,点击发送按钮后,去sentinel后台刷新界面,发现已经有一个服务进来了:
点开列表:
点击簇点链路-减少库存方法控制器后的流控按钮:
选择QPS,单机阈值选择1,也就是1s只能点击一次,点击新增,回到在线文档,快速点击发送按钮:
然后就被限流了,测试完成。实际开发中可以根据需要选择不同的限流条件。
Sentinel总结
Sentinel的基础使用到这里就结束了,这些东西先上都是可以在服务器上安装的,说起来这些操作不大像是我们开发干的活,哦,除了代码部分,偏运维些,但我们要会,更多Sentinel功能和介绍,请移步官网查看,下一个知识点是什么呢?让我们一起来看看。
网关
网关是SpringGateway,和前面几个框架不同的是,它不是阿里巴巴开发的,而是由Spring提供的。意不意外?另外,不是只有SpringGateway一个网关,2020年之前用的比较多的是奈非提供的微服务组件和框架,Gateway对应zuul,Sentinel对应Hystrix,Dubbo对应ribbon+feign,Nacos对应Eureka,这点,知道就行,现在我们都使用Spring Cloud全家桶。
什么是网关?
这时候我有点期待网关是阿里巴巴开发的了?为什么?因为网关的官网是纯英文的,这就是老外提供框架的一个弊端,对于国内大多数开发者来说不太友好,毕竟我们只过了CET-4而已。奉上官网地址:Spring Cloud Gateway
网关,字面意思理解,就是网络的关卡,恰如其名,即网络的统一入口,程序中的网关就是微服务项目提供的外界所有请求统一访问的微服务项目,此项目提供了统一入口,方便对所有进入的请求进行统一的检查和管理。
网关的主要功能有:
- 将所有请求统一经过网关
- 可以对这些请求进行检查
- 方便记录所有请求的日志
- 可以统一将所有请求路由到正确的服务上
为什么使用网关?
为什么使用网关?那可能要从网关的几个特点说起了:
- 内置断言
- 内置过滤器
- 动态路由
本以为快要收工了,才发现网关这东西挺麻烦的,但不说清楚总觉得又不舒服,又开始考虑单独开一篇来写了,我觉得行,应该篇幅也不会短,但是吧,就觉得豆鞋7w字了,不合适,还是继续往这里堆吧,你们就当看书了好不?
内置断言
断言都知道是干嘛的吧?SSM框架学习过了,可以认为断言就是一个判断条件,满足条件就给你断了,常见的内置断言都有哪些?看下面:
- after
- before
- between
- cookie
- header
- host
- method
- path
- query
- remoteaddr
咋用?不好意思,太多了,不方便一一列举出来,稍后在本项目中会演示一些。那断言就过了啊,用到了自己去查就行,这个不用死记硬背。
内置过滤器
内置过滤器和SSM框架学习的filter过滤器可不一样,千万别搞混,不过我估计也没人想起来这茬吧?不过还是要说明下。内置过滤器允许我们在路由请求到目标资源的同时,对这个请求进行一些加工或处理。
比如,AddRequestParameter过滤器可以在请求中强制增加一些参数,这个很好用,外界是不知道的,从安全角度来说有些用处。其他的过滤器遇到了再去了解,不多讲了。
动态路由
网关配置的时候需要配置具体的url,但是微服务项目众多,每个都配置的话会越来越冗余,维护的工作量也会越来越大,我们希望能够根据固定特征自动路由到每个微服务模块,这也是网关动态路由的实际意义,配置时,开启动态路由功能就可以实现。下面,我们通过实际操作在本项目中进行演示。
网关在我们项目中的使用
创建网关子工程
创建成功后删除test文件夹。
添加依赖
父项目添加子项目:
<modules> <module>cloud-commons</module> <module>cloud-bussiness</module> <module>cloud-cart</module> <module>cloud-order</module> <module>cloud-stock</module> <module>gateway</module> </modules>
gateway子项目依赖如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.codingfire</groupId> <artifactId>cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <groupId>com.codingfire</groupId> <artifactId>gateway</artifactId> <version>0.0.1-SNAPSHOT</version> <name>gateway</name> <description>Demo project for Spring Boot</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- 网负载均衡支持 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <!-- Spring Gateway 网关依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!--聚合网关 knife4j--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency> </dependencies> </project>
添加配置
修改配置文件后缀为yml,添加如下配置:
spring: application: name: gateway-server cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: # 开启网关动态路由 enabled: true main: web-application-type: reactive server: port: 10000
配置knife4j
配置knife4j是因为我们希望通过网关解决不同的子服务之间切换时需要修改端口的问题,我们这个案例就是用于此。
接下来,网关项目下创建一个config包,包下创建GatewayProvider:
package com.codingfire.gateway.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Component
public class GatewayProvider implements SwaggerResourcesProvider {
/**
* 接口地址
*/
public static final String API_URI = "/v2/api-docs";
/**
* 路由加载器
*/
@Autowired
private RouteLocator routeLocator;
/**
* 网关应用名称
*/
@Value("${spring.application.name}")
private String applicationName;
@Override
public List<SwaggerResource> get() {
//接口资源列表
List<SwaggerResource> resources = new ArrayList<>();
//服务名称列表
List<String> routeHosts = new ArrayList<>();
// 获取所有可用的应用名称
routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
.filter(route -> !applicationName.equals(route.getUri().getHost()))
.subscribe(route -> routeHosts.add(route.getUri().getHost()));
// 去重,多负载服务只添加一次
Set<String> existsServer = new HashSet<>();
routeHosts.forEach(host -> {
// 拼接url
String url = "/" + host + API_URI;
//不存在则添加
if (!existsServer.contains(url)) {
existsServer.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(host);
resources.add(swaggerResource);
}
});
return resources;
}
}
接着创建controller包,包下创建GatewayController:
package com.codingfire.gateway.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;
import java.util.Optional;
@RestController
@RequestMapping("/swagger-resources")
public class GatewayController {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public GatewayController(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping("/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
继续创建filter包,包下创建GatewayFilter类:
package com.codingfire.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
@Component
public class GatewayHeaderFilter extends AbstractGatewayFilterFactory {
private static final String HEADER_NAME = "X-Forwarded-Prefix";
private static final String URI = "/v2/api-docs";
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
if (!StringUtils.endsWithIgnoreCase(path,URI )) {
return chain.filter(exchange);
}
String basePath = path.substring(0, path.lastIndexOf(URI));
ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return chain.filter(newExchange);
};
}
}
以上几个类你不用关心具体啥意思,用的时候粘过来就行了,想了解原理的可以自己去看。
测试网关成果
在开始之前,确保nacos,seata和Sentinel都是活的,接着依次启动cart,stock,order和bussiness项目,这四个项目启动正常后,最后启动Gateway项目,项目启动完成,我们来分别测试一下几个地址:
http://localhost:10000/nacos-stock/doc.html
http://localhost:10000/nacos-cart/doc.html
http://localhost:10000/nacos-order/doc.html
http://localhost:10000/nacos-bussiness/doc.html
如果都能访问到对应模块的在线文档,则说明我们网关的测试就成功了。 网关能干的事还有很多,这只是其中一种用法,具体有些功能呢?再贴一下,大家熟悉一下:
- 将所有请求统一经过网关
- 可以对这些请求进行检查
- 方便记录所有请求的日志
- 可以统一将所有请求路由到正确的服务上
我们的项目中用了1,4功能,我们来看一张图:
可以看到,接口名已经变了,前面多了注册服务中心所用的名字,不需要更换端口,通过切换服务名称切换地址,这就是第四个功能。这都是网关的杰作。关于检查请求和记录日志,也比较套路,大家可以在工作中慢慢学习。
网关总结
总结到这里,对网关的介绍结束了,总的来说不算太复杂,但是如果要检查庆贺记录日志则会比目前的代码稍难一点,其实也算不上难,只是要看具体的需求。好了,就到了这里了。
结语
到这里,本章微服务就要跟大家说再见了,近8w字,要老命了。虽然微服务博主介绍完了,但微服务绝对不止这些,博主的介绍只是一部分,目的是让大家了解它是什么,有个方向,所以代码覆盖也并不全面,本来还有redis,es等的部分,但考虑到篇幅太长,不适合再写下去,所以就打算单独拎出来写了,另外大家开发时可以尝试使用虚拟机,会更方便,相信大家应该也有所耳闻。项目大家还留着,后面的学习中还会用得着。青山不改,绿水长流,咱们下篇再见。
版权归原作者 CodingFire 所有, 如有侵权,请联系我们删除。