diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 053d9c6060b..4f04cfc6b4f 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -37,6 +37,12 @@ dependencies { optional("org.eclipse.jetty:jetty-servlet") { 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.apache.httpcomponents.client5:httpclient5') optional('org.apache.httpcomponents.core5:httpcore5-reactive') diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyHeadersAdapter.java index 77ec2978cf3..e874cac13da 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyHeadersAdapter.java @@ -18,7 +18,6 @@ package org.springframework.http.client.reactive; import java.util.AbstractSet; import java.util.Collection; -import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -186,7 +185,6 @@ class JettyHeadersAdapter implements MultiValueMap { public Iterator>> iterator() { return new EntryIterator(); } - @Override public int size() { return headers.size(); @@ -203,16 +201,16 @@ class JettyHeadersAdapter implements MultiValueMap { private class EntryIterator implements Iterator>> { - private final Enumeration names = headers.getFieldNames(); + private final Iterator names = headers.getFieldNamesCollection().iterator(); @Override public boolean hasNext() { - return this.names.hasMoreElements(); + return this.names.hasNext(); } @Override public Entry> next() { - return new HeaderEntry(this.names.nextElement()); + return new HeaderEntry(this.names.next()); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHeadersAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHeadersAdapter.java index 99e6e30faf0..63447a515bc 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHeadersAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHeadersAdapter.java @@ -18,7 +18,6 @@ package org.springframework.http.server.reactive; import java.util.AbstractSet; import java.util.Collection; -import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -46,8 +45,8 @@ class JettyHeadersAdapter implements MultiValueMap { private final HttpFields.Mutable headers; - JettyHeadersAdapter(HttpFields.Mutable headers) { - this.headers = headers; + JettyHeadersAdapter(HttpFields headers) { + this.headers = (headers instanceof HttpFields.Mutable mutable ? mutable : HttpFields.build(headers)); } @@ -170,7 +169,6 @@ class JettyHeadersAdapter implements MultiValueMap { public Iterator>> iterator() { return new EntryIterator(); } - @Override public int size() { return headers.size(); @@ -187,16 +185,16 @@ class JettyHeadersAdapter implements MultiValueMap { private class EntryIterator implements Iterator>> { - private final Enumeration names = headers.getFieldNames(); + private final Iterator names = headers.getFieldNamesCollection().iterator(); @Override public boolean hasNext() { - return this.names.hasMoreElements(); + return this.names.hasNext(); } @Override public Entry> next() { - return new HeaderEntry(this.names.nextElement()); + return new HeaderEntry(this.names.next()); } } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHttpHandlerAdapter.java index db6d2093cd5..dc1e4f877a4 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/JettyHttpHandlerAdapter.java @@ -17,12 +17,13 @@ package org.springframework.http.server.reactive; import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Method; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import jakarta.servlet.AsyncContext; -import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequestWrapper; 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.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; +import org.springframework.util.ReflectionUtils; /** * {@link ServletHttpHandlerAdapter} extension that uses Jetty APIs for writing @@ -50,6 +54,23 @@ import org.springframework.util.MultiValueMap; */ 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) { super(httpHandler); } @@ -84,7 +105,13 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter { private static MultiValueMap createHeaders(HttpServletRequest 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); } @@ -115,7 +142,13 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter { private static HttpHeaders createHeaders(HttpServletResponse 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)); } @@ -135,16 +168,32 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter { @Override protected int writeToOutputStream(DataBuffer dataBuffer) throws IOException { - ByteBuffer input = dataBuffer.toByteBuffer(); - int len = input.remaining(); - ServletResponse response = getNativeResponse(); - ((HttpOutput) response.getOutputStream()).write(input); - return len; + OutputStream output = getOutputStream(); + if (jetty11Present) { + if (output instanceof HttpOutput httpOutput) { + ByteBuffer input = dataBuffer.toByteBuffer(); + 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 protected void applyHeaders() { HttpServletResponse response = getNativeResponse(); + MediaType contentType = null; try { contentType = getHeaders().getContentType(); @@ -156,10 +205,12 @@ public class JettyHttpHandlerAdapter extends ServletHttpHandlerAdapter { if (response.getContentType() == null && contentType != null) { response.setContentType(contentType.toString()); } + Charset charset = (contentType != null ? contentType.getCharset() : null); if (response.getCharacterEncoding() == null && charset != null) { response.setCharacterEncoding(charset.name()); } + long contentLength = getHeaders().getContentLength(); if (contentLength != -1) { response.setContentLengthLong(contentLength); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index e010c1bdad4..bf62a8d676d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -56,6 +56,7 @@ import org.springframework.util.StringUtils; * Adapt {@link ServerHttpRequest} to the Servlet {@link HttpServletRequest}. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 5.0 */ class ServletServerHttpRequest extends AbstractServerHttpRequest { @@ -65,6 +66,8 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { private final HttpServletRequest request; + private final ServletInputStream inputStream; + private final RequestBodyPublisher bodyPublisher; private final Object cookieLock = new Object(); @@ -99,8 +102,8 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { this.asyncListener = new RequestAsyncListener(); // Tomcat expects ReadListener registration on initial thread - ServletInputStream inputStream = request.getInputStream(); - this.bodyPublisher = new RequestBodyPublisher(inputStream); + this.inputStream = request.getInputStream(); + this.bodyPublisher = new RequestBodyPublisher(this.inputStream); this.bodyPublisher.registerReadListener(); } @@ -231,6 +234,13 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { 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. * 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. */ DataBuffer readFromInputStream() throws IOException { - int read = this.request.getInputStream().read(this.buffer); + int read = this.inputStream.read(this.buffer); logBytesRead(read); if (read > 0) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java index 4ca820990e7..032e2b9efd4 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpResponse.java @@ -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"); * 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}. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 5.0 */ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse { @@ -185,6 +186,13 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse { 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. * Invoked only when {@link ServletOutputStream#isReady()} returns "true" diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java index b4ccef6f03b..bbc9528b569 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java @@ -23,9 +23,6 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; 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.HttpServletRequestWrapper; import jakarta.servlet.http.HttpServletResponse; @@ -57,7 +54,6 @@ import org.springframework.util.ReflectionUtils; */ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { - public TomcatHttpHandlerAdapter(HttpHandler httpHandler) { super(httpHandler); } @@ -130,11 +126,11 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { @Override protected DataBuffer readFromInputStream() throws IOException { - ServletInputStream inputStream = ((ServletRequest) getNativeRequest()).getInputStream(); - if (!(inputStream instanceof CoyoteInputStream coyoteInputStream)) { + if (!(getInputStream() instanceof CoyoteInputStream coyoteInputStream)) { // It's possible InputStream can be wrapped, preventing use of CoyoteInputStream return super.readFromInputStream(); } + ByteBuffer byteBuffer = this.factory.isDirect() ? ByteBuffer.allocateDirect(this.bufferSize) : ByteBuffer.allocate(this.bufferSize); @@ -198,6 +194,7 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { @Override protected void applyHeaders() { HttpServletResponse response = getNativeResponse(); + MediaType contentType = null; try { contentType = getHeaders().getContentType(); @@ -210,10 +207,12 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { response.setContentType(contentType.toString()); } getHeaders().remove(HttpHeaders.CONTENT_TYPE); + Charset charset = (contentType != null ? contentType.getCharset() : null); if (response.getCharacterEncoding() == null && charset != null) { response.setCharacterEncoding(charset.name()); } + long contentLength = getHeaders().getContentLength(); if (contentLength != -1) { response.setContentLengthLong(contentLength); @@ -223,10 +222,13 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { @Override protected int writeToOutputStream(DataBuffer dataBuffer) throws IOException { + if (!(getOutputStream() instanceof CoyoteOutputStream coyoteOutputStream)) { + return super.writeToOutputStream(dataBuffer); + } + ByteBuffer input = dataBuffer.toByteBuffer(); int len = input.remaining(); - ServletResponse response = getNativeResponse(); - ((CoyoteOutputStream) response.getOutputStream()).write(input); + coyoteOutputStream.write(input); return len; } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java index 8e95e6550a9..d6a91be662d 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/client/JettyXhrTransport.java @@ -20,6 +20,7 @@ import java.io.ByteArrayOutputStream; import java.net.URI; import java.nio.ByteBuffer; import java.util.Enumeration; +import java.util.Iterator; import java.util.concurrent.CompletableFuture; import org.eclipse.jetty.client.HttpClient; @@ -171,9 +172,9 @@ public class JettyXhrTransport extends AbstractXhrTransport implements Lifecycle private static HttpHeaders toHttpHeaders(HttpFields httpFields) { HttpHeaders responseHeaders = new HttpHeaders(); - Enumeration names = httpFields.getFieldNames(); - while (names.hasMoreElements()) { - String name = names.nextElement(); + Iterator names = httpFields.getFieldNamesCollection().iterator(); + while (names.hasNext()) { + String name = names.next(); Enumeration values = httpFields.getValues(name); while (values.hasMoreElements()) { String value = values.nextElement();