Implement Eclipse Jetty core HTTP handler adapter

This provides an implementation of an HTTP Handler Adapter that is coded
directly to the Eclipse Jetty core API, bypassing any servlet
implementation.

This includes a Jetty implementation of the spring `WebSocketClient`
interface, `JettyWebSocketClient`, using an explicit dependency to the
jetty-websocket-api.

Closes gh-32097

Co-authored-by: Lachlan Roberts <lachlan@webtide.com>
Co-authored-by: Arjen Poutsma <arjen.poutsma@broadcom.com>
This commit is contained in:
gregw
2024-01-15 19:05:09 +11:00
committed by Simon Baslé
parent b7ec028149
commit 0a60c622cc
33 changed files with 1701 additions and 526 deletions

View File

@@ -355,7 +355,7 @@ public class DefaultDataBuffer implements DataBuffer {
}
@Override
public DataBuffer split(int index) {
public DefaultDataBuffer split(int index) {
checkIndex(index);
ByteBuffer split = this.byteBuffer.duplicate().clear()

View File

@@ -0,0 +1,359 @@
/*
* Copyright 2002-2024 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.ByteBuffer;
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.IntPredicate;
import org.eclipse.jetty.io.Content;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Implementation of the {@code DataBuffer} interface that can wrap a Jetty
* {@link Content.Chunk}. Typically constructed with {@link JettyDataBufferFactory}.
*
* @author Greg Wilkins
* @author Lachlan Roberts
* @author Arjen Poutsma
* @since 6.2
*/
public final class JettyDataBuffer implements PooledDataBuffer {
private final DefaultDataBuffer delegate;
@Nullable
private final Content.Chunk chunk;
private final JettyDataBufferFactory bufferFactory;
private final AtomicInteger refCount = new AtomicInteger(1);
JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate, Content.Chunk chunk) {
Assert.notNull(bufferFactory, "BufferFactory must not be null");
Assert.notNull(delegate, "Delegate must not be null");
Assert.notNull(chunk, "Chunk must not be null");
this.bufferFactory = bufferFactory;
this.delegate = delegate;
this.chunk = chunk;
this.chunk.retain();
}
JettyDataBuffer(JettyDataBufferFactory bufferFactory, DefaultDataBuffer delegate) {
Assert.notNull(bufferFactory, "BufferFactory must not be null");
Assert.notNull(delegate, "Delegate must not be null");
this.bufferFactory = bufferFactory;
this.delegate = delegate;
this.chunk = null;
}
@Override
public boolean isAllocated() {
return this.refCount.get() > 0;
}
@Override
public PooledDataBuffer retain() {
int result = this.refCount.updateAndGet(c -> {
if (c != 0) {
return c + 1;
}
else {
return 0;
}
});
if (result != 0 && this.chunk != null) {
this.chunk.retain();
}
return this;
}
@Override
public PooledDataBuffer touch(Object hint) {
return this;
}
@Override
public boolean release() {
int result = this.refCount.updateAndGet(c -> {
if (c != 0) {
return c - 1;
}
else {
throw new IllegalStateException("JettyDataBuffer already released: " + this);
}
});
if (this.chunk != null) {
return this.chunk.release();
}
else {
return result == 0;
}
}
@Override
public DataBufferFactory factory() {
return this.bufferFactory;
}
// delegation
@Override
public int indexOf(IntPredicate predicate, int fromIndex) {
return this.delegate.indexOf(predicate, fromIndex);
}
@Override
public int lastIndexOf(IntPredicate predicate, int fromIndex) {
return this.delegate.lastIndexOf(predicate, fromIndex);
}
@Override
public int readableByteCount() {
return this.delegate.readableByteCount();
}
@Override
public int writableByteCount() {
return this.delegate.writableByteCount();
}
@Override
public int capacity() {
return this.delegate.capacity();
}
@Override
@Deprecated
public DataBuffer capacity(int capacity) {
this.delegate.capacity(capacity);
return this;
}
@Override
public DataBuffer ensureWritable(int capacity) {
this.delegate.ensureWritable(capacity);
return this;
}
@Override
public int readPosition() {
return this.delegate.readPosition();
}
@Override
public DataBuffer readPosition(int readPosition) {
this.delegate.readPosition(readPosition);
return this;
}
@Override
public int writePosition() {
return this.delegate.writePosition();
}
@Override
public DataBuffer writePosition(int writePosition) {
this.delegate.writePosition(writePosition);
return this;
}
@Override
public byte getByte(int index) {
return this.delegate.getByte(index);
}
@Override
public byte read() {
return this.delegate.read();
}
@Override
public DataBuffer read(byte[] destination) {
this.delegate.read(destination);
return this;
}
@Override
public DataBuffer read(byte[] destination, int offset, int length) {
this.delegate.read(destination, offset, length);
return this;
}
@Override
public DataBuffer write(byte b) {
this.delegate.write(b);
return this;
}
@Override
public DataBuffer write(byte[] source) {
this.delegate.write(source);
return this;
}
@Override
public DataBuffer write(byte[] source, int offset, int length) {
this.delegate.write(source, offset, length);
return this;
}
@Override
public DataBuffer write(DataBuffer... buffers) {
this.delegate.write(buffers);
return this;
}
@Override
public DataBuffer write(ByteBuffer... buffers) {
this.delegate.write(buffers);
return this;
}
@Override
@Deprecated
public DataBuffer slice(int index, int length) {
DefaultDataBuffer delegateSlice = this.delegate.slice(index, length);
if (this.chunk != null) {
this.chunk.retain();
return new JettyDataBuffer(this.bufferFactory, delegateSlice, this.chunk);
}
else {
return new JettyDataBuffer(this.bufferFactory, delegateSlice);
}
}
@Override
public DataBuffer split(int index) {
DefaultDataBuffer delegateSplit = this.delegate.split(index);
if (this.chunk != null) {
this.chunk.retain();
return new JettyDataBuffer(this.bufferFactory, delegateSplit, this.chunk);
}
else {
return new JettyDataBuffer(this.bufferFactory, delegateSplit);
}
}
@Override
@Deprecated
public ByteBuffer asByteBuffer() {
return this.delegate.asByteBuffer();
}
@Override
@Deprecated
public ByteBuffer asByteBuffer(int index, int length) {
return this.delegate.asByteBuffer(index, length);
}
@Override
@Deprecated
public ByteBuffer toByteBuffer(int index, int length) {
return this.delegate.toByteBuffer(index, length);
}
@Override
public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) {
this.delegate.toByteBuffer(srcPos, dest, destPos, length);
}
@Override
public ByteBufferIterator readableByteBuffers() {
ByteBufferIterator delegateIterator = this.delegate.readableByteBuffers();
if (this.chunk != null) {
return new JettyByteBufferIterator(delegateIterator, this.chunk);
}
else {
return delegateIterator;
}
}
@Override
public ByteBufferIterator writableByteBuffers() {
ByteBufferIterator delegateIterator = this.delegate.writableByteBuffers();
if (this.chunk != null) {
return new JettyByteBufferIterator(delegateIterator, this.chunk);
}
else {
return delegateIterator;
}
}
@Override
public String toString(int index, int length, Charset charset) {
return this.delegate.toString(index, length, charset);
}
@Override
public int hashCode() {
return this.delegate.hashCode();
}
@Override
public boolean equals(Object o) {
return this == o || (o instanceof JettyDataBuffer other &&
this.delegate.equals(other.delegate));
}
@Override
public String toString() {
return String.format("JettyDataBuffer (r: %d, w: %d, c: %d)",
readPosition(), writePosition(), capacity());
}
private static final class JettyByteBufferIterator implements ByteBufferIterator {
private final ByteBufferIterator delegate;
private final Content.Chunk chunk;
public JettyByteBufferIterator(ByteBufferIterator delegate, Content.Chunk chunk) {
Assert.notNull(delegate, "Delegate must not be null");
Assert.notNull(chunk, "Chunk must not be null");
this.delegate = delegate;
this.chunk = chunk;
this.chunk.retain();
}
@Override
public void close() {
this.delegate.close();
this.chunk.release();
}
@Override
public boolean hasNext() {
return this.delegate.hasNext();
}
@Override
public ByteBuffer next() {
return this.delegate.next();
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright 2002-2024 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.ByteBuffer;
import java.util.List;
import org.eclipse.jetty.io.Content;
/**
* Implementation of the {@code DataBufferFactory} interface that creates
* {@link JettyDataBuffer} instances.
*
* @author Arjen Poutsma
* @since 6.2
*/
public class JettyDataBufferFactory implements DataBufferFactory {
private final DefaultDataBufferFactory delegate;
/**
* Creates a new {@code JettyDataBufferFactory} with default settings.
*/
public JettyDataBufferFactory() {
this(false);
}
/**
* Creates a new {@code JettyDataBufferFactory}, indicating whether direct
* buffers should be created by {@link #allocateBuffer()} and
* {@link #allocateBuffer(int)}.
* @param preferDirect {@code true} if direct buffers are to be preferred;
* {@code false} otherwise
*/
public JettyDataBufferFactory(boolean preferDirect) {
this(preferDirect, DefaultDataBufferFactory.DEFAULT_INITIAL_CAPACITY);
}
/**
* Creates a new {@code JettyDataBufferFactory}, indicating whether direct
* buffers should be created by {@link #allocateBuffer()} and
* {@link #allocateBuffer(int)}, and what the capacity is to be used for
* {@link #allocateBuffer()}.
* @param preferDirect {@code true} if direct buffers are to be preferred;
* {@code false} otherwise
*/
public JettyDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) {
this.delegate = new DefaultDataBufferFactory(preferDirect, defaultInitialCapacity);
}
@Override
@Deprecated
public JettyDataBuffer allocateBuffer() {
DefaultDataBuffer delegate = this.delegate.allocateBuffer();
return new JettyDataBuffer(this, delegate);
}
@Override
public JettyDataBuffer allocateBuffer(int initialCapacity) {
DefaultDataBuffer delegate = this.delegate.allocateBuffer(initialCapacity);
return new JettyDataBuffer(this, delegate);
}
@Override
public JettyDataBuffer wrap(ByteBuffer byteBuffer) {
DefaultDataBuffer delegate = this.delegate.wrap(byteBuffer);
return new JettyDataBuffer(this, delegate);
}
@Override
public JettyDataBuffer wrap(byte[] bytes) {
DefaultDataBuffer delegate = this.delegate.wrap(bytes);
return new JettyDataBuffer(this, delegate);
}
public JettyDataBuffer wrap(Content.Chunk chunk) {
ByteBuffer byteBuffer = chunk.getByteBuffer();
DefaultDataBuffer delegate = this.delegate.wrap(byteBuffer);
return new JettyDataBuffer(this, delegate, chunk);
}
@Override
public JettyDataBuffer join(List<? extends DataBuffer> dataBuffers) {
DefaultDataBuffer delegate = this.delegate.join(dataBuffers);
return new JettyDataBuffer(this, delegate);
}
@Override
public boolean isDirect() {
return this.delegate.isDirect();
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2002-2024 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.ByteBuffer;
import org.eclipse.jetty.io.Content;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
/**
* @author Arjen Poutsma
*/
public class JettyDataBufferTests {
private final JettyDataBufferFactory dataBufferFactory = new JettyDataBufferFactory();
@Test
void releaseRetainChunk() {
ByteBuffer buffer = ByteBuffer.allocate(3);
Content.Chunk mockChunk = mock();
given(mockChunk.getByteBuffer()).willReturn(buffer);
given(mockChunk.release()).willReturn(false, false, true);
JettyDataBuffer dataBuffer = this.dataBufferFactory.wrap(mockChunk);
dataBuffer.retain();
dataBuffer.retain();
assertThat(dataBuffer.release()).isFalse();
assertThat(dataBuffer.release()).isFalse();
assertThat(dataBuffer.release()).isTrue();
assertThatIllegalStateException().isThrownBy(dataBuffer::release);
then(mockChunk).should(times(3)).retain();
then(mockChunk).should(times(3)).release();
}
}

View File

@@ -69,6 +69,15 @@ class PooledDataBufferTests {
}
}
@Nested
class Jetty implements PooledDataBufferTestingTrait {
@Override
public DataBufferFactory createDataBufferFactory() {
return new JettyDataBufferFactory();
}
}
interface PooledDataBufferTestingTrait {
@@ -82,10 +91,14 @@ class PooledDataBufferTests {
default void retainAndRelease() {
PooledDataBuffer buffer = createDataBuffer(1);
buffer.write((byte) 'a');
assertThat(buffer.isAllocated()).isTrue();
buffer.retain();
assertThat(buffer.isAllocated()).isTrue();
assertThat(buffer.release()).isFalse();
assertThat(buffer.isAllocated()).isTrue();
assertThat(buffer.release()).isTrue();
assertThat(buffer.isAllocated()).isFalse();
}
@Test