Skip to content

DiscoveryClient 路由定义定位器

概述

在微服务架构中,服务发现是一个核心组件。Spring Cloud Gateway 提供了基于服务注册中心自动创建路由的能力,这大大简化了网关的配置管理。通过 DiscoveryClient 路由定义定位器,我们可以让网关自动为注册中心中的服务创建路由规则。

IMPORTANT

DiscoveryClient 路由定义定位器能够基于服务注册中心(如 Eureka、Consul、Zookeeper 或 Kubernetes)中注册的服务自动创建路由。这种方式特别适合动态微服务环境,避免了手动维护大量路由配置的麻烦。

工作原理

当启用 DiscoveryClient 路由定位器后,Spring Cloud Gateway 会:

  1. 从服务注册中心获取所有已注册的服务
  2. 为每个服务自动创建一个路由规则
  3. 使用负载均衡协议 lb://service-name 转发请求
  4. 应用默认的断言和过滤器规则

基础配置

1. 添加依赖

首先,确保项目中包含必要的依赖:

kotlin
dependencies {
    // Spring Cloud Gateway
    implementation("org.springframework.cloud:spring-cloud-starter-gateway")

    // 负载均衡器支持(必需)
    implementation("org.springframework.cloud:spring-cloud-starter-loadbalancer")

    // 服务发现客户端(选择其中一个)
    implementation("org.springframework.cloud:spring-cloud-starter-netflix-eureka-client")
    // 或者
    // implementation("org.springframework.cloud:spring-cloud-starter-consul-discovery")
    // 或者
    // implementation("org.springframework.cloud:spring-cloud-starter-zookeeper-discovery")
}
java
<dependencies>
    <!-- Spring Cloud 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>

    <!-- 服务发现客户端(选择其中一个) -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

2. 启用 DiscoveryClient 路由定位器

在配置文件中启用该功能:

yaml
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 启用 DiscoveryClient 路由定位器
          lower-case-service-id: true # 将服务ID转换为小写(推荐)
properties
# 启用 DiscoveryClient 路由定位器
spring.cloud.gateway.discovery.locator.enabled=true
# 将服务ID转换为小写(推荐)
spring.cloud.gateway.discovery.locator.lower-case-service-id=true

建议设置 `lower-case-service-id=true`,这样可以统一使用小写的服务名称进行路由,避免大小写不一致导致的问题。

默认路由规则

当启用 DiscoveryClient 路由定位器后,系统会为每个注册的服务自动创建以下默认规则:

默认断言(Predicate)

  • 路径断言/serviceId/**
    • 其中 serviceId 是从 DiscoveryClient 获取的服务 ID

默认过滤器(Filter)

  • 路径重写过滤器RewritePath
    • 正则表达式:/serviceId/?(?<remaining>.*)
    • 替换规则:/${remaining}
    • 作用:在请求发送到下游服务前移除服务 ID 部分

实际业务场景示例

假设我们有一个电商系统,注册中心中有以下服务:

  • user-service:用户服务
  • order-service:订单服务
  • product-service:商品服务
  • payment-service:支付服务

启用 DiscoveryClient 路由定位器后,会自动创建以下路由:

请求路径目标服务重写后的路径
/user-service/api/profilelb://user-service/api/profile
/order-service/api/orders/123lb://order-service/api/orders/123
/product-service/api/productslb://product-service/api/products
/payment-service/api/paylb://payment-service/api/pay

Kotlin 配置示例

kotlin
@Configuration
@EnableDiscoveryClient
class GatewayConfig {

    /**
     * 自定义 DiscoveryClient 定位器配置
     * 这个配置会在自动路由的基础上进行增强
     */
    @Bean
    fun discoveryLocatorProperties(): DiscoveryLocatorProperties {
        return DiscoveryLocatorProperties().apply {
            // 启用定位器
            isEnabled = true
            // 服务ID转小写
            isLowerCaseServiceId = true
            // 包含表达式,可以过滤特定的服务
            includeExpression = "metadata['gateway'] == 'true'"
        }
    }

    /**
     * 自定义服务实例的负载均衡配置
     */
    @Bean
    @LoadBalanced
    fun webClient(): WebClient {
        return WebClient.builder()
            .codecs { it.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
            .build()
    }
}

自定义断言和过滤器

虽然默认配置在大多数场景下已经足够,但在复杂的生产环境中,我们通常需要自定义断言和过滤器。

配置示例

yaml
spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
          predicates:
            # 保留默认的路径断言
            - name: Path
              args:
                pattern: "'/'+serviceId+'/**'"
            # 添加主机名断言,限制只有特定域名可以访问
            - name: Host
              args:
                pattern: "'**.example.com'"
          filters:
            # 添加熔断器
            - name: CircuitBreaker
              args:
                name: "serviceId"
                fallbackUri: "forward:/fallback"
            # 保留默认的路径重写过滤器
            - name: RewritePath
              args:
                regexp: "'/' + serviceId + '/?(?<remaining>.*)'"
                replacement: "'/${remaining}'"
            # 添加限流过滤器
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenish-rate: 10
                redis-rate-limiter.burst-capacity: 20
                key-resolver: "#{@pathKeyResolver}"
