目录
一、认识微服务
单体架构
分布式架构
微服务
SpringCloud
内容知识
技术栈对比
服务拆分
远程调用
二、Eureka注册中心
搭建注册中心
服务注册
服务拉取
三、Ribbon负载均衡
源码跟踪
流程总结
负载均衡策略
自定义策略
饥饿加载
四、Nacos注册中心
服务注册
分级存储模型
配置集群
NacosRule
权重配置
环境隔离
创建namespace
配置namespace
临时实例
五、Nacos配置中心
创建配置
拉取配置
配置热更新
@RefreshScope
@ConfigurationProperties
配置共享
配置优先级
六、Feign远程调用
Feign使用
自定义配置
性能优化
最佳实践
继承方式
抽取方式
七、Gateway网关
入门使用
流程图
断言工厂
过滤器工厂
全局过滤器
过滤器顺序
跨域问题
八、RabbitMQ
同步异步通讯
MQ消息队列
入门案例
publisher实现
consumer实现
SpringAMQP
BasicQueue(普通队列)
WorkQueue(工作队列)
发布/订阅
Fanout
Direct
Topic
消息转换器
单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
优点:架构简单,部署成本低
缺点:耦合度高(维护困难、升级困难)
分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。
优点:降低服务耦合,有利于服务升级和拓展
缺点:服务调用关系错综复杂
分布式架构虽然降低了服务耦合,但是服务拆分时也有很多问题需要思考:
人们需要制定一套行之有效的标准来约束分布式架构。
微服务的架构特征:
微服务的上述特性其实是在给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性。做到高内聚,低耦合。
因此,可以认为微服务是一种经过良好架构设计的分布式架构方案 。
其中在 Java 领域最引人注目的就是 SpringCloud 提供的方案了。
SpringCloud 是目前国内使用最广泛的微服务框架。官网地址:Spring Cloud。
SpringCloud 集成了各种微服务功能组件,并基于 SpringBoot 实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
其中常见的组件包括:
需要学习的微服务知识内容
技术栈
服务拆分注意事项
单一职责:不同微服务,不要重复开发相同业务
数据独立:不要访问其它微服务的数据库
面向服务:将自己的业务暴露为接口,供其它微服务调用
cloud-demo:父工程,管理依赖
要求:
微服务项目下,打开 idea 中的 Service,可以很方便的启动。
启动完成后,访问 http://localhost:8080/order/101
正如上面的服务拆分要求中所提到,
订单服务如果需要查询用户信息, 只能调用用户服务的 Restful 接口,不能查询用户数据库
因此我们需要知道 Java 如何去发送 http 请求,Spring 提供了一个 RestTemplate 工具,只需要把它创建出来即可。(即注入 Bean)
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* 创建RestTemplate并注入spring容器
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
发送请求,自动序列化为 Java 对象。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.查询用户
String url = "http://localhost:8081/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
// 4.返回
return order;
}
}
启动完成后,访问:
http://localhost:8080/order/101
在上面代码的 url 中,我们可以发现调用服务的地址采用硬编码,这在后续的开发中肯定是不理想的,这就需要服务注册中心(Eureka)来帮我们解决这个事情。
最广为人知的注册中心就是 Eureka,其结构如下:
order-service 如何得知 user-service 实例地址?
order-service 如何从多个 user-service 实例中选择具体的实例?
order-service从实例列表中利用负载均衡算法选中一个实例地址,向该实例地址发起远程调用
order-service 如何得知某个 user-service 实例是否依然健康,是不是已经宕机?
接下来我们动手实践的步骤包括:
搭建 eureka-server
引入 SpringCloud 为 eureka 提供的 starter 依赖,注意这里是用 server
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
编写启动类
注意要添加一个 @EnableEurekaServer
注解,开启 eureka 的注册中心功能
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class,args);
}
}
编写配置文件
编写一个 application.yml 文件,内容如下:
server:
port: 10086
spring:
application:
name: eurekaserver
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
其中 default-zone
是因为前面配置类开启了注册中心所需要配置的 eureka 的地址信息,因为 eureka 本身也是一个微服务,这里也要将自己注册进来,当后面 eureka 集群时,这里就可以填写多个,使用 “,” 隔开。
启动完成后,访问 http://localhost:10086/
将 user-service、order-service 都注册到 eureka
引入 SpringCloud 为 eureka 提供的 starter 依赖,注意这里是用 client
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
在启动类上添加注解:@EnableEurekaClient
在 application.yml 文件,添加下面的配置:
spring:
application:
#name:orderservice
name: userservice
eureka:
client:
service-url:
defaultZone: http:127.0.0.1:10086/eureka
3个项目启动后,访问 http://localhost:10086/
这里另外再补充个小技巧,我们可以通过 idea 的多实例启动,来查看 Eureka 的集群效果。
4个项目启动后,访问 http://localhost:10086/
在 order-service 中完成服务拉取,然后通过负载均衡挑选一个服务,实现远程调用
下面我们让 order-service 向 eureka-server 拉取 user-service 的信息,实现服务发现。
首先给 RestTemplate
这个 Bean 添加一个 @LoadBalanced
注解,用于开启负载均衡。(后面会讲)
/**
* 创建RestTemplate并注入spring容器
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
修改 OrderService 访问的url路径,用服务名代替ip、端口:
spring 会自动帮助我们从 eureka-server 中,根据 userservice 这个服务名称,获取实例列表后去完成负载均衡。
我们添加了 @LoadBalanced
注解,即可实现负载均衡功能,这是什么原理呢?
SpringCloud 底层提供了一个名为 Ribbon 的组件,来实现负载均衡功能。
为什么我们只输入了 service 名称就可以访问了呢?为什么不需要获取ip和端口,这显然有人帮我们根据 service 名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor
,这个类会在对 RestTemplate 的请求进行拦截,然后从 Eureka 根据服务 id 获取服务列表,随后利用负载均衡算法得到真实的服务地址信息,替换服务 id。
我们进行源码跟踪:
这里的 intercept()
方法,拦截了用户的 HttpRequest 请求,然后做了几件事:
request.getURI()
:获取请求uri,即 http://user-service/user/8originalUri.getHost()
:获取uri路径的主机名,其实就是服务id user-service
this.loadBalancer.execute()
:处理服务id,和用户请求这里的 this.loadBalancer
是 LoadBalancerClient
类型
继续跟入 execute()
方法:
getLoadBalancer(serviceId)
:根据服务id获取 ILoadBalancer
,而 ILoadBalancer
会拿着服务 id 去 eureka 中获取服务列表。getServer(loadBalancer)
:利用内置的负载均衡算法,从服务列表中选择一个。在图中可以看到获取了8082端口的服务可以看到获取服务时,通过一个 getServer()
方法来做负载均衡:
我们继续跟入:
继续跟踪源码 chooseServer()
方法,发现这么一段代码:
我们看看这个 rule
是谁:
这里的 rule 默认值是一个 RoundRobinRule
,看类的介绍:
负载均衡默认使用了轮训算法,当然我们也可以自定义。
SpringCloud Ribbon 底层采用了一个拦截器,拦截了 RestTemplate 发出的请求,对地址做了修改。
基本流程如下:
RestTemplate
请求 http://userservice/user/1RibbonLoadBalancerClient
会从请求url中获取服务名称,也就是 user-serviceDynamicServerListLoadBalancer
根据 user-service 到 eureka 拉取服务列表IRule
利用内置负载均衡规则,从列表中选择一个,例如 localhost:8081RibbonLoadBalancerClient
修改请求地址,用 localhost:8081 替代 userservice,得到 http://localhost:8081/user/1,发起真实请求负载均衡的规则都定义在 IRule 接口中,而 IRule 有很多不同的实现类:
不同规则的含义如下:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略:(1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule 规则的客户端也会将其忽略。并发连接数的上限,可以由客户端设置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule | 忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
默认的实现就是 ZoneAvoidanceRule
,是一种轮询方案。
通过定义 IRule 实现可以修改负载均衡规则,有两种方式:
1 代码方式在 order-service 中的 OrderApplication 类中,定义一个新的 IRule:
2 配置文件方式:在 order-service 的 application.yml 文件中,添加新的配置也可以修改规则:
userservice: # 给需要调用的微服务配置负载均衡规则,orderservice服务去调用userservice服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
注意:一般用默认的负载均衡规则,不做修改。
当我们启动 orderservice,第一次访问时,时间消耗会大很多,这是因为 Ribbon 懒加载的机制。
Ribbon 默认是采用懒加载,即第一次访问时才会去创建 LoadBalanceClient,拉取集群地址,所以请求时间会很长。
而饥饿加载则会在项目启动时创建 LoadBalanceClient,降低第一次访问的耗时,通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true
clients: userservice # 项目启动时直接去拉取userservice的集群,多个用","隔开
SpringCloudAlibaba 推出了一个名为 Nacos 的注册中心,在国外也有大量的使用。
解压启动 Nacos访问:http://localhost:8848/nacos/
这里上来就直接服务注册,很多东西可能有疑惑,其实 Nacos 本身就是一个 SprintBoot 项目,这点你从启动的控制台打印就可以看出来,所以就不再需要去额外搭建一个像 Eureka 的注册中心。
引入依赖
在 cloud-demo 父工程中引入 SpringCloudAlibaba 的依赖:
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.2.6.RELEASE
pom
import
然后在 user-service 和 order-service 中的pom文件中引入 nacos-discovery 依赖:
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
配置nacos地址
在 user-service 和 order-service 的 application.yml 中添加 nacos 地址:
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
项目重新启动后,可以看到三个服务都被注册进了 Nacos
浏览器访问:http://localhost:8080/order/101,正常访问,同时负载均衡也正常。
一个服务可以有多个实例,例如我们的 user-service,可以有:
假如这些实例分布于全国各地的不同机房,例如:
Nacos就将同一机房内的实例,划分为一个集群。
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:杭州机房内的 order-service 应该优先访问同机房的 user-service。
接下来我们给 user-service 配置集群
修改 user-service 的 application.yml 文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称 HZ杭州
重启两个 user-service 实例后,我们再去启动一个上海集群的实例。
-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH
查看 nacos 控制台:
Ribbon的默认实现 ZoneAvoidanceRule
并不能实现根据同集群优先来实现负载均衡,我们把规则改成 NacosRule 即可。我们是用 orderservice 调用 userservice,所以在 orderservice 配置规则。
@Bean
public IRule iRule(){
//默认为轮询规则,这里自定义为随机规则
return new NacosRule();
}
另外,你同样可以使用配置的形式来完成,具体参考上面的 Ribbon 栏目。
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule #负载均衡规则
然后,再对 orderservice 配置集群。
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
现在我启动了四个服务,分别是:
访问地址:http://localhost:8080/order/101
在访问中我们发现,只有同在一个 HZ 集群下的 userservice、userservice1 会被调用,并且是随机的。
我们试着把 userservice、userservice2 停掉。依旧可以访问。
在 userservice3 控制台可以看到发出了一串的警告,因为 orderservice 本身是在 HZ 集群的,这波 HZ 集群没有了 userservice,就会去别的集群找。
实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。但默认情况下 NacosRule 是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos 提供了权重配置来控制访问频率,0~1 之间,权重越大则访问频率越高,权重修改为 0,则该实例永远不会被访问。
在 Nacos 控制台,找到 user-service 的实例列表,点击编辑,即可修改权重。
在弹出的编辑窗口,修改权重
另外,在服务升级的时候,有一种较好的方案:我们也可以通过调整权重来进行平滑升级,例如:先把 userservice 权重调节为 0,让用户先流向 userservice2、userservice3,升级 userservice后,再把权重从 0 调到 0.1,让一部分用户先体验,用户体验稳定后就可以往上调权重啦。
Nacos 提供了 namespace 来实现环境隔离功能。
默认情况下,所有 service、data、group 都在同一个 namespace,名为 public(保留空间):
我们可以点击页面新增按钮,添加一个 namespace:
然后,填写表单:
就能在页面看到一个新的 namespace:
给微服务配置 namespace 只能通过修改配置来实现。
例如,修改 order-service 的 application.yml 文件:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间ID
重启 order-service 后,访问控制台。
public
dev
此时访问 order-service,因为 namespace 不同,会导致找不到 userservice,控制台会报错:
Nacos 的服务实例分为两种类型:
配置一个服务实例为永久实例:
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例
另外,Nacos 集群默认采用AP方式(可用性),当集群中存在非临时实例时,采用CP模式(一致性);而 Eureka 采用AP方式,不可切换。(这里说的是 CAP 原理,后面会写到)
Nacos除了可以做注册中心,同样可以做配置管理来使用。
当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
Nacos 一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
在 Nacos 控制面板中添加配置文件
然后在弹出的表单中,填写配置信息:
注意:项目的核心配置,需要热更新的配置才有放到 nacos 管理的必要。基本不会变更的一些配置(例如数据库连接)还是保存在微服务本地比较好。
首先我们需要了解 Nacos 读取配置文件的环节是在哪一步,在没加入 Nacos 配置之前,获取配置是这样:
加入 Nacos 配置,它的读取是在 application.yml 之前的:
这时候如果把 nacos 地址放在 application.yml 中,显然是不合适的,Nacos 就无法根据地址去获取配置了。
因此,nacos 地址必须放在优先级最高的 bootstrap.yml 文件。
引入 nacos-config 依赖
首先,在 user-service 服务中,引入 nacos-config 的客户端依赖:
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-config
添加 bootstrap.yml
然后,在 user-service 中添加一个 bootstrap.yml 文件,内容如下:
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
根据 spring.cloud.nacos.server-addr 获取 nacos地址,再根据${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
作为文件id,来读取配置。
在这个例子例中,就是去读取 userservice-dev.yaml
使用代码来验证是否拉取成功
在 user-service 中的 UserController 中添加业务逻辑,读取 pattern.dateformat 配置并使用:
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("now")
public String now(){
//格式化时间
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
启动服务后,访问:http://localhost:8081/user/now
我们最终的目的,是修改 nacos 中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。
有两种方式:1. 用 @value
读取配置时,搭配 @RefreshScope
;2. 直接用 @ConfigurationProperties
读取配置
方式一:在 @Value
注入的变量所在类上添加注解 @RefreshScope
方式二:使用 @ConfigurationProperties
注解读取配置文件,就不需要加 @RefreshScope
注解。
在 user-service 服务中,添加一个 PatternProperties 类,读取 patterrn.dateformat
属性
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
private String envSharedValue;
private String name;
}
@Autowired
private PatternProperties properties;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
}
其实在服务启动时,nacos 会读取多个配置文件,例如:
[spring.application.name]-[spring.profiles.active].yaml
,例如:userservice-dev.yaml[spring.application.name].yaml
,例如:userservice.yaml这里的 [spring.application.name].yaml
不包含环境,因此可以被多个环境共享。
添加一个环境共享配置
我们在 nacos 中添加一个 userservice.yaml 文件:
在 user-service 中读取共享配置
在 user-service 服务中,修改 PatternProperties 类,读取新添加的属性:
在 user-service 服务中,修改 UserController,添加一个方法:
运行两个 UserApplication,使用不同的profile
修改 UserApplication2 这个启动项,改变其profile值:
这样,UserApplication(8081) 使用的 profile 是 dev,UserApplication2(8082) 使用的 profile 是test
启动 UserApplication 和 UserApplication2
访问地址:http://localhost:8081/user/prop,结果:
访问地址:http://localhost:8082/user/prop,结果:
可以看出来,不管是 dev,还是 test 环境,都读取到了 envSharedValue 这个属性的值。
上面的都是同一个微服务下,那么不同微服务之间可以环境共享吗?
通过下面的两种方式来指定:
spring:
cloud:
nacos:
config:
file-extension: yaml # 文件后缀名
extends-configs: # 多微服务间共享的配置列表
- dataId: common.yaml # 要共享的配置文件id
spring:
cloud:
nacos:
config:
file-extension: yaml # 文件后缀名
shared-configs: # 多微服务间共享的配置列表
- dataId: common.yaml # 要共享的配置文件id
当 nacos、服务本地同时出现相同属性时,优先级有高低之分。
更细致的配置
我们以前利用 RestTemplate 发起远程调用的代码:
Feign 是一个声明式的 http 客户端,官方地址:GitHub - OpenFeign/feign: Feign makes writing java http clients easier
其作用就是帮助我们优雅的实现 http 请求的发送,解决上面提到的问题。
引入依赖
我们在 order-service 引入 feign 依赖:
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
org.springframework.cloud
spring-cloud-starter-openfeign
添加注解
在 order-service 启动类添加注解开启 Feign
请求接口
在 order-service 中新建一个接口,内容如下
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
@FeignClient("userservice")
:其中参数填写的是微服务名
@GetMapping("/user/{id}")
:其中参数填写的是请求路径
这个客户端主要是基于 SpringMVC 的注解 @GetMapping
来声明远程调用的信息
Feign 可以帮助我们发送 http 请求,无需自己使用 RestTemplate 来发送了。
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.查询用户
User user = userClient.findById(order.getUserId());
// 3.将用户信息封装进订单
order.setUser(user);
// 4.返回
return order;
}
Feign 可以支持很多的自定义配置,如下表所示:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign.Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign.Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的 @Bean 覆盖默认 Bean 即可。下面以日志为例来演示如何自定义配置。
基于配置文件修改 feign 的日志级别可以针对单个服务:
feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别
也可以针对所有服务:
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
而日志的级别分为四种:
也可以基于 Java 代码来修改日志级别,先声明一个类,然后声明一个 Logger.Level 的对象
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
如果要全局生效,将其放到启动类的 @EnableFeignClients
这个注解中:
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
如果是局部生效,则把它放到对应的 @FeignClient
这个注解中:
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
Feign 底层发起 http 请求,依赖于其它的框架。其底层客户端实现有:
因此提高 Feign 性能的主要手段就是使用连接池代替默认的 URLConnection
另外,日志级别应该尽量用 basic/none,可以有效提高性能。
这里我们用 Apache 的HttpClient来演示连接池。
在 order-service 的 pom 文件中引入 HttpClient 依赖
io.github.openfeign
feign-httpclient
配置连接池
在 order-service 的 application.yml 中添加配置
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
在 FeignClientFactoryBean 中的 loadBalance 方法中打断点
Debug 方式启动 order-service 服务,可以看到这里的 client,底层就是 HttpClient
一样的代码可以通过继承来共享:
1)定义一个 API 接口,利用定义方法,并基于 SpringMVC 注解做声明
2)Feign 客户端、Controller 都集成该接口
优点
缺点
将 FeignClient 抽取为独立模块,并且把接口有关的 POJO、默认的 Feign 配置都放到这个模块中,提供给所有消费者使用。
例如:将 UserClient、User、Feign 的默认配置都抽取到一个 feign-api 包中,所有微服务引用该依赖包,即可直接使用。
接下来我们就用该方法在代码中实现
首先创建一个 module,命名为 feign-api
在 feign-api 中然后引入依赖
org.springframework.cloud
spring-cloud-starter-openfeign
order-service中的 UserClient、User 都复制到 feign-api 项目中
在order-service中使用 feign-api
首先,删除 order-service 中的 UserClient、User
在 order-service 中引入 feign-api
com.xn2001.feign
feign-api
1.0
修改注解
当定义的 FeignClient 不在 SpringBootApplication 的扫描包范围下时,这些 FeignClient 就不能使用。
修改 order-service 启动类上的 @EnableFeignClients 注解
@EnableFeignClients(basePackages = "com.xn2001.feign.clients")
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
Gateway 网关是我们服务的守门神,所有微服务的统一入口。
网关的核心功能特性:
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过 gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。
在 SpringCloud 中网关的实现包括两种:
Zuul 是基于 Servlet 实现,属于阻塞式编程。而 Spring Cloud Gateway 则是基于 Spring5 中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
org.springframework.cloud
spring-cloud-starter-gateway
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
创建 application.yml 文件,内容如下:
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
我们将符合Path
规则的一切请求,都代理到 uri
参数指定的地址。
上面的例子中,我们将 /user/**
开头的请求,代理到 lb://userservice
,其中 lb 是负载均衡(LoadBalance),根据服务名拉取服务列表,实现负载均衡。
重启网关,访问 http://localhost:10010/user/1 时,符合 /user/**
规则,请求转发到 uri:http://userservice/user/1
多个 predicates 的话,要同时满足规则,下文有例子。
路由配置包括:
我们在配置文件中写的断言规则只是字符串,这些字符串会被 Predicate Factory 读取并处理,转变为路由判断的条件。
例如 Path=/user/**
是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
类来处理的,像这样的断言工厂在 Spring Cloud Gateway 还有十几个
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=**.somehost.org , **.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 |
官方文档: Spring Cloud Gateway
一般的,我们只需要掌握 Path,加上官方文档的例子,就可以应对各种工作场景了。
predicates:
- Path=/order/**
- After=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
像这样的规则,现在是 2021年8月22日01:32:42,很明显 After 条件不满足,可以不会转发,路由不起作用。
GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
Spring提供了31种不同的路由过滤器工厂。
官方文档: Spring Cloud Gateway
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
下面我们以 AddRequestHeader 为例:
需求:给所有进入 userservice 的请求添加一个请求头:sign=xn2001.com is eternal
只需要修改 gateway 服务的 application.yml文件,添加路由过滤即可。
spring:
cloud:
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
filters:
- AddRequestHeader=sign, xn2001.com is eternal # 添加请求头
如何验证,我们修改 userservice 中的一个接口
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id, @RequestHeader(value = "sign", required = false) String sign) {
log.warn(sign);
return userService.queryById(id);
}
重启两个服务,访问:http://localhost:10010/user/1
可以看到控制台打印出了这个请求头
当然,Gateway 也是有全局过滤器的,如果要对所有的路由都生效,则可以将过滤器工厂写到 default-filters 下:
spring:
cloud:
gateway:
default-filters:
- AddRequestHeader=sign, xn2001.com is eternal # 添加请求头
上面介绍的过滤器工厂,网关提供了 31 种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与 GatewayFilter 的作用一样。区别在于 GlobalFilter 的逻辑可以写代码来自定义规则;而 GatewayFilter 通过配置定义,处理逻辑是固定的。
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件
如果同时满足则放行,否则拦截。
@Component
public class AuthorizeFilter implements GlobalFilter, Ordered {
// 测试:http://localhost:10010/order/101?authorization=admin
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取第一个 authorization 参数
String authorization = exchange.getRequest().getQueryParams().getFirst("authorization");
if ("admin".equals(authorization)){
// 放行
return chain.filter(exchange);
}
// 设置拦截状态码信息
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 设置拦截
return exchange.getResponse().setComplete();
}
// 设置过滤器优先级,值越低优先级越高
// 也可以使用 @Order 注解
@Override
public int getOrder() {
return 0;
}
}
请求进入网关会碰到三类过滤器:DefaultFilter、当前路由的过滤器、GlobalFilter;
请求路由后,会将三者合并到一个过滤器链(集合)中,排序后依次执行每个过滤器.
排序的规则是什么呢?
不了解跨域问题的同学可以百度了解一下;在 Gateway 网关中解决跨域问题还是比较方便的。
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求 allowedOrigins: “*” 允许所有网站
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
微服务间通讯有同步和异步两种方式
同步通讯:就像打电话,需要实时响应。
异步通讯:就像发邮件,不需要马上回复。
两种方式各有优劣,打电话可以立即得到响应,但是你却不能跟多个人同时通话。发送邮件可以同时与多个人收发邮件,但是往往响应会有延迟。
我们之前学习的 Feign 调用就属于同步方式,虽然调用可以实时得到结果,但存在下面的问题:
同步调用的优点:
同步调用的缺点:
异步调用则可以避免上述问题,我们以购买商品为例,用户支付后需要调用订单服务完成订单状态修改,调用物流服务,从仓库分配响应的库存并准备发货。在事件模式中,支付服务是事件发布者(publisher),在支付完成后只需要发布一个支付成功的事件(event),事件中带上订单id。订单服务和物流服务是事件订阅者(Consumer),订阅支付成功的事件,监听到事件后完成自己业务即可。
为了解除事件发布者与订阅者之间的耦合,两者并不是直接通信,而是有一个中间人(Broker)。发布者发布事件到Broker,不关心谁来订阅事件。订阅者从Broker订阅事件,不关心谁发来的消息。
Broker 是一个像数据总线一样的东西,所有的服务要接收数据和发送数据都发到这个总线上,这个总线就像协议一样,让服务间的通讯变得标准和可控。
异步调用好处:
异步调用缺点:
MQ,中文是消息队列(MessageQueue),字面来看就是存放消息的队列,也就是事件驱动架构中的 Broker
比较常见的 MQ 实现:
几种常见MQ的对比:
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP、XMPP、SMTP、STOMP | OpenWire、STOMP、REST、XMPP、AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
以 RabbitMQ 为例,我们在 Centos7 虚拟机中使用 Docker 来安装
在线拉取镜像
docker pull rabbitmq:3-management
执行下面的命令来运行MQ容器
docker run \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=123456 \
--name mq \
--hostname mq1 \
-p 15672:15672 \
-p 5672:5672 \
-d \
rabbitmq:3-management
启动成功后访问地址:http://192.168.211.128:15672
RabbitMQ 中的一些角色
MQ 的基本结构
RabbitMQ 官方提供了 5 个不同的 Demo 示例,对应了不同的消息模型。
Hello World 模型
官方的 HelloWorld 是基于最基础的消息队列模型来实现的,只包括三个角色:
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.211.128");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "Hello RabbitMQ!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:[" + message + "]");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.211.128");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("admin");
factory.setPassword("123456");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:[" + message + "]");
}
});
System.out.println("等待接收消息中");
}
}
SpringAMQP 是基于 RabbitMQ 封装的一套模板,并且还利用 SpringBoot 对其实现了自动装配,使用起来非常方便。
SpringAMQP 的官方地址:Spring AMQP
SpringAMQP 提供了三个功能:
org.springframework.boot
spring-boot-starter-amqp
首先配置 MQ地址,在 publisher、consumer 服务中的 application.yml 中添加配置
spring:
rabbitmq:
host: 192.168.xx.xx # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: admin # 用户名
password: 123456 # 密码
在 consumer 服务中添加监听队列
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg){
System.out.println("消费者接收到了:"+msg);
}
}
在 publisher 服务中添加发送消息的测试类
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage2SimpleQueue(){
String queueName = "simple.queue";
String message = "hello world";
rabbitTemplate.convertAndSend(queueName,message);
}
}
Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。
此时就可以使用 work 模型,多个消费者共同处理消息处理,速度就能大大提高了。
我们循环发送,模拟大量消息堆积现象,在 publisher 服务中的 SpringAmqpTest 类中添加一个测试方法:
/**
* workQueue
* 向队列中不停发送消息,模拟消息堆积。
*/
@Test
public void testWorkQueue() throws InterruptedException {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, message_";
for (int i = 0; i < 50; i++) {
// 发送消息
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
消息接收
要模拟多个消费者绑定同一个队列,我们在 consumer 服务的 RabbitMQListener 中添加2个新的方法:
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到了:"+msg + ","+ LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("消费者2接收到了:"+msg + ","+ LocalTime.now());
Thread.sleep(200);
}
启动 ConsumerApplication 后,在执行 publisher 服务中刚刚编写的发送测试方法 testWorkQueue
可以看到消费者1很快完成了自己的25条消息。消费者2却在缓慢的处理自己的25条消息。
也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这是因为 RabbitMQ 默认有一个消息预取机制,显然这不是我们想要的结果,我们需要的是能者多劳嘛,所以去限制每次只能取一条消息,可以解决这个问题。
在 spring 中有一个简单的配置,设置 prefetch 属性,我们修改 consumer 服务的 application.yml 文件,添加配置
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
Work 模型的使用:
图中可以看到,在订阅模型中,多了一个 exchange 角色,而且过程略有变化
Exchange:交换机,一方面,接收生产者发送的消息;另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于 Exchange 的类型。Exchange 有以下3种类型:
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
Fanout,英文翻译是扇出,在 MQ 中我们也可以称为广播。
在广播模式下,消息发送流程是这样的:
接下里我们用 SpringAMQP 来简单实现 FanoutExchange
声明队列和交换机
Spring 提供了一个接口 Exchange,来表示所有不同类型的交换机。
在 consumer 中创建一个类,声明队列、交换机、绑定对象 Binding
@Configuration
public class FanoutConfig {
// 交换机 itcast.fanout
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("itcast.fanout");
}
// 队列1 fanout.queue1
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
// 队列2 fanout.queue2
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
// 绑定关系
@Bean
public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
通过这样 @Bean
的方式来申明确实比较麻烦,其实我们也是可以直接通过 @RabbitListener
注解来完成的,代码如下:
在 consumer 服务的 SpringRabbitListener 中添加三个方法,作为消费者
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg){
System.out.println("消费者1接收到了:"+msg);
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg){
System.out.println("消费者2接收到了:"+msg);
}
在 publisher 服务的 SpringAmqpTest 类中添加测试方法
@Test
public void testSendFanoutExchange(){
// 交换机
String exchange = "itcast.fanout";
String message = "hello message__";
rabbitTemplate.convertAndSend(exchange,"",message);
}
运行该方法,可以发现 fanout.queue1、fanout.queue2 都收到了交换机的消息。
总结一下:
交换机的作用是什么?
在 Fanout 模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到 DirectExchange
在 Direct 模型下:
RoutingKey
(路由key)RoutingKey
。Routing Key
进行判断,只有队列的Routingkey
与消息的 Routing key
完全一致,才会接收到消息在 consumer 的 SpringRabbitListener 中添加两个消费者,同时基于注解来声明队列和交换机
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "itcast.direct"),
key = {"red","blue"}
))
public void listenDirectQueue1(String msg){
System.out.println("消费者接收到了direct.queue1的消息:"+msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "itcast.direct"),
key = {"red","yellow"}
))
public void listenDirectQueue2(String msg){
System.out.println("消费者接收到了direct.queue2的消息:"+msg);
}
在 publisher 服务的 SpringAmqpTest 类中添加测试方法
@Test
public void testSendDirectExchangeA(){
// 交换机
String exchange = "itcast.direct";
String message = "hello blue";
rabbitTemplate.convertAndSend(exchange,"blue",message);
}
@Test
public void testSendDirectExchangeB(){
// 交换机
String exchange = "itcast.direct";
String message = "helloworld";
rabbitTemplate.convertAndSend(exchange,"red",message);
}
Topic
与 Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型可以让队列在绑定Routing key
的时候使用通配符!
通配符规则:
#
:匹配一个或多个词
*
:只能匹配一个词
例如:
item.#
:能够匹配item.spu.insert
或者 item.spu
item.*
:只能匹配item.spu
china.#
,因此凡是以 china.
开头的 routing key
都会被匹配到。包括 china.news 和 china.weather#.news
,因此凡是以 .news
结尾的 routing key
都会被匹配。包括 china.news 和 japan.news @RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "itcast.topic"),
key = {"china.#"}
))
public void listenTopicQueue1(String msg){
System.out.println("消费者接收到了topic.queue1的消息:"+msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "itcast.topic"),
key = {"#.news"}
))
public void listenTopicQueue2(String msg){
System.out.println("消费者接收到了topic.queue2的消息:"+msg);
}
/**
* topic
* 向交换机发送消息
*/
@Test
public void testSendTopicExchange(){
// 交换机
String exchange = "itcast.topic";
String message = "hello china";
rabbitTemplate.convertAndSend(exchange,"china.news",message);
}
Spring 会把你发送的消息序列化为字节发送给 MQ,接收消息的时候,还会把字节反序列化为 Java 对象。
默认情况下 Spring 采用的序列化方式是 JDK 序列化。
我们可以去试一下效果
@Bean
public Queue objectQueue(){
return new Queue("object.queue");
}
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map msg){
System.out.println("接收到了object.queue的消息:"+msg);
}
@Test
public void testSendObject(){
Map msg = new HashMap<>();
msg.put("name","lqh");
msg.put("age",22);
rabbitTemplate.convertAndSend("object.queue",msg);
}
众所周知,JDK序列化存在下列问题:
我们推荐可以使用 JSON 来序列化
在 publisher 和 consumer 两个服务中都引入依赖
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
2.9.10
配置消息转换器。
在各自的启动类中添加一个 Bean 即可
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
@SpringBootApplication
public class PublisherApplication {
public static void main(String[] args) {
SpringApplication.run(PublisherApplication.class);
}
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
转载自:微服务技术栈 - 乐心湖's Blog | 技术小白的技术博客 (xn2001.com)