Explicación detallada del uso de Feign en proyectos reales.

Introducción

En nuestro estudio diario, simplemente sabemos cómo llamar a la interfaz de simulación o realizar una degradación del servicio; sin embargo, cuando utilizamos la simulación en proyectos de nivel empresarial, enfrentaremos los siguientes problemas:

  1. ¿Cómo se debe proporcionar al cliente Feign?
  2. ¿Debería empaquetarse la interfaz llamada por Feign?
  3. ¿Cómo captura Feign las excepciones comerciales en el lado de la producción empresarial?

¿Cómo se debe proporcionar un cliente de Feign?

¿Cómo proporcionar una interfaz fingida con el mundo exterior?
analizar:

  1. El lado del consumidor necesita hacer referencia a estas interfaces fingidas. Si la interfaz fingida está escrita directamente en el proyecto del consumidor, si otro también necesita la interfaz fingida, ¿tendrá que escribirse nuevamente? Naturalmente, consideraremos hacer que la interfaz de simulación sea independiente. Quien necesite la interfaz fingida puede agregar las dependencias correspondientes.

  2. La interfaz de simulación contiene objetos de entidad. Generalmente estas entidades están en el proveedor. Al transformar a través de la interfaz fingida, necesitamos extraer las clases de entidad utilizadas en el controlador. Se puede procesar de las dos maneras siguientes
    : El método 1 extrae la clase de entidad y la coloca en un módulo independiente. Las interfaces proveedor y fingir dependen del módulo de clase de entidad respectivamente. El método 2
    coloca la clase de entidad en el módulo de la interfaz fingir. y el proveedor depende de este módulo fingido
    Proyecto Hay muchos casos que se manejan de acuerdo con el método uno, lo que no causará dependencia del código que no necesita ser utilizado;

    Insertar descripción de la imagen aquí

2. ¿Debería empaquetarse la interfaz llamada por Feign?

2.1.Descripción del problema

