背景

项目中要把spring-boot 2.3.2版本升级到spring boot 2.6.2版本。 如果要升级spring boot 到2.6.2. 就必须要把spring cloud升级到2021.0.x版本(具体可以看最下面参考资料部分)。但是Spring cloud 在2020.0版本的时候已经移除了ribbon, Hystrix,Zuul.. 等等,而我们的项目还在用这些东西。 Gateway是Spring cloud 官方给出的替换方案。到底能不能用OpenFeign或Zuul了?下面是我在升级过程中遇到的一些问题以及一些尝试过的方案。

要升级到的版本

LibVresion
spring-cloud-starter-gateway3.1.0
spring-cloud-dependencies2021.0.0
spring-cloud-starter-netflix-zuul2.2.6.RELEASE
spring-cloud-starter-openfeign3.1.0

1. 单用Zuul做路由时的问题

LibVresion
spring-cloud-dependencies2021.0.0
spring-cloud-starter-netflix-zuul2.2.6.RELEASE

1.1 问题 java.lang.NoSuchMethodError: org.springframework.boot.web.servlet.error.ErrorController.getErrorPath()Ljava/lang/String;
    at 

错误信息

Caused by: java.lang.NoSuchMethodError: org.springframework.boot.web.servlet.error.ErrorController.getErrorPath()Ljava/lang/String;
	at org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping.lookupHandler(ZuulHandlerMapping.java:87) ~[spring-cloud-netflix-zuul-2.2.10.RELEASE.jar:2.2.10.RELEASE]
	at org.springframework.web.servlet.handler.AbstractUrlHandlerMapping.getHandlerInternal(AbstractUrlHandlerMapping.java:152) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:498) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1261) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1043) ~[spring-webmvc-5.3.14.jar:5.3.14]
	... 38 common frames omitted

解决方法:

 加一个类就可以了。

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.CallbackFilter;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.cglib.proxy.NoOp;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.cloud.netflix.zuul.web.ZuulController;
import org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Fix for Zuul configuration with Spring Boot 2.5.x + Zuul - "NoSuchMethodError: ErrorController.getErrorPath()":
 */
@Configuration
public class ZuulConfiguration {
  /**
   * The path returned by ErrorController.getErrorPath() with Spring Boot < 2.5
   * (and no longer available on Spring Boot >= 2.5).
   */
  private static final String ERROR_PATH = "/error";
  private static final String METHOD = "lookupHandler";

  /**
   * Constructs a new bean post-processor for Zuul.
   *
   * @param routeLocator    the route locator.
   * @param zuulController  the Zuul controller.
   * @param errorController the error controller.
   * @return the new bean post-processor.
   */
  @Bean
  public ZuulPostProcessor zuulPostProcessor(@Autowired RouteLocator routeLocator,
                                             @Autowired ZuulController zuulController,
                                             @Autowired(required = false) ErrorController errorController) {
      System.out.println("ZuulPostProcessor....");
    return new ZuulPostProcessor(routeLocator, zuulController, errorController);
  }

  private enum LookupHandlerCallbackFilter implements CallbackFilter {
    INSTANCE;

    @Override
    public int accept(Method method) {
      if (METHOD.equals(method.getName())) {
        return 0;
      }
      return 1;
    }
  }

  private enum LookupHandlerMethodInterceptor implements MethodInterceptor {
    INSTANCE;

    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
      if (ERROR_PATH.equals(args[0])) {
        // by entering this branch we avoid the ZuulHandlerMapping.lookupHandler method to trigger the 
        // NoSuchMethodError 
        return null;
      }
      return methodProxy.invokeSuper(target, args);
    }
  }

  private static final class ZuulPostProcessor implements BeanPostProcessor {

    private final RouteLocator routeLocator;
    private final ZuulController zuulController;
    private final boolean hasErrorController;

    ZuulPostProcessor(RouteLocator routeLocator, ZuulController zuulController, ErrorController errorController) {
      this.routeLocator = routeLocator;
      this.zuulController = zuulController;
      this.hasErrorController = (errorController != null);
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
      if (hasErrorController && (bean instanceof ZuulHandlerMapping)) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ZuulHandlerMapping.class);
        enhancer.setCallbackFilter(LookupHandlerCallbackFilter.INSTANCE); // only for lookupHandler
        enhancer.setCallbacks(new Callback[] {LookupHandlerMethodInterceptor.INSTANCE, NoOp.INSTANCE});
        Constructor<?> ctor = ZuulHandlerMapping.class.getConstructors()[0];
        return enhancer.create(ctor.getParameterTypes(), new Object[] {routeLocator, zuulController});
      }
      return bean;
    }
  }
}

