Introduce support for Netty 5 Buffer

This commit introduces support for Netty 5's Buffer, in the form of
Netty5DataBuffer. Because of the new API offered by Buffer, several
changes have been made to the DataBuffer API:

- CloseableDataBuffer is a simpler alternative to PooledDataBuffer, and
  implemented by Netty5DataBuffer. DataBufferUtils::release can now
  handle CloseableDataBuffer as well as PooledDataBuffer.
- PooledDataBuffer::touch has been moved into a separate interface:
  TouchableDataBuffer, which is implemented by Netty5DataBuffer.
- The capacity of DataBuffers can no longer be reduced, they can only
  grow larger. As a consequence, DataBuffer::capacity(int) has been
  deprecated, but ensureWritable (formally ensureCapacity) still exists.
- DataBuffer::slice and retainedSlice have been deprecated in favor of
  split, a new method that ensures that memory regions do not overlap.
- DataBuffer::asByteBuffer has been deprecated in favor of toByteBuffer,
  a new method that returns a copy, instead of shared data.
- DataBufferFactory::allocateBuffer has been deprecated in favor of
  allocateBuffer(int).

Closes gh-28874
This commit is contained in:
Arjen Poutsma
2022-07-27 15:24:14 +02:00
parent 8a61866ee2
commit 9c33d2707a
64 changed files with 1663 additions and 360 deletions

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2002-2022 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.codec;
import java.nio.charset.StandardCharsets;
import java.util.function.Consumer;
import io.netty5.buffer.api.Buffer;
import io.netty5.buffer.api.DefaultBufferAllocators;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
*/
class Netty5BufferDecoderTests extends AbstractDecoderTests<Netty5BufferDecoder> {
private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8);
private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8);
Netty5BufferDecoderTests() {
super(new Netty5BufferDecoder());
}
@Override
@Test
public void canDecode() {
assertThat(this.decoder.canDecode(ResolvableType.forClass(Buffer.class),
MimeTypeUtils.TEXT_PLAIN)).isTrue();
assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class),
MimeTypeUtils.TEXT_PLAIN)).isFalse();
assertThat(this.decoder.canDecode(ResolvableType.forClass(Buffer.class),
MimeTypeUtils.APPLICATION_JSON)).isTrue();
}
@Override
@Test
public void decode() {
Flux<DataBuffer> input = Flux.concat(
dataBuffer(this.fooBytes),
dataBuffer(this.barBytes));
testDecodeAll(input, Buffer.class, step -> step
.consumeNextWith(expectByteBuffer(DefaultBufferAllocators.preferredAllocator().copyOf(this.fooBytes)))
.consumeNextWith(expectByteBuffer(DefaultBufferAllocators.preferredAllocator().copyOf(this.barBytes)))
.verifyComplete());
}
@Override
@Test
public void decodeToMono() {
Flux<DataBuffer> input = Flux.concat(
dataBuffer(this.fooBytes),
dataBuffer(this.barBytes));
Buffer expected = DefaultBufferAllocators.preferredAllocator().allocate(this.fooBytes.length + this.barBytes.length)
.writeBytes(this.fooBytes)
.writeBytes(this.barBytes)
.readerOffset(0);
testDecodeToMonoAll(input, Buffer.class, step -> step
.consumeNextWith(expectByteBuffer(expected))
.verifyComplete());
}
private Consumer<Buffer> expectByteBuffer(Buffer expected) {
return actual -> assertThat(actual).isEqualTo(expected);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2002-2022 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.codec;
import java.nio.charset.StandardCharsets;
import io.netty5.buffer.api.Buffer;
import io.netty5.buffer.api.DefaultBufferAllocators;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import org.springframework.core.ResolvableType;
import org.springframework.core.testfixture.codec.AbstractEncoderTests;
import org.springframework.util.MimeTypeUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
*/
class Netty5BufferEncoderTests extends AbstractEncoderTests<Netty5BufferEncoder> {
private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8);
private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8);
Netty5BufferEncoderTests() {
super(new Netty5BufferEncoder());
}
@Override
@Test
public void canEncode() {
assertThat(this.encoder.canEncode(ResolvableType.forClass(Buffer.class),
MimeTypeUtils.TEXT_PLAIN)).isTrue();
assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class),
MimeTypeUtils.TEXT_PLAIN)).isFalse();
assertThat(this.encoder.canEncode(ResolvableType.forClass(Buffer.class),
MimeTypeUtils.APPLICATION_JSON)).isTrue();
// gh-20024
assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse();
}
@Override
@Test
public void encode() {
Flux<Buffer> input = Flux.just(this.fooBytes, this.barBytes)
.map(DefaultBufferAllocators.preferredAllocator()::copyOf);
testEncodeAll(input, Buffer.class, step -> step
.consumeNextWith(expectBytes(this.fooBytes))
.consumeNextWith(expectBytes(this.barBytes))
.verifyComplete());
}
}

View File

@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.junit.jupiter.api.Assumptions.assumeFalse;
/**
* @author Arjen Poutsma
@@ -402,6 +403,9 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
@ParameterizedDataBufferAllocatingTest
void decreaseCapacityLowReadPosition(DataBufferFactory bufferFactory) {
assumeFalse(bufferFactory instanceof Netty5DataBufferFactory,
"Netty 5 does not support decreasing the capacity");
super.bufferFactory = bufferFactory;
DataBuffer buffer = createDataBuffer(2);
@@ -414,6 +418,9 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
@ParameterizedDataBufferAllocatingTest
void decreaseCapacityHighReadPosition(DataBufferFactory bufferFactory) {
assumeFalse(bufferFactory instanceof Netty5DataBufferFactory,
"Netty 5 does not support decreasing the capacity");
super.bufferFactory = bufferFactory;
DataBuffer buffer = createDataBuffer(2);
@@ -492,6 +499,7 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
}
@ParameterizedDataBufferAllocatingTest
@SuppressWarnings("deprecation")
void asByteBuffer(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
@@ -513,6 +521,7 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
}
@ParameterizedDataBufferAllocatingTest
@SuppressWarnings("deprecation")
void asByteBufferIndexLength(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
@@ -522,6 +531,9 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
ByteBuffer result = buffer.asByteBuffer(1, 2);
assertThat(result.capacity()).isEqualTo(2);
assumeFalse(bufferFactory instanceof Netty5DataBufferFactory,
"Netty 5 does share the internal buffer");
buffer.write((byte) 'c');
assertThat(result.remaining()).isEqualTo(2);
@@ -533,7 +545,11 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
}
@ParameterizedDataBufferAllocatingTest
@SuppressWarnings("deprecation")
void byteBufferContainsDataBufferChanges(DataBufferFactory bufferFactory) {
assumeFalse(bufferFactory instanceof Netty5DataBufferFactory,
"Netty 5 does not support sharing data between buffers");
super.bufferFactory = bufferFactory;
DataBuffer dataBuffer = createDataBuffer(1);
@@ -549,7 +565,11 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
}
@ParameterizedDataBufferAllocatingTest
@SuppressWarnings("deprecation")
void dataBufferContainsByteBufferChanges(DataBufferFactory bufferFactory) {
assumeFalse(bufferFactory instanceof Netty5DataBufferFactory,
"Netty 5 does not support sharing data between buffers");
super.bufferFactory = bufferFactory;
DataBuffer dataBuffer = createDataBuffer(1);
@@ -565,6 +585,7 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
}
@ParameterizedDataBufferAllocatingTest
@SuppressWarnings("deprecation")
void emptyAsByteBuffer(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
@@ -576,6 +597,45 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
release(buffer);
}
@ParameterizedDataBufferAllocatingTest
void toByteBuffer(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
DataBuffer buffer = createDataBuffer(4);
buffer.write(new byte[]{'a', 'b', 'c'});
buffer.read(); // skip a
ByteBuffer result = buffer.toByteBuffer();
assertThat(result.capacity()).isEqualTo(2);
assertThat(result.remaining()).isEqualTo(2);
byte[] resultBytes = new byte[2];
result.get(resultBytes);
assertThat(resultBytes).isEqualTo(new byte[]{'b', 'c'});
release(buffer);
}
@ParameterizedDataBufferAllocatingTest
void toByteBufferIndexLength(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
DataBuffer buffer = createDataBuffer(3);
buffer.write(new byte[]{'a', 'b', 'c'});
ByteBuffer result = buffer.toByteBuffer(1, 2);
assertThat(result.capacity()).isEqualTo(2);
assertThat(result.remaining()).isEqualTo(2);
byte[] resultBytes = new byte[2];
result.get(resultBytes);
assertThat(resultBytes).isEqualTo(new byte[]{'b', 'c'});
release(buffer);
}
@ParameterizedDataBufferAllocatingTest
void indexOf(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
@@ -630,6 +690,7 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
}
@ParameterizedDataBufferAllocatingTest
@SuppressWarnings("deprecation")
void slice(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
@@ -638,7 +699,6 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
DataBuffer slice = buffer.slice(1, 2);
assertThat(slice.readableByteCount()).isEqualTo(2);
assertThatException().isThrownBy(() -> slice.write((byte) 0));
buffer.write((byte) 'c');
assertThat(buffer.readableByteCount()).isEqualTo(3);
@@ -651,13 +711,18 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
result = new byte[2];
slice.read(result);
assertThat(result).isEqualTo(new byte[]{'b', 'c'});
if (!(bufferFactory instanceof Netty5DataBufferFactory)) {
assertThat(result).isEqualTo(new byte[]{'b', 'c'});
}
else {
assertThat(result).isEqualTo(new byte[]{'b', 0});
release(slice);
}
release(buffer);
}
@ParameterizedDataBufferAllocatingTest
@SuppressWarnings("deprecation")
void retainedSlice(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
@@ -666,7 +731,6 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
DataBuffer slice = buffer.retainedSlice(1, 2);
assertThat(slice.readableByteCount()).isEqualTo(2);
assertThatException().isThrownBy(() -> slice.write((byte) 0));
buffer.write((byte) 'c');
assertThat(buffer.readableByteCount()).isEqualTo(3);
@@ -679,8 +743,12 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
result = new byte[2];
slice.read(result);
assertThat(result).isEqualTo(new byte[]{'b', 'c'});
if (!(bufferFactory instanceof Netty5DataBufferFactory)) {
assertThat(result).isEqualTo(new byte[]{'b', 'c'});
}
else {
assertThat(result).isEqualTo(new byte[]{'b', 0});
}
release(buffer, slice);
}
@@ -705,6 +773,58 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
release(buffer);
}
@ParameterizedDataBufferAllocatingTest
void split(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
DataBuffer buffer = createDataBuffer(3);
buffer.write(new byte[]{'a', 'b'});
assertThatException().isThrownBy(() -> buffer.split(-1));
assertThatException().isThrownBy(() -> buffer.split(4));
DataBuffer split = buffer.split(1);
assertThat(split.readPosition()).isEqualTo(0);
assertThat(split.writePosition()).isEqualTo(1);
assertThat(split.capacity()).isEqualTo(1);
assertThat(split.readableByteCount()).isEqualTo(1);
byte[] bytes = new byte[1];
split.read(bytes);
assertThat(bytes).containsExactly('a');
assertThat(buffer.readPosition()).isEqualTo(0);
assertThat(buffer.writePosition()).isEqualTo(1);
assertThat(buffer.capacity()).isEqualTo(2);
buffer.write((byte) 'c');
assertThat(buffer.readableByteCount()).isEqualTo(2);
bytes = new byte[2];
buffer.read(bytes);
assertThat(bytes).isEqualTo(new byte[]{'b', 'c'});
DataBuffer buffer2 = createDataBuffer(1);
buffer2.write(new byte[]{'a'});
split = buffer2.split(1);
assertThat(split.readPosition()).isEqualTo(0);
assertThat(split.writePosition()).isEqualTo(1);
assertThat(split.capacity()).isEqualTo(1);
assertThat(split.readableByteCount()).isEqualTo(1);
bytes = new byte[1];
split.read(bytes);
assertThat(bytes).containsExactly('a');
assertThat(buffer2.readPosition()).isEqualTo(0);
assertThat(buffer2.writePosition()).isEqualTo(0);
assertThat(buffer2.capacity()).isEqualTo(0);
assertThat(buffer.readableByteCount()).isEqualTo(0);
release(buffer, buffer2);
}
@ParameterizedDataBufferAllocatingTest
void join(DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;

View File

@@ -115,7 +115,7 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests {
DataBufferUtils.readByteChannel(() -> channel, super.bufferFactory, 3);
StepVerifier.create(result)
.consumeNextWith(stringConsumer("foo"))
.consumeNextWith(stringConsumer(""))
.expectError(IOException.class)
.verify(Duration.ofSeconds(3));
}
@@ -170,17 +170,15 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests {
willAnswer(invocation -> {
ByteBuffer byteBuffer = invocation.getArgument(0);
byteBuffer.put("foo".getBytes(StandardCharsets.UTF_8));
byteBuffer.flip();
long pos = invocation.getArgument(1);
assertThat(pos).isEqualTo(0);
DataBuffer dataBuffer = invocation.getArgument(2);
CompletionHandler<Integer, DataBuffer> completionHandler = invocation.getArgument(3);
completionHandler.completed(3, dataBuffer);
CompletionHandler<Integer, ByteBuffer> completionHandler = invocation.getArgument(3);
completionHandler.completed(3, byteBuffer);
return null;
}).willAnswer(invocation -> {
DataBuffer dataBuffer = invocation.getArgument(2);
CompletionHandler<Integer, DataBuffer> completionHandler = invocation.getArgument(3);
completionHandler.failed(new IOException(), dataBuffer);
ByteBuffer byteBuffer = invocation.getArgument(0);
CompletionHandler<Integer, ByteBuffer> completionHandler = invocation.getArgument(3);
completionHandler.failed(new IOException(), byteBuffer);
return null;
})
.given(channel).read(any(), anyLong(), any(), any());