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
)
- 根据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