参考资料

java - NoSuchMethodError: org.springframework.boot.web.servlet.error.ErrorController.getErrorPath() in latest spring-cloud-starter-netflix-zuul - Stack Overflow

1.2 问题 com.netflix.client.ClientException: Load balancer does not have available server for client: spring-cloud-client2

相关代码

zuul.routes.test.path=/spring-cloud-client2/**
zuul.routes.test.serviceId=spring-cloud-client2

如果你想用Eureka取其它组件的host和port是。会报下面错误

Caused by: com.netflix.client.ClientException: Load balancer does not have available server for client: spring-cloud-client2
	at com.netflix.loadbalancer.LoadBalancerContext.getServerFromLoadBalancer(LoadBalancerContext.java:483) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
	at com.netflix.loadbalancer.reactive.LoadBalancerCommand$1.call(LoadBalancerCommand.java:184) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
	at com.netflix.loadbalancer.reactive.LoadBalancerCommand$1.call(LoadBalancerCommand.java:180) ~[ribbon-loadbalancer-2.3.0.jar:2.3.0]
	at rx.Observable.unsafeSubscribe(Observable.java:10327) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.operators.OnSubscribeConcatMap.call(OnSubscribeConcatMap.java:94) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.operators.OnSubscribeConcatMap.call(OnSubscribeConcatMap.java:42) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.Observable.unsafeSubscribe(Observable.java:10327) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.operators.OperatorRetryWithPredicate$SourceSubscriber$1.call(OperatorRetryWithPredicate.java:127) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.schedulers.TrampolineScheduler$InnerCurrentThreadScheduler.enqueue(TrampolineScheduler.java:73) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.schedulers.TrampolineScheduler$InnerCurrentThreadScheduler.schedule(TrampolineScheduler.java:52) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.operators.OperatorRetryWithPredicate$SourceSubscriber.onNext(OperatorRetryWithPredicate.java:79) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.operators.OperatorRetryWithPredicate$SourceSubscriber.onNext(OperatorRetryWithPredicate.java:45) ~[rxjava-1.3.8.jar:1.3.8]

原因:

spring-cloud-netfix-eureka-client已经把Ribbon利用Eureka取注册在其上的信息这部分代码已经移除了。所有Zuul只能利用静态信息了。

原来是利用DomainExtractingServerList类取Eureka里注册信息。

org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList 

现在是用ConfigurationBasedServerList只能读配置文件的信息。

com.netflix.loadbalancer.ConfigurationBasedServerList

解决方法:

1. 加下面配置类

@RibbonClients(defaultConfiguration = RibbonEurekaClientConfig.class)

2. 创建下面配置类


import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.annotation.Bean;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AvailabilityFilteringRule;
import com.netflix.loadbalancer.DummyPing;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;

public class RibbonEurekaClientConfig {
    @Bean
    public IPing ribbonPing(IClientConfig config) {
        return new DummyPing();
    }
    @Bean
    public IRule ribbonRule(IClientConfig config) {
        return new AvailabilityFilteringRule();
    }
    @Autowired
    DiscoveryClient discoveryClient;
    @Bean
    public ServerList<Server> getServerList(IClientConfig config) {

        return new ServerList<Server>() {
            @Override
            public List<Server> getInitialListOfServers() {
                return new ArrayList<>();
            }

            @Override
            public List<Server> getUpdatedListOfServers() {
                List<Server> serverList = new ArrayList<>();

                List<ServiceInstance> list = discoveryClient.getInstances(config.getClientName());
                for (ServiceInstance instance : list) {
                    serverList.add(new Server(instance.getHost(), instance.getPort()));
                }
                return serverList;
            }
        };
    }
}

 参考资料

microservices - Zuul Forwarding error, Load balancer does not have available server for client - Stack Overflow

最新的修改

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.*;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonClientName;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

@Configuration
@ConditionalOnBean(SpringClientFactory.class)
public class RibbonConfiguration {

    private static final Log log = LogFactory.getLog(RibbonConfiguration.class);

    @Autowired
    private DiscoveryClient discoveryClient;

    @Bean
    public IPing ribbonPing() {
        return new DummyPing();
    }

    @Bean
    public IRule ribbonRule(IClientConfig clientConfig) {
        AvailabilityFilteringRule rule = new AvailabilityFilteringRule();
        rule.initWithNiwsConfig(clientConfig);
        return rule;
    }

    @Bean
    public ServerList<?> ribbonServerList(IClientConfig clientConfig) {

        return new ServerList<Server>() {
            @Override
            public List<Server> getInitialListOfServers() {
                return new ArrayList<>();
            }

            @Override
            public List<Server> getUpdatedListOfServers() {
                List<Server> serverList = new ArrayList<>();
                List<ServiceInstance> instances = discoveryClient.getInstances(clientConfig.getClientName());
                if (instances != null && instances.size() == 0) {
                    return serverList;
                }
                for (ServiceInstance instance : instances) {
                    if (instance.isSecure()) {
                        serverList.add(new Server("https", instance.getHost(), instance.getPort()));
                    } else {
                        serverList.add(new Server("http", instance.getHost(), instance.getPort()));
                    }
                }
                return serverList;
            }
        };
    }

}

2.  Zuul 与OpenFeign 一起使用的问题

错误信息


java.lang.AbstractMethodError: org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient.choose(Ljava/lang/String;Lorg/springframework/cloud/client/loadbalancer/Request;)Lorg/springframework/cloud/client/ServiceInstance;
	at org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient.execute(FeignBlockingLoadBalancerClient.java:97) ~[spring-cloud-openfeign-core-3.1.0.jar:3.1.0]
	at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:119) ~[feign-core-11.7.jar:na]
	at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) ~[feign-core-11.7.jar:na]
	at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100) ~[feign-core-11.7.jar:na]
	at com.sun.proxy.$Proxy126.test1(Unknown Source) ~[na:na]
	at com.spring_cloud_client1.CarController.test12(CarController.java:28) ~[classes/:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_51]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_51]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_51]
	at java.lang.reflect.Method.invoke(Method.java:497) ~[na:1.8.0_51]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.14.jar:5.3.14]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.56.jar:4.0.FR]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.14.jar:5.3.14]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.56.jar:4.0.FR]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.56.jar:9.0.56]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.14.jar:5.3.14]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.14.jar:5.3.14]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.56.jar:9.0.56]
	at 

原因:

OpenFeign依赖spring-cloud-starter-loadbalancer做负载均衡,之前是用Ribbon的。如果加了spring-cloud-starter-netflix-zuul后,zuul依赖了Ribbon做负载均衡。

LoadBalancerClient 有两个RibbonLoadBalancerClient和BlockingLoadBalancerClient类。结果OpenFeigin用RibbonLoadBalancerClient使用报错。

如果单用OpenFeign是不会有这个问题的。

总结:

OpenFeigin 用BlockingLoadBalancerClient, 但是Zuul用RibbonLoadBalancerClient。 但是这两类继承同一个类LoadBalancerClient而且只能实例化一个。

public class BlockingLoadBalancerClientAutoConfiguration {

	@Bean
	@ConditionalOnBean(LoadBalancerClientFactory.class)
	@ConditionalOnMissingBean
	public LoadBalancerClient blockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory) {
		return new BlockingLoadBalancerClient(loadBalancerClientFactory);
	}
public class RibbonAutoConfiguration {

...
	@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}

解决方法:

	@Bean
	public LoadBalancerClient blockingLoadBalancerClient(LoadBalancerClientFactory loadBalancerClientFactory) {
		return new BlockingLoadBalancerClient(loadBalancerClientFactory);
	}

3. OpenFeign与Gateway同时用的问题

LibVresion
spring-cloud-starter-gateway3.1.0
spring-cloud-dependencies2021.0.0
spring-cloud-starter-openfeign3.1.0

错误信息


java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2
	at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.4.13.jar:3.4.13]
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	*__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
	*__checkpoint ⇢ HTTP GET "/test1-2" [ExceptionHandlingWebHandler]
Original Stack Trace:
		at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:83) ~[reactor-core-3.4.13.jar:3.4.13]
		at reactor.core.publisher.Mono.block(Mono.java:1707) ~[reactor-core-3.4.13.jar:3.4.13]
		at org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient.choose(

原因:

spring-cloud-starter-gateway 依赖的是 spring-boot-starter-webflux, 但是 OpenFeign 用的是spring-boot-starter-web。

spring-boot-starter-webflux和spring-boot-starter-web 二者只能选一。如果选了spring-boot-starter-webflux。就报上面的错误。

如果你选了spring-boot-starter-web。 那么就下面错误

Description:

Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway.

Action:

Please set spring.main.web-application-type=reactive or remove spring-boot-starter-web dependency.

解决方法:

只能把spring-cloud-starter-gateway分到一个独立的spirng-boot项目了。如果要做路由,就用这个独立的项目

4. 参考资料

4.1. Spring boot 与spring cloud 对应匹配版本

Spring Cloud

Spring cloudTrain

Spring Boot Version

2021.0.x aka Jubilee

2.6.x

2020.0.x aka Ilford

2.4.x, 2.5.x (Starting with 2020.0.3)

Hoxton

2.2.x, 2.3.x (Starting with SR5)

Greenwich

2.1.x

Finchley

2.0.x

Edgware

1.5.x

Dalston

1.5.x

Table 1. Release train Spring Boot compatibility

4.2. Spring cloud 2020.0版本的release Notes

https://github.com/spring-cloud/spring-cloud-release/wiki/Spring-Cloud-2020.0-Release-Notes

 Breaking changes

  • As announced, the following modules have been removed from spring-cloud-netflix:
    • spring-cloud-netflix-archaius
    • spring-cloud-netflix-concurrency-limits
    • spring-cloud-netflix-core
    • spring-cloud-netflix-dependencies
    • spring-cloud-netflix-hystrix
    • spring-cloud-netflix-hystrix-contract
    • spring-cloud-netflix-hystrix-dashboard
    • spring-cloud-netflix-hystrix-stream
    • spring-cloud-netflix-ribbon
    • spring-cloud-netflix-sidecar
    • spring-cloud-netflix-turbine
    • spring-cloud-netflix-turbine-stream
    • spring-cloud-netflix-zuul
    • spring-cloud-starter-netflix-archaius
    • spring-cloud-starter-netflix-hystrix
    • spring-cloud-starter-netflix-hystrix-dashboard
    • spring-cloud-starter-netflix-ribbon
    • spring-cloud-starter-netflix-turbine
    • spring-cloud-starter-netflix-turbine-stream
    • spring-cloud-starter-netflix-zuul
    • Support for ribbon, hystrix and zuul was removed across the release train projects.

4.3. Spring cloud 官方对于Hystrix,Riboon, zuul和Archaius给出的替换方案

Spring Cloud Greenwich.RC1 available now

Replacements

We recommend the following as replacements for the functionality provided by these modules.

CurrentReplacement
HystrixResilience4j
Hystrix Dashboard / TurbineMicrometer + Monitoring System
RibbonSpring Cloud Loadbalancer
Zuul 1Spring Cloud Gateway
Archaius 1Spring Boot external config + Spring Cloud Config

Look for a future blog post on Spring Cloud Loadbalancer and integration with a new Netflix project Concurrency Limits.

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