如果說用Spring Boot+Spring MVC是開發單體應用(或單體服務)的利器,那么Spring Boot+Spring MVC+Spring Cloud將是開發分布式應用(快速構建微服務)的又一法寶,相信大家如果看到我近期總結的《JAVA WEB快速入門》系列文章,對Spring Boot+Spring MVC應該是比較熟悉了吧,從本文開始,一起來熟悉Spring Cloud、玩轉Spring Cloud,至於什么是Spring Cloud?我這里就不再介紹了,網上資源太多了,比如:大話Spring Cloud、SpringCloud是什么?,當然介紹Spring Cloud系列文章也比較多(比如:https://blog.csdn.net/forezp/article/details/70148833),大家也可以參考,我這里只是結合當前最新的Spring Boot、Spring MVC、Spring Cloud來重新演練一遍,把重要的知識點、遇到的一些坑分享出來,一來是為自己做記錄(所謂“好記性不如爛筆頭”),二來可以避免大家學習時走彎路,又因為介紹Spring Cloud文章實在太多了,故玩轉Spring Cloud系列文章更多的是以把實現的DEMO代碼一步步貼出來,一些組件名詞我就不再詳細解釋了,然后對於涉及的重要知識點及踩坑點進行說明,以便大家可以:知其然還能知其所以然。(注:所有示例代碼均采用IDEA IDE編寫)
一、實現eureka server(注冊中心)
1.1.通過IDEA來創建一個空的spring boot項目(類型是:maven-archtype-quickstart,這樣最精簡,當然如果你使用webapp項目也是可以,只是認為沒有必要)。
創建步驟有2種,第一種是使用maven創建: maven->maven-archtype-quickstart,然后手動添加相關的spring boot依賴;第二種是使用spring initializer->填寫項目參數->選擇相關依賴(可直接選擇spring cloud相關依賴,如:eureka,這樣就一步到位,這里全部先不選),最終的初始POM XML如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>cn.zuowenjun.cloud</groupId> <artifactId>eurekaserver</artifactId> <version>1.0-SNAPSHOT</version> <name>eurekaserver</name> <!-- FIXME change it to the project's website --> <url>http://www.zuowenjun.cn</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> </parent> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
如上所示(如果不是請改成這樣,如果只是多點依賴沒關系,當然我認為此時只需要這么多的依賴即可,多了也無用),我們只是有spring boot的POM依賴,並沒有spring cloud的相關依賴。
1.2添加spring cloud相關依賴,如下所示:(添加了dependencyManagement節點,並配置spring-cloud-dependencies pom import依賴,目的是:便於依賴繼承,與parent節點功能類似,添加具體依賴時,若包含在parent中或pom import依賴中則無需版本號,能夠保證組件的一致性,詳見:https://blog.csdn.net/mn960mn/article/details/50894022,相反如果沒有配置spring-cloud-dependencies pom import依賴,則添加具體依賴時需要指定version版本號,而且需要注意各依賴組件間的兼容性問題,如下面我把version注釋掉)
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> ... ...其它原有依賴 <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> <!--<version>2.1.0.RELEASE</version>--> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> <!--<version>2.1.0.RELEASE</version>--> </dependency> </dependencies>
1.3.在resouces目錄下(若沒有請創建,注意設為souces root目錄,方法:右鍵文件夾->Mark directory as->souces root)創建application.yml(或application.properties,本文示例全部使用yml),添加如下配置:
server:
port: 8800
spring:
applcation:
name: demo-eurekaserver
# config detail:https://www.jianshu.com/p/98f4e5f6bca7 or https://blog.csdn.net/wo18237095579/article/details/83276352
eureka:
instance:
hostname: eurekaserver1 #實例主機名,集群時需要且唯一
server:
enable-self-preservation: true #自我保護,正式環境不要這么做
eviction-interval-timer-in-ms: 5000 #定期清理失效節點,默認60s
peer-eureka-nodes-update-interval-ms: 6000 #同步更新節點頻率,默認10min
renewal-percent-threshold: 0.49 #默認0.85
response-cache-auto-expiration-in-seconds: 30
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:${server.port}/eureka/
1.4.在spring boot 啟動類中添加@EnableEurekaServer即可,如下代碼:

package cn.zuowenjun.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @EnableEurekaServer @SpringBootApplication public class App { public static void main( String[] args ) { SpringApplication.run(App.class, args); } }
整個項目結構如下圖示,啟動后瀏覽地址:http://localhost:8800/,會出現spring eureka的主頁,就表明eureka server成功了。
二、實現service provider(含eureka client)--服務提供者
【即:具體微服務項目,注冊服務信息,暴露API】,當然也有可能同時是service consumer【服務消費者】,需要遠程調用其它服務
2.1.參照1.1方式創建一個空的spring boot項目,然后添加spring cloud 相關依賴(這里主要是:eureka-client【實現服務自動發現與注冊】、web【即:springMVC,實現服務API】),POM XML添加配置如下:
<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--當.yml配置不生效時,應添加snakeyaml依賴,但一般spring-boot-starter中默認有此依賴,非spring boot項目需要添加--> <dependency> <groupId>org.yaml</groupId> <artifactId>snakeyaml</artifactId> <version>1.23</version> </dependency> </dependencies>
2.2.在application.yml文件中添加如下配置(若沒有請參見1.3法創建):注意spring.application.name,這個是服務實例名,注冊及服務消費時均需使用該名稱
server:
port: 8801
spring:
application:
name: helloservice
ip: localhost #自定義配置,在demo代碼中有用到
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8800/eureka/
3.3.編寫controller 服務相關代碼,在spring boot啟動類上添加@EnableDiscoveryClient注解,具體完整實現代碼如下:(除了@EnableDiscoveryClient注解,基余代碼與普通的spring MVC項目代碼均相同)

//controller: package cn.zuowenjun.cloud.controller; import cn.zuowenjun.cloud.model.Result; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class DemoController { @Value("${spring.application.name}") private String serviceName; @Value("${spring.application.ip}") private String address; @Value("${server.port}") private String port; @Autowired DiscoveryClient discoveryClient; @GetMapping(value = "/") public String index(){ return "demo service"; } @RequestMapping("/hello") public Object hello(){ return discoveryClient.getServices(); } @RequestMapping("/info") public Result info(){ Result result = new Result(); result.setServiceName(serviceName); result.setHost(String.format("%s:%s", address, port)); result.setMessage("hello"); return result; } @RequestMapping(value = "/multiply/{a}/{b}") public Result multiply(@PathVariable("a") int a,@PathVariable("b") int b){ Result result = new Result(); result.setServiceName(serviceName); result.setHost(String.format("%s:%s", address, port)); result.setMessage("ok"); result.setContent(a * b); return result; } } //model: package cn.zuowenjun.cloud.model; public class Result { private int code; private String message; private Object content; private String serviceName; private String host; public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Object getContent() { return content; } public void setContent(Object content) { this.content = content; } public String getServiceName() { return serviceName; } public void setServiceName(String serviceName) { this.serviceName = serviceName; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } } //App spring boot啟動類: package cn.zuowenjun.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @EnableDiscoveryClient @SpringBootApplication public class App { public static void main( String[] args ) { SpringApplication.run(App.class, args); } }
完成上述步驟后即實現了服務提供者項目,完整項目結構如下圖示,啟動運行http://localhost:8801/multiply/324/561(只需關注這個服務方法,后面服務消費會調用這個方法) ,可以看到正常響應出JSON結果,如:"code":0,"message":"ok","content":181764,"serviceName":"helloservice","host":"localhost:8801"}
為了后面服務消費者能體驗出負載均衡的效果,可以把該項目再以另一個端口(server.port=8802)重新啟動運行一個實例(IDEA啟動多個實例的方法請參見:https://blog.csdn.net/forezp/article/details/76408139,最后不一定要改yml中的port配置,也可以直接在Edit Configuration--> program argements中指定:--server.port=8802即可,原理與直接通過命令:java -jar xxx --server.port=8802類似),這樣就會有兩個服務提供者了,如果查看eureka server主頁(http://localhost:8800/)會在Instances currently registered with Eureka列表中展示出2個服務實例信息,如下圖示:
三、實現service consumer(含eureka client)--服務消費者
【即:需要調用微服務API的項目,相對eureka,service provider來講,就是客戶端,消費方】,當然也有可能是service provider【服務提供者】,暴露服務API給其它微服務項目
3.0.參照1.1方式創建一個空的spring boot項目,然后添加spring cloud 相關依賴(這里僅先是:eureka-client【實現服務自動發現與注冊】、web【即:springMVC,實現服務API】),POM XML添加配置如下:

<properties> <java.version>1.8</java.version> <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <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>
3.1方式一:使用restTemplate+ribbon實現服務消費(負載均衡調用遠程服務)
3.1.1.在POM XML中添加spring-cloud-starter-netflix-ribbon依賴,如下:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency>
3.1.2.編寫controller相關代碼(含遠程服務調用類HelloService),修改spring boot 啟動類,具體完整實現代碼如下:

//spring boot啟動類: package cn.zuowenjun.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableDiscoveryClient @SpringBootApplication class EurekaclientconsumerApplication { public static void main(String[] args) { SpringApplication.run(EurekaclientconsumerApplication.class, args); } @LoadBalanced @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } } //controller: package cn.zuowenjun.cloud.controller; import cn.zuowenjun.cloud.service.HelloRemoteService; import cn.zuowenjun.cloud.service.HelloService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @Autowired private HelloService helloService; @RequestMapping("/x") public Object multiplyForRestTemplate(@RequestParam int a, @RequestParam int b) { return helloService.multiply(a,b); } } //HelloService(遠程服務代理類) : package cn.zuowenjun.cloud.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class HelloService { @Autowired private RestTemplate restTemplate; @Value("${spring.application.helloServiceProvider}") private String helloServiceName; public Object multiply(int a,int b){ String url="http://"+ helloServiceName +"/multiply/" + a +"/" + b; return restTemplate.getForObject(url,String.class); } }
如上代碼中最核心的是:HelloService類,通過這個類遠程調用【消費】注冊在eureka server上對應的服務API,而這個類中最核心的對象是:RestTemplate,而這個又是通過在spring boot啟動類(EurekaclientconsumerApplication)中通過代碼注入到Spring IOC容器中的(當然也可以自定義一個config類然后統一寫BEAN注入的方法),重點請看這個restTemplate Bean注冊方法上面的注解:@LoadBalanced,這個就是實現負載均衡(默認是采用輪詢的負載均衡算法,還有其它的負載均衡Rule),就這么簡單嗎?是的,用起來簡單,但內部實現還是非常復雜的,Ribbon的運行原理詳見:深入理解Ribbon之源碼解析,核心思路是:RestTemplate內部維護了一個被@LoadBalance注解的RestTemplate列表,而這些RestTemplate列表又被添加了LoadBalancerInterceptor攔截器,而LoadBalancerInterceptor內部又使用了LoadBalancerClient,而LoadBalancerClient(實現類:RibbonLoadBalancerClient)具體選擇服務實例的邏輯又由ILoadBalancer來處理,ILoadBalancer通過配置IRule、IPing等信息,向EurekaClient獲取注冊列表的信息,並定時向EurekaClient發送“ping”心跳,進而檢查是否更新了服務列表,最后得到注冊服務實例列表后,ILoadBalancer根據IRule的策略進行負載均衡。
3.1.3.在application.yml文件中添加如下配置(若沒有請參見1.3法創建):
server:
port: 8666
spring:
application:
name: ribbonclient
helloServiceProvider: helloservice #自定義配置,指定訪問遠程服務名稱,當然也可以寫死在代碼中
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8800/eureka/ #指向eureka server
完成上述步驟即實現了一個基於Ribbon的負載均衡服務消費者(客戶端)項目。
3.2方式二:使用feign實現服務消費(負載均衡調用遠程服務調用)
我們仍然基於3.1節原有項目基礎上實現基於feign的負載均衡服務調用,注意feign的底層仍然使用了Ribbon。當然也可以單獨創一個新的spring boot項目(參照第一節介紹)然后再按下文步驟操作即可。
3.2.1.在POM XML中添加spring-cloud-starter-openfeign依賴,配置如下:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
3.2.2.在spring boot啟動類(EurekaclientconsumerApplication)上添加:@EnableFeignClients 注解,然后在cn.zuowenjun.cloud.service包中添加自定義HelloRemoteService,這個就是遠程服務調用接口類(或稱:客戶端代理類【接口】),這個就是與3.1中定義的HelloService作用完全類似,只是實現方式不同而矣,最后在controller中添加一個新的API ACTION方法,以便可以調用HelloRemoteService中的服務方法,完整實現代碼如下:

//spring boot啟動類 package cn.zuowenjun.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @EnableDiscoveryClient @SpringBootApplication @EnableFeignClients(basePackages = "cn.zuowenjun.cloud.service") // 如果啟動類不在根目錄需要指定basePackages,否則不需要 class EurekaclientconsumerApplication { public static void main(String[] args) { SpringApplication.run(EurekaclientconsumerApplication.class, args); } @LoadBalanced @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } } //HelloRemoteService: package cn.zuowenjun.cloud.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; /* * bug-refer https://blog.csdn.net/zlh313_01/article/details/80309144 * bug-refer https://blog.csdn.net/alinyua/article/details/80070890 */ @FeignClient(name= "helloservice") public interface HelloRemoteService { @RequestMapping("/multiply/{a}/{b}") Object multiply(@PathVariable("a") int a, @PathVariable("b") int b); } //controller: package cn.zuowenjun.cloud.controller; import cn.zuowenjun.cloud.service.HelloRemoteService; import cn.zuowenjun.cloud.service.HelloService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @Autowired private HelloService helloService; @Autowired private HelloRemoteService helloRemoteService; @RequestMapping("/x") public Object multiplyForRestTemplate(@RequestParam int a, @RequestParam int b) { return helloService.multiply(a,b); } @RequestMapping("/multiply/{a}/{b}") public Object multiplyForFeignClient(@PathVariable int a, @PathVariable int b) { return helloRemoteService.multiply(a,b); } }
如上代碼HelloRemoteService是重點,需要注意:
a.必需是interface,因為@FeignClient注解只能用於interface中,而且很顯然HelloRemoteService 是遠程調用,本地不應有實現的,如果知道原理就更明白這個接口只是為了生成可供restTemplate調用的URL方法而矣;
b.@FeignClient注解的name(別名屬性)或value必填,這個就是需要遠程調用服務的應用名稱【即:表明消費哪個服務】
c.接口中定義的方法應與遠程服務的controller中的方法保持一致(方法簽名,注解),同時注意方法上的一些映射請求的注解,如:@RequestMapping,這些與我們在spring MVC用法相同,但含義卻不相同,spring MVC是指處理請求路徑,而這里是調用請求路徑,這個路徑必需與服務提供者API 的對應的ACITON方法上的保持相同,否則將無法成功發送請求。常見問題及解決辦法可參見:https://blog.csdn.net/zlh313_01/article/details/80309144
3.2.3.application.yml配置與3.1.3配置相同,即保持不變即可,最后啟動項目即可(現在這個項目同時包含了Ribbon與Feign的負載均衡遠程調用服務的方式),通過多次訪問:http://localhost:8666/x?a=數字&b=數字 (基於Ribbon實現)、http://localhost:8666/multiply/數字/數字(基於Feign實現)可以看到遠程調用服務成功(即:消費服務成功)。
FeignClient的運行原理詳見:深入理解Feign之源碼解析,核心思路是:spring boot項目啟動時檢查@EnableFeignClients,若有則掃描被@FeignClient注解接口並注入到spring IOC容器中,然后在請求被@ FeignCleint標注的接口方法時,會通過JDK動態代理來生成具體的RequesTemplate,RequesTemplate又會生成Request,Request交給Client去處理,最后Client被封裝到LoadBalanceClient類,這個類Ribbon中的LoadBalancerClient相同,后面的負載均衡的處理請求相同。
項目結構及遠程調用效果如下圖所示:
、
、
四、下面分享相關可參考的博文資料鏈接:
Spring Cloud之Eureka服務注冊與發現(概念原理篇)
Spring Cloud Netflix - Eureka Server源碼閱讀
提示:本文相關示例項目代碼已上傳GITHUB,地址如下:
https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaserver
https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaclient
https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaclientconsumer
說明:文中若有不足之處歡迎指出,碼字不易,請多支持,謝謝!