Browse Source
- Add maxInMemorySize property to Decoder and HttpMessageReader implementations that aggregate input to trigger DataBufferLimitException when reached. - For codecs that call DataBufferUtils#join, there is now an overloaded variant with a maxInMemorySize extra argument. Internally, a custom LimitedDataBufferList is used to count and enforce the limit. - Jackson2Tokenizer and XmlEventDecoder support those limits per streamed JSON object. See gh-23884pull/23891/head
16 changed files with 672 additions and 68 deletions
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.core.io.buffer; |
||||
|
||||
/** |
||||
* Exception that indicates the cumulative number of bytes consumed from a |
||||
* stream of {@link DataBuffer DataBuffer}'s exceeded some pre-configured limit. |
||||
* This can be raised when data buffers are cached and aggregated, e.g. |
||||
* {@link DataBufferUtils#join}. Or it could also be raised when data buffers |
||||
* have been released but a parsed representation is being aggregated, e.g. async |
||||
* parsing with Jackson. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.1.11 |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class DataBufferLimitException extends IllegalStateException { |
||||
|
||||
|
||||
public DataBufferLimitException(String message) { |
||||
super(message); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.core.io.buffer; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.Collection; |
||||
import java.util.List; |
||||
import java.util.function.Predicate; |
||||
|
||||
import reactor.core.publisher.Flux; |
||||
|
||||
/** |
||||
* Custom {@link List} to collect data buffers with and enforce a |
||||
* limit on the total number of bytes buffered. For use with "collect" or |
||||
* other buffering operators in declarative APIs, e.g. {@link Flux}. |
||||
* |
||||
* <p>Adding elements increases the byte count and if the limit is exceeded, |
||||
* {@link DataBufferLimitException} is raised. {@link #clear()} resets the |
||||
* count. Remove and set are not supported. |
||||
* |
||||
* <p><strong>Note:</strong> This class does not automatically release the |
||||
* buffers it contains. It is usually preferable to use hooks such as |
||||
* {@link Flux#doOnDiscard} that also take care of cancel and error signals, |
||||
* or otherwise {@link #releaseAndClear()} can be used. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.1.11 |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class LimitedDataBufferList extends ArrayList<DataBuffer> { |
||||
|
||||
private final int maxByteCount; |
||||
|
||||
private int byteCount; |
||||
|
||||
|
||||
public LimitedDataBufferList(int maxByteCount) { |
||||
this.maxByteCount = maxByteCount; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public boolean add(DataBuffer buffer) { |
||||
boolean result = super.add(buffer); |
||||
if (result) { |
||||
updateCount(buffer.readableByteCount()); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public void add(int index, DataBuffer buffer) { |
||||
super.add(index, buffer); |
||||
updateCount(buffer.readableByteCount()); |
||||
} |
||||
|
||||
@Override |
||||
public boolean addAll(Collection<? extends DataBuffer> collection) { |
||||
boolean result = super.addAll(collection); |
||||
collection.forEach(buffer -> updateCount(buffer.readableByteCount())); |
||||
return result; |
||||
} |
||||
|
||||
@Override |
||||
public boolean addAll(int index, Collection<? extends DataBuffer> collection) { |
||||
boolean result = super.addAll(index, collection); |
||||
collection.forEach(buffer -> updateCount(buffer.readableByteCount())); |
||||
return result; |
||||
} |
||||
|
||||
private void updateCount(int bytesToAdd) { |
||||
if (this.maxByteCount < 0) { |
||||
return; |
||||
} |
||||
if (bytesToAdd > Integer.MAX_VALUE - this.byteCount) { |
||||
raiseLimitException(); |
||||
} |
||||
else { |
||||
this.byteCount += bytesToAdd; |
||||
if (this.byteCount > this.maxByteCount) { |
||||
raiseLimitException(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void raiseLimitException() { |
||||
// Do not release here, it's likely down via doOnDiscard..
|
||||
throw new DataBufferLimitException( |
||||
"Exceeded limit on max bytes to buffer : " + this.maxByteCount); |
||||
} |
||||
|
||||
@Override |
||||
public DataBuffer remove(int index) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean remove(Object o) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
protected void removeRange(int fromIndex, int toIndex) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean removeAll(Collection<?> c) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean removeIf(Predicate<? super DataBuffer> filter) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
public DataBuffer set(int index, DataBuffer element) { |
||||
throw new UnsupportedOperationException(); |
||||
} |
||||
|
||||
@Override |
||||
public void clear() { |
||||
this.byteCount = 0; |
||||
super.clear(); |
||||
} |
||||
|
||||
/** |
||||
* Shortcut to {@link DataBufferUtils#release release} all data buffers and |
||||
* then {@link #clear()}. |
||||
*/ |
||||
public void releaseAndClear() { |
||||
forEach(buf -> { |
||||
try { |
||||
DataBufferUtils.release(buf); |
||||
} |
||||
catch (Throwable ex) { |
||||
// Keep going..
|
||||
} |
||||
}); |
||||
clear(); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.core.io.buffer; |
||||
|
||||
import java.nio.charset.StandardCharsets; |
||||
|
||||
import org.assertj.core.api.Assertions; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
/** |
||||
* Unit tests for {@link LimitedDataBufferList}. |
||||
* @author Rossen Stoyanchev |
||||
* @since 5.1.11 |
||||
*/ |
||||
public class LimitedDataBufferListTests { |
||||
|
||||
private final static DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); |
||||
|
||||
|
||||
@Test |
||||
void limitEnforced() { |
||||
Assertions.assertThatThrownBy(() -> new LimitedDataBufferList(5).add(toDataBuffer("123456"))) |
||||
.isInstanceOf(DataBufferLimitException.class); |
||||
} |
||||
|
||||
@Test |
||||
void limitIgnored() { |
||||
new LimitedDataBufferList(-1).add(toDataBuffer("123456")); |
||||
} |
||||
|
||||
@Test |
||||
void clearResetsCount() { |
||||
LimitedDataBufferList list = new LimitedDataBufferList(5); |
||||
list.add(toDataBuffer("12345")); |
||||
list.clear(); |
||||
list.add(toDataBuffer("12345")); |
||||
} |
||||
|
||||
|
||||
private static DataBuffer toDataBuffer(String value) { |
||||
return bufferFactory.wrap(value.getBytes(StandardCharsets.UTF_8)); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue