本篇文章是Java微服务的扩展,以补全目前主流微服务组件Nacos的内容。

微服务回顾

单体架构
所有功能集中在一个项目内开发,打包部署,优点是架构简单,部署成本低,但功能耦合度高。

分布式架构
将业务模块拆分为多个独立部署、独立运行的服务项目,降低耦合度,有利于服务升级扩展,但架构设计复杂,维护成本高,适合中大型业务系统。

分布式架构主要考虑的问题:

  1. 服务拆分粒度,太粗会导致服务内仍有耦合,太细则会增大维护部署成本;
  2. 地址维护方式,服务更换地址时调用方如何感知;
  3. 远程调用方式,同步HTTP、RPC,异步消息队列;
  4. 健康状态维护,避免下游服务宕机导致连锁挂。

微服务设计原则

  1. 单一职责:一个微服务只负责一个完整业务域,避免重复开发和内部耦合;
  2. 面向服务:服务对外仅暴露标准化远程 API,隐藏内部实现细节;
  3. 自治:技术独立、数据独立、部署独立;
  4. 隔离性强:设计容错及降级策略,避免其他服务影响。

为实现微服务功能,通常需要如下组件

  • 注册中心:注册和拉取服务信息,维护服务地址和健康状态;
  • 配置中心:配置集中存储,实现配置热更新;
  • 服务网关:请求路由到具体服务,实现过滤和负载均衡。

不同体系实现微服务的具体技术栈如下:

技术体系DubboSpring CloudSpring Cloud Alibaba
注册中心ZookeeperEurekaNacos、Eureka
远程调用Dubbo RPCOpenFeign HTTPOpenFeign / Dubbo RPC
配置中心无原生,对接Nacos/ApolloSpring Cloud ConfigNacos
网关组件无内置,搭配Gateway/SoulSpring Cloud GatewaySpring Cloud Gateway
监控和保护HystrixSentinel

一张来自Nacos官网的微服务全景图
微服务全景图

Nacos 安装使用

Nacos,全称 Dynamic Naming and Configuration Service 动态服务发现与配置管理服务,完成微服务中的注册中心和配置中心功能。

Nacos安装

官网:https://nacos.io/,目前提供了一键安装的命令,但目前学习过程JDK用的还是老版本,所以选择去github上安装早期版本。

到下载页:https://github.com/alibaba/nacos/releases 找个2.x版本下载,

bin目录下开启cmd,启动命令

// 单机启动,默认集群启动,缺少相关配置会卡死
startup.cmd -m standalone 

启动目录
启动后通过http://127.0.0.1:8848/nacos即可访问控制台,默认账号密码都是nacos
nacos控制台

服务注册

Java中引入依赖如下:

<!-- nacos服务发现包 -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
	<!-- 版本与springboot适配 -->
	<version>2021.0.5.0</version>
</dependency>

服务注册配置

spring:
  # 服务名称(Nacos服务列表显示的名字,必填)
  application:
    name: acgs_service
  # Nacos 通用配置
  cloud:
    nacos:
      # Nacos服务地址,多个逗号分隔 127.0.0.1:8848,192.168.1.100:8848
      server-addr: 127.0.0.1:8848
      discovery:
        # 开启服务注册(默认true,写上更清晰)
        enabled: true

启动类增加注解:@EnableDiscoveryClient
启动后可在控制台查看到服务:
服务注册

服务分级存储

服务可能跨地域集群部署,如分为北京集群、杭州集群、深圳集群等,服务调用时希望尽可能调用本地的服务,防止跨集群访问,可在nacos中使用cluster-name配置集群名称。

使用方法为:

   discovery:
     # 集群名称
     cluster-name: bj

启动多个实例的方法,复制配置后指定启动配置文件,这种方式仍然会加载application.yml主配置文件
启动配置文件

进入服务详情可见已配置的集群
服务集群

负载均衡

负载均衡策略可选轮询、随机、按权重,配置方法如下:

# 开启nacos权重负载均衡策略
loadbalancer:
  nacos:
    enabled: true

权重修改方式如下:
修改服务器权重
这种方式最灵活,用于按服务器性能负载均衡。

该功能同时可用于平滑升级,通过调整权重给服务器摘流,完成升级后再逐步升级权重接管新流量。

环境隔离

Nacos提供了基于环境namespace的隔离,如开发dev、测试test和生产prod环境。

