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

ch4*_*4mp 4 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);
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

以及我如何使用它:

@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
    ...
}
Run Code Online (Sandbox Code Playgroud)

ch4*_*4mp 7

顺便说一句,我之前给出的解决方案有效,但我的初衷是个坏主意:错误是错误,不应在名义流量上处理。像 Feign 那样抛出异常,并用 an 处理它@ExceptionHandler是 Spring MVC 世界中更好的方法。

所以有两个解决方案:

  • 添加一个@ExceptionHandlerforFeignException
  • 配置FeignClientErrorDecoder对错误在您的业务层知道一个异常翻译(并且已经提供@ExceptionHandler了)

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

FeignClient with conf(抱歉feign -form引入的噪音)

@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()));
                }
            };
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

常见业务异常

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

错误响应 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

佯客户端使用的通知错误是如何透明地处理(没有的try / catch)由于@ControllerAdvice@ExceptionHandler({SroException.class})

@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()));
    }
}
Run Code Online (Sandbox Code Playgroud)