zoukankan      html  css  js  c++  java
  • SpringCloud学习之Zuul统一异常处理及回退

    一、Filter中统一异常处理

      其实在SpringCloud的Edgware SR2版本中对于ZuulFilter中的错误有统一的处理,但是在实际开发当中对于错误的响应方式,我想每个团队都有自己的处理规范。那么如何做到自定义的异常处理呢?

    我们可以先参考一下SpringCloud提供的SendErrorFilter:

    /*
     * Copyright 2013-2015 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package org.springframework.cloud.netflix.zuul.filters.post;
    
    import javax.servlet.RequestDispatcher;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
    import org.springframework.util.ReflectionUtils;
    import org.springframework.util.StringUtils;
    
    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.context.RequestContext;
    import com.netflix.zuul.exception.ZuulException;
    
    import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ERROR_TYPE;
    import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_ERROR_FILTER_ORDER;
    
    /**
     * Error {@link ZuulFilter} that forwards to /error (by default) if {@link RequestContext#getThrowable()} is not null.
     *
     * @author Spencer Gibb
     */
    //TODO: move to error package in Edgware
    public class SendErrorFilter extends ZuulFilter {
    
        private static final Log log = LogFactory.getLog(SendErrorFilter.class);
        protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
    
        @Value("${error.path:/error}")
        private String errorPath;
    
        @Override
        public String filterType() {
            return ERROR_TYPE;
        }
    
        @Override
        public int filterOrder() {
            return SEND_ERROR_FILTER_ORDER;
        }
    
        @Override
        public boolean shouldFilter() {
            RequestContext ctx = RequestContext.getCurrentContext();
            // only forward to errorPath if it hasn't been forwarded to already
            return ctx.getThrowable() != null
                    && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
        }
    
        @Override
        public Object run() {
            try {
                RequestContext ctx = RequestContext.getCurrentContext();
                ZuulException exception = findZuulException(ctx.getThrowable());
                HttpServletRequest request = ctx.getRequest();
    
                request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);
    
                log.warn("Error during filtering", exception);
                request.setAttribute("javax.servlet.error.exception", exception);
    
                if (StringUtils.hasText(exception.errorCause)) {
                    request.setAttribute("javax.servlet.error.message", exception.errorCause);
                }
    
                RequestDispatcher dispatcher = request.getRequestDispatcher(
                        this.errorPath);
                if (dispatcher != null) {
                    ctx.set(SEND_ERROR_FILTER_RAN, true);
                    if (!ctx.getResponse().isCommitted()) {
                        ctx.setResponseStatusCode(exception.nStatusCode);
                        dispatcher.forward(request, ctx.getResponse());
                    }
                }
            }
            catch (Exception ex) {
                ReflectionUtils.rethrowRuntimeException(ex);
            }
            return null;
        }
    
        ZuulException findZuulException(Throwable throwable) {
            if (throwable.getCause() instanceof ZuulRuntimeException) {
                // this was a failure initiated by one of the local filters
                return (ZuulException) throwable.getCause().getCause();
            }
    
            if (throwable.getCause() instanceof ZuulException) {
                // wrapped zuul exception
                return (ZuulException) throwable.getCause();
            }
    
            if (throwable instanceof ZuulException) {
                // exception thrown by zuul lifecycle
                return (ZuulException) throwable;
            }
    
            // fallback, should never get here
            return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
        }
    
        public void setErrorPath(String errorPath) {
            this.errorPath = errorPath;
        }
    
    }
    View Code

      在这里我们可以找到几个关键点:

      1)在上述代码中,我们可以发现filter已经将相关的错误信息放到request当中了:

        request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);

        request.setAttribute("javax.servlet.error.exception", exception);

        request.setAttribute("javax.servlet.error.message", exception.errorCause);

      2)错误处理完毕后,会转发到 xxx/error的地址来处理

      那么我们可以来做个试验,我们在gateway-service项目模块里,创建一个会抛出异常的filter:

    package com.hzgj.lyrk.springcloud.gateway.server.filter;
    
    import com.netflix.zuul.ZuulFilter;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Component;
    
    @Component
    @Slf4j
    public class MyZuulFilter extends ZuulFilter {
        @Override
        public String filterType() {
            return "post";
        }
    
        @Override
        public int filterOrder() {
            return 9;
        }
    
        @Override
        public boolean shouldFilter() {
            return true;
        }
    
        @Override
        public Object run() {
            log.info("run error test ...");
            throw new RuntimeException();
           // return null;
        }
    }
    View Code

      紧接着我们定义一个控制器,来做错误处理:

    package com.hzgj.lyrk.springcloud.gateway.server.filter;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletRequest;
    
    @RestController
    public class ErrorHandler {
    
        @GetMapping(value = "/error")
        public ResponseEntity<ErrorBean> error(HttpServletRequest request) {
            String message = request.getAttribute("javax.servlet.error.message").toString();
            ErrorBean errorBean = new ErrorBean();
            errorBean.setMessage(message);
            errorBean.setReason("程序出错");
            return new ResponseEntity<>(errorBean, HttpStatus.BAD_GATEWAY);
        }
    
        private static class ErrorBean {
            private String message;
    
            private String reason;
    
            public String getMessage() {
                return message;
            }
    
            public void setMessage(String message) {
                this.message = message;
            }
    
            public String getReason() {
                return reason;
            }
    
            public void setReason(String reason) {
                this.reason = reason;
            }
        }
    }
    View Code

      启动项目后,我们通过网关访问一下试试:

    二、关于zuul回退的问题

    1、关于zuul的超时问题:

      这个问题网上有很多解决方案,但是我还要贴一下源代码,请关注这个类 AbstractRibbonCommand,在这个类里集成了hystrix与ribbon。

    /*
     * Copyright 2013-2016 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     *
     */
    
    package org.springframework.cloud.netflix.zuul.filters.route.support;
    
    import org.apache.commons.logging.Log;
    import org.apache.commons.logging.LogFactory;
    import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration;
    import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
    import org.springframework.cloud.netflix.ribbon.support.AbstractLoadBalancingClient;
    import org.springframework.cloud.netflix.ribbon.support.ContextAwareRequest;
    import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
    import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand;
    import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
    import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
    import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
    import org.springframework.http.client.ClientHttpResponse;
    import com.netflix.client.AbstractLoadBalancerAwareClient;
    import com.netflix.client.ClientRequest;
    import com.netflix.client.config.DefaultClientConfigImpl;
    import com.netflix.client.config.IClientConfig;
    import com.netflix.client.config.IClientConfigKey;
    import com.netflix.client.http.HttpResponse;
    import com.netflix.config.DynamicIntProperty;
    import com.netflix.config.DynamicPropertyFactory;
    import com.netflix.hystrix.HystrixCommand;
    import com.netflix.hystrix.HystrixCommandGroupKey;
    import com.netflix.hystrix.HystrixCommandKey;
    import com.netflix.hystrix.HystrixCommandProperties;
    import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy;
    import com.netflix.hystrix.HystrixThreadPoolKey;
    import com.netflix.zuul.constants.ZuulConstants;
    import com.netflix.zuul.context.RequestContext;
    
    /**
     * @author Spencer Gibb
     */
    public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwareClient<RQ, RS>, RQ extends ClientRequest, RS extends HttpResponse>
            extends HystrixCommand<ClientHttpResponse> implements RibbonCommand {
    
        private static final Log LOGGER = LogFactory.getLog(AbstractRibbonCommand.class);
        protected final LBC client;
        protected RibbonCommandContext context;
        protected ZuulFallbackProvider zuulFallbackProvider;
        protected IClientConfig config;
    
        public AbstractRibbonCommand(LBC client, RibbonCommandContext context,
                ZuulProperties zuulProperties) {
            this("default", client, context, zuulProperties);
        }
    
        public AbstractRibbonCommand(String commandKey, LBC client,
                RibbonCommandContext context, ZuulProperties zuulProperties) {
            this(commandKey, client, context, zuulProperties, null);
        }
    
        public AbstractRibbonCommand(String commandKey, LBC client,
                                     RibbonCommandContext context, ZuulProperties zuulProperties,
                                     ZuulFallbackProvider fallbackProvider) {
            this(commandKey, client, context, zuulProperties, fallbackProvider, null);
        }
    
        public AbstractRibbonCommand(String commandKey, LBC client,
                                     RibbonCommandContext context, ZuulProperties zuulProperties,
                                     ZuulFallbackProvider fallbackProvider, IClientConfig config) {
            this(getSetter(commandKey, zuulProperties, config), client, context, fallbackProvider, config);
        }
    
        protected AbstractRibbonCommand(Setter setter, LBC client,
                                     RibbonCommandContext context,
                                     ZuulFallbackProvider fallbackProvider, IClientConfig config) {
            super(setter);
            this.client = client;
            this.context = context;
            this.zuulFallbackProvider = fallbackProvider;
            this.config = config;
        }
    
        protected static HystrixCommandProperties.Setter createSetter(IClientConfig config, String commandKey, ZuulProperties zuulProperties) {
            int hystrixTimeout = getHystrixTimeout(config, commandKey);
            return HystrixCommandProperties.Setter().withExecutionIsolationStrategy(
                    zuulProperties.getRibbonIsolationStrategy()).withExecutionTimeoutInMilliseconds(hystrixTimeout);
        }
    
        protected static int getHystrixTimeout(IClientConfig config, String commandKey) {
            int ribbonTimeout = getRibbonTimeout(config, commandKey);
            DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance();
            int defaultHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds",
                0).get();
            int commandHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command." + commandKey + ".execution.isolation.thread.timeoutInMilliseconds",
                0).get();
            int hystrixTimeout;
            if(commandHystrixTimeout > 0) {
                hystrixTimeout = commandHystrixTimeout;
            }
            else if(defaultHystrixTimeout > 0) {
                hystrixTimeout = defaultHystrixTimeout;
            } else {
                hystrixTimeout = ribbonTimeout;
            }
            if(hystrixTimeout < ribbonTimeout) {
                LOGGER.warn("The Hystrix timeout of " + hystrixTimeout + "ms for the command " + commandKey +
                    " is set lower than the combination of the Ribbon read and connect timeout, " + ribbonTimeout + "ms.");
            }
            return hystrixTimeout;
        }
    
        protected static int getRibbonTimeout(IClientConfig config, String commandKey) {
            int ribbonTimeout;
            if (config == null) {
                ribbonTimeout = RibbonClientConfiguration.DEFAULT_READ_TIMEOUT + RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT;
            } else {
                int ribbonReadTimeout = getTimeout(config, commandKey, "ReadTimeout",
                    IClientConfigKey.Keys.ReadTimeout, RibbonClientConfiguration.DEFAULT_READ_TIMEOUT);
                int ribbonConnectTimeout = getTimeout(config, commandKey, "ConnectTimeout",
                    IClientConfigKey.Keys.ConnectTimeout, RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT);
                int maxAutoRetries = getTimeout(config, commandKey, "MaxAutoRetries",
                    IClientConfigKey.Keys.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES);
                int maxAutoRetriesNextServer = getTimeout(config, commandKey, "MaxAutoRetriesNextServer",
                    IClientConfigKey.Keys.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER);
                ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);
            }
            return ribbonTimeout;
        }
    
        private static int getTimeout(IClientConfig config, String commandKey, String property, IClientConfigKey<Integer> configKey, int defaultValue) {
            DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance();
            return dynamicPropertyFactory.getIntProperty(commandKey + "." + config.getNameSpace() + "." + property, config.get(configKey, defaultValue)).get();
        }
    
        @Deprecated
        //TODO remove in 2.0.x
        protected static Setter getSetter(final String commandKey, ZuulProperties zuulProperties) {
            return getSetter(commandKey, zuulProperties, null);
        }
    
        protected static Setter getSetter(final String commandKey,
                ZuulProperties zuulProperties, IClientConfig config) {
    
            // @formatter:off
            Setter commandSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand"))
                                    .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
            final HystrixCommandProperties.Setter setter = createSetter(config, commandKey, zuulProperties);
            if (zuulProperties.getRibbonIsolationStrategy() == ExecutionIsolationStrategy.SEMAPHORE){
                final String name = ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores";
                // we want to default to semaphore-isolation since this wraps
                // 2 others commands that are already thread isolated
                final DynamicIntProperty value = DynamicPropertyFactory.getInstance()
                        .getIntProperty(name, zuulProperties.getSemaphore().getMaxSemaphores());
                setter.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get());
            } else if (zuulProperties.getThreadPool().isUseSeparateThreadPools()) {
                final String threadPoolKey = zuulProperties.getThreadPool().getThreadPoolKeyPrefix() + commandKey;
                commandSetter.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(threadPoolKey));
            }
            
            return commandSetter.andCommandPropertiesDefaults(setter);
            // @formatter:on
        }
    
        @Override
        protected ClientHttpResponse run() throws Exception {
            final RequestContext context = RequestContext.getCurrentContext();
    
            RQ request = createRequest();
            RS response;
            
            boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
                    && ((AbstractLoadBalancingClient)this.client).isClientRetryable((ContextAwareRequest)request);
            
            if (retryableClient) {
                response = this.client.execute(request, config);
            } else {
                response = this.client.executeWithLoadBalancer(request, config);
            }
            context.set("ribbonResponse", response);
    
            // Explicitly close the HttpResponse if the Hystrix command timed out to
            // release the underlying HTTP connection held by the response.
            //
            if (this.isResponseTimedOut()) {
                if (response != null) {
                    response.close();
                }
            }
    
            return new RibbonHttpResponse(response);
        }
    
        @Override
        protected ClientHttpResponse getFallback() {
            if(zuulFallbackProvider != null) {
                return getFallbackResponse();
            }
            return super.getFallback();
        }
    
        protected ClientHttpResponse getFallbackResponse() {
            if (zuulFallbackProvider instanceof FallbackProvider) {
                Throwable cause = getFailedExecutionException();
                cause = cause == null ? getExecutionException() : cause;
                if (cause == null) {
                    zuulFallbackProvider.fallbackResponse();
                } else {
                    return ((FallbackProvider) zuulFallbackProvider).fallbackResponse(cause);
                }
            }
            return zuulFallbackProvider.fallbackResponse();
        }
    
        public LBC getClient() {
            return client;
        }
    
        public RibbonCommandContext getContext() {
            return context;
        }
    
        protected abstract RQ createRequest() throws Exception;
    }
    View Code

      请注意:getRibbonTimeout方法与getHystrixTimeout方法,其中这两个方法 commandKey的值为路由的名称,比如说我们访问:http://localhost:8088/order-server/xxx来访问order-server服务, 那么commandKey 就为order-server

      根据源代码,我们先设置gateway-server的超时参数:

    #全局的ribbon设置
    ribbon:
      ConnectTimeout: 3000
      ReadTimeout: 3000
    hystrix:
      command:
        default:
          execution:
            isolation:
              thread:
                timeoutInMilliseconds: 3000
    zuul:
      host:
        connectTimeoutMillis: 10000
    View Code

      当然也可以单独为order-server设置ribbon的超时参数:order-server.ribbon.xxxx=xxx , 为了演示zuul中的回退效果,我在这里把Hystrix超时时间设置短一点。当然最好不要将Hystrix默认的超时时间设置的比Ribbon的超时时间短,源码里遇到此情况已经给与我们警告了。

      那么我们在order-server下添加如下方法:

        @GetMapping("/sleep/{sleepTime}")
        public String sleep(@PathVariable Long sleepTime) throws InterruptedException {
            TimeUnit.SECONDS.sleep(sleepTime);
            return "SUCCESS";
        }

      

    2、zuul的回退方法

    我们可以实现ZuulFallbackProvider接口,实现代码:

    package com.hzgj.lyrk.springcloud.gateway.server.filter;
    
    import com.google.common.collect.ImmutableMap;
    import com.google.gson.GsonBuilder;
    import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.client.ClientHttpResponse;
    import org.springframework.stereotype.Component;
    
    import java.io.ByteArrayInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.time.LocalDateTime;
    import java.time.LocalTime;
    
    @Component
    public class FallBackHandler implements ZuulFallbackProvider {
    
        @Override
        public String getRoute() {
            //代表所有的路由都适配该设置
            return "*";
        }
    
        @Override
        public ClientHttpResponse fallbackResponse() {
            return new ClientHttpResponse() {
                @Override
                public HttpStatus getStatusCode() throws IOException {
                    return HttpStatus.OK;
                }
    
                @Override
                public int getRawStatusCode() throws IOException {
                    return 200;
                }
    
                @Override
                public String getStatusText() throws IOException {
                    return "OK";
                }
    
                @Override
                public void close() {
    
                }
    
                @Override
                public InputStream getBody() throws IOException {
                    String result = new GsonBuilder().create().toJson(ImmutableMap.of("errorCode", 500, "content", "请求失败", "time", LocalDateTime.now()));
                    return new ByteArrayInputStream(result.getBytes());
                }
    
                @Override
                public HttpHeaders getHeaders() {
                    HttpHeaders headers = new HttpHeaders();
                    headers.setContentType(MediaType.APPLICATION_JSON);
                    return headers;
                }
            };
        }
    }
    View Code

    此时我们访问:http://localhost:8088/order-server/sleep/6 得到如下结果:

     当我们访问:http://localhost:8088/order-server/sleep/1 就得到如下结果:

  • 相关阅读:
    阿里云 k8s 部署 Spring Cloud Alibaba 微服务实践 (四) 自动化部署
    阿里云 k8s 部署 Spring Cloud Alibaba 微服务实践 (三) 服务观测
    阿里云 k8s 部署 Spring Cloud Alibaba 微服务实践 (二) 部署微服务程序
    阿里云 k8s 部署 Spring Cloud Alibaba 微服务实践 (一) 部署 Nacos
    C++知识点
    libmkl 学习笔记
    基于tesseract-OCR进行中文识别
    poco编译与运行
    Linux下的I/O复用与epoll详解(转载)
    高并发网络编程之epoll详解(转载)
  • 原文地址:https://www.cnblogs.com/niechen/p/8856551.html
Copyright © 2011-2022 走看看