以下内容的代码可见:SpringCloud_learn/day02
1.Nacos配置管理
之前提到的
Nacos
是作为注册中心,除此之外它还有配置管理功能
统一配置管理
假设有多个微服务之间有关联,此时修改了某个微服务的配置后其他相关的微服务也需要重启,十分麻烦,此时就需要
Nacos
进行统一的配置管理,并且实现配置更改热更新(修改配置后服务不需要重启即可生效),具体步骤如下:
- 在
Nacos
中添加配置信息:
- 填写配置信息:图中填写的配置是有热更新需求的配置(比如控制启用哪个服务的配置或者模板类型的配置就可写在这)
将部分配置放到
Nacos
后,此时微服务就要获取到这些配置,首先需要注意以下两点:
- 获取步骤是先读取到
Nacos
中配置的内容,再将其和本地配置文件的内容进行合并- 假设将
Nacos
地址放在本地配置文件中,就无法先读取到Nacos
的配置文件,所以将Nacos
地址放在优先级较高的配置文件bootstrap.yml
中从
Nacos
和本地获取配置具体操作步骤如下:
- 引入
Nacos
的配置管理客户端依赖:<!--nacos配置管理依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
- 在
userservice
中的resource
目录添加一个bootstrap.yml
文件(一般将与Nacos
相关的配置放在这个文件中,同时将application.yml
中重复的配置删除):# ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件ID去Nacos读取配置 spring: application: name: userservice # 服务名称 profiles: active: dev # 环境 cloud: nacos: server-addr: localhost:8848 # nacos地址 config: file-extension: yaml # 文件后缀名
- 测试是否读到
Naocs
配置文件:输入http://localhost:8081/user/now看是否输出相应格式的日期@Value("${pattern.dateformat}") private String dateformat; @GetMapping("now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat)); }
tips:
- 如果无法一直无法读取到相应日期可以重新
install
该模块
配置自动刷新
Nacos
中的配置文件变更后,微服务无需重启就可感知。有以下两种方式:
- 在
@Value
注入的变量所在类上添加注解@RefreshScope
@Slf4j @RestController @RequestMapping("/user") @RefreshScope public class UserController { @Value("${pattern.dateformat}") private String dateformat; ... }
使用
@ConfigurationProperties
注解(推荐使用)
- 首先在
user-service
服务中添加一个类config/PatternProperties
用于读取patterrn.dateformat
属性:@Data @Component @ConfigurationProperties(prefix = "pattern") public class PatternProperties { private String dateformat; }
- 在
UserController
中使用该类代替@Value
:之前的@Value
和@RefreshScope
就不需要了@GetMapping("now") public String now() { return LocalDateTime .now() .format(DateTimeFormatter.ofPattern(patternProperties.getDateformat())); }
tips:
- 建议将一些关键参数和需要运行时调整的参数放到
nacos
配置中心,一般都是自定义配置
多环境配置共享
微服务启动时会从
nacos
读取多个配置文件,无论profile
如何变化,[spring.application.name].yaml
一定会加载,因此多环境共享配置可写入该文件
[spring.application.name]-[spring.profiles.active].yaml
,如userservice-dev.yaml
[spring.application.name].yaml
,如userservice.yaml
假设在能读取到的配置文件中存在多个系统的属性,具体该读取哪个配置文件?
服务名-profile.yaml
>服务名称.yaml
> 本地配置(前两个就是Nacos
中的配置)tips:
- 每次更换测试环境或开发环境都要在配置文件中改写十分麻烦,可以在配置中直接更改再启动服务
搭建Nacos集群
之前
Nacos
全是单节点,而在生产环境中一定要部署为集群状态。部署的集群结构如下:
- 包含3个
Nacos
节点(因为还是在一台机器上实践,所以将它们端口配置为不一样即可)- 使用
Nginx
作为负载均衡器代理3个Nacos
Nacos
的配置从数据库(这里还是以单点数据库为例)中读取搭建集群步骤如下:
初始化数据库:
SQL
代码见SQL.txt
(之后在Nacos
进行的配置都会写入该数据库)下载并配置
Nacos
:
- 进入
Nacos
的conf
目录,修改配置文件cluster.conf.example
,重命名为cluster.conf
- 在
cluster.conf
文件中添加以下内容:127.0.0.1:8845 127.0.0.1.8846 127.0.0.1.8847
- 修改同一个目录下的
application.properties
文件,添加数据库配置:# 把注释符号去掉并修改相应信息即可 spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/xxx?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=xxx db.password.0=xxx
启动
Nacos
:
- 将
Nacos
文件夹复制三份并分别命名(nacos1、nacos2、nacos3
)- 分别修改三个文件夹中的
conf/application.properties
的端口号server.port=8845 server.port=8846 server.port=8847
- 分别启动三个
Nacos
节点:startup.cmd
(以集群启动不再加参数)配置
Nginx
反向代理:
- 修改
conf/nginx.conf
文件:注意是添加到http{xxx}
范围内upstream nacos-cluster { server 127.0.0.1:8845; server 127.0.0.1:8846; server 127.0.0.1:8847; } server { listen 80; server_name localhost; location /nacos { proxy_pass http://nacos-cluster; } }
- 启动
Nginx
(start nginx.exe
),在浏览器访问http://localhost/nacos即可(停止Nginx的命令为nginx.exe -s stop
)修改服务中的
application.yml
文件:spring: cloud: nacos: server-addr: localhost:80 # Nacos地址(即在Nginx中配置的代理地址)
2.http客户端Feign
Feign替代RestTemplate
以前在
order-service
中利用RestTemplate
发起远程调用的代码如下,主要存在两个问题:String url = "http://userservice/user/" + order.getUserId(); User user = restTemplate.getForObject(url, User.class);
- 代码可读性差,编程体验不统一
- 参数复杂,
URL
难以维护
Feign
是一个声明式的http
客户端,它可以优雅的实现http
请求的发送,解决上述问题。使用Feign
的步骤如下:
- 引入依赖:
<!-- Feign客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
- 在
order-service
的启动类添加注解开启Feign
的功能:@MapperScan("cn.itcast.order.mapper") @EnableFeignClients @SpringBootApplication public class OrderApplication { ... }
- 编写
Feign
客户端:主要是基于SpringMVC
的注解来声明远程调用的信息@FeignClient("userservice") public interface UserClient { // 下面代码和RestTemplate代码中其实都是一一对应的 @GetMapping("/user/{id}") User findById(@PathVariable("id") Long id); }
- 用
Feign
客户端代替RestTemplate
:// 使用Feign实现远程调用 @Autowired private UserClient userClient; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2.查询用户 User user = userClient.findById(order.getUserId()); // 3.封装user信息 order.setUser(user); // 4.返回 return order; }
tips:
Feign
的依赖包已经集成了ribbon
,所以实现了负载均衡- 注意
order-service
服务的端口不要和nginx
使用的8080
端口冲突,同时把server-addr
改为localhost:80
自定义Feign的配置
Feign
可以使用自定义配置来覆盖默认配置,可修改的配置如下:一般要配置的就是日志级别,配置
Feign
日志有两种方式:
配置文件方式:可全局生效或局部生效
feign: client: config: userservice: # 写服务名称就是针对某个微服务的局部配置 # default: # 写"default"就是全局配置 loggerLevel: FULL # 日志级别
Java
代码方式
- 先声明一个
Bean
:该类不需要加注解标记为配置类public class DefaultFeignConfiguration { @Bean public Logger.Level logLevel(){ return Logger.Level.BASIC; } }
- 如果需要全局配置,则把它放到
@EnableFeignClients
这个注解中:@MapperScan("cn.itcast.order.mapper") @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class) @SpringBootApplication public class OrderApplication { ... }
- 如果需要局部配置,则把它放到
@FeignClient
这个注解中:@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration.class) public interface UserClient { @GetMapping("/user/{id}") User findById(@PathVariable("id") Long id); }
Feign的性能优化
Feign
底层的客户端实现:
URLConnection
:默认实现,不支持连接池Apache HttpClient
:支持连接池OKHttp
:支持连接池因此优化
Feign
的性能主要包括:
使用连接池代替默认的
URLConnection
,具体步骤如下:
- 引入依赖:
<!-- Feign的httpClient依赖 --> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
- 配置连接池:
feign: client: config: default: loggerLevel: BASIC # 日志级别,BASIC即基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大连接数 max-connections-per-route: 50 # 每个路径的最大连接数
日志级别最好用
basic
或none
Feign的最佳实践
Feign
的客户端与Controller
代码相似,可以对重复的代码进行简化。具体采用以下两种方式:
给消费者(即
order-service
)的FeignClient
和提供者(即user-service
)的Controller
定义统一的父接口作为标准:
步骤:
- 定义一个
API
接口,利用定义方法并基于SpringMVC
注解做声明Feign
客户端和Controller
都集成改接口优缺点:
- 简单且实现了代码共享
- 服务提供方、服务消费方紧耦合
- 参数列表中的注解映射并不会继承,因此
Controller
中必须再次声明方法、参数列表、注解将
FeignClient
抽取为独立模块,并且把接口有关的POJO
、默认的Feign
配置都放到这个模块中,提供给所有消费者使用
步骤:
创建名为
feign-api
模块在
feign-api
中然后引入feign
的starter
依赖将
order-service
中的UserClient
、User
、DefaultFeignConfiguration
复制到feign-api
(复制后将order-service
相关代码删除)在
order-service
的pom
文件中中引入feign-api
的依赖:<!-- 引入feign-api --> <dependency> <groupId>cn.itcast.demo</groupId> <artifactId>feign-api</artifactId> <version>1.0</version> </dependency>
注意点:现在
UserClient
在cn.itcast.feign.clients
包下,而order-service
的@EnableFeignClients
注解是在cn.itcast.order
包下,导致order-service
启动类扫描不到UserClient
,注入UserClient
失败。解决方法如下:
- 指定
Feign
应该扫描的包:会将该包下的所有clients
全部扫描@EnableFeignClients(basePackages = "cn.itcast.feign.clients", defaultConfiguration = DefaultFeignConfiguration.class) @SpringBootApplication public class OrderApplication { ... }
- 指定
FeignClient
字节码:推荐使用@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class) @SpringBootApplication public class OrderApplication { ... }
3.统一网关Gateway
它旨在为微服务架构提供一种简单有效的统一的
API
路由管理方式
为什么需要网关
Gateway
网关是所有微服务的统一入口。其核心功能特性如下:
- 身份认证和权限校验:需要校验用户是是否有请求资格,没有则进行拦截
- 服务路由、负载均衡:网关不处理业务,而是根据某种规则把请求转发到某个微服务,该过程即路由。路由的目标服务有多个时需要做负载均衡
- 请求限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大
SpringCloud
中网关的实现包括两种:
SpringCloudGateway
:基于Spring5
中提供的WebFlux
,属于响应式编程的实现,具备更好的性能zuul
:基于Servlet
的实现,属于阻塞式编程
搭建网关服务
具体步骤如下:
- 创建新模块(如果没有使用模板创建则记得手动创建启动类),并引入
SpringCloudGateway
的依赖和nacos
的服务发现依赖(网关也是一个服务):<!-- 网关 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- nacos服务发现依赖 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
- 编写基础配置和路由规则:
server: port: 10010 # 网关端口 spring: application: name: gateway # 服务名称 cloud: nacos: # server-addr: localhost:8848 # nacos地址(未配置集群和Nginx前) server-addr: localhost:80 # nacos地址(即在Nginx中配置的代理地址) gateway: routes: # 网关路由配置 - id: user-service # 路由id,自定义,唯一即可 # uri: http://127.0.0.1:8081 # 可使用固定地址作为路由的目标地址 uri: lb://userservice # 可使用服务名称作为路由的目标地址,其中lb即负载均衡 predicates: # 路由断言,即判断请求是否符合路由规则的条件 - Path=/user/** # 按照路径匹配,只要以/user/开头就符合要求
- 启动网关:之后访问http://localhost:10010/user/1时,因为符合
/user/**
规则,所以请求转发到http://userservice/user/1网关路由的流程图如下:
网关路由可以配置的内容如下:
- 路由
id
:路由的唯一标示- 路由目标
uri
:路由的目标地址,http
代表固定地址,lb
代表根据服务名负载均衡- 路由断言
predicates
:判断路由的规则- 路由过滤器
filters
:对请求或响应做处理
断言工厂
在配置文件中写的断言规则只是字符串,这些字符串会被
Predicate Factory
读取并处理,转变为路由判断的条件。例如Path=/user/**
是按照路径匹配,该规则由PathRoutePredicateFactory
类处理。以下是SpringCloudGateway
的断言工厂:
过滤器工厂
GatewayFilter
是网关中提供的一种过滤器,可对进入网关的请求和微服务返回的响应做处理:
Spring
提供了多种不同的路由过滤器工厂:当前过滤器写在某个具体路由下只对访问具体服务的请求有效,要对所有的路由都生效可将过滤器工厂写到
default
下:spring: application: name: gateway cloud: gateway: routes: - id: user-service uri: lb://userservice predicates: - Path=/user/** - id: order-service uri: lb://orderservice predicates: - Path=/order/** # 给所有进入userservice的请求添加请求头 filters: # 针对某个服务请求的过滤器 - AddRequestHeader=test filter1! # 添加请求头 # 给所有服务的请求添加请求头 default-filters: # 默认过滤器,会对所有的路由请求都生效 - AddRequestHeader=test filter2!
全局过滤器
之前介绍的每种过滤器的作用都是固定的,如果想在拦截请求后做自己的业务逻辑则没法实现(即
GatewayFilter
通过配置定义,处理逻辑固定),此时需要全局过滤器GlobalFilter
。假设现在定义全局过滤器拦截请求,判断请求的参数是否满足下面条件(如果同时满足则放行,否则拦截):
- 参数中是否有
authorization
authorization
参数值是否为admin
具体实现步骤如下:
- 自定义类实现
GlobalFilter
接口- 添加
@Order
注解- 编写处理逻辑
// GlobalFilter接口的定义如下 public interface GlobalFilter { /*** 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理 * * @param exchange 请求上下文,里面可以获取Request、Response等信息 * @param chain 用来把请求委托给下一个过滤器 * @return {@code Mono<Void>} 返回标示当前过滤器业务结束 */ Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain); } // 自定义类 @Order(-1) // 参数值越小,优先级越大 @Component public class AuthorizeFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取请求参数 MultiValueMap<String, String> params = exchange.getRequest().getQueryParams(); // 2.获取authorization参数 String auth = params.getFirst("authorization"); // 3.校验 if ("admin".equals(auth)) { // 符合要求则放行 return chain.filter(exchange); } // 4.如果不符合要求则拦截 // 4.1.禁止访问,设置状态码 exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); // 4.2.结束处理 return exchange.getResponse().setComplete(); } }
过滤器执行顺序
请求路由后,会将当前路由过滤器和
DefaultFilter
、GlobalFilter
合并到一个过滤器链中,排序后依次执行每个过滤器。执行顺序如下:
- 每一个过滤器都必须指定一个
int
类型的order
值,order
值越小,优先级越高,执行顺序越靠前GlobalFilter
通过实现Ordered
接口或添加@Order
注解来指定order
值,由我们自己指定- 路由过滤器和
defaultFilter
的order
由Spring
指定,默认是按照声明顺序从1递增(虽然两种过滤器声明在一个配置文件中,但分开计数)- 当过滤器的
order
值一样时按照defaultFilter > 路由过滤器 > GlobalFilter
的顺序执行
跨域问题处理
跨域:域名不一致就是跨域
- 域名不同: www.taobao.com和www.taobao.org、www.jd.com和miaosha.jd.com
- 域名相同,端口不同:
localhost:8080
和localhost:8081
跨域问题:浏览器禁止请求的发起者与服务端发生跨域
ajax
请求,即ajax
请求被浏览器拦截
解决方案:一般使用CORS
(浏览器询问服务器能否允许某个请求跨域),网关处理跨域采用的同样是CORS
方案,进行以下配置即可(可以通过resources/index.html
进行跨域测试)spring: cloud: gateway: globalcors: # 全局的跨域处理 add-to-simple-url-handler-mapping: true # 解决options请求被拦截间题 corsConfigurations: '[/**]': al1owed0rigins: #允许哪些网站的跨域请求 - "http://localhost:8090" allowedMethods: #允许的跨域ajax的请求方式 - "GET" - "POST" - "DELETE" - "PUT" - "OPTIONS" allowedHeaders: "*" # 允许在请求中携带的头信息 allowCredentials: true # 是否允许携带cookie maxAge: 360000 # 这次跨域检测的有效期
tips:
- 对于服务器端的两个模块间的请求不属于浏览器的请求,并且不是
ajax
请求,所以不会存在跨域问题- 浏览器询问服务器某个请求能否跨域属于
options
请求,所以需要解决询问请求被拦截的问题- 在跨域检测的有效期内每次请求都需要去询问下服务器,不跨域就放行。过了有效期后所有请求就直接放行,提升性能
参考
黑马程序员SpringCloud框架P24-P41