This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push: new d9def06167 Reduce minimum Java version to 17 d9def06167 is described below commit d9def06167fe566674e00ebdbdce8591a228cce2 Author: Mark Thomas <ma...@apache.org> AuthorDate: Fri Jan 19 12:15:20 2024 +0000 Reduce minimum Java version to 17 Uses some API that is deprecated in Java 21 but not 17. JreCompat has not been used to avoid the deprecated API if running on Java 21+. --- build.xml | 8 +- conf/web.xml | 4 +- .../authenticator/SpnegoAuthenticator.java | 65 +++++++++--- .../core/ThreadLocalLeakPreventionListener.java | 1 - .../catalina/valves/StuckThreadDetectionValve.java | 10 +- .../catalina/webresources/CachedResource.java | 3 - java/org/apache/coyote/Request.java | 4 +- java/org/apache/jasper/EmbeddedServletOptions.java | 4 +- java/org/apache/jasper/JspC.java | 4 +- java/org/apache/jasper/compiler/JDTCompiler.java | 14 +-- .../tomcat/dbcp/dbcp2/DelegatingStatement.java | 2 +- java/org/apache/tomcat/util/Diagnostics.java | 1 - .../org/apache/tomcat/util/compat/Jre19Compat.java | 84 +++++++++++++++ .../org/apache/tomcat/util/compat/Jre21Compat.java | 82 ++++++++++++++ .../org/apache/tomcat/util/compat/Jre22Compat.java | 2 +- java/org/apache/tomcat/util/compat/JreCompat.java | 118 ++++++++++++++++++++- .../apache/tomcat/util/net/AbstractEndpoint.java | 1 - .../tomcat/util/threads/VirtualThreadExecutor.java | 11 +- .../TestAsyncContextImplListenerOnComplete.java | 31 +++--- test/org/apache/juli/TestThreadNameCache.java | 2 +- webapps/docs/changelog.xml | 12 +++ webapps/docs/jasper-howto.xml | 4 +- webapps/docs/tomcat-docs.xsl | 4 +- 23 files changed, 398 insertions(+), 73 deletions(-) diff --git a/build.xml b/build.xml index 22379895f9..76d1feb07c 100644 --- a/build.xml +++ b/build.xml @@ -104,11 +104,11 @@ <!-- build output directory for generated reflectionless code --> <property name="tomcat.xreflect" value="${tomcat.output}/xreflect"/> - <!-- Jakarta EE 11 platform requires Java 21+ --> + <!-- Jakarta EE 11 platform requires Java 17+ --> <!-- Keep in sync with webapps/docs/tomcat-docs.xsl --> - <property name="compile.release" value="21"/> - <property name="min.java.version" value="21"/> - <property name="build.java.version" value="21"/> + <property name="compile.release" value="17"/> + <property name="min.java.version" value="17"/> + <property name="build.java.version" value="17"/> <property name="release.java.version" value="22"/> <!-- Check Java Build Version --> diff --git a/conf/web.xml b/conf/web.xml index 0a1261e3b3..788f411d48 100644 --- a/conf/web.xml +++ b/conf/web.xml @@ -160,9 +160,9 @@ <!-- pages. See the jasper documentation for more --> <!-- information. --> <!-- --> - <!-- compilerSourceVM Compiler source VM. [21] --> + <!-- compilerSourceVM Compiler source VM. [17] --> <!-- --> - <!-- compilerTargetVM Compiler target VM. [21] --> + <!-- compilerTargetVM Compiler target VM. [17] --> <!-- --> <!-- development Is Jasper used in development mode? If true, --> <!-- the frequency at which JSPs are checked for --> diff --git a/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java b/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java index d76edb9ecf..6377416ea4 100644 --- a/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java +++ b/java/org/apache/catalina/authenticator/SpnegoAuthenticator.java @@ -19,8 +19,10 @@ package org.apache.catalina.authenticator; import java.io.File; import java.io.IOException; import java.security.Principal; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; import java.util.LinkedHashMap; -import java.util.concurrent.CompletionException; import java.util.regex.Pattern; import javax.security.auth.Subject; @@ -30,6 +32,7 @@ import javax.security.auth.login.LoginException; import jakarta.servlet.http.HttpServletResponse; import org.apache.catalina.LifecycleException; +import org.apache.catalina.Realm; import org.apache.catalina.connector.Request; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; @@ -205,16 +208,11 @@ public class SpnegoAuthenticator extends AuthenticatorBase { } else { credentialLifetime = GSSCredential.DEFAULT_LIFETIME; } + final PrivilegedExceptionAction<GSSCredential> action = () -> manager.createCredential(null, + credentialLifetime, new Oid("1.3.6.1.5.5.2"), GSSCredential.ACCEPT_ONLY); + gssContext = manager.createContext(Subject.doAs(subject, action)); - gssContext = manager.createContext(Subject.callAs(subject, () -> { - return manager.createCredential(null, credentialLifetime, new Oid("1.3.6.1.5.5.2"), - GSSCredential.ACCEPT_ONLY); - })); - - final GSSContext gssContextFinal = gssContext; - outToken = Subject.callAs(subject, () -> { - return gssContextFinal.acceptSecContext(decoded, 0, decoded.length); - }); + outToken = Subject.doAs(lc.getSubject(), new AcceptAction(gssContext, decoded)); if (outToken == null) { if (log.isDebugEnabled()) { @@ -226,9 +224,8 @@ public class SpnegoAuthenticator extends AuthenticatorBase { return false; } - principal = Subject.callAs(subject, () -> { - return context.getRealm().authenticate(gssContextFinal, storeDelegatedCredential); - }); + principal = Subject.doAs(subject, + new AuthenticateAction(context.getRealm(), gssContext, storeDelegatedCredential)); } catch (GSSException e) { if (log.isDebugEnabled()) { @@ -237,7 +234,7 @@ public class SpnegoAuthenticator extends AuthenticatorBase { response.setHeader(AUTH_HEADER_NAME, AUTH_HEADER_VALUE_NEGOTIATE); response.sendError(HttpServletResponse.SC_UNAUTHORIZED); return false; - } catch (CompletionException e) { + } catch (PrivilegedActionException e) { Throwable cause = e.getCause(); if (cause instanceof GSSException) { if (log.isDebugEnabled()) { @@ -294,6 +291,46 @@ public class SpnegoAuthenticator extends AuthenticatorBase { } + /** + * This class gets a gss credential via a privileged action. + */ + public static class AcceptAction implements PrivilegedExceptionAction<byte[]> { + + GSSContext gssContext; + + byte[] decoded; + + public AcceptAction(GSSContext context, byte[] decodedToken) { + this.gssContext = context; + this.decoded = decodedToken; + } + + @Override + public byte[] run() throws GSSException { + return gssContext.acceptSecContext(decoded, 0, decoded.length); + } + } + + + public static class AuthenticateAction implements PrivilegedAction<Principal> { + + private final Realm realm; + private final GSSContext gssContext; + private final boolean storeDelegatedCredential; + + public AuthenticateAction(Realm realm, GSSContext gssContext, boolean storeDelegatedCredential) { + this.realm = realm; + this.gssContext = gssContext; + this.storeDelegatedCredential = storeDelegatedCredential; + } + + @Override + public Principal run() { + return realm.authenticate(gssContext, storeDelegatedCredential); + } + } + + /** * This class implements a hack around an incompatibility between the SPNEGO implementation in Windows and the * SPNEGO implementation in Java 8 update 40 onwards. It was introduced by the change to fix this bug: diff --git a/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java b/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java index 0389c3ae02..c413317186 100644 --- a/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java +++ b/java/org/apache/catalina/core/ThreadLocalLeakPreventionListener.java @@ -119,7 +119,6 @@ public class ThreadLocalLeakPreventionListener extends FrameworkListener { } if (executor instanceof ThreadPoolExecutor) { - @SuppressWarnings("resource") ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor; threadPoolExecutor.contextStopping(); } else if (executor instanceof StandardThreadExecutor) { diff --git a/java/org/apache/catalina/valves/StuckThreadDetectionValve.java b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java index a19629fde2..a523029d75 100644 --- a/java/org/apache/catalina/valves/StuckThreadDetectionValve.java +++ b/java/org/apache/catalina/valves/StuckThreadDetectionValve.java @@ -138,7 +138,7 @@ public class StuckThreadDetectionValve extends ValveBase { String msg = sm.getString("stuckThreadDetectionValve.notifyStuckThreadDetected", monitoredThread.getThread().getName(), Long.valueOf(activeTime), monitoredThread.getStartTime(), Integer.valueOf(numStuckThreads), monitoredThread.getRequestUri(), Integer.valueOf(threshold), - String.valueOf(monitoredThread.getThread().threadId())); + String.valueOf(monitoredThread.getThread().getId())); // msg += "\n" + getStackTraceAsString(trace); Throwable th = new Throwable(); th.setStackTrace(monitoredThread.getThread().getStackTrace()); @@ -171,7 +171,7 @@ public class StuckThreadDetectionValve extends ValveBase { // GC'ing, as the reference is removed from the Map in the finally clause Thread currentThread = Thread.currentThread(); - Long key = Long.valueOf(currentThread.threadId()); + Long key = Long.valueOf(currentThread.getId()); StringBuffer requestUrl = request.getRequestURL(); if (request.getQueryString() != null) { requestUrl.append('?'); @@ -231,7 +231,7 @@ public class StuckThreadDetectionValve extends ValveBase { List<Long> idList = new ArrayList<>(); for (MonitoredThread monitoredThread : activeThreads.values()) { if (monitoredThread.isMarkedAsStuck()) { - idList.add(Long.valueOf(monitoredThread.getThread().threadId())); + idList.add(Long.valueOf(monitoredThread.getThread().getId())); } } @@ -345,7 +345,7 @@ public class StuckThreadDetectionValve extends ValveBase { String msg = sm.getString("stuckThreadDetectionValve.notifyStuckThreadInterrupted", this.getThread().getName(), Long.valueOf(getActiveTimeInMillis()), this.getStartTime(), this.getRequestUri(), Long.valueOf(interruptThreadThreshold), - String.valueOf(this.getThread().threadId())); + String.valueOf(this.getThread().getId())); Throwable th = new Throwable(); th.setStackTrace(this.getThread().getStackTrace()); log.warn(msg, th); @@ -371,7 +371,7 @@ public class StuckThreadDetectionValve extends ValveBase { CompletedStuckThread(Thread thread, long totalActiveTime) { this.threadName = thread.getName(); - this.threadId = thread.threadId(); + this.threadId = thread.getId(); this.totalActiveTime = totalActiveTime; } diff --git a/java/org/apache/catalina/webresources/CachedResource.java b/java/org/apache/catalina/webresources/CachedResource.java index 96dcf0c011..14d2b378ca 100644 --- a/java/org/apache/catalina/webresources/CachedResource.java +++ b/java/org/apache/catalina/webresources/CachedResource.java @@ -341,9 +341,6 @@ public class CachedResource implements WebResource { try { CachedResourceURLStreamHandler handler = new CachedResourceURLStreamHandler(resourceURL, root, webAppPath, usesClassLoaderResources); - // Need the custom handler for all protocols. URL.of(...) does not allow file to use a custom handler. - // Ignore the deprecation for now. - @SuppressWarnings("deprecation") URL result = new URL(null, resourceURL.toExternalForm(), handler); handler.setCacheURL(result); return result; diff --git a/java/org/apache/coyote/Request.java b/java/org/apache/coyote/Request.java index 6d2ab8b640..245251b2e3 100644 --- a/java/org/apache/coyote/Request.java +++ b/java/org/apache/coyote/Request.java @@ -738,12 +738,12 @@ public final class Request { public void setRequestThread() { Thread t = Thread.currentThread(); - threadId = t.threadId(); + threadId = t.getId(); getRequestProcessor().setWorkerThreadName(t.getName()); } public boolean isRequestThread() { - return Thread.currentThread().threadId() == threadId; + return Thread.currentThread().getId() == threadId; } // -------------------- Per-Request "notes" -------------------- diff --git a/java/org/apache/jasper/EmbeddedServletOptions.java b/java/org/apache/jasper/EmbeddedServletOptions.java index 81df0af31a..451dfbcfaf 100644 --- a/java/org/apache/jasper/EmbeddedServletOptions.java +++ b/java/org/apache/jasper/EmbeddedServletOptions.java @@ -124,12 +124,12 @@ public final class EmbeddedServletOptions implements Options { /** * Compiler target VM. */ - private String compilerTargetVM = "21"; + private String compilerTargetVM = "17"; /** * The compiler source VM. */ - private String compilerSourceVM = "21"; + private String compilerSourceVM = "17"; /** * The compiler class name. diff --git a/java/org/apache/jasper/JspC.java b/java/org/apache/jasper/JspC.java index e31918cca5..8656617bef 100644 --- a/java/org/apache/jasper/JspC.java +++ b/java/org/apache/jasper/JspC.java @@ -197,8 +197,8 @@ public class JspC extends Task implements Options { protected String compiler = null; - protected String compilerTargetVM = "21"; - protected String compilerSourceVM = "21"; + protected String compilerTargetVM = "17"; + protected String compilerSourceVM = "17"; protected boolean classDebugInfo = true; diff --git a/java/org/apache/jasper/compiler/JDTCompiler.java b/java/org/apache/jasper/compiler/JDTCompiler.java index 727f61393a..16bf7060f7 100644 --- a/java/org/apache/jasper/compiler/JDTCompiler.java +++ b/java/org/apache/jasper/compiler/JDTCompiler.java @@ -328,11 +328,11 @@ public class JDTCompiler extends org.apache.jasper.compiler.Compiler { settings.put(CompilerOptions.OPTION_Source, "22"); } else { log.warn(Localizer.getMessage("jsp.warning.unknown.sourceVM", opt)); - settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_21); + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_17); } } else { - // Default to 21 - settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_21); + // Default to 17 + settings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_17); } // Target JVM @@ -407,12 +407,12 @@ public class JDTCompiler extends org.apache.jasper.compiler.Compiler { settings.put(CompilerOptions.OPTION_Compliance, "22"); } else { log.warn(Localizer.getMessage("jsp.warning.unknown.targetVM", opt)); - settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_21); + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_17); } } else { - // Default to 21 - settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_21); - settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_21); + // Default to 17 + settings.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_17); + settings.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_17); } final IProblemFactory problemFactory = new DefaultProblemFactory(Locale.getDefault()); diff --git a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java index 9e5853894a..ff5d937fc4 100644 --- a/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java +++ b/java/org/apache/tomcat/dbcp/dbcp2/DelegatingStatement.java @@ -368,7 +368,7 @@ public class DelegatingStatement extends AbandonedTrace implements Statement { } } - @SuppressWarnings("removal") // Need Commons DBCP to address this + @SuppressWarnings("deprecation") // Need Commons DBCP to address this @Override protected void finalize() throws Throwable { // This is required because of statement pooling. The poolable diff --git a/java/org/apache/tomcat/util/Diagnostics.java b/java/org/apache/tomcat/util/Diagnostics.java index e4fb8631be..77091e5c49 100644 --- a/java/org/apache/tomcat/util/Diagnostics.java +++ b/java/org/apache/tomcat/util/Diagnostics.java @@ -487,7 +487,6 @@ public class Diagnostics { * @param requestedSm the StringManager to use * @return the formatted JVM information text */ - @SuppressWarnings("deprecation") public static String getVMInfo(StringManager requestedSm) { StringBuilder sb = new StringBuilder(); diff --git a/java/org/apache/tomcat/util/compat/Jre19Compat.java b/java/org/apache/tomcat/util/compat/Jre19Compat.java new file mode 100644 index 0000000000..60ee0c2dc1 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/Jre19Compat.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.tomcat.util.compat; + +import java.lang.reflect.Field; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class Jre19Compat extends JreCompat { + + private static final Log log = LogFactory.getLog(Jre19Compat.class); + private static final StringManager sm = StringManager.getManager(Jre19Compat.class); + + private static final boolean supported; + + static { + // Don't need any Java 19 specific classes (yet) so just test for one of + // the new ones for now. + Class<?> c1 = null; + try { + c1 = Class.forName("java.lang.WrongThreadException"); + } catch (ClassNotFoundException cnfe) { + // Must be pre-Java 19 + log.debug(sm.getString("jre19Compat.javaPre19"), cnfe); + } + + supported = (c1 != null); + } + + static boolean isSupported() { + return supported; + } + + @Override + public Object getExecutor(Thread thread) + throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + + Object result = super.getExecutor(thread); + + if (result == null) { + Object holder = null; + Object task = null; + try { + Field holderField = thread.getClass().getDeclaredField("holder"); + holderField.setAccessible(true); + holder = holderField.get(thread); + + Field taskField = holder.getClass().getDeclaredField("task"); + taskField.setAccessible(true); + task = taskField.get(holder); + } catch (NoSuchFieldException nfe) { + return null; + } + + if (task!= null && task.getClass().getCanonicalName() != null && + (task.getClass().getCanonicalName().equals( + "org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") || + task.getClass().getCanonicalName().equals( + "java.util.concurrent.ThreadPoolExecutor.Worker"))) { + Field executorField = task.getClass().getDeclaredField("this$0"); + executorField.setAccessible(true); + result = executorField.get(task); + } + } + + return result; + } +} diff --git a/java/org/apache/tomcat/util/compat/Jre21Compat.java b/java/org/apache/tomcat/util/compat/Jre21Compat.java new file mode 100644 index 0000000000..d06c8519a7 --- /dev/null +++ b/java/org/apache/tomcat/util/compat/Jre21Compat.java @@ -0,0 +1,82 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.tomcat.util.compat; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; + +public class Jre21Compat extends Jre19Compat { + + private static final Log log = LogFactory.getLog(Jre21Compat.class); + private static final StringManager sm = StringManager.getManager(Jre21Compat.class); + + private static final Method nameMethod; + private static final Method startMethod; + private static final Method ofVirtualMethod; + + + static { + Class<?> c1 = null; + Method m1 = null; + Method m2 = null; + Method m3 = null; + + try { + c1 = Class.forName("java.lang.Thread$Builder"); + m1 = c1.getMethod("name", String.class, long.class); + m2 = c1.getMethod("start", Runnable.class); + m3 = Thread.class.getMethod("ofVirtual", (Class<?>[]) null); + } catch (ClassNotFoundException e) { + // Must be pre-Java 21 + log.debug(sm.getString("jre21Compat.javaPre21"), e); + } catch (ReflectiveOperationException e) { + // Should never happen + log.error(sm.getString("jre21Compat.unexpected"), e); + } + nameMethod = m1; + startMethod = m2; + ofVirtualMethod = m3; + } + + static boolean isSupported() { + return ofVirtualMethod != null; + } + + @Override + public Object createVirtualThreadBuilder(String name) { + try { + Object threadBuilder = ofVirtualMethod.invoke(null, (Object[]) null); + nameMethod.invoke(threadBuilder, name, Long.valueOf(0)); + return threadBuilder; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } + + @Override + public void threadBuilderStart(Object threadBuilder, Runnable command) { + try { + startMethod.invoke(threadBuilder, command); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new UnsupportedOperationException(e); + } + } +} diff --git a/java/org/apache/tomcat/util/compat/Jre22Compat.java b/java/org/apache/tomcat/util/compat/Jre22Compat.java index 6e5416694c..d685cd56ec 100644 --- a/java/org/apache/tomcat/util/compat/Jre22Compat.java +++ b/java/org/apache/tomcat/util/compat/Jre22Compat.java @@ -22,7 +22,7 @@ import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.res.StringManager; -public class Jre22Compat extends JreCompat { +public class Jre22Compat extends Jre21Compat { private static final Log log = LogFactory.getLog(Jre22Compat.class); private static final StringManager sm = StringManager.getManager(Jre22Compat.class); diff --git a/java/org/apache/tomcat/util/compat/JreCompat.java b/java/org/apache/tomcat/util/compat/JreCompat.java index 7273b0b233..743f76e64f 100644 --- a/java/org/apache/tomcat/util/compat/JreCompat.java +++ b/java/org/apache/tomcat/util/compat/JreCompat.java @@ -16,16 +16,23 @@ */ package org.apache.tomcat.util.compat; +import java.lang.reflect.Field; + +import org.apache.tomcat.util.res.StringManager; + /** * This is the base implementation class for JRE compatibility and provides an - * implementation based on Java 21. Sub-classes may extend this class and provide + * implementation based on Java 17. Sub-classes may extend this class and provide * alternative implementations for later JRE versions */ public class JreCompat { private static final JreCompat instance; private static final boolean graalAvailable; + private static final boolean jre19Available; + private static final boolean jre21Available; private static final boolean jre22Available; + private static final StringManager sm = StringManager.getManager(JreCompat.class); static { boolean result = false; @@ -39,13 +46,28 @@ public class JreCompat { } graalAvailable = result || System.getProperty("org.graalvm.nativeimage.imagecode") != null; - // This is Tomcat 11.0.x with a minimum Java version of Java 21. + // This is Tomcat 11.0.x with a minimum Java version of Java 17. + // Look for the highest supported JVM first if (Jre22Compat.isSupported()) { instance = new Jre22Compat(); jre22Available = true; + jre21Available = true; + jre19Available = true; + } else if (Jre21Compat.isSupported()) { + instance = new Jre21Compat(); + jre22Available = false; + jre21Available = true; + jre19Available = true; + } else if (Jre19Compat.isSupported()) { + instance = new Jre19Compat(); + jre22Available = false; + jre21Available = false; + jre19Available = true; } else { instance = new JreCompat(); jre22Available = false; + jre21Available = false; + jre19Available = false; } } @@ -60,9 +82,101 @@ public class JreCompat { } + public static boolean isJre19Available() { + return jre19Available; + } + + + public static boolean isJre21Available() { + return jre21Available; + } + + public static boolean isJre22Available() { return jre22Available; } + // Java 17 implementations of Java 19 methods + + /** + * Obtains the executor, if any, used to create the provided thread. + * + * @param thread The thread to examine + * + * @return The executor, if any, that created the provided thread + * + * @throws NoSuchFieldException + * If a field used via reflection to obtain the executor cannot + * be found + * @throws SecurityException + * If a security exception occurs while trying to identify the + * executor + * @throws IllegalArgumentException + * If the instance object does not match the class of the field + * when obtaining a field value via reflection + * @throws IllegalAccessException + * If a field is not accessible due to access restrictions + */ + public Object getExecutor(Thread thread) + throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { + + Object result = null; + + // Runnable wrapped by Thread + // "target" in Sun/Oracle JDK + // "runnable" in IBM JDK + // "action" in Apache Harmony + Object target = null; + for (String fieldName : new String[] { "target", "runnable", "action" }) { + try { + Field targetField = thread.getClass().getDeclaredField(fieldName); + targetField.setAccessible(true); + target = targetField.get(thread); + break; + } catch (NoSuchFieldException nfe) { + continue; + } + } + + // "java.util.concurrent" code is in public domain, + // so all implementations are similar including our + // internal fork. + if (target != null && target.getClass().getCanonicalName() != null && + (target.getClass().getCanonicalName().equals( + "org.apache.tomcat.util.threads.ThreadPoolExecutor.Worker") || + target.getClass().getCanonicalName().equals( + "java.util.concurrent.ThreadPoolExecutor.Worker"))) { + Field executorField = target.getClass().getDeclaredField("this$0"); + executorField.setAccessible(true); + result = executorField.get(target); + } + + return result; + } + + + // Java 17 implementations of Java 21 methods + + /** + * Create a thread builder for virtual threads using the given name to name the threads. + * + * @param name The base name for the threads + * + * @return The thread buidler for virtual threads + */ + public Object createVirtualThreadBuilder(String name) { + throw new UnsupportedOperationException(sm.getString("jreCompat.noVirtualThreads")); + } + + + /** + * Create a thread with the given thread builder and use it to execute the given runnable. + * + * @param threadBuilder The thread builder to use to create a thread + * @param command The command to run + */ + public void threadBuilderStart(Object threadBuilder, Runnable command) { + throw new UnsupportedOperationException(sm.getString("jreCompat.noVirtualThreads")); + } } diff --git a/java/org/apache/tomcat/util/net/AbstractEndpoint.java b/java/org/apache/tomcat/util/net/AbstractEndpoint.java index fb6080eaeb..4c9942a3b4 100644 --- a/java/org/apache/tomcat/util/net/AbstractEndpoint.java +++ b/java/org/apache/tomcat/util/net/AbstractEndpoint.java @@ -1210,7 +1210,6 @@ public abstract class AbstractEndpoint<S,U> { this.executor = null; if (executor instanceof ThreadPoolExecutor) { //this is our internal one, so we need to shut it down - @SuppressWarnings("resource") ThreadPoolExecutor tpe = (ThreadPoolExecutor) executor; tpe.shutdownNow(); long timeout = getExecutorTerminationTimeoutMillis(); diff --git a/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java b/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java index 461d16e05f..54453a9a66 100644 --- a/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java +++ b/java/org/apache/tomcat/util/threads/VirtualThreadExecutor.java @@ -23,6 +23,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import org.apache.tomcat.util.compat.JreCompat; import org.apache.tomcat.util.res.StringManager; /** @@ -34,10 +35,12 @@ public class VirtualThreadExecutor extends AbstractExecutorService { private CountDownLatch shutdown = new CountDownLatch(1); - private Thread.Builder threadBuilder; + private final JreCompat jreCompat = JreCompat.getInstance(); + + private Object threadBuilder; public VirtualThreadExecutor(String namePrefix) { - threadBuilder = Thread.ofVirtual().name(namePrefix, 0); + threadBuilder = jreCompat.createVirtualThreadBuilder(namePrefix); } @Override @@ -46,7 +49,7 @@ public class VirtualThreadExecutor extends AbstractExecutorService { throw new RejectedExecutionException( sm.getString("virtualThreadExecutor.taskRejected", command.toString(), this.toString())); } - threadBuilder.start(command); + jreCompat.threadBuilderStart(threadBuilder, command); } @Override @@ -92,4 +95,4 @@ public class VirtualThreadExecutor extends AbstractExecutorService { public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { return shutdown.await(timeout, unit); } -} \ No newline at end of file +} diff --git a/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java b/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java index 23b6d6518a..dfdc33b9d0 100644 --- a/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java +++ b/test/org/apache/catalina/core/TestAsyncContextImplListenerOnComplete.java @@ -38,6 +38,7 @@ import org.junit.Test; import org.apache.catalina.Context; import org.apache.catalina.Wrapper; +import org.apache.catalina.startup.SimpleHttpClient; import org.apache.catalina.startup.Tomcat; import org.apache.catalina.startup.TomcatBaseTest; @@ -79,11 +80,9 @@ public class TestAsyncContextImplListenerOnComplete extends TomcatBaseTest { socket.connect(new InetSocketAddress("localhost", port)); try (var writer = new OutputStreamWriter(socket.getOutputStream())) { - writer.write(""" - GET /repro - Accept: text/event-stream - - """); + writer.write("GET /repro" + SimpleHttpClient.CRLF + + "Accept: text/event-stream" + SimpleHttpClient.CRLF + + SimpleHttpClient.CRLF); writer.flush(); } Thread.sleep(1_000); @@ -146,18 +145,18 @@ public class TestAsyncContextImplListenerOnComplete extends TomcatBaseTest { private final Set<AsyncContext> contexts = new HashSet<>(); private EventSource() { - Thread.ofVirtual() - .name(getClass().getSimpleName()) - .start(() -> { - while (true) { - try { - Thread.sleep(2000); - send("PING"); - } catch (InterruptedException e) { - System.out.println("Failed to sleep: " + e); - } + Runnable r = () -> { + while (true) { + try { + Thread.sleep(2000); + send("PING"); + } catch (InterruptedException e) { + System.out.println("Failed to sleep: " + e); } - }); + } + }; + Thread t = new Thread(r); + t.start(); } public void send(String message) { diff --git a/test/org/apache/juli/TestThreadNameCache.java b/test/org/apache/juli/TestThreadNameCache.java index e67ea01f14..65c7a6e6b2 100644 --- a/test/org/apache/juli/TestThreadNameCache.java +++ b/test/org/apache/juli/TestThreadNameCache.java @@ -38,7 +38,7 @@ public class TestThreadNameCache { @Override public void run() { setName(THREAD_NAME); - threadId = Integer.valueOf((int) threadId()); + threadId = Integer.valueOf((int) getId()); threadIdLatch.countDown(); try { cacheLatch.await(); diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 32de8976c1..5f3a32e564 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -105,6 +105,13 @@ issues do not "pop up" wrt. others). --> <section name="Tomcat 11.0.0-M18 (markt)" rtext="in development"> + <subsection name="General"> + <changelog> + <update> + Reduce the minimum supported Java version to Java 17. (markt) + </update> + </changelog> + </subsection> <subsection name="Jasper"> <changelog> <add> @@ -114,6 +121,11 @@ for the Jakarta EL 6.0 API. The property support has also been refined for greater consistency. (markt) </add> + <update> + The defaults for <code>compilerSourceVM</code> and + <code>compilerTargetVM</code> have been updated to 17 to align with Java + 17 being the minimum Java version required for Tomcat 11. (markt) + </update> </changelog> </subsection> </section> diff --git a/webapps/docs/jasper-howto.xml b/webapps/docs/jasper-howto.xml index 31b49ce50a..ac73faafea 100644 --- a/webapps/docs/jasper-howto.xml +++ b/webapps/docs/jasper-howto.xml @@ -106,10 +106,10 @@ set then <code>setenv.[sh|bat]</code> should be used to add to the <code>CLASSPATH</code> environment variable.</li> <li><strong>compilerSourceVM</strong> - What JDK version are the source files -compatible with? (Default value: <code>21</code>)</li> +compatible with? (Default value: <code>17</code>)</li> <li><strong>compilerTargetVM</strong> - What JDK version are the generated files -compatible with? (Default value: <code>21</code>)</li> +compatible with? (Default value: <code>17</code>)</li> <li><strong>development</strong> - Is Jasper used in development mode? If true, the frequency at which JSPs are checked for modification may be specified via diff --git a/webapps/docs/tomcat-docs.xsl b/webapps/docs/tomcat-docs.xsl index d152384e45..54abf79897 100644 --- a/webapps/docs/tomcat-docs.xsl +++ b/webapps/docs/tomcat-docs.xsl @@ -37,8 +37,8 @@ <xsl:param name="version" select="'11.0.x'"/> <xsl:param name="majorversion" select="'11'"/> <xsl:param name="majorminorversion" select="'11.0'"/> - <xsl:param name="minjavaversion" select="'21'"/> - <xsl:param name="buildjavaversion" select="'21'"/> + <xsl:param name="minjavaversion" select="'17'"/> + <xsl:param name="buildjavaversion" select="'22'"/> <xsl:param name="antversionrequired" select="'1.10.2'"/> <xsl:param name="build-date" select="'MMM d yyyy'"/> <xsl:param name="build-date-iso-8601" select="'yyyy-MM-dd'"/> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org