SpringCloud环境搭建与示例演示2(基于IDEA)
.。。。。。。。。。。。。。。。。。。。。。
在 SpringCloud环境搭建与示例演示1(基于IDEA) 中,我们已经搭建好了,商品项目,用户项目和订单项目,分别为sp02,sp03 和sp04,那这3个项目之间要相互调用,就是从sp04订单项目,后面 要远程调用sp02商品项目 和 sp03用户项目,那要远程调用的话,那sp04 就得知道sp02 和sp03 的地址,那sp04如果知道它们的地址呢,那如果在sp04里面直接写死,http://localhost:8001,和 http://localhost:8101,直接写死的话,这样去调用的话就不是很好,因为写死的话,将来服务器的地址如果改变的话,那sp04的代码,即也要修改,那如何能不用把地址写死,也能去调用,而且不管sp02这台服务器,它的地址怎么变,我的sp04总能调用到它,那这个就需要通过 注册中心来做。
注册中心的作用:就是注册中心里面维护着一个地址注册表,注册表里面呢,保存着sp02,sp03 和sp04,这3个服务器的地址,它们分别把自己的地址注册到注册表里,在注册时,它们会注册自己的服务名 和服务ID,比如我们的sp02的服务名叫item-service,这个名字是配置在sp02的application.yml配置文件里面配置的,由app.name配置好的就是item-service,那sp02它会把自己的地址呢,注册到注册表,然后注册的时候,使用item-service这个名字来注册,然后后面就是它的地址:item-service localhost:8001,那sp03用户在启动的时候,也会把自己的服务名和地址注册到注册中心的注册表里面,注册一个user-service localhost:8101,那么order启动的时候也要注册,order-service localhost:8201,就是这3个服务,在它们启动的时候,都要把自己服务名和地址向服务中心进行注册,注册到注册中心的注册表中:
那这样,将来sp04想去调用sp03或者sp02,那就首先需要获得注册中心的这张注册表,得到注册表之后,sp04就可以知道sp02和sp03的地址,分别是什么地址,那就通过这第一个地址,sp02商品服务的地址,就可以去远程调用商品服务,那通过sp03yoghurt服务的地址,就可以去远程调用用户服务,即先得到注册表,然后根据注册表的地址去调用(这个注册中心的作用就类似于58同城,58同城上就注册了各种服务的联系方式(谁都可以在58注册,中间商太多,大多数联系方式都是中介)),我们的服务和服务之间调用,也是从注册中心先得到这张注册表,得到注册表之后,就有了各个服务的地址,就可以通过这些地址去相互调用。那么简言之,注册中心的主要作用就是服务的发现。
那我们服务调用者和注册中心的关系,仅仅是一个注册和发现的关系,把地址向注册中心的注册表注册,那如果想去调用某个服务的话呢,那调用者服务器就先从注册中心拉取注册表,把注册表拉取到本地,然后在本地得到注册表中各个服务的地址之后呢,直接用这些地址去调用其他服务,调用的时候,是跟注册中心完全无关的,因为注册表已经从注册中心拉取到了本地服务器上,那如果调用者服务器已经拉取了注册表,这时注册中心崩溃了,那没有了注册中心,我们依然可以通过本地已经拉取的注册表去执行远程调用操作,那这里我们要说的是eureka注册中心,对于zookeeper注册中心也是一样的。
那注册中心的产品其实很多,市场上流行的注册中心有zk,eureka,当前还有其他的,比如阿里的nacos注册中心,还有etcd,consul等,到底用哪一个,还是看不同公司的使用习惯而定,比如之前使用dubbo实现远程调用时,注册中心用的是zk,这其实是阿里的一个使用习惯,阿里的很多项目用的注册中心都是zk,阿里是习惯使用zk的,那除了zk之外,阿里还自己开发了注册中心nacos,在国内用的也是非常的多,然后是eureka是springcloud推荐使用的,注册中心的主要功能都是大同小异,就和58同城一样,无非就是注册个地址而已,简单的,我们就是把注册中心当做一张注册表来使用,往里面注册个地址,服务发现的时候呢,把这个注册表拉取过来,就直接得到一张地址表就完了。
那eureka的运行机制需要知道4点,一是注册,二是拉取,三是心跳,还有一个是自我保护机制,
1.注册:就是服务启动时,把服务的名称和地址注册到注册中心的注册表里,每一个服务启动都应该把自己的服务名和地址,向注册中心进行注册,这个注册过程会一次次反复尝试注册,直到注册成功为止;比如注册中心挂了,或者注册中心还没有启动时,那没有注册中心,我们启动服务时,需要连接注册中心,那注册中心根本没启动,或者说注册中心崩溃了,挂掉了,那连接注册中心就会连接失败,连接失败也无所谓,因为eureka在注册的时候,它会一次一次的反复注册,比如商品服务item-service在注册自己的地址时,会一次一次反复连接,反复注册,直到这个注册中心服务正常的启动起来,eureka尝试连接注册中心进行注册,连接成功了,也能把地方发给注册中心了,最终完成注册。那注册中心和微服务之间的启用顺序,应该先是注册中心,然后启动微服务时可以成功连接注册中心,成功注册服务地址,但是先启用微服务也是可以的,但eureka会在微服务启动之后,一次一次反复尝试去连接注册中心,直到注册成功为止。
2.拉取:启动微服务后,eureka会每30秒拉取一次注册表,更新注册表,即当微服务第一次完成注册时,把自己的地址注册到注册中心,然后会立即把注册中心的注册表拉取回微服务本地,得到注册表,之后每隔30秒,微服务都会重复拉取注册表,更新注册表,那这时如果有一个新的微服务项目启动了,把地址也注册到了注册中心的注册表中,那其他的微服务项目是不能立即感知新启动的微服务的地址的,而是等到那个微服务项目下一次拉取的时间点,重新拉取新的注册表刷新地址才可以,所以30秒之内任意时间都有可能,最长是等30秒。
3.心跳:注册中心是每30秒发送一次心跳,那么如果这个注册中心服务器连续丢失3次心跳,注册表要接收一个微服务的心跳,但是连续3次都收不到,那注册中心就认为这个服务已经死掉了,注册中心会把这个服务的地址从注册表中删除,即eureka连续3次收不到心跳,会删除这个服务的地址。
4.自我保护机制:是一种特殊情况,心跳检测是正常情况下的从注册中心删除服务地址的情况,但是如果15秒内,85%以上服务器出现心跳异常,所谓心跳异常就是只要有一次心跳丢失就算是心跳异常情况,如果85%以上的服务器都出现心跳异常,注册中心eureka都收不到心跳,那就很有可能是网络中断了,注册中心和各个微服务之间是有网络连接的,那网络连接中断了,那谁的心跳就都收不到了,那现在的情况,就很可能是100%的服务都收不到心跳,而不是服务运行出错了,不是服务本身的问题。那由于网络原因,15秒内,85%以上服务器都出现了心跳异常,那这时就会触发自我保护机制,在这个机制下,会保护所有的注册地址不删除,那么即时连续丢失3次心跳也不会删除,以防止出现由于网络中断等原因导致服务无法连接注册中心,如果没有自我保护机制,注册中心会把所有服务的地址全部删除,注册中心没有了任何地址,这时网络又恢复了,其中的一个服务去向注册中心拉取注册表的话,那拉取的注册表就是一张空表,那用这张空表去调用 其他的微服务,注册表中没有任何地址就无法去远程调用其他服务了。这种服务得到空表或者地址不全的注册表,无法获得完整注册信息的情况,即保护所有的地址不删除,所有服务信息不丢失,等待网络恢复后,可以自动退出保护模式,恢复正常。另外,在开发测试期间,因为要经常的关闭重启服务,自我保护模式可能会影响测试,这时可以先关闭自我保护模式。
这时eureka注册中心运行机制的四个功能,只需添加依赖配置即可,都是全自动执行。
eureka和zookeeper的区别:
eureka是AP,强调的是可用性,A表示可用性,P表示分区容错性。不一致也没关系,只要过一段时间,最长30秒能达到一致就可以。
然后zookeeper是CP,强调的 是一致性:只要地址发生变化,它就会通知所有的客户端,要求所有客户端进行刷新,
一致性和可用性的区别还体现在自我保护机制,zookeeper是没有自我保护机制的,zookeeper只要出现连续3次心跳丢失就要把地址删除,即时是因为网络的原因,比如网络故障,网络不稳定等,也会删除服务,zookeeper是要保持强一致性。eureka是要保障故障期间的可用;
还有一点是eureka和zookeeper的集群不一样,eureka的集群是一种对等结构,然后zookeeper的集群是一种主从结构,所谓主从结构就是指一台主服务器跟几台从服务器,那客户端连接时注册拉取,都是连接的是主服务器,和主服务器通信,那除非主服务器宕机,那这时候就会有一个选举的过程,这个选举过程是比较耗时的,首先从服务器,需要一次一次地反复地去确认, 反复确认主服务确实宕机了,这个是需要时间的,那等从服务器都确认好之后呢,那现在要进行选举,选举出一台新的主服务器,那新的主服务器出现以后,那才能对外来提供服务,那在新主服务器选举出来之前,那服务是中断的,主服务器宕机,新的主服务器还没选举出来,这时候会出现服务中断的情况,这是主从结构;那对等结构指的是没有什么主从之分,所有服务器都是对等的,角色都是一样的,那客户端连接的时候,既然角色都是相同的,没有什么主从之分,那连接的时候,就需要同时去连接多台服务器,同时向多台服务器来进行注册,那这样的话,即使一台服务器宕机也没关系,其他的服务器还是可以直接连接的,这样是不会出现任何的服务中断的情况,一台宕机,其他的还可以正常连接正常访问,这叫对等的结构,这是eureka和zookeeper集群的不同,一个是对等,一个是主从。
zookeeper集群是主从结构,一般要求zookeeper注册中心的服务器数量是奇数数量,因为主服务器宕机时,从服务器需要进行投票选举新的主服务器;而eureka集群是对等结构,eureka服务器的数量没有要求,奇数偶数都可以。
eureka (注册中心)服务器的搭建:
springcloud致力于整合所有微服务的工具,以做到开箱即用的效果,让工具的使用更加简便。springcloud是基于springboot的一个工具集,通过springboot的约定好的自动配置,约定大于配置,默认的配置,一般地使用默认配置即可,除非有特殊的需求,也可以对配置进行调整,eureka也是一样,我们只需要做很简单的配置,就可以搭建eureka服务器:
1. 添加 eureka-server 依赖:连接eureka服务器注册的各个微服务器,是eureka的客户端。
2.yml添加配置:需要配置如下信息:
第一个是hostname主机名,我们把主机名配置成eureka1,这个主机名的作用是在集群内部来区分每一台eureka服务器,因为eureka注册中心,可能会搭建集群,比如eureka1,eureka2,eureka3,那它们的hostname都是不一样的,但它们的整体的服务名应该是同一个名字,比如可以起一个叫eureka-server;即整体的eureka服务名叫eureka-server,如果这个整体服务当中集群了多台服务器,那每台服务器都应该有一个hostname来加以区分;
第二个是关闭保护模式,在网络不稳定的情况下可以触发保护模式,即15秒内超过85%以上的服务器出现心跳异常,会触发保护模式,所有的注册地址都不会删除,那在开发阶段,如果不删除地址会影响开发测试,因为经常会启停服务器,很容易就达到了自我保护模式的条件,而保护模式又不删除eureka注册中心的注册地址,这样会影响开发人员测试,所以在开发测试期间可以先把保护模式关闭,等产品上线时再开启保护模式;
第三个是不注册也不拉取,即如果是一个集群服务器,服务中包括,比如eureka1,eureka2,eureka3,组成服务器的集群,那eureka1要向eureka2和eureka3进行注册,把自己的地址注册到eureka2和eureka3,另外也要从eureka2和eureka3拉取地址注册表,那同样eureka2也要向eureka1和eureka3注册;eureka3也要向eureka1和eureka2注册,即它们之间要互相注册,也要互相拉取地址表,那如果现在只有eureka1这一台服务器,没有集群eureka2和eureka3,那单台服务器是不用自己向自己注册,或者自己从自己拉取的,即在没有集群时,单台服务器是不需要执行注册和拉取操作的,所以如果是单台服务器,可以配置成不注册也不拉取,后面还会做eureka的集群,如果是集群的话,那就需要互相注册,互相拉取,那后面再对配置进行相应的调整,即单台服务器不注册也不拉取,不向自己注册,也不从自己拉取;
3.启动类添加注解 @EnableEurekaServer:启动类上要添加这个注解,启用eureka服务器,那这个注解可以触发eureka-server的自动配置,通过这个@EnableEurekaServer这个注解来触发eureka的自动配置。
那以上3步配置好,eureka服务器就直接搭建完成了,具体操作如下:
新建一个Module模块:
点击Next:
Group:cn.tedu 、Artifact:sp05-eureka、Type:Maven、Language:Java、Packaging:Jar、Java Version:8、Version:0.0.1-SNAPSHOT、Name:sp05-eureka、Description:Demo Project for Spring Boot、Package:cn.tedu.sp05:然后Next:
注: 如果是商业版IDEA,在Group框输入组名时,会联动Package框的包名;在Artifact框输入项目名,会联动Name和Package框的项目名称,自动补全。比如Group:cn.tedu,Artifact:sp05-eureka,自动补全的Name:sp05-eureka,包名为Package:cn.tedu.sp05eureka;
然后,添加的依赖,为Eureka Server,如果是商业版IDEA,会自动将添加的依赖显示在Selected Dependencies下方(社区版IDEA无任何显示):
IDEA社区版显示如下:
然后将sp05-eureka,放到springcloud1这个工程目录下(在Content root框最后一级输入项目名,会联动补全Module name 和 Module file location的默认存储位置);
然后是yml和启动类这两步配置,先配置application.yml,如果项目没有自动添加到Maven管理视图,则手动添加:
然后在Maven视图,点击此项目的validate检验
在打开的Run视图下,点击启动按钮:
在Maven视图下选择sp05-eureka,右键点击,Generate Sources and Update Folders:
等待依赖下载完成:
依赖下载完成后可能会报错如下错误,因为是通过中央库新建的SpringBoot项目时,使用的中央maven库eureka版本已经更新到Hoxton.SR8,而阿里云maven镜像并没有同步中央库,但这个报错并不影响正常使用eureka:
文本信息:
Failed to read artifact descriptor for org.springframework.cloud:spring-cloud-starter-netflix-eureka-server:jar:unknown Failure to transfer org.springframework.cloud:spring-cloud-dependencies:pom:Hoxton.SR8 from https://maven.aliyun.com/repository/public was cached in the local repository, resolution will not be reattempted until the update interval of aliyun has elapsed or updates are forced. Original error: Could not transfer artifact org.springframework.cloud:spring-cloud-dependencies:pom:Hoxton.SR8 from/to aliyun (https://maven.aliyun.com/repository/public): Transfer failed for https://maven.aliyun.com/repository/public/org/springframework/cloud/spring-cloud-dependencies/Hoxton.SR8/spring-cloud-dependencies-Hoxton.SR8.pom 'dependencies.dependency.version' for org.springframework.cloud:spring-cloud-starter-netflix-eureka-server:jar is missing.
如果想解决这个报错,可以通过使用中央库maven下载依赖,如果网络不允许,使用阿里云镜像maven库,可以通过降低eureka版本,比如将Hoxton.SR8,修改为Hoxton.SR6:
代码如下:
<properties> <java.version>1.8</java.version> <!--<spring-cloud.version>Hoxton.SR8</spring-cloud.version>--> <spring-cloud.version>Hoxton.SR6</spring-cloud.version> </properties>
打开application.yml:首先配置的是app.name,就叫做eureka-server;然后是port端口,这里用2001端口,之后是eureka的注册信息,一个是hostname,起个名叫eureka1,如果后面配置集群时,还要再搭建一台服务器,还会有eureka2;然后是自我保护模式叫self-preservation,把它关闭,设置成false,然后还有是注册和拉取,都设置成不注册也不拉取,fetch-registry: false,register-with-eureka: false,这个不注册不拉取是针对单台服务器的,对于单台服务器,不用自己向自己注册,也不用自己从自己拉取:
代码如下:
spring: application: name: eureka-server server: port: 2001 eureka: instance: hostname: eureka1 server: enable-self-preservation: false client: register-with-eureka: false fetch-registry: false
那yml配置完成以后,下一步就加个注解,eureka服务器就搭建好了:
代码如下:
package cn.tedu.sp05; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class Sp05EurekaApplication { public static void main(String[] args) { SpringApplication.run(Sp05EurekaApplication.class, args); } }
最后就可以启动eureka服务器了,打开Services界面,把Sp05EurekaApplication启动起来(如果是第一次启动服务,一般需要先通过启动类启动,然后才能添加到Services的Application视图下),就可以访问eureka了:
那在访问之前,还有一步需要修改Windows的hosts文件(C:\Windows\System32\drivers\etc\hosts),添加eureka域名IP映射,eureka1和eureka2都映射到本机,后面配置集群时,还需要搭建eureka2服务器,在这先配置好,这样在访问eureka时,就可以通过域名http://eureka1:2001或者通过http://eureka2:2002去访问了:
如果访问http://eureka1;2001访问不了:
可以先通过http://localhost:2001查看是否可以通过IP地址访问:
如果通过IP地址可以访问eureka,则判断hosts文件是否hosts文件配置的问题:
可以通过Win+R,输入drivers,快速打开hosts文件所在的drivers文件目录:
然后进入到目录 C:\Windows\System32\drivers\etc\hosts:
检查hosts文件内容:
然后通过http://eureka1:2001访问,如下:
那eureka启动之后,控制台可能会出现如下提示信息:
文本日志:
2020-10-05 14:45:07.372 INFO 7120 --- [ main] c.n.eureka.DefaultEurekaServerContext : Initializing ... 2020-10-05 14:45:07.374 INFO 7120 --- [ main] c.n.eureka.cluster.PeerEurekaNodes : Adding new peer nodes [http://localhost:8761/eureka/] 2020-10-05 14:45:07.701 INFO 7120 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using JSON encoding codec LegacyJacksonJson 2020-10-05 14:45:07.702 INFO 7120 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using JSON decoding codec LegacyJacksonJson 2020-10-05 14:45:07.702 INFO 7120 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using XML encoding codec XStreamXml 2020-10-05 14:45:07.702 INFO 7120 --- [ main] c.n.d.provider.DiscoveryJerseyProvider : Using XML decoding codec XStreamXml 2020-10-05 14:45:07.829 INFO 7120 --- [ main] c.n.eureka.cluster.PeerEurekaNodes : Replica node URL: http://localhost:8761/eureka/
这里提示了
Adding new peer nodes [http://localhost:8761/eureka/]...和 Replica node URL: http://localhost:8761/eureka/
那 Adding new peer nodes [http://localhost:8761/eureka/],这是在添加一个eureka的集群服务器,这台服务器的地址就是,http://localhost:8761/eureka/,那我们现在配置的这个eureka1是2001端口,但是这个2001的eureka启动之后,它会自动配置一台集群服务器localhost:8761;
然后,Replica node URL: http://localhost:8761/eureka/,这是副本服务器的地址为:http://localhost:8761/eureka/
这是eureka的自动配置,在我们启动2001这个端口的eureka服务器时,它启动后默认就自动配置了一台集群服务器,为localhost:8761,当然这个服务器实际上是不存在的,我们没有这个服务器,而它自动添加了这个集群服务器,想去连接这个8761,但实际上这台服务器根本不存在,那我们这个2001的eureka要去连接自动配置的这个集群服务器8761,但实际这个8761是不存在的,根据eureka的版本不同,表现出来的也会有所区别,我们这里只是提示了两条信息,有些版本会在控制台出现异常或报错,8761连接超时异常:
Caused by: org.apache.http.conn.ConnectTimeoutException: Connect to localhost:8761 timed out
那关于这个异常或者提示,我们后面会自己添加,配置eureka集群服务器eureka2,用2002这个端口,2001和2002这两个eureka服务器互相连接,那只要自己搭建了集群,这个默认的集群服务8761就不存在了,这个错就会自动消失了。或者也可以
在eureka的yml配置中添加service-url: defaultZone: http://eureka1:2001/eureka这条配置信息,不过因为后面还要自己配置集群,这里就不去管它了:
那我们的eureka服务器就搭建好了,有了eureka服务器,那其他的服务就要向eureka进行注册,我们的sp02商品,sp03用户和sp04订单,这3个微服务项目启动时,就都要向eureka1进行注册,把各自的服务名和地址注册到eureka,那这3个微服务项目是服务器的提供者,比如58同城就相当于注册中心,商家则需要把自己的地址联系方式注册到58同城,就是服务器的提供者,我们这里的微服务项目就是作为提供者来向eureka注册中心进行注册。
那服务提供者,如何连接eureka,然后向eureka进行注册,之前是搭建eureka服务器的配置,那现在是在服务提供者这里进行配置,对项目sp02,sp03和sp04都要进行如下相同的修改:
首先,需要添加一个eureka的客户端依赖,eureka-client 依赖,那添加依赖我们需要在pom.xml中添加两端配置:
一个是SpringCloud需要有一个dependencyManagement(在sp05-eureka的pom.xml中可以找到),这个dependencyManagement,其实引用的是springcloud的一个pom文件,这个pom文件里面集成了SpringCloud包含的所有工具的版本号,所有SpringCloud集成的这些工具,这些工具的版本都在一个pom文件里:
代码如下:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
这个dependencyManagement内还指定了springcloud的版本<version>${spring-cloud.version}</version>:
这个版本在pom文件的依赖上方有一个属性:
代码如下:
<properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR6</spring-cloud.version> </properties>
这是先有这一步基础配置dependencyManagement,这个标签内嵌套了一个dependencies的dependency,这里指定了springcloud的版本,是一个变量:
<version>${spring-cloud.version}</version>
这个${spring-cloud.version},这个变量的值是在properties标签里定义的,然后这个变量就代表了Hoxton.SR6,而spring-cloud.version是随便起了一个变量名而已,这个名字可以任意定义,比如叫cloud-version,还是默认spring-cloud.version都可以:
<properties> <java.version>1.8</java.version> <cloud-version>Hoxton.SR6</cloud-version> </properties>
然后,后面在dependencyManagement里指定版本时,也相应的:
<version>${cloud-version}</version>
通过${cloud-version}这种格式,这个自定义的cloud-version变量的名字去引用properties这个属性标签,即它的值Hoxton.SR6即可;当然,我们也可以直接在dependencyManagement这个标签内指定特定的版本值:
<properties> <java.version>1.8</java.version> <!-- <cloud-version>Hoxton.SR6</cloud-version> --> </properties>
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId>
<!-- <version>${cloud-version}</version> --> <version>Hoxton.SR6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
从而省去了properties这个属性标签内去指定springcloud的版本的标签;我们现在用的这个Hoxton版本,现在(2020年10月)已经升级到SR8,我们用SR4到SR8都可以用,变化不大,无非它可能是调整了一些bug,修改一些问题等等,这都是一些补丁版本。
另外就是在dependencies标签内,还要加一个dependency,即eureka的客户端依赖,而这几段代码如果自己去复制也是比较麻烦的,我们可以利用Edit Starters插件去自动生成:
注:配置eureka客户端时,dependencyManagement标签内需要指定好springcloud的版本,dependencies标签内需要有eureka-client的依赖,Edit Starters插件可以在pom.xml文件中通过Alt+Insert找到,这个插件的作用就是SpringBoot依赖管理界面,可以直接在这个依赖管理界面勾选所需要的依赖,包括dependencyManagement,包括dependency依赖项,以及springcloud的版本都可以通过Edit Starters插件自动添加好:
点击ok:
等待:
选择Eureka Discovery Client,点击Ok:
这个Edit Starters工具可能还有bug,比如dependencyManagement位置错误,并且其内部缺少dependencies,如果自动添加的依赖有错误,自己手动调整:
手动修改后如下:
代码如下:
... ... <dependency> <groupId>cn.tedu</groupId> <artifactId>sp01-commons</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR8</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
说明:SpringBoot和SpringCloud对应的版本和版本号,这个Springcloud的版本不是数字,是ABCDEFG...,按照字母顺序排列的单词,这些单词都是伦敦地铁站的站名,用伦敦地铁站站名从A-Z排列,现在使用的是H开头的Hoxton SR8,那下一个版本应该就是一个I开头的单词:
以上是第一步,添加springcloud的版本依赖和eureka依赖,那下一步就是yml配置,那在yml配置里面,配置eureka服务器的地址:service-url.defaultZone=http://eureka1:2001/eureka:
代码如下:
spring: application: name: item-service server: port: 8001 eureka: client: service-url: defaultZone: http://eureka1:2001/eureka
这里这个defaultZone属性叫默认的地点,那除了这个默认地点,在这里也可以配置成其他的自定义的地点,比如配置成beijing,或者配置成上海,或者shenzhen,这里的点信息可以自己来定义,但是如果不使用默认地点,这个地点必须得由云服务商来提供,需要花钱买,买这个eureka服务,云服务商可能会把这个服务器可能会部署到不同的地区,那这里就需要根据自己购买的云服务的信息来填写,这个地点信息得由云服务商来提供,但没有使用云服务,就是自己的服务器,那这里就只能写defaultZone,然后后面跟的就是我们自己的eureka服务器,首先eureka服务器的地址为,http://eureke1:2001,然后后面再加一个子路径/eureka,这个子路径是我们注册,拉取,包括发送心跳检测等所有的这些,和eureka服务器之间的通信操作,都是通过这个地址进行的。客户端连接的时候访问的是这个地址,http://eureka1:2001/eureka,向这个地址来提交信息,然后从这个地址来拉取注册表,都是使用的eureka1服务器地址下的eureka这个子路径。
那这是sp02商品服务的eureka客户端配置,pom.xml文件的配置和yml的配置,同样地,把sp02的这两点配置也完全相同的添加到sp03和sp04上。
以上两个配置就可以连接了,那还有第三项配置是可选的,添加注解@EnableDiscoveryclient,新版本的eureka可加可不加,但是在一些旧版本中,这个依赖时必须要加的,我们这里就省略这个第三项配置。
那sp02,sp03和sp04都配置完成以后,就把这三个服务器重新启动,重新启来之后,控制台最终会看到着这么一条日志注册状态,状态码为204,registration status: 204,如果是204状态码就说明是注册成功了:
然后看看它们在eureka里面有没有进行注册,即访问http://eureka:2001,查看eureka的注册地址表里面有没有这个注册信息:
注:eureka的客户端配置需要单独配置每个服务里,是不可以写在通用项目中的。
搭建eureka服务器,并让eureka的客户端连接到eureka服务器,都实现完成了,那下一个话题是eureka和 "服务器提供者" 的高可用,所谓高可用就是部署多台服务器,如何其中一台服务器宕机会自动连接到其他的集群服务器上,以实现业务功能的健壮性;那高可用需要让所有的服务,包括各个微服务项目,以及eureka自身都需要有备份,但这里考虑服务器性能和数量的限制,只对一个商品服务和eureka自身的高可用进行测试,那首先看商品项目sp02的高可用,我们需要做的就是启动两个商品服务,那在一台电脑上启动多个服务的话,需要改变服务的端口号加以区分,如果在不同的主机就可以相同的端口,因为主机IP地址是不同的;我们是在同一台主机启动两个相同的服务,那如果改变服务的端口,我们正启动一个SpringBoot应用可以通过java -jar命令,后面加上所需启动的jar文件所在的位置,比如,java -jar item.jar,如果直接这样运行的话,那就会通过在yml中默认配置的端口号启动,比如我们这里商品项目里配置的端口是8001,那如果想换端口启动,只需要在默认端口启动命令之后,加一个指定端口的参数,两减号和所指定的服务端口号,比如,java -jar item.jar --server.port=8002,加上指定端口参数这条命令,相当于把项目yml中配置的端口号覆盖,通过命令行指定另外一个端口,这种通过双减号的配置方式是SpringBoot配置参数的方式,给SpringBoot传参数,SpringBoot项目启动的时候,会根据我们命令行的这个命令指定的端口参数来启动,我们这里是用8002来覆盖8001;那通过命令行启动jar文件项目,这里我们利用开发工具IDEA先对商品项目进行打包:选择sp02-itemservice项目中的pom.xml右键 -- Run Maven -- install,或者在Maven管理视图中选install均可,如果想跳过测试直接打包项目,也可以选择Run Maven下的New Goal...选项,弹出Create and Run窗口,执行命令 install -Dskiptests,跳过测试进行打包,可以节省测试的时间:打包过程中可能需要下载一些插件,可能需要几分钟等待:
那以上是通过命令行执行jar文件,修改端口的方式,那也可以直接利用开发工具直接配置不同端口的统一服务项目,IDEA和STS也有所区别,STS中直接有一个BootDashBoard可以用来开启不同端口的统一服务项目,如果是IDEA可以通过快捷键Alt+8,打开Services视图,或者选择工具栏的View -- Tool Windows -- Services也可以;打开Services视图以后,可以看到列出的启动项目,如果是商用版的IDEA是可以直接列出Spring Boot标识:
如果是社区版的是不支持Spring Boot的启动配置的,那这里 用的是Application:
Application选项可以通过最右侧的小加号,选择Run Configuration Type进行添加:
如果是社区版的IDEA,就选择Application; 如果是商用版的IDEA,就选SpringBoot:
那现在我们需要对商品项目做一下配置,我们要在开发工具中配置两个启动项,一个启动项配置成8001端口,另外一个启动项配置成8002端口,java -jar item.jar --server.port=8001;java -jar item.jar --server.port=8002;那如果在开发工具中加上配置端口的启动参数,我们可以在Services界面里选中所有配置的商品服务项目,然后右键,选择Edit Configuration...:
也可以直接选择上方菜单栏的配置启动工作区的下列框设置:
配置如下,Name改为Sp02Item-8001,Program arguments,改为 --server.port=8001:
然后,再将刚才配置好的启动项复制一份,选择Sp02Item-8001,右键选择Copy Configuration...:
把名字和端口改为8002即可,Name:Sp02Item-8002,Program arguments: --server.port=8001:
最后,把这个两个商品项目Sp02Item-8001 和 Sp02Item-8002 都启动起来,查看控制台端口是否为指定端口:
然后访问http://eureka1:2001,查看eureka里面,8001和8002这两台服务器是不是都注册了:
那这两台商品服务器都注册到eureka之后,那其他微服务项目的高可用,或者eureka服务器的高可用其实就非常简单,就多启动几个就ok了,想要几台服务器就不停地启动就行,那如果是在不同的机器上启动的话,那根本就什么都不用配置,直接在多台主机上直接启动,不用任何的配置,那单台主机上,我们需要更换端口,那就配置一下启动参数即可,这是商品服务器的高可用;
那接下来看一下eureka的高可用,那么eureka的高可用就不只是要换端口了,还有两个配置需要做调整,之前eureka的配置有这么几项,一个是hostname,指的是主机名,主机名的作用就是在集群内部来区分不同的服务器,那如果有多台eureka服务器的话,那它们的名字就得不一样,比如分别叫,eureka1,eureka2和eureka3,然后还有一个是enable-self-preservation,指的是自我保护模式是否可用,这个设置成false,关闭包含模式,在项目开发期间,为了避免影响测试,这个值就保持是false就行;
还有register-with-eureka 和 fetch-register,指的是注册和拉取,都设置成false,表示不注册也不拉取,这个不注册不拉取是针对单台服务器设置的,即如果只有一个eureka服务器,那么就不用向自己注册,也不用从自己拉取,但如果是多台服务器的话,那服务器和服务器之间,要互相连接,互相注册,互相拉取,比如有三个,分别为1,2和3,那1要连接2和3 ,向2和3都注册,或者1也要从2和3去拉取注册表,同样地,2也要连接1和3,向1和3注册,从1和3拉取,那3 也一样,要连接1和2,从1和2拉取,它们3个要互相连接,互相注册,互相拉取,所以对于eureka的集群配置来说,注册和拉取都必须打开,设置为true,所以现在要配置eureka集群的话,就需要修改hostname,不同的服务器要有不同的主机名,还要修改注册和拉取的配置改为true,那这两个地方的配置,需要针对两台服务器分别进行配置;
那可以这样做,在sp05-eureka这个项目中在添加两个配置文件,一个配置文件叫application-eureka1.yml,然后另一个配置文件叫application-eureka2.yml,这两个配置文件叫profile配置,它们都是profile配置,这个profile也可以通过三个减号分隔,在一个配置文件里进行配置多个profile,那配置好的profile,我们可以有选择的进行加载,加载指定的profile,而SpringBoot的这种profile配置,可以用三个减号来写在一个配置文件里面,或者也可以用,分别放在不同的配置文件里,这样来配置profile,比如我们这里的application-eureka1.yml 和 application-eureka2.yml,这两个profile的名字呢,其实就是它们的文件名,文件后面跟一个减号eureka1,这个eureka1就是这个application-eureka1.yml文件的profile名,然后这个eureka2就是这个application-eureka2.yml文件的profile名,这是profile的名称,我们将来加载配置的时候,可以有选择去加载eureka1这个profile,或者eureka2,如果选择加载eureka1,那就是不加载eureka2,那它的加载过程就是先加载主配置文件application.yml,加载主配置之后,就会加载这个eureka1的profile配置,即主配置和所选择加载profile配置会合并加载,那合并加载的话,如果有相同的配置项,比如主配置和profile配置中都有register-with-eureka配置属性,但是值是不一样的,一个false,一个是true,那最终生效的是profile中的配置的值,profile会把主配置中相同的属性进行覆盖,所以在profile的配置里想对主配置中已经存在的属性进行调整修改的话,就可以在profile里面进行相应的配置,主配置可以做一些通用配置,比如app.name,集群服务名;关闭包含模式等;
最后,集群中所有的eureka服务器得互相连接,互相注册,那这个还得分别为两个profile文件另加一个配置server-url.defaultZone,eureka1需要连接eureka2,而eureka2需要连接eureka1,互相连接,所以在eureka1中的profile设置,server-url.defaultZone: http://eureka2:2002/eureka;反过来,在eureka2中的profile设置,server-url.defaultZone: http://eureka1:2001/eureka;那接下来首先是添加两个新的profile的配置文件,直接复制粘贴application.yml,调整一下即可,:
内容修改如下:
代码如下:
# application-eureka1.yml eureka: instance: # 主机名,集群内部区分不同服务器 hostname: eureka1 client: # 单台服务器,不注册不拉取 register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka2:2002/eureka
eureka1写完以后,再粘贴一个eureka2出来:
代码如下:
# application-eureka2.yml eureka: instance: # 主机名,集群内部区分不同服务器 hostname: eureka2 client: # 单台服务器,不注册不拉取 register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka1:2001/eureka
那下面如果要应用某一个profile的话,需要这样设置,在启动eureka服务时,加一个启动参数去激活某一个profile,即 java -jar eureka.jar --spring.profiles.active=eureka1,如果想激活eureka1,就在后面写eureka1;想激活eureka2,就在后面写eureka2;那另外这两个eureka也要在不同的端口上启动,那还要再加一个参数,指定端口号,我们这里eureka1用2001端口,eureka2用2002端口:即启动eureka1时:java -jar eureka.jar --spring.profiles.active=eureka1 --server.port=2001;启动eureka2时:java -jar eureka.jar --spring.profiles.active=eureka2 --server.port=2002;
那加好profile配置文件,下一步就是修改启动参数,在Services界面选择Sp05Eureka右键,选择Edit Configuration...,或者通过菜单栏上的选框打开工作区配置窗口也可:
修改Name为Sp05Eureka-2001,修改Program arguments为--spring.profiles.active=eureka1 --server.port=2001:
同样,我们把这个启动项复制一下:
将Name和Program arguments的端口号都改为2002:
注: 指定的profile的名字,必须跟profile的yml配置文件横杆后半部分对应;而配置的hostname可以任意取名,用于访问eureka服务器地址时对应;
然后,启动分别启动这两个eureka服务:
注:社区版的IDEA,在Services视图下的Application,最多只能显示5个项目,如果有多出来的项目要么被新添加的项目覆盖,依然只显示5个,要么将Application中多余的项目图标变成灰色:
测试结果如下,如果访问 http://eureka1:2001/,可以看到它的副本服务器DS Replicas是eureka2:
然后访问 http://eureka2:2002/,eureka2的副本服务器DS Replicas是eureka1:
那关于服务器提供者,我们这里的sp02商品,sp03用户和sp04订单三个微服务项目,它们在注册地址的时候,如果只向eureka1注册,这是可以的,注册到eureka1的地址会自动的向eureka2进行复制,eureka2会向eureka1,也是每隔30秒去拉取一次,复制eureka1的地址表,如果eureka1里面有新的地址注册进来,那eureka1里面注册的地址也会被拉取到eureka2,自动的复制更新eureka2的地址表,所以eureka的客户端,我们这里的3个微服务项目,如果只向eureka1进行注册也是没有问题的,因为eureka2会自动地每隔30秒从eureka1拉取一次地址表,当然eureka1也会每隔30秒从eureka2进行拉取,互相拉取复制,最终eureka集群的所有服务器的地址表都会达到一致,但是如果eureka的客户端只向eureka1进行注册,那eureka1宕机,eureka2就也没有用了,因为eureka的客户端根本就不去连接eureka2,这样就失去集群的意义了,所以eureka的客户端在连接的 时候,不能只去连接集群中的一个eureka服务器,而是要同时去连接集群中的所有服务器,或多台服务器,那么eureka的客户端在注册的时候,就会同时向连接的所有eureka服务器进行注册,同时连接多个eureka服务器,这样做的目的就是为了防止eureka服务器宕机,比如eureka1宕机没关系,我这个eureka2还是可用的,还可以继续连接eureka2,从eureka2去拉取地址表,或者向eureka2进行注册,所以,我们这里eureka的客户端,即sp02,sp03和sp04三个微服务项目要同时连接eureka1和eureka2两个eureka服务器,不能只连接一个,目的就是为了防止宕机,这就是eureka集群的高可用;
那现在我们需要再去调整一下sp02,sp03和sp04三个微服务项目,让它们各自多连接一个eureka2服务器,这个配置是非常简单的,就是在每个客户端连接eureka的yml配置的连接地址的配置上,多加一个eureka2服务器的地址:http://eureka2:2002/eureka,然后中间以逗号隔开即可,可以通过双击Shift搜索application.yml,分别找到sp02,sp03和sp04三个微服务项目中的yml配置文件,分别修改如下:
sp02的application.yml:
spring: application: name: item-service server: port: 8001 eureka: client: service-url: defaultZone: http://eureka1:2001/eureka,http://eureka2:2002/eureka
sp03的application.yml:
spring: application: name: user-service server: port: 8101 # 测试用的用户数据,7,8,9三个用户数据 sp: user-service: users: "[{\"id\":7, \"username\":\"abc\",\"password\":\"123\"},{\"id\":8, \"username\":\"def\",\"password\":\"456\"},{\"id\":9, \"username\":\"ghi\",\"password\":\"789\"}]" eureka: client: service-url: defaultZone: http://eureka1:2001/eureka,http://eureka2:2002/eureka
sp04的application.yml:
spring: application: name: order-service server: port: 8201 eureka: client: service-url: defaultZone: http://eureka1:2001/eureka,http://eureka2:2002/eureka
说明:eureka至少要启动两台服务器做集群,因为宕机时有发生,可能是程序本身的问题,程序有bug,也有可能是硬件出现故障,内存损坏,硬盘损坏是非常常见的;为防止服务器出现故障,如果规模比较大可以根据需求进行部署eureka服务器,如果规模更大的话,eureka服务器甚至还会部署在不同的地区,比如分别在北上广深都有部署多台eureka服务器,当服务器去连接的时候,可能会同时会连接不同地区的多台服务器,部署到不同地区主要是为了避免自然灾害,地震火灾水灾等,进行容灾备份,当然还可以对传输速度也有部分提高。
服务的消费者ribbon
RestTemplate
那eureka注册中心和服务器提供者的集群和高可用就完成了,下一个话题是服务的消费者ribbon,那服务的消费者得去调用我这些服务了,sp02,sp03和sp04;那如果用springcloud,它提供了什么工具能做这个远程调用,那我们部署这么一个服务sp06-ribbon,作为Demo功能演示项目,在这个项目里面我们就测试远程调用,那后面我们这个远程调用测试完成以后,这个demo项目就没用了,后面我们会把这个项目直接去掉直接删除,后面就不用这个项目了,这里就只是拿这个项目做一下功能测试,用它去调用sp02,sp03和sp04,演示一下远程调用的实现步骤,这个功能演示完以后,这个项目就没有用了,会把它去掉。
那最终我们要实现的远程调用业务,还是要实现从sp04订单业务里面,去调用商品和用户模块的业务功能,这是最终要实现的远程调用,那现在我们先不管订单这里,我们利用这个demo演示项目做一个纯粹的远程调用功能的测试,先把这个功能测试清楚,知道这个东西怎么添加,怎么应用,如何去做远程调用的,用这么一个项目去调用sp02,sp03和sp04,演示怎么做远程调用。
那现在看看远程调用怎么加,要做远程调用,我们现在要用一个SpringBoot提供的工具,这个工具是SpringBoot提供的,注意它不是Springcloud提供的,跟springcloud没啥关系,由SpringBoot提供的远程调用工具,叫RestTemplate,这个名字里有个Rest,很显然,它就是去调用这种Rest的API,实际上是通过HTTP去调用的,当我们运行一个web应用时,那RestTemplate,它就可以向这个web应用发送一个HTTP请求,然后得到对方的响应,那这种Rest的API的调用,一般我们得到的结果就是json格式的数据,那我们把这个json格式的字符串再转换成java的对象来使用,就这么一个简单的工具。
这个RestTemplate工具的功能是非常的简单的,它就有点类似于HttpClient工具,也是去发送请求,得到响应,那它们的区别是什么呢,这个HttpClient是比较偏底层的工具,因为它的API使用起来相当的繁琐,代码写起来非常的啰嗦,很多HTTP的协议,比如协议头,协议体的内容,都需要自己去一个一个设置,设置完之后发送给服务器,那得到的响应也得自己通过它的多个API方法,去进行一步一步的解析和处理,用起来非常的麻烦;那这个RestTemplate,那它相当于是一个高层的API,它底层封装了HTTP这些繁琐的操作,那暴露给我们开发者的API方法是非常简单的,我们只需要调用它的一个方法,就可以完成整个过程,请求、响应,包括解析json,都可以一步完成,所以RestTemplate,它是一个高层的,底层对HTTP协议进了行封装的对象;
那关于RestTemplate的API,我们后面用到的只有它的两个方法,一个是向服务器发送的get请求方法,叫getForObject(...),对于请求获取的响应数据,这个方法可以做自动的转换成一个java对象,那正常的响应,我们Rest的API一般得到的返回数据是Json格式的数据,而getForObject这个方法就可以直接把json转换成我们需要的java对象;请求、响应和类型转换一步搞定,用起来非常的简单方便;那方法接收的参数一个是url地址,向这个地址进行请求,然后第二部分是要转换数据类型,我们最后要把结果转换成一个java对象,这要写一个类型.class,把返回的结果转成一个java对象,然后第三部分是提交的参数数据,因为我们get请求的url里面有可能会携带问号,后面拼接一个或多个参数数据,那在这里就可以添加提交的参数数据,那这个参数数据如何和url路径的参数位置对应上,它提供了这么一种格式,通过大括号内部为位置编号,来充当一个占位符,然后提交的数据,有几个占位符,后面就对应跟几个参数,参数就会被自动按照顺序填充到url对应的位置上,比如,getForObject(url?a={1}&b={2}&c={3}, Type.class, 55, 3, 77),55填充到1的位置,3填充到2的位置,77填充到3的位置;那么这个getForObject方法的参数就包含这么3个部分,一个url,一个转换类型,一个提交的数据:
getForObject(url, 类型.class, 提交的数据);
然后另外一个方法发送的就是post请求,叫postForObject,和getForObject的方法作用和结构基本类似,但postForObject接收参数时是,先url,然后是提交的数据,然后第3个是转换的类型.class;与getForObject相比,post提交的参数数据会被放到协议体里,然后参数的顺序也有所区别:
postForObject(url, 提交的数据, 类型.class);
这个RestTemplate的两个方法是非常的的简单,都是一句代码搞定,就可以去调用一个远程的服务地址,这是做远程调用的工具RestTemplate,这个工具和SpringCloud无关,是由SpringBoot提供的。
ribbon
那我们先测试RestTemplate这个工具是如何去做远程调用的,测试完成以后,下一步,我们就测试SpringCloud提供的一个叫ribbon的工具,ribbon是对RestTemplate进行了封装,或者说进行功能增强的一个对象。那RestTemplate本身是一个非常简单的工具,就是做远程调用的请求和响应,但是ribbon是对RestTemplate进行了增强,对Template进行封装之后,又添加了负载均衡和重试功能,而如果只用RestTemplate的话,是没有负载均衡和重试功能的,RestTemplate仅仅只是做了一个远程调用。那我们分两步进行测试,先测试用RestTemplate做远程调用,然后再测试如果添加和使用ribbon。
那首先新建一个Module模块:
新建一个SpringBoot项目:
Group: cn.tedu,Artifact: sp06-ribbon,Java Version: 8,Name: sp06-ribbon,Package: cn.tedu.sp06
添加的依赖一个是Spring Web,一个是eureka的客户端依赖Eureka Discovery Client,然后是Ribbon依赖Ribbon [Maintenance]:
这个项目如果要做远程调用,去调用其他服务,我们后面还是要用eureka去获取地址表,得到地址表之后,再根据eureka里面地址表的地址去调用,那sp06-ribbon要调用的话,首先是从eureka服务器拉取地址到sp06-ribbon这个项目中,得到地址之后就可以根据第一个服务的地址sp02调用第一个服务,根据第二个服务的地址sp03调用第二个服务,根据第三个服务的地址sp04调用第3个服务,即ribbon得去连接eureka,从eureka拉取地址表,所以我们在添加依赖时需要加上Eureka的客户端,Eureka Discovery Client,另外还要添加ribbon依赖,后面第二步要测试ribbon,这里提前加好依赖,其实这个ribbon依赖应该也不用加,因为这个Eureka Discovery Client里面应该是已经间接的依赖ribbon了,有间接依赖,但保险起见,这里也把ribbon依赖加上,首先测试RestTemplate这个工具;然后我们把sp06-ribbon这个项目放到springcloud1这个工程下面:
项目sp06-ribbon建好 以后,那我们还需要在这个项目的pom.xml文件中,添加一个sp01-commons的依赖,让它依赖于sp01通用项目,我们这里要用到通用项目里面的工具类,还有pojo实体类,通过Alt+insert快捷键,搜索sp01:
添加依赖:
然后是yml配置,如果项目没有添加到Maven管理,先添加入Maven管理:
修改yml文件,首先是基本的配置项app.name:ribbon,然后是端口server.port:3001,还要配置连接eureka,要从eureka获取地址表,得到地址表以后,ribbon再根据地址表的地址去远程调用微服务项目,eureka的service-url.defaultZone:http://eureka1:2001/eureka,http://eureka1:2001/eureka,通过两个eureka的地址连接这两个eureka服务器:
这两个基本配置就做完了,一个是在pom.xml中添加依赖,在创建SpringBoot项目时,添加了web,eureka客户端 和 ribbon这3个依赖:
然后又手动添加了通用项目sp01-commons依赖:
然后在application.yml中配置了三个基本 的配置项,应用名,端口号,还有连接eureka的服务器地址:
代码如下:
spring: application: name: ribbon server: port: 3001 eureka: client: service-url: defaultZone: http://eureka1:2001/eureka,http://eureka2:2002/eureka
那以上配置完之后,就可以开始写代码了,首先是要测试RestTemplate远程调用工具,那这个工具的对象实例需要我们自来创建,这个对象我直接在启动类里面去创建,当然也可以选择不在启动类创建,自己加一个自动配置类,现在就为了方便,不去加自动配置类了,直接用启动类这个主程序里添加,先写一个注解@Bean,修饰一个方法,这个方法起名叫restTemplate,返回值为RestTemplate类型的对象,方法内部return,直接new RestTemplate();,新建一个RestTemplate实例,直接返回即可,这个实例需要自己来创建,即新建一个RestTemplate实例,然后通过注解@Bean,把它放到Spring容器中管理:
代码如下:
package cn.tedu.sp06; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class Sp06RibbonApplication { public static void main(String[] args) { SpringApplication.run(Sp06RibbonApplication.class, args); } @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
另外,itemService启动时要向所有配置的eureka服务器进行注册,每隔30s也要分别向所有的eureka服务器拉取注册表,集群的eureka服务器之间还要互相拉取注册表,这样eureka服务器越多,对微服务运行效率是有影响的,而且还会占用网络带宽。如果注册的服务非常的多,eureka服务器也比较多的话,那这个网络上传输的注册表就可能会有很多的数据,注册表里会注册大量的服务器地址,服务器甚至可能达到成千上万台,那注册表中就会注册成千上万个服务器地址,这个注册表本身占用的内存就会变大,这样每个服务在从eureka拉取注册表时,那带宽还会有占用的情况,但这个带宽占用的再多,即便注册表里有成千上万个地址,注册表的数据量也不会太大,当然对带宽可能也有些影响,但影响不会太大,如果觉得带宽的占用接受不了,那也可以增大带宽来解决这个问题,就是服务器和服务器之间现在用千兆网络,如果千兆网络不够可以通过增加光纤再增加带宽,服务之间注册和拉取注册表主要影响的是网络连接和传输问题,对我们服务本身的运行效率的影响并不太大,因为从注册表中查找其中一个地址去调用服务,这个运算量对于计算机来说并不算什么,就是处理一些地址数据而已。
那我们已经RestTemplate对象交给了Spring来管理,现在我们就要利用这个RestTemplate实例去做远程调用,去调用sp02,sp03和sp04,商品,用户和订单这几项服务,测试RestTemplate这个工具是如何实现远程调用功能的,那想利用这个RestTemplate去做远程调用,我们还得在这个sp06-ribbon项目当中添加一个Controller,我们浏览器访问时,是访问sp06这个项目的Controller,然后Controller里用RestTemplate这个工具去调用远程服务,所以我们首先在这个项目里面添加一个控制器Controller,提供几个访问路径,我们新建有一个控制器叫RibbonController,放在controller包下,虽然叫RibbonController,但这一步我们还不测试ribbon,只是单纯的去测试RestTemplate这个工具而已,在这个RibbonController中,首先在类上添加两个注解@RestController和@Slf4j,把这个类交给spring管理,方法返回值设置成json格式,和添加日志输出,然后通过@Autowired把RestTemplate注入进来:
package cn.tedu.sp06.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@Slf4j
public class RibbonController {
@Autowired
private RestTemplate rt;
}
然后是加一个调用路径@getMapping("/item-service/{orderId}"),我们加一个 /item-service子路径,当访问这个子路径时,在这个子路径后面传一个参数orderId,那在这个路径对应的方法里,我们要去远程调用商品服务item-service,然后向这个商品服务提交orderId参数,来查询这个订单的商品数据,那远程的商品服务它返回的是一个json格式的数据,我们要对json数据做一个转换,转换成我们需要对象,那转换的成的对象还是一个jsonResult类型,这个JsonResult封装了商品列表的数据,即JsonResult<list<Item>>,把它作为方法返回值,去响应客户端浏览器,这个路径对应的方法名,我们叫getItems,方法参数就是路径传过来的orderId,即@PathVariable String orderId;然后在getItems这个方法体里面,我们就要使用RestTemplate这个工具调用远程商品服务,通过它的getForObject方法发送一个get请求,传入第一个参数url,远程商品服务的请求地址是http://localhost:8001,后面再加一个占位符 /{1},这个占位符是RestTemplate工具自己定义的一种占位符格式,如果还有第二个参数就再加一个{2}占位,它们之间以逗号分隔,以此类推;那我们后面还要用路径方法中传递过来的参数orderId来填充这个占位符,这个占位符由getForObject方法后面的参数决定,还有一个参数是responseType,需要转换成的对象类型,远程调用商品服务返回的结果是一个json数据,那这个json要转换成的对象类型由这个参数所决定,我们这里转换的数据类型为JsonResult.class;然后最后一个参数就是要提交的参数数据,我们这里根据url路径后面只有一个占位符,所以只有一个提交参数orderId填充{1}占位符的位置,最后return,将结果返回:
@GetMapping("/item-service/{orderId}")
public JsonResult<List<Item>> getItems(@PathVariable String orderId){
//使用 rt 工具,调用远程商品服务
return rt.getForObject("http://localhost:8001/{1}", JsonResult.class, orderId);
}
这个方法就是向参数中指定的路径提交一个get请求,请求中的参数由占位符{1}代替,方法底层会由后面的参数orderId进行填充,然后调用远程服务的url返回的结果是一个json数据,这里通过第二个参数指定为JsonResult对象类型,方法底层会把调用远程接口获取的json数据转换成JsonResult类型,最后把这个JsonResult返回给浏览器;这是第一个方法getItems,远程调用商品服务获取商品列表操作;那在商品服务中,还有一个是减少商品的库存的操作,这是一个post请求,我们要向远程服务提交一个post请求,因为在商品项目sp02-itemservcie里面,我们定义的减少商品库存操作,处理的就是post请求,@PostMapping("/decreaseNumber"),所以访问这个远程商品服务的url请求路径时只接受post请求,get请求是不接受的;
所以RibbonController这里,我们远程调用商品服务的减少商品库存功能时,也必须是一个post请求,而且在post请求的协议体里,我们还要携带商品信息的数据,使用json格式作为参数进行传输,比如[{...}, {...}, ...],中括号里面的大括号表示一个一个的商品信息,把这个json串放入请求协议体里向远程进行提交,那提交的路径依然加一个/item-service子路径,之后再加一个表示功能子路径/decreaseNumber,即,
@PostMapping("/item-service/decreaseNumber")
那这个路径所对应的方法也起名叫decreaseNumer,方法的返回结果也为JsonResult,然后这个方法的参数为 List<Item> items,并用注解@RequestBody修饰,从请求的协议体以json格式来接收参数,接收的是一组商品列表,然后方法体内通过RestTemplate工具的postForObject方法发送一个post请求,远程调用商品服务的减少库存操作,并把商品数据放在协议体中,向远程服务提交,发送post请求;postForObject方法的第一个参数url为商品服务的这个请求地址,http://localhost:8001/decreaseNumer,第二个参数提交post请求协议里的数据items,由浏览器端post请求提交的协议体中得到,直接把这一组商品放入协议体,这个RestTemplate工具会对Java对象做自动的数据转换,把商品列表数据由java对象会自动的转换成json格式,放入协议体,然后第3个参数responseType返回的类型,还是JsonResult.class:
完整代码:
package cn.tedu.sp06.controller; import cn.tedu.sp01.pojo.Item; import cn.tedu.web.util.JsonResult; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import java.util.List; @RestController @Slf4j public class RibbonController { @Autowired private RestTemplate rt; @GetMapping("/item-service/{orderId}") public JsonResult<List<Item>> getItems(@PathVariable String orderId){ //使用 rt 工具,调用远程商品服务 return rt.getForObject("http://localhost:8001/{1}", JsonResult.class, orderId); } @PostMapping("/item-service/decreaseNumber") public JsonResult<?> decreaseNumber(@RequestBody List<Item> items){ //用 rt 调用远程服务,发送post请求 //商品数据放在协议体中,向远程服务提交 return rt.postForObject("http://localhost:8001/decreaseNumber", items, JsonResult.class); } }
注:此处的postForObject方法第第二个参数,是否可以直接传入json串,从而省去在此方法上进行@RequestBody转换的操作(见: Spring - RestTemplate 方法概述)。
我们这里使用的是RestTemplate这个工具去做的远程调用,它并不是由springcloud的提供的,就是SpringBoot或者说由Spring提供的一个调用远程服务的便捷API。我们以上分别演示了这个工具的get和post请求的远程调用方法,那接下来就可以测试了,测试这个Demo演示项目远程调用商品服务sp02-itemservice,首先启动这个这个Demo项目sp060-ribbon,当然商品服务sp02也需要是启动的,启动完之后,访问的话,我们需要访问的是sp06这个项目中的这两个路径 @GetMapping("/item-service/{orderId}") 和 @PostMapping("/item-service/decreaseNumber");那我们创建的sp06项目所指定的端口为3001,首先访问 http://localhost:3001/item-service/354t3t34t,IP地址和端口为localhost:3001,先是跟一个子路径item-service,然后随便传一个订单ID,响应结果如下:
那这个结果是从远程的商品服务调用返回来的,我这里调用的是sp06项目的3001端口,然后,这个3001端口的sp06项目调用的是sp02商品服务的8001端口,然后再从8001端口的这个sp02商品服务把响应结果返回给3001端口的sp06项目,sp06再把结果返回给浏览器,得到我们现在所看到的结果,这个结果实际上是商品服务的执行结果;然后还有一个post请求的访问路径是 http://localhost:3001/item-service/decreaseNumber,减少商品库存操作,我们利用postman这个工具去测试,数据呢,还是在body体里面,我们传原始的json格式,两件商品 [{"id":1, "name":"abc", "number":23},{"id":2, "name":"def", "number":11}],
响应结果如下,减少商品库存成功:
测试访问路径汇总,后面4个为用户和订单服务:
http://localhost:3001/item-service/35http://localhost:3001/item-service/decreaseNumber
使用postman,POST发送以下格式数据:[{"id":1, "name":"abc", "number":23},{"id":2, "name":"def", "number":11}]
http://localhost:3001/user-service/7
http://localhost:3001/user-service/7/score?score=100
http://localhost:3001/order-service/123abc
http://localhost:3001/order-service/
同样,这里还是做了一个远程调用,3001端口的这个sp06项目,它去调用了远程服务8001端口的sp02项目,这里都是post请求,首先在浏览器端向sp06发起post请求,然后sp06再通过RestTemplate工具远程调用sp02商品服务,发起post请求,然后再得到返回的结果。
那远程调用sp02商品服务做完了,我们再把远程调用sp03用户服务和sp04订单服务也加上,把它们全都写在一块,都写在RibbonController里,那在调用商品服务之后:
(1) 先写调用用户服务的两个操作,第一个是通过用户ID获取用户 @GetMapp("/user-service/{userId}"),然后再来一个在其后加一个score子路径 @GetMapp("/user-service/{userId}/score"),是通过用户ID给此用户增加积分,这是远程调用用户的操作路径;
(2) 然后还有订单的是用order-service子路径,并在其后加一个orderId,即 @GetMapping("/order-service/{orderId}"),根据订单ID获取订单信息;然后还有一个是直接访问订单服务的根路径 @GetMapping("/order-service/"),保存订单;
代码如下:
@GetMapping("/user-service/{userId}") @GetMapping("/user-service/{userId}/score") @GetMapping("/order-service/{orderId}") @GetMapping("/order-service/")
然后这4个路径所对应的4个方法,
(1) 第一个方法:通过用户ID获取用户的方法 getUser(@PathVariable Integer userId),参数是接收userId,返回的对象是JsonResult<User>,方法体里调用RestTemplate的getForObject方法, return rt.getForObject("http://localhost:8101/{1}", JsonResult.class, userId);,第一个参数是url路径访问用户服务,第二个参数是返回的类型,第三个参数是uriVariables;
(2) 第二个方法:通过用户ID增加用户的积分的方法addScore(@PathVariable Integer userId, Integer score),参数接收两个,一个是userId,还有一个是积分值score,这个score接收的是通过在score子路径后面,问号拼接到这个url路径上的参数,比如"/user-service/{userId}/score?score=1000",方法的返回对象为JsonResult<?>,这里为什么是问号????,方法体里调用RestTemplate的getForObject方法, return rt.getForObject("http://localhost:8101/{1}/score?score={2}", JsonResult.class, userId, score);,第一个参数是url路径,这个请求路径里面有两个uri参数,第二个参数返回的类型,之后是url路径当中的两个uri参数,一个是useId,一个是score;
(3) 第三个方法:通过订单ID获取订单信息的方法getOrder(@PathVariable String orderId),接收的路径参数是orderId,方法的返回值为JsonResult<Order>,方法体调用getForObject方法 return rt.getForObject("http://localhost:8201/{1}", JsonResult.class, orderId);,第一个参数url路径是http://localhost:8201/{1},第二个参数返回的类型为JsonResult.class,之后url占位符的uri参数为orderId;
(4) 第四个方法:添加订单,保存订单的方法 addOrder(),这个方法不接收任何的参数,因为这里做的是Demo测试,数据都是写死的 (正常是要传入需要增加的订单对象作为参数的),方法的返回值就是JsonResult,不指定泛型,方法体里直接调用getForObjet方法 return rt.getForObject("http://localhost:8201", JsonResult.class);,url没有占位符参数,不需要指定uri参数;
这是调用用户服务和订单服务,获取用户,增加用户积分,获取订单,保存订单的四个方法:
代码如下:
package cn.tedu.sp06.controller;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.sp01.pojo.Order;
import cn.tedu.sp01.pojo.User;
import cn.tedu.web.util.JsonResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
@Slf4j
public class RibbonController {
@Autowired
private RestTemplate rt;
@GetMapping("/item-service/{orderId}")
public JsonResult<List<Item>> getItems(@PathVariable String orderId){
//使用 rt 工具,调用远程商品服务
return rt.getForObject("http://localhost:8001/{1}", JsonResult.class, orderId);
}
@PostMapping("/item-service/decreaseNumber")
public JsonResult<?> decreaseNumber(@RequestBody List<Item> items){
//用 rt 调用远程服务,发送post请求
//商品数据放在协议体中,向远程服务提交
return rt.postForObject("http://localhost:8001/decreaseNumber", items, JsonResult.class);
}
@GetMapping("/user-service/{userId}")
public JsonResult<User> getUser(@PathVariable Integer userId) {
return rt.getForObject("http://localhost:8101/{1}", JsonResult.class, userId);
}
@GetMapping("/user-service/{userId}/score")
public JsonResult<?> addScore(@PathVariable Integer userId, Integer score) {
return rt.getForObject("http://localhost:8101/{1}/score?score={2}", JsonResult.class, userId, score);
}
@GetMapping("/order-service/{orderId}")
public JsonResult<Order> getOrder(@PathVariable String orderId) {
return rt.getForObject("http://localhost:8201/{1}",JsonResult.class,orderId);
}
@GetMapping("/order-service/")
public JsonResult addOrder() {
return rt.getForObject("http://localhost:8201/",JsonResult.class);
}
}
那这用户服务和订单服务的这4个方法就加在了RibbonController中,接下来重启sp06-ribbon这个项目测试一下:
测试访问路径如下:
http://localhost:3001/user-service/7
http://localhost:3001/user-service/7/score?score=100
http://localhost:3001/order-service/123abc
http://localhost:3001/order-service/
访问效果如下:
(1) 调用用户服务获取用户:http://localhost:3001/user-service/7
(2) 调用用户服务增加用户积分:http://localhost:3001/user-service/7/score?score=100
(3) 调用订单服务获取订单,获取订单是只有订单ID,用户和商品在这没有,因为后面是要做远程调用,远程调用用户来获取用户,或者远程调用商品来获取商品,这两步的远程调用还没加,所以只有一个ID:http://localhost:3001/order-service/123abc
(4) 添加订单,保存订单,添加订单就返回一个200ok的结果,其他的信息都没有,就代表保存成功,保存成功返回一个200的ok结果:http://localhost:3001/order-service/
注:为什么在输出网页上会标出执行时间与访问网址,而在代码中没有去写输出语句,难道是和jsonView插件有关????
那我们已经测试了用RestTemplate工具实现远程调用的功能,这个工具的功能是非常简单且单一的,那我们调用商品的8001端口的这台服务器,我们通过访问localhost:8001/item-service/{1},这种写死的方式去调用商品服务,就只能调用这一台服务器,如果启动两台,甚至更多的商品服务器,做服务器集群,在集群服务器间来回的轮询调用请求,实现负载均衡等功能,那就得用ribbon来实现:Ribbon是一个对RestTemplate做了进一步的封装和功能的增强远程调用工具,它除了可以实现远程调用,还实现了负载均衡的功能,通过对RestTemplate添加 @LoadBalanced 注解 增强其功能,实现负载均衡;那负载均衡的实现过程如下:
首先,从注册中心eureka获得地址注册表,在eureka地址注册表中保存着我们集群服务器的注册信息,包括集群服务名和集群中对应的各个服务器的访问地址,比如商品服务的集群服务ID为item-service,有localhost:8001和localhost:8002两个地址,然后在这些地址之间就可以轮询调用;然后,调用地址使用集群服务ID,而不是写具体的访问的那台主机的IP地址和端口号,比如商品服务的集群服务ID为item-service,那么访问集群服务的地址就可以写成 http://item-service/{1},那如果写成集群服务ID的话,它就根据服务ID来获得这个集群服务器的地址列表,来获得它的多个地址,那之后就在这个集群服务器的列表当中的服务器地址,来回轮询调用;那这个Ribbon底层远程调用的实现,其实还是通过RestTemplate来调用的,它得到集群服务器地址列表中的一个地址,然后用RestTemplate去调用这个地址,再得到地址表里面第二个地址,还是用RestTemplate去调用,再取出第三个地址,还是一样用RestTemplate去调用,所以底层的调用实现还是使用的RestTemplate,只不过是在调用之前,会有这样的一个过程:
1.从注册中心获得地址表
2.调用地址使用服务id: http://item-service/{1}
3.根据服务id,获得这个服务集群服务器主机的地址列表
4.在集群地址列表中轮询调用
那概括来讲,实现这个负载均衡就是三步操作,第一步,添加ribbon的依赖;第二步就是添加@LoadBalanced注解,这个注解是要加到RestTemplate上面;第三步就是把调用的地址改成集群服务器的ID,而不直接写主机地址。
先检查一下是否有ribbon的依赖,打开开发工具IDEA项目sp06-ribbon下的pom.xml文件,选择Dependency Analyzer,查找如下:
这个ribbon依赖在创建项目时,我们单独添加了一个spring-cloud-starter-netflix-ribbon,但是这个依赖其实是可以不加的,加上也没影响,其实在这个eureka客户端里面,已经集成了这个ribbon的依赖,只要有eureka-client,而eureka-client里面呢,有ribbon的依赖。
然后之后是添加@LoadBalanced注解,这个注解是加到RestTemplate这个对象上,就是我们在SpringBoot启动类里创建了RestTemplate实例,并通过注解@Bean,把这个对象放入了Spring容器,那现在我门在RestTemplate这个对象上要多加一个注解,就是@LoadBalanced,这个注解是由Ribbon提供的,加上了这个注解之后,就对RestTemplate进行了功能的增强。
代码如下:
package cn.tedu.sp06; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication public class Sp06RibbonApplication { public static void main(String[] args) { SpringApplication.run(Sp06RibbonApplication.class, args); } @LoadBalanced @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
然后第3步,在做调用的时候,调用的地址,我们要改成集群服务的id,就是在RibbonController里面,我们都是用RestTemplate去调用,调用的地址之前写的都是主机的地址,比如 http://localhost:8001/{1},http://localhost:8101/{1} 等:
那现在我们在RestTemplate的getForObject或postForObject,这两个方法传入的url参数,就不写主机地址了,而是要改成写集群服务器的serviceId,写每一个服务,它的服务ID,第一个呢是调用商品服务,那就改成商品服务item-service:
那这个名字其实就是在eureka的注册表里面注册的名字,我们管它叫服务ID,或者叫服务名,这里是item-service,order-service 和 user-service:
那item-service,通过这个在eureka注册中心注册集群服务名,就可以得到这个集群里的多个主机地址,比如localhost:8001 和localhost:8002:
然后其他的调用地址也改成serviceId,订单服务是order-service代替主机的IP和端口,那这样ribbon就可以对RestTemplate进行功能增强,添加负载均衡的功能,当使用RestTemplate调用时,就会获取注册表当中的多个主机地址,然后对多个地址进行轮询,代码如下:
package cn.tedu.sp06.controller; import cn.tedu.sp01.pojo.Item; import cn.tedu.sp01.pojo.Order; import cn.tedu.sp01.pojo.User; import cn.tedu.web.util.JsonResult; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; import java.util.List; @RestController @Slf4j public class RibbonController { @Autowired private RestTemplate rt; @GetMapping("/item-service/{orderId}") public JsonResult<List<Item>> getItems(@PathVariable String orderId){ //使用 rt 工具,调用远程商品服务 return rt.getForObject("http://item-service/{1}", JsonResult.class, orderId); } @PostMapping("/item-service/decreaseNumber") public JsonResult<?> decreaseNumber(@RequestBody List<Item> items){ //用 rt 调用远程服务,发送post请求 //商品数据放在协议体中,向远程服务提交 return rt.postForObject("http://item-service/decreaseNumber", items, JsonResult.class); } @GetMapping("/user-service/{userId}") public JsonResult<User> getUser(@PathVariable Integer userId) { return rt.getForObject("http://user-service/{1}", JsonResult.class, userId); } @GetMapping("/user-service/{userId}/score") public JsonResult<?> addScore(@PathVariable Integer userId, Integer score) { return rt.getForObject("http://user-service/{1}/score?score={2}", JsonResult.class, userId, score); } @GetMapping("/order-service/{orderId}") public JsonResult<Order> getOrder(@PathVariable String orderId) { return rt.getForObject("http://order-service/{1}",JsonResult.class,orderId); } @GetMapping("/order-service/") public JsonResult addOrder() { return rt.getForObject("http://order-service/",JsonResult.class); } }
那我们把商品集群服务的8001和8002都启动起来,eureka集群服务器也启动起来,sp06-ribbon服务器也启动起来:
那现在我们去调用商品服务,因为商品服务是有多台服务器做了集群,访问http://localhost:3001,通过调用3001端口的这台ribbon服务器,去调用商品服务item-service,再传入一个id,即 http://localhost:3001/item-service/46y4y43y45y,那现在是调用了商品服务器集群中的8001端口的服务器:
再刷新,8002端口的商品服务器:
那商品集群服务器中的8001& 8002,这两台服务器会轮询请求。在开发工具的控制台也会看到,8001请求了几次:
然后,8002请求了几次:
测试访问:http://localhost:3001/item-service/34 如下:
另外需要注意的是,Maven Helper插件的应用:
我们需要这个Maven Helper插件来管理依赖,利用这个插件来管理依赖,是非常方便的,我们打开项目的pom.xml文件:
(1) 可以在打开pom.xml之后的Dependency Analyzer选项下搜索框直接搜索;可以查看所有依赖完整的依赖树结构,树状结构;查看有冲突的jar包,那这种冲突可能没有问题,也可能有问题,如果有问题的话可以在这里方便的将某个冲突的版本进行排除;
(2) 可以对依赖进行管理,比如当某个依赖下的子依赖,与项目中原有依赖产生冲突时,想把这个依赖排除掉,利用MavenHelper操作就比较方便,可以首先搜素到这个依赖,比如搜素dataformat:
然后,选中多余的依赖,鼠标右键,选中Exclude,而实际这个操作就是在pom.xml文件,通过对要排除的依赖添加exclusions标签进行操作的:
这里spring-cloud-starter-netflix-eureka-server会间接的依赖于很多其他的jar包,通过MavenHelper插件就可以方便地排除掉多余的dataformat依赖,实际MavenHelper修改了配置文件中的代码,修改如下:
sssssssssssss