一、微服务
1.1 微服务技术栈
微服务,简单来说,就是将很多功能拆分称一个一个的服务集群,同时服务网关进行服务的筛选。注册中心中注册了所有服务的接口,配置中心中集中了各个服务的配置。在此基础上,又进行分布式日志服务,进行日志采集,并使用系统监控,链路追踪技术,找到每个服务的调用链路。其中还涉及到分布式缓存技术。同时,Jenkins加docker等技术,实现了持续集成。
1.2 微服务特征
微服务是一种经过良好架构设计的分布式架构方案,微服务架构特征:
- 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责,避免重复业务开发
- 面向服务:微服务对外暴露业务接口
- 自治:团队独立、技术独立、数据独立、部署独立
- 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
二、SpringCloud概念
2.1 SpringCloud简介
- SpringCloud官网
- SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验
2.2 服务拆分注意事项
- 不同微服务,不要重复开发相同业务
- 微服务数据独立,不要访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
2.3 提供者和消费者
- 服务提供者:暴露接口给其它微服务调用
- 服务消费者:调用其它微服务提供的接口
- 提供者与消费者角色其实是相对的,一个服务可以同时是服务提供者和服务消费者
三、Eureka注册中心
3.1 EureKa的作用
- 注册服务信息。每一个服务提供者在启动的时候,都会向EureKa注册服务的信息,例如ip,port等。Eureka会保存这些信息
- 拉取服务。消费者可以向Eureka拉去其他服务的信息,例如去拿到另一个服务的ip和port等
- 负载均衡。在消费者拉去到另一个服务的信息后,通过负载均衡算法选择可用的合适服务器
- 远程调用。消费者通过负载均衡选举出了一个可用的ip和port后,通过http请求向另一个服务发送请求,进行远程调用
【注:服务提供者每隔30秒会向Eureka发送心跳,保持健康状态,若健康状态不正常,则将该服务的信息从服务列表中剔除,保证消费者拉取的都是最新的信息】
3.2 Eureka的搭建
pom文件,这里继承了父工程的依赖,父工程中已经引入了springboot,版本需要对应,我这里springboot的版本是2.6.11,对应的springcloud版本是2021.0.3,Eureka的版本由spring版本控制自动匹配。【父工程的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>study_springcloud</artifactId>
<groupId>com.study</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
父工程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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.study</groupId>
<artifactId>study_springcloud</artifactId>
<version>0.0.1-SNAPSHOT</version>
<modules>
<module>user-service</module>
<module>order-service</module>
<module>eureka-server</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.11</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>
<spring-cloud.version>2021.0.3</spring-cloud.version>
<mysql.version>5.1.47</mysql.version>
<mybatis.version>2.2.2</mybatis.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml,我这里定义了Eureka的端口为10086
server:
port: 10086 # 服务端口
spring:
application:
name: eurekaserver
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://localhost:10086/eureka
main函数
package com.study.eureka;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
待启动完成后,访问http://localhost:10086,即可看到Eureka界面
3.3 Eureka服务注册
- 引入依赖。在服务中引入Eureka的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 添加配置。在要注册服务的yml文件中,加入Eureka的配置
【主要是spring.application.name和eureka.client.service-url.defaultZone的配置】
server:
port: 8552
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: userservice # user服务的名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://localhost:10086/eureka
- 重启服务,在页面中看到已经服务已经注册到eureka
3.4 Eureka服务发现
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 添加配置。在要注册服务的yml文件中,加入Eureka的配置
【主要是spring.application.name和eureka.client.service-url.defaultZone的配置】
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
eureka:
client:
service-url: # eureka的地址信息
defaultZone: http://localhost:10086/eureka
- 将调用生产者服务的ip和port,替换成服务注册时的注册名
package com.study.order.service;
import com.study.order.mapper.OrderMapper;
import com.study.order.pojo.Order;
import com.study.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestTemplate;
/**
* @author wangchaoyang
* @since 2023/2/14 15:37
*/
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
@GetMapping("{orderId}")
public Order queryOrderById(@PathVariable("orderId") Long orderId) {
Order order = orderMapper.findById(orderId);
//原来:String url = "http://localhost:8551/user/" + order.getUserId();
String url = "http://userservice/user/" + order.getUserId();
User user = restTemplate.getForObject(url, User.class);
order.setUser(user);
return order;
}
}
- 在消费者服务启动类中,创建RestTemplate对象,并加上@LoadBalanced注解
package com.study;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.study.order.mapper")
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
四、Nacos注册中心
4.1 Nacos安装
在Nacos官网,下载安装包。可以在网上搜索下,Springboot版本和Nacos版本的对应关系。
创建nacos_config的MySQL数据库,并执行conf文件夹下的nacos-mysql.sql文件。
添加application.properties文件,并添加如下配置。另外如果想要修改nacos的端口,也可在此文件中修改。
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456
进入bin目录下使用cmd输入 startup.cmd -m standalone 命令,进行单机运行。【注意路径不能有中文】
点击上图中nacos图标旁边的地址,进入浏览器查看页面。登录的默认账号和密码都是nacos。
4.2 Nacos使用
引入依赖,在父工程中的dependencyManagement中添加nacos管理依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
在各个服务中,添加nacos依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
在application.yml文件中,写入nacos配置。主要是spring.cloud.nacos.server-addr的值。
server:
port: 8552
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
分别开启生产者和消费者服务,可以在nacos页面看到注册的服务
4.3 Nacos服务分级存储
- 一级是服务,例如userservice服务
- 二级是集群,一个机房的多个实例叫集群,例如杭州集群和上海集群
- 三级是实例,例如杭州机房的某台部署了userservice的服务器
4.4 配置实例的集群属性
方法是,修改application.yml文件,添加spring.cloud.nacos.discovery.cluster-name属性。
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: userservice # user服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
在注册中心页面可以查看到集群名称和实例。
4.5 Nacos根据集群负载均衡
4.5.1 案例演示
现在给userservice配置集群HZ,开启两个实例;给userservice配置集群SH,开启一个实例。给orderservice配置集群HZ,开启一个实例。
userservice配置两个集群,三个实例
orderservice配置一个集群,一个实例
要做到的是,在HZ集群的orderservice远程调用userservice服务的时候,只访问HZ集群内的userservice,即只访问本地集群,因为跨集群访问的速度会很慢,不建议跨集群访问。
方法:在orderservice中的application.yml文件中,添加配置user.ribbon.NFLoadBalancerRuleClassName即可。
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
4.5.2 NacosRule负载均衡策略
- NacosRule优先选择同集群服务实例列表
- 当本地集群提供者服务都挂掉了,才会找其他集群访问,并且会报警告信息
- 确定了可用实例列表后,再使用随机负载均衡策略,挑选实例
4.5.3 Nacos加权负载均衡
- 在nacos控制台,可以设置实例的权重值,0~1之间
- 同集群内的多个实例,权重越高,被访问的频率越高
- 权重设置为0时,则完全不会被访问
4.6 Nacos环境隔离
Nacos中服务存储和数据存储的最外层都是一个名为namespace的东西, 用来做最外层隔离。不通namespace中的服务互不可见。
可以在Nacos的页面中,创建命名空间
此时,会生成一个唯一的命名空间ID
可以看到,dev命名空间下没有服务
在服务中添加配置,主要是spring.cloud.nacos.discovery.namespace参数
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
namespace: 23312037-1af0-4fd3-b146-25cf8de5d839 # 命名空间-dev环境
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
五、Nacos配置管理
5.1 Nacos配置
Nacos不仅可以当注册中心,也可以当配置中心。就是将服务中的一些配置,放置到nacos中管理,这里配置往往是平时需要修改的配置。
在Nacos的配置管理中,点击“+”进行添加配置。
5.2 Nacos如何加载配置
在项目启动后,会先读取Nacos中配置文件的内容,再读取本地application.yml文件中的配置,将两者合并后使用。
将Nacos配置文件的地址放bootstrap.yml配置文件中,因为如果放到application.yml中,这个文件在Nacos配置文件后加载,所以读取不到。而bootstrap.yml文件的优先级高于application.yml,所以放在bootstrap.yml中。
5.3 Nacos管理配置的步骤
1. 在Nacos中添加配置文件,如5.1所示
2. 在微服务中引入依赖
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加载bootstrap文件-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
3. 创建bootstrap.yaml文件,并写入配置
主要写服务名称,环境,后缀名,来找到Nacos配置的名称
spring:
application:
name: userservice
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
config:
file-extension: yaml # 文件后缀名
4. 删除application.yml中的重复配置
5. 通过@Value注解,可以获取到Nacos配置中的值
package com.study.user.web;
import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// 获取Nacos配置中的值
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/{id}")
public User quertById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth:" + truth);
return userService.queryById(id);
}
@GetMapping("/now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
6. 开启服务,访问http://localhost:8552/user/now请求,可以获取到Nacos配置中的值.
5.4 Nacos热更新
当Nacos配置文件更新时,Nacos可以支持不重启项目,就能热更新这些更新的配置。
方式一:在@Value注入的变量所在类上添加注解@RefreshScope
package com.study.user.web;
import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}")
private String dateformat;
@GetMapping("/{id}")
public User quertById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth:" + truth);
return userService.queryById(id);
}
@GetMapping("/now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
方式二【推荐】:通过配置类进行加载
创建PatternProperties类,通过@ConfigurationProperties注解的profix匹配配置中的前缀,类里面的属性值为profix下面的值。
package com.study.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
获取配置时,通过注入类,调用类的getter方法获取值即可。
package com.study.user.web;
import com.study.config.PatternProperties;
import com.study.user.pojo.User;
import com.study.user.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties patternProperties;
@GetMapping("/{id}")
public User quertById(@PathVariable("id") Long id,
@RequestHeader(value = "Truth", required = false) String truth) {
System.out.println("truth:" + truth);
return userService.queryById(id);
}
@GetMapping("/now")
public String now() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
}
5.5 Nacos多环境共享
在多服务中,如果Nacos的配置中有相同的部分,可以抽取出来,放到[服务名].yaml文件中。
- [服务名]-[spring.profile.active].yaml,为环境配置
- [服务名].yaml,为默认配置,多环境共享
- 优先级:[服务名]-[环境].yaml > [服务名].yaml > 本地配置
5.6 Nacos集群配置
5.6.1 开启Nacos集群
-
更改nacos/conf下的cluster.conf.example文件名为cluster.conf,并修改cluster.conf配置文件,增加集群的ip和port
注意这里的ip和port,三台集群的端口号要相差2,相差1会出现端口被占用问题,是因为Nacos2以上版本,会增加gRPC端口,会有1000和1001的偏移量127.0.0.1:8842 127.0.0.1:8844 127.0.0.1:8846
-
修改每个Nacos的端口,分别为配置的三个端口号【这里以8842为例】
-
分别启动三个Nacos,在每个Nacos的bin目录下,进入命令提示符敲命令startup.cmd即可。其中一个启动的界面如下
5.6.2 Nginx负载均衡
1. 下载nginx的包,修改conf/nginx.conf文件配置,将如下代码添加到http块中
upstream nacos-cluster是让nginx对这个集群做负载均衡,集群中有三台机器;server是监听81端口,location /nacos是当访问localhost:81/nacos的时候,nginx会代理到集群中去。
upstream nacos-cluster {
server 127.0.0.1:8842;
server 127.0.0.1:8844;
server 127.0.0.1:8846;
}
server {
listen 81;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
2. 在nginx目录下,start nginx.exe启动即可
3. 将bootstrap.yaml文件中的nacos地址,配置为负载地址
spring:
application:
name: userservice
profiles:
active: dev # 环境
cloud:
nacos:
server-addr: localhost:81 # nacos负载地址
config:
file-extension: yaml # 文件后缀名
4. 这里启动两台服务,在浏览器访问http://localhost:81/nacos,打开nacos页面
可以看到,Nacos健康实例为2个,集群搭建成功。
六、Feign远程调用
6.1 Feign使用
Feign是一种远程调用的方式,前几章节我们远程调用,是用RestTemplate发起http请求,但代码可读性差,url维护不方便,使用Feign来解决这些问题。
- 添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
因为ribbon的负载均衡会和Feign的相冲突,所以要排出ribbon依赖,如下
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
- 在OrderService启动类中添加注解@EnableFeignClients
package com.study;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
- 创建接口,接口中包含请求参数、请求方式、请求路径和返回值类型
package com.study.order.clients;
import com.study.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
- 在service中用FeignClinet中定义的方法,去代替RestTemplate请求的形式即可
package com.study.order.service;
import com.study.order.clients.UserClient;
import com.study.order.mapper.OrderMapper;
import com.study.order.pojo.Order;
import com.study.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.client.RestTemplate;
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(@PathVariable("orderId") Long orderId) {
Order order = orderMapper.findById(orderId);
User user = userClient.findById(order.getUserId());
order.setUser(user);
return order;
}
}
6.2 Feign自定义配置
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
方式一:配置文件方式
在OrderService的配置文件中,添加Feign自定义配置
feign.client.config.default.loggerLevel配置是全局配置,如果default改成某个服务的名称,则是针对某个微服务的配置
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
logging:
level:
com.study: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
feign:
client:
config:
default:
loggerLevel: FULL
方式二:Java代码方式
创建Feign配置类
package com.study.order.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel() {
return Logger.Level.BASIC;
}
}
如果这个配置仅想某个服务有效,就在某个服务上添加@FeignClient 的configuration参数
package com.study.order.clients;
import com.study.order.config.DefaultFeignConfiguration;
import com.study.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
如果想让配置全局有效,那么可以在启动类上添加@EnableFeignClients的configuration参数
package com.study;
import com.study.order.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
6.3 Feign优化
- 日志级别开启到BASIC
- 发送远程调用时吗,使用httpclient连接池代替原生URLConnection【步骤如下】
引入依赖
<!--httpclient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
在application.yaml文件中添加配置
server:
port: 8551
spring:
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password: 123
driver-class-name: com.mysql.jdbc.Driver
application:
name: orderservice # order服务的名称
cloud:
nacos:
server-addr: localhost:8848 # nacos服务地址
discovery:
cluster-name: HZ # 集群名称
mybatis:
type-aliases-package: com.study.user.pojo
configuration:
map-underscore-to-camel-case: true
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # nacos负载均衡规则
logging:
level:
com.study: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
feign:
client:
config:
default:
loggerLevel: BASIC
httpclient:
enable: true # 支持httpClint开关
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路径的最大连接数
6.4 Feign最佳实践
将FeignClient抽取为独立模块,并且把有关的pojo、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
-
新建一个模块,命名为feign-api,然后引入feign的starter依赖
-
将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中
-
在order-service中引入feign-api依赖
-
当前定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用,可以在启动类上指定FeignClient的字节码
@EnableFeignClients(clients = UserClient.class)
package com.study.order;
import com.study.feign.clients.UserClient;
import com.study.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.study.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
七、Gateway网关
7.1 网关的功能
- 对用户请求做身份认证和权限校验
- 将用户请求路由到服务器,并实现负载均衡
- 对用户请求做限流
7.2 搭建网关服务
创建gateway的模块,并修改pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.study.gateway</groupId>
<artifactId>gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway</name>
<description>gateway</description>
<parent>
<artifactId>study_springcloud</artifactId>
<groupId>com.study</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--nacos服务注册发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--Gateway网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
创建application.yaml文件,修改里面的网关配置
server:
port: 10010
logging:
level:
com.study: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 120.53.238.31:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
启动网关服务
package com.study.gateway.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
7.3 路由配置
- 路由id:路由的唯一标识
- 路由目标(uri):路由的目标地址,http表示固定地址,lb表示根据服务名负载均衡
- 路由断言(predicates):判断路由的规则
- 路由过滤器(filter):对请求或响应做处理
7.4 路由断言工厂
- 我们在配置文件中写的断言规则只是个字符串,这些字符串会被Predictes Factory读取并处理,转变为路由判断的条件
- 例如,Path=/user/**是按照路径批匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate. PathRoutePredicateFactory类来处理的
- spring提供了11中基本的Predicate工厂
7.5 路由过滤器
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理。
spring提供了31种不同的路由过滤器工厂(详细可见spring官网)
在配置文件中某个路由下加入filter配置,指定这个路由的过滤规则;要统一指定所有路由的过滤器,则可以在routes下增加default-filter配置
server:
port: 10010
logging:
level:
com.study: debug
pattern:
dateformat: MM-dd HH:mm:ss:SSS
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 120.53.238.31:8848 # nacos地址
gateway:
routes:
- id: user-service # 路由标识,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
filters:
- AddRequestHeader=Truth, Hello World!
- id: order-service
uri: lb://orderservice
predicates:
- Path=/order/**
# default-filter:
# - AddRequestHeader=Truth, All!
7.6 GlobalFilter全局过滤器
- 全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样
- 区别在于GatewayFilter通过配置定义,处理逻辑是固定的。而GlobalFilter的逻辑需要自 己写代码实现
- 定义方式是实现GlobalFilter接口
7.7 过滤器的执行顺序
- 请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、 GlobalFilter
- 请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter, 合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
八、Docker
8.1 Docker如何解决依赖兼容问题
- Docker将程序、lib(系统函数库)、Dep(依赖)和配置与应用一起打包,形成可移植的镜像,迁移到任意Linux操作系统
- 将每个应用放在一个隔离容器里去运行,使用沙箱机制,避免互相干扰
8.2 镜像和容器
- 镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。
- 容器(Container) :镜像中的应用程序运行后形成的进程就是容器,只是Docker会给容器做隔离,对外不可见。
8.3 Docker架构
Docker是一个CS架构,由两部分组成
- 服务端(server):Docker守护进程,负责处理Docker指令,管理镜像、容器等
- 客户端(cilient):通过命令或RestAPI向docker服务端发指令,可以在本地或远程向服务端发送指令
docker build指令,会被docker daemon守护进程接收和处理,由它将提供的数据,构建成一个镜像;
docker pull指令,会被docker daemon守护进程接收和处理,由它进行去Registry服务端拉取指定的镜像;
docker run指令,会被docker daemon守护进程接收和处理,告诉server要去创建容器,这时守护进程就会帮助创建容器。
DockerHub是一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为DockerRegistry
8.4 Docker安装
Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。
Docker CE 分为 stable
test
和 nightly
三个更新频道。
先卸载原有的Docker
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine \
docker-ce
安装Docker,使用yum安装
-
更新本地镜像源
# 设置docker镜像源 yum-config-manager \ --add-repo \ https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo yum makecache fast
-
安装Docker
yum install -y docker-ce
-
在启动Docker前,关闭防火墙
systemctl start docker
-
查看Docker版本
docker -v
-
配置镜像加速,针对docker客户端版本大于1.10.0的用户
sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://n0dwemtq.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker
8.5 Docker镜像操作命令
镜像名称一般分为两部分:[repository]:[tag],在没有指定tag时,默认是latest,代表最新版本镜像
8.5.1 从DockerHub上拉取镜像
打开DockerHub网站,在搜索栏搜索想要的镜像并选择其中一个
上面会有指令:docker pull nginx
,在linux下执行这个命令
使用docker images
查看本地所有的镜像
如果是Dockerfile来制作一个镜像,使用docker build来进行:
docker build -t 镜像名:latest .
-t
参数用于指定镜像名称和标签,.
表示 Dockerfile 所在的当前目录
8.5.2 将镜像打包成一个文件
使用docker save -o AA.tar nginx:latest
命令,进行对nginx:latest镜像进行打包,其中AA.tar是定义打出包的包名,nginx:latest是将哪个镜像进行打包
8.5.3 删除一个镜像
使用docker rmi nginx:latest
进行删除nginx:latest这个镜像,也可以通过docker rmi 镜像id
进行删除
8.5.4 将一个镜像文件导入
使用docker load -i xxxx
这个命令,来将xxxx镜像导入
8.6 Docker容器相关命令
8.6.1 创建运行一个容器
命令:docker run --name containerName -p 80:80 -d nginx
命令解读:
- docker run:创建并运行一个容器
- - -name:给容器起名
- - p:将宿主机端口与容器端口进行映射,冒号左边是宿主机端口,右边是容器端口
- - d:后台运行容器
- nginx:镜像名称,例如nginx
8.6.2 查看容器
命令:docker ps
查看运行中的容器
命令:docker ps -a
查看所有容器
8.6.3 查看容器的日志
命令:docker logs -f 容器名
命令解读: -f 参数意思是持续跟踪日志
8.6.4 启动/停止/删除一个容器
命令:docker start 容器名
启动一个容器
命令:docker stop 容器名
停止一个容器的运行
命令:docker rm 容器名
删除一个容器,加上-f参数是强制删除运行中的容器
8.6.5 进入容器内部
命令:docker exec -it 容器名 bash
命令解读:
- docker exec:进入容器内部,执行一个命令
- -it:给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
- bash:进入容器后执行的命令,bash是一个linux终端交互命令
8.7 Dockerfile自定义镜像
8.7.1 镜像分层
- 镜像是分层结构,每一层称为一个Layer
- Baselmage层:包含基本的系统函数库、 环境变量、文件系统
- Entrypoint:入口,是镜像中应用启动的命令
- 其它:在Baselmage基础上添加依赖、安装程序、完成整个应用的安装和配置
8.7.2 Dockerfile
Dockerfile就是一个文本文件,其中包含一个个指令,用指令来说明要执行什么操作来构建镜像,每一个指令都会形成一层layer
九、MQ消息队列
9.1 同步调用
同步调用就是按顺序一个一个调用服务,等待上一个服务调用完成后,才开始调用下一个服务,时效性强,可以立即得到结果。
同步调用的问题:
- 耦合度高,酶促加入新需求,都会修改原来代码
- 性能下降,调用者需要等待服务提供者响应,如果调用链过长,则响应时间就等于每次调用时间的总和
- 资源浪费,调用链中的每个服务在等待响应的过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
- 级联失败,服务提供者出现问题,所有调用方都会出问题
9.2 异步调用
异步调用会设置一个Broker,由它来管理服务。当服务调用者请求Broker时候,Broker会向它管理的服务发布消息,然后这些收到消息的服务就会去处理相关业务。前提是Broker管理的服务要先订阅Broker,只有订阅了Broker的服务才能收到Broker发布的消息。
异步通信优点:
- 耦合度低
- 吞吐量提升
- 故障隔离
- 流量削峰
异步通信缺点:
- 依赖于Broker的可靠性、安全性和吞吐能力
- 架构复杂,业务没有明显的流程线,不好追踪管理
9.3 什么是MQ
MQ即MessageQueue,消息队列,也就是Broker
9.4 安装RabbitMQ
利用Docker在线拉取镜像
docker pull rabbitmq:3-management
安装启动MQ
docker run -e RABBITMQ_DEFAULT_USER=root -e RABBITMQ_DEFAULT_PASS=123 --name mq --hostname mq1 -p 15672:15672 -p 5672:5672 -d rabbitmq:3-management
其中,15672端口是管理端界面的访问端口,5672是后续通讯端口。启动MQ后,可以使用ip:15672来访问管理端界面,账号就是上面设置的root,密码就是123
channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtual host:虚拟主机,是对queue、exchange等资源的逻辑分组
9.5 SpringAMQP
9.5.1 什么是SpringAMQP
Advanced Message Queuing Protocol,是用于在应用程序或之间传递业务消息的开放标准,该协议与语言和平台无关,更符合微服务中独立性的要求。
SpringAMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中Spring-amqp是基础抽象,Spring-rabbit是底层的默认实现。
9.5.2 使用SpringAMQP发送消息
-
在父工程中引入spring-amqp依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
-
配置SpringAMQP,在application.yml中配置相关配置
spring: rabbitmq: host: 8.130.111.12 port: 5672 username: root password: xxxxxx virtual-host: /
-
在publisher服务中利用RabbitTemplate发送消息到simple.queue队列
package com.study.publisher.spring; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2SimpleQueue() { String queueName = "simple.queue"; String message = "hello, spring amqp"; rabbitTemplate.convertAndSend(queueName, message); } }
-
在管理端查看发布的消息
9.5.3 使用SpringAMQP消费消息
-
配置SpringAMQP,在application.yml中配置相关配置
spring: rabbitmq: host: 8.130.111.12 port: 5672 username: root password: xxxxxx virtual-host: /
-
在consumer中编写消费逻辑,使用@RabbitListener注解指定队列名称,就能监听simple.queue,接收消息了
package com.study.consumer.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") public void listenSimpleQueue(String msg) { System.out.println("消费者接收到simple.queue消息:" + msg); } }
-
启动consumer后,发现消息已经被消费了
9.6 SpringAMQP之Work Queue工作队列
Work Queue,工作队列,可以提高消息处理速度,避免队列消息堆积
-
在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
package com.study.publisher.spring; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2WorkQueue() throws InterruptedException { String queueName = "simple.queue"; String message = "hello, spring__"; for (int i = 0; i < 50; i++) { rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(20); } } }
-
在consumer服务中定义两个消息监听者,都监听simple.queue队列,消费者1每秒处理50条消息,消费者2每秒处理10条消息
package com.study.consumer.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.time.LocalTime; @Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") public void listenWorkQueue1(String msg) throws InterruptedException { System.out.println("消费者1接收到simple.queue消息:【" + msg + "】" + LocalTime.now()); Thread.sleep(20); } @RabbitListener(queues = "simple.queue") public void listenWorkQueue2(String msg) throws InterruptedException { System.out.println("消费者2........接收到simple.queue消息:【" + msg + "】" + LocalTime.now()); Thread.sleep(200); } }
-
我们发现,消费者1会处理偶数条的消息,而消费者2会处理奇数条的消息,这是消息预取导致的,消费者2处理的慢,但也会去取偶数条的所有消息,导致消费者1处理完所有奇数消息后就不处理了,造成了资源浪费
-
消费预取限制。修改application.yml文件,设置prefetch值,设置消息预取为1,意思是每次只取一条消息,消费完了后再取。
spring: rabbitmq: host: 8.130.111.12 port: 5672 username: root password: xxxxxx virtual-host: / listener: simple: prefetch: 1 # 消息预取
9.7 SpringAMQP发布、订阅模型
发布订阅模型与之前的区别在于,允许将同一消息发送给多个消费者,实现方式是加入了exchange交换机
exchange类型包括:Fanout广播、Direct路由、Topic话题
9.7.1 发布订阅Fanout Exchange
Fanout Exchange会将接收到的消息路由到每一个跟其绑定的queue
-
在consumer服务中,利用代码声明队列、交换机,并将两者绑定
package com.study.consumer.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @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.queue1 @Bean public Queue fanoutQueue2() { return new Queue("fanout.queue2"); } // 绑定队列1到交换机 @Bean public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) { return BindingBuilder .bind(fanoutQueue1) .to(fanoutExchange); } // 绑定队列2到交换机 @Bean public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange) { return BindingBuilder .bind(fanoutQueue2) .to(fanoutExchange); } }
-
在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
package com.study.consumer.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class SpringRabbitListener { @RabbitListener(queues = "fanout.queue1") public void listenFanoutQueue1(String msg) { System.out.println("消费者接收到fanout.queue1消息:【" + msg + "】"); } @RabbitListener(queues = "fanout.queue2") public void listenFanoutQueue2(String msg) { System.out.println("消费者接收到fanout.queue2消息:【" + msg + "】"); } }
-
在publisher中编写测试方法,向itcast.fanout发送消息
package com.study.publisher.spring; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendFanoutExchange() { // 交换机名称 String exchangeName = "itcast.fanout"; // 消息 String message = "hello, everyone!"; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, "", message); } }
-
测试,发现两个队列均收到了消息
9.7.2 发布订阅Direct Exchange
- Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式
- 每一个Queue都与Exchange设置一个BindingKey
- 发布者发送消息时,指定消息的RoutingKey
- Exchange将消息路由到BindingKey与消息RoutingKey一致的队列
-
消费者consumer编写两个消费者方法,分别监听direct.queue1和direct.queue2,并利用@RabbitListener声明Exchange、Queue、RoutingKey
package com.study.consumer.listener; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class SpringRabbitListener { @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"), exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT), key = {"red", "blue"} )) public void listenerDirectQueue1(String msg) { System.out.println("消费者接收到direct.queue1消息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT), key = {"red", "yellow"} )) public void listenerDirectQueue2(String msg) { System.out.println("消费者接收到direct.queue2消息:【" + msg + "】"); } }
-
在publisher服务的测试类中添加测试方法
package com.study.publisher.spring; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendDirectExchange() { // 交换机名称 String exchangeName = "itcast.direct"; // 消息 String message = "hello, red!"; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, "red", message); } }
9.7.3 Direct和Fanout交换机的差异
- Fanout交换机将消息路由给每一个与之绑定的队列
- Direct交换机根据RoutingKey判断路由给哪个队列
- 如果多个队列具有相同的RoutingKey,则与Fanout功能类似
9.7.4 发布订阅Topic Exchange
-
TopicExchange与DirectExchange类似,区别在于routingKey必须是多个单词的列表,并且以
.
分割 -
Queue与Exchange指定BindingKey时可以使用通配符
- # :代表0个或多个单词
- * :代表1个单词
-
利用@RabbitListener声明Exchange、Queue、RoutingKey,在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
package com.study.consumer.listener; import org.springframework.amqp.core.ExchangeTypes; import org.springframework.amqp.rabbit.annotation.Exchange; import org.springframework.amqp.rabbit.annotation.Queue; import org.springframework.amqp.rabbit.annotation.QueueBinding; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; @Component public class SpringRabbitListener { @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue1"), exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC), key = "china.#" )) public void listenerTopicQueue1(String msg) { System.out.println("消费者接收到topic.queue1消息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue2"), exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC), key = "#.news" )) public void listenerTopicQueue2(String msg) { System.out.println("消费者接收到topic.queue2消息:【" + msg + "】"); } }
-
在publisher中编写测试方法,向itcast.topic发送消息
package com.study.publisher.spring; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendTopicExchange() { // 交换机名称 String exchangeName = "itcast.topic"; // 消息 String message = "这是一条新闻!"; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, "china.news", message); } }
-
启动后,可以看到满足条件的queue1和queue2都收到了消息
9.8 消息转换器
- Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的
- 默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化
- 如果要修改只需要定义一个MessageConverter类型的Bean即可,推荐用JSON方式序列化
-
声明一个队列
@Bean public Queue objectQueue() { return new Queue("object.queue"); }
-
消息发送方发送Map类型的参数
@Test public void testSendObjectQueue() { Map<String, Object> msg = new HashMap<>(); msg.put("name", "张三"); msg.put("age", 21); rabbitTemplate.convertAndSend("object.queue", msg); }
-
会发现消息无法识别成Map集合类型
-
修改序列化为json形式,首先引入依赖
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
-
序列化为json
package com.study.publisher; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class PublisherApplication { public static void main(String[] args) { SpringApplication.run(PublisherApplication.class, args); } // 序列化 @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } }
-
再次发送消息,查看已经序列化为json
-
消息接收
@RabbitListener(queues = "object.queue") public void listenObjectQueue(Map<String, Object> msg) { System.out.println("接收到object.queue消息:" + msg); }