Skip to content

Commit ab6b5f9

Browse files
Support Virtual Threads in Jetty & Tomcat (#701)
Allows enabling virtual thread support for Jetty and Tomcat. Undertow has issues with Virtual threads so not implemented. See spring-projects/spring-boot#39812 Co-authored-by: Sergio del Amo <[email protected]> --------- Co-authored-by: Sergio del Amo <[email protected]>
1 parent 9135e6e commit ab6b5f9

File tree

7 files changed

+209
-48
lines changed

7 files changed

+209
-48
lines changed

http-server-jetty/src/main/java/io/micronaut/servlet/jetty/JettyFactory.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,20 @@
2020
import io.micronaut.context.annotation.Primary;
2121
import io.micronaut.context.env.Environment;
2222
import io.micronaut.context.exceptions.ConfigurationException;
23+
import io.micronaut.core.annotation.NonNull;
2324
import io.micronaut.core.io.ResourceResolver;
2425
import io.micronaut.core.io.socket.SocketUtils;
2526
import io.micronaut.http.ssl.ClientAuthentication;
2627
import io.micronaut.http.ssl.SslConfiguration;
28+
import io.micronaut.inject.qualifiers.Qualifiers;
29+
import io.micronaut.scheduling.LoomSupport;
30+
import io.micronaut.scheduling.TaskExecutors;
2731
import io.micronaut.servlet.engine.DefaultMicronautServlet;
2832
import io.micronaut.servlet.engine.MicronautServletConfiguration;
2933
import io.micronaut.servlet.engine.server.ServletServerFactory;
3034
import io.micronaut.servlet.engine.server.ServletStaticResourceConfiguration;
3135
import jakarta.inject.Singleton;
36+
import java.util.concurrent.ExecutorService;
3237
import org.eclipse.jetty.http.HttpVersion;
3338
import org.eclipse.jetty.server.HttpConfiguration;
3439
import org.eclipse.jetty.server.HttpConnectionFactory;
@@ -43,6 +48,7 @@
4348
import org.eclipse.jetty.util.resource.Resource;
4449
import org.eclipse.jetty.util.resource.ResourceCollection;
4550
import org.eclipse.jetty.util.ssl.SslContextFactory;
51+
import org.eclipse.jetty.util.thread.QueuedThreadPool;
4652
import org.slf4j.Logger;
4753
import org.slf4j.LoggerFactory;
4854

@@ -111,7 +117,7 @@ protected Server jettyServer(
111117
final Integer port = getConfiguredPort();
112118
String contextPath = getContextPath();
113119

114-
Server server = new Server();
120+
Server server = newServer(applicationContext, configuration);
115121

116122
final ServletContextHandler contextHandler = new ServletContextHandler(server, contextPath, false, false);
117123
final ServletHolder servletHolder = new ServletHolder(new DefaultMicronautServlet(applicationContext));
@@ -202,11 +208,31 @@ protected Server jettyServer(
202208
return server;
203209
}
204210

211+
/**
212+
* Create a new server instance.
213+
* @param applicationContext The application context
214+
* @param configuration The configuration
215+
* @return The server
216+
*/
217+
protected @NonNull Server newServer(@NonNull ApplicationContext applicationContext, @NonNull MicronautServletConfiguration configuration) {
218+
Server server;
219+
if (configuration.isEnableVirtualThreads() && LoomSupport.isSupported()) {
220+
QueuedThreadPool threadPool = new QueuedThreadPool();
221+
threadPool.setVirtualThreadsExecutor(
222+
applicationContext.getBean(ExecutorService.class, Qualifiers.byName(TaskExecutors.BLOCKING))
223+
);
224+
server = new Server(threadPool);
225+
} else {
226+
server = new Server();
227+
}
228+
return server;
229+
}
230+
205231
/**
206232
* For each static resource configuration, create a {@link ContextHandler} that serves the static resources.
207233
*
208-
* @param config
209-
* @return
234+
* @param config The static resource configuration
235+
* @return the context handler
210236
*/
211237
private ContextHandler toHandler(ServletStaticResourceConfiguration config) {
212238
Resource[] resourceArray = config.getPaths().stream()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.micronaut.servlet.jetty
2+
3+
import io.micronaut.test.extensions.spock.annotation.MicronautTest
4+
import jakarta.inject.Inject
5+
import org.eclipse.jetty.server.Server
6+
import org.eclipse.jetty.util.thread.QueuedThreadPool
7+
import spock.lang.Requires
8+
import spock.lang.Specification
9+
10+
@MicronautTest
11+
@Requires({ jvm.java21 })
12+
class JettyVirtualThreadSpec extends Specification {
13+
@Inject Server server
14+
15+
void "test virtual thread enabled on JDK 21+"() {
16+
expect:
17+
server.threadPool instanceof QueuedThreadPool
18+
server.threadPool.virtualThreadsExecutor != null
19+
20+
}
21+
}

http-server-tomcat/src/main/java/io/micronaut/servlet/tomcat/TomcatFactory.java

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
package io.micronaut.servlet.tomcat;
1717

18+
import io.micronaut.context.annotation.Requires;
19+
import io.micronaut.core.annotation.Nullable;
20+
import io.micronaut.core.util.StringUtils;
21+
import jakarta.inject.Named;
1822
import java.io.File;
1923
import java.util.List;
2024

@@ -48,6 +52,7 @@
4852
@Factory
4953
public class TomcatFactory extends ServletServerFactory {
5054

55+
private static final String HTTPS = "HTTPS";
5156
private static final Logger LOG = LoggerFactory.getLogger(TomcatFactory.class);
5257

5358
/**
@@ -77,12 +82,16 @@ public TomcatConfiguration getServerConfiguration() {
7782
* The Tomcat server bean.
7883
*
7984
* @param connector The connector
85+
* @param httpsConnector The HTTPS connector
8086
* @param configuration The servlet configuration
8187
* @return The Tomcat server
8288
*/
8389
@Singleton
8490
@Primary
85-
protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration configuration) {
91+
protected Tomcat tomcatServer(
92+
Connector connector,
93+
@Named(HTTPS) @Nullable Connector httpsConnector,
94+
MicronautServletConfiguration configuration) {
8695
configuration.setAsyncFileServingEnabled(false);
8796
Tomcat tomcat = new Tomcat();
8897
tomcat.setHostname(getConfiguredHost());
@@ -118,56 +127,15 @@ protected Tomcat tomcatServer(Connector connector, MicronautServletConfiguration
118127
configuration.getMultipartConfigElement()
119128
.ifPresent(servlet::setMultipartConfigElement);
120129

121-
SslConfiguration sslConfiguration = getSslConfiguration();
122-
if (sslConfiguration.isEnabled()) {
123-
String protocol = sslConfiguration.getProtocol().orElse("TLS");
124-
int sslPort = sslConfiguration.getPort();
125-
if (sslPort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) {
126-
sslPort = 0;
127-
}
128-
Connector httpsConnector = new Connector();
129-
SSLHostConfig sslHostConfig = new SSLHostConfig();
130-
SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.UNDEFINED);
131-
sslHostConfig.addCertificate(certificate);
132-
httpsConnector.addSslHostConfig(sslHostConfig);
133-
httpsConnector.setPort(sslPort);
134-
httpsConnector.setSecure(true);
135-
httpsConnector.setScheme("https");
136-
httpsConnector.setProperty("clientAuth", "false");
137-
httpsConnector.setProperty("sslProtocol", protocol);
138-
httpsConnector.setProperty("SSLEnabled", "true");
139-
sslConfiguration.getCiphers().ifPresent(cyphers ->
140-
sslHostConfig.setCiphers(String.join(",", cyphers))
141-
);
142-
sslConfiguration.getClientAuthentication().ifPresent(ca ->
143-
httpsConnector.setProperty("clientAuth", ca == ClientAuthentication.WANT ? "want" : "true")
144-
);
145-
146-
147-
SslConfiguration.KeyStoreConfiguration keyStoreConfig = sslConfiguration.getKeyStore();
148-
keyStoreConfig.getPassword().ifPresent(certificate::setCertificateKeystorePassword);
149-
keyStoreConfig.getPath().ifPresent(certificate::setCertificateKeystoreFile);
150-
keyStoreConfig.getProvider().ifPresent(certificate::setCertificateKeystorePassword);
151-
keyStoreConfig.getType().ifPresent(certificate::setCertificateKeystoreType);
152-
153-
SslConfiguration.TrustStoreConfiguration trustStore = sslConfiguration.getTrustStore();
154-
trustStore.getPassword().ifPresent(sslHostConfig::setTruststorePassword);
155-
trustStore.getPath().ifPresent(sslHostConfig::setTruststoreFile);
156-
trustStore.getProvider().ifPresent(sslHostConfig::setTruststoreProvider);
157-
trustStore.getType().ifPresent(sslHostConfig::setTruststoreType);
158-
159-
SslConfiguration.KeyConfiguration keyConfig = sslConfiguration.getKey();
160-
keyConfig.getAlias().ifPresent(certificate::setCertificateKeyAlias);
161-
keyConfig.getPassword().ifPresent(certificate::setCertificateKeyPassword);
162-
130+
if (httpsConnector != null) {
163131
tomcat.getService().addConnector(httpsConnector);
164132
}
165133

166134
return tomcat;
167135
}
168136

169137
/**
170-
* @return Create the protocol.
138+
* @return Create the connector.
171139
*/
172140
@Singleton
173141
@Primary
@@ -177,4 +145,54 @@ protected Connector tomcatConnector() {
177145
return tomcatConnector;
178146
}
179147

180-
}
148+
/**
149+
* The HTTPS connector.
150+
* @param sslConfiguration The SSL configuration.
151+
* @return The SSL connector
152+
*/
153+
@Singleton
154+
@Named(HTTPS)
155+
@Requires(property = SslConfiguration.PREFIX + ".enabled", value = StringUtils.TRUE)
156+
protected Connector sslConnector(SslConfiguration sslConfiguration) {
157+
String protocol = sslConfiguration.getProtocol().orElse("TLS");
158+
int sslPort = sslConfiguration.getPort();
159+
if (sslPort == SslConfiguration.DEFAULT_PORT && getEnvironment().getActiveNames().contains(Environment.TEST)) {
160+
sslPort = 0;
161+
}
162+
Connector httpsConnector = new Connector();
163+
SSLHostConfig sslHostConfig = new SSLHostConfig();
164+
SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, SSLHostConfigCertificate.Type.UNDEFINED);
165+
sslHostConfig.addCertificate(certificate);
166+
httpsConnector.addSslHostConfig(sslHostConfig);
167+
httpsConnector.setPort(sslPort);
168+
httpsConnector.setSecure(true);
169+
httpsConnector.setScheme("https");
170+
httpsConnector.setProperty("clientAuth", "false");
171+
httpsConnector.setProperty("sslProtocol", protocol);
172+
httpsConnector.setProperty("SSLEnabled", "true");
173+
sslConfiguration.getCiphers().ifPresent(cyphers ->
174+
sslHostConfig.setCiphers(String.join(",", cyphers))
175+
);
176+
sslConfiguration.getClientAuthentication().ifPresent(ca ->
177+
httpsConnector.setProperty("clientAuth", ca == ClientAuthentication.WANT ? "want" : "true")
178+
);
179+
180+
181+
SslConfiguration.KeyStoreConfiguration keyStoreConfig = sslConfiguration.getKeyStore();
182+
keyStoreConfig.getPassword().ifPresent(certificate::setCertificateKeystorePassword);
183+
keyStoreConfig.getPath().ifPresent(certificate::setCertificateKeystoreFile);
184+
keyStoreConfig.getProvider().ifPresent(certificate::setCertificateKeystorePassword);
185+
keyStoreConfig.getType().ifPresent(certificate::setCertificateKeystoreType);
186+
187+
SslConfiguration.TrustStoreConfiguration trustStore = sslConfiguration.getTrustStore();
188+
trustStore.getPassword().ifPresent(sslHostConfig::setTruststorePassword);
189+
trustStore.getPath().ifPresent(sslHostConfig::setTruststoreFile);
190+
trustStore.getProvider().ifPresent(sslHostConfig::setTruststoreProvider);
191+
trustStore.getType().ifPresent(sslHostConfig::setTruststoreType);
192+
193+
SslConfiguration.KeyConfiguration keyConfig = sslConfiguration.getKey();
194+
keyConfig.getAlias().ifPresent(certificate::setCertificateKeyAlias);
195+
keyConfig.getPassword().ifPresent(certificate::setCertificateKeyPassword);
196+
return httpsConnector;
197+
}
198+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2017-2024 original authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.micronaut.servlet.tomcat;
17+
18+
import io.micronaut.context.annotation.Requires;
19+
import io.micronaut.context.event.BeanCreatedEvent;
20+
import io.micronaut.context.event.BeanCreatedEventListener;
21+
import io.micronaut.core.annotation.NonNull;
22+
import io.micronaut.servlet.http.ServletConfiguration;
23+
import jakarta.inject.Singleton;
24+
import org.apache.catalina.connector.Connector;
25+
import org.apache.coyote.ProtocolHandler;
26+
import org.apache.tomcat.util.threads.VirtualThreadExecutor;
27+
28+
/**
29+
* Enables virtual thread configuration if enabled.
30+
*/
31+
@Requires(sdk = Requires.Sdk.JAVA, version = "21")
32+
@Singleton
33+
class TomcatVirtualThreadEnabler implements BeanCreatedEventListener<Connector> {
34+
private final ServletConfiguration servletConfiguration;
35+
36+
public TomcatVirtualThreadEnabler(ServletConfiguration servletConfiguration) {
37+
this.servletConfiguration = servletConfiguration;
38+
}
39+
40+
@Override
41+
public Connector onCreated(@NonNull BeanCreatedEvent<Connector> event) {
42+
Connector connector = event.getBean();
43+
if (servletConfiguration.isEnableVirtualThreads()) {
44+
ProtocolHandler protocolHandler = connector.getProtocolHandler();
45+
protocolHandler.setExecutor(new VirtualThreadExecutor("tomcat-handler-"));
46+
}
47+
return connector;
48+
}
49+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.micronaut.servlet.tomcat
2+
3+
4+
import io.micronaut.test.extensions.spock.annotation.MicronautTest
5+
import jakarta.inject.Inject
6+
import org.apache.catalina.startup.Tomcat
7+
import org.apache.tomcat.util.threads.VirtualThreadExecutor
8+
import spock.lang.Requires
9+
import spock.lang.Specification
10+
11+
@MicronautTest
12+
@Requires({ jvm.java21 })
13+
class TomcatVirtualThreadSpec extends Specification {
14+
@Inject Tomcat server
15+
16+
void "test virtual thread enabled on JDK 21+"() {
17+
expect:
18+
server.connector.protocolHandler.executor instanceof VirtualThreadExecutor
19+
}
20+
}

servlet-core/src/main/java/io/micronaut/servlet/http/ServletConfiguration.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,16 @@ public interface ServletConfiguration {
3838
default boolean isAsyncSupported() {
3939
return true;
4040
}
41+
42+
/**
43+
* Whether to enable virtual thread support if available.
44+
*
45+
* <p>If virtual threads are not available this option does nothing.</p>
46+
*
47+
* @return True if they should be enabled
48+
* @since 4.8.0
49+
*/
50+
default boolean isEnableVirtualThreads() {
51+
return true;
52+
}
4153
}

servlet-engine/src/main/java/io/micronaut/servlet/engine/MicronautServletConfiguration.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public class MicronautServletConfiguration implements Named, ServletConfiguratio
5050
private boolean asyncFileServingEnabled = true;
5151

5252
private boolean asyncSupported = true;
53+
private boolean enableVirtualThreads = true;
5354

5455

5556
/**
@@ -105,6 +106,20 @@ public void setTestAsyncSupported(@Nullable Boolean asyncSupported) {
105106
}
106107
}
107108

109+
@Override
110+
public boolean isEnableVirtualThreads() {
111+
return this.enableVirtualThreads;
112+
}
113+
114+
/**
115+
* Whether virtual threads are enabled.
116+
* @param enableVirtualThreads True if they are enabled
117+
* @since 4.8.0
118+
*/
119+
public void setEnableVirtualThreads(boolean enableVirtualThreads) {
120+
this.enableVirtualThreads = enableVirtualThreads;
121+
}
122+
108123
/**
109124
* @return The servlet mapping.
110125
*/

0 commit comments

Comments
 (0)