最近接到个项目是透传第三方接口,第一个想到的是利用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,进行解压,否则响应信息是乱码,故而无法解析处理。

🔎借鉴文章 解析HttpResponse InputStream乱码问题