SpringCloud(七)路由网关Zuul

SpringCloud(七)路由网关Zuul

在微服务架构中,路由是一个很重要的组成部分。比如,/可以映射到你的Web服务,/api/users映射到你的用户服务,而/api/shop可以映射到你的商城服务。SpringCloud中的Zuul是基于JVM的路由和服务端负载均衡器,可以有效地将微服务的接口纳入统一管理暴露给外部。

引入并启用Zuul

新建一个服务zuul-service作为路由网关服务。
pom.xml中引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>

使用@EnableZuulProxy启用Zuul(使用@EnableZuulServer也可以启用Zuul,只是不会自动从Eureka中获取并自动代理服务,也不会自动加载部分Zuul过滤器,但是可以选择性地替换代理平台的各个部分)。

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplicationStarter {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplicationStarter.class, args);
    }
}

路由配置

application.yaml进行zuul路由配置。

info:
  name: Zuul Service

server:
  port: 8301

#不设置为false,就不能调用/routes获取路由表
management:
  security:
    enabled: false

zuul:
  host:
    #代理普通http请求的超时时间
    socket-timeout-millis: 2000
    connect-timeout-millis: 1000
    max-total-connections: 2000
    max-per-route-connections: 200
  ignored-services: 'sms-service'
  routes:
    sms-service: /smsApi/**
    users:
      path: /userApi/**
      service-id: user-service
    users2:
      path: /userApi2/**
      url: http://localhost:8002
    sms2:
      service-id: sms-service
      path: /sms/**
      stripPrefix: false
    forward:
      path: /forward/**
      url: forward:/myZuul
    service-by-ribbon: /service-by-ribbon/**
  #设置zuul.prefix所有请求都需要添加/api前缀
  #prefix: /api
  #strip-prefix: true


########hystrix相关配置
# 注意项:
# 1、zuul环境下,信号量模式下并发量的大小,zuul.semaphore.maxSemaphores这种配置方式优先级最高
# 2、zuul环境下,资源隔离策略默认信号量,zuul.ribbonIsolationStrategy这种配置方式优先级最高
# 3、zuul环境下,commandGroup 固定为RibbonCommand
# 4、zuul环境下,commandKey 对应每个服务的serviceId
#
hystrix:
  command:
    # 这是默认的配置
    default:
      execution:
        timeout:
          enabled: true
        isolation:
          thread:
            # 命令执行超时时间
            timeoutInMilliseconds: 2000


ribbon:
  # 配置ribbon默认的超时时间
  ConnectTimeout: 1000
  ReadTimeout: 2000
  # 是否开启重试
  OkToRetryOnAllOperations: true
  # 重试期间,实例切换次数
  MaxAutoRetriesNextServer: 1
  # 当前实例重试次数
  MaxAutoRetries: 0
  eureka:
    enabled: false

# 定义一个针对service-by-ribbon服务的负载均衡器,服务实例信息来自配置文件,zuul默认可以集成
# 服务名
service-by-ribbon:
  # 服务实例列表
  listOfServers: http://localhost:8001
  ribbon:
    # 负载策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    # 设置它的服务实例信息来自配置文件, 如果不设置NIWSServerListClassName就会去euereka里面找
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
  • Zuul会自动读取注册中心的已经注册的服务。user-service服务会自动设置/user-service/**这样的路由,即/user-service/users会被代理到user-service服务的/users请求。
  • zuul.ignoredServices可以指定忽略注册中心获取的服务
  • zuul.routes.<serviceId>=<path>路由key使用一个服务名称,对应一个路由路径
  • zuul.routes.<key>.serviceId=<serviceId>指定一个服务对应路由路径为zuul.routes.<key>.path
  • zuul.routes.<key>.url=<url>指定一个服务的url或者使用forward转向Zuul服务的接口,对应路由路径为zuul.routes.<key>.path
  • zuul.routes.<ribbon>=<path>使用自定义Ribbon实现路由

注意:<url>是服务的请求路径,<path>是设置的代理路径
<serviceId>和<url>不能同时存在,即一个路由要么对应一个url,要么对应一个服务

Zuul服务启动完成后,可以访问http://localhost:8301/routes获取路由列表
在这里插入图片描述

{
    "/userApi/**": "user-service",
    "/userApi2/**": "http://localhost:8002",
    "/sms/**": "sms-service",
    "/forward/**": "forward:/myZuul",
    "/smsApi/**": "sms-service",
    "/service-by-ribbon/**": "service-by-ribbon",
    "/zuul-service/**": "zuul-service",
    "/eureka-server/**": "eureka-server",
    "/config-server/**": "config-server",
    "/user-service/**": "user-service"
}

这样我们就能根据不同的请求路径实现路由和代理功能。

  • 根据Eureka发现服务实现路由代理(http://localhost:8301/user-service/user/exception
    在这里插入图片描述
  • 根据路由key实现路由代理(http://localhost:8301/smsApi/sms
    根据路由key实现调用
  • 根据serviceId实现路由代理(http://localhost:8301/userApi/user/exception
    根据serviceId实现调用
  • 根据url实现路由代理(http://localhost:8301/userApi2/user/exception
    在这里插入图片描述
  • 使用zuul.routes.<routeName>.stripPrefix=false在向服务发起请求时不会去掉path前缀,即http://localhost:8301/sms会代理到sms-service服务的/sms接口(如果stripPrefix设置为true我们需要使用http://localhost:8301/sms/sms才能正常访问到这个接口)。
    在这里插入图片描述
  • forward将请求转发至本地处理(http://localhost:8301/forward/test)会将请求转发至本地的/myZuul/test接口。
    在这里插入图片描述
    /myZuul/testzuul-service的一个接口,如下:
@RestController
@RequestMapping("/myZuul")
public class MyZuulController {

    @RequestMapping("/test")
    public String test() {
        return "Hello, you are visiting a local endpoint!";
    }
}
  • 使用Ribbon配置的服务(localhost:8301/service-by-ribbon/sms
    在这里插入图片描述
    设置zuul.prefix=/api后,意味着给所有的路由设置了一个全局的前缀,所有的请求前面增加/api前缀即可。如http://localhost:8301/api/user-service/user/exceptionhttp://localhost:8301/api/smsApi/sms等。

动态路由

Zuul结合SpringCloud配置中心,在修改路由配置信息后刷新配置可立即生效,无需重启Zuul服务,这样就实现了动态路由。

降级策略

当一个路由短路时,可以使用一个自定义的ZuulFallbackProvider实现服务降级。在这个bean里面你需要指定路由id并且返回一个ClientHttpReponse作为服务降级之后的请求结果。
下面是一个Zuul的降级实现

@Component
public class UserFallbackProvider implements ZuulFallbackProvider {

    /**
     * 对应的路由id,如果所有路由使用同一个fallback就返回*或者null
     * @return
     */
    @Override
    public String getRoute() {
        // return "user-service";
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        ClientHttpResponse response = 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 {
                return new ByteArrayInputStream("invoke failed, fallback...".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.TEXT_PLAIN);
                return headers;
            }
        };
        return response;
    }
}

