最近接到个项目是透传第三方接口,第一个想到的是利用SpringCloud的网关组件去实现,但在实现过程中遇到几个问题,解决后记录下来,便于给其他人作为参考。
【问题一】对真实接口的包装,隐藏真实的请求接口
大致是有个/getOrderDetail的接口,这个接口的逻辑是如果本地缓存中有未失效的记录,那从缓存中取出该条记录返回;如果缓存中没有,这时需要调用第三方接口/getOrder。由于配置的Zuul路由路径与我定义的接口前缀路径一致,如/oilpayment/,这时直接访问/oilpayment/getOrderDetail,如果缓存中不存在记录的话,会被Zuul作为服务接口进行路由,而后端服务是没有这个接口的,所以访问不成功。即使是采用RestTemplate这个调用接口的方式,也不成功。根本原因在于Zuul在路由过程中,依然将getOrderDetail作为路由的requestUri,我们只需要新建Route类型的过滤器,在其中将requestUri手动指定为getOrder即可,如下:
/**
* Created by IntelliJ IDEA 2020.
* FileName: RouteUrlRedirectFilter.java
*
* @Author: Shingmo Yeung
* @Date: 2020/5/28 17:54
* @Version: 1.0
* To change this template use File Or Preferences | Settings | Editor | File and Code Templates.
* File Description: 路由地址过滤器,处理自定义uri(非CP服务提供)
*/
@Component
@RefreshScope
public class RouteUrlRedirectFilter extends ZuulFilter implements LoggerInterface {
/**
* 自定义过滤器访问路径,逗号分隔
*/
@Value(value = "${order.filter.route.access.urls}")
private String accessUrls;
/**
* 重定向CP服务加油下单接口
*/
@Value(value = "${order.filter.route.redirect.orderplace.uri}")
private String orderPlaceRedirectUri;
/**
* {@link StringRedisTemplate}
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 过滤器类型
*
* @return String
*/
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
/**
* 过滤器顺序
*
* @return int
*/
@Override
public int filterOrder() {
return FilterConstants.SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
}
/**
* 过滤标识
*
* @return boolean
*/
@Override
public boolean shouldFilter() {
return FilterHandler.isFilter(accessUrls);
}
/**
* 过滤器执行业务处理
*
* @return Object
*/
@Override
public Object run() {
String orderId = null;
String orderKey = null;
String orderInfo = null;
Map<String, Object> paramsMap = null;
RequestContext requestContext = null;
HttpServletRequest httpServletRequest = null;
try {
requestContext = RequestContext.getCurrentContext();
httpServletRequest = requestContext.getRequest();
//获取所有提交参数
paramsMap = MapUtil.convert(httpServletRequest.getParameterMap());
//参数校验
boolean isPassed = this.validateParam(paramsMap);
if (isPassed) {
orderId = String.valueOf(paramsMap.get(PARAM_NAME_ORDER_ID));
orderKey = REDIS_ORDER_KEY.concat(orderId);
logger.info("订单号 {}, 用于缓存的订单key {}", orderId, orderKey);
//检查Redis中是否存在对应的key
boolean isExist = this.stringRedisTemplate.hasKey(orderKey);
if (isExist) {
//从Redis中获取对应key的值
orderInfo = this.stringRedisTemplate.opsForValue().get(orderKey);
if (StringUtils.isNotEmpty(orderInfo)) {
//返回自定义结果
FilterHandler.queryOrderFromRedis(requestContext, orderInfo);
} else {
//可能支付成功缓存被删除,更改路由uri,查询CP接口
this.routeRedirect(requestContext, this.orderPlaceRedirectUri);
}
} else {
//可能支付成功缓存被删除,更改路由uri,查询CP接口
this.routeRedirect(requestContext, this.orderPlaceRedirectUri);
}
} else {
//返回自定义结果
FilterHandler.sendCustomResult(requestContext, new ResponseBean(ResultEnum.INVALID_INPUT, null));
}
} catch (Exception e) {
logger.error("[ROUTE]过滤器执行业务处理出现异常 {}", 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_ORDER_ID) && paramsMap.containsKey(PARAM_NAME_PHONE)) {
isVerified = true;
}
}
return isVerified;
}
/**
* 重定向路由
*
* @param requestContext {@link RequestContext}
* @param redirectUri 重定向请求的uri
*/
private void routeRedirect(RequestContext requestContext, String redirectUri) {
logger.info("[ROUTE]过滤器即将重定向路由到 {}", redirectUri);
requestContext.set(ZUUL_FILTER_REQUEST_URI, redirectUri);
}
}
【问题二】拦截Zuul路由接口后的响应信息,便于自定义处理
有时候,我们需要将接口返回的信息包装后再返回。那么,在Zuul中这个需求是通过定义POST类型的过滤器来实现的。
/**
* Created by IntelliJ IDEA 2020.
* FileName: PostOrderFilter.java
*
* @Author: Shingmo Yeung
* @Date: 2020/5/29 09:38
* @Version: 1.0
* To change this template use File Or Preferences | Settings | Editor | File and Code Templates.
* File Description: Response响应信息过滤器,用于过滤响应信息自定义处理后返回
*/
@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,进行解压,否则响应信息是乱码,故而无法解析处理。