一. 前言
Spring已经渐渐地成为了Java程序员的必备技能,尤其是Java Web方向。同时,Spring也一直在发展,由于Spring存在配置臃肿的问题,所以就出现了Spring Boot框架,用于简化新Spring应用的初始搭建以及开发过程。近年来微服务架构也越来越火,所以在Spring Boot的基础上又出现了Spring Cloud。如果你是常年使用Spring的Java Web程序员,那么Spring Boot跟Spring Cloud很值得一学。
本系列文章并非是完整的教程,我阅读的是”纯洁的微笑“的博客:。以及书籍《Spring Cloud与Docker微服务架构实战》by周立。相信每一位学习编程的朋友都遇到过一个问题,跟着书上/教程上一步一步走,结果却不一样。有的时候是版本的问题,有的时候是自己的操作出现纰漏,也可能是教程本身也存在纰漏。无论如何,学习是一个挖坑再填坑的过程,在这个过程中我们的技术也会快速提高。所以这篇文章就是记录我在学习Spring Cloud过程中所遇到的问题,以及如何解决(虽然也有构建步骤,但目前写得比较简单,建议结合上面推荐的博客,书籍使用)。如果有朋友也遇到了类似的问题,可以参考一下。
此处使用的Spring Boot版本:2.2.1.RELEASE,Spring Cloud版本:2.2.0.RELEASE。虽然我阅读的主要教程是上面的书籍,但我自己敲的demo项目跟书上有一点不同。书上把每一个章节的项目都分开,比如一个章节是学Feign,一个章节是Hystrix,并没有把它们都结合在一起,我认为这样缺少了一个很重要的工作:整合。然而整合却是必不可少的,既然都使用到了微服务架构,那么自然是要使用多个模板组件。所以我一共只写了一个项目,代码依次在上一个章节代码的基础上继续增加,既要保证功能完好,又要保证功能之间可以兼容。除此之外,书上目前的版本还是Spring Cloud 1.x,毫无疑问已经过时,所以我也改成了2.x。虽然我认为做出的这两个改变是值得的,但确实在学习过程中也遇到了不少的坑,所以也特地记录了下来。
二. 前置知识
Spring Boot是Spring的一套快速配置框架,而Spring Cloud是一个基于Spring Boot实现的云应用开发工具。所以,Spring Boot可以离开Spring Cloud独立使用开发项目,而Spring Cloud不能离开Spring Boot,属于依赖的关系。所以如果你还是使用Spring,而非Spring Boot,那么至少要了解Spring Boot与Spring的区别。可以参考这篇文章入门:http://www.ityouknow.com/springboot/2016/01/06/spring-boot-quick-start.html
可以看到,Spring Boot项目的构建非常简单,无须再考虑各种诸如xml,数据库,配置文件的配置,更多的只需要注解,达到了非常简便的效果。项目的结构也跟Spring相差无几:
src / main / java :
主程序入口
src / main / resources :
配置文件
src / test / java :
测试
在java / com / xx / xxx里的主目录(即与model,service,controller等packages同级的目录),要有一个Application.java,这个其实就是Spring Boot框架的入口,相当于Spring里的XML配置。不过这里抛弃了XML,使用了注解的方式:@SpringBootApplication。目前要引入jar包所需要的Maven依赖:
1 | <dependencies> |
其实大部分在Maven默认项目里都已经添加好了,剩下的只需要添加支持web的模块:
1 | <dependency> |
接下来对于Controller层的开发也很简单:@RestController,@RequestMapping即可(方法返回String类型,不需要什么ModelAndView)
@RequestMapping是指Controller里的方法都以JSON格式输出,无须再写Jackson等配置了。
对于测试:@Test,@BeforeEach (对于JUnit5,@Before and @After no longer exists; use @BeforeEach and @AfterEach instead)
PS:主要是原本的Before跟After注解就是在每一个Test方法执行时都会执行,改得更加符合其作用
微服务架构需要有多个modules,如果你愿意的话,你可以把一个modules当作一个项目来操作,但到了微服务这一块,这种做法就比较愚蠢,于是最好是尝试把多个modules放到同一个project中(使用的是IDEA)。参考网址:https://blog.csdn.net/sinat_30160727/article/details/78109769
遇到的问题:
①子module跟父module的pom.xml有什么异同?
父module的pom如下,可以看到包含了Spring Boot的基本信息,还有子modules的构成
1 | <parent> |
至于子module,之前该怎么样就怎么样,但parent元素无须做出改动,即:
1 | <parent> |
②写好src/main/java之后,程序无法运行,显示错误:Your ApplicationContext is unlikely to start due to a @ComponentScan of the default package.
解决:因为之前错误的父子module关系,导致我的子modules其实还是有默认package的,比如之前的package都是com.hong.microservice-xxxx-provider之类的。但实际上正确的操作,这时候子module的src/main/java,是没有任何package的,即在java目录下创建一个A.java,会没有package语句。而SpringBoot的Application.java确切地说,并不是要放在根目录,而是要放在所有其他类/包的同级/父级目录,是不能够直接放在src/main/java目录下的,必须手动创建一个包把它放进去。SpringBoot在写启动类的时候如果不使用@ComponentScan指明对象扫描范围,默认指扫描当前启动类所在的包里的对象,如果当前启动类没有包,则在启动时会报错。(因为默认并没有包,所以就报错了)
解决方案也很显然,自己手动创建了一个basic的package,然后其他的package跟Application.java都放进去,如下图:
③Test无法通过,但直接运行可以通过
解决:看清楚是@Before还是@BeforeEach。至于其他的,目前其实可以直接运行,但如果测试代码出错,后面会踩坑,可以尝试到时候解决,也可以提前把测试代码删掉了避免踩坑(但你知道是测试代码哪里出错了吗)
④创建第二个MicroService的时候,Test又无法通过,而且不是@BeforeEach的问题。报错:
java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @Context
解决:可能是因为存在了多个service,无法默认指定等等的(毕竟多个module的配置其实我还不是很熟悉),这时候最好就是直接在测试类上面显示指明要测试的是哪个类,如下:
1 | .class) (classes = MicroserviceSimpleConsumerMovieApplication |
添加之后,测试也可以运行了,但还是不通过。然后才知道,test目录下的包默认是要跟java下的是一样的。所以,既然java目录下的Application.java的路径是 basic/Application.java (basic是我们自己添加的package),所以test下也需要添加一个名为basic的package(必须名字也为basic)
⑤Caused by: java.lang.IllegalArgumentException: Not a managed type
无论是哪个服务,对于domain层的配置都要重复编写。@Entity等等的。
over,运行通过(尽管第一个service的测试还是存在问题,但毕竟我对MockMvc的语法一无所知(问题③))
三. 添加Eureka
微服务架构的核心思想就是分离各个服务,每一个服务单独作为一个module/project。那么服务如何被其他项目/服务调用?这时候就要用到Eureka,服务注册中心。服务把自己注册到Eureka上,然后其他服务可以在Eureka上发现并调用,这就是微服务的基本机制。
步骤:
pom.xml添加Eureka依赖
1 | <dependency> |
这种maven依赖直接去maven repository查找就能找到,还能找到每一个版本。这里值得注意的是,添加的是server,不要错误地添加了client包。同时Spring Cloud的版本号比较特别,使用英语记录版本 ,以下是Spring Cloud英语版本对应的Spring Boot版本:
Spring Cloud版本 | Spring Boot版本 |
---|---|
Hoxton | 2.2.x |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
添加完依赖包,其实就可以运行了。然后要把服务注册到Eureka。这时候才是版本冲突的地方。ERROR org.springframework.boot.SpringApplication - Application run failed
一般这种Application run failed,基本就是版本号冲突 的问题了。看了一下,我写的还是书上的Edgware.RELEASE,参考官网的改成了HOXTON.RELEASE,就解决了这个问题。
然后又有其他的错误, Failed to start component [NonLoginAuthenticator [StandardEngine[Tomcat] .StandardHost[localhost] .TomcatEmbedded。。。
网上查到的也是更加奇怪,说是SDK里的servlet-api.jar与log4j.jar起冲突了。其实关于前面的版本号为什么非要添加到Eureka才出问题已经挺奇怪了,这里就更奇怪了,怎么突然就两个jar起冲突了?唯一的解释只能是Eureka毕竟是带有记录功能的,所以就包含了log4j.jar,然后就与servlet-api.jar冲突了。可是我的SDK里却没有这个东西,去External libraries找到了这个东西,然后去掉,竟然真的就可以了。所以说,编程还是需要多积累经验。
效果图:可以看到MicroService已经注册到了Eureka上
四. 把多个服务注册到Eureka上
看起来很简单,但却遇到了非常多的问题。首先打开consumer服务的时候,发现pom.xml报错了。想当然的以为又只是IDEA抽风了而已,因为直接运行是可行的,但后来发现确实不是如此。
maven之前欠下的债算是要还了,每当我有一个知识点掌握得含糊不清,得过且过,也许我可以安逸地度过很久,甚至是几年,但总会有再栽跟头的一天。之前我对maven,真的就是把它当作了一个简单的eclipse的import path的功能了,目的实现了就行了,什么<dependencyManagement>,完全不想去弄清楚。之前创建子module之间的依赖,我其实就搞不懂为什么能直接用cn.hongscar.xx来引用依赖?难道因为是项目内部就可以直接引用?没有去想。
首先项目一直会出现今天早上的那个错误,显然就是servlet-api.jar的问题,才发现这东西会一直存在,即使删掉了,maven也会把它自动下载回来。很显然,删掉是很愚蠢的操作,但当时毕竟只想立竿见影,我一直以来都是这么做的,显然正确的操作是添加一个maven的例外。所以正确的操作是添一个<exclusions>在<dependency>里。这确实就可行了,但真的是如此吗?首先,确实是添加了<exclusions>就可以工作了,但为什么maven会 自动把servlet-api.jar下回来,难道这还需要时机?得看你手速快不快,在它把servlet-api.jar下回来之前运行?实际上,就是因为项目本身就存在问题,所以每次rebuild,reimport的时候就把jar包搞回来了。无论如何,<exclusions>显然是更好的选择。
关于我要的目标,其实Maven的基本命令就已经包括了:
-v :查询Maven版本
compile: 编译,把Java源文件编译成.class文件
test:测试项目,测试test目录下的测试用例
package:将项目打包成jar包
clean:删除target文件夹(删除缓存,重新生成)
install:将当前项目放到Maven的本地仓库中,供其他项目使用
我想要的就是把项目安装到本地仓库里,总不能把项目delete了重新创建一次吧。然后在install的过程中查看到底出现了什么问题,这才是解决问题的核心。而不是,clean,complie统统乱点一遍。
然后在install的过程中就能看到问题了:
Failure to … in … was cached in the local repository, resolution will not be reattempted until the update interval of nexus has elapsed or updates are forced.
首先是Hoxton.RELEASE文件已经缓存了,并且还在使用,就不能update了。这时候真的就是简单粗暴就好,删了,让它重新下载,这个问题就解决了。
接下来的问题更离谱,说是测试代码有问题,所以不能成功build。想起来昨天不懂测试语法,想着项目能正常运行就好,测试代码就没管,没想到就这里埋下了大坑。于是把测试代码全部注释掉,同时rebuild一次(更新classes文件夹),果然,build就成功了。maven本地仓库也就成功出现了该模块。所以,项目到底如何加到maven本地仓库,为什么之前出错,都是有迹可循的,真的不仅仅就是奇怪的经验那么简单。当然,经验是否有用?至少从中能知道,测试代码并不是那么的人畜无害,在正式的build,install(项目打包过程),测试代码也是关键的一环,它有权终止整个过程,尽管你的主程序没有出错,只是测试代码写错了。
其实很多时候,把很多东西都归结于经验,确实有种给自己诡辩的嫌疑。就像是看到pom.xml报错了,第一时间想的是不是IDEA抽风了,完全不去想到底什么原因。诚然,”奇怪“的情况是存在的,但并不能一概而论,很多时候,经验不能成为一个完美的借口。
结果图: 可见两个service都成功添加到Eureka中。
五. 构建高可用的Eureka Server
先从搭建两个Eureka Server互相注册复制开始。根据书上的例子,首先需要修改hosts文件。然后先去了解了一下hosts文件是什么(位置:C:\Windows\System32\drivers\etc,如果是Linux,是在/etc/hosts),主要就是平时我们上网都要输入域名,然后传递到DNS服务器再进行映射,而DNS服务映射到具体的IP地址是需要时间的。这时候我们可以在hosts文件里自定义一些domain name(可以是自定义,可以是公开的),以及对应的IP地址,那么当我们下次输入这个domain name的时候,就直接映射到IP地址,跳过了DNS,因此速度也就更快。比如GitHub.com的IP地址是192.30.255.113。那么添加一行:
192.30.255.113 github.com
即可使得下次输入GitHub.com就能自动映射到对应的IP addr。
平时我们用的localhost跟127.0.0.1也是如此。127.0.0.1默认就是本地的IP地址,而localhost就是写在hosts里的域名。但在hosts里可以看到这一行是被注释掉的,为什么?因为下面还有一行注释:
# localhost name resolution is handled within DNS itself.
也就是说,DNS服务器内部已经把这个写好了,成为了约定,这样就避免了你把localhost乱搞成了其他东西。所以并不是说注释是无效的,而是这一行直接成为了DNS的默认约定配置吧。
OK,然后修改hosts是要给127.0.0.1新增几个新域名(peer1跟peer2)
然后在application.yml里的配置,使用— 可以将yml文件分成多段,由一个spring:profiles来决定(在后续启动的时候传入这个参数,表示要使用哪段yml文件),如果没有指定spring:profiles,则无论是传入哪个参数,都会生效。
启动的时候需要:java -jar xxx.jar --spring.profiles.active=peer1 / peer2
显然,首先我们需要将项目打包成jar文件,然后再分别启动(spring.profiles参数分别为peer1和peer2)
这时就出现了第一个问题,如何打包成jar文件?
联想到昨天吃的亏,印象中maven里就包含了这个操作,确实如此。但成功打包之后,却无法运行,显示:xxx没有主清单属性
那么到底是什么是主清单属性?其实也很好猜,就是找不到主类。网上看的大致说法是,maven的打包jar操作是有点特殊的,跟普通的java方式打包生成的目录是不太一致的。于是又尝试了一下mvn install,mvn clean等等命令,都不行。
但在中途也学到了一手:给mvn的命令增加一个-e参数,可以显示详细的错误信息:
比如这里的错误信息是can’t not delete,那是因为我打开了几个cmd,然后文件正在使用,无法进行mvn clean,把cmd窗口关闭了之后重新clean就可以了。
之后添加了一个叫maven-shade-plugin的插件,还是不行(为什么要添加这些额外的插件,还是有思考的,基本就是要把MainClass的路径也写进入,一并打包)
这时候感觉maven的plugins操作还是不太好用,直接用mvn install -e查看为什么出错,发现是:
Caused by: org.codehaus.plexus.component.configurator.ComponentConfigurationException: Cannot find ‘resource’ in class org.apache.maven.plugins.shade.resource.ManifestResourceTransformer
完全没有头绪,但在网上有一个很好的帖子:
简单概括就是:SpringBoot本身是存在一个打包插件,而这个另外加的shade反而形成了冲突。那么就好办了,把SpringBoot的打包插件去掉?一看,pom.xml并没有该插件,但该service是依赖了provider service的,看了一下provider service的pom.xml,确实存在这个插件:
1 | <build> |
于是想着那就删掉这个!但结果还是不行?可能是别的地方还存在这个插件的导入?毕竟我在最初对maven的版本管理是很混乱的。但我确实暂时不想深究这个问题,直接换了另一种做法:那就不用这个shade插件了,直接使用springboot自带的打包插件。然后给当前pom.xml加上了上述插件(看来依赖是不会继承plugins的),这时候mvn clean && mvn install
,成功,如图:
PS:此时我并没有立刻启动Eureka Server节点,而是查看了一下MANIFEST.MF文件,发现确实增加了大概的“主清单属性”,如图:(之前是没有Spring-Boot-Classes等等属性的)
PPPS:对了,vs code打开了MF文件之后就死机了。。
1 | Manifest-Version: 1.0 |
这时候已经猜到成功了,然后按照很前面的命令启动了两个Eureka Server节点,确实如此:
peer1看起来一开始报错,但其实很正常,因为peer2还没开始运行,后面peer2运行起来之后,二者就成功互相复制了。效果如图:
虽然启动成功了,似乎也成功复制了,但与预期有一点不同(peer1复制了peer2,但peer2没有复制peer1,仍然复制的peer2,配置文件似乎没有写错)
DS Replicas有点奇怪,但*Instances currently8确实都是二者成功复制了。
有说法是。DS Replicas是指从哪里同步数据,测试先启动peer2,再启动peer1会不会发生改变。(这也是我的个人想法,所以按理说,peer2处的DS Replicas应该是peer1才对)
可是较换peer1跟peer2的启动顺序,Instances currently依然是复制了,但DS Replicas依然二者都是peer2,所以与启动顺序无关。(可能是根据yml的顺序来决定,后面的就是最后的DS,毕竟这两个peer其实都指向同一个IP)。可能是peer1和peer2的复制顺序真的不是简单的顺序复制吧,网上说如果真的是那样,那就变回了中心化组件,跟微服务架构背道而驰。
六. 给Eureka Serer增加一个认证
首先添加依赖:
1 | <dependency> |
其实只要添加了这个依赖,那么此项目在运行时就需要登录。(如果是Server,那么登录Server控制台需要登录,其他服务想注册到此Server也要登录。如果是Service,那么使用Service的时候也要登录)如果不在yml里配置用户名跟密码,默认用户名就是user,密码会在运行时随机生成,打印在控制台。
然后一开始在yml的配置里出现了问题,其实不用查也可以猜到,是版本的原因,2.x换成下面的:
1 | spring: |
所以最后的yml配置文件如下:
1 | spring: |
值得注意的是,前面是否需要加 xx:yy@localhost:8761/eureka/ ,好像加不加都无所谓。。
然后一开始遇到了奇怪的错误,最后把密码删掉了也不行。最后翻GitHub以前的记录,才发现,我连两个false还有port都没有配置,所以最基本的配置一定要记忆清楚。。
接下来是要将服务注册到需要认证的Eureka Server。一开始遭遇到了问题:com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
很显然,就是说不能注册到任何已知的服务器。网上搜到的答案都是针对Eureka Server本身不需要注册(最开始那两个false参数),但显然这里我的错误就是service无法通过Eureka Server的认证。
尝试了几次都不行,最后通过“内华达穷举法”,才发现问题是CSRF。只要把CSRF禁用即可:
1 |
|
至于到底是在yml里设置security参数,然后在defaultZone里增加${…}:${…}@local…,还是直接注入一个DiscoveryClientOptionalArgs的Bean,都可以达到认证的效果,如下:
方法①:
1 | import com.netflix.discovery.DiscoveryClient.DiscoveryClientOptionalArgs; |
方法②:
1 | spring: |
二者无论是单独使用,还是一起使用,都可以达到认证的效果。
七. 关于Eureka的元数据metadata
metadata包括标准元数据(自带的,可在服务之间传递信息),还有自定义元数据(一般不会改变客户端的行为,相当于参数传递,给一个参考值)
其实这部分的工作并不多,但主要是让我意识到了,版本控制真的不容易,即便是引入了Maven。之前我在主项目里没有引入spring-boot,然后在provider跟Eureka中都分别引入了。其实没有必要,在主项目引入,然后provider就不必引入了,即使引入了也会被视为duplicate。
同时我需要解决@RestController返回的是XML而不是JSON的问题,发现问题是因为Eureka-Server引入了Jackson-dataformat-xml这个包,所以自然地去exclude掉就好(需要在maven里慢慢找这个包到底从属于哪个包,当然如果记忆好的话,确实是引入Eureka之后才出现这个问题,自然也就是在Eureka-Server的包里了)
然后发现Provider跟Consumer的返回类型都正常了,但Eureka下的apps(测试自定义metadata的入口)依然是XML类型。一看发现项目仍然存在Jackson-dataformat-xml,应该是Eureka Server也自己引入了spring-cloud-eureka-server的原因,所以我把它去掉了(想着这样直接获取主项目的就好),但这时项目却无法运行了。原因是找不到Jackson-dataformat-xml,猜测是因为Eureka Server没有导入这个包了,于是主项目的exclude就出现了问题(现在回想这个逻辑是不对的,为什么?)。但并不是,把exclude删掉了也不行。才发现哪个找不到Jackson-dataformat-xml,是指项目需要这个,但却不存在。所以说,其实项目还是需要这个包的,尤其是在使用metadata的时候?于是我把这个给彻底去掉了,这样就不可运行了。所以只好主项目依旧exclude,Eureka Server依然导入spring-cloud-eureka-server,但不exclude(此时子项目跟父项目存在不同,当然就以子项目的来覆盖)
所以最后provider等服务是返回JSON了,但apps却仍然是XML。不过想想,毕竟我对apps还不太了解,也不是我自己手动写的@RestController,所以返回XML其实也无可厚非。
PS:最后有一个点,Consumer注入了Provider的依赖,这两个之间是不能同时注入同一个Bean的(会冲突),而且其实只要Provider存在这个依赖,那么Consumer也就存在。所以Consumer没有注入注册Bean,也没有在yml里编写认证用户密码,同样是可以注册到Eureka的。相反,在Provider已经写了认证机制,在Consumer再写一遍,会使得Consumer无法认证,无法注册到Eureka。
Eureka部分到此结束。