本文會以一個簡單而完整的業務來闡述Spring Cloud Finchley.RELEASE版本經常使用組件的使用。以下圖所示,本文會覆蓋的組件有:html
本文的例子使用的依賴版本是:java
各項組件詳細使用請參見官網,Spring組件版本變化差別較大,網上代碼複製粘貼不必定可以適用,最最好的資料來源只有官網+閱讀源代碼,直接給出地址方便你閱讀本文的時候閱讀官網的文檔:mysql
以下貼出全部基礎組件(除數據庫)和業務組件的架構圖,箭頭表明調用關係(實現是業務服務調用、虛線是基礎服務調用),藍色框表明基礎組件(服務器)
這套架構中有關微服務以及消息隊列的設計理念,請參考我以前的《朱曄的互聯網架構實戰心得》系列文章。下面,咱們開始這次Spring Cloud之旅,Spring Cloud內容太多,本文分上下兩節,而且不會介紹太多理論性的東西,這些知識點能夠介紹一本書,本文更多的意義是給出一個可行可用的實際的示例代碼供你參考。git
本文咱們會作一個相對實際的例子,來演示互聯網金融業務募集項目和放款的過程。三個表的表結構以下:github
CREATE TABLE `invest` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `project_id` bigint(20) unsigned NOT NULL, `project_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `investor_id` bigint(20) unsigned NOT NULL, `investor_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `borrower_id` bigint(20) unsigned NOT NULL, `borrower_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `amount` decimal(10,2) unsigned NOT NULL, `status` tinyint(4) NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) CREATE TABLE `project` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `reason` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `borrower_id` bigint(20) unsigned NOT NULL, `total_amount` decimal(10,0) unsigned NOT NULL, `remain_amount` decimal(10,0) unsigned NOT NULL, `status` tinyint(3) unsigned NOT NULL COMMENT '1-募集中 2-募集完成 3-已放款', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) USING BTREE ) CREATE TABLE `user` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `available_balance` decimal(10,2) unsigned NOT NULL, `frozen_balance` decimal(10,2) unsigned NOT NULL, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) USING BTREE )
咱們會搭建四個業務服務,其中三個是被其它服務同步調用的服務,一個是監聽MQ異步處理消息的服務:web
整個業務包含了同步服務調用和異步消息處理,業務簡單而有表明性。可是在這裏咱們並無演示Spring Cloud Config的使用,以前也提到過,國內開源的幾個配置中心比Cloud Config功能強大太多太多,目前Cloud Config實用性很差,在這裏就不歸入演示了。
下面咱們來逐一實現每個組件和服務。redis
咱們先來新建一個父模塊的pom:spring
<?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>me.josephzhu</groupId> <artifactId>springcloud101</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>springcloud101-investservice-api</module> <module>springcloud101-investservice-server</module> <module>springcloud101-userservice-api</module> <module>springcloud101-userservice-server</module> <module>springcloud101-projectservice-api</module> <module>springcloud101-projectservice-server</module> <module>springcloud101-eureka-server</module> <module>springcloud101-zuul-server</module> <module>springcloud101-turbine-server</module> <module>springcloud101-projectservice-listener</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-releasetrain</artifactId> <version>Lovelace-RELEASE</version> <scope>import</scope> <type>pom</type> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream-dependencies</artifactId> <version>Fishtown.M3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/libs-milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> </project>
第一個要搭建的服務就是用於服務註冊的Eureka服務器:sql
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>spring101-eureka-server</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> </project>
在resources文件夾下建立一個配置文件application.yml(對於Spring Cloud項目因爲配置實在是太多,爲了模塊感層次感強一點,這裏咱們使用yml格式):數據庫
server: port: 8865 eureka: instance: hostname: localhost client: registry-fetch-interval-seconds: 5 registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ server: enable-self-preservation: true eviction-interval-timer-in-ms: 5000 spring: application: name: eurka-server
在這裏,爲了簡單期間,咱們搭建的是一個Standalone的註冊服務(這裏,咱們注意到Eureka有一個自我保護的開關,默認開啓,自我保護的意思是短期大批節點和Eureka斷開的話,這個通常是網絡問題,自我保護會開啓防止節點註銷,在以後的測試過程當中由於咱們會常常重啓調試服務,因此若是遇到節點不註銷的問題能夠暫時關閉這個功能),分配了8865端口(咱們約定,基礎組件分配的端口以88開頭),隨後創建一個主程序文件:
package me.josephzhu.springcloud101.eurekaserver; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run( EurekaServerApplication.class, args ); } }
對於搭建Spring Cloud的一些基礎組件的服務,每每就是三步,加依賴,加配置,加註解開關便可。
Zuul是一個代理網關,具備路由和過濾兩大功能。而且直接能和Eureka註冊服務以及Sleuth鏈路監控整合,很是方便。在這裏,咱們會同時演示兩個功能,咱們會進行路由配置,使網關作一個反向代理,咱們也會自定義一個前置過濾器作安全攔截。
首先,新建一個模塊:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-zuul-server</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> </dependencies> </project>
隨後加一個配置文件:
server: port: 8866 spring: application: name: zuulserver main: allow-bean-definition-overriding: true zipkin: base-url: http://localhost:9411 sleuth: feign: enabled: true sampler: probability: 1.0 eureka: client: serviceUrl: defaultZone: http://localhost:8865/eureka/ registry-fetch-interval-seconds: 5 zuul: routes: invest: path: /invest/** serviceId: investservice user: path: /user/** serviceId: userservice project: path: /project/** serviceId: projectservice host: socket-timeout-millis: 60000 connect-timeout-millis: 60000 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: always
Zuul網關咱們這裏使用8866端口,這裏重點看一下路由的配置:
package me.josephzhu.springcloud101.zuul.server; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE; @Component public class TokenFilter extends ZuulFilter { @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return PRE_DECORATION_FILTER_ORDER - 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String token = request.getParameter("token"); if(token == null) { ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); try { ctx.getResponse().setCharacterEncoding("UTF-8"); ctx.getResponse().getWriter().write("禁止訪問"); } catch (Exception e){} return null; } return null; } }
這個前置過濾演示了一個受權校驗的例子,檢查請求是否提供了token參數,若是沒有的話拒絕轉發服務,返回401響應狀態碼和錯誤信息。
下面實現服務程序:
package me.josephzhu.springcloud101.zuul.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication @EnableZuulProxy @EnableDiscoveryClient public class ZuulServerApplication { public static void main(String[] args) { SpringApplication.run( ZuulServerApplication.class, args ); } }
這裏解釋一下兩個註解:
Turbine用於彙總Hystrix服務斷路器監控流。Spring Cloud還提供了Hystrix的Dashboard,在這裏咱們把這兩個功能集合在一個服務中運行。三部曲第一步依賴:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-turbine-server</artifactId> <dependencies> <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-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-turbine</artifactId> </dependency> </dependencies> </project>
第二步配置:
server: port: 8867 spring: application: name: turbineserver eureka: client: serviceUrl: defaultZone: http://localhost:8865/eureka/ management: endpoints: web: exposure: include: "*" endpoint: health: show-details: always turbine: aggregator: clusterConfig: default clusterNameExpression: "'default'" combine-host: true instanceUrlSuffix: default: actuator/hystrix.stream app-config: investservice,userservice,projectservice,projectservice-listener
Turbine服務咱們使用8867端口,這裏重點看一下turbine下面的配置項:
咱們來看一下文首的架構圖,這裏的Turbine實際上是從各個配置的服務讀取監控流來彙總監控數據的,並非像Zipkin這種由服務主動上報數據的方式。固然,咱們還能夠經過Turbine Stream的功能讓客戶端主動上報數據(經過消息隊列),這裏就不詳細展開闡述了。下面是第三步:
package me.josephzhu.springcloud101.turbine.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; import org.springframework.cloud.netflix.turbine.EnableTurbine; @SpringBootApplication @EnableDiscoveryClient @EnableHystrix @EnableHystrixDashboard @EnableCircuitBreaker @EnableTurbine public class TurbineServerApplication { public static void main(String[] args) { SpringApplication.run( TurbineServerApplication.class, args ); } }
以後會展現使用截圖。
Zipkin用於收集分佈式追蹤信息(同時扮演了服務端以及查看後臺的角色),搭建方式請參見官網https://github.com/openzipkin/zipkin ,最簡單的方式是去https://dl.bintray.com/openzipkin/maven/io/zipkin/java/zipkin-server/直接下載jar包運行便可,在生產環境強烈建議配置後端存儲爲ES或Mysql等等,這裏咱們用於演示不進行任何其它配置了。咱們直接啓動便可,默認運行在9411端口:
以後咱們展現全鏈路監控的截圖。
咱們先來新建一個被依賴最多的業務服務,每個服務分兩個項目,API定義和實現。Spring Cloud推薦API定義客戶端和服務端分別本身定義,不共享API接口,這樣耦合更低。我以爲互聯網項目注重快速開發,服務多而且每每用於內部調用,仍是共享接口方式更切實際,在這裏咱們演示的是接口共享方式的實踐。首先新建API項目的模塊:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-userservice-api</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> </project>
API項目不包含任何服務端實現,所以這裏只是引入了feign。在API接口項目中,咱們通常定義兩個東西,一是服務接口定義,二是傳輸數據DTO定義。用戶DTO以下:
package me.josephzhu.springcloud101.userservice.api; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.util.Date; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class User { private Long id; private String name; private BigDecimal availableBalance; private BigDecimal frozenBalance; private Date createdAt; }
對於DTO我建議從新定義一份,不要直接使用數據庫的Entity,前者用於服務之間對外的數據傳輸,後者用於服務內部和數據庫進行交互,不能耦合在一塊兒混爲一談,雖然這多了一些轉化工做。
用戶服務以下:
package me.josephzhu.springcloud101.userservice.api; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; public interface UserService { @GetMapping("getUser") User getUser(@RequestParam("id") long id) throws Exception; @PostMapping("consumeMoney") BigDecimal consumeMoney(@RequestParam("investorId") long investorId, @RequestParam("amount") BigDecimal amount) throws Exception; @PostMapping("lendpayMoney") BigDecimal lendpayMoney(@RequestParam("investorId") long investorId, @RequestParam("borrowerId") long borrowerId, @RequestParam("amount") BigDecimal amount) throws Exception; }
這裏定義了三個服務接口,在介紹服務實現的時候再來介紹這三個接口。
API模塊是會被服務實現的服務端和其它服務使用的客戶端引用的,自己不具有獨立使用功能,因此也就沒有啓動類。
下面咱們實現用戶服務服務端,首先是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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-userservice-server</artifactId> <dependencies> <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> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.github.gavlyukovskiy</groupId> <artifactId>p6spy-spring-boot-starter</artifactId> <version>1.4.3</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.8.2</version> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-userservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
因爲咱們的服務具備發現、監控、數據訪問、分佈式鎖全功能,因此引入的依賴比較多一點:
下面咱們創建一個配置文件,此次咱們創建的是properties格式(只是爲了說明更方便一點,網上有工具能夠進行properties和yml的轉換):
下面實現服務,首先定義數據庫實體:
package me.josephzhu.springcloud101.userservice.server; import lombok.Data; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.math.BigDecimal; import java.util.Date; @Data @Entity @Table(name = "user") @EntityListeners(AuditingEntityListener.class) public class UserEntity { @Id @GeneratedValue private Long id; private String name; private BigDecimal availableBalance; private BigDecimal frozenBalance; @CreatedDate private Date createdAt; @LastModifiedDate private Date updatedAt; }
沒有什麼特殊的,只是咱們使用了@CreatedDate和@LastModifiedDate註解來生成記錄的建立和修改時間。下面是數據訪問資源庫,一鍵實現增刪改查:
package me.josephzhu.springcloud101.userservice.server; import org.springframework.data.repository.CrudRepository; public interface UserRepository extends CrudRepository<UserEntity, Long> { }
服務實現以下:
package me.josephzhu.springcloud101.userservice.server; import me.josephzhu.springcloud101.userservice.api.User; import me.josephzhu.springcloud101.userservice.api.UserService; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; @RestController public class UserServiceController implements UserService { @Autowired UserRepository userRepository; @Autowired RedissonClient redissonClient; @Override public User getUser(long id) { return userRepository.findById(id).map(userEntity -> User.builder() .id(userEntity.getId()) .availableBalance(userEntity.getAvailableBalance()) .frozenBalance(userEntity.getFrozenBalance()) .name(userEntity.getName()) .createdAt(userEntity.getCreatedAt()) .build()) .orElse(null); } @Override public BigDecimal consumeMoney(long investorId, BigDecimal amount) { RLock lock = redissonClient.getLock("User" + investorId); lock.lock(); try { UserEntity user = userRepository.findById(investorId).orElse(null); if (user != null && user.getAvailableBalance().compareTo(amount)>=0) { user.setAvailableBalance(user.getAvailableBalance().subtract(amount)); user.setFrozenBalance(user.getFrozenBalance().add(amount)); userRepository.save(user); return amount; } return null; } finally { lock.unlock(); } } @Override @Transactional(rollbackFor = Exception.class) public BigDecimal lendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception { RLock lock = redissonClient.getLock("User" + investorId); lock.lock(); try { UserEntity investor = userRepository.findById(investorId).orElse(null); UserEntity borrower = userRepository.findById(borrowerId).orElse(null); if (investor != null && borrower != null && investor.getFrozenBalance().compareTo(amount) >= 0) { investor.setFrozenBalance(investor.getFrozenBalance().subtract(amount)); userRepository.save(investor); borrower.setAvailableBalance(borrower.getAvailableBalance().add(amount)); userRepository.save(borrower); return amount; } return null; } finally { lock.unlock(); } } }
這裏實現了三個服務接口:
這裏咱們看到因爲咱們的實現類直接實現了接口(共享Feign接口方式),在實現業務邏輯的時候不須要去考慮參數如何獲取,接口暴露地址等事情。
最後實現主程序:
package me.josephzhu.springcloud101.userservice.server; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableDiscoveryClient @EnableJpaAuditing @EnableHystrix @EnableCircuitBreaker @Configuration public class UserServiceApplication { @Bean RedissonClient redissonClient() { return Redisson.create(); } public static void main(String[] args) { SpringApplication.run( UserServiceApplication.class, args ); } }
全部服務咱們都一視同仁,開啓服務發現、斷路器、斷路器監控等功能。這裏額外定義了一下Redisson的配置。
項目服務和用戶服務比較相似,惟一區別是項目服務會用到外部其它服務(用戶服務)。首先定義項目服務接口模塊:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-projectservice-api</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> </project>
接口中的DTO:
package me.josephzhu.springcloud101.projectservice.api; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.util.Date; @Data @Builder @NoArgsConstructor @AllArgsConstructor public class Project { private Long id; private BigDecimal totalAmount; private BigDecimal remainAmount; private String name; private String reason; private long borrowerId; private String borrowerName; private int status; private Date createdAt; }
以及服務定義:
package me.josephzhu.springcloud101.projectservice.api; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; public interface ProjectService { @GetMapping("getProject") Project getProject(@RequestParam("id") long id) throws Exception; @PostMapping("gotInvested") BigDecimal gotInvested(@RequestParam("id") long id, @RequestParam("amount") BigDecimal amount) throws Exception; @PostMapping("lendpay") BigDecimal lendpay(@RequestParam("id") long id) throws Exception; }
不作過多說明了,直接來實現服務實現模塊:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-projectservice-server</artifactId> <dependencies> <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> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>com.github.gavlyukovskiy</groupId> <artifactId>p6spy-spring-boot-starter</artifactId> <version>1.4.3</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-projectservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-userservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
依賴和用戶服務基本一致,只有幾個區別:
下面是配置:
server: port: 8762 spring: application: name: projectservice cloud: stream: bindings: output: destination: zhuye datasource: url: jdbc:mysql://localhost:3306/p2p?useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver zipkin: base-url: http://localhost:9411 sleuth: feign: enabled: true sampler: probability: 1.0 jpa: show-sql: true hibernate: use-new-id-generator-mappings: false feign: hystrix: enabled: true eureka: client: serviceUrl: defaultZone: http://localhost:8865/eureka/ registry-fetch-interval-seconds: 5 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: always
項目服務的配置直接把用戶服務的配置拿來改一下便可,有幾個須要改的地方:
首先實現項目實體類:
package me.josephzhu.springcloud101.projectservice.server; import lombok.Data; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.math.BigDecimal; import java.util.Date; @Data @Entity @Table(name = "project") @EntityListeners(AuditingEntityListener.class) public class ProjectEntity { @Id @GeneratedValue private Long id; private BigDecimal totalAmount; private BigDecimal remainAmount; private String name; private String reason; private long borrowerId; private int status; @CreatedDate private Date createdAt; @LastModifiedDate private Date updatedAt; }
而後是數據訪問增刪改查Repository:
package me.josephzhu.springcloud101.projectservice.server; import org.springframework.data.repository.CrudRepository; public interface ProjectRepository extends CrudRepository<ProjectEntity, Long> { }
而後是依賴的外部用戶服務:
package me.josephzhu.springcloud101.projectservice.server; import me.josephzhu.springcloud101.userservice.api.User; import me.josephzhu.springcloud101.userservice.api.UserService; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import java.math.BigDecimal; @FeignClient(value = "userservice",fallback = RemoteUserService.Fallback.class) public interface RemoteUserService extends UserService { @Component class Fallback implements RemoteUserService { @Override public User getUser(long id) throws Exception { return null; } @Override public BigDecimal consumeMoney(long id, BigDecimal amount) throws Exception { return null; } @Override public BigDecimal lendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception { return null; } } }
這裏咱們須要聲明@Feign註解根據服務名稱來使用外部的用戶服務,此外,咱們還定義了服務熔斷時的Fallback類,實現上咱們給出了返回null的空實現。
最關鍵的服務實現以下:
package me.josephzhu.springcloud101.projectservice.server; import lombok.extern.slf4j.Slf4j; import me.josephzhu.springcloud101.projectservice.api.Project; import me.josephzhu.springcloud101.projectservice.api.ProjectService; import me.josephzhu.springcloud101.userservice.api.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.messaging.Source; import org.springframework.integration.support.MessageBuilder; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; @RestController @Slf4j @EnableBinding(Source.class) public class ProjectServiceController implements ProjectService { @Autowired ProjectRepository projectRepository; @Autowired RemoteUserService remoteUserService; @Override public Project getProject(long id) throws Exception { ProjectEntity projectEntity = projectRepository.findById(id).orElse(null); if (projectEntity == null) return null; User borrower = remoteUserService.getUser(projectEntity.getBorrowerId()); if (borrower == null) return null; return Project.builder() .id(projectEntity.getId()) .borrowerId(borrower.getId()) .borrowerName(borrower.getName()) .name(projectEntity.getName()) .reason(projectEntity.getReason()) .status(projectEntity.getStatus()) .totalAmount(projectEntity.getTotalAmount()) .remainAmount(projectEntity.getRemainAmount()) .createdAt(projectEntity.getCreatedAt()) .build(); } @Override public BigDecimal gotInvested(long id, BigDecimal amount) throws Exception { ProjectEntity projectEntity = projectRepository.findById(id).orElse(null); if (projectEntity != null && projectEntity.getRemainAmount().compareTo(amount)>=0) { projectEntity.setRemainAmount(projectEntity.getRemainAmount().subtract(amount)); projectRepository.save(projectEntity); if (projectEntity.getRemainAmount().compareTo(new BigDecimal("0"))==0) { User borrower = remoteUserService.getUser(projectEntity.getBorrowerId()); if (borrower != null) { projectEntity.setStatus(2); projectRepository.save(projectEntity); projectStatusChanged(Project.builder() .id(projectEntity.getId()) .borrowerId(borrower.getId()) .borrowerName(borrower.getName()) .name(projectEntity.getName()) .reason(projectEntity.getReason()) .status(projectEntity.getStatus()) .totalAmount(projectEntity.getTotalAmount()) .remainAmount(projectEntity.getRemainAmount()) .createdAt(projectEntity.getCreatedAt()) .build()); } return amount; } return amount; } return null; } @Override public BigDecimal lendpay(long id) throws Exception { Thread.sleep(5000); ProjectEntity project = projectRepository.findById(id).orElse(null); if (project != null) { project.setStatus(3); projectRepository.save(project); return project.getTotalAmount(); } return null; } @Autowired Source source; private void projectStatusChanged(Project project){ if (project.getStatus() == 2) try { source.output().send(MessageBuilder.withPayload(project).build()); } catch (Exception ex) { log.error("發送MQ失敗", ex); } } }
三個方法的業務邏輯以下:
最後定義啓動類:
package me.josephzhu.springcloud101.projectservice.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients @EnableJpaAuditing @EnableHystrix @EnableCircuitBreaker public class ProjectServiceApplication { public static void main(String[] args) { SpringApplication.run( ProjectServiceApplication.class, args ); } }
投資服務和前兩個服務也是相似的,只不過它更復雜點,會依賴用戶服務和項目服務。首先創建一個服務定義模塊:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-investservice-api</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> </project>
而後DTO:
package me.josephzhu.springcloud101.investservice.api; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.util.Date; @Data @Builder @AllArgsConstructor @NoArgsConstructor public class Invest { private Long id; private long investorId; private long borrowerId; private long projectId; private int status; private BigDecimal amount; private Date createdAt; private Date updatedAt; }
以及接口定義:
package me.josephzhu.springcloud101.investservice.api; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import java.math.BigDecimal; import java.util.List; public interface InvestService { @PostMapping("createInvest") Invest createOrder(@RequestParam("userId") long userId, @RequestParam("projectId") long projectId, @RequestParam("amount") BigDecimal amount) throws Exception; @GetMapping("getOrders") List<Invest> getOrders(@RequestParam("projectId") long projectId) throws Exception; }
實現了定義模塊後來實現服務模塊:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-investservice-server</artifactId> <dependencies> <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> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>com.github.gavlyukovskiy</groupId> <artifactId>p6spy-spring-boot-starter</artifactId> <version>1.4.3</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-investservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-userservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-projectservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
依賴使用和用戶服務基本相似,只是多了幾個外部服務接口的引入。
而後是配置:
server: port: 8763 spring: application: name: investservice datasource: url: jdbc:mysql://localhost:3306/p2p?useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver zipkin: base-url: http://localhost:9411 sleuth: feign: enabled: true sampler: probability: 1.0 jpa: show-sql: true hibernate: use-new-id-generator-mappings: false feign: hystrix: enabled: true eureka: client: serviceUrl: defaultZone: http://localhost:8865/eureka/ registry-fetch-interval-seconds: 5 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: always
和用戶服務也是相似,只是修改了端口和程序名。
如今來建立數據實體:
package me.josephzhu.springcloud101.investservice.server; import lombok.Data; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; import java.math.BigDecimal; import java.util.Date; @Data @Entity @Table(name = "invest") @EntityListeners(AuditingEntityListener.class) public class InvestEntity { @Id @GeneratedValue private Long id; private long investorId; private long borrowerId; private long projectId; private String investorName; private String borrowerName; private String projectName; private BigDecimal amount; private int status; @CreatedDate private Date createdAt; @LastModifiedDate private Date updatedAt; }
數據訪問Repository:
package me.josephzhu.springcloud101.investservice.server; import org.springframework.data.repository.CrudRepository; import java.util.List; public interface InvestRepository extends CrudRepository<InvestEntity, Long> { List<InvestEntity> findByProjectIdAndStatus(long projectId, int status); }
具有熔斷Fallback的用戶外部服務客戶端:
package me.josephzhu.springcloud101.investservice.server; import lombok.extern.slf4j.Slf4j; import me.josephzhu.springcloud101.userservice.api.User; import me.josephzhu.springcloud101.userservice.api.UserService; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import java.math.BigDecimal; @FeignClient(value = "userservice", fallback = RemoteUserService.Fallback.class) public interface RemoteUserService extends UserService { @Component @Slf4j class Fallback implements RemoteUserService { @Override public User getUser(long id) throws Exception { log.warn("getUser fallback"); return null; } @Override public BigDecimal consumeMoney(long id, BigDecimal amount) throws Exception { log.warn("consumeMoney fallback"); return null; } @Override public BigDecimal lendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception { log.warn("lendpayMoney fallback"); return null; } } }
項目服務訪問客戶端:
package me.josephzhu.springcloud101.investservice.server; import me.josephzhu.springcloud101.projectservice.api.Project; import me.josephzhu.springcloud101.projectservice.api.ProjectService; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import java.math.BigDecimal; @FeignClient(value = "projectservice", fallback = RemoteProjectService.Fallback.class) public interface RemoteProjectService extends ProjectService { @Component class Fallback implements RemoteProjectService { @Override public Project getProject(long id) throws Exception { return null; } @Override public BigDecimal gotInvested(long id, BigDecimal amount) throws Exception { return null; } @Override public BigDecimal lendpay(long id) throws Exception { return null; } } }
服務接口實現:
package me.josephzhu.springcloud101.investservice.server; import lombok.extern.slf4j.Slf4j; import me.josephzhu.springcloud101.investservice.api.Invest; import me.josephzhu.springcloud101.investservice.api.InvestService; import me.josephzhu.springcloud101.projectservice.api.Project; import me.josephzhu.springcloud101.userservice.api.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.RestController; import java.math.BigDecimal; import java.util.List; import java.util.stream.Collectors; @RestController @Slf4j public class InvestServiceController implements InvestService { @Autowired InvestRepository investRepository; @Autowired RemoteUserService remoteUserService; @Autowired RemoteProjectService remoteProjectService; @Override @Transactional(rollbackFor = Exception.class) public Invest createOrder(long userId, long projectId, BigDecimal amount) throws Exception { User investor = remoteUserService.getUser(userId); if (investor == null) throw new Exception("無效用戶ID"); if (amount.compareTo(investor.getAvailableBalance()) > 0) throw new Exception("用戶餘額不足"); Project project = remoteProjectService.getProject(projectId); if (project == null) throw new Exception("無效項目ID"); if (amount.compareTo(project.getRemainAmount()) > 0) throw new Exception("項目餘額不足"); if (project.getStatus() !=1) throw new Exception("項目不是募集中狀不能投資"); InvestEntity investEntity = new InvestEntity(); investEntity.setInvestorId(investor.getId()); investEntity.setInvestorName(investor.getName()); investEntity.setAmount(amount); investEntity.setBorrowerId(project.getBorrowerId()); investEntity.setBorrowerName(project.getBorrowerName()); investEntity.setProjectId(project.getId()); investEntity.setProjectName(project.getName()); investEntity.setStatus(1); investRepository.save(investEntity); if (remoteUserService.consumeMoney(userId, amount) == null) throw new Exception("用戶消費失敗"); if (remoteProjectService.gotInvested(projectId, amount) == null) throw new Exception("項目投資失敗"); return Invest.builder() .id(investEntity.getId()) .amount(investEntity.getAmount()) .borrowerId(investEntity.getBorrowerId()) .investorId(investEntity.getInvestorId()) .projectId(investEntity.getProjectId()) .status(investEntity.getStatus()) .createdAt(investEntity.getCreatedAt()) .updatedAt(investEntity.getUpdatedAt()) .build(); } @Override public List<Invest> getOrders(long projectId) throws Exception { return investRepository.findByProjectIdAndStatus(projectId,1).stream() .map(investEntity -> Invest.builder() .id(investEntity.getId()) .amount(investEntity.getAmount()) .borrowerId(investEntity.getBorrowerId()) .investorId(investEntity.getInvestorId()) .projectId(investEntity.getProjectId()) .status(investEntity.getStatus()) .createdAt(investEntity.getCreatedAt()) .updatedAt(investEntity.getUpdatedAt()) .build()) .collect(Collectors.toList()); } }
投資服務定義了兩個接口:
啓動類以下:
package me.josephzhu.springcloud101.investservice.server; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.hystrix.EnableHystrix; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.context.ApplicationContext; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import java.util.Arrays; import java.util.stream.Stream; @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients @EnableJpaAuditing @EnableHystrix @EnableCircuitBreaker public class InvestServiceApplication implements CommandLineRunner{ public static void main(String[] args) { SpringApplication.run( InvestServiceApplication.class, args ); } @Autowired ApplicationContext applicationContext; @Override public void run(String... args) throws Exception { System.out.println("全部註解:"); Stream.of(applicationContext.getBeanDefinitionNames()) .map(applicationContext::getBean) .map(bean-> Arrays.asList(bean.getClass().getAnnotations())) .flatMap(a->a.stream()) .filter(annotation -> annotation.annotationType().getName().startsWith("org.springframework.cloud")) .forEach(System.out::println); } }
和其它幾個服務同樣沒啥特殊的,只是這裏多了個Runner,這個是我本身玩的,想輸出一下Spring中的Bean上定義的和Spring Cloud相關的註解,和業務沒有關係。
最後一個服務是監聽MQ進行處理的項目(消息)監聽服務。這個服務實際上是能夠和其它服務進行合併的,可是爲了清晰咱們仍是分開作了一個模塊:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springcloud101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springcloud101-projectservice-listener</artifactId> <dependencies> <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> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-stream</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.7</version> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-userservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-projectservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>me.josephzhu</groupId> <artifactId>springcloud101-investservice-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
引入了Stream相關依賴,去掉了數據訪問相關依賴,由於這裏咱們只會調用外部服務,服務自己不會進行數據訪問。
配置信息以下:
server: port: 8764 spring: application: name: projectservice-listener cloud: stream: bindings: input: destination: zhuye zipkin: base-url: http://localhost:9411 sleuth: feign: enabled: true sampler: probability: 1.0 feign: hystrix: enabled: true eureka: client: serviceUrl: defaultZone: http://localhost:8865/eureka/ registry-fetch-interval-seconds: 5 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: always
惟一值得注意的是,這裏咱們定義了Spring Cloud Input綁定到也是以前定義的Output的那個交換機zhuye上面,實現了MQ發送接受數據連通。
下面咱們定義了三個外部服務客戶端(代碼和其它地方使用的如出一轍。
投資服務:
package me.josephzhu.springcloud101.projectservice.listener; import me.josephzhu.springcloud101.investservice.api.InvestService; import org.springframework.cloud.openfeign.FeignClient; @FeignClient(value = "investservice") public interface RemoteInvestService extends InvestService { }
用戶服務:
package me.josephzhu.springcloud101.projectservice.listener; import lombok.extern.slf4j.Slf4j; import me.josephzhu.springcloud101.userservice.api.User; import me.josephzhu.springcloud101.userservice.api.UserService; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import java.math.BigDecimal; @FeignClient(value = "userservice", fallback = RemoteUserService.Fallback.class) public interface RemoteUserService extends UserService { @Component @Slf4j class Fallback implements RemoteUserService { @Override public User getUser(long id) throws Exception { log.warn("getUser fallback"); return null; } @Override public BigDecimal consumeMoney(long id, BigDecimal amount) throws Exception { log.warn("consumeMoney fallback"); return null; } @Override public BigDecimal lendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception { log.warn("lendpayMoney fallback"); return null; } } }
項目服務:
package me.josephzhu.springcloud101.projectservice.listener; import me.josephzhu.springcloud101.projectservice.api.Project; import me.josephzhu.springcloud101.projectservice.api.ProjectService; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; import java.math.BigDecimal; @FeignClient(value = "projectservice", fallback = RemoteProjectService.Fallback.class) public interface RemoteProjectService extends ProjectService { @Component class Fallback implements RemoteProjectService { @Override public Project getProject(long id) throws Exception { return null; } @Override public BigDecimal gotInvested(long id, BigDecimal amount) throws Exception { return null; } @Override public BigDecimal lendpay(long id) throws Exception { return null; } } }
監聽程序實現以下:
package me.josephzhu.springcloud101.projectservice.listener; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import me.josephzhu.springcloud101.projectservice.api.Project; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.stream.annotation.EnableBinding; import org.springframework.cloud.stream.annotation.StreamListener; import org.springframework.cloud.stream.messaging.Sink; import org.springframework.stereotype.Component; @Component @EnableBinding(Sink.class) @Slf4j public class ProjectServiceListener { @Autowired RemoteUserService remoteUserService; @Autowired RemoteProjectService remoteProjectService; @Autowired RemoteInvestService remoteInvestService; static ObjectMapper objectMapper = new ObjectMapper(); @StreamListener(Sink.INPUT) public void handleProject(Project project) { try { log.info("收到消息: " + project); if (project.getStatus() == 2) { remoteInvestService.getOrders(project.getId()) .forEach(invest -> { try { remoteUserService.lendpayMoney(invest.getInvestorId(), invest.getBorrowerId(), invest.getAmount()); } catch (Exception ex) { try { log.error("處理放款的時候遇到異常:" + objectMapper.writeValueAsString(invest), ex); } catch (JsonProcessingException e) { } } }); remoteProjectService.lendpay(project.getId()); } } catch (Exception ex) { log.error("處理消息出現異常",ex); } } }
咱們經過@StreamListener方便實現消息監聽,在收聽到Project消息(其實最標準的應該爲MQ消息定義一個XXNotification的DTO,好比ProjectStatusChangedNotification,這裏咱們偷懶直接使用了Project這個DTO)後:
這裏能夠看到,雖然lendpay接口耗時好久(裏面休眠5秒)可是因爲處理是異步的,不會影響投資訂單這個操做,這是經過MQ進行異步處理的應用點之一。
激動人心的時刻來了,咱們來經過演示看一下咱們這套Spring Cloud微服務體系的功能。
先啓動Eureka,而後依次啓動全部的基礎服務,最後依次啓動全部的業務服務。
所有啓動後,訪問一下http://localhost:8865/來查看Eureka註冊中心:
這裏能夠看到全部服務已經註冊在線:
訪問http://localhost:8761/getUser?id=1能夠測試用戶服務:
訪問http://localhost:8762/getProject?id=2能夠測試項目服務:
咱們來初始化一下數據庫,默認有一個項目信息:
還有兩個投資人和一個借款人:
如今來經過網關訪問http://localhost:8866/invest/createInvest投資服務(使用網關進行路由,咱們配置的是匹配invest/**這個path路由到投資服務,直接訪問服務的時候無需提供invest前綴)使用投資人1作一次投資:
在沒有提供token的時候會出現錯誤,加上token後訪問成功:
能夠看到投資後投資人凍結帳戶爲100,項目剩餘金額爲900,多了一條投資記錄:
咱們使用投資人1測試5次投資,使用投資人2測試5次投資,測試後能夠看到項目狀態變爲了3放款完成:
數據庫中有10條投資記錄:
兩個投資人的凍結餘額都爲0,可用餘額分別少了500,借款人可用餘額多了1000,說明放款成功了?:
同時能夠在ProjectListner的日誌中看到收到消息的日誌:
咱們能夠訪問http://localhost:15672打開RabbitMQ都是管理臺看一下咱們那條消息的狀況:
能夠看到在隊列中的確有一條消息先收到而後不久後(大概是6秒後)獲得了ack處理完畢。隊列綁定到了zhuye這個交換機上:
至此,咱們已經演示了Zuul、Eureka和Stream,如今咱們來看一下斷路器功能。
咱們首先訪問http://localhost:8867/hystrix:
而後輸入http://localhost:8867/turbine.stream(Turbine聚合監控數據流)進入監控面板:
多訪問幾回投資服務接口能夠看到每個服務方法的斷路器狀況以及三套服務斷路器線程池的狀況,咱們接下去關閉用戶服務,再多訪問幾回投資服務接口,能夠看到getUser斷路器打開(getUser方法有個紅點):
同時在投資服務日誌中能夠看到斷路器走了Fallback的用戶服務:
最後,咱們訪問Zipkin來看一下服務鏈路監控的威力,訪問http://localhost:9411/zipkin/而後點擊按照最近排序能夠看到有一條很長的鏈路:
點進去看看:
整個鏈路覆蓋:
這是一篇超長的文章,在本文中咱們以一個實際的業務例子介紹演示了以下內容:
總結一下我對Spring Cloud的見解:
但願本文對你有用,完整代碼見https://github.com/JosephZhu1983/SpringCloud101。