前面我们搭建了User、Order、Pay几个服务模块,它们之间互相通信都是直接访问对象,在生产环境下,一般我们要求用户登录授权之后才能执行相关操作,如果每个微服务都去做一套登录检查逻辑,那么会对性能造成大量的损耗,所以我们需要一个插件把登录检查公共的逻辑进行统一抽取,那么只需要做一套检查逻辑就可以了,那么我们需要用到SpringCloud 的Zuul插件来实现这个功能;
Zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet(filter)应用。Zuul 在云平台上提供动态路由(请求分发),监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门,也要注册入Eureka,用一张图来理解zuul在架构中的的角色:
需要注意的是,zuul本身是一个独立的服务,默认集成了Ribbon,zuul通过Ribbon将客户端的请求分发到下游的微服务,所以zuul需要通过Eureka做服务发行,同时zuul也集成了Hystrix。
根据上图理解 ,我们需要建立独立的工程去搭建Zuul服务,同时需要把Zuul注册到EurekaServer,因为当请求过来时,zuul需要通过EurekaServer获取下游的微服务通信地址,使用Ribbon发起调用。
新建一个子模块,导入相关jar包
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.boot
spring-boot-starter-web
com.alibaba
fastjson
1.2.50
主配置类:
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy //开启zuul 可以看做是 @EnableZuulServer 的增强版 ,一般用这个
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class,args);
}
}
yml配置:
eureka:
client:
serviceUrl: #注册中心地址
defaultZone: http://peer1:1010/eureka/,http://peer2:1011/eureka/,http://peer3:1012/eureka/
instance:
prefer-ip-address: true #使用ip地址注册
instance-id: zuul-server:1060 #实列id地址
spring:
application:
name: zuul-server #服务名
server:
port: 1060 #端口
zuul:
prefix: "/servers" #统一访问前缀
ignoredServices: "*" #禁用掉使用浏览器通过服务名的方式访问服务
routes:
pay-server: "/pay/**" #指定pay-server这个服务使用 /pay路径来访问 - 别名
order-server: "/order/**" #指定order-server这个服务使用 /order路径来访问
注意: 我们对zuul主要做了三个配置
zuul.prefix : 作为统一的前缀,在浏览器访问的时候需要加上该前缀
zuul.ignoredServices : 忽略使用服务名方式访问服务,而是通过routes指定的路径进行访问
zuul.routes : 配置服务的访问路径
注意:在么有使用zuul之前我们是通过 http://localhost:1040/pay/1 来直接访问支付服务,现在需要通过zuul来访问,格式如下:http:// zuul的ip : zuul的port /zuul前缀 / 服务路径 /服务的controller路径 ,即:
http://localhost:1060/servers/pay/pay/1
特别说明:其实这里我们直接浏览器也能访问到目标服务,即可以通过: http://localhost:1040/pay/1 绕过zuul,但是这种情况不用担心,因为在产品上线的时候我们都是内网部署,只有zuul我们部署成外网,也就是说直接访问目标微服务的方式是访问不到的,所以我们只需要通过zuul访问即可。
启动服务,访问地址:
拿到结果,说明我们zuul已经集成成功
接下来就是重点
zuul的底层是通过各种Filter来实现的,zuul中的filter按照执行顺序分为了“pre”前置(”custom”自定义一般是前置),“routing”路由,“post”后置,以及“error”异常Filter组成,当各种Filter出现了异常,请求会跳转到“error filter”,然后再经过“post filter” 最后返回结果,下面是Filter的执行流程图:
注意:
正常流程:
请求到达首先会经过pre类型过滤器,而后到达routing类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。
异常流程:
整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,再error处理完毕后,会将请求交给POST过滤器,最后返回给用户。
如果是error过滤器自己出现异常,最终也会进入POST过滤器,而后返回。
如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和routing不同的时,请求不会再到达POST过滤器了。
Zuul提供了一个抽象的Filter:ZuulFilter我们可以通过该抽象类来自定义Filter,该Filter有四个核心方法,如下:
public class LoginCheckFilter extends ZuulFilter {
@Override
public String filterType() {
return null;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return false;
}
@Override
public Object run() throws ZuulException {
return null;
}
}
filterType :是用来指定filter的类型的(类型见常量类:FilterConstants)
filterOrder :是filter的执行顺序,越小越先执行
shouldFilter :是其父接口IZuulFilter的方法,用来决定run方法是否要被执行
run :是其父接口IZuulFilter的方法,该方法是Filter的核心业务方法
package cn.wly.filter;
import com.alibaba.fastjson.JSON;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class LoginCheckFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE; // "pre";
}
@Override
public int filterOrder() {
return FilterConstants.SEND_ERROR_FILTER_ORDER; //0
}
@Override
public boolean shouldFilter() {
//获取当前请求对象
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
//获取当前对象的请求地址
String requestURI = request.getRequestURI();
//判断当前请求是否是登录和注册,如果不是就需要去做验证,如果是就不验证
if (requestURI.endsWith("/login") || requestURI.endsWith("/register")){
return false;
}
return true;
}
@Override
public Object run() throws ZuulException {
//获取当前请求对象
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
HttpServletResponse response = RequestContext.getCurrentContext().getResponse();
//获取当前对象的token
String token = request.getHeader("token");
//判断当前请求头里面是否含有token,如果没有就返回给客户一个json格式的提示信息
if(!StringUtils.hasLength(token)){
Map resultMap=new HashMap<>();
resultMap.put("error", false);
resultMap.put("message","请登录后再访问" );
//使用阿里json工具类把对象转成json字符串
String jsonString = JSON.toJSONString(resultMap);
//设置响应编码格式
response.setContentType("application/json;charset=utf-8");
//设置响应状态码
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); //int SC_UNAUTHORIZED = 401;
try {
//使用json形式输出到浏览器
response.getWriter().print(jsonString);
} catch (IOException e) {
e.printStackTrace();
}
//告诉zuul不再往后执行
RequestContext.getCurrentContext().setSendZuulResponse(false);
}
return null;
}
}
注意:
记得该类上面别忘了加@Component 注解,不然不会生效
在 filterType方法中我们返回“pre”前置filter的常量,让他成为前置filter(登录检查需要在请求的最前面来做)
在filterOrder方法中返回的顺序值是 1 ,执行顺序越小越先执行
在shouldFilter方法中通过判断请求的url来决定是否需要做登录检查,返回true就是要做然后才会执行run方法
在run方法中我们通过获取请求头中的token判断是否登录,如果没登录就返回错误信息,阻止继续执行。
RequestContext.getCurrentContext() 是一个Zuul提供的请求上下文对象
在返回JSON格式的错误信息时我用到了fastjson,需要在zuul工程中导入依赖
重启Zuul服务访问测试:http://localhost:1060/servers/pay/pay/1
结果如图
说明过滤器已经生效,然后我们使用postman工具,携带一个token测试:
拿到结果;
zuul作为服务网关面向的是客户端(浏览器),当服务调用链路出现异常,我们不希望直接把异常信息抛给客户端,而是希望触发降级,返回友好的提示信息,所以我们需要去配置zuul的熔断机制。
在zuul中要实现熔断功能需要实现ZuulFallbackProvider接口,该接口提供了两个方法:getRoute用来指定熔断功能应用于哪些路由的服务,fallbackResponse方法为熔断功能时执行的方法(用来返回托底数据) ,接口代码如下:
public interface FallbackProvider {
/**
* The route this fallback will be used for.
* @return The route the fallback will be used for.
*/
public 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);
}
我们来对对pay-server做一个熔断器配置,当zuul先pay-server发起调用,服务调用失败,触发熔断,返回一句话“抱歉,服务不可用”
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
@Component
public class PayServerFallback implements FallbackProvider {
private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class);
// 指定要处理的服务。
@Override
public String getRoute() {
return "pay-server"; //"*"代表所有服务都有作用
}
/**
* @param route :服务的路由
* @param cause : 异常
* @return ClientHttpResponse:熔断后的换回值
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return 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("抱歉,服务不可用".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
提示:如果要让此熔断功能作用于所有的服务,将 getRoute 方法中的返、回值修改为“*”即可。
关闭支付服务测试:http://localhost:1060/servers/pay/1 提示服务不可用
Zuul集成了hystrix,如果服务的调用
链过长,或者ribbon调用事件过长,可能会触发Hystrix的熔断机制,导致请求拿不到正常的结果,我们通常会对Ribbon和Hystrix的超时时间配置。如下配置对所有消费者微服务都有用:
zuul配置文件加上如下配置:
zuul:
retryable: true #是否开启重试功能
ribbon:
MaxAutoRetries: 1 #对当前服务的重试次数
MaxAutoRetriesNextServer: 1 #切换相同Server的次数
OkToRetryOnAllOperations: false # 对所有的操作请求都进行重试,如post就不能重试,如果没做幂等处理,重试多次post会造成数据的多次添加或修改
ConnectTimeout: 3000 #请求连接的超时时间
ReadTimeout: 6000#请求处理的超时时间
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 8000
#如果配置ribbon的重试,hystrix的超时时间要大于ribbon的超时时间
在学习Ribbon的章节我们为了让服务加快第一次调用,我们可以通过设置Ribbon的饥饿加载,zuul底层通过Ribbon实现负载均衡器,所以也需要指定饥饿加载,配置如下:
zuul:
ribbon:
eager-load.enabled: true # 饥饿加载
需要注意的是,zuul是通过读取路由配置来实现饥饿加载的,所以如果要让eager-load.enabled: true起作用,我们一般不会使用默认的路由方式,而是单独配置路由规则,如上配置。