Introducción detallada a la implementación y el uso de la puerta de enlace del servicio Spring Cloud Gateway

1. ¿Por qué necesita una puerta de enlace de servicio?

1. ¿Qué es una puerta de enlace de servicio?

        En la arquitectura monolítica tradicional, solo se necesita abrir un servicio para las llamadas de los clientes, pero en la arquitectura de microservicios, un sistema se divide en múltiples microservicios.Si no hay una puerta de enlace, el cliente solo puede registrar la invocación de cada microservicio localmente. , cuando la cantidad de microservicios a llamar es grande, necesita comprender la interfaz de cada servicio, lo cual es mucho trabajo. ¿Qué tipo de mejora se puede lograr después de tener una puerta de enlace?

        Como la única entrada de tráfico del sistema, la puerta de enlace encapsula la arquitectura del sistema interno. Todas las solicitudes pasan primero por la puerta de enlace, y la puerta de enlace enruta las solicitudes a los microservicios apropiados. Por lo tanto, las ventajas de usar la puerta de enlace son:

  • (1) Simplificar el trabajo del cliente. Una vez que la puerta de enlace encapsula los microservicios, el cliente solo necesita interactuar con la puerta de enlace sin llamar a diferentes servicios;
  • (2) Reducir el acoplamiento entre funciones. Una vez modificada la interfaz del servicio, solo es necesario modificar la estrategia de enrutamiento del gateway, y no es necesario modificar cada cliente que llama a la función, por lo que se reduce el acoplamiento entre programas.
  • (3) Liberar a los desarrolladores para que se centren en la implementación de la lógica empresarial. La puerta de enlace implementa funciones no relacionadas con el negocio, como enrutamiento de servicios (escala de grises y ABTest), balanceo de carga, control de acceso, fusible de control de flujo y degradación, y no es necesario tenerlo en cuenta al implementar cada API de servicio.

        Pero el gateway API también tiene sus carencias, en la arquitectura descentralizada de microservicios, el gateway se convierte en un punto central o cuello de botella, lo que añade un componente de alta disponibilidad que debemos desarrollar, desplegar y mantener. Es por ello que el diseño del gateway debe considerar que aunque el gateway API esté caído, no afectará la invocación y operación del servicio, por lo que es necesario contar con la capacidad de cacheo de datos para la respuesta del gateway, y proteja el servicio de back-end devolviendo datos almacenados en caché o datos predeterminados.

        La puerta de enlace también tiene ciertos requisitos sobre el método de llamada del servicio. La puerta de enlace API debe admitir preferentemente E/S sin bloqueo asíncrono y síncrono. Si el servicio se llama de forma síncrona y con bloqueo, se puede entender que no hay un desacoplamiento completo entre los módulos de microservicio. , es decir, si A depende de la API proporcionada por B, si el servicio proporcionado por B no está disponible, afectará directamente la falta de disponibilidad de A, a menos que la llamada de servicio síncrono se almacene en caché en la capa de puerta de enlace API o del lado del cliente. Por lo tanto, para desacoplar por completo, se recomienda elegir un método asíncrono para llamadas de microservicio. Para el escenario en el que la puerta de enlace API necesita combinar varias API detalladas en la parte inferior, se recomienda utilizar el modelo de programación receptiva en lugar del método de devolución de llamada asíncrono tradicional para combinar el código. La composición API en sí puede llamarse en paralelo o sucesivamente. y, a menudo, es difícil controlar el método de devolución de llamada.

2. Las funciones básicas de la puerta de enlace de servicio:

3. La diferencia entre la puerta de enlace de tráfico y la puerta de enlace de servicio:

        Las posiciones de las puertas de enlace de tráfico y las puertas de enlace de servicio en la arquitectura general del sistema se muestran en la figura anterior. Las puertas de enlace de tráfico (como Nignx) proporcionan políticas globales que no están relacionadas con las aplicaciones comerciales de back-end, como la descarga de certificados HTTPS, el firewall web, Monitoreo de tráfico global, etc. Las puertas de enlace de microservicios (como Spring Cloud Gateway) se refieren a políticas que están estrechamente vinculadas con el negocio y brindan una única estrategia de nivel de dominio comercial, como el gobierno del servicio y la autenticación de identidad. Es decir, la puerta de enlace de tráfico es responsable de la programación del tráfico de norte a sur y la protección de la seguridad, y la puerta de enlace de microservicio es responsable de la programación del tráfico de este a oeste y la gobernanza del servicio.

En segundo lugar, el despliegue de la puerta de enlace de servicio:

1. Comparación y selección de puertas de enlace principales:

 (1) Puerta de enlace Kong: Kong tiene un rendimiento muy bueno y es muy adecuado para puertas de enlace de tráfico, pero no se recomienda usar Kong para puertas de enlace comerciales para sistemas complejos, principalmente debido a consideraciones de ingeniería.

(2) puerta de enlace Zuul1.x: Zuul 1.0 tiene una gran experiencia en el aterrizaje, pero tiene un rendimiento deficiente y se basa en el bloqueo síncrono de E/S. Es adecuado para arquitecturas pequeñas y medianas, pero no adecuado para escenarios con alto tráfico concurrente, porque es fácil provocar el agotamiento de subprocesos, lo que provoca que se rechacen las solicitudes.

(3) puerta de enlace puerta de enlace: potente y rica en funciones y buen rendimiento, el punto de referencia oficial RPS (solicitudes por segundo) es 1,6 veces mayor que el de Zuul, y es muy compatible con el ecosistema SpringCloud. Programación de transmisión + soporte para asincronía es suficiente para el desarrollo lo ha elegido.

(4) Zuul 2.x: el rendimiento es similar al de la puerta de enlace. Se basa en el no bloqueo y admite conexiones largas, pero SpringCloud no tiene planes de integrar zuul2, y los componentes relacionados con Netflix han anunciado que han entrado en un período de mantenimiento, y la perspectiva es desconocida.

        En resumen, la puerta de enlace de la puerta de enlace es más adecuada para el proyecto SpringCloud y, desde la perspectiva de las tendencias de desarrollo, es inevitable que la puerta de enlace reemplace a zuul.

2. Construcción de la puerta de enlace Spring Cloud Gateway:

(1) Declare el número de versión de la dependencia:

	<properties>
		<spring-boot.version>2.3.2.RELEASE</spring-boot.version>
		<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
		<spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
	</properties>

	<!-- 只声明依赖,不引入依赖 -->
	<dependencyManagement>
		<dependencies>
			<!-- 声明springBoot版本 -->
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-dependencies</artifactId>
				<version>${spring-boot.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
			<!-- 声明springCloud版本 -->
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
			<!-- 声明 springCloud Alibaba 版本 -->
			<dependency>
				<groupId>com.alibaba.cloud</groupId>
				<artifactId>spring-cloud-alibaba-dependencies</artifactId>
				<version>${spring-cloud-alibaba.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

(2) Agregar dependencias:

<!-- 引入gateway网关 -->
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-gateway</artifactId>
	<exclusions>
        <exclusion>
			<groupId>org.springframework.boot</groupId>
        	<artifactId>spring-boot-starter-web</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Nota : asegúrese de excluir la dependencia spring-boot-starter-web, de lo contrario, el inicio informará un error

(3) Configure el nombre del proyecto y el puerto:

server:
  port: 9023
  servlet:
    context-path: /${spring.application.name}
spring:
  application:
    name: gateway

Bueno, el proyecto de puerta de enlace está completo, de hecho, se agrega dicha dependencia.La configuración detallada y la función se describen a continuación.

3. Descripción de los elementos de configuración de Spring Cloud Gateway:

        Antes de presentar los elementos de configuración de Spring Cloud Gateway, comprendamos varios términos básicos de Spring Cloud Gateway:

  • Predicado: Consulte la nueva función Predicado de Java8, que permite a los desarrolladores hacer coincidir cualquier contenido en las solicitudes HTTP, como encabezados de solicitud o parámetros de solicitud, y finalmente devuelve un valor booleano de acuerdo con el resultado de la coincidencia.
  • Ruta: consta de ID, URI de destino, conjunto de aserciones y conjunto de filtros. Si la afirmación agregada es verdadera, reenvíe a esta ruta.
  • Filtro: el contenido de las solicitudes y respuestas se puede modificar antes o después de que se devuelva la solicitud.

3.1 Ruta de enrutamiento:

        La ruta se compone principalmente de la identificación de la ruta, el uri de destino, el conjunto de aserciones y el conjunto de filtros, así que echemos un breve vistazo a lo que hacen estas propiedades.

(1) id: identificador de enrutamiento, nombre único y arbitrario (el valor predeterminado es uuid, que generalmente no se usa y debe personalizarse)

(2) uri: la dirección de destino a la que finalmente se reenvía la solicitud

(3) orden: prioridad de enrutamiento, cuanto menor sea el número, mayor será la prioridad

(4) predicados: la matriz de aserciones, es decir, la condición de juicio, si el valor de retorno es booleano, la solicitud se reenvía al servicio especificado por el atributo uri

(5) filtros: matriz de filtros, en el proceso de transmisión de solicitudes, realice algunas modificaciones a la solicitud

3.2 Afirmar predicado:

        El predicado proviene de la interfaz Java8. El predicado toma un parámetro de entrada y devuelve un resultado booleano. Esta interfaz contiene varios métodos predeterminados para combinar predicados en otra lógica compleja (p. ej.: Y, O, NO).

        El predicado se puede usar para verificar los parámetros de solicitud de la interfaz y para determinar si los datos nuevos y antiguos han cambiado y deben actualizarse. Spring Cloud Gateway tiene muchas predicciones integradas. El código fuente de estas predicciones se encuentra en el paquete org.springframework.cloud.gateway.handler.predicate. Puede leerlo si está interesado. Algunas afirmaciones integradas son las siguientes:

Las 11 afirmaciones anteriores no se describirán aquí. La documentación oficial es muy clara: https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/

Tomemos la última aserción de peso como ejemplo para presentar cómo configurarlo. La configuración es la siguiente:

spring:
  cloud:
    gateway:
      # 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
      routes:
        # 路由标识,要求唯一,名称任意
        - id: gateway-provider_1
		  # 请求最终被转发到的目标地址
          uri: http://localhost:9024
          # 设置断言
          predicates:
            # Path Route Predicate Factory 断言,满足 /gateway/provider/** 路径的请求都会被路由到 http://localhost:9024 这个uri中
            - Path=/gateway/provider/**
            # Weight Route Predicate Factory 断言,同一分组按照权重进行分配流量,这里分配了80%
            # 第一个group1是分组名,第二个参数是权重
            - Weight=group1, 8
          # 配置过滤器(局部)
          filters:
            # StripPrefix:去除原始请求路径中的前1级路径,即/gateway
            - StripPrefix=1            
            
        - id: gateway-provider_2
          uri: http://localhost:9025
          # 设置断言
          predicates:
            - Path=/gateway/provider/**
            # Weight Route Predicate Factory,同一分组按照权重进行分配流量,这里分配了20%
            - Weight=group1, 2
		  # 配置过滤器(局部)
          filters:
            # StripPrefix:去除原始请求路径中的前1级路径,即/gateway
            - StripPrefix=1            

        Los nombres de aserciones en Spring Cloud Gateway están estandarizados en el formato: "xxx + RoutePredicateFactory", como la aserción de peso WeightRoutePredicateFactory, luego toman directamente el "Peso" anterior al configurar.

        Si el reenvío de ruta coincide con dos o más, se reenvía de acuerdo con el orden de configuración y la ruta se configura arriba: "Path=/gateway/provider/**", si no se configura ningún peso, se debe reenviar a " http ://localhost:9024 ”, pero como la configuración está configurada con pesos y la misma agrupación, el tráfico se distribuye según la relación de peso.

3.3, filtro de filtro:

Ciclo de vida del filtro Gateway:

  • PRE : este filtro se invoca antes de enrutar la solicitud. Podemos usar este filtro para implementar la autenticación, seleccionar los microservicios solicitados en el clúster, registrar información de depuración y más.
  • POST : este filtro se ejecuta después de enrutar al microservicio. Dichos filtros se pueden usar para agregar encabezados HTTP estándar a las respuestas, recopilar estadísticas y métricas, enviar respuestas de microservicios a clientes y más.

Los filtros de puerta de enlace se pueden dividir en dos tipos desde el ámbito de actuación:

  • GatewayFilter : aplicado a una sola ruta o una ruta agrupada (debe configurarse en el archivo de configuración)
  • GlobalFilter : aplicado a todas las rutas (no se requiere configuración, tiene efecto globalmente)

(1) Filtro local GatewayFilter:

        Hay muchos filtros locales integrados en Spring Cloud Gateway, como se muestra a continuación:

         El filtro local debe configurarse en la ruta especificada para que surta efecto y no lo hace de forma predeterminada. Tome el filtro "AddResponseHeaderGatewayFilterFactory" como ejemplo, agregue Header a la respuesta original, la configuración es la siguiente:

spring:
  cloud:
    gateway:
      routes:
        - id: gateway-provider_1
          uri: http://localhost:9024
          predicates:
            - Path=/gateway/provider/**
          # 配置过滤器(局部)
          filters:
            - AddResponseHeader=X-Response-Foo, Bar
            # StripPrefix:去除原始请求路径中的前1级路径,即/gateway
            - StripPrefix=1   

El navegador solicita y encuentra que el par clave-valor X-Response-Foo=Bar ya existe en el encabezado de respuesta, como se muestra a continuación:

        En el ejemplo anterior, también usamos otro filtro local, StripPrefixGatewayFilterFactory, que se usa principalmente para truncar la ruta de la solicitud original. Cuando solicitamos localhost:9023/gateway/provider/test, la solicitud real se reenviará a http:/ /localhost:9024 y truncado a " http://localhost:9024/provider/test "

Nota : el nombre del filtro solo necesita escribir el prefijo, y el nombre del filtro debe ser "xxx + GatewayFilterFactory" (incluido el personalizado).

Para obtener más configuraciones de filtros, consulte la documentación oficial: https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/#gatewayfilter-factories

(2) Filtro local personalizado:

        Si bien los filtros incorporados pueden resolver muchos escenarios, es inevitable que se deban personalizar algunos requisitos especiales para un filtro. Veamos cómo personalizar los filtros locales.

/**
 * 名称必须是xxxGatewayFilterFactory形式
 * todo:模拟授权的验证,具体逻辑根据业务完善
 */
@Component
@Slf4j
public class AuthorizeGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizeGatewayFilterFactory.Config> {

    private static final String AUTHORIZE_TOKEN = "token";

    //构造函数,加载Config
    public AuthorizeGatewayFilterFactory() {
        //固定写法
        super(AuthorizeGatewayFilterFactory.Config.class);
        log.info("Loaded GatewayFilterFactory [Authorize]");
    }

    //读取配置文件中的参数 赋值到 配置类中
    @Override
    public List<String> shortcutFieldOrder() {
        //Config.enabled
        return Arrays.asList("enabled");
    }

    @Override
    public GatewayFilter apply(AuthorizeGatewayFilterFactory.Config config) {
        return (exchange, chain) -> {
            //判断是否开启授权验证
            if (!config.isEnabled()) {
                return chain.filter(exchange);
            }

            ServerHttpRequest request = exchange.getRequest();
            HttpHeaders headers = request.getHeaders();
            //从请求头中获取token
            String token = headers.getFirst(AUTHORIZE_TOKEN);
            if (token == null) {
                //从请求头参数中获取token
                token = request.getQueryParams().getFirst(AUTHORIZE_TOKEN);
            }

            ServerHttpResponse response = exchange.getResponse();
            //如果token为空,直接返回401,未授权
            if (StringUtils.isEmpty(token)) {
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                //处理完成,直接拦截,不再进行下去
                return response.setComplete();
            }
            /**
             * todo chain.filter(exchange) 之前的都是过滤器的前置处理
             *
             * chain.filter().then(
             *  过滤器的后置处理...........
             * )
             */
            //授权正常,继续下一个过滤器链的调用
            return chain.filter(exchange);
        };
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Config {
        // 控制是否开启认证
        private boolean enabled;
    }
}

El filtro local debe estar configurado en la ruta para que surta efecto, la configuración es la siguiente:

spring:
  cloud:
    gateway:
      routes:
        - id: gateway-provider_1
          uri: http://localhost:9024
          predicates:
            - Path=/gateway/provider/**
          ## 配置过滤器(局部)
          filters:
            - AddResponseHeader=X-Response-Foo, Bar
            ## AuthorizeGatewayFilterFactory自定义过滤器配置,值为true需要验证授权,false不需要
            - Authorize=true

En este momento, visite directamente: http://localhost:9023/gateway/provider/port , sin token, regrese como se muestra a continuación:

Los parámetros de la solicitud traen el token: http://localhost:9023/gateway/provider/port?token=abcdcdecd-ddcdeicd12 , regresa correctamente, como se muestra a continuación:

        La AuthorizeGatewayFilterFactory anterior solo implica el preprocesamiento del filtro, y el posprocesamiento se realiza en el método then() en chain.filter().then(). Para obtener más información, consulte TimeGatewayFilterFactory en el código fuente del proyecto, el el código no lo vuelve a pegar, como se muestra a continuación:

(3) filtro global GlobalFilter:

        Los filtros globales se aplican a todas las rutas sin la configuración del desarrollador. Spring Cloud Gateway también tiene algunos filtros globales integrados, como se muestra a continuación:

        La función de GlobalFilter es en realidad la misma que la de GatewayFilter, pero el alcance de GlobalFilter es todas las configuraciones de enrutamiento, no vinculadas a la configuración de enrutamiento especificada. Múltiples GlobalFilters pueden especificar el orden de ejecución a través de los métodos @Order o getOrder() Cuanto menor sea el valor del pedido, mayor será la prioridad de ejecución.

        Tenga en cuenta que, dado que hay dos tipos de filtros, previo y posterior, si el filtro de tipo previo tiene un valor de pedido menor, entonces debe estar en la parte superior de la cadena de filtros previos, y si el filtro de tipo posterior tiene un valor de pedido menor, entonces debería estar en la parte superior de la cadena de prefiltro La capa inferior de la cadena de postfiltro. El diagrama esquemático es el siguiente:

(4) Personaliza el filtro global:

        Por supuesto, además de los filtros globales integrados, también se requieren filtros personalizados en el trabajo real. Vamos a presentar cómo personalizarlos. Simulamos la función de registro de acceso de Nginx para registrar la información relevante de cada solicitud. el código se muestra a continuación:

@Slf4j
@Component
@Order(value = Integer.MIN_VALUE)
public class AccessLogGlobalFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //filter的前置处理
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().pathWithinApplication().value();
        InetSocketAddress remoteAddress = request.getRemoteAddress();
        return chain
                //继续调用filter
                .filter(exchange)
                //filter的后置处理
                .then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();
            HttpStatus statusCode = response.getStatusCode();
            log.info("请求路径:{},远程IP地址:{},响应码:{}", path, remoteAddress, statusCode);
        }));
    }
}

Bueno, el filtro global no necesita configurarse en la ruta, se puede inyectar en el contenedor IOC para que tenga efecto globalmente.

En este punto, se emite una solicitud y la consola imprime la siguiente información:

Ruta de solicitud: /puerta de enlace/proveedor/puerto, dirección IP remota: /0:0:0:0:0:0:0:1:64114, código de respuesta: 200 OK

4. Gateway integra el registro de nacos para realizar el descubrimiento de servicios:

        No hay un registro integrado en la demostración anterior y cada configuración de enrutamiento especifica un uri de servicio fijo, como se muestra a continuación:

¿Qué hay de malo en hacer esto?

  • El servicio de puerta de enlace necesita saber el nombre de dominio o la dirección IP de todos los servicios.Además, una vez que se modifica el nombre de dominio o la dirección IP del servicio, se debe modificar la uri en la configuración de enrutamiento.
  • No se puede lograr el equilibrio de carga en el clúster de servicio

        Luego, podemos integrar el registro en este momento, de modo que la puerta de enlace pueda obtener automáticamente el uri del registro y lograr el equilibrio de carga. Aquí tomamos el registro de nacos como ejemplo para presentar

(1) Nuevas dependencias en el archivo pom:

<!--nacos注册中心-->
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

(2) Agregue la anotación @EnableDiscoveryClient a la clase de inicio para habilitar la función de registro, como se muestra a continuación:

 (3) Configure la dirección del registro de nacos:

nacos:
  namespace: 856a40d7-6548-4494-bdb9-c44491865f63
  url: 120.76.129.106:80
spring:
  cloud:
    nacos:
      discovery:
      	server-addr: ${nacos.url}
        namespace: ${nacos.namespace}
        register-enabled: true

(4) Configuración de enrutamiento del servicio:

spring:
  cloud:
    gateway:
      routes:
        - id: gateway-provider_1
          ## 使用了lb形式,从注册中心负载均衡的获取uri
          uri: lb://gateway-provider
          ## 配置断言
          predicates:
            - Path=/gateway/provider/**
          filters:
            - AddResponseHeader=X-Response-Foo, Bar

La única diferencia en la configuración de enrutamiento es el uri de la ruta, en el formato: lb://service-name, que es una forma fija de escribir:

  • lb: formato fijo, que se refiere a obtener microservicios por nombre de nacos y seguir la estrategia de balanceo de carga
  • service-name: el nombre del servicio del registro de nacos, que no tiene la forma de una dirección IP

        ¿Por qué se puede habilitar el equilibrio de carga especificando lb? Como se mencionó anteriormente, el filtro global LoadBalancerClientFilter es responsable del direccionamiento de enrutamiento y el equilibrio de carga. Puede ver el siguiente código fuente:

(5) Habilite la configuración de enrutamiento automático de la puerta de enlace:

        Con el desarrollo continuo de la arquitectura de nuestro sistema, la cantidad de microservicios en el sistema definitivamente aumentará. Es imposible para nosotros configurar una nueva regla de enrutamiento en la puerta de enlace cada vez que agregamos un servicio. Dichos costos de mantenimiento son muy altos, especialmente en En muchos casos, llevaremos un identificador de enrutamiento en la ruta de la solicitud para facilitar el reenvío, y este identificador de enrutamiento generalmente es el nombre del servicio en el registro, por lo que aquí es donde podemos habilitar la función de enrutamiento automático de Spring Cloud Gateway. puerta de enlace Cree automáticamente un enrutador para cada servicio en función del nombre del servicio del registro y reenvíe la ruta de solicitud que comienza con el nombre del servicio al servicio correspondiente. La configuración es la siguiente:

# enabled:默认为false,设置为true表明spring cloud gateway开启服务发现和路由的功能,网关自动根据注册中心的服务名为每个服务创建一个router,将以服务名开头的请求路径转发到对应的服务
spring.cloud.gateway.discovery.locator.enabled = true
# lowerCaseServiceId:启动 locator.enabled=true 自动路由时,路由的路径默认会使用大写ID,若想要使用小写ID,可将lowerCaseServiceId设置为true
spring.cloud.gateway.discovery.locator.lower-case-service-id = true

        Cabe señalar aquí que dado que nuestro proyecto de puerta de enlace está configurado con la propiedad server.servlet.context-path, esto causará el problema de falla de enrutamiento automático, por lo que debemos realizar las siguientes dos modificaciones:

# 重写过滤链,解决项目设置了 server.servlet.context-path 导致 locator.enabled=true 默认路由策略404的问题
spring.cloud.gateway.discovery.locator.filters[0] = PreserveHostHeader
@Configuration
public class GatewayConfig
{
    @Value ("${server.servlet.context-path}")
    private String prefix;

    /**
     * 过滤 server.servlet.context-path 属性配置的项目路径,防止对后续路由策略产生影响,因为 gateway 网关不支持 servlet
     */
    @Bean
    @Order (-1)
    public WebFilter apiPrefixFilter()
    {
        return (exchange, chain) ->
        {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getRawPath();

            path = path.startsWith(prefix) ? path.replaceFirst(prefix, "") : path;
            ServerHttpRequest newRequest = request.mutate().path(path).build();

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

        Hasta ahora, hemos habilitado la función de enrutamiento automático de Spring Cloud Gateway.La puerta de enlace crea automáticamente un enrutador para cada servicio en función del nombre del servicio del registro y reenvía la ruta de solicitud que comienza con el nombre del servicio al servicio correspondiente. Suponiendo que el nombre de servicio de nuestro proveedor de servicios en el registro de nacos es "proveedor de puerta de enlace", solo necesitamos acceder a " http://localhost:9023/gateway/gateway-provider/test " para reenviar con éxito la solicitud más allá

5. Gateway integra Apollo para lograr una configuración de enrutamiento dinámico:

        Todos los ejemplos anteriores escriben una serie de configuraciones de la puerta de enlace en el archivo de configuración del proyecto. Una vez que se cambia la política de enrutamiento, el proyecto debe reiniciarse, por lo que el costo de mantenimiento es muy alto, especialmente porque la puerta de enlace de servicio es el punto central. del sistema, una vez que ocurre un problema en el reinicio, afectará a la La superficie será muy grande. Por lo tanto, almacenamos la configuración de la puerta de enlace en el centro de configuración, para que el centro de configuración lo gestione de manera uniforme. Una vez que la ruta cambia , solo necesita ser modificado en el centro de configuración para reducir riesgos y fallar en tiempo real. Esta sección toma el centro de configuración de Apollo como ejemplo para presentar la siguiente implementación de configuración de enrutamiento dinámico:

(1) Agregar dependencias del centro de configuración de Apollo:

<!-- Apollo 统一配置中心 -->
<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.7.0</version>
</dependency>

(2) Agregue la clase de actualización del oyente de cambio de ruta de Apollo:

import com.ctrip.framework.apollo.enums.PropertyChangeType;
import com.ctrip.framework.apollo.model.ConfigChange;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;

/**
 * Apollo路由更改监听刷新
 */
@Configuration
public class GatewayPropertRefresher implements ApplicationContextAware, ApplicationEventPublisherAware
{
    private static final Logger logger = LoggerFactory.getLogger(GatewayPropertRefresher.class);

    private static final String ID_PATTERN = "spring\\.cloud\\.gateway\\.routes\\[\\d+\\]\\.id";

    private static final String DEFAULT_FILTER_PATTERN = "spring\\.cloud\\.gateway\\.default-filters\\[\\d+\\]\\.name";


    private ApplicationContext applicationContext;

    private ApplicationEventPublisher publisher;

    @Autowired
    private  GatewayProperties gatewayProperties;

    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }


    /**
     * 监听路由修改
     */
    @ApolloConfigChangeListener(interestedKeyPrefixes = "spring.cloud.gateway.")
    public void onChange(ConfigChangeEvent changeEvent)
    {
        refreshGatewayProperties(changeEvent);
    }

    /**
     * 刷新路由信息
     */
    private void refreshGatewayProperties(ConfigChangeEvent changeEvent)
    {
        logger.info("gateway网关配置 刷新开始!");

        preDestroyGatewayProperties(changeEvent);
        //更新配置
        this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
        //更新路由
        refreshGatewayRouteDefinition();

        logger.info("gateway网关配置 刷新完成!");
    }

    /***
     * GatewayProperties没有@PreDestroy和destroy方法
     * org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#rebind(java.lang.String)中destroyBean时不会销毁当前对象
     * 如果把spring.cloud.gateway.前缀的配置项全部删除(例如需要动态删除最后一个路由的场景),initializeBean时也无法创建新的bean,则return当前bean
     * 若仍保留有spring.cloud.gateway.routes[n]或spring.cloud.gateway.default-filters[n]等配置,initializeBean时会注入新的属性替换已有的bean
     * 这个方法提供了类似@PreDestroy的操作,根据配置文件的实际情况把org.springframework.cloud.gateway.config.GatewayProperties#routes
     * 和org.springframework.cloud.gateway.config.GatewayProperties#defaultFilters两个集合清空
     */
    private synchronized void preDestroyGatewayProperties(ConfigChangeEvent changeEvent)
    {
        logger.info("Pre Destroy GatewayProperties 操作开始!");

        final boolean needClearRoutes = this.checkNeedClear(changeEvent, ID_PATTERN, this.gatewayProperties.getRoutes().size());
        if (needClearRoutes)
        {
            this.gatewayProperties.setRoutes(new ArrayList());
        }

        final boolean needClearDefaultFilters = this.checkNeedClear(changeEvent, DEFAULT_FILTER_PATTERN, this.gatewayProperties.getDefaultFilters().size());
        if (needClearDefaultFilters)
        {
            this.gatewayProperties.setRoutes(new ArrayList());
        }

        logger.info("Pre Destroy GatewayProperties 操作完成!");
    }


    private void refreshGatewayRouteDefinition()
    {
        logger.info("Refreshing Gateway RouteDefinition 操作开始!");

        this.publisher.publishEvent(new RefreshRoutesEvent(this));

        logger.info("Gateway RouteDefinition refreshed 操作完成!");
    }

    /***
     * 根据changeEvent和定义的pattern匹配key,如果所有对应PropertyChangeType为DELETED则需要清空GatewayProperties里相关集合
     */
    private boolean checkNeedClear(ConfigChangeEvent changeEvent, String pattern, int existSize) {

        return changeEvent.changedKeys().stream().filter(key -> key.matches(pattern)).filter(key ->
        {
            ConfigChange change = changeEvent.getChange(key);
            return PropertyChangeType.DELETED.equals(change.getChangeType());
        }).count() == existSize;
    }
}

(3) Exponer el punto final punto final:

# 暴露endpoint端点,暴露路由信息,有获取所有路由、刷新路由、查看单个路由、删除路由等方法
management.endpoints.web.exposure.include = *
management.endpoint.health.show-details = always

        Hasta ahora, hemos completado el centro de configuración Apollo de integración de puerta de enlace para lograr una configuración de enrutamiento dinámico. Una vez que cambia el enrutamiento, solo necesita modificarse en el centro de configuración para ser monitoreado e invalidado en tiempo real.

Si ha integrado Nacos o MySQL para la configuración de enrutamiento dinámico, puede consultar los siguientes dos artículos:

(1) Integre Nacos para la configuración de enrutamiento dinámico: https://www.cnblogs.com/jian0110/p/12862569.html

(2) Integrar MySQL para la configuración de enrutamiento dinámico: https://blog.csdn.net/qq_42714869/article/details/92794911

6. Personalice el controlador de excepciones global:

        A través de la prueba anterior, podemos ver un fenómeno: una vez que el microservicio enrutado está fuera de línea o desconectado, Spring Cloud Gateway devuelve directamente una página de error, como se muestra a continuación:

        Obviamente, este tipo de información de excepción no es amigable, y la información de excepción devuelta debe personalizarse en la arquitectura de separación de front-end y back-end. Los servicios tradicionales de Spring Boot usan @ControllerAdvice para ajustar el manejo de excepciones globales, pero debido a que el servicio está fuera de línea, la solicitud no llega. Por lo tanto, se debe personalizar una capa de manejo de excepciones globales en la puerta de enlace, para interactuar con el cliente de manera más amigable.

        Spring Cloud Gateway proporciona una variedad de métodos de procesamiento global, solo uno de los cuales se presenta hoy, y la implementación es bastante elegante:

        Cree una clase GlobalErrorExceptionHandler directamente, implemente ErrorWebExceptionHandler y reescriba el método handle. El código es el siguiente:

/**
 * 用于网关的全局异常处理
 * @Order(-1):优先级一定要比ResponseStatusExceptionHandler低
 */
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalErrorExceptionHandler implements ErrorWebExceptionHandler {

 private final ObjectMapper objectMapper;

 @SuppressWarnings({"rawtypes", "unchecked", "NullableProblems"})
 @Override
 public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
  ServerHttpResponse response = exchange.getResponse();
  if (response.isCommitted()) {
   return Mono.error(ex);
  }

  // JOSN格式返回
  response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
  if (ex instanceof ResponseStatusException) {
   response.setStatusCode(((ResponseStatusException) ex).getStatus());
  }

  return response.writeWith(Mono.fromSupplier(() -> {
   DataBufferFactory bufferFactory = response.bufferFactory();
   try {
    //todo 返回响应结果,根据业务需求,自己定制
    CommonResponse resultMsg = new CommonResponse("500",ex.getMessage(),null);
    return bufferFactory.wrap(objectMapper.writeValueAsBytes(resultMsg));
   }
   catch (JsonProcessingException e) {
    log.error("Error writing response", ex);
    return bufferFactory.wrap(new byte[0]);
   }
  }));
 }
}

        Bueno, se ha personalizado el manejo global de excepciones, vamos a probarlo, en este momento los datos JSON se devuelven normalmente (el estilo de JSON se personaliza de acuerdo a las necesidades de la arquitectura), como se muestra en la siguiente figura:

Artículo de referencia: Spring Cloud Gateway mata 10 preguntas?

Supongo que te gusta

Origin blog.csdn.net/a745233700/article/details/122917167
Recomendado
Clasificación