Nacos 配置中心

通过 Nacos 控制台添加配置, 进行统一配置管理

一般分三部分:

  • 配置文件 id (常按 [服务名称]-[profile].[后缀] 命名)
  • 分组
  • 配置格式 (一般用 propertiesyaml)

服务获取配置

Spring 启动服务的一般步骤:

flowchart LR

1[项目启动]--> 2[读取配置文件\napplication.yml] --> 创建Spring容器 --> 加载bean

如果按照本地的项目启动方式, 服务不可能读取到 Nacos 的配置

我们需要换种方式来启动 Spring 项目:

  1. 引入 Nacos 配置依赖
<dependency>
  <groupId>com.alibaba.cloud</groupId>
  <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  1. 新建引导文件 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 最佳实践.png

抽取步骤:

  1. 创建一个模块, 命名为 feign-api, 引入 feign 的 stater 依赖
  2. 配置 Feign, 并把相关联的 POJO 和实现接口放入该模块中
  3. 在消费者中引入 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 工厂:

Predicate 工厂.png


路由过滤器

路由过滤器 Gateway Filter 是网关提供的一种过滤器, 可以对进入网关的请求和微服务返回的响应做处理

对请求和响应都可做处理

Spirng 提供了 31 种不同的路由过滤器工厂, 例如:

过滤器示例.png

配置过滤器:

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 合并到一个过滤器链(集合中), 排序后依次执行每个过滤器

过滤器执行顺序.png

  • 路由过滤器和 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
最后修改:2025 年 07 月 03 日
如果觉得我的文章对你有用,请随意赞赏