查看: 275|回复: 0

[Java代码] Spring Cloud学习教程之Zuul统一异常处理与回退

发表于 2018-5-4 08:00:02

前言

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

本文主要给大家介绍了关于Spring Cloud Zuul统一异常处理与回退的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

一、Filter中统一异常处理

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

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

  1. /*
  2. * Copyright 2013-2015 the original author or authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.springframework.cloud.netflix.zuul.filters.post;
  17. import javax.servlet.RequestDispatcher;
  18. import javax.servlet.http.HttpServletRequest;
  19. import javax.servlet.http.HttpServletResponse;
  20. import org.apache.commons.logging.Log;
  21. import org.apache.commons.logging.LogFactory;
  22. import org.springframework.beans.factory.annotation.Value;
  23. import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
  24. import org.springframework.util.ReflectionUtils;
  25. import org.springframework.util.StringUtils;
  26. import com.netflix.zuul.ZuulFilter;
  27. import com.netflix.zuul.context.RequestContext;
  28. import com.netflix.zuul.exception.ZuulException;
  29. import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ERROR_TYPE;
  30. import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_ERROR_FILTER_ORDER;
  31. /**
  32. * Error {[url=home.php?mod=space&uid=17823]@LINK[/url] ZuulFilter} that forwards to /error (by default) if {@link RequestContext#getThrowable()} is not null.
  33. *
  34. * @author Spencer Gibb
  35. */
  36. //TODO: move to error package in Edgware
  37. public class SendErrorFilter extends ZuulFilter {
  38. private static final Log log = LogFactory.getLog(SendErrorFilter.class);
  39. protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
  40. @Value("${error.path:/error}")
  41. private String errorPath;
  42. @Override
  43. public String filterType() {
  44. return ERROR_TYPE;
  45. }
  46. @Override
  47. public int filterOrder() {
  48. return SEND_ERROR_FILTER_ORDER;
  49. }
  50. @Override
  51. public boolean shouldFilter() {
  52. RequestContext ctx = RequestContext.getCurrentContext();
  53. // only forward to errorPath if it hasn't been forwarded to already
  54. return ctx.getThrowable() != null
  55. && !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
  56. }
  57. @Override
  58. public Object run() {
  59. try {
  60. RequestContext ctx = RequestContext.getCurrentContext();
  61. ZuulException exception = findZuulException(ctx.getThrowable());
  62. HttpServletRequest request = ctx.getRequest();
  63. request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);
  64. log.warn("Error during filtering", exception);
  65. request.setAttribute("javax.servlet.error.exception", exception);
  66. if (StringUtils.hasText(exception.errorCause)) {
  67. request.setAttribute("javax.servlet.error.message", exception.errorCause);
  68. }
  69. RequestDispatcher dispatcher = request.getRequestDispatcher(
  70. this.errorPath);
  71. if (dispatcher != null) {
  72. ctx.set(SEND_ERROR_FILTER_RAN, true);
  73. if (!ctx.getResponse().isCommitted()) {
  74. ctx.setResponseStatusCode(exception.nStatusCode);
  75. dispatcher.forward(request, ctx.getResponse());
  76. }
  77. }
  78. }
  79. catch (Exception ex) {
  80. ReflectionUtils.rethrowRuntimeException(ex);
  81. }
  82. return null;
  83. }
  84. ZuulException findZuulException(Throwable throwable) {
  85. if (throwable.getCause() instanceof ZuulRuntimeException) {
  86. // this was a failure initiated by one of the local filters
  87. return (ZuulException) throwable.getCause().getCause();
  88. }
  89. if (throwable.getCause() instanceof ZuulException) {
  90. // wrapped zuul exception
  91. return (ZuulException) throwable.getCause();
  92. }
  93. if (throwable instanceof ZuulException) {
  94. // exception thrown by zuul lifecycle
  95. return (ZuulException) throwable;
  96. }
  97. // fallback, should never get here
  98. return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
  99. }
  100. public void setErrorPath(String errorPath) {
  101. this.errorPath = errorPath;
  102. }
  103. }
复制代码

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

  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:

  1. package com.hzgj.lyrk.springcloud.gateway.server.filter;
  2. import com.netflix.zuul.ZuulFilter;
  3. import lombok.extern.slf4j.Slf4j;
  4. import org.springframework.stereotype.Component;
  5. @Component
  6. @Slf4j
  7. public class MyZuulFilter extends ZuulFilter {
  8. @Override
  9. public String filterType() {
  10. return "post";
  11. }
  12. @Override
  13. public int filterOrder() {
  14. return 9;
  15. }
  16. @Override
  17. public boolean shouldFilter() {
  18. return true;
  19. }
  20. @Override
  21. public Object run() {
  22. log.info("run error test ...");
  23. throw new RuntimeException();
  24. // return null;
  25. }
  26. }