getRoute()方法返回的是路由id,如果希望这个降级策略对所有路由生效,返回null或者*即可。
访问http://localhost:8301/userApi/user/timeout或者http://localhost:8301/user-service/user/timeout
在这里插入图片描述
访问http://localhost:8301/userApi2/user/timeout会出现超时的错误。尽管我们的降级策略针对的是所有路由,但是/userApi2/**走的是url配置的路由,ZuulFallbackProvider只会对Ribbon进行寻路的路由生效。使用url的路由在寻找原服务时使用的是SimpleHostRoutingFilter。从Eureka中读取的服务,使用
zuul.routes.<serviceId>=<path>zuul.routes.<key>.serviceId=<serviceId>zuul.routes.<ribbon>=<path>这种方式配置的路由会使用RibbonRoutingFilter进行寻路。RibbonRoutingFilter会创建一个RibbonCommandRibbonCommand继承了HystrixExecutable

	protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
		Map<String, Object> info = this.helper.debug(context.getMethod(),
				context.getUri(), context.getHeaders(), context.getParams(),
				context.getRequestEntity());
		// 创建RibonCommand
		RibbonCommand command = this.ribbonCommandFactory.create(context);
		try {
			ClientHttpResponse response = command.execute();
			this.helper.appendDebug(info, response.getStatusCode().value(),
					response.getHeaders());
			return response;
		}
		catch (HystrixRuntimeException ex) {
			return handleException(info, ex);
		}

	}

HttpClientRibbonCommandFactory.java中创建HttpClientRibbonCommand

	@Override
	public HttpClientRibbonCommand create(final RibbonCommandContext context) {
		// 根据serviceId获取ZuulFallbackProvider
		ZuulFallbackProvider zuulFallbackProvider = getFallbackProvider(context.getServiceId());
		final String serviceId = context.getServiceId();
		final RibbonLoadBalancingHttpClient client = this.clientFactory.getClient(
				serviceId, RibbonLoadBalancingHttpClient.class);
		client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));

		return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties, zuulFallbackProvider,
				clientFactory.getClientConfig(serviceId));
	}

