FeignClient抛出而不是返回具有错误http状态的ResponseEntity

时间:2018-01-25 11:25:31

标签: spring-cloud-feign

由于我使用ResponseEntity<T>作为我的FeignClient方法的返回值,我希望它返回一个具有400状态的ResponseEntity,如果它是服务器返回的内容。但它会抛出FeignException

如何从FeignClient获取正确的ResponseEntity而不是Exception?

这是我的FeignClient:

@FeignClient(value = "uaa", configuration = OauthFeignClient.Conf.class)
public interface OauthFeignClient {

    @RequestMapping(
            value = "/oauth/token",
            method = RequestMethod.POST,
            consumes = MULTIPART_FORM_DATA_VALUE,
            produces = APPLICATION_JSON_VALUE)
    ResponseEntity<OauthTokenResponse> token(Map<String, ?> formParams);

    class Conf {

        @Value("${oauth.client.password}")
        String oauthClientPassword;

        @Bean
        public Encoder feignFormEncoder() {
            return new SpringFormEncoder();
        }

        @Bean
        public Contract feignContract() {
            return new SpringMvcContract();
        }

        @Bean
        public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
            return new BasicAuthRequestInterceptor("web-client", oauthClientPassword);
        }

    }
}

以及我如何使用它:

@PostMapping("/login")
public ResponseEntity<LoginTokenPair> getTokens(@RequestBody @Valid LoginRequest userCredentials) {
    Map<String, String> formData = new HashMap<>();

    ResponseEntity<OauthTokenResponse> response = oauthFeignClient.token(formData);

    //code never reached if contacted service returns a 400
    ...
}

2 个答案:

答案 0 :(得分:2)

顺便说一下,我在工作之前给出的解决方案,但我最初的意图是坏主意:错误是一个错误,不应该在名义流程上处理。抛出异常,就像Feign那样,并用@ExceptionHandler处理它是一个更好的方式进入Spring MVC世界。

所以有两个解决方案:

  • @ExceptionHandler
  • 添加FeignException
  • 使用FeignClient配置ErrorDecoder,以便在您的业务层知道的异常中转换错误(并且已经提供@ExceptionHandler

我更喜欢第二种解决方案,因为收到的错误消息结构可能会从客户端更改为另一种,因此您可以通过每客户端错误解码从这些错误中提取更精细的数据。

FeignClient with conf (抱歉由于假装形式引入的噪音)

@FeignClient(value = "uaa", configuration = OauthFeignClient.Config.class)
public interface OauthFeignClient {

    @RequestMapping(
            value = "/oauth/token",
            method = RequestMethod.POST,
            consumes = MULTIPART_FORM_DATA_VALUE,
            produces = APPLICATION_JSON_VALUE)
    DefaultOAuth2AccessToken token(Map<String, ?> formParams);

    @Configuration
    class Config {

        @Value("${oauth.client.password}")
        String oauthClientPassword;

        @Autowired
        private ObjectFactory<HttpMessageConverters> messageConverters;

        @Bean
        public Encoder feignFormEncoder() {
            return new SpringFormEncoder(new SpringEncoder(messageConverters));
        }

        @Bean
        public Decoder springDecoder() {
            return new ResponseEntityDecoder(new SpringDecoder(messageConverters));
        }

        @Bean
        public Contract feignContract() {
            return new SpringMvcContract();
        }

        @Bean
        public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
            return new BasicAuthRequestInterceptor("web-client", oauthClientPassword);
        }

        @Bean
        public ErrorDecoder uaaErrorDecoder(Decoder decoder) {
            return (methodKey, response) -> {
                try {
                    OAuth2Exception uaaException = (OAuth2Exception) decoder.decode(response, OAuth2Exception.class);
                    return new SroException(
                            uaaException.getHttpErrorCode(),
                            uaaException.getOAuth2ErrorCode(),
                            Arrays.asList(uaaException.getSummary()));

                } catch (Exception e) {
                    return new SroException(
                            response.status(),
                            "Authorization server responded with " + response.status() + " but failed to parse error payload",
                            Arrays.asList(e.getMessage()));
                }
            };
        }
    }
}

常见业务例外

public class SroException extends RuntimeException implements Serializable {
    public final int status;

    public final List<String> errors;

    public SroException(final int status, final String message, final Collection<String> errors) {
        super(message);
        this.status = status;
        this.errors = Collections.unmodifiableList(new ArrayList<>(errors));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof SroException)) return false;
        SroException sroException = (SroException) o;
        return status == sroException.status &&
                Objects.equals(super.getMessage(), sroException.getMessage()) &&
                Objects.equals(errors, sroException.errors);
    }

    @Override
    public int hashCode() {
        return Objects.hash(status, super.getMessage(), errors);
    }
}

错误处理程序(从ResponseEntityExceptionHandler扩展名中提取)

@ExceptionHandler({SroException.class})
public ResponseEntity<Object> handleSroException(SroException ex) {
    return new SroError(ex).toResponse();
}

错误回复DTO

@XmlRootElement
public class SroError implements Serializable {
    public final int status;

    public final String message;

    public final List<String> errors;

    public SroError(final int status, final String message, final Collection<String> errors) {
        this.status = status;
        this.message = message;
        this.errors = Collections.unmodifiableList(new ArrayList<>(errors));
    }

    public SroError(final SroException e) {
        this.status = e.status;
        this.message = e.getMessage();
        this.errors = Collections.unmodifiableList(e.errors);
    }

    protected SroError() {
        this.status = -1;
        this.message = null;
        this.errors = null;
    }

