微服务系列:SpringCloudGateway自定义Predicate Factory和GatewayFilter Factory

396 阅读4分钟

Predicate Factory

在spring cloud gateway组件中,提供了如Before、After、Path等Route Predicate Factory供我们使用,这些断言工厂可以满足大多数的场景。但在企业开发中难免会碰到现有的功能无法满足需求,需要进行二次开发的情况,下面基于我的需求来定制我的断言工厂。
spring cloud gateway开发者指引中可以看到,自定义PredicateFactory需要实现RoutePredicateFactory接口,或者继承AbstractRoutePredicateFactory 抽象类,那我们也可以来参考现有的工厂AfterRoutePredicateFactory。

public class AfterRoutePredicateFactory extends AbstractRoutePredicateFactory<AfterRoutePredicateFactory.Config> {

    public AfterRoutePredicateFactory() {
		super(Config.class);
	}

    @Override
	public List<String> shortcutFieldOrder() {
		return Collections.singletonList("datetime");
	}

    @Override
	public Predicate<ServerWebExchange> apply(Config config) {
    }

    public static class Config {

		@NotNull
		private ZonedDateTime datetime;

		public ZonedDateTime getDatetime() {
			return datetime;
		}

		public void setDatetime(ZonedDateTime datetime) {
			this.datetime = datetime;
		}

	}
}

从AfterRoutePredicateFactory的定义可以看到,共有几个步骤:

  1. 继承AbstractRoutePredicateFactory,并且泛型需要一个Config类;
  2. 定义一个内部类Config;
  3. 提供一个无参构造并调用AbstractRoutePredicateFactory的有参构造并传入Config.class;
  4. 重写apply方法返回一个断言;
  5. 重写shortcutFieldOrder支持短配置。

再交代一下我的需求,在近期迁移代码时发现原有的代码有一些提供给第三方调用的接口定义方式如下:

@RestController
public class GatewayController {

    @PostMapping("gateway")
    public Result<?> gateway(@RequestParam("app_id") String appid, 
                             @RequestParam("method") String method,
                            ...) {
        switch(method) {
            case "user.add":
                // do something...
                break;
            case "user.update":
                // do something...
                break;
            case "bill.select":
                // do something...
                break;
            default:
                throw new UnsupportedOperationException;
        }
        return Result.ok(...);
    }
}

对于上面接口定义,第三方请求过来是同一个地址http://ip:port/xxx-gateway/gateway, 在Query部分传入method来区分对应接口。在迁移到微服务时我就想把这个逻辑用断言工厂替代,也方便后续开发。
开干,定义一个ApiMethodRoutePredicateFactory并交给spring容器管理,在apply方法中从request获取到客户端传入的method,由于我希望这个断言规则支持多个method,所以我就在Config中定义了一个List,最后通过contains方法来判断是否匹配method。需要注意的是,AfterRoutePredicateFactory只支持一个参数,所以它没有重写shortcutType,在PathRoutePredicateFactory中也是List,它重写了shortcutType并返回了ShortcutType.GATHER_LIST,可以参考它来实现。这样在路由配置时就支持短配置了。

@Component
public class ApiMethodRoutePredicateFactory extends AbstractRoutePredicateFactory<ApiMethodRoutePredicateFactory.Config> {

    public ApiMethodRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("methods");
    }

    @Override
    public ShortcutType shortcutType() {
        return ShortcutType.GATHER_LIST;
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return exchange -> {
            ServerHttpRequest request = exchange.getRequest();
            String method = request.getQueryParams()
                    .getFirst("method");
            return config.getMethods()
                    .contains(method);
        };
    }

    @Validated
    @Data
    public static class Config {
        private List<String> methods;
    }
}

下面是我用这个断言工厂的示例配置:

spring:
  cloud:
    gateway:
      routes:
        - id: xxx-gateway
          uri: lb://xxx-service
          predicates:
            - Path=/xxx-gateway/gateway
            - ApiMethod=user.add,user.update
            

至此,自定义的ApiMethodRoutePredicateFactory完工。

GatewayFilter Factory