前面介绍了让启动类读取指定配置文件的方法,Nacos提供了服务的分环境管理,不同命名空间的服务不可互相访问

默认public是保留空间,新建命名空间流程如下:
新建命名空间
项目使用命名空间配置如下:

discovery:
  # 命名空间,生成的命名空间id
  namespace: f2617631-0ff7-4ac1-92b4-14fd49ff6f03

配置管理

该功能主要用于实现配置修改的热更新,可用于存储开关类型的配置信息(非所有配置),但Nacos 的远程配置是在项目启动后才拉取的,无法在启动时获取到配置中心的参数,因此需引入bootstrap引导文件,该文件加载时机早于application.yml,新增如下依赖:

<!-- 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>
    <version>3.1.5</version>
</dependency>

bootstrap中添加nacos相关配置,此时链路变成了:
bootstrap调用链路
新增配置

data-id命名规则:服务名称+运行环境
配置内容,数据与:前有空格

读取方式:

@RestController
// 热更新注解,配置变化则值变化
@RefreshScope
// 批量数据读取可用下面的注解,prefix为第一个index,后续变量名data可直接对应
// @ConfigurationProperties(prefix = "acgs")
public class test {

    // 读取配置中心数据
    @Value("${acgs.data:未读取到配置中心数据}")
    private String data;

    @GetMapping("/getConfig")
    public String getConfig() {
        return data;
    }
}

远程调用Feign

引入Feign

远程调用可使用restTemplate封装的http请求,但当参数复杂时难以维护,且代码中需维护字段与逻辑,十分不优雅。

@PostMapping("/callPost")
public String callPost(@RequestBody Map<String,Object> param) {
    String url = "http://acgs_service/postApi";
    // postForObject:携带JSON参数发起POST
    String result = restTemplate.postForObject(url, param, String.class);
    return "POST远程返回:" + result;
}

Feign提供了声明式http请求的方法,实现优雅发送http,首先引入feign:

<!-- 引入Feign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.1.5</version>
</dependency>

<!-- Feign 依赖负载均衡 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    <version>3.1.5</version>
</dependency>

快速使用

启动类中添加注解扫描Feign

@EnableFeignClients

远程调用接口声明服务和路径:

// value = 目标服务在Nacos注册的服务名
@FeignClient(value = "acgs_service")
public interface RemoteTestFeign {

    // 对应被调用服务的接口地址,根据参数不同写法也不同
    @GetMapping("/test{id}")
    String getRemoteMsg(@PathVariable("id") Long id));
}

注入接口实现远程调用:

@RestController
public class FeignCallController {
    @Autowired
    private RemoteTestFeign remoteTestFeign;

    @GetMapping("/feign/test")
    public String callRemote() {
        // 自动负载均衡访问acgs_service服务
        return "Feign调用返回:" + remoteTestFeign.getRemoteMsg();
    }
}

性能优化

Feign默认采用HttpURLConnection,这种方式没有连接池,每次远程调用新建 TCP 连接,频繁三次握手,高并发下大量 TIME_WAIT 端口占用,极易出现连接超时、端口耗尽。

通常可切换为OkHttp,首先引入依赖:

<!-- feign okhttp适配器 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

配置中开启:

spring:
  cloud:
    openfeign:
      # 启用okhttp客户端
      okhttp:
        enabled: true
      # 连接池全局配置
      max-connections: 200
      max-connections-per-route: 50

工程实践

Feign只是将实际服务与url对应的部分进行了解耦,同样要求请求方式和地址完全对应,实践中通常将Feign模块单独剥离出来,调用方引用依赖,避免耦合。

Feign的工程实践
加载时可全量扫描引入

@EnableFeignClients(basePackages = {
        "com.acgs.feign.client"
})
public class AcgsApplication {
    public static void main(String[] args) {
        SpringApplication.run(AcgsApplication.class, args);
    }
}

也可以按需引入:

@EnableFeignClients(clients = {
        RemoteTestFeign.class,
        RemoteOrderFeign.class
})
public class AcgsApplication {
    public static void main(String[] args) {
        SpringApplication.run(AcgsApplication.class, args);
    }
}

统一网关Gateway

引入Gateway

前面的Nacos和Feign提供了服务注册发现、配置中心及服务调用功能,Gateway组件则提供了对外的统一访问接口,具体功能如下:

  • 身份认证和权限校验:拦截所有请求,校验请求携带的凭证,避免每个微服务重复开发登录校验逻辑;
  • 服务路由和负载均衡:实现请求转发,屏蔽后端服务真实地址,同时可实现负载均衡和熔断降级;
  • 请求限流:保护后端微服务,防止突发流量压垮业务集群。