复制代码

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

  1. package com.hzgj.lyrk.springcloud.gateway.server.filter;
  2. import org.springframework.http.HttpStatus;
  3. import org.springframework.http.ResponseEntity;
  4. import org.springframework.web.bind.annotation.GetMapping;
  5. import org.springframework.web.bind.annotation.RestController;
  6. import javax.servlet.http.HttpServletRequest;
  7. @RestController
  8. public class ErrorHandler {
  9. @GetMapping(value = "/error")
  10. public ResponseEntity<ErrorBean> error(HttpServletRequest request) {
  11. String message = request.getAttribute("javax.servlet.error.message").toString();
  12. ErrorBean errorBean = new ErrorBean();
  13. errorBean.setMessage(message);
  14. errorBean.setReason("程序出错");
  15. return new ResponseEntity<>(errorBean, HttpStatus.BAD_GATEWAY);
  16. }
  17. private static class ErrorBean {
  18. private String message;
  19. private String reason;
  20. public String getMessage() {
  21. return message;
  22. }
  23. public void setMessage(String message) {
  24. this.message = message;
  25. }
  26. public String getReason() {
  27. return reason;
  28. }
  29. public void setReason(String reason) {
  30. this.reason = reason;
  31. }
  32. }
  33. }
复制代码

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

二、关于zuul回退的问题

