前言
最近参考个项目是"透传"第三方接口,第一个想到的是利用Spring Cloud的网关组件去实现,
但在实现过程中遇到几个问题,
解决后记录下来,便于给其他人作为参考。
借鉴文章 代码人生 codingforever.cn
问题一 对真实接口的包装, 隐藏真实的请求接口
大致是有个/getOrderDetail的接口,这个接口的逻辑是
如果本地缓存中有未失效的记录,那从缓存中取出该条记录返回;
如果缓存中没有,这时需要调用第三方接口/getOrder。
由于配置的Zuul路由路径与我定义的接口前缀路径一致,如/oilpayment/,这时直接访问/oilpayment/getOrderDetail,如果缓存中不存在记录的话,会被Zuul作为服务接口进行路由,而后端服务是没有这个接口的,所以访问不成功。
即使是采用RestTemplate这个调用接口的方式,也不成功。
根本原因在于Zuul在路由过程中,依然将getOrderDetail作为路由的requestUri,我们只需要新建Route类型的过滤器,在其中将requestUri手动指定为getOrder即可,如下:
/** * @author caochen * @PackgeName: com.xkcoding.task.job * @Description: TODO(路由重定向) * @date 2020/6/3 23:05
*/
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
@Component
public class UrlRedirectFilter extends ZuulFilter implements AbstractLogger{
/**
* 重定向的规则,根据key来重定向到val.
*/
private static MapurlMap=new HashMap<>();
static {
urlMap.put("t", "/test/");
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String url = request.getRequestURI(); // 列子 [/user/login/loginWx]
String[] split = url.split("/", 3); // 这里切割一下,好让下面判断是否是需要修改url的.
if (split.length>=2) {
String val = urlMap.get(split[1]);
if (StringUtils.isNotEmpty(val)) {
url=url.replaceFirst("/"+split[1]+"/", val);// 根据配置好的去将url替换掉,这里可以写自己的转换url的规则
ctx.put(FilterConstants.REQUEST_URI_KEY, url); // 将替换掉的url set进去,在对应的转发请求的url就会使用这个url
}
}
return null;
}
@Override
public boolean shouldFilter() {
return true;
}
//filterOrder:过滤的顺序
@Override
public int filterOrder() {
return 1;
}
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
}
Zuul源码分析
下面来分析下zuul的源码.首先在
org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter
这个根据uri决定调用哪一个route 的过滤器中有
ctx.put(REQUEST_URI_KEY, route.getPath());
这行代码,也就在这put了一次这个url对应的值,
我们将url修改后再put 进去,zuul 将会在转发的时候
也就是 SendForwardFilter 这个里面 (贴一段源码)
@Override
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
String path = (String) ctx.get(FORWARD_TO_KEY);
RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path);
if (dispatcher != null) {
ctx.set(SEND_FORWARD_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
dispatcher.forward(ctx.getRequest(), ctx.getResponse());
ctx.getResponse().flushBuffer();
}
}
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
这里 取的是
FORWARD_TO_KEY
这个key所存储的url .也就是最终转发的url值.
问题三 拦截Zuul路由接口后响应信息, 便于自定义处理
有时候,我们需要将接口返回的信息包装后再返回。那么,在Zuul中这个需求是通过定义POST类型的过滤器来实现的。
/** * @author caochen * @PackgeName: com.xkcoding.task.job * @Description: TODO(Post请求过滤器) * @date 2020/6/3 23:05 */
@Component
@RefreshScope
public class PostOrderFilter extends ZuulFilter implements LoggerInterface {
/**
* 自定义过滤器访问路径,逗号分隔
*/
@Value(value = "${order.filter.post.access.urls}")
private String accessUrls;
/**
* {@link OrderService}
*/
@Autowired
private OrderService orderService;
/**
* 过滤器类型
*
* @return String
*/
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
/**
* 过滤器顺序
*
* @return int
*/
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 100;
}
/**
* 过滤标识
*
* @return boolean
*/
@Override
public boolean shouldFilter() {
return FilterHandler.isFilter(accessUrls);
}
/**
* 过滤器执行业务处理
*
* @return Object
*/
@Override
public Object run() {
String result = null;
InputStream inputStream = null;
Map<String, Object> paramsMap = null;
RequestContext requestContext = null;
GZIPInputStream gzipInputStream = null;
HttpServletRequest httpServletRequest = null;
OrderPlaceResponseBean orderPlaceResponseBean = null;
try {
requestContext = RequestContext.getCurrentContext();
httpServletRequest = requestContext.getRequest();
//获取所有提交参数
paramsMap = MapUtil.convert(httpServletRequest.getParameterMap());
//参数校验
boolean isPassed = this.validateParam(paramsMap);
if (isPassed) {
//获取解析响应信息
inputStream = requestContext.getResponseDataStream();
//Zuul默认返回响应信息被压缩,为gzip格式
gzipInputStream = new GZIPInputStream(inputStream);
result = StreamUtils.copyToString(gzipInputStream, StandardCharsets.UTF_8);
logger.info("[POST]过滤器加油下单CP接口响应内容 {}", result);
orderPlaceResponseBean = JSON.parseObject(result, OrderPlaceResponseBean.class);
//code==200,正常返回下单信息;否则返回无结果
if (orderPlaceResponseBean.getCode() == HttpStatus.OK.value()) {
//code==200,组装下单信息,缓存入Redis
orderPlaceResponseBean = this.orderService.placingOrderCache(orderPlaceResponseBean, String.valueOf(paramsMap.get(PARAM_NAME_GAS_NAME)));
} else {
//code!=200,返回具体接口响应信息
orderPlaceResponseBean = new OrderPlaceResponseBean(orderPlaceResponseBean.getCode(), orderPlaceResponseBean.getMessage(), null);
}
//关闭资源
inputStream.close();
//返回响应信息
requestContext.setResponseBody(JSON.toJSONString(orderPlaceResponseBean, SerializerFeature.WriteNullStringAsEmpty));
} else {
//即使透传CP接口成功,缺少参数也返回参数不合法或未输入
requestContext.setResponseBody(JSON.toJSONString(new ResponseBean(ResultEnum.INVALID_INPUT, null), SerializerFeature.WriteNullStringAsEmpty));
}
} catch (Exception e) {
logger.error("[POST]过滤器执行业务处理出现异常 {}", e.getMessage());
}
return null;
}
/**
* 参数校验
*
* @param paramsMap 表单参数
* @return boolean
*/
private boolean validateParam(Map<String, Object> paramsMap) {
boolean isVerified = false;
if (paramsMap != null && !paramsMap.isEmpty()) {
//下单
if (paramsMap.containsKey(PARAM_NAME_GAS_NAME)) {
isVerified = true;
}
}
return isVerified;
}
}
需要注意的是,在接收响应内容时,由于Zuul返回的是压缩后的内容,所以必须包装一层InputStream,进行解压,否则响应信息是乱码,故而无法解析处理。
借鉴文章 代码人生_杨大大 https://codingforever.cn/
曹辰
欢迎关注,我的微信公共号
分享也是一种生活态度 分享所思, 所得。如此而已 Write By CaoChen
我是[香港脚台湾人],这个微信公共号(summerhins)的定位是自我提升,范围主要包括学习方法、注意力、习惯等等,同时不定期分享一些五花八门的读书笔记和影评。
欢迎点击文章标题下方的作者关注。
由于水平有限,不足和错误之处在所难免,希望大家能够批评指出。
我的博客:http://www.cnblogs.com/hins/
我的GitHub:https://github.com/caochenhins
我的微信公共号:曹辰的思考笔记
真诚赞赏,手留余香
打赏
微信支付
支付宝