接着上面继续讲,在完成method匹配之后,就需要将请求转发到具体的业务处理方法了。再来看一下GatewayController的接口定义,有一个bizContent存放的是一个json字符串,这就是接口定义的参数了,在每一个case中需要将它转成对应的实体类,然后将实体类传递给对应的Dubbo接口进行业务处理。

@RestController
public class GatewayController {

    @PostMapping("gateway")
    public Result<?> gateway(@RequestParam("app_id") String appid, 
                             @RequestParam("method") String method,
                             // json
                             @RequestParam("biz_Content") String bizContent,
                            ...) {
        switch(method) {
            case "user.add":
                UserAdd user = JSONObject.parseObject(bizContent, UserAdd.class);
                // do something...
                break;
            case "user.update":
                UserUpdate user = JSONObject.parseObject(bizContent, UserUpdate.class);
                // do something...
                break;
            case "bill.select":
                Bill bill = JSONObject.parseObject(bizContent, Bill.class);
                // do something...
                break;
            default:
                throw new UnsupportedOperationException;
        }
        return Result.ok(...);
    }
}
我的想法是将Dubbo接口转换成对应的微服务接口,接口的地址就是客户端传入的method参数,或者可以将同类型的一系列接口配置在一条断言规则中,比如ApiMethod=user.add,user.update,那就需要将request path重写,并且将bizContent写入到请求体中,路由到微服务接口时由springMVC自动完成数据封装。
@Component
@Slf4j
public class BizContentGatewayFilterFactory extends AbstractGatewayFilterFactory<BizContentGatewayFilterFactory.Config> {

    public BizContentGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return List.of("path");
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            MultiValueMap<String, String> queryParams = request.getQueryParams();
            String routeMethod = queryParams.getFirst("method");
            Map<String, String> map = new HashMap<>();
            map.put("method", routeMethod);
            String newPath = StringSubstitutor.replace(config.getPath()
                    .replace("$\\", "$"), map);
            String bizContent = queryParams.getFirst("biz_content");
            if (bizContent == null) {
                bizContent = "";
            }
            ServerHttpRequestDecorator requestDecorator = new RewriteRequestBodyServerHttpRequestDecorator(request, bizContent.getBytes(StandardCharsets.UTF_8));
            ServerHttpRequest newRequest = requestDecorator.mutate()
                    .path(newPath)
                    .build();
            exchange.getAttributes()
                    .put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI());
            return chain.filter(exchange.mutate()
                    .request(newRequest)
                    .build());
        };
    }

    @Data
    public static class Config {
        /**
         * 目标接口路径。支持使用占位符{}替换请求参数中的method
         */
        private String path;
    }
}
public class RewriteRequestBodyServerHttpRequestDecorator extends ServerHttpRequestDecorator {

    private final byte[] requestBody;

    private int length;

    public RewriteRequestBodyServerHttpRequestDecorator(ServerHttpRequest delegate, byte[] body) {
        super(delegate);
        this.requestBody = body;
        if (body != null) {
            length = body.length;
        }
    }

    @Override
    public HttpHeaders getHeaders() {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.putAll(super.getHeaders());
        if (length > 0) {
            httpHeaders.setContentLength(length);
        } else {
            httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
        }
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        return httpHeaders;
    }

    @Override
    public Flux<DataBuffer> getBody() {
        return DataBufferUtils.read(new ByteArrayResource(requestBody),
                new NettyDataBufferFactory(ByteBufAllocator.DEFAULT), length);
    }
}

下面是完整的路由配置:

spring:
  cloud:
    gateway:
      routes:
        - id: xxx-gateway
          uri: lb://xxx-service
          predicates:
            - Path=/xxx-gateway/gateway
            - ApiMethod=user.add,user.update
          filters:
            - BizContent=/user/$\{method}

微服务处理接口定义:

@RestController
@RequestMapping("user")
public class UserController {

    @PostMapping("user.add")
    public Result<?> add(@RequestBody UserAdd userAdd) {}

    @PostMapping("user.update")
    public Result<?> add(@RequestBody UserUpdate userUpdate) {}
}

至此,两个工厂完美结合。