1、关于zuul的超时问题:

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

  1. /*
  2. * Copyright 2013-2016 the original author or authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. package org.springframework.cloud.netflix.zuul.filters.route.support;
  18. import org.apache.commons.logging.Log;
  19. import org.apache.commons.logging.LogFactory;
  20. import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration;
  21. import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
  22. import org.springframework.cloud.netflix.ribbon.support.AbstractLoadBalancingClient;
  23. import org.springframework.cloud.netflix.ribbon.support.ContextAwareRequest;
  24. import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
  25. import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand;
  26. import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
  27. import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
  28. import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
  29. import org.springframework.http.client.ClientHttpResponse;
  30. import com.netflix.client.AbstractLoadBalancerAwareClient;
  31. import com.netflix.client.ClientRequest;
  32. import com.netflix.client.config.DefaultClientConfigImpl;
  33. import com.netflix.client.config.IClientConfig;
  34. import com.netflix.client.config.IClientConfigKey;
  35. import com.netflix.client.http.HttpResponse;
  36. import com.netflix.config.DynamicIntProperty;
  37. import com.netflix.config.DynamicPropertyFactory;
  38. import com.netflix.hystrix.HystrixCommand;
  39. import com.netflix.hystrix.HystrixCommandGroupKey;
  40. import com.netflix.hystrix.HystrixCommandKey;
  41. import com.netflix.hystrix.HystrixCommandProperties;
  42. import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy;
  43. import com.netflix.hystrix.HystrixThreadPoolKey;
  44. import com.netflix.zuul.constants.ZuulConstants;
  45. import com.netflix.zuul.context.RequestContext;
  46. /**
  47. * @author Spencer Gibb
  48. */
  49. public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwareClient<RQ, RS>, RQ extends ClientRequest, RS extends HttpResponse>
  50. extends HystrixCommand<ClientHttpResponse> implements RibbonCommand {
  51. private static final Log LOGGER = LogFactory.getLog(AbstractRibbonCommand.class);
  52. protected final LBC client;
  53. protected RibbonCommandContext context;
  54. protected ZuulFallbackProvider zuulFallbackProvider;
  55. protected IClientConfig config;
  56. public AbstractRibbonCommand(LBC client, RibbonCommandContext context,
  57. ZuulProperties zuulProperties) {
  58. this("default", client, context, zuulProperties);
  59. }
  60. public AbstractRibbonCommand(String commandKey, LBC client,
  61. RibbonCommandContext context, ZuulProperties zuulProperties) {
  62. this(commandKey, client, context, zuulProperties, null);
  63. }
  64. public AbstractRibbonCommand(String commandKey, LBC client,
  65. RibbonCommandContext context, ZuulProperties zuulProperties,
  66. ZuulFallbackProvider fallbackProvider) {
  67. this(commandKey, client, context, zuulProperties, fallbackProvider, null);
  68. }
  69. public AbstractRibbonCommand(String commandKey, LBC client,
  70. RibbonCommandContext context, ZuulProperties zuulProperties,
  71. ZuulFallbackProvider fallbackProvider, IClientConfig config) {
  72. this(getSetter(commandKey, zuulProperties, config), client, context, fallbackProvider, config);
  73. }
  74. protected AbstractRibbonCommand(Setter setter, LBC client,
  75. RibbonCommandContext context,
  76. ZuulFallbackProvider fallbackProvider, IClientConfig config) {
  77. super(setter);
  78. this.client = client;
  79. this.context = context;
  80. this.zuulFallbackProvider = fallbackProvider;
  81. this.config = config;
  82. }
  83. protected static HystrixCommandProperties.Setter createSetter(IClientConfig config, String commandKey, ZuulProperties zuulProperties) {
  84. int hystrixTimeout = getHystrixTimeout(config, commandKey);
  85. return HystrixCommandProperties.Setter().withExecutionIsolationStrategy(
  86. zuulProperties.getRibbonIsolationStrategy()).withExecutionTimeoutInMilliseconds(hystrixTimeout);
  87. }
  88. protected static int getHystrixTimeout(IClientConfig config, String commandKey) {
  89. int ribbonTimeout = getRibbonTimeout(config, commandKey);
  90. DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance();
  91. int defaultHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds",
  92. 0).get();
  93. int commandHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command." + commandKey + ".execution.isolation.thread.timeoutInMilliseconds",
  94. 0).get();
  95. int hystrixTimeout;
  96. if(commandHystrixTimeout > 0) {
  97. hystrixTimeout = commandHystrixTimeout;
  98. }
  99. else if(defaultHystrixTimeout > 0) {
  100. hystrixTimeout = defaultHystrixTimeout;
  101. } else {
  102. hystrixTimeout = ribbonTimeout;
  103. }
  104. if(hystrixTimeout < ribbonTimeout) {
  105. LOGGER.warn("The Hystrix timeout of " + hystrixTimeout + "ms for the command " + commandKey +
  106. " is set lower than the combination of the Ribbon read and connect timeout, " + ribbonTimeout + "ms.");
  107. }
  108. return hystrixTimeout;
  109. }
  110. protected static int getRibbonTimeout(IClientConfig config, String commandKey) {
  111. int ribbonTimeout;
  112. if (config == null) {
  113. ribbonTimeout = RibbonClientConfiguration.DEFAULT_READ_TIMEOUT + RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT;
  114. } else {
  115. int ribbonReadTimeout = getTimeout(config, commandKey, "ReadTimeout",
  116. IClientConfigKey.Keys.ReadTimeout, RibbonClientConfiguration.DEFAULT_READ_TIMEOUT);
  117. int ribbonConnectTimeout = getTimeout(config, commandKey, "ConnectTimeout",
  118. IClientConfigKey.Keys.ConnectTimeout, RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT);
  119. int maxAutoRetries = getTimeout(config, commandKey, "MaxAutoRetries",
  120. IClientConfigKey.Keys.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES);
  121. int maxAutoRetriesNextServer = getTimeout(config, commandKey, "MaxAutoRetriesNextServer",
  122. IClientConfigKey.Keys.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER);
  123. ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);
  124. }
  125. return ribbonTimeout;
  126. }
  127. private static int getTimeout(IClientConfig config, String commandKey, String property, IClientConfigKey<Integer> configKey, int defaultValue) {
  128. DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance();
  129. return dynamicPropertyFactory.getIntProperty(commandKey + "." + config.getNameSpace() + "." + property, config.get(configKey, defaultValue)).get();
  130. }
  131. @Deprecated
  132. //TODO remove in 2.0.x
  133. protected static Setter getSetter(final String commandKey, ZuulProperties zuulProperties) {
  134. return getSetter(commandKey, zuulProperties, null);
  135. }
  136. protected static Setter getSetter(final String commandKey,
  137. ZuulProperties zuulProperties, IClientConfig config) {
  138. // @formatter:off
  139. Setter commandSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand"))
  140. .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
  141. final HystrixCommandProperties.Setter setter = createSetter(config, commandKey, zuulProperties);
  142. if (zuulProperties.getRibbonIsolationStrategy() == ExecutionIsolationStrategy.SEMAPHORE){
  143. final String name = ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores";
  144. // we want to default to semaphore-isolation since this wraps
  145. // 2 others commands that are already thread isolated
  146. final DynamicIntProperty value = DynamicPropertyFactory.getInstance()
  147. .getIntProperty(name, zuulProperties.getSemaphore().getMaxSemaphores());
  148. setter.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get());
  149. } else if (zuulProperties.getThreadPool().isUseSeparateThreadPools()) {
  150. final String threadPoolKey = zuulProperties.getThreadPool().getThreadPoolKeyPrefix() + commandKey;
  151. commandSetter.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(threadPoolKey));
  152. }
  153. return commandSetter.andCommandPropertiesDefaults(setter);
  154. // @formatter:on
  155. }
  156. @Override
  157. protected ClientHttpResponse run() throws Exception {
  158. final RequestContext context = RequestContext.getCurrentContext();
  159. RQ request = createRequest();
  160. RS response;
  161. boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
  162. && ((AbstractLoadBalancingClient)this.client).isClientRetryable((ContextAwareRequest)request);
  163. if (retryableClient) {
  164. response = this.client.execute(request, config);
  165. } else {
  166. response = this.client.executeWithLoadBalancer(request, config);
  167. }
  168. context.set("ribbonResponse", response);
  169. // Explicitly close the HttpResponse if the Hystrix command timed out to
  170. // release the underlying HTTP connection held by the response.
  171. //
  172. if (this.isResponseTimedOut()) {
  173. if (response != null) {
  174. response.close();
  175. }
  176. }
  177. return new RibbonHttpResponse(response);
  178. }
  179. @Override
  180. protected ClientHttpResponse getFallback() {
  181. if(zuulFallbackProvider != null) {
  182. return getFallbackResponse();
  183. }
  184. return super.getFallback();
  185. }
  186. protected ClientHttpResponse getFallbackResponse() {
  187. if (zuulFallbackProvider instanceof FallbackProvider) {
  188. Throwable cause = getFailedExecutionException();
  189. cause = cause == null ? getExecutionException() : cause;
  190. if (cause == null) {
  191. zuulFallbackProvider.fallbackResponse();
  192. } else {
  193. return ((FallbackProvider) zuulFallbackProvider).fallbackResponse(cause);
  194. }
  195. }
  196. return zuulFallbackProvider.fallbackResponse();
  197. }
  198. public LBC getClient() {
  199. return client;
  200. }
  201. public RibbonCommandContext getContext() {
  202. return context;
  203. }
  204. protected abstract RQ createRequest() throws Exception;
  205. }
