Spring Cloud学习教程之Zuul统一异常处理与回退

网友投稿 380 2023-02-07


Spring Cloud学习教程之Zuul统一异常处理与回退

前言

Zuul 是Netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年公司的架构组决定自研一个网关产品,集动态路由,动态权限,限流配额等功能为一体,为其他部门的项目提供统一的外网调用管理,最终形成产品(这方面阿里其实已经有成熟的网关产品了,但是不太适用于个性化的配置,也没有集成权限和限流降级)。

本文主要给大家介绍了关于Spring Cloud 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://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;

}

}

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

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

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

request.setAttribute("javax.servlet.error.http://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;

}

}

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

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 error(HttpServletRequest request) {

String message = request.getAttribute("javax.servlet.error.message").toString(bxYwrQh);

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;

}

}

}

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

二、关于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://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, RQ extends ClientRequest, RS extends HttpResponse>

extends HystrixCommand 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 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;

}

请注意: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

当然也可以单独为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;

}

};

}

}

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

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java基于命令模式实现邮局发信功能详解
下一篇:Spring Cloud学习教程之DiscoveryClient的深入探究
相关文章

 发表评论

暂时没有评论,来抢沙发吧~