properties
# 路径断言配置
spring.cloud.gateway.discovery.locator.predicates[0].name=Path
spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]="'/'+serviceId+'/**'"

# 主机名断言配置
spring.cloud.gateway.discovery.locator.predicates[1].name=Host
spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]="'**.example.com'"

# 熔断器过滤器配置
spring.cloud.gateway.discovery.locator.filters[0].name=CircuitBreaker
spring.cloud.gateway.discovery.locator.filters[0].args[name]=serviceId
spring.cloud.gateway.discovery.locator.filters[0].args[fallbackUri]=forward:/fallback

# 路径重写过滤器配置
spring.cloud.gateway.discovery.locator.filters[1].name=RewritePath
spring.cloud.gateway.discovery.locator.filters[1].args[regexp]="'/' + serviceId + '/?(?<remaining>.*)'"
spring.cloud.gateway.discovery.locator.filters[1].args[replacement]="'/${remaining}'"

自定义配置类

kotlin
@Configuration
class CustomGatewayConfig {

    /**
     * 自定义路径解析器,用于限流
     */
    @Bean
    @Primary
    fun pathKeyResolver(): KeyResolver {
        return KeyResolver { exchange ->
            Mono.just(exchange.request.path.value())
        }
    }

    /**
     * 熔断器回退处理器
     */
    @RestController
    class FallbackController {

        @RequestMapping("/fallback")
        fun fallback(): Mono<Map<String, Any>> {
            return Mono.just(mapOf(
                "code" to 503,
                "message" to "服务暂时不可用,请稍后重试",
                "timestamp" to System.currentTimeMillis()
            ))
        }
    }

    /**
     * 自定义全局过滤器,添加追踪信息
     */
    @Component
    class TraceGlobalFilter : GlobalFilter, Ordered {

        override fun filter(exchange: ServerWebExchange, chain: GatewayFilterChain): Mono<Void> {
            val request = exchange.request.mutate()
                .header("X-Trace-Id", UUID.randomUUID().toString())
                .header("X-Gateway-Time", System.currentTimeMillis().toString())
                .build()

            return chain.filter(exchange.mutate().request(request).build())
        }

        override fun getOrder(): Int = -1
    }
}

高级配置场景

1. 基于服务元数据的路由

kotlin
@Configuration
class MetadataBasedRoutingConfig {

    /**
     * 基于服务元数据创建路由
     * 只有标记了 gateway=true 的服务才会被网关路由
     */
    @Bean
    fun metadataRouteDefinitionLocator(
        discoveryClient: DiscoveryClient,
        properties: GatewayProperties
    ): RouteDefinitionLocator {
        return object : RouteDefinitionLocator {
            override fun getRouteDefinitions(): Flux<RouteDefinition> {
                return Flux.fromIterable(discoveryClient.services)
                    .flatMap { serviceId ->
                        Flux.fromIterable(discoveryClient.getInstances(serviceId))
                    }
                    .filter { instance ->
                        // 只处理包含特定元数据的服务
                        instance.metadata["gateway"] == "true"
                    }
                    .map { instance ->
                        createRouteDefinition(instance)
                    }
            }
        }
    }

    private fun createRouteDefinition(instance: ServiceInstance): RouteDefinition {
        val routeDefinition = RouteDefinition()
        routeDefinition.id = "${instance.serviceId}-route"
        routeDefinition.uri = URI.create("lb://${instance.serviceId}")

        // 创建路径断言
        val pathPredicate = PredicateDefinition()
        pathPredicate.name = "Path"
        pathPredicate.args = mutableMapOf("pattern" to "/${instance.serviceId}/**")

        // 创建版本断言(基于元数据)
        val versionPredicate = PredicateDefinition()
        versionPredicate.name = "Header"
        versionPredicate.args = mutableMapOf(
            "header" to "X-Service-Version",
            "regexp" to (instance.metadata["version"] ?: "v1")
        )

        routeDefinition.predicates = listOf(pathPredicate, versionPredicate)

        // 创建路径重写过滤器
        val rewriteFilter = FilterDefinition()
        rewriteFilter.name = "RewritePath"
        rewriteFilter.args = mutableMapOf(
            "regexp" to "/${instance.serviceId}/(?<segment>.*)",
            "replacement" to "/\${segment}"
        )

        routeDefinition.filters = listOf(rewriteFilter)

        return routeDefinition
    }
}

2. 动态路由更新