引入Gateway的方式如下:

<!-- Spring Cloud Gateway 网关核心 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

配置方法为:

# 服务基础配置
spring:
  application:
    name: gateway-server # 网关服务名,注册到Nacos,网关本质也是一个服务
cloud:
    gateway:
      # 开启服务发现自动路由(基于Nacos服务名转发 lb://服务名)
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true # 服务名小写匹配
      # 路由规则(两种写法:代码配置/yml配置,这里yml静态路由示例)
      routes:
        # 路由1:用户服务
        - id: user-service # 路由唯一id
          uri: lb://user-server # lb代表负载均衡,对应nacos注册服务名
          predicates: # 路由断言,判断请求是否转发
            - Path=/api/user/** # 匹配路径
          filters: # 过滤器
            - StripPrefix=1 # 去掉第一层路径 /api,转发到后端 /user/**

断言工厂

断言(Predicate)用于匹配请求,只有满足断言条件,当前路由才会生效;可多个断言组合,全部满足才匹配路由,不满足的会直接返回404。

所有断言被断言工厂Predicate Factory读取并处理,转换为具体的路由判断条件,常用断言工厂示例如下:

断言工厂说明YAML配置示例
Path根据请求URL路径匹配,支持通配符- Path=/api/user/**
Method根据HTTP请求方式匹配- Method=GET,POST
Query匹配URL请求参数,可校验参数存在或固定值- Query=token
- Query=name,admin
Header校验请求头,支持正则匹配头部值- Header=Token
- Header=Version,\d+
Cookie校验请求携带指定Cookie,支持正则- Cookie=sid,[0-9a-z]+
Host根据请求域名Host匹配,支持泛域名- Host=**.demo.com
RemoteAddr根据客户端IP网段匹配(CIDR)- RemoteAddr=127.0.0.1,192.168.1.0/24
After仅匹配指定时间之后的请求- After=2026-01-01T00:00:00.000+08:00[Asia/Shanghai]
Before仅匹配指定时间之前的请求- Before=2026-12-31T23:59:59.000+08:00[Asia/Shanghai]
Between匹配指定时间段内的请求- Between=2026-01-01T00:00:00.000+08:00[Asia/Shanghai],2026-12-31T23:59:59.000+08:00[Asia/Shanghai]
Weight灰度权重路由,同组分配流量比例- Weight=userGroup,8

过滤器 Filter

过滤器(Gateway Filter)用于处理请求和微服务的响应,可以实现添加请求头、响应头、移除请求头等操作,常用过滤器如下:

过滤器名称功能说明YAML配置示例
StripPrefix截取去掉路径前N层前缀- StripPrefix=1
RewritePath重写请求URL路径,正则替换- RewritePath=/api/(? .*),/${path}
AddRequestHeader统一添加请求头传递给后端服务- AddRequestHeader=gateway-source,gateway-server
AddResponseHeader给前端响应新增返回头- AddResponseHeader=server-version,1.0
RemoveRequestHeader删除指定请求头- RemoveRequestHeader=token
RemoveResponseHeader删除返回给前端的响应头- RemoveResponseHeader=Server
RequestHeaderSize限制请求头最大大小- RequestHeaderSize=5000
Retry请求失败自动重试,支持配置重试次数、响应码- name: Retry
args:
retries:2
statuses:500,502
RequestRateLimiter限流过滤器,限制接口QPS- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate:10
CircuitBreaker服务熔断降级,整合Sentinel/Resilience4j- name: CircuitBreaker
args:
name:userBreaker
fallbackUri:forward:/fallback
RedirectTo满足条件时302重定向跳转- RedirectTo=302,https://www.baidu.com
SetPath直接替换请求完整路径- SetPath=/user/getInfo
SetRequestHeader覆盖已有请求头的值- SetRequestHeader=token,global-token-123
SetStatus修改返回前端的HTTP状态码- SetStatus=403

上面指定的都是局部过滤器,针对特定路由的过滤条件,也可以指定全局过滤器default-filters,所有路由统一生效,配置方法如下:

spring:
  cloud:
    gateway:
      default-filters:
        # 全局添加请求头
        - AddRequestHeader=traceId,global-trace
        # 统一去掉一层路径前缀
        - StripPrefix=1

代码可实现GlobalFilter并重写filter方法,自定义过滤器,示例如下:

/**
 * 实现Gateway过滤器核心方法
 * 所有全局过滤器GlobalFilter、局部GatewayFilter都必须重写该方法
 * Gateway基于WebFlux响应式,全程使用Mono/Flux异步类型,不能用同步Servlet对象
 */
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // ServerWebExchange:请求上下文,封装本次请求Request、响应Response全部信息
    // GatewayFilterChain:过滤器链,代表当前路由后续所有过滤器 + 转发后端服务逻辑
    // 返回值Mono<Void>:响应式异步结果,代表本次过滤处理完成
    // 前置逻辑:请求转发前执行
        String path = exchange.getRequest().getPath().value();
        System.out.println("访问路径:" + path);
        
        // 放行,执行后续过滤器与路由
        return chain.filter(exchange);
}