En un proyecto de separación plana de front-end y back-end, cuando el back-end devuelve datos de la interfaz al front-end, el formato de retorno generalmente estará unificado; nuestro Controlador se verá básicamente así:

    @GetMapping("getTest")
    public Result<TestVO> getTest() {
    
    
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        return Result.success(testVO);
    

La definición de la interfaz de Feign debe ser coherente con la clase de implementación;

Insertar descripción de la imagen aquí
Entonces, cuando usamos la interfaz simulada de este método, la situación es así.

    @GetMapping("getContent")
    public Result<String> getContent() {
    
    
        String content=null;
        Result<TestVO> test = commentRestApi.getTest();
        if (test.isSuccess()) {
    
    
            TestVO data = test.getData();
             content = data.getContent();
        }else {
    
    
            throw new  RuntimeException(test.getMessage());
        }
        
        return Result.success(content);
    }

Aquí primero debemos obtener la clase de empaquetado Resultado y luego analizar el resultado devuelto en un objeto TestVO específico mediante juicio. Obviamente, este código tiene dos problemas:

  • Cada interfaz del Controlador necesita usar manualmente Result.success para empaquetar los resultados.
  • Cuando se llama a Feign, es necesario descomprimirlo de la clase de empaquetado en el objeto de entidad requerido.

Hay muchas, muchas interfaces en el proyecto, ¿no es demasiado inútil realizar este tipo de operaciones constantemente? ! ! Sin duda, añade una carga de desarrollo innecesaria.

2.2.Resolución de problemas

El objetivo de optimización también es muy claro:

  • Cuando lo llamamos a través de Feign, obtenemos el objeto de entidad directamente sin desensamblaje adicional.
  • Cuando el front-end llama directamente a través de la puerta de enlace, se devuelve un cuerpo de paquete unificado.

Aquí podemos usar ResponseBodyAdvice para lograr esto. Al mejorar el cuerpo de retorno del Controlador, si se reconoce una llamada de Feign, el objeto se devolverá directamente. De lo contrario, se nos agregará una estructura de empaquetado unificada. ( SpringBoot encapsula uniformemente los resultados devueltos por la capa del controlador )

Nueva pregunta: ¿Cómo identificar si es una llamada de Feign o una llamada directa desde el gateway?

Implementado en base a anotaciones personalizadas e implementado en base al interceptor Feign.

  • Implementado en base a anotaciones personalizadas.

    Personalice una anotación, como @ResponseNotIntercept, y marque la interfaz de Feign con esta anotación, de modo que pueda usar esta anotación para hacer coincidir cuando use la coincidencia de ResponseBodyAdvice.
    Insertar descripción de la imagen aquí
    Sin embargo, este método tiene un inconveniente, es decir, el front-end y fingir no se pueden compartir. Por ejemplo, se puede llamar a una interfaz usuario/get/{id} a través de fingir o directamente a través de la puerta de enlace. Para usar este método, usted Necesito escribir 2 interfaces con rutas diferentes.

  • ​Implementado en base al interceptor Feign​

    Para llamadas de Feign, agregue un identificador especial al interceptor de Feign. Al convertir el objeto, si se descubre que es una llamada de Feign, el objeto se devolverá directamente.

Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí

Los pasos de implementación específicos del segundo método:

  1. Agregue un encabezado de solicitud específico T_REQUEST_ID a la solicitud de simulación en el interceptor de simulación

/**
 * @ClassName: OpenFeignConfig Feign拦截器
 * @Description: 对于Feign的调用,在请求头中加上特殊标识
 * @Author: wang xiao le
 * @Date: 2023/08/25 23:13
 **/
@ConditionalOnClass(Feign.class)
@Configuration
public class OpenFeignConfig implements RequestInterceptor {
    
    

    /**
     * Feign请求唯一标识
     */
    public static final String T_REQUEST_ID = "T_REQUEST_ID";


    /**
     * get请求标头
     *
     * @param request 请求
     * @return {@link Map }<{@link String }, {@link String }>
     * @Author wxl
     * @Date 2023-08-27
     **/
    private Map<String, String> getRequestHeaders(HttpServletRequest request) {
    
    
        Map<String, String> map = new HashMap<>(16);
        Enumeration headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
    
    
            String key = (String) headerNames.nextElement();
            String value = request.getHeader(key);
            map.put(key, value);
        }
        return map;
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
    
    
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (null != attributes) {
    
    
            HttpServletRequest request = attributes.getRequest();
            Map<String, String> headers = getRequestHeaders(request);

            // 传递所有请求头,防止部分丢失
            for (Map.Entry<String, String> entry : headers.entrySet()) {
    
    
                requestTemplate.header(entry.getKey(), entry.getValue());
            }

            // 微服务之间传递的唯一标识,区分大小写所以通过httpServletRequest获取
            if (request.getHeader(T_REQUEST_ID) == null) {
    
    
                String sid = String.valueOf(UUID.randomUUID());
                requestTemplate.header(T_REQUEST_ID, sid);
            }

        }
    }


}

  1. Personalice CommonResponseResult e implemente ResponseBodyAdvice
/**
 * 如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
 *
 * @RestControllerAdvice(basePackages = "com.wxl52d41")
 * @ClassName: CommonResponseResult
 * @Description: controller返回结果统一封装
 * @Author wxl
 * @Date 2023-08-27
 * @Version 1.0.0
 **/