Zuul Filter

Zuul进行代理时,会有一系列的Zuul Filter对Http请求的request和response进行封装和操作。
一个Zuul Filter有下面四个要素:

  • Type:类型。Zuul Filter的类型包括preroutingposterrorrouting过滤器是在路由阶段执行的,负责寻找原服务、请求转发和返回接收。prepost分别在routing之前和之后执行。如果Zuul执行代理的过程中抛出ZuulException异常,则会被error过滤器捕获并进行相应处理。
    Zuul Request Lifecycle
  • Execution Order:执行顺序。通过一个整型的值从小到大依次执行(相同类型过滤器间互相比较)。
  • Criteria:执行条件。当满足一定条件时,才会执行该过滤器。
  • Action:执行动作。当执行条件满足时,进行的操作。
    实现一个过滤器只要继承ZuulFilter,并实现filterType()filterOrder()shouldFilter()run()四个方法。这些方法与上面的四个要素对应。
    如果要禁用一个Zuul过滤器,只需要配置zuul.<SimpleClassName>.<filterType>.disable=true,比如需要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter需要配置zuul.SendResponseFilter.post.disable=true
    下面我们使用一个pre过滤器实现token验证,如果Http header里面没有一个固定的token,则禁止访问。
    禁用Zuul默认的error过滤器,设置固定的token和需要验证的路由key名单
zuul:
 # 禁用SpringCloud自带的error filter
 SendErrorFilter:
   error:
     disable: true

zuul-filter:
 token-filter:
   # 访问时,需要进行认证的路由key
   un-auth-routes:
     - users
     - smsApi
   # 固定的token
   static-token: xF2fdi8M

读取自定义token配置信息

@Component
@ConfigurationProperties("zuulFilter.tokenFilter")
public class TokenValidateConfiguration {

    // 在这个列表里面存储的routeId都是需要使用TokenValidateFilter过滤的
    private List<String> unAuthRoutes;

    // 给定的token
    private String staticToken;

    public List<String> getUnAuthRoutes() {
        return unAuthRoutes;
    }

    public void setUnAuthRoutes(List<String> unAuthRoutes) {
        this.unAuthRoutes = unAuthRoutes;
    }

    public String getStaticToken() {
        return staticToken;
    }

    public void setStaticToken(String staticToken) {
        this.staticToken = staticToken;
    }
}

自定义过滤器

@Component
public class TokenValidateFilter extends ZuulFilter {

    protected static final Logger logger = LoggerFactory.getLogger(TokenValidateFilter.class);

    @Autowired
    private TokenValidateConfiguration tvConfig;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        return tvConfig.getUnAuthRoutes().contains(ctx.get(FilterConstants.PROXY_KEY));
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getHeader("Authorization");
        if (token == null) {
            logger.warn("Http Header Authorization is null");
            forbidden();
            return null;
        }

        String staticToken = tvConfig.getStaticToken();
        if (StringUtils.isBlank(staticToken)) {
            logger.warn("property zuulFilter.tokenFilter.staticToken was not set");
            forbidden();
        } else if (!staticToken.equals(token)) {
            logger.warn("token is not valid");
            forbidden();
        }
        return null;
    }

    /**
     * 设置response的状态码为403
     */
    private void forbidden() {
        // zuul中,将请求附带的信息存在线程变量中。
        RequestContext.getCurrentContext().setResponseStatusCode(HttpStatus.FORBIDDEN.value());
        ReflectionUtils.rethrowRuntimeException(new ZuulException("token is not valid", HttpStatus.FORBIDDEN.value(),
                "token校验不通过"));
    }
} 

注意:如果使用zuul.routes.<serviceId>=<url>方式配置的路由,则ctx.get(FilterConstants.PROXY_KEY)会得到去掉头尾的url(/smsApi/**会得到smsApi/smsApi/target/**会得到smsApi/target),而并非路由key。所以之前配置文件中的路由

zuul:
  routes:
    sms-service: /smsApi/**

在请求的时候也需要携带token信息。

  • 不携带token直接访问http://localhost:8301/userApi/user/exception
    在这里插入图片描述
  • 访问http://localhost:8301/userApi2/user/exception时不需要进行拦截
    在这里插入图片描述
  • 携带正确的token访问http://localhost:8301/userApi/user/exception
    在这里插入图片描述
    说明我们的TokenValidateFilter生效了。
    类似地我们可以新建一个error过滤器,当捕获到ZuulException时,返回一个JSON对象。
@Component
public class SendErrorRestFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(SendErrorRestFilter.class);

    @Override
    public String filterType() {
        return FilterConstants.ERROR_TYPE;
    }

    @Override
    public int filterOrder() {
        return FilterConstants.SEND_ERROR_FILTER_ORDER;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = getCause(context.getThrowable());
        // 获取response状态码
        int status = context.getResponseStatusCode();
        JSONObject info = new JSONObject();
        info.put("code", "异常码" + status);
        info.put("message", throwable.getMessage());
        // 记录日志
        logger.warn("请求异常,被error filter拦截", context.getClass());

        // 设置response
        context.setResponseBody(info.toJSONString());
        context.getResponse().setContentType("application/json;charset=UTF-8");
        context.getResponse().setStatus(HttpStatus.OK.value());

        // 处理了异常之后清空异常
        context.remove("throwable");
        return null;
    }

    private Throwable getCause(Throwable throwable) {
        while (throwable.getCause() != null) {
            throwable = throwable.getCause();
        }
        return throwable;
    }
}

我们仍然关闭默认的error过滤器,不使用token访问http://localhost:8301/userApi/user/exception。可以看到返回的状态码已经变成了200,且返回数据为json。
在这里插入图片描述

使用Zuul上传文件

如果使用了@EnableZuulProxy代理路径上传文件,要尽量保证文件很小,避免超时。对于大文件,有一个替代路径/zuul/**可以绕过Spring DispatcherServlet(避免Multipart处理)。即如果 zuul.routes.customers=/customers/**那样你可以将大文件发送到“/ zuul / customers / *”。zuul.servletPath使得servlet路径外部化。如果大文件通过Ribbon上传也需要提升超时设置,例如

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

我们在user-service服务中增加一个/user/uploadImg接口用于上传文件。

    @RequestMapping("/uploadImg")
    public String uploadImg(MultipartFile file) throws IOException {
        String srcName = file.getOriginalFilename();
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String dstName = "D:/springcloud/upload/" + uuid +"-" + srcName;
        File dstFile = new File(dstName);
        File parentFile = dstFile.getParentFile();
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        try (InputStream in = file.getInputStream(); OutputStream out = new FileOutputStream(dstFile)) {
            StreamUtils.copy(in, out);
        }
        return dstName;
    }

我们对Multipart进行设置,允许大文件的上传。

@Configuration
public class UploadConfig {

    public static final String maxFileSize = "1024MB";

    public static final String maxRequestSize = "2048MB";

    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        // 单个文件最大
        factory.setMaxFileSize(maxFileSize);
        // 设置总上传数据总大小
        factory.setMaxRequestSize(maxRequestSize);
        return factory.createMultipartConfig();
    }
}

或者直接作如下设置

spring:
	http:
		multipart:
	      max-file-size: 1024MB
	      max-request-size: 2048MB

下面将使用一个大约25M的文件测试不同请求方式下的文件上传。

  1. 直接向user-service服务发起请求(http://localhost:8002/user/uploadImg),上传大文件成功。
    在这里插入图片描述
  2. 使用Zuul代理到user-service服务上传大文件失败,由于文件过大请求被拒绝,后台报错信息如下(http://localhost:8301/userApi2/user/uploadImg)。
    大文件上传失败
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (26977350) exceeds the configured maximum (10485760)
  1. 使用Zuul代理添加/zuul/**前缀绕过Spring DispatcherServlet进行文件上传成功(http://localhost:8301/zuul/userApi2/user/uploadImg)。
    Zuul上传文件绕过DispatcherServlet
  2. 使用Zuul代理且Ribbon负载均衡的服务,如果不增加超时时间设置,将会自动降级(http://localhost:8301/zuul/userApi/user/uploadImg)。
    在这里插入图片描述
  3. 使用Zuul代理且Ribbon负载均衡的服务,修改Hystrix和Ribbon的超时时间后,上传文件成功(http://localhost:8301/zuul/userApi/user/uploadImg)。
    在这里插入图片描述

相关代码
SpringCloudDemo-Zuul


参考
Zuul Wiki-How it works

猜你喜欢

转载自blog.csdn.net/chenxyz707/article/details/82854142