在 Spring 引导微服务中使用 FeignClient 时出现错误 302

Error 302 Using FeignClient in Spring Boot Microservices

FeignClient 有问题。我部署了 Spring 启动应用程序,我在调用特定的假客户端时出错,当我想要与用户微服务的特定方法通信时使用注册微服务时出现错误,使用其他方法问题没有发生,我还有一个用于发现的 Eureka 服务器和一个带有 Spring Cloud Gateway 的网关,配置了权限配置。我在应用程序中有@EnableEurekaClient 和@EnableFeignClients,它们可以在 Eureka 服务器上看到,并使用 resilience4j 实现 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]

对于我的 registro 微服务:

型号:

@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,假客户端工作正常:

我在微服务中使用的方法是:

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

我不明白为什么会这样

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

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

中指定的 API

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

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

在这种情况下,我只需删除带有 @ResponseStatus 注释的固定 302 状态,并将 API 更改为 return a 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 情况,因为找不到用户。或者甚至更好,您可以简单地创建一个 object 作为对您的 API 的响应,它具有布尔值,无论用户是否存在,例如:

{
  "exists": false
}

然后您可以在您的 Feign 客户端中映射此 object 并处理纯布尔值。

最后,如果你想坚持使用 302 状态代码,你可以将 Feign 客户端定义更改为 return a feign.Response class 而不是 Boolean.

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

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

查看我的博客以获取有关 Feign 的文章:arnoldgalovics.com Feign articles

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