复制代码

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

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

  1. #全局的ribbon设置
  2. ribbon:
  3. ConnectTimeout: 3000
  4. ReadTimeout: 3000
  5. hystrix:
  6. command:
  7. default:
  8. execution:
  9. isolation:
  10. thread:
  11. timeoutInMilliseconds: 3000
  12. zuul:
  13. host:
  14. connectTimeoutMillis: 10000
复制代码

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

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

  1. @GetMapping("/sleep/{sleepTime}")
  2. public String sleep(@PathVariable Long sleepTime) throws InterruptedException {
  3. TimeUnit.SECONDS.sleep(sleepTime);
  4. return "SUCCESS";
  5. }
复制代码

2、zuul的回退方法

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

  1. package com.hzgj.lyrk.springcloud.gateway.server.filter;
  2. import com.google.common.collect.ImmutableMap;
  3. import com.google.gson.GsonBuilder;
  4. import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
  5. import org.springframework.http.HttpHeaders;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.MediaType;
  8. import org.springframework.http.client.ClientHttpResponse;
  9. import org.springframework.stereotype.Component;
  10. import java.io.ByteArrayInputStream;
  11. import java.io.IOException;
  12. import java.io.InputStream;
  13. import java.time.LocalDateTime;
  14. import java.time.LocalTime;
  15. @Component
  16. public class FallBackHandler implements ZuulFallbackProvider {
  17. @Override
  18. public String getRoute() {
  19. //代表所有的路由都适配该设置
  20. return "*";
  21. }
  22. @Override
  23. public ClientHttpResponse fallbackResponse() {
  24. return new ClientHttpResponse() {
  25. @Override
  26. public HttpStatus getStatusCode() throws IOException {
  27. return HttpStatus.OK;
  28. }
  29. @Override
  30. public int getRawStatusCode() throws IOException {
  31. return 200;
  32. }
  33. @Override
  34. public String getStatusText() throws IOException {
  35. return "OK";
  36. }
  37. @Override
  38. public void close() {
  39. }
  40. @Override
  41. public InputStream getBody() throws IOException {
  42. String result = new GsonBuilder().create().toJson(ImmutableMap.of("errorCode", 500, "content", "请求失败", "time", LocalDateTime.now()));
  43. return new ByteArrayInputStream(result.getBytes());
  44. }
  45. @Override
  46. public HttpHeaders getHeaders() {
  47. HttpHeaders headers = new HttpHeaders();
  48. headers.setContentType(MediaType.APPLICATION_JSON);
  49. return headers;
  50. }
  51. };
  52. }
  53. }
复制代码

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

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

总结

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



回复

使用道具 举报