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>.pathzuul.routes.<key>.url=<url>指定一个服务的url或者使用forward转向Zuul服务的接口,对应路由路径为zuul.routes.<key>.pathzuul.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)

- 根据serviceId实现路由代理(
http://localhost:8301/userApi/user/exception)

- 根据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/test是zuul-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/exception,http://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会创建一个RibbonCommand,RibbonCommand继承了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的类型包括
pre,routing,post和error。routing过滤器是在路由阶段执行的,负责寻找原服务、请求转发和返回接收。pre和post分别在routing之前和之后执行。如果Zuul执行代理的过程中抛出ZuulException异常,则会被error过滤器捕获并进行相应处理。

- 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的文件测试不同请求方式下的文件上传。
- 直接向
user-service服务发起请求(http://localhost:8002/user/uploadImg),上传大文件成功。

- 使用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)
- 使用Zuul代理添加
/zuul/**前缀绕过SpringDispatcherServlet进行文件上传成功(http://localhost:8301/zuul/userApi2/user/uploadImg)。

- 使用Zuul代理且Ribbon负载均衡的服务,如果不增加超时时间设置,将会自动降级(
http://localhost:8301/zuul/userApi/user/uploadImg)。

- 使用Zuul代理且Ribbon负载均衡的服务,修改Hystrix和Ribbon的超时时间后,上传文件成功(
http://localhost:8301/zuul/userApi/user/uploadImg)。

相关代码
SpringCloudDemo-Zuul
本文深入探讨SpringCloud中的Zuul路由网关,介绍其配置、动态路由、降级策略及过滤器使用。涵盖依赖引入、路由配置、文件上传等关键功能,助力微服务架构的统一接口管理和负载均衡。
168万+

被折叠的 条评论
为什么被折叠?