kotlin
@Component
class DynamicRouteService(
    private val routeDefinitionWriter: RouteDefinitionWriter,
    private val applicationEventPublisher: ApplicationEventPublisher,
    private val discoveryClient: DiscoveryClient
) {

    /**
     * 监听服务注册事件,动态更新路由
     */
    @EventListener
    fun handleServiceRegistration(event: InstanceRegisteredEvent<*>) {
        refreshRoutes()
    }

    /**
     * 监听服务注销事件,动态删除路由
     */
    @EventListener
    fun handleServiceDeregistration(event: InstanceDeregisteredEvent<*>) {
        refreshRoutes()
    }

    /**
     * 刷新所有路由
     */
    fun refreshRoutes() {
        try {
            // 获取当前所有服务
            val services = discoveryClient.services

            services.forEach { serviceId ->
                val instances = discoveryClient.getInstances(serviceId)
                if (instances.isNotEmpty()) {
                    // 创建或更新路由
                    createOrUpdateRoute(serviceId, instances.first())
                }
            }

            // 发布路由刷新事件
            applicationEventPublisher.publishEvent(RefreshRoutesEvent(this))

        } catch (e: Exception) {
            log.error("刷新路由失败", e)
        }
    }

    private fun createOrUpdateRoute(serviceId: String, instance: ServiceInstance) {
        val routeDefinition = RouteDefinition().apply {
            id = "$serviceId-auto-route"
            uri = URI.create("lb://$serviceId")
            predicates = listOf(
                PredicateDefinition().apply {
                    name = "Path"
                    args = mutableMapOf("pattern" to "/$serviceId/**")
                }
            )
            filters = listOf(
                FilterDefinition().apply {
                    name = "RewritePath"
                    args = mutableMapOf(
                        "regexp" to "/$serviceId/(?<remaining>.*)",
                        "replacement" to "/\${remaining}"
                    )
                }
            )
        }

        routeDefinitionWriter.save(Mono.just(routeDefinition)).subscribe()
    }

    companion object {
        private val log = LoggerFactory.getLogger(DynamicRouteService::class.java)
    }
}

监控和调试

1. 路由信息查看

启用 Gateway 的 Actuator 端点来查看当前的路由信息:

yaml
management:
  endpoints:
    web:
      exposure:
        include: gateway
  endpoint:
    gateway:
      enabled: true

可以通过以下端点查看路由信息:

  • GET /actuator/gateway/routes - 查看所有路由
  • GET /actuator/gateway/routes/{id} - 查看特定路由
  • POST /actuator/gateway/refresh - 刷新路由缓存

2. 日志配置

yaml
logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    org.springframework.cloud.loadbalancer: DEBUG
    org.springframework.cloud.discovery: DEBUG

3. 自定义监控

kotlin
@Component
class GatewayMetrics(meterRegistry: MeterRegistry) {

    private val routeCounter = Counter.builder("gateway.route.requests")
        .description("Gateway route request count")
        .register(meterRegistry)

    private val routeTimer = Timer.builder("gateway.route.duration")
        .description("Gateway route request duration")
        .register(meterRegistry)

    @EventListener
    fun handleRouteRequest(event: RouteRequestEvent) {
        routeCounter.increment(
            Tags.of(
                Tag.of("service", event.serviceId),
                Tag.of("status", event.status.toString())
            )
        )
    }
}

最佳实践

TIP

服务命名规范

  1. 使用一致的命名约定(如 kebab-case)
  2. 避免在服务名中使用特殊字符
  3. 使用有意义的服务名称,便于路由管理

WARNING

安全考虑

  1. 不是所有的服务都应该通过网关暴露
  2. 使用服务元数据来标记哪些服务可以被网关路由
  3. 实施适当的认证和授权机制

IMPORTANT

性能优化

  1. 合理设置服务发现的刷新间隔
  2. 使用连接池和负载均衡策略
  3. 监控网关的性能指标,及时发现瓶颈

故障排查

常见问题

  1. 路由未生成

    • 检查 spring.cloud.gateway.discovery.locator.enabled 是否为 true
    • 确认服务已正确注册到服务注册中心
    • 查看网关日志中的路由创建信息
  2. 负载均衡失败

    • 确认已添加 spring-cloud-starter-loadbalancer 依赖
    • 检查服务实例是否健康
  3. 路径重写异常

    • 验证正则表达式是否正确
    • 检查替换规则的语法
Details

调试技巧可以通过以下方式调试路由问题:

kotlin
@RestController
class DebugController(private val routeLocator: RouteLocator) {

    @GetMapping("/debug/routes")
    fun getRoutes(): Flux<Route> {
        return routeLocator.routes
    }

    @GetMapping("/debug/discovery")
    fun getServices(discoveryClient: DiscoveryClient): List<String> {
        return discoveryClient.services
    }
}

总结

DiscoveryClient 路由定义定位器是 Spring Cloud Gateway 中一个强大的功能,它能够:

  1. 自动化路由管理:基于服务注册中心自动创建和维护路由
  2. 简化配置:减少手动配置路由的工作量
  3. 动态适应:自动适应服务的上线和下线
  4. 负载均衡:内置负载均衡支持

通过合理的配置和自定义,可以构建一个既灵活又强大的 API 网关系统,为微服务架构提供统一的入口点和流量管理能力。

TIP

在生产环境中使用时,建议结合服务网格(如 Istio)或配置中心(如 Nacos Config)来实现更高级的流量管理和配置管理功能。