使用Spring Cloud Gateway开发内部API网关时,当业务的Http请求体大小超过256K时,会出现如下报错:Exceeded limit on max bytes to buffer : 262144。
Spring Boot框架给了两种方式来修改这个大小的方式:
方式一:使用修改配置参数值,spring.max-in-memory-size: 1024 * 1024 的方式
spring: application: name: gateway codec: max-in-memory-size: 1024 * 1024
方式二:使用WebFluxConfigurer,通过set方法设置max-in-memory-size
@Override public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { configurer.defaultCodecs().maxInMemorySize((int) maxInMemorySize.toBytes()); }
但在实际上线以后发现并为解决256K的限制,这是为什么呢?
首先排查了SpringBoot是如何使用spring.max-in-memory-size这个参数的,当业务在启动时候设置的参数到底是怎么执行的。
这个类主要是加载Encoders和Decoders,如果我们使用的application/json的请求格式,并且项目里面也存在jackson2的包,则SpringBoot会加载,Jackson2JsonDecoder 和Jackson2JsonEncoder:
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ObjectMapper.class) static class JacksonCodecConfiguration { @Bean @Order(0) @ConditionalOnBean(ObjectMapper.class) CodecCustomizer jacksonCodecCustomizer(ObjectMapper objectMapper) { return (configurer) -> { CodecConfigurer.DefaultCodecs defaults = configurer.defaultCodecs(); // 这里会new一个Jackson2JsonDecoder defaults.jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper, EMPTY_MIME_TYPES)); defaults.jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper, EMPTY_MIME_TYPES)); }; } }
通过追踪到这里,我们可以看出来是执行了setMaxInMemorySize方法的,那么我们只需要知道这个set之前的值是从哪里来的。
如果通过上述的两个方式来修改setMaxInMemorySize,会调用BaseDefaultCodecs的maxInMemorySize方法来设置的:
org.springframework.http.codec.support.BaseDefaultCodecs @Override public void maxInMemorySize(int byteCount) { if (!ObjectUtils.nullSafeEquals(this.maxInMemorySize, byteCount)) { this.maxInMemorySize = byteCount; initReaders(); } }
到这里可以确定的逻辑是,不管是通过哪种set maxInMemorySize的方式,都会执行Jackson2JsonDecoder父类AbstractJackson2Decoder的setMaxInMemorySize方法来修改maxInMemorySize这个私有属性:
而这个maxInMemorySize默认的256 * 1024 就是控制256K请求体的大小。具体可以查看org.springframework.http.codec.json.AbstractJackson2Decoder的decodeToMono方法,这个方法负责将http请求的body转换成json格式:
@Override public Mono
DataBufferUtils.join(input, this.maxInMemorySize)方法里面使用了LimitedDataBufferList:
如果超过了maxByteCount则会抛出raiseLimitException,这里就是异常具体抛出的地方:
既然代码流程里面执行了setMaxInMemorySize,但是却没生效,那么这里到底执行以后maxInMemorySize的值是多少呢?
通过arthas工具查看了AbstractJackson2Decoder值:
// sc 获取classLoadHash: sc -d org.springframework.http.codec.json.AbstractJackson2Decoder // 获取value [arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[0].maxInMemorySize' @Integer[1048576] [arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[1].maxInMemorySize' @Integer[262144] [arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[2].maxInMemorySize' @Integer[262144] [arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[3].maxInMemorySize' @Integer[262144] [arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[4].maxInMemorySize' @Integer[262144] [arthas@48]$ vmtool -c 18b4aac2 -a getInstances --className org.springframework.http.codec.json.AbstractJackson2Decoder --express '#val=instances[5].maxInMemorySize'
通过获取多个AbstractJackson2Decoder实例的maxInMemorySize属性发现只有一个设置了1048576=1M,其他都是262144=256K,看到这里已经可以得出结论,不是setMaxInMemorySize没有生效,而是只生效了一个实例,因为初始化的时候只new了一个Jackson2JsonDecoder。
那其他的Jackson2JsonDecoder是从哪里来的?
查看了Spring Cloud Gateway源码,发现了它给的代码中将Http Request Body 转换成Json的方式如下:
org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory
在witeWith方法里面定义一个将Reques转换成Json的方法deserializeJSONRequest:
这里我们会发现如果你按照官方给的这种写法,每次请求进来会new Jackson2JsonDecoder,每次new出来的新对象是没有执行setMaxInMemorySize操作的,所以后期new的对象都默认使用了256 * 1024。
到此问题已经得到结论,不是SpringBoot框架setMaxInMemorySize没有生效,而是后期new的对象没有重新setMaxInMemorySize。
如果要解决上述问题,我们需要保证每个新new的Jackson2JsonDecoder都能够使用设置的spring.max-in-memory-size,方法可以参考如下几种解决方案:
这种方法简单粗暴,直接将AbstractJackson2Decoder的maxInMemorySize改成spring.max-in-memory-size,通过自己写的AbstractJackson2Decoder覆盖SpringBoot框架里的AbstractJackson2Decoder,亲测有效。
将maxInMemorySize改成static以后,则SpringBoot框架在初始化的时候执行一次setMaxInMemorySize则也能解决其他new的Jackson2JsonDecoder 对象maxInMemorySize不生效问题:
private FluxdeserializeJSONRequest(ServerWebExchange exchange) { return exchange.getRequest().getBody().map(dataBufferBody -> { ResolvableType targetType = ResolvableType.forType(JsonNode.class); Jackson2JsonDecoder Jackson2JsonDecoder = new Jackson2JsonDecoder(); // 每次创建对象以后需要设置setMaxInMemorySize Jackson2JsonDecoder.setMaxInMemorySize(1024 * 1024); return Jackson2JsonDecoder.decode(dataBufferBody, targetType, null, null); }).cast(JsonNode.class); }