Early support for Jetty 12 (developed against 12.0.0.alpha2)

Reflective getHeaders calls to be revisited; see GitHub issue #8938 in Jetty project.
HttpOutput optimization commented out still in order to avoid alpha build dependency.

See gh-29575
This commit is contained in:
Juergen Hoeller 2022-11-25 17:04:48 +01:00
parent 4ff18741fd
commit 45d45c2989
8 changed files with 109 additions and 35 deletions

View File

@ -37,6 +37,12 @@ dependencies {
optional("org.eclipse.jetty:jetty-servlet") { optional("org.eclipse.jetty:jetty-servlet") {
exclude group: "jakarta.servlet", module: "jakarta.servlet-api" exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
} }
/* Jetty 12: see org.springframework.http.server.reactive.JettyHttpHandlerAdapter
optional("org.eclipse.jetty.ee10:jetty-ee10-servlet:12.0.0.alpha2") {
exclude group: "jakarta.servlet", module: "jakarta.servlet-api"
exclude group: "org.eclipse.jetty", module: "jetty-session"
}
*/
optional("org.eclipse.jetty:jetty-reactive-httpclient") optional("org.eclipse.jetty:jetty-reactive-httpclient")
optional('org.apache.httpcomponents.client5:httpclient5') optional('org.apache.httpcomponents.client5:httpclient5')
optional('org.apache.httpcomponents.core5:httpcore5-reactive') optional('org.apache.httpcomponents.core5:httpcore5-reactive')

View File

@ -18,7 +18,6 @@ package org.springframework.http.client.reactive;
import java.util.AbstractSet; import java.util.AbstractSet;
import java.util.Collection; import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -186,7 +185,6 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
public Iterator<Entry<String, List<String>>> iterator() { public Iterator<Entry<String, List<String>>> iterator() {
return new EntryIterator(); return new EntryIterator();
} }
@Override @Override
public int size() { public int size() {
return headers.size(); return headers.size();
@ -203,16 +201,16 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
private class EntryIterator implements Iterator<Entry<String, List<String>>> { private class EntryIterator implements Iterator<Entry<String, List<String>>> {
private final Enumeration<String> names = headers.getFieldNames(); private final Iterator<String> names = headers.getFieldNamesCollection().iterator();
@Override @Override
public boolean hasNext() { public boolean hasNext() {
return this.names.hasMoreElements(); return this.names.hasNext();
} }
@Override @Override
public Entry<String, List<String>> next() { public Entry<String, List<String>> next() {
return new HeaderEntry(this.names.nextElement()); return new HeaderEntry(this.names.next());
} }
} }

View File

@ -18,7 +18,6 @@ package org.springframework.http.server.reactive;
import java.util.AbstractSet; import java.util.AbstractSet;
import java.util.Collection; import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -46,8 +45,8 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
private final HttpFields.Mutable headers; private final HttpFields.Mutable headers;
JettyHeadersAdapter(HttpFields.Mutable headers) { JettyHeadersAdapter(HttpFields headers) {
this.headers = headers; this.headers = (headers instanceof HttpFields.Mutable mutable ? mutable : HttpFields.build(headers));
} }
@ -170,7 +169,6 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
public Iterator<Entry<String, List<String>>> iterator() { public Iterator<Entry<String, List<String>>> iterator() {
return new EntryIterator(); return new EntryIterator();
} }
@Override @Override
public int size() { public int size() {
return headers.size(); return headers.size();
@ -187,16 +185,16 @@ class JettyHeadersAdapter implements MultiValueMap<String, String> {
private class EntryIterator implements Iterator<Entry<String, List<String>>> { private class EntryIterator implements Iterator<Entry<String, List<String>>> {
private final Enumeration<String> names = headers.getFieldNames(); private final Iterator<String> names = headers.getFieldNamesCollection().iterator();
@Override @Override
public boolean hasNext() { public boolean hasNext() {
return this.names.hasMoreElements(); return this.names.hasNext();
} }
@Override @Override
public Entry<String, List<String>> next() { public Entry<String, List<String>> next() {
return new HeaderEntry(this.names.nextElement()); return new HeaderEntry(this.names.next());
} }
} }

View File

@ -17,12 +17,13 @@
package org.springframework.http.server.reactive; package org.springframework.http.server.reactive;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -36,8 +37,11 @@ import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
/** /**
* {@link ServletHttpHandlerAdapter} extension that uses Jetty APIs for writing * {@link ServletHttpHandlerAdapter} extension that uses Jetty APIs for writing
@ -50,6 +54,23 @@ import org.springframework.util.MultiValueMap;
*/ */
public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter { public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter {
private static final boolean jetty11Present = ClassUtils.isPresent(
"org.eclipse.jetty.server.HttpOutput", JettyHttpHandlerAdapter.class.getClassLoader());
/* Jetty 12: see spring-web.gradle
private static final boolean jetty12Present = ClassUtils.isPresent(
"org.eclipse.jetty.ee10.servlet.HttpOutput", JettyHttpHandlerAdapter.class.getClassLoader());
*/
@Nullable
private static final Method getRequestHeadersMethod =
ClassUtils.getMethodIfAvailable(Request.class, "getHeaders");
@Nullable
private static final Method getResponseHeadersMethod =
ClassUtils.getMethodIfAvailable(Response.class, "getHeaders");
public JettyHttpHandlerAdapter(HttpHandler httpHandler) { public JettyHttpHandlerAdapter(HttpHandler httpHandler) {
super(httpHandler); super(httpHandler);
} }
@ -84,7 +105,13 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter {
private static MultiValueMap<String, String> createHeaders(HttpServletRequest servletRequest) { private static MultiValueMap<String, String> createHeaders(HttpServletRequest servletRequest) {
Request request = getRequest(servletRequest); Request request = getRequest(servletRequest);
HttpFields.Mutable fields = HttpFields.build(request.getHttpFields()); HttpFields fields;
if (getRequestHeadersMethod != null) {
fields = (HttpFields) ReflectionUtils.invokeMethod(getRequestHeadersMethod, request);
}
else {
fields = request.getHttpFields();
}
return new JettyHeadersAdapter(fields); return new JettyHeadersAdapter(fields);
} }
@ -115,7 +142,13 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter {
private static HttpHeaders createHeaders(HttpServletResponse servletResponse) { private static HttpHeaders createHeaders(HttpServletResponse servletResponse) {
Response response = getResponse(servletResponse); Response response = getResponse(servletResponse);
HttpFields.Mutable fields = response.getHttpFields(); HttpFields fields;
if (getResponseHeadersMethod != null) {
fields = (HttpFields) ReflectionUtils.invokeMethod(getResponseHeadersMethod, response);
}
else {
fields = response.getHttpFields();
}
return new HttpHeaders(new JettyHeadersAdapter(fields)); return new HttpHeaders(new JettyHeadersAdapter(fields));
} }
@ -135,16 +168,32 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter {
@Override @Override
protected int writeToOutputStream(DataBuffer dataBuffer) throws IOException { protected int writeToOutputStream(DataBuffer dataBuffer) throws IOException {
ByteBuffer input = dataBuffer.toByteBuffer(); OutputStream output = getOutputStream();
int len = input.remaining(); if (jetty11Present) {
ServletResponse response = getNativeResponse(); if (output instanceof HttpOutput httpOutput) {
((HttpOutput) response.getOutputStream()).write(input); ByteBuffer input = dataBuffer.toByteBuffer();
return len; int len = input.remaining();
httpOutput.write(input);
return len;
}
}
/* Jetty 12: see spring-web.gradle
else if (jetty12Present) {
if (output instanceof org.eclipse.jetty.ee10.servlet.HttpOutput httpOutput) {
ByteBuffer input = dataBuffer.toByteBuffer();
int len = input.remaining();
httpOutput.write(input);
return len;
}
}
*/
return super.writeToOutputStream(dataBuffer);
} }
@Override @Override
protected void applyHeaders() { protected void applyHeaders() {
HttpServletResponse response = getNativeResponse(); HttpServletResponse response = getNativeResponse();
MediaType contentType = null; MediaType contentType = null;
try { try {
contentType = getHeaders().getContentType(); contentType = getHeaders().getContentType();
@ -156,10 +205,12 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter {
if (response.getContentType() == null && contentType != null) { if (response.getContentType() == null && contentType != null) {
response.setContentType(contentType.toString()); response.setContentType(contentType.toString());
} }
Charset charset = (contentType != null ? contentType.getCharset() : null); Charset charset = (contentType != null ? contentType.getCharset() : null);
if (response.getCharacterEncoding() == null && charset != null) { if (response.getCharacterEncoding() == null && charset != null) {
response.setCharacterEncoding(charset.name()); response.setCharacterEncoding(charset.name());
} }
long contentLength = getHeaders().getContentLength(); long contentLength = getHeaders().getContentLength();
if (contentLength != -1) { if (contentLength != -1) {
response.setContentLengthLong(contentLength); response.setContentLengthLong(contentLength);

View File

@ -56,6 +56,7 @@ import org.springframework.util.StringUtils;
* Adapt {@link ServerHttpRequest} to the Servlet {@link HttpServletRequest}. * Adapt {@link ServerHttpRequest} to the Servlet {@link HttpServletRequest}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 5.0 * @since 5.0
*/ */
class ServletServerHttpRequest extends AbstractServerHttpRequest { class ServletServerHttpRequest extends AbstractServerHttpRequest {
@ -65,6 +66,8 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest {
private final HttpServletRequest request; private final HttpServletRequest request;
private final ServletInputStream inputStream;
private final RequestBodyPublisher bodyPublisher; private final RequestBodyPublisher bodyPublisher;
private final Object cookieLock = new Object(); private final Object cookieLock = new Object();
@ -99,8 +102,8 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest {
this.asyncListener = new RequestAsyncListener(); this.asyncListener = new RequestAsyncListener();
// Tomcat expects ReadListener registration on initial thread // Tomcat expects ReadListener registration on initial thread
ServletInputStream inputStream = request.getInputStream(); this.inputStream = request.getInputStream();
this.bodyPublisher = new RequestBodyPublisher(inputStream); this.bodyPublisher = new RequestBodyPublisher(this.inputStream);
this.bodyPublisher.registerReadListener(); this.bodyPublisher.registerReadListener();
} }
@ -231,6 +234,13 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest {
return this.asyncListener; return this.asyncListener;
} }
/**
* Return the {@link ServletInputStream} for the current response.
*/
protected final ServletInputStream getInputStream() {
return this.inputStream;
}
/** /**
* Read from the request body InputStream and return a DataBuffer. * Read from the request body InputStream and return a DataBuffer.
* Invoked only when {@link ServletInputStream#isReady()} returns "true". * Invoked only when {@link ServletInputStream#isReady()} returns "true".
@ -239,7 +249,7 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest {
* or {@link #EOF_BUFFER} if the input stream returned -1. * or {@link #EOF_BUFFER} if the input stream returned -1.
*/ */
DataBuffer readFromInputStream() throws IOException { DataBuffer readFromInputStream() throws IOException {
int read = this.request.getInputStream().read(this.buffer); int read = this.inputStream.read(this.buffer);
logBytesRead(read); logBytesRead(read);
if (read > 0) { if (read > 0) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2021 the original author or authors. * Copyright 2002-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -44,6 +44,7 @@ import org.springframework.util.Assert;
* Adapt {@link ServerHttpResponse} to the Servlet {@link HttpServletResponse}. * Adapt {@link ServerHttpResponse} to the Servlet {@link HttpServletResponse}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 5.0 * @since 5.0
*/ */
class ServletServerHttpResponse extends AbstractListenerServerHttpResponse { class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
@ -185,6 +186,13 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
return processor; return processor;
} }
/**
* Return the {@link ServletOutputStream} for the current response.
*/
protected final ServletOutputStream getOutputStream() {
return this.outputStream;
}
/** /**
* Write the DataBuffer to the response body OutputStream. * Write the DataBuffer to the response body OutputStream.
* Invoked only when {@link ServletOutputStream#isReady()} returns "true" * Invoked only when {@link ServletOutputStream#isReady()} returns "true"

View File

@ -23,9 +23,6 @@ import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -57,7 +54,6 @@ import org.springframework.util.ReflectionUtils;
*/ */
public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter {
public TomcatHttpHandlerAdapter(HttpHandler httpHandler) { public TomcatHttpHandlerAdapter(HttpHandler httpHandler) {
super(httpHandler); super(httpHandler);
} }
@ -130,11 +126,11 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter {
@Override @Override
protected DataBuffer readFromInputStream() throws IOException { protected DataBuffer readFromInputStream() throws IOException {
ServletInputStream inputStream = ((ServletRequest) getNativeRequest()).getInputStream(); if (!(getInputStream() instanceof CoyoteInputStream coyoteInputStream)) {
if (!(inputStream instanceof CoyoteInputStream coyoteInputStream)) {
// It's possible InputStream can be wrapped, preventing use of CoyoteInputStream // It's possible InputStream can be wrapped, preventing use of CoyoteInputStream
return super.readFromInputStream(); return super.readFromInputStream();
} }
ByteBuffer byteBuffer = this.factory.isDirect() ? ByteBuffer byteBuffer = this.factory.isDirect() ?
ByteBuffer.allocateDirect(this.bufferSize) : ByteBuffer.allocateDirect(this.bufferSize) :
ByteBuffer.allocate(this.bufferSize); ByteBuffer.allocate(this.bufferSize);
@ -198,6 +194,7 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter {
@Override @Override
protected void applyHeaders() { protected void applyHeaders() {
HttpServletResponse response = getNativeResponse(); HttpServletResponse response = getNativeResponse();
MediaType contentType = null; MediaType contentType = null;
try { try {
contentType = getHeaders().getContentType(); contentType = getHeaders().getContentType();
@ -210,10 +207,12 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter {
response.setContentType(contentType.toString()); response.setContentType(contentType.toString());
} }
getHeaders().remove(HttpHeaders.CONTENT_TYPE); getHeaders().remove(HttpHeaders.CONTENT_TYPE);
Charset charset = (contentType != null ? contentType.getCharset() : null); Charset charset = (contentType != null ? contentType.getCharset() : null);
if (response.getCharacterEncoding() == null && charset != null) { if (response.getCharacterEncoding() == null && charset != null) {
response.setCharacterEncoding(charset.name()); response.setCharacterEncoding(charset.name());
} }
long contentLength = getHeaders().getContentLength(); long contentLength = getHeaders().getContentLength();
if (contentLength != -1) { if (contentLength != -1) {
response.setContentLengthLong(contentLength); response.setContentLengthLong(contentLength);
@ -223,10 +222,13 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter {
@Override @Override
protected int writeToOutputStream(DataBuffer dataBuffer) throws IOException { protected int writeToOutputStream(DataBuffer dataBuffer) throws IOException {
if (!(getOutputStream() instanceof CoyoteOutputStream coyoteOutputStream)) {
return super.writeToOutputStream(dataBuffer);
}
ByteBuffer input = dataBuffer.toByteBuffer(); ByteBuffer input = dataBuffer.toByteBuffer();
int len = input.remaining(); int len = input.remaining();
ServletResponse response = getNativeResponse(); coyoteOutputStream.write(input);
((CoyoteOutputStream) response.getOutputStream()).write(input);
return len; return len;
} }
} }

View File

@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream;
import java.net.URI; import java.net.URI;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.Iterator;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.HttpClient;
@ -171,9 +172,9 @@ public class JettyXhrTransport extends AbstractXhrTransport implements Lifecycle
private static HttpHeaders toHttpHeaders(HttpFields httpFields) { private static HttpHeaders toHttpHeaders(HttpFields httpFields) {
HttpHeaders responseHeaders = new HttpHeaders(); HttpHeaders responseHeaders = new HttpHeaders();
Enumeration<String> names = httpFields.getFieldNames(); Iterator<String> names = httpFields.getFieldNamesCollection().iterator();
while (names.hasMoreElements()) { while (names.hasNext()) {
String name = names.nextElement(); String name = names.next();
Enumeration<String> values = httpFields.getValues(name); Enumeration<String> values = httpFields.getValues(name);
while (values.hasMoreElements()) { while (values.hasMoreElements()) {
String value = values.nextElement(); String value = values.nextElement();