在 Spring Boot 微服务中使用 FeignClient 报错 302

Posted

技术标签:

【中文标题】在 Spring Boot 微服务中使用 FeignClient 报错 302【英文标题】:Error 302 Using FeignClient in Spring Boot Microservices 【发布时间】:2021-12-30 10:04:47 【问题描述】:

我遇到了 FeignClient 的问题。我部署了 Spring Boot 应用程序,在调用特定 feign 客户端时出现错误,当我想与用户微服务的特定方法通信时使用注册微服务时出现错误,而使用其他方法时问题不会发生,我还有一个用于发现的 Eureka 服务器和一个带有 Spring Cloud Gateway 的网关,配置了权限配置。我在应用程序中有@EnableEurekaClient 和@EnableFeignClients,它们可以在Eureka 服务器上看到,并使用resilience4j 实现CircuitBreaker。 对于测试,我使用邮递员。

请求:

没有CircuitBreaker我得到这个错误

feign.FeignException: [302] during [GET] to [http://app-usuarios/users/usuarioExisteDatos/?username=admin&email=admin%40udea.edu.co&cellPhone=3128211358] [UsersFeignClient#preguntarUsuarioExiste(String,String,String)]: true
at feign.FeignException.errorStatus(FeignException.java:182) ~[feign-core-10.12.jar:na]
at feign.FeignException.errorStatus(FeignException.java:169) ~[feign-core-10.12.jar:na]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:92) ~[feign-core-10.12.jar:na]
at feign.AsyncResponseHandler.handleResponse(AsyncResponseHandler.java:96) ~[feign-core-10.12.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:138) ~[feign-core-10.12.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) ~[feign-core-10.12.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100) ~[feign-core-10.12.jar:na]
at jdk.proxy11/jdk.proxy11.$Proxy250.preguntarUsuarioExiste(Unknown Source) ~[na:na]
at com.app.registro.controllers.RegistroController.crearNuevo(RegistroController.java:28) ~[classes/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:567) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.13.jar:5.3.13]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.13.jar:5.3.13]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.13.jar:5.3.13]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:681) ~[tomcat-embed-core-9.0.55.jar:4.0.FR]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.13.jar:5.3.13]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.55.jar:4.0.FR]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.55.jar:9.0.55]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.13.jar:5.3.13]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.13.jar:5.3.13]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.13.jar:5.3.13]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.13.jar:5.3.13]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.13.jar:5.3.13]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.13.jar:5.3.13]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1722) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.55.jar:9.0.55]
at java.base/java.lang.Thread.run(Thread.java:831) ~[na:na]

使用断路器:

