网关的主要作用:
spring cloud基于Netflix Zuul来实现网关功能
org.springframework.cloud
spring-cloud-starter-zuul
该依赖不仅包含了核心依赖zuul-core,还包含了网关所需的其它依赖:Hystrix、Ribbon和Actuator(可以通过/routes端点查看路由规则)
大多数情况下,网关需要注册到注册中心,所以也要引入eureka依赖,大致所需依赖如下:
org.springframework.boot
spring-boot-starter-actuator
org.springframework.cloud
spring-cloud-starter-zuul
org.springframework.cloud
spring-cloud-starter-eureka
org.springframework.cloud
spring-cloud-starter-feign
在入口类处加上@EnableZuulProxy开启Zuul网关功能。
该方式需要指定目标服务地址,无需依赖注册中心,需要花大量时间来维护path和url的对应关系。
zuul.routes.yourRouterName.path=/sourceAddress/**
zuul.routes.yourRouterName.url=targetAddress
yourRouterName是路由名字,可随意取,但一组path和url的路由名字必须相同,sourceAddress通常取值与yourRouterName相同,如下:
zuul.routes.studentInfo.path=/studentInfo/**
zuul.routes.studentInfo.url=http://localhost:8080/
假设网关地址为http://localhost:5555,当我们访问http://localhost:5555/studentInfo/studentscore时,网关会把请求路由到http://localhost:8080/studentscore。http://localhost:8080是studentInfo这个服务的一个微服务地址。
该方式依赖于eureka,可以通过微服务名字来路由,如:
zuul.routes.studentInfo.path=/studentInfo/**
zuul.routes.studentInfo.serviceId=studentInfo
studentInfo是一个注册到注册中心的微服务,当我们访问网关http://localhost:5555/studentInfo/studentscore时,会路由到微服务studentInfo的studentscore接口,我们无需关心微服务studentInfo的实例个数及地址信息,服务列表由eureka来维护,请求操作由ribbon(负载均衡)和Hytrixs(熔断控制)来完成。
如果不想依赖于eureka,也是可以的:
zuul.routes.studentInfo.path=/studentInfo/**
zuul.routes.studentInfo.serviceId=studentInfo
ribbon.eureka.enabled=false
studentInfo.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/,http://localhost:8082/
这又回到了传统路由方式了,只是可以利用ribbon做负载均衡。
这里可以做一些权限校验,请求参数校验,甚至是响应结果校验。只要继承抽象类ZuulFilter,实现其4个抽象方法即可对请求(包括响应)拦截和过滤。
1、public String filterType()
过滤器类型,决定过滤器在请求的哪个阶段被执行:
pre:在请求被路由之前执行;
route:在路由请求时执行,这里处理的事情是,将外部请求路由到具体的服务实例上去;
post:在请求路由之后(route和error之后)执行,不管是否发生错误,总会进入post过滤器执行,可以获取响应报文;
error:处理请求发生错误时执行,如果异常来自pre或route阶段,error处理完之后还是会进入post,如果错误来自post,则error处理完后不再重复进入post;
2、public int filterOrder()
过滤器执行顺序,如果有多个过滤器,可以通过该值来确定过滤器的执行顺序,值越小优先级越高,起始为0。如果过滤器没有顺序要求,则该值可以相同,如都返回0。
3、public boolean shouldFilter()
过滤器是否生效
4、public Object run()
过滤器具体逻辑,这里常用到一些方法:
1)RequestContext ctx = RequestContext.getCurrentContext();获取请求上下文对象
2)HttpServletRequest request = ctx.getRequest();获取请求对象
request.getServletPath();获取请求url中的servlet部分路径信息
request.getRequestURL();获取请求url
通过如下语句可以获取请求数据(包括请求头部和请求参数)
String req = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
可以进一步转成map等类型:JSON.parseObject(req, Map.class);
如果请求是get类型,也可以通过request.getParameter();或request.getParameterMap();来获取请求参数。
3)获取响应报文头部信息
List> objs = ctx.getZuulResponseHeaders();
for(Pair obj:objs){
String key = obj.first();
String value = obj.second();
//TODO
}
4)获取响应报文的body信息
InputStream resStream = ctx.getResponseDataStream();
try {
String body = StreamUtils.copyToString(resStream, Charset.forName("UTF-8"));
Map bodyMap = JSON.parseObject(body, Map.class);
} catch (IOException e) {}
应该也可以用String body = ctx.getResponseBody();来获取报文的body信息。
5)过滤请求
可以通过以下语句过滤掉请求,不对其进行路由
ctx.setSendZuulResponse(false);//为false的时候,filterType需要是pre,如果是post,则已经路由了
默认为true,即路由请求,如果校验不通过,或请求参数不满足要求,可以将其设为false。过滤请求后,通常会设置一些返回码和返回报文内容
6)设置返回码
ctx.setResponseStatusCode(401);//通常在请求被过滤掉的时候使用,如果请求能被路由,则用默认返回码即可
7)设置返回报文body的内容
ctx.setResponseBody(str);
如果请求被过滤掉,可以在这里设置一些过滤原因等信息。如果请求被路由,也可以用该方法对返回的响应报文加工处理,例如去掉或加入一些内容。
自定义ZuulFilter如下:
@Component
public class MyZuulFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return true;
}
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// TODO 过滤逻辑
return null;// 返回null即可
}
}
MyZuulFilter需要用@component注解,或在配置类中利用@Bean创建实例
Zuul有默认的异常处理过滤器,但有时候我们希望在发生异常时能返回自定义的异常信息,这可以通过继承DefaultErrorAttributes来实现:
@Component
public class MyErrorAttribute extends DefaultErrorAttributes {
@Override
public Map getErrorAttributes(
RequestAttributes requestAttributes, boolean includeStackTrace) {
Map result = new LinkedHashMap(2);
result.put("code", "错误码");
result.put("msg", "错误说明");
return result;
}
}
当网关路由请求到微服务时,如果服务不可用或请求超时,则会抛出异常,Hystrix为请求失败提供回调接口(和Feign的回调一样),Zuul包含了Hystrix。
实现起来很简单,不同Spring boot版本接口有一点不同
假如spring cloud版本是Edgware.SR3,Spring boot版本是1.5.8.RELEASE,则实现FallbackProvider接口:
@Component
public class YourRouterNameFallbackProvider implements FallbackProvider{
private static final ObjectMapper mapper = new ObjectMapper();
@Override
public String getRoute() {
return "yourRouterName";
}
@Override
public ClientHttpResponse fallbackResponse() {
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() {
return;
}
@Override
public InputStream getBody() throws IOException {
Map bodyMap = new HashMap();
bodyMap.put("code", "错误码");
bodyMap.put("msg", "错误信息");
return new ByteArrayInputStream(mapper.writeValueAsString(
bodyMap).getBytes("UTF-8"));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return headers;
}
};
}
@Override
public ClientHttpResponse fallbackResponse(Throwable cause) {
//可以利用cause获取异常的内容
return fallbackResponse();
}
}
getRoute()指定回调对应的路由名字,与配置文件一致;
两个fallbackResponse方法(一个无参,一个有参)用于设置响应报文的头部和body内容;
假如使用的Spring cloud是C或D版本,则实现ZuulFallbackProvider接口,和上面的一样,只是后者只有一个无参的fallbackResponse方法,我们无法获取导致异常的原因。
如果回调类把路由请求的异常捕获并处理了,则异常不会再进入MyErrorAttribute。
从前面内容看到,有多个地方可以处理异常:
服务间调用异常(服务不可用、超时等)时,只会进入指定的回调类或自定义异常类,不会进入类型为error的ZuulFilter,但最后还是会进入类型为post的ZuulFilter,如果有对应的回调类,则只会进入回调FallbackProvider,不会再进入自定义异常类,没有指定的回调类可用时才会进入自定义处理类。
回调类和自定义异常都只能处理服务间调用异常,其它异常则考虑用类型为error的ZuulFilter,ZuulFilter只能处理网关内部抛出的异常,比如在一些处理逻辑上的运行时异常等。
因为所有的请求都会通过网关,所以可以在网关统计接口的响应时间及成功率等监控数据,统计响应时间可以通过创建一个pre和一个post的ZuulFilter。在pre的ZuulFilter中记下开始时间,在post的ZuulFilter中计算时间差,因为一个请求是同一个线程,所以,可以利用ThreadLocal来实现。
如果配置中心是通过git来管理配置的,可以自动刷新路由配置,非git配置中心,可以通过应用自身定时刷新配置,然后修改路由规则。
首先,需要在配置类或启动类配置bean:
@Bean
public ZuulProperties zuulProperties() {
return new ZuulProperties();
}
然后,在定时任务中修改该bean的配置:
@Component
public class ConfigScheduled {
@Autowired
public ZuulProperties zuulProperties;
@Scheduled(fixedRate = 10000)//单位是毫秒
public void zuulRoute() {
Map routeMap = zuulProperties.getRoutes();
ZuulRoute zuulRoute = routeMap.get("userInfoservice");
/*
* 这里切到另一个网关,比如两组独立的服务,可以轮流更新两组服务的应用而不会导致服务中断
* zuulRoute.setUrl("http://localhost:1003")是直接切到userInfo-service这个服务
* http://localhost:1003是userInfo-service的服务地址
*/
zuulRoute.setUrl("http://127.0.0.1:1002/userInfo-service");
}
}
上面代码是把网关1(http://127.0.0.1:1001)的请求路由到网关2(http://127.0.0.1:1002),网关1的property配置文件内容是:
zuul.routes.userInfoservice.path=/userInfo-service/**
zuul.routes.userInfoservice.serviceId=userInfo-service
如果直接路由到服务userInfo-service,则是zuulRoute.setUrl("http://127.0.0.1:1003"),不需要再加服务名userInfo-service。
ZuulRoute的url优先级比serviceId要高,如果url非空,则url生效,如果url为空,则serviceId生效,所以,如果想使用配置文件中的路由配置,则只需把url设为null。
动态修改路由规则在更新应用时,可以做到服务不中断。
请求经过网关,会删除被认为属于敏感的header信息,默认情况下会消除以下头部信息:
Authorization、Set-Cookie、Cookie、Host、Connection、Content-Length、Content-Encoding、Server、Transfer-Encoding、X-Application-Context
如果不希望被删除以上信息,可以修改配置项zuul.sensitive-headers,如果所有敏感信息都不消除,可以把该配置项设为空,如
zuul.sensitive-headers=
如果只要消除其中某几个头部,可以配置如下:
zuul.sensitive-headers=Set-Cookie、Cookie、Host、Connection、Content-Length、Content-Encoding、Server、Transfer-Encoding、X-Application-Context
RequestContext ctx = RequestContext.getCurrentContext();//获取请求上下文对象
HttpServletRequest request = ctx.getRequest();//获取请求对象
request.getHeader("X-B3-TraceId");//获取发给网关的请求的头部信息
ctx.addZuulRequestHeader("X-B3-TraceId", "new value");//往转发报文加头部信息(add a header to be sent to the origin)
参考:https://blog.csdn.net/a1430490717/article/details/105404753/