@RestControllerAdvice
public class CommonResponseResult implements ResponseBodyAdvice<Object> {
    
    
    /**
     * 支持注解@ResponseNotIntercept,使某些方法无需使用Result封装
     *
     * @param returnType    返回类型
     * @param converterType 选择的转换器类型
     * @return true 时会执行beforeBodyWrite方法,false时直接返回给前端
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
    
    
        if (returnType.getDeclaringClass().isAnnotationPresent(ResponseNotIntercept.class)) {
    
    
            //若在类中加了@ResponseNotIntercept 则该类中的方法不用做统一的拦截
            return false;
        }
        if (returnType.getMethod().isAnnotationPresent(ResponseNotIntercept.class)) {
    
    
            //若方法上加了@ResponseNotIntercept 则该方法不用做统一的拦截
            return false;
        }
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
    
    

        if (request.getHeaders().containsKey(OpenFeignConfig.T_REQUEST_ID)) {
    
    
            //Feign请求时通过拦截器设置请求头,如果是Feign请求则直接返回实体对象
            return body;
        }
        if (body instanceof Result) {
    
    
            // 提供一定的灵活度,如果body已经被包装了,就不进行包装
            return body;
        }
        if (body instanceof String) {
    
    
            //解决返回值为字符串时,不能正常包装
            return JSON.toJSONString(Result.success(body));
        }
        return Result.success(body);
    }
}

  1. Modifique la interfaz de backend del proveedor para devolver el objeto y la interfaz de simulación
    . Si es una solicitud de simulación, no se realizará ninguna conversión; de lo contrario, se empaquetará por resultado.
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        return testVO;
    }

Insertar descripción de la imagen aquí

  1. Modifique la lógica de llamada simulada en el módulo del consumidor.
    No es necesario devolver los ResultData encapsulados en la interfaz y la mejora automática se logra a través de ResponseBodyAdvice.
   @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO one = commentRestApi.getOne();
        return one;
    }

  1. La prueba
    se realiza del lado del consumidor. Se descubre que los métodos devueltos al llamar a la interfaz simulada en la consola no están encapsulados de manera uniforme.
    Insertar descripción de la imagen aquí

Llame al método de la capa del proveedor directamente a través del cartero. Los métodos de descubrimiento están unificados y encapsulados.

Insertar descripción de la imagen aquí

En circunstancias normales, nuestro objetivo de optimización se logra: el objeto de entidad se devuelve directamente a través de la llamada Feign y el paquete unificado se devuelve a través de la llamada de puerta de enlace. Parece perfecto, pero en realidad es terrible, lo que lleva a la tercera pregunta: ¿cómo maneja Feign las excepciones?

¿Cómo captura SanFeign las anomalías empresariales en el lado de la producción empresarial?

3.1.Análisis

El productor realizará una verificación de reglas comerciales en los métodos de interfaz proporcionados. Se lanzarán excepciones comerciales para solicitudes de llamada que no cumplan con las reglas comerciales. En circunstancias normales, habrá un controlador de excepciones global en el proyecto, que capturará la excepción comercial BusinessException. y lo encapsula en un cuerpo de paquete unificado y lo devuelve a la persona que llama. Ahora simulemos este escenario empresarial:

  1. El productor genera una excepción comercial
    y el nombre en la empresa simulada está vacío.
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        if (true) {
    
    
            throw new BusinessException(ResultEnum.VALIDATE_FAILED.getCode(), "名称为空");
        }
        return testVO;
    }

  1. El interceptor de excepciones global captura excepciones comerciales
   /**
     * 捕获 自定 异常
     */
    @ExceptionHandler({
    
    BusinessException.class}
    public Result<?> handleBusinessException(BusinessException ex) {
    
    
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getCode(),ex.getMessage());
    }
  1. El lado del consumidor llama a la interfaz fingida anormal.
    @Resource
    CommentRestApi commentRestApi;


    @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO one = commentRestApi.getOne();
        System.out.println("one = " + one);
        return one;
    }

3.2.Fingir no puede detectar la excepción

  1. Resultados de la observación:
    llame al método getOne () en el consumidor y descubra que no hay ninguna excepción en la información devuelta. Todos los campos del objeto en los datos están configurados en nulo, de la siguiente manera: vea el registro del lado del proveedor y, de hecho, genere un registro personalizado
    Insertar descripción de la imagen aquí
    . excepción:
    Insertar descripción de la imagen aquí
    establezca el nivel de registro de Feign en COMPLETO para ver los resultados devueltos:
    @Bean
    Logger.Level feginLoggerLevel(){
    
    
        return Logger.Level.FULL;
    }