路由优先级

路由可在yml中通过order配置优先级,该值越小越优先,可指定负值,优先级一致时,default>局部>global。

示例如下:

spring:
  cloud:
    gateway:
      routes:
        # 路由1 优先级最高
        - id: api-user-detail
          order: 0
          predicates:
            - Path=/api/user/detail/**
          uri: lb://user-service

        # 路由2 优先级次之
        - id: api-user-all
          order: 1
          predicates:
            - Path=/api/user/**
          uri: lb://user-service

自定义全局路由也可以使用注解@Order配置,示例如下:

@Order(-1) // 注解指定过滤器优先级,-1比默认0更早执行
public class AuthGlobalFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 鉴权前置逻辑
        return chain.filter(exchange);
    }
}

CORS跨域配置

跨域定义
浏览器禁止发起者与服务端发生跨域ajax请求,域名、端口不同都是跨域。

路由可全局配置,也可单条配置,示例如下:

# 全局跨域
spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          # 匹配所有请求路径
          '[/**]':
            allowed-origins: "https://xxx.com"        # 允许来源域名
            allowed-methods: GET,POST,PUT,DELETE,OPTIONS # 请求方法
            allowed-headers: "*"
            allow-credentials: false     # 允许携带cookie,为true时allowed-origins不能为*
            max-age: 360000                # 预检请求有效期,单位秒
        # 统一开启跨域缓存
        add-to-simple-url-handler-mapping: true

# 单路由跨域
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-server
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=1
            # 局部CORS配置
            - name: Cors
              args:
                allowedOrigins: http://localhost:8081
                allowedMethods: GET,POST,OPTIONS
                allowedHeaders: "*"
                allowCredentials: false

总结

整体链路

微服务整体架构

微服务配置总文件

spring:
  # 服务名称
  application:
    name: acgsService
  # 开发环境
  profiles:
    active: prod
  # 微服务配置
  cloud:
    # Nacos通用配置,注册与配置中心
    nacos:
      # Nacos服务地址,多个逗号分隔 127.0.0.1:8848,192.168.1.100:8848
      server-addr: 127.0.0.1:8848
      discovery:
        # 开启服务注册(默认true,写上更清晰)
        enabled: true
        # 集群名称
        cluster-name: bj
        # 命名空间,生成的命名空间id
        namespace: f2617631-0ff7-4ac1-92b4-14fd49ff6f03
      # 配置中心
      config:
        # 配置文件后缀yml
        file-extension: yaml
        # 开启自动热更新(默认true,显式声明保险)
        refresh-enabled: true
    # Feign 性能优化池
    openfeign:
      # 启用okhttp客户端
      okhttp:
        enabled: true
    # 负载均衡配置
    loadbalancer:
      # 启用nacos权重规则
      nacos:
        enabled: true
    # 路由服务注册(单独服务)
    gateway:
      # 开启服务发现自动路由(基于Nacos服务名转发 lb://服务名)
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true # 服务名小写匹配
      # 路由规则(两种写法:代码配置/yml配置,这里yml静态路由示例)
      routes:
        # 路由1:用户服务
        - id: user-service # 路由唯一id
          uri: lb://user-server # lb代表负载均衡,对应nacos注册服务名
          predicates: # 路由断言,判断请求是否转发
            - Path=/api/user/** # 匹配路径
          filters: # 过滤器
            - StripPrefix=1 # 去掉第一层路径 /api,转发到后端 /user/**

更多推荐