本文内容基于《Spring Cloud微服务实战》,翟永超著。
出现背景
通过上文所述的Spring Cloud Eureka实现高可用的服务注册中心以及实现微服务的注册与发现、Spring Cloud Ribbon或Spring Cloud Feign实现服务间负载均衡的接口调用、Spring Cloud Hystrix实现线程隔离并加入熔断机制,以避免在微服务架构中因个别服务出现异常而引起级联故障蔓延,此时能实现下图所示架构:
Open Service是一个对外的RESTful API服务,它通过F5、Nginx等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用。
问题:
- 从运维人员的角度看,需要一套机制来有效降低维护路由规则与服务实例列表的难度;
- 从开发人员的角度看,需要一套机制能够很好地解决微服务架构中,对于微服务接口访问时各前置校验的冗余问题。
解决方案:
- 对于路由规则的维护,Spring Cloud Zuul默认会通过以服务名作为ContextPath的方式来创建路由映射;
- Spring Cloud Zuul通过与Spring Cloud Eureka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有其他微服务的实例信息,使得将维护服务实例的工作交给了服务治理框架自动完成;
- Spring Cloud Zuul提供了一套过滤器机制,对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。
构建网关
- 依赖
- 启动类
- 配置
请求路由
- 传统路由方式
- 面向服务的路由
请求过滤
package com.yeta.apigateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
/**
* 在请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误
* @author YETA
* @date 2019/03/15/17:54
*/
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(ZuulFilter.class);
/**
* 过滤器的类型
* pre表示在请求被路由之前执行
* routing表示在路由请求时被调用
* post表示在routing和error过滤器之后被调用
* error表示处理请求时发生错误时被调用
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
* 过滤器的执行顺序
* 数字越小优先级越高
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 判断该过滤器是否需要被执行
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器的具体逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
Object accessToken = request.getParameter("accessToken");
if (accessToken == null) {
log.warn("access token is empty");
requestContext.setSendZuulResponse(false); //不进行路由
requestContext.setResponseStatusCode(401); //设置返回错误码
requestContext.setResponseBody("access token is empty"); //设置返回内容
return null;
}
log.info("access token is ok");
return null;
}
}
API网关对微服务架构的重要性
- 作为系统的统一入口,屏蔽了系统内部各个微服务的细节;
- 与服务治理框架结合,实现自动化的服务实例维护一级负载均衡的路由转发;
- 可以实现接口权限检验与微服务业务逻辑的解耦;
- 通过过滤器,在各生命周期中去校验请求的内容,将原本在对外服务层做的校验前移,保证了微服务的无状态性,同时降低了微服务的测试难度,让服务本身更集中关注业务逻辑的处理。
路由详解
- 传统路由配置
单实例配置
多实例配置
- 服务路由配置
- 服务路由的默认规则
当为Spring Cloud Zuul构建的API网关服务引入Spring Cloud Eureka之后,它为Eureka中的每个服务都自动创建一个默认路由规则,以服务名作为请求前缀。
- 自定义路由映射规则
系统在迭代过程中,有时候会需要为一组互相配合的微服务定义一个版本标识来方便管理它们的版本关系,根据这个标识可以很容易知道这些服务需要一起启动并配合使用。如类似命名:userservice-v1、orderservice-v1。
- 路径匹配
在Zuul中,路径匹配的路径表达式采用了Ant风格定义。
通配符 | 说明 |
? | 匹配任意单个字符 |
/user-service/? | 匹配/user-service/a、/user-service/b |
* | 匹配任意数量的字符 |
/user-service/* | 匹配/user-service/a、/user-service/aa、/user-service/bb |
** | 匹配任意数量的字符,支持多级目录 |
/user-service/** | 匹配/user-service/a/b |
如果需要考虑路由规则的顺序问题,参考用YAML文件来配置,properties文件无法保证有序。
- 忽略表达式
- 路由前缀
- 本地跳转
- Cookie与头信息
默认情况下,Spring Cloud Zuul在请求路由时,会过滤掉HTTP请求头信息中的一些敏感信息,防止它们被传递到下游的外部服务器,包括Cookie、Set-Cookie、Authorization三个属性。
- Hystrix和Ribbon支持
Zulul包含hystrix和ribbon的依赖,所以拥有现成隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能。当使用path与url的映射关系来配置路由规则的时候,对于路由转发的请求不会采用HystrixCommand来包装,所以这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。
过滤器详解
- 请求生命周期
- pre类型的过滤器:主要是在进行请求路由之前做一些前置加工,比如请求的校验等;
- routing类型的过滤器:主要是将请求转发到具体服务实例上去,等待返回结果;
- post类型的过滤器:主要是对处理结果进行一些加工或转换;
- error类型的过滤器:主要是在前三个阶段中发生异常时触发,最终还是流向post类型的过滤器。
- 异常处理
参考
https://blog.csdn.net/qq_31349087/article/details/79614184
https://www.jb51.net/article/138773.htm
/**
* 在自定义过滤器中处理异常
* @author YETA
* @date 2019/03/15/17:54
*/
@Component
public class ThrowExceptionFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(ZuulFilter.class);
/**
* 过滤器的类型
* pre表示在请求被路由之前执行
* routing表示在路由请求时被调用
* post表示在routing和error过滤器之后被调用
* error表示处理请求时发生错误时被调用
* @return
*/
@Override
public String filterType() {
return PRE_TYPE;
}
/**
* 过滤器的执行顺序
* 数字越小优先级越高
* @return
*/
@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
}
/**
* 判断该过滤器是否需要被执行
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器的具体逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
int a = 1 / 0;
return null;
}
}
/**
* 异常处理接口
* @author YETA
* @date 2019/03/19/15:53
*/
@RestController
public class ErrorHandlerController implements ErrorController {
@Override
public String getErrorPath() {
return "/error";
}
@GetMapping(value = "/error")
public Object error(HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
result.put("status_code", request.getAttribute("javax.servlet.error.status_code"));
StringBuffer sb = new StringBuffer(request.getAttribute("javax.servlet.error.message").toString());
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
while (exception.getCause() != null) {
sb.append(" ").append(exception.getCause().getMessage());
exception = (Exception) exception.getCause();
}
result.put("message", sb);
return result;
}
}
- 禁用过滤器