Insertar descripción de la imagen aquí
En el registro, podemos ver que Feign realmente obtuvo el resultado del objeto unificado convertido por el controlador de excepciones global, y el código de respuesta es 200, que es una respuesta normal. Cuando el consumidor acepta el objeto como TestVO, los atributos no se pueden convertir y todos se tratan como valores NULL.

Obviamente, esto no se ajusta a nuestra lógica comercial normal. Deberíamos devolver directamente la excepción lanzada por el productor. Entonces, ¿cómo lidiar con eso?

Es muy simple: solo necesitamos establecer un código de respuesta distinto de 200 para la excepción comercial en el interceptor de excepciones global , como por ejemplo:
Insertar descripción de la imagen aquí

    /**
     * 捕获 自定 异常
     */
    @ExceptionHandler({
    
    BusinessException.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public Result<?> handleBusinessException(BusinessException ex) {
    
    
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getCode(),ex.getMessage());
    }

De esta manera, el consumidor normalmente puede detectar la excepción comercial lanzada por el productor, como se muestra en la siguiente figura:Insertar descripción de la imagen aquí

3.3 Las excepciones se encapsulan adicionalmente

Aunque se puede obtener la excepción, Feign la encapsula nuevamente en función de la excepción comercial después de detectar la excepción.

PorqueCuando el resultado de la llamada fingida es un código de respuesta distinto de 200, se activa el análisis de excepciones de Feign, que lo envolverá en FeignException, es decir, lo envolverá nuevamente según nuestra excepción comercial.

Puede poner un punto de interrupción en el método feign.codec.ErrorDecoder#decode() para observar los resultados de la ejecución, de la siguiente manera:
Insertar descripción de la imagen aquí
Obviamente, no necesitamos esta excepción empaquetada, debemos capturar directamente el negocio del productor. La excepción se lanza directamente al interfaz, entonces, ¿cómo solucionar esto?

3.4.Solución

Es muy simple. Solo necesitamos reescribir el analizador de excepciones de Feign, volver a implementar la lógica de decodificación y devolver una BusinessException normal. ¡Entonces el interceptor de excepciones global capturará la BusinessException! ( Se siente un poco como muñecos nidos infinitos)

El código se muestra a continuación:

  1. Reescribir el analizador de excepciones Feign
/**
 * @ClassName: OpenFeignErrorDecoder
 * @Description: 解决Feign的异常包装,统一返回结果
 * @Author wxl
 * @Date 2023-08-26
 * @Version 1.0.0
 **/
@Configuration
public class OpenFeignErrorDecoder implements ErrorDecoder {
    
    
    /**
     * Feign异常解析
     *
     * @param methodKey 方法名
     * @param response  响应体
     * @return {@link Exception }
     * @Author wxl
     * @Date 2023-08-26
     **/
    @SneakyThrows
    @Override
    public Exception decode(String methodKey, Response response) {
    
    
        //获取数据
        String body = Util.toString(response.body().asReader(Charset.defaultCharset()));
        Result<?> result = JSON.parseObject(body, Result.class);
        if (!result.isSuccess()) {
    
    
            return new BusinessException(result.getStatus(), result.getMessage());
        }
        return new BusinessException(500, "Feign client 调用异常");
    }
    
}

  1. La información de excepción generada al llamar nuevamente a la capa de proveedor
    puede ser capturada por la capa de consumidor y procesada en una excepción personalizada a través del analizador de excepciones personalizado, y ya no está envuelta por la excepción simulada predeterminada; la excepción personalizada lanzada se devuelve uniformemente para el procesamiento de encapsulación. .Insertar descripción de la imagen aquíInsertar descripción de la imagen aquí
    Insertar descripción de la imagen aquí

Código fuente del caso

Cinta transportadora del código fuente del caso

Supongo que te gusta

Origin blog.csdn.net/weixin_43811057/article/details/132522402
Recomendado
Clasificación