Skip to content

Commit 7529489

Browse files
Try to get rid of legacy API which can break starting with java 17 (#409)
Co-authored-by: Slawomir Jaranowski <[email protected]>
1 parent c8d3fe0 commit 7529489

File tree

6 files changed

+155
-55
lines changed

6 files changed

+155
-55
lines changed

pom.xml

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@
125125
<mavenVersion>3.6.3</mavenVersion>
126126
<recommendedJavaBuildVersion>11</recommendedJavaBuildVersion>
127127
<slf4j.version>1.7.36</slf4j.version>
128+
<asm.version>9.6</asm.version>
128129
<invoker.parallelThreads>1C</invoker.parallelThreads>
129130
<project.build.outputTimestamp>2023-11-17T00:21:18Z</project.build.outputTimestamp>
130131
</properties>
@@ -193,6 +194,17 @@
193194
<version>1.4.0</version>
194195
</dependency>
195196

197+
<dependency>
198+
<groupId>org.ow2.asm</groupId>
199+
<artifactId>asm</artifactId>
200+
<version>${asm.version}</version>
201+
</dependency>
202+
<dependency>
203+
<groupId>org.ow2.asm</groupId>
204+
<artifactId>asm-commons</artifactId>
205+
<version>${asm.version}</version>
206+
</dependency>
207+
196208
<dependency>
197209
<groupId>junit</groupId>
198210
<artifactId>junit</artifactId>
@@ -354,15 +366,6 @@
354366
</plugins>
355367
</build>
356368
</profile>
357-
<profile>
358-
<id>java17+</id>
359-
<activation>
360-
<jdk>[17,)</jdk>
361-
</activation>
362-
<properties>
363-
<invoker.security.manager>-Djava.security.manager=allow</invoker.security.manager>
364-
</properties>
365-
</profile>
366369
<profile>
367370
<id>run-its</id>
368371
<build>
@@ -388,11 +391,6 @@
388391
<scriptVariables>
389392
<projectVersion>${project.version}</projectVersion>
390393
</scriptVariables>
391-
<!--
392-
Necessary on JDK 17+ for ITs "mexec-gh-389-block-exit-*" to avoid "UnsupportedOperationException:
393-
The Security Manager is deprecated and will be removed in a future release". See profile 'java17+'.
394-
-->
395-
<mavenOpts>${invoker.security.manager}</mavenOpts>
396394
</configuration>
397395
<executions>
398396
<execution>

src/it/projects/mexec-gh-389-block-exit-non-zero/verify.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ assert buildLogLines[infoMessageLineNumber - 1] == "[one, two, three]"
2424
// Verify that subsequent lines contain the beginning of the thrown SystemExitException stack trace
2525
assert buildLogLines[infoMessageLineNumber + 1].startsWith("[WARNING]")
2626
assert buildLogLines[infoMessageLineNumber + 2].contains("SystemExitException: System::exit was called with return code 123")
27-
assert buildLogLines[infoMessageLineNumber + 3].contains("SystemExitManager.checkExit (SystemExitManager.java")
27+
assert buildLogLines[infoMessageLineNumber + 3].contains("SystemExitManager.exit (SystemExitManager.java")
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package org.codehaus.mojo.exec;
2+
3+
/*
4+
* Licensed to the Apache Software Foundation (ASF) under one
5+
* or more contributor license agreements. See the NOTICE file
6+
* distributed with this work for additional information
7+
* regarding copyright ownership. The ASF licenses this file
8+
* to you under the Apache License, Version 2.0 (the
9+
* "License"); you may not use this file except in compliance
10+
* with the License. You may obtain a copy of the License at
11+
*
12+
* http://www.apache.org/licenses/LICENSE-2.0
13+
*
14+
* Unless required by applicable law or agreed to in writing,
15+
* software distributed under the License is distributed on an
16+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
* KIND, either express or implied. See the License for the
18+
* specific language governing permissions and limitations
19+
* under the License.
20+
*/
21+
22+
import java.lang.instrument.ClassFileTransformer;
23+
import java.security.ProtectionDomain;
24+
25+
import org.objectweb.asm.ClassReader;
26+
import org.objectweb.asm.ClassVisitor;
27+
import org.objectweb.asm.ClassWriter;
28+
import org.objectweb.asm.MethodVisitor;
29+
import org.objectweb.asm.commons.GeneratorAdapter;
30+
31+
import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
32+
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
33+
import static org.objectweb.asm.Opcodes.ASM9;
34+
35+
public class BlockExitTransformer implements ClassFileTransformer {
36+
@Override
37+
public byte[] transform(
38+
final ClassLoader loader,
39+
final String className,
40+
final Class<?> classBeingRedefined,
41+
final ProtectionDomain protectionDomain,
42+
final byte[] classfileBuffer) {
43+
try {
44+
final ClassReader reader = new ClassReader(classfileBuffer);
45+
final ClassWriter writer = new ClassWriter(COMPUTE_FRAMES);
46+
final SystemExitOverrideVisitor visitor = new SystemExitOverrideVisitor(writer);
47+
reader.accept(visitor, EXPAND_FRAMES);
48+
return writer.toByteArray();
49+
} catch (final RuntimeException re) { // too old asm for ex, ignore these classes to not block the rest
50+
return null;
51+
}
52+
}
53+
54+
private static class SystemExitOverrideVisitor extends ClassVisitor {
55+
private static final String SYSTEM_REPLACEMENT =
56+
SystemExitManager.class.getName().replace('.', '/');
57+
58+
private SystemExitOverrideVisitor(final ClassVisitor visitor) {
59+
super(ASM9, visitor);
60+
}
61+
62+
@Override
63+
public MethodVisitor visitMethod(
64+
final int access,
65+
final String name,
66+
final String descriptor,
67+
final String signature,
68+
final String[] exceptions) {
69+
return new GeneratorAdapter(
70+
ASM9,
71+
super.visitMethod(access, name, descriptor, signature, exceptions),
72+
access,
73+
name,
74+
descriptor) {
75+
@Override
76+
public void visitMethodInsn(
77+
final int opcode,
78+
final String owner,
79+
final String name,
80+
final String descriptor,
81+
final boolean isInterface) {
82+
if (owner.equals("java/lang/System") && name.equals("exit")) {
83+
mv.visitMethodInsn(opcode, SYSTEM_REPLACEMENT, name, descriptor, isInterface);
84+
} else {
85+
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
86+
}
87+
}
88+
};
89+
}
90+
}
91+
}

src/main/java/org/codehaus/mojo/exec/ExecJavaMojo.java

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,6 @@ public class ExecJavaMojo extends AbstractExecMojo {
202202
* exception. This way, the error is propagated without terminating the whole Maven JVM. In previous versions, users
203203
* had to use the {@code exec} instead of the {@code java} goal in such cases, which now with this option is no
204204
* longer necessary.
205-
* <p>
206-
* <b>Caveat:</b> Since JDK 17, you need to explicitly allow security manager usage when using this option, e.g. by
207-
* setting {@code -Djava.security.manager=allow} in {@code MAVEN_OPTS}. Otherwise, the JVM will throw an
208-
* {@link UnsupportedOperationException} with a message like "The Security Manager is deprecated and will be removed
209-
* in a future release".
210205
*
211206
* @since 3.2.0
212207
*/
@@ -266,8 +261,6 @@ public void execute() throws MojoExecutionException, MojoFailureException {
266261
bootClassName = mainClass;
267262
}
268263

269-
SecurityManager originalSecurityManager = System.getSecurityManager();
270-
271264
try {
272265
Class<?> bootClass =
273266
Thread.currentThread().getContextClassLoader().loadClass(bootClassName);
@@ -277,9 +270,6 @@ public void execute() throws MojoExecutionException, MojoFailureException {
277270
MethodHandle mainHandle =
278271
lookup.findStatic(bootClass, "main", MethodType.methodType(void.class, String[].class));
279272

280-
if (blockSystemExit) {
281-
System.setSecurityManager(new SystemExitManager(originalSecurityManager));
282-
}
283273
mainHandle.invoke(arguments);
284274
} catch (IllegalAccessException | NoSuchMethodException | NoSuchMethodError e) { // just pass it on
285275
Thread.currentThread()
@@ -302,10 +292,6 @@ public void execute() throws MojoExecutionException, MojoFailureException {
302292
}
303293
} catch (Throwable e) { // just pass it on
304294
Thread.currentThread().getThreadGroup().uncaughtException(Thread.currentThread(), e);
305-
} finally {
306-
if (blockSystemExit) {
307-
System.setSecurityManager(originalSecurityManager);
308-
}
309295
}
310296
},
311297
mainClass + ".main()");
@@ -329,7 +315,7 @@ public void execute() throws MojoExecutionException, MojoFailureException {
329315

330316
try {
331317
threadGroup.destroy();
332-
} catch (IllegalThreadStateException e) {
318+
} catch (RuntimeException /* missing method in future java version */ e) {
333319
getLog().warn("Couldn't destroy threadgroup " + threadGroup, e);
334320
}
335321
}
@@ -544,11 +530,14 @@ private URLClassLoader getClassLoader() throws MojoExecutionException {
544530
this.addAdditionalClasspathElements(classpathURLs);
545531

546532
try {
547-
return URLClassLoaderBuilder.builder()
533+
final URLClassLoaderBuilder builder = URLClassLoaderBuilder.builder()
548534
.setLogger(getLog())
549535
.setPaths(classpathURLs)
550-
.setExclusions(classpathFilenameExclusions)
551-
.build();
536+
.setExclusions(classpathFilenameExclusions);
537+
if (blockSystemExit) {
538+
builder.setTransformer(new BlockExitTransformer());
539+
}
540+
return builder.build();
552541
} catch (NullPointerException | IOException e) {
553542
throw new MojoExecutionException(e.getMessage(), e);
554543
}

src/main/java/org/codehaus/mojo/exec/SystemExitManager.java

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,14 @@
1616
* limitations under the License.
1717
*/
1818

19-
import java.security.Permission;
20-
2119
/**
22-
* A special security manager (SM) passing on permission checks to the original SM it replaces, except for
23-
* {@link #checkExit(int)}
20+
* Will be used by {@link BlockExitTransformer} to replace {@link System#exit(int)} by this implementation.
2421
*
2522
* @author Alexander Kriegisch
2623
*/
27-
public class SystemExitManager extends SecurityManager {
28-
private final SecurityManager originalSecurityManager;
29-
30-
public SystemExitManager(SecurityManager originalSecurityManager) {
31-
this.originalSecurityManager = originalSecurityManager;
24+
public final class SystemExitManager {
25+
private SystemExitManager() {
26+
// no-op
3227
}
3328

3429
/**
@@ -52,15 +47,7 @@ public SystemExitManager(SecurityManager originalSecurityManager) {
5247
*
5348
* @param status the exit status
5449
*/
55-
@Override
56-
public void checkExit(int status) {
50+
public static void exit(final int status) {
5751
throw new SystemExitException("System::exit was called with return code " + status, status);
5852
}
59-
60-
@Override
61-
public void checkPermission(Permission perm) {
62-
if (originalSecurityManager != null) {
63-
originalSecurityManager.checkPermission(perm);
64-
}
65-
}
6653
}

src/main/java/org/codehaus/mojo/exec/URLClassLoaderBuilder.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
import java.io.ByteArrayOutputStream;
2323
import java.io.File;
2424
import java.io.IOException;
25+
import java.io.InputStream;
26+
import java.lang.instrument.ClassFileTransformer;
27+
import java.lang.instrument.IllegalClassFormatException;
2528
import java.net.MalformedURLException;
2629
import java.net.URL;
2730
import java.net.URLClassLoader;
@@ -34,6 +37,7 @@
3437
import java.util.List;
3538

3639
import org.apache.maven.plugin.logging.Log;
40+
import org.codehaus.plexus.util.IOUtil;
3741

3842
import static java.util.Arrays.asList;
3943

@@ -46,13 +50,19 @@ class URLClassLoaderBuilder {
4650
private Log logger;
4751
private Collection<Path> paths;
4852
private Collection<String> exclusions;
53+
private ClassFileTransformer transformer;
4954

5055
private URLClassLoaderBuilder() {}
5156

5257
static URLClassLoaderBuilder builder() {
5358
return new URLClassLoaderBuilder();
5459
}
5560

61+
public URLClassLoaderBuilder setTransformer(final ClassFileTransformer transformer) {
62+
this.transformer = transformer;
63+
return this;
64+
}
65+
5666
URLClassLoaderBuilder setLogger(Log logger) {
5767
this.logger = logger;
5868
return this;
@@ -86,7 +96,7 @@ URLClassLoader build() throws IOException {
8696
}
8797
}
8898

89-
return new ExecJavaClassLoader(urls.toArray(new URL[0]));
99+
return new ExecJavaClassLoader(urls.toArray(new URL[0]), transformer, logger);
90100
}
91101

92102
// child first strategy
@@ -100,10 +110,14 @@ private static class ExecJavaClassLoader extends URLClassLoader {
100110
}
101111

102112
private final String jre;
113+
private final Log logger;
114+
private final ClassFileTransformer transformer;
103115

104-
public ExecJavaClassLoader(URL[] urls) {
116+
public ExecJavaClassLoader(final URL[] urls, final ClassFileTransformer transformer, final Log logger) {
105117
super(urls);
106-
jre = getJre();
118+
this.jre = getJre();
119+
this.logger = logger;
120+
this.transformer = transformer;
107121
}
108122

109123
@Override
@@ -112,6 +126,10 @@ public Class<?> loadClass(final String name, final boolean resolve) throws Class
112126
throw new ClassNotFoundException();
113127
}
114128

129+
if ("org.codehaus.mojo.exec.SystemExitManager".equals(name)) {
130+
return SystemExitManager.class;
131+
}
132+
115133
synchronized (getClassLoadingLock(name)) {
116134
Class<?> clazz;
117135

@@ -135,7 +153,7 @@ public Class<?> loadClass(final String name, final boolean resolve) throws Class
135153

136154
// look for it in this classloader
137155
try {
138-
clazz = super.findClass(name);
156+
clazz = transformer != null ? doFindClass(name) : super.findClass(name);
139157
if (clazz != null) {
140158
if (postLoad(resolve, clazz)) {
141159
return clazz;
@@ -164,6 +182,23 @@ public Class<?> loadClass(final String name, final boolean resolve) throws Class
164182
}
165183
}
166184

185+
private Class<?> doFindClass(final String name) throws ClassNotFoundException {
186+
final String resource = name.replace('.', '/') + ".class";
187+
final URL url = super.findResource(resource);
188+
if (url == null) {
189+
throw new ClassNotFoundException(name);
190+
}
191+
192+
try (final InputStream inputStream = url.openStream()) {
193+
final byte[] raw = IOUtil.toByteArray(inputStream);
194+
final byte[] res = transformer.transform(this, name, null, null, raw);
195+
final byte[] bin = res == null ? raw : res;
196+
return super.defineClass(name, bin, 0, bin.length);
197+
} catch (final ClassFormatError | IOException | IllegalClassFormatException var4) {
198+
throw new ClassNotFoundException(name, var4);
199+
}
200+
}
201+
167202
@Override
168203
public Enumeration<URL> getResources(String name) throws IOException {
169204
final Enumeration<URL> selfResources = findResources(name);

0 commit comments

Comments
 (0)