[302] during [GET] to [http://app-usuarios/users/usuarioExisteDatos/?username=admin&email=admin%40udea.edu.co&cellPhone=3128211358] [UsersFeignClient#preguntarUsuarioExiste(String,String,String)]: [true]

对于我的注册微服务:

型号:

@Document(collection = "registro")
public class Registro 

    @Id
    private String id;

    @NotBlank(message = "Username cannot be null")
    @Size(max = 20)
    @Indexed(unique = true)
    @Pattern(regexp = "[A-Za-z0-9_.-]+", message = "Solo se permite:'_' o '.' o '-'")
    private String username;

    @NotBlank(message = "Password cannot be null")
    @Pattern(regexp = "[^ ]*+", message = "Caracter: ' ' (Espacio en blanco) invalido")
    @Size(min = 6, max = 20, message = "About Me must be between 6 and 20 characters")
    private String password;

    @NotBlank(message = "Cell phone cannot be null")
    @Pattern(regexp = "[0-9]+", message = "Solo numeros")
    @Size(max = 50)
    @Indexed(unique = true)
    private String cellPhone;

    @NotBlank(message = "Email cannot be null")
    @Size(max = 50)
    @Pattern(regexp = "[^ ]*+", message = "Caracter: ' ' (Espacio en blanco) invalido")
    @Email(message = "Email should be valid")
    @Indexed(unique = true)
    private String email;

    private String codigo;
    private List<String> roles;

    ** Constructors, setters and getters

我的客户:

@FeignClient(name = "app-usuarios")
public interface UsersFeignClient 

    @GetMapping("/users/usuarioExisteDatos")
    public Boolean preguntarUsuarioExiste(@RequestParam(value = "username") String username,
            @RequestParam(value = "email") String email, @RequestParam(value = "cellPhone") String cellPhone);
    
    @GetMapping("/users/listar")
    public List<Usuario> listarUsuarios();

我的控制器:

@RestController
public class RegistroController 

    private final Logger logger = LoggerFactory.getLogger(RegistroController.class);

    @SuppressWarnings("rawtypes")
    @Autowired
    private CircuitBreakerFactory cbFactory;

    @Autowired
    UsersFeignClient uClient;

    @GetMapping("/registro/listarUsuarios")
    public List<Usuario> verUsuarios() 
        return uClient.listarUsuarios();
    

    @PostMapping("/registro/crearNuevo")
    @ResponseStatus(code = HttpStatus.CREATED)
    public Boolean crearNuevo(@RequestBody @Validated Registro registro) 
        // return uClient.preguntarUsuarioExiste(registro.getUsername(),
        // registro.getEmail(), registro.getCellPhone());
        return (Boolean) cbFactory.create("usuarios").run(() -> uClient.preguntarUsuarioExiste(registro.getUsername(),
                registro.getEmail(), registro.getCellPhone()), e -> preguntarUsuarioExiste2(registro.getUsername(), e));
    

    private Object preguntarUsuarioExiste2(String username, Throwable e) 
        logger.info(e.getMessage());
        return false;
    


我的应用程序属性:

#-------APP-------
spring.application.name=app-registro
server.port=$PORT:0

#-----MongoDb------
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.authentication-database=admin
spring.data.mongodb.username=user
spring.data.mongodb.password=user
spring.data.mongodb.database=usuariosApp
spring.data.mongodb.auto-index-creation: true

#-----Eureka-------
eureka.instance.metadataMap.instanceId=$spring.application.name:$spring.application.instance_id:$random.value
eureka.client.service-url.defaultZone=http://localhost:8761/eureka

management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always

#-----Feign-------
feign.client.config.default.connect-timeout=10000
feign.client.config.default.read-timeout=10000
feign.client.config.default.logger-level=full

我的 Pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.app.registro</groupId>
    <artifactId>App-Registro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>App-Registro</name>
    <description>Registro for App</description>
    <properties>
        <java.version>11</java.version>
        <spring-cloud.version>2020.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-mongodb</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
            <version>3.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>$spring-cloud.version</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

我在微服务usuarios中的方法:

@GetMapping("/users/usuarioExisteDatos")
    @ResponseStatus(HttpStatus.FOUND)
    public Boolean preguntarUsuarioExiste(@RequestParam(value = "username") String username,
            @RequestParam(value = "email") String email, @RequestParam(value = "cellPhone") String cellPhone)
            throws InterruptedException 
        return uRepository.existsByUsernameOrEmailOrCellPhone(username, email, cellPhone);
    

请注意,您正在调用 MongoRepository 接口

如果我调用另一个客户端方法 listUsers,feign 客户端可以正常工作:

我在微服务usuarios中的方法是:

@GetMapping("/users/listar")
    @ResponseStatus(code = HttpStatus.CREATED)
    public List<Usuario> listarUsuarios() 
        return uRepository.findAll();
    

我不明白为什么会这样

【问题讨论】:

【参考方案1】:

您在这里有几个选择,但让我澄清一下为什么会发生这种情况。 Feign 是 API 的 HTTP 绑定器。在正常情况下,当您进行后端-后端通信时,事实上接受的 HTTP 状态代码是 2xx,表示一切都按预期工作。当 API 以 3xx(在您的情况下为 302)响应时,这表示重定向,通常用于指示浏览器在执行某些操作时将用户重定向到另一个页面。

无论如何,既然我们已经弄清楚了为什么会发生这种情况,让我们看看为什么您的 Feign 客户端会这样。所有 Feign 客户端都有一个名为 follow-redirects 的配置参数。这控制在收到3xx HTTP 响应时,Feign 客户端是否应该自动尝试调用响应的Location 标头中指定的 API。

默认情况下,此参数设置为 true,这意味着将遵循重定向,并且对于作为客户端用户的您而言,它将是透明的。从异常中,我认为您以某种方式禁用了它,或者您可能使用了手动禁用重定向的 HTTP 客户端。

虽然我可以从您在preguntarUsuarioExiste 方法中的实现中清楚地看到您正在尝试确定系统中是否存在用户。在这种情况下,302 HTTP Found 状态没有意义,即使我理解您为什么要使用它(因为该术语反映了用户的存在)。

在这种情况下,我只需使用@ResponseStatus 注释删除固定的302 状态并更改API 以返回ResponseEntity,而不是动态解析状态代码。像这样的:

@GetMapping("/users/usuarioExisteDatos")
public ResponseEntity<?> preguntarUsuarioExiste(@RequestParam(value = "username") String username, @RequestParam(value = "email") String email, @RequestParam(value = "cellPhone") String cellPhone) throws InterruptedException 
    boolean exists = uRepository.existsByUsernameOrEmailOrCellPhone(username, email, cellPhone);
    if (exists) 
        return ResponseEntity.ok().build();
     else 
        return ResponseEntity.notFound().build();
    

这样当你从 Feign 客户端调用 API 时,你可以简单地处理 404 的情况,因为没有找到用户。或者更好的是,您可以简单地创建一个对象作为 API 的响应,无论用户是否存在,该对象都具有布尔值,例如:


  "exists": false

然后你可以在你的 Feign 客户端中映射这个对象并处理纯布尔值。

最后,如果您想坚持使用 302 状态码,您可以更改您的 Feign 客户端定义以返回 feign.Response 类而不是 Boolean

这样,它不会因异常而失败,但您将完全控制响应应该发生的事情。您可以访问状态码、正文以及您需要的一切。

我强烈建议您多了解一下 Feign,您可能会陷入更多的罪魁祸首,尤其是当您将它与 Eureka 和 Resilience4J 等服务弹性工具结合使用时。而且我不是想在这里做广告,但我真的相信你需要一些指导。

查看我的博客关于 Feign 的文章:arnoldgalovics.com Feign articles

另外,请查看我的 Feign、Spring Cloud OpenFeign 和 Resilience4J 集成课程。我几乎涵盖了您需要的所有内容:Mastering microservice communication with Spring Cloud Feign

【讨论】:

以上是关于在 Spring Boot 微服务中使用 FeignClient 报错 302的主要内容,如果未能解决你的问题,请参考以下文章

最新版Spring Cloud Alibaba微服务架构-Openfeign服务调用篇

最新版Spring Cloud Alibaba微服务架构-Openfeign服务调用篇

使用Spring Boot创建微服务

使用 Spring Boot 的微服务中的安全问题

Spring Boot 微服务 - 依赖

如何在 Spring Boot 微服务架构中使用 Keycloak 实现 JWT?