有些人可能学了Spring-Cloud后想要将Spring-Cloud整合进自己的项目,但是自己的项目是一个旧项目。那么要想升级就会很复杂,但是这一节讲的网关至少是可以轻松与旧项目整合起来的。
Spring-Cloud体系中后端的服务将会有无法估量的数量,可能只有几个也可能有上百个,并且同一个服务可能都会部署好几个,那么让前端直接调用,我想前端心态要炸了。所以为了简化前端的调用,就有了zuul这样的api gateway。同时它也可以提供负载均衡、反向代理、权限认证的作用。
那么我就来看看zuul怎么使用吧!
本案例使用Spring-Cloud Hoxton.SR5 版本
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>
spring:
application:
name: spring-cloud-zuul
server:
port: 8888
zuul:
routes:
baidu:
path: /it/** #访问/it/**都会重定向到百度首页
url: http://www.ityouknow.com/
hello:
path: /hello/**
url: http://localhost:9000/
启动类中添加@EnableZuulProxy
注解启用zuul代理
@SpringBootApplication
@EnableZuulProxy
public class SpringCloudZuulApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudZuulApplication.class, args);
}
}
将服务启动,访问http://localhost:8888/it/spring-cloud
就会看到纯洁的微笑
的Spring Cloud
系列博客。
我们再将之前的服务提供者启动,然后访问http://localhost:8888/hello/hello?name=bennett
就可以正常得到hello
接口的返回了。
利用这一点我们就可以将旧项目和Spring-Cloud项目整合在一起。
上面的提供者服务只有一个,如果有很多个我们就要配置很多个这样的路由,很显然这样做会很累的。那么这时候我们就需要依赖Eureka这样的注册发现服务来帮助我们完成这个工作了,zuul默认会代理所有在Eureka里注册的服务,这样我们就不必每个服务都手动设置代理了。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
我们先来尝试手动的代理注册到Eureka的服务。
zuul:
routes:
api-a:
path: /producer/**
serviceId: spring-cloud-producer
eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka/
然后重启zuul,并将Eureka启动。
访问http://localhost:8888/producer/hello?name=bennett
,可以正常得到我们的hello接口的返回信息。
现在我们把刚才配置的路由去掉,重启zuul。
再次访问http://localhost:8888/spring-cloud-producer/hello?name=bennett
,我们可以得到跟刚才一样的返回信息,如果觉得是缓存也可以把name
的值换成别的。
访问zuul代理的服务的规则:http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**
如果因为网络等原因导致路由代理失败,那么服务就会暂时不可用,如果我们希望可以连接重试的话zuul也为我们实现了这个功能,它需要与spring-retry
结合使用。
<dependency>
<groupId>org.springframework.retrygroupId>
<artifactId>spring-retryartifactId>
dependency>
zuul:
retryable: true #开启重试
ribbon:
MaxAutoRetries: 2 # 当前服务最大重试次数
MaxAutoRetriesNextServer: 0 # 切换相同Server的次数
将zuul服务重启,为了模拟超时重试,我们可以用线程睡眠实现。
修改服务提供者的hello接口,让这个接口的线程睡眠60000毫秒,并且在执行睡眠前添加日志看是否有重试。
@RestController
public class HelloController {
private static final Logger log = LoggerFactory.getLogger(HelloController.class);
@RequestMapping("/hello")
public String hello(String name, HttpServletRequest request) {
int port = request.getServerPort();
log.info("request from {}, name is {}", port, name);
// 模拟超时异常
try {
Thread.sleep(1000000);
} catch (Exception e) {
log.error(" hello {} error", port, e);
}
return "hello " + name + ", this is " + port + " first message";
}
}
修改完后我们将服务重启,访问http://localhost:8888/spring-cloud-producer/hello?name=bennett
,我们通过日志可以看到,总共输出了三次request from 9000, name is bennett
。这说明重试确实在起作用。
上面我们测试时,连接超时后重试了两次之后还是超时,最后服务就给我们返回了超时的错误。但是那么我们有没有办法像Hystrix
一样提供一个熔断降级呢?答案时肯定的,zuul为我们提供一个FallbackProvider
接口类,我们可以通过实现这个类来自定义我们的熔断降级。
package org.springframework.cloud.netflix.zuul.filters.route;
import org.springframework.http.client.ClientHttpResponse;
/**
* Provides fallback when a failure occurs on a route.
*
* @author Ryan Baxter
* @author Dominik Mostek
*/
public interface FallbackProvider {
/**
* The route this fallback will be used for.
* @return The route the fallback will be used for.
*/
String getRoute();
/**
* Provides a fallback response based on the cause of the failed execution.
* @param route The route the fallback is for
* @param cause cause of the main method failure, may be null
* @return the fallback response
*/
ClientHttpResponse fallbackResponse(String route, Throwable cause);
}
getRoute()
方法是我们要熔断降级的路由fallbackResponse()
方法可以处理熔断降级操作接下来我们自定义一个实现类
@Component
public class ProducerFallback implements FallbackProvider {
private static final Logger log = LoggerFactory.getLogger(ProducerFallback.class);
@Override
public String getRoute() {
return "spring-cloud-producer";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause != null && cause.getCause() != null) {
log.info("Exception {}", cause.getCause().getMessage());
}
return new ClientHttpResponse() {
/**
* Return the headers of this message.
*
* @return a corresponding HttpHeaders object (never {@code null})
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
/**
* Return the body of the message as an input stream.
*
* @return the input stream body (never {@code null})
* @throws IOException in case of I/O errors
*/
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("The service is unavailable.".getBytes());
}
/**
* Get the HTTP status code as an {@link HttpStatus} enum value.
* For status codes not supported by {@code HttpStatus}, use
* {@link #getRawStatusCode()} instead.
*
* @return the HTTP status as an HttpStatus enum value (never {@code null})
* @throws IOException in case of I/O errors
* @throws IllegalArgumentException in case of an unknown HTTP status code
* @see HttpStatus#valueOf(int)
* @since #getRawStatusCode()
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
/**
* Get the HTTP status code (potentially non-standard and not
* resolvable through the {@link HttpStatus} enum) as an integer.
*
* @return the HTTP status as an integer value
* @throws IOException in case of I/O errors
* @see #getStatusCode()
* @see HttpStatus#resolve(int)
* @since 3.1.1
*/
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
/**
* Get the HTTP status text of the response.
*
* @return the HTTP status text
* @throws IOException in case of I/O errors
*/
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.getReasonPhrase();
}
/**
* Close this response, freeing any resources created.
*/
@Override
public void close() {
}
};
}
}
经过这样处理,再次访问之前的超时接口就不会再发生直接响应服务超时的状态了,返回的将是我们给的The service is unavailable.
,同样的我们也可以在这个地方添加我们自己的业务处理。
Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理Servlet的类型 |
pre | -2 | Servlet30WrapperFilter | 包装HttpServletRequest请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
可以在application.yml中配置需要禁用的filter,格式:
zuul:
FormBodyWrapperFilter:
pre:
disable: true
实现自定义Filter,需要继承ZuulFilter的类,并覆盖其中的4个方法。
下面自定义了一个检查是否携带token的的过滤器,没有token的不对其路由。
@Component
public class TokenFilter extends ZuulFilter {
private static final Logger log = LoggerFactory.getLogger(TokenFilter.class);
@Override
public String filterType() {
// 可以在路由前调用,总共有四种pre,post,error,route
return "pre";
}
@Override
public int filterOrder() {
// filter调用优先级,数字越小优先级越高
return 0;
}
@Override
public boolean shouldFilter() {
// 是否需要执行filter,默认为false不执行
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext rc = RequestContext.getCurrentContext();
HttpServletRequest request = rc.getRequest();
log.info("--->> TokenFilter {}, {}", request.getMethod(), request.getRequestURL());
String token = request.getParameter("token");
if (StringUtils.isBlank(token)) {
// 不对其路由
rc.setSendZuulResponse(false);
rc.setResponseStatusCode(400);
rc.setResponseBody("token is empty");
rc.set("isSuccess", false);
} else {
// 进行路由
rc.setSendZuulResponse(true);
rc.setResponseStatusCode(200);
rc.set("isSuccess", true);
}
return null;
}
}
将服务重启,访问http://localhost:8888/spring-cloud-producer/hello?name=bennett
,我们得到了400的状态码以及token is empty
的响应信息,并没有给我们路由到9000
端口。
我将token参数加上,再次访问http://localhost:8888/spring-cloud-producer/hello?name=bennett&token=token
我们得到了正确的响应结果(如果没有注释掉之前的线程睡眠,则返回的是服务不可用)。
关于zuul网关的就学习到这里了。
纯洁的微笑:服务网关zuul初级篇、服务网关Zuul高级篇
SpringCloud中文网:路由和过滤器:Zuul
码云:spring-cloud-demo