Improve names of classes generated by the SpEL compiler

Prior to this commit, the SpEL compiler generated classes in a package
named "spel" with names following the pattern "Ex#", where # was an
index starting with 2.

This resulted in class names such as:

- spel.Ex2
- spel.Ex3

This commit improves the names of classes created by the SpEL compiler
by generating classes in a package named
"org.springframework.expression.spel.generated" with names following
the pattern "CompiledExpression#####", where ##### is a 0-padded
counter starting with 00001.

This results in class names such as:

- org.springframework.expression.spel.generated.CompiledExpression00001
- org.springframework.expression.spel.generated.CompiledExpression00002

This commit also moves the saveGeneratedClassFile() method from
SpelCompilationCoverageTests to SpelCompiler and enhances it to:

- Save classes in a "build/generated-classes" directory.
- Convert package names to directories.
- Create missing parent directories.
- Use logging instead of System.out.println().

Running a test with saveGeneratedClassFile() enabled now logs something
similar to the following.

DEBUG o.s.e.s.s.SpelCompiler - Saving compiled SpEL expression [(#root.empty ? 0 : #root.size)] to [/Users/<username>/spring-framework/spring-expression/build/generated-classes/org/springframework/expression/spel/generated/CompiledExpression00001.class]

Closes gh-32497
This commit is contained in:
Sam Brannen 2024-03-20 12:34:01 +01:00
parent 2f070e59e6
commit 7f40b49f4d
2 changed files with 29 additions and 25 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@ -81,7 +81,7 @@ public final class SpelCompiler implements Opcodes {
private volatile ChildClassLoader childClassLoader;
// Counter suffix for generated classes within this SpelCompiler instance
private final AtomicInteger suffixId = new AtomicInteger(1);
private final AtomicInteger suffixId = new AtomicInteger(0);
private SpelCompiler(@Nullable ClassLoader classloader) {
@ -122,21 +122,22 @@ public final class SpelCompiler implements Opcodes {
return null;
}
private int getNextSuffix() {
return this.suffixId.incrementAndGet();
private String getNextSuffix() {
return "%05d".formatted(this.suffixId.incrementAndGet());
}
/**
* Generate the class that encapsulates the compiled expression and define it.
* The generated class will be a subtype of CompiledExpression.
* <p>The generated class will be a subtype of {@link CompiledExpression}.
* @param expressionToCompile the expression to be compiled
* @return the expression call, or {@code null} if the decision was to opt out of
* compilation during code generation
*/
@Nullable
private Class<? extends CompiledExpression> createExpressionClass(SpelNodeImpl expressionToCompile) {
// Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression'
String className = "spel/Ex" + getNextSuffix();
// Create class outline:
// org.springframework.expression.spel.generated.CompiledExpression##### extends org.springframework.expression.spel.CompiledExpression
String className = "org/springframework/expression/spel/generated/CompiledExpression" + getNextSuffix();
String evaluationContextClass = "org/springframework/expression/EvaluationContext";
ClassWriter cw = new ExpressionClassWriter();
cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
@ -184,12 +185,31 @@ public final class SpelCompiler implements Opcodes {
cf.finish();
byte[] data = cw.toByteArray();
// TODO Save generated class files conditionally based on a debug flag.
// Source code for the following method resides in SpelCompilationCoverageTests.
// TODO Save generated class files conditionally based on a flag.
// saveGeneratedClassFile(expressionToCompile.toStringAST(), className, data);
return loadClass(StringUtils.replace(className, "/", "."), data);
}
// NOTE: saveGeneratedClassFile() can be uncommented in order to review generated byte code for
// debugging purposes. See also: https://github.com/spring-projects/spring-framework/issues/29548
//
// private static void saveGeneratedClassFile(String stringAST, String className, byte[] data) {
// try {
// // TODO Make target directory configurable.
// String targetDir = "build/generated-classes";
// Path path = Path.of(targetDir, className + ".class");
// Files.deleteIfExists(path);
// Files.createDirectories(path.getParent());
// if (logger.isDebugEnabled()) {
// logger.debug("Saving compiled SpEL expression [%s] to [%s]".formatted(stringAST, path.toAbsolutePath()));
// }
// Files.copy(new ByteArrayInputStream(data), path);
// }
// catch (IOException ex) {
// throw new UncheckedIOException(ex);
// }
// }
/**
* Load a compiled expression class. Makes sure the classloaders aren't used too much
* because they anchor compiled classes in memory and prevent GC. If you have expressions

View File

@ -6724,20 +6724,4 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
}
}
// NOTE: saveGeneratedClassFile() can be copied to SpelCompiler and uncommented
// at the end of createExpressionClass(SpelNodeImpl) in order to review generated
// byte code for debugging purposes.
//
// private static void saveGeneratedClassFile(String stringAST, String className, byte[] data) {
// try {
// Path path = Path.of("build", StringUtils.replace(className, "/", ".") + ".class");
// Files.deleteIfExists(path);
// System.out.println("Writing compiled SpEL expression [%s] to [%s]".formatted(stringAST, path.toAbsolutePath()));
// Files.copy(new ByteArrayInputStream(data), path);
// }
// catch (IOException ex) {
// throw new UncheckedIOException(ex);
// }
// }
}