    public ResponseEntity<Object> toResponse() {
        return new ResponseEntity(this, HttpStatus.valueOf(this.status));
    }

    public ResponseEntity<Object> toResponse(HttpHeaders headers) {
        return new ResponseEntity(this, headers, HttpStatus.valueOf(this.status));
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof SroError)) return false;
        SroError sroException = (SroError) o;
        return status == sroException.status &&
                Objects.equals(message, sroException.message) &&
                Objects.equals(errors, sroException.errors);
    }

    @Override
    public int hashCode() {

        return Objects.hash(status, message, errors);
    }
}

假设客户端使用,请注意@ControllerAdvice&amp;和@ExceptionHandler({SroException.class})&amp; @RestController @RequestMapping("/uaa") public class AuthenticationController { private static final BearerToken REVOCATION_TOKEN = new BearerToken("", 0L); private final OauthFeignClient oauthFeignClient; private final int refreshTokenValidity; @Autowired public AuthenticationController( OauthFeignClient oauthFeignClient, @Value("${oauth.ttl.refresh-token}") int refreshTokenValidity) { this.oauthFeignClient = oauthFeignClient; this.refreshTokenValidity = refreshTokenValidity; } @PostMapping("/login") public ResponseEntity<LoginTokenPair> getTokens(@RequestBody @Valid LoginRequest userCredentials) { Map<String, String> formData = new HashMap<>(); formData.put("grant_type", "password"); formData.put("client_id", "web-client"); formData.put("username", userCredentials.username); formData.put("password", userCredentials.password); formData.put("scope", "openid"); DefaultOAuth2AccessToken response = oauthFeignClient.token(formData); return ResponseEntity.ok(new LoginTokenPair( new BearerToken(response.getValue(), response.getExpiresIn()), new BearerToken(response.getRefreshToken().getValue(), refreshTokenValidity))); } @PostMapping("/logout") public ResponseEntity<LoginTokenPair> revokeTokens() { return ResponseEntity .ok(new LoginTokenPair(REVOCATION_TOKEN, REVOCATION_TOKEN)); } @PostMapping("/refresh") public ResponseEntity<BearerToken> refreshToken(@RequestHeader("refresh_token") String refresh_token) { Map<String, String> formData = new HashMap<>(); formData.put("grant_type", "refresh_token"); formData.put("client_id", "web-client"); formData.put("refresh_token", refresh_token); formData.put("scope", "openid"); DefaultOAuth2AccessToken response = oauthFeignClient.token(formData); return ResponseEntity.ok(new BearerToken(response.getValue(), response.getExpiresIn())); } }

library(dplyr)
filter(df, any(NUM > 2) & any(NUM < -2))

# A tibble: 16 x 3
# Groups:   id, grp [2]
      id grp     NUM
   <int> <chr> <dbl>
 1     0 01    -4.00
 2     0 01    -3.00
 3     0 01    -2.00
 4     0 01    -1.00
 5     0 01     1.00
 6     0 01     2.00
 7     0 01     3.00
 8     0 01     4.00
 9     1 02    -3.00
10     1 02    -2.00
11     1 02    -1.00
12     1 02     1.00
13     1 02     2.00
14     1 02     3.00
15     1 02     4.00
16     1 02     5.00

答案 1 :(得分:1)

所以,看一下源代码,它认为只有解决方案实际上是使用feign.Response作为FeignClient方法的返回类型,并用new ObjectMapper().readValue(response.body().asReader(), clazz)之类的方式手动解码正文(保护2xx状态为当然,因为对于错误状态,身体很可能是错误描述而不是有效的有效载荷;)。

即使状态不在2xx范围内,也可以提取和转发状态,标题,正文等。

编辑: 这是一种转发状态,标题和映射JSON正文的方法(如果可能):

public static class JsonFeignResponseHelper {
    private final ObjectMapper json = new ObjectMapper();

    public <T> Optional<T> decode(Response response, Class<T> clazz) {
        if(response.status() >= 200 && response.status() < 300) {
            try {
                return Optional.of(json.readValue(response.body().asReader(), clazz));
            } catch(IOException e) {
                return Optional.empty();
            }
        } else {
            return Optional.empty();
        }
    }

    public <T, U> ResponseEntity<U> toResponseEntity(Response response, Class<T> clazz, Function<? super T, ? extends U> mapper) {
        Optional<U> payload = decode(response, clazz).map(mapper);

        return new ResponseEntity(
                payload.orElse(null),//didn't find a way to feed body with original content if payload is empty
                convertHeaders(response.headers()),
                HttpStatus.valueOf(response.status()));
    }

    public MultiValueMap<String, String>  convertHeaders(Map<String, Collection<String>> responseHeaders) {
        MultiValueMap<String, String> responseEntityHeaders = new LinkedMultiValueMap<>();
        responseHeaders.entrySet().stream().forEach(e -> 
                responseEntityHeaders.put(e.getKey(), new ArrayList<>(e.getValue())));
        return responseEntityHeaders;
    }
}

可以使用如下:

@PostMapping("/login")
public ResponseEntity<LoginTokenPair> getTokens(@RequestBody @Valid LoginRequest userCredentials) throws IOException {
    Response response = oauthFeignClient.token();

    return feignHelper.toResponseEntity(
            response,
            OauthTokenResponse.class,
            oauthTokenResponse -> new LoginTokenPair(
                    new BearerToken(oauthTokenResponse.access_token, oauthTokenResponse.expires_in),
                    new BearerToken(oauthTokenResponse.refresh_token, refreshTokenValidity)));
}

这会保存标头和状态代码,但会丢失错误消息:/