本文会以一个简单而完整的业务来阐述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。