실제 프로젝트에서 Feign을 활용하는 방법에 대한 자세한 설명

소개

일상적인 연구에서는 feign 인터페이스를 호출하는 방법이나 서비스 다운그레이드를 수행하는 방법만 알고 있지만 엔터프라이즈 수준 프로젝트에서 feign을 사용할 경우 다음과 같은 문제에 직면하게 됩니다.

  1. Feign 클라이언트는 어떻게 제공되어야 하나요?
  2. Feign이 호출하는 인터페이스를 패키징해야 합니까?
  3. Feign은 비즈니스 생산 측면에서 비즈니스 예외를 어떻게 포착합니까?

Feign 클라이언트는 어떻게 제공되어야 하나요?

외부 세계에 가짜 인터페이스를 제공하는 방법은 무엇입니까?
분석하다:

  1. 소비자 측에서는 이러한 가짜 인터페이스를 참조해야 하는데, 가짜 인터페이스가 소비자 프로젝트에서 직접 작성된 경우 다른 프로젝트에도 가짜 인터페이스가 필요한 경우 다시 작성해야 합니까? 당연히 우리는 가짜 인터페이스를 독립적으로 만드는 것을 고려할 것입니다. 가짜 인터페이스가 필요한 사람은 누구나 해당 종속성을 추가할 수 있습니다.

  2. feign 인터페이스에는 엔터티 개체가 포함되어 있습니다. 일반적으로 이러한 엔터티는 공급자에 있으며, feign 인터페이스를 통해 변환할 때 컨트롤러에서 사용되는 엔터티 클래스를 추출해야 합니다. 방법 1은
    엔터티 클래스를 추출하여 독립된 모듈에 넣는 방법 1은 Provider 인터페이스와 feign 인터페이스가 각각 엔터티 클래스 모듈에 의존하고, 방법
    2는 엔터티 클래스를 feign 인터페이스의 모듈에 넣는 방법이다. 공급자는 이 가짜 모듈에 의존합니다.
    프로젝트 첫 번째 방법에 따라 처리되는 경우가 많으며, 이는 사용할 필요가 없는 코드에 대한 종속성을 유발하지 않습니다.

    여기에 이미지 설명을 삽입하세요.

2. Feign이 호출하는 인터페이스를 패키징해야 하나요?

2.1.문제 설명

플랫한 프런트엔드와 백엔드 분리 프로젝트에서 백엔드가 인터페이스 데이터를 프런트엔드로 반환할 때 반환 형식은 일반적으로 통합됩니다. 컨트롤러는 기본적으로 다음과 같습니다.

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

Feign의 인터페이스 정의는 구현 클래스와 일치해야 합니다.

여기에 이미지 설명을 삽입하세요.
그래서 이 방법의 feign 인터페이스를 사용하면 상황은 이렇습니다.

    @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);
    }

여기서는 먼저 Result 패키징 클래스를 얻은 다음 판단을 통해 반환된 결과를 특정 TestVO 객체로 구문 분석해야 합니다.분명히 이 코드에는 두 가지 문제가 있습니다.

  • 각 컨트롤러 인터페이스는 결과를 패키징하기 위해 Result.success를 수동으로 사용해야 합니다.
  • Feign이 호출되면 패키징 클래스에서 필수 엔터티 개체로 압축을 풀어야 합니다.

프로젝트에는 수많은 인터페이스가 있는데, 이런 작업을 지속적으로 하는 것은 너무 쓸모없는 것 아닌가요? ! ! 이는 의심할 여지 없이 불필요한 개발 부담을 가중시킵니다.

2.2.문제 해결

최적화 목표도 매우 명확합니다.​

  • Feign을 통해 호출하면 별도의 어셈블리 해제 없이 엔터티 개체를 직접 가져옵니다.
  • 프런트 엔드가 게이트웨이를 통해 직접 호출하는 경우 통합 패키징 본문이 반환됩니다.

여기서는 ResponseBodyAdvice를 사용하여 이를 달성할 수 있는데, 컨트롤러 반환 본문을 강화하여 Feign의 호출이 인식되면 개체가 직접 반환되고, 그렇지 않으면 통일된 패키징 구조가 추가됩니다. ( SpringBoot는 컨트롤러 레이어에서 반환된 결과를 균일하게 캡슐화합니다 .)

새로운 질문: Feign에서 걸려온 전화인지 게이트웨이에서 직접 걸려온 전화인지 어떻게 식별할 수 있나요?

사용자 정의 주석을 기반으로 구현되고 Feign 인터셉터를 기반으로 구현됩니다.

  • 사용자 정의 주석을 기반으로 구현됨

    @ResponseNotIntercept와 같은 주석을 사용자 정의하고 이 주석으로 Feign의 인터페이스를 표시하면 ResponseBodyAdvice 일치를 사용할 때 이 주석을 사용하여 일치시킬 수 있습니다.
    여기에 이미지 설명을 삽입하세요.
    그러나 이 방법은 front end와 feign을 공유할 수 없다는 단점이 있는데, 예를 들어 user/get/{id} 인터페이스는 feign을 통해 호출하거나 게이트웨이를 통해 직접 호출할 수 있다. 서로 다른 경로를 가진 2개의 인터페이스를 작성해야 합니다.

  • ​Feign 인터셉터 기반으로 구현됨​

    Feign 호출의 경우 Feign 인터셉터에 특수 식별자를 추가하고, 객체 변환 시 Feign 호출인 것으로 확인되면 해당 객체를 직접 반환합니다.

여기에 이미지 설명을 삽입하세요.
여기에 이미지 설명을 삽입하세요.

두 번째 방법의 구체적인 구현 단계는 다음과 같습니다.

  1. 가짜 인터셉터의 가짜 요청에 특정 요청 헤더 T_REQUEST_ID를 추가합니다.

/**
 * @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. CommonResponseResult를 사용자 정의하고 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. 객체와 feign 인터페이스를 반환하도록 공급자 백엔드 인터페이스를 수정합니다
    . Feign 요청인 경우 변환이 수행되지 않고, 그렇지 않으면 Result로 패키징됩니다.
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        return testVO;
    }

여기에 이미지 설명을 삽입하세요.

  1. 소비자 모듈에서 가짜 호출 로직을 수정합니다.
    인터페이스에서 캡슐화된 ResultData를 반환할 필요가 없으며 ResponseBodyAdvice를 통해 자동 개선이 이루어집니다.
   @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO one = commentRestApi.getOne();
        return one;
    }

  1. 테스트는
    소비자 측에서 호출됩니다. 콘솔에서 feign 인터페이스를 호출하여 반환된 메서드가 균일하게 캡슐화되지 않은 것으로 나타났습니다.
    여기에 이미지 설명을 삽입하세요.

Postman을 통해 직접 공급자 계층 메서드를 호출합니다. 검색 방법은 통합되고 캡슐화됩니다.

여기에 이미지 설명을 삽입하세요.

정상적인 상황에서는 최적화 목표가 달성되며 Feign 호출을 통해 엔터티 개체가 직접 반환되고 게이트웨이 호출을 통해 통합 패키지가 반환됩니다. 완벽해 보이지만 실제로는 끔찍합니다. 세 번째 질문인 Feign은 예외를 어떻게 처리합니까?

SanFeign은 비즈니스 생산 측면에서 비즈니스 이상 현상을 어떻게 포착합니까?

3.1.분석

생산자는 제공된 인터페이스 메소드에 대해 비즈니스 규칙 검증을 수행합니다. 비즈니스 규칙을 준수하지 않는 호출 요청에 대해 비즈니스 예외가 발생합니다. 일반적인 상황에서는 프로젝트에 비즈니스 예외 BusinessException을 캡처하는 전역 예외 핸들러가 있습니다. , 이를 통합 패키지 본문으로 캡슐화하여 호출자에게 반환합니다. 이제 이 비즈니스 시나리오를 시뮬레이션해 보겠습니다.

  1. 생산자가 비즈니스 예외를 발생시키고
    시뮬레이션된 비즈니스의 이름이 비어 있습니다.
    /**
     * 对象返回值测试,是否能正常封装返回体
     */
    @GetMapping("getOne")
    public TestVO getOne() {
    
    
        TestVO testVO = new TestVO("1", "测试标题", "无内容", "小明");
        if (true) {
    
    
            throw new BusinessException(ResultEnum.VALIDATE_FAILED.getCode(), "名称为空");
        }
        return testVO;
    }

  1. 전역 예외 인터셉터는 비즈니스 예외를 캡처합니다.
   /**
     * 捕获 自定 异常
     */
    @ExceptionHandler({
    
    BusinessException.class}
    public Result<?> handleBusinessException(BusinessException ex) {
    
    
        log.error(ex.getMessage(), ex);
        return Result.failed(ex.getCode(),ex.getMessage());
    }
  1. 소비자 측에서는 비정상적인 가짜 인터페이스를 호출합니다.
    @Resource
    CommentRestApi commentRestApi;


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

3.2.가장 예외를 잡을 수 없다

  1. 관찰 결과:
    Consumer에서 getOne() 메소드를 호출하여 반환된 정보에 예외가 없음을 확인합니다. 데이터의 개체 필드는 다음과 같이 모두 null로 설정되어 있습니다. 공급자
    여기에 이미지 설명을 삽입하세요.
    측 로그를 보고 실제로 사용자 정의를 throw합니다. 예외:
    여기에 이미지 설명을 삽입하세요.
    반환 결과를 보려면 Feign의 로그 수준을 FULL로 설정하세요.
    @Bean
    Logger.Level feginLoggerLevel(){
    
    
        return Logger.Level.FULL;
    }

여기에 이미지 설명을 삽입하세요.
로그를 보면 Feign이 전역 예외 처리기에 의해 변환된 통합 객체 결과를 실제로 획득했으며 응답 코드는 200으로 정상적인 응답임을 알 수 있습니다. 소비자가 개체를 TestVO로 수락하면 속성은 변환될 수 없으며 모두 NULL 값으로 처리됩니다.

분명히 이것은 우리의 일반적인 비즈니스 로직에 맞지 않기 때문에 생산자가 던진 예외를 직접 반환해야 하는데 어떻게 처리해야 할까요?

매우 간단합니다. 전역 예외 인터셉터에서 비즈니스 예외에 대해 다음과 같이 200이 아닌 응답 코드만 설정 됩니다.
여기에 이미지 설명을 삽입하세요.

    /**
     * 捕获 自定 异常
     */
    @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());
    }

이러한 방식으로 소비자는 일반적으로 다음 그림과 같이 생산자가 던진 비즈니스 예외를 포착할 수 있습니다.여기에 이미지 설명을 삽입하세요.

3.3 예외사항이 추가로 캡슐화됩니다

예외를 얻을 수는 있지만 Feign은 예외를 포착한 후 비즈니스 예외를 기반으로 다시 캡슐화합니다.

왜냐하면feign 호출 결과가 200이 아닌 응답 코드인 경우 Feign의 예외 구문 분석이 시작되며, Feign의 예외 구문 분석기는 이를 FeignException으로 래핑합니다. 즉, 우리의 비즈니스 예외를 기반으로 다시 래핑합니다.

feign.codec.ErrorDecoder#decode() 메서드에 중단점을 넣어 실행 결과를 관찰할 수 있습니다. 다음과 같이 실행 결과를 관찰할 수 있습니다.
여기에 이미지 설명을 삽입하세요.
당연히 이 래핑된 예외는 필요하지 않습니다. 생산자의 비즈니스를 직접 캡처해야 합니다. 예외는 코드에 직접 발생합니다. 프론트엔드, 그렇다면 어떻게 해결해야 할까요?

3.4.해결책

매우 간단합니다. Feign의 예외 구문 분석기를 다시 작성하고, 디코드 로직을 다시 구현하고, 일반 BusinessException을 반환하기만 하면 전역 예외 인터셉터가 BusinessException을 캡처합니다! ( 뭔가 무한히 중첩된 인형같은 느낌)

코드는 아래와 같이 표시됩니다.

  1. 가짜 예외 파서 다시 작성
/**
 * @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. 공급자 계층을 다시 호출하여
    발생한 예외 정보는 소비자 계층에서 캡처하여 사용자 정의 예외 구문 분석기를 통해 사용자 정의 예외로 처리할 수 있으며 더 이상 기본 가짜 예외로 래핑되지 않습니다. 던져진 사용자 정의 예외는 캡슐화 처리를 위해 균일하게 반환됩니다. .여기에 이미지 설명을 삽입하세요.여기에 이미지 설명을 삽입하세요.
    여기에 이미지 설명을 삽입하세요.

케이스 소스코드

케이스 소스 코드 컨베이어 벨트

추천

출처blog.csdn.net/weixin_43811057/article/details/132522402