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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章