Nacos 配置中心
通过 Nacos 控制台添加配置, 进行统一配置管理
一般分三部分:
- 配置文件 id (常按
[服务名称]-[profile].[后缀]
命名) - 分组
- 配置格式 (一般用
properties
或yaml
)
服务获取配置
Spring 启动服务的一般步骤:
flowchart LR
1[项目启动]--> 2[读取配置文件\napplication.yml] --> 创建Spring容器 --> 加载bean
如果按照本地的项目启动方式, 服务不可能读取到 Nacos 的配置
我们需要换种方式来启动 Spring 项目:
- 引入 Nacos 配置依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
- 新建引导文件
bootstrap.yml
, 并配置
bootstrap.yml
优先级高于application.yml
spring:
application:
name: user-service
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.233.128:8848
config:
# namespace: edc5e1fe-a6f4-4c33-951e-a0f2f4a25d00
file-extension: yml
一定要注意命名空间, 如果注册中心创建的配置步骤默认 public 命名空间中, 则这里的 bootstrap.yml
也要配置上命名空间 (否则无法从 Nacos 中读取配置)
该 bootstrap.yml
匹配的 Data ID 为 user-service-dev.yml
服务项目配置完毕后, 还需在 Nacos 控制台中添加相应配置, 即 application.yml
中的内容
获取配置测试
我们可以测试一下配置的获取
配置获取示例
创建一个新的接口 GET /now
返回当前时间, 但是时间的格式化范式是从 Nacos 中获取的
首先在 Nacos 配置中心添加一个时间格式化范式的字符串:
pattern:
dateformat: yyyy-MM-dd HH:mm:ss
然后在控制器类中编写获取配置的代码:
@Value("${pattern:dateformat}")
private String dateFormat; // 获取配置字符串
@GetMapping("/now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateFormat, Locale.CHINA));
}
重运行, 并测试接口
配置热更新
如果我们在 Nacos 控制台中更新了配置, 把格式化范式改为 YYYY-MM-dd
, 尝试访问 /now
接口, 结果发现时间格式并没有发生变化
这是因为没有启用配置热更新
要想启用配置热更新,
一种方法是可以在类上添加注解 @RefreshScope
, 即可实现配置热更新
另一种方法是采用配置类访问配置属性 (方便配置管理)
以这个配置为例:
pattern:
dateformat: YYYY-MM
一般的做法是创建一个配置类, 并使用 @ConfigurationProperties
即将配置类放在一起
@Data
@Component
@ConfigurationProperties(prefix = "pattern") // 配置前缀
public class DateFormatConfig{
private String dateformat;
}
访问配置:
@RestController
@RefreshScope // 热更新
@RequestMapping("/")
public class RootController{
@AutoWired
private DateFormatConfig dateFormatConfig;
@GetMapping("now")
public void now(){
// 访问配置
System.out.println(dateFormatConfig.dateFormat);
}
}
多环境共享
微服务启动时会从 nacos 读取多个配置文件:
[服务名]-[profile.active].[后缀]
[服务名].[后缀]
无论 profile
如何变化, [服务名].[后缀]
这个文件一定会加载, 因此多环境共享配置可以写入这个文件
服务名即application.yml
中的spring.application.name
配置
Nacos 多配置具有优先级:
[服务名]-[profile.active].[后缀]
> [服务名].[后缀]
> 本地配置
Feign - http客户端
String requestUrl = "http://user-service/user/" + order.getUserId();
User user = restTemplate.getForObject(requestUrl, User.class);
使用 RestTemplate
的问题:
- 代码可读性差
- 参数复杂的 URL 难以维护
Feign 是一个声明式的 http 客户端, 可以帮助我们更优雅的实实现 http 请求的发送
引入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在启动类上添加注解以启用 Feign
@EnableFeignClients
创建一个包 client
创建对应服务的客户端接口, 封装 Feign 客户端
比如我们要调用用户服务, 接口名: UserClient
@FeignClient("服务名")
注解用于指定服务名
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
public User queryById(@PathVariable long id);
}
最后, 注入并调用客户端:
@AutoWired
private UserClient userClient;
// 调用
User user = userClient.queryById(userId);
自定义配置
可修改的配置如下:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 日志级别 | 四种不同的级别: NONE, BASIC, HEADERS, FULL |
feign.codec.Decoder | 响应结果解析器 | 响应解析, 比如将 json 字符串转为 java 对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码, 便于通过 http 请求发送 |
feign.Contract | 支持的注解格式 | 默认是 SpringMVC 的注解 |
feign.Retryer | 失败重试机制 | 请求失败重试机制, 默认是无, 不过会使用 Ribbon 的重试 |
这里我们配置日志级别
可通过 application.yml
配置:
feign:
client:
config:
# 全局配置
default:
loggerLevel: BASIC
# 写某个服务的名称, 则是针对某个微服务的配置
user-service:
loggerLevel: FULL
也可通过注解配置:
首先需声明一个配置 Bean
public class FeignClientConfiguration{
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.Basic;
}
}
然后添加注解, 同样分全局与局部
全局配置:
@EnableFeignClient(defaultConfiguration = FeignClientConfiguration.class)
局部配置, 将其注解放在相应的客户端接口上:
@FeignClient(value = "user-service", FeignClientConfiguration.class)
性能优化
Feign 底层的客户端实现:
URLConnection
默认实现, 不支持连接池Apache HttpClient
支持线程池OKHttp
支持连接池
根据底层实现, 可以从两方面对 Feign 进行性能优化:
- 使用连接池代替默认的
URLConnection
- 日志级别最好用 BASIC 或 NONE
替代客户端底层实现为 Apache HttpClient
连接池
引入依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
然后配置连接池:
feign:
httpclient:
enable: true
max-connection: 200 # 最大连接数
max-connection-per-route: 50 # 每个路径最大连接数
需根据请求量合理配置连接池
最佳实践
如果按照最先的写法, 专门封装一个 user-service 客户端的接口, 可行但会有一个问题, 代码会发生重复, 消费者和提供者各有一个重复的接口代码
要避免代码重复, 可以:
- 继承 - 定义一个父接口作为标准 (不太建议, 耦合度高, 参数映射无法被继承, 文件多)
- 抽取 - 将 FeignClient 抽取为独立模块, 并把使用有关的 POJO, 默认的 Feign 配置都放到这个模块中, 提供给所有消费者使用
POJO(Plain Ordinary Java Object)简单Java对象
抽取步骤:
- 创建一个模块, 命名为
feign-api
, 引入 feign 的 stater 依赖 - 配置 Feign, 并把相关联的 POJO 和实现接口放入该模块中
- 在消费者中引入
feign-api
依赖并调用
我们可以把 feign-api
依赖放入父级依赖中, 好让所有服务能够调用模块对象:
<dependency>
<groupId>top.voiblog.demo</groupId>
<artifactId>feign-api</artifactId>
</dependency>
当修改完对应的 pojo 包的导入后, 启动服务, 结果报错
required a bean of type 'top.voiblog.reignapi.client.UserClient' that could not be found
这样的报错是因为我们定义的 Client 不在服务的扫描包范围内, 有两种方法解决:
指定 FeignClient 的所在包:
@EnableFeignClients(basePackages = "top.voiblog.feignapi.client")
或是指定 FeignClient 字节码:
@EnableFeignClients(basePackageClasses = {UserClient.class})
推荐使用指定包的方法, 在包下有一堆 Client 类的情况下, 不用挨个指定
网关
统一网关 Gateway
网关功能:
- 身份验证和权限校验
- 服务路由, 负载均衡
- 请求限流
在 Spring中网关的实现包括两种:
- gateway
- zuul
Zuul 是基于 Servlet 的实现, 属于阻塞式编程, 而 SpringCloudGateway 则是基于 Spring 5 中提供的 WebFlux, 属于响应式编程实现, 具备更好的性能
搭建网关服务
首先创建新的模块 gateway, 引入 SpringCloudGateway 和 Nacos 发现依赖
<!-- 网关依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
然后编写路由配置:
路由配置包括:
- 路由 id
路由目标
uri
路由的目标地址http
代表固定地址lb
代表服务名负载均衡
- 路由断言
predicates
- 判断路由规则 - 路由过滤器
filters
对请求或响应做处理
server:
# 网关端口
port: 10010
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.233.128:8848
# 网关配置
gateway:
routes:
# 路由 id (规则 id), 自定义, 唯一即可
- id: user-service
# 服务名称
uri: lb:http://user-service
# 路由断言, 判断请求是否符合路由规则的条件
predicates:
# 路径匹配, 以 /user/ 开头的请求符合要求
- Path=/user/**
最后, 编写 SpringBoot 启动类, 开启网关服务
断言工厂
predicates
路由断言, 判断请求是否符合要求, 符合则转发到路由目的地
配置的路由断言字符串会被 Predicate Factory 读取并处理, 转变为路由判断条件
Spring 提供了 11 中基本 Predicate 工厂:
路由过滤器
路由过滤器 Gateway Filter 是网关提供的一种过滤器, 可以对进入网关的请求和微服务返回的响应做处理
对请求和响应都可做处理
Spirng 提供了 31 种不同的路由过滤器工厂, 例如:
配置过滤器:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb:http://user-service
predicates:
- Path=/user/**
filters:
# 添加请求头字段
- AddResponseHeader=Message, Hello form filter!
# 对所有路由生效
default-filter:
- AddResponseHeader=Message,Hello form filter!
配置在路由下的过滤器只对当前路由请求生效
全局过滤器
全局过滤器 Global Filter 的作用也是处理一切进入网关的请求和微服务响应, 与 Gateway Filter 的作用是一样的
不同的是, Global Filter 的逻辑需要写代码实现, 可以完成一些自定义过滤需求
创建 filter
包
然后实现 GlobalFilter
接口, 添加 @Order
注解
@Order(-1) // 优先级最高 (越小优先级越高)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
String auth = params.getFirst("authorization");
if (auth != null && auth.equals("admin")){
// 放行
return chain.filter(exchange);
}
// 拦截
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
}
除了 @Order
注解指定优先级, 还可实现 Ordered
接口来指定优先级
@Component
public class AuthorizeFilter implements GlobalFilter, Order{
...
@Override
public int getOrder(){ return -1; }
}
过滤器执行顺序
请求进入网关会碰到三类过滤器:
- 路由过滤器
- Default Filter
- Global Filter
请求路由后, 会将当前路由过滤器和 DefaultFilter, GlobalFilter 合并到一个过滤器链(集合中), 排序后依次执行每个过滤器
- 路由过滤器和
DefaultFilter
的 order 由 Spring 指定, 默认是按照声明顺序从 1 递增 - 当过滤路由器的 order 值一样时, 会按照
DefaultFilter
-> 路由过滤器 ->GlobalFilter
的顺序执行
跨域问题
域名或端口不同都是跨域
跨域问题: 浏览器禁止请求的发起者与服务发生跨域请求, 请求被拦截的问题
通过 CORS 机制, 处理跨域
spring:
cloud:
gateway:
# 配置 CORS
globalcors:
# 防止 OPTIONS 请求被拦截
add-to-simple-url-handler-mapping: true
corsConfiguration:
'[/**]':
allowedOrigins:
- http://yourhost.com
allowedMethods:
- GET
- POST
- DELETE
- PUT
- OPTIONS
# 允许的请求头字段
allowedHeaders: '*'
# 是否允许携带 Cookie
allowCredentials: true
# 跨域检测的有效期
maxAge: 360000