Remove LocalVariableTableParameterNameDiscoverer

Closes gh-29559
This commit is contained in:
Juergen Hoeller 2023-04-30 23:05:09 +02:00
parent 9342317291
commit 4d15b58ca4
3 changed files with 2 additions and 622 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -18,8 +18,7 @@ package org.springframework.core;
/**
* Default implementation of the {@link ParameterNameDiscoverer} strategy interface,
* delegating to the Java 8 standard reflection mechanism, with a deprecated fallback
* to {@link LocalVariableTableParameterNameDiscoverer}.
* delegating to the Java 8 standard reflection mechanism.
*
* <p>If a Kotlin reflection implementation is present,
* {@link KotlinReflectionParameterNameDiscoverer} is added first in the list and
@ -36,7 +35,6 @@ package org.springframework.core;
*/
public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
@SuppressWarnings("removal")
public DefaultParameterNameDiscoverer() {
if (KotlinDetector.isKotlinReflectPresent()) {
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
@ -44,12 +42,6 @@ public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDisc
// Recommended approach on Java 8+: compilation with -parameters.
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
// Deprecated fallback to class file parsing for -debug symbols.
// Does not work on native images without class file resources.
if (!NativeDetector.inNativeImage()) {
addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
}
}

View File

@ -1,286 +0,0 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://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.springframework.core;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.asm.ClassReader;
import org.springframework.asm.ClassVisitor;
import org.springframework.asm.Label;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
import org.springframework.asm.SpringAsmInfo;
import org.springframework.asm.Type;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* Implementation of {@link ParameterNameDiscoverer} that uses the LocalVariableTable
* information in the method attributes to discover parameter names. Returns
* {@code null} if the class file was compiled without debug information.
*
* <p>Uses ObjectWeb's ASM library for analyzing class files. Each discoverer instance
* caches the ASM discovered information for each introspected Class, in a thread-safe
* manner. It is recommended to reuse ParameterNameDiscoverer instances as far as possible.
*
* <p>This class is deprecated in the 6.0 generation and scheduled for removal in 6.1
* since it is effectively superseded by {@link StandardReflectionParameterNameDiscoverer}.
* For the time being, this discoverer logs a warning every time it actually inspects a
* class file which is particularly useful for identifying remaining gaps in usage of
* the standard "-parameters" compiler flag, and also unintended over-inspection of
* e.g. JDK core library classes (which are not compiled with the "-parameters" flag).
*
* @author Adrian Colyer
* @author Costin Leau
* @author Juergen Hoeller
* @author Chris Beams
* @author Sam Brannen
* @since 2.0
* @see StandardReflectionParameterNameDiscoverer
* @see DefaultParameterNameDiscoverer
* @deprecated as of 6.0.1, in favor of {@link StandardReflectionParameterNameDiscoverer}
* (with the "-parameters" compiler flag)
*/
@Deprecated(since = "6.0.1", forRemoval = true)
public class LocalVariableTableParameterNameDiscoverer implements ParameterNameDiscoverer {
private static final Log logger = LogFactory.getLog(LocalVariableTableParameterNameDiscoverer.class);
// marker object for classes that do not have any debug info
private static final Map<Executable, String[]> NO_DEBUG_INFO_MAP = Collections.emptyMap();
// the cache uses a nested index (value is a map) to keep the top level cache relatively small in size
private final Map<Class<?>, Map<Executable, String[]>> parameterNamesCache = new ConcurrentHashMap<>(32);
@Override
@Nullable
public String[] getParameterNames(Method method) {
Method originalMethod = BridgeMethodResolver.findBridgedMethod(method);
return doGetParameterNames(originalMethod);
}
@Override
@Nullable
public String[] getParameterNames(Constructor<?> ctor) {
return doGetParameterNames(ctor);
}
@Nullable
private String[] doGetParameterNames(Executable executable) {
Class<?> declaringClass = executable.getDeclaringClass();
Map<Executable, String[]> map = this.parameterNamesCache.computeIfAbsent(declaringClass, this::inspectClass);
return (map != NO_DEBUG_INFO_MAP ? map.get(executable) : null);
}
/**
* Inspects the target class.
* <p>Exceptions will be logged, and a marker map returned to indicate the
* lack of debug information.
*/
private Map<Executable, String[]> inspectClass(Class<?> clazz) {
InputStream is = clazz.getResourceAsStream(ClassUtils.getClassFileName(clazz));
if (is == null) {
// We couldn't load the class file, which is not fatal as it
// simply means this method of discovering parameter names won't work.
if (logger.isDebugEnabled()) {
logger.debug("Cannot find '.class' file for class [" + clazz +
"] - unable to determine constructor/method parameter names");
}
return NO_DEBUG_INFO_MAP;
}
// We cannot use try-with-resources here for the InputStream, since we have
// custom handling of the close() method in a finally-block.
try {
ClassReader classReader = new ClassReader(is);
Map<Executable, String[]> map = new ConcurrentHashMap<>(32);
classReader.accept(new ParameterNameDiscoveringVisitor(clazz, map), 0);
if (logger.isWarnEnabled()) {
logger.warn("Using deprecated '-debug' fallback for parameter name resolution. Compile the " +
"affected code with '-parameters' instead or avoid its introspection: " + clazz.getName());
}
return map;
}
catch (IOException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Exception thrown while reading '.class' file for class [" + clazz +
"] - unable to determine constructor/method parameter names", ex);
}
}
catch (IllegalArgumentException ex) {
if (logger.isDebugEnabled()) {
logger.debug("ASM ClassReader failed to parse class file [" + clazz +
"], probably due to a new Java class file version that isn't supported yet " +
"- unable to determine constructor/method parameter names", ex);
}
}
finally {
try {
is.close();
}
catch (IOException ex) {
// ignore
}
}
return NO_DEBUG_INFO_MAP;
}
/**
* Helper class that inspects all methods and constructors and then
* attempts to find the parameter names for the given {@link Executable}.
*/
private static class ParameterNameDiscoveringVisitor extends ClassVisitor {
private static final String STATIC_CLASS_INIT = "<clinit>";
private final Class<?> clazz;
private final Map<Executable, String[]> executableMap;
public ParameterNameDiscoveringVisitor(Class<?> clazz, Map<Executable, String[]> executableMap) {
super(SpringAsmInfo.ASM_VERSION);
this.clazz = clazz;
this.executableMap = executableMap;
}
@Override
@Nullable
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// exclude synthetic + bridged && static class initialization
if (!isSyntheticOrBridged(access) && !STATIC_CLASS_INIT.equals(name)) {
return new LocalVariableTableVisitor(this.clazz, this.executableMap, name, desc, isStatic(access));
}
return null;
}
private static boolean isSyntheticOrBridged(int access) {
return (((access & Opcodes.ACC_SYNTHETIC) | (access & Opcodes.ACC_BRIDGE)) > 0);
}
private static boolean isStatic(int access) {
return ((access & Opcodes.ACC_STATIC) > 0);
}
}
private static class LocalVariableTableVisitor extends MethodVisitor {
private static final String CONSTRUCTOR = "<init>";
private final Class<?> clazz;
private final Map<Executable, String[]> executableMap;
private final String name;
private final Type[] args;
private final String[] parameterNames;
private final boolean isStatic;
private boolean hasLvtInfo = false;
/*
* The nth entry contains the slot index of the LVT table entry holding the
* argument name for the nth parameter.
*/
private final int[] lvtSlotIndex;
public LocalVariableTableVisitor(Class<?> clazz, Map<Executable, String[]> map, String name, String desc, boolean isStatic) {
super(SpringAsmInfo.ASM_VERSION);
this.clazz = clazz;
this.executableMap = map;
this.name = name;
this.args = Type.getArgumentTypes(desc);
this.parameterNames = new String[this.args.length];
this.isStatic = isStatic;
this.lvtSlotIndex = computeLvtSlotIndices(isStatic, this.args);
}
@Override
public void visitLocalVariable(String name, String description, String signature, Label start, Label end, int index) {
this.hasLvtInfo = true;
for (int i = 0; i < this.lvtSlotIndex.length; i++) {
if (this.lvtSlotIndex[i] == index) {
this.parameterNames[i] = name;
}
}
}
@Override
public void visitEnd() {
if (this.hasLvtInfo || (this.isStatic && this.parameterNames.length == 0)) {
// visitLocalVariable will never be called for static no args methods
// which doesn't use any local variables.
// This means that hasLvtInfo could be false for that kind of methods
// even if the class has local variable info.
this.executableMap.put(resolveExecutable(), this.parameterNames);
}
}
private Executable resolveExecutable() {
ClassLoader loader = this.clazz.getClassLoader();
Class<?>[] argTypes = new Class<?>[this.args.length];
for (int i = 0; i < this.args.length; i++) {
argTypes[i] = ClassUtils.resolveClassName(this.args[i].getClassName(), loader);
}
try {
if (CONSTRUCTOR.equals(this.name)) {
return this.clazz.getDeclaredConstructor(argTypes);
}
return this.clazz.getDeclaredMethod(this.name, argTypes);
}
catch (NoSuchMethodException ex) {
throw new IllegalStateException("Method [" + this.name +
"] was discovered in the .class file but cannot be resolved in the class object", ex);
}
}
private static int[] computeLvtSlotIndices(boolean isStatic, Type[] paramTypes) {
int[] lvtIndex = new int[paramTypes.length];
int nextIndex = (isStatic ? 0 : 1);
for (int i = 0; i < paramTypes.length; i++) {
lvtIndex[i] = nextIndex;
if (isWideType(paramTypes[i])) {
nextIndex += 2;
}
else {
nextIndex++;
}
}
return lvtIndex;
}
private static boolean isWideType(Type aType) {
// float is not a wide type
return (aType == Type.LONG_TYPE || aType == Type.DOUBLE_TYPE);
}
}
}

View File

@ -1,326 +0,0 @@
/*
* Copyright 2002-2023 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.
* You may obtain a copy of the License at
*
* https://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.springframework.core;
import java.awt.Component;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Date;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.tests.sample.objects.TestObject;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Adrian Colyer
*/
class LocalVariableTableParameterNameDiscovererTests {
@SuppressWarnings("removal")
private final ParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
@Test
void methodParameterNameDiscoveryNoArgs() throws NoSuchMethodException {
Method getName = TestObject.class.getMethod("getName");
String[] names = discoverer.getParameterNames(getName);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("no argument names").isEmpty();
}
@Test
void methodParameterNameDiscoveryWithArgs() throws NoSuchMethodException {
Method setName = TestObject.class.getMethod("setName", String.class);
String[] names = discoverer.getParameterNames(setName);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("one argument").hasSize(1);
assertThat(names[0]).isEqualTo("name");
}
@Test
void consParameterNameDiscoveryNoArgs() throws NoSuchMethodException {
Constructor<TestObject> noArgsCons = TestObject.class.getConstructor();
String[] names = discoverer.getParameterNames(noArgsCons);
assertThat(names).as("should find cons info").isNotNull();
assertThat(names).as("no argument names").isEmpty();
}
@Test
void consParameterNameDiscoveryArgs() throws NoSuchMethodException {
Constructor<TestObject> twoArgCons = TestObject.class.getConstructor(String.class, int.class);
String[] names = discoverer.getParameterNames(twoArgCons);
assertThat(names).as("should find cons info").isNotNull();
assertThat(names).as("one argument").hasSize(2);
assertThat(names[0]).isEqualTo("name");
assertThat(names[1]).isEqualTo("age");
}
@Test
void staticMethodParameterNameDiscoveryNoArgs() throws NoSuchMethodException {
Method m = getClass().getMethod("staticMethodNoLocalVars");
String[] names = discoverer.getParameterNames(m);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("no argument names").isEmpty();
}
@Test
void overloadedStaticMethod() throws Exception {
Class<? extends LocalVariableTableParameterNameDiscovererTests> clazz = this.getClass();
Method m1 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE);
String[] names = discoverer.getParameterNames(m1);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("two arguments").hasSize(2);
assertThat(names[0]).isEqualTo("x");
assertThat(names[1]).isEqualTo("y");
Method m2 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE, Long.TYPE);
names = discoverer.getParameterNames(m2);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("three arguments").hasSize(3);
assertThat(names[0]).isEqualTo("x");
assertThat(names[1]).isEqualTo("y");
assertThat(names[2]).isEqualTo("z");
}
@Test
void overloadedStaticMethodInInnerClass() throws Exception {
Class<InnerClass> clazz = InnerClass.class;
Method m1 = clazz.getMethod("staticMethod", Long.TYPE);
String[] names = discoverer.getParameterNames(m1);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("one argument").hasSize(1);
assertThat(names[0]).isEqualTo("x");
Method m2 = clazz.getMethod("staticMethod", Long.TYPE, Long.TYPE);
names = discoverer.getParameterNames(m2);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("two arguments").hasSize(2);
assertThat(names[0]).isEqualTo("x");
assertThat(names[1]).isEqualTo("y");
}
@Test
void overloadedMethod() throws Exception {
Class<? extends LocalVariableTableParameterNameDiscovererTests> clazz = this.getClass();
Method m1 = clazz.getMethod("instanceMethod", Double.TYPE, Double.TYPE);
String[] names = discoverer.getParameterNames(m1);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("two arguments").hasSize(2);
assertThat(names[0]).isEqualTo("x");
assertThat(names[1]).isEqualTo("y");
Method m2 = clazz.getMethod("instanceMethod", Double.TYPE, Double.TYPE, Double.TYPE);
names = discoverer.getParameterNames(m2);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("three arguments").hasSize(3);
assertThat(names[0]).isEqualTo("x");
assertThat(names[1]).isEqualTo("y");
assertThat(names[2]).isEqualTo("z");
}
@Test
void overloadedMethodInInnerClass() throws Exception {
Class<InnerClass> clazz = InnerClass.class;
Method m1 = clazz.getMethod("instanceMethod", String.class);
String[] names = discoverer.getParameterNames(m1);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("one argument").hasSize(1);
assertThat(names[0]).isEqualTo("aa");
Method m2 = clazz.getMethod("instanceMethod", String.class, String.class);
names = discoverer.getParameterNames(m2);
assertThat(names).as("should find method info").isNotNull();
assertThat(names).as("two arguments").hasSize(2);
assertThat(names[0]).isEqualTo("aa");
assertThat(names[1]).isEqualTo("bb");
}
@Test
void generifiedClass() throws Exception {
Class<?> clazz = GenerifiedClass.class;
Constructor<?> ctor = clazz.getDeclaredConstructor(Object.class);
String[] names = discoverer.getParameterNames(ctor);
assertThat(names).hasSize(1);
assertThat(names[0]).isEqualTo("key");
ctor = clazz.getDeclaredConstructor(Object.class, Object.class);
names = discoverer.getParameterNames(ctor);
assertThat(names).hasSize(2);
assertThat(names[0]).isEqualTo("key");
assertThat(names[1]).isEqualTo("value");
Method m = clazz.getMethod("generifiedStaticMethod", Object.class);
names = discoverer.getParameterNames(m);
assertThat(names).hasSize(1);
assertThat(names[0]).isEqualTo("param");
m = clazz.getMethod("generifiedMethod", Object.class, long.class, Object.class, Object.class);
names = discoverer.getParameterNames(m);
assertThat(names).hasSize(4);
assertThat(names[0]).isEqualTo("param");
assertThat(names[1]).isEqualTo("x");
assertThat(names[2]).isEqualTo("key");
assertThat(names[3]).isEqualTo("value");
m = clazz.getMethod("voidStaticMethod", Object.class, long.class, int.class);
names = discoverer.getParameterNames(m);
assertThat(names).hasSize(3);
assertThat(names[0]).isEqualTo("obj");
assertThat(names[1]).isEqualTo("x");
assertThat(names[2]).isEqualTo("i");
m = clazz.getMethod("nonVoidStaticMethod", Object.class, long.class, int.class);
names = discoverer.getParameterNames(m);
assertThat(names).hasSize(3);
assertThat(names[0]).isEqualTo("obj");
assertThat(names[1]).isEqualTo("x");
assertThat(names[2]).isEqualTo("i");
m = clazz.getMethod("getDate");
names = discoverer.getParameterNames(m);
assertThat(names).isEmpty();
}
@Disabled("Ignored because Ubuntu packages OpenJDK with debug symbols enabled. See SPR-8078.")
@Test
void classesWithoutDebugSymbols() throws Exception {
// JDK classes don't have debug information (usually)
Class<Component> clazz = Component.class;
String methodName = "list";
Method m = clazz.getMethod(methodName);
String[] names = discoverer.getParameterNames(m);
assertThat(names).isNull();
m = clazz.getMethod(methodName, PrintStream.class);
names = discoverer.getParameterNames(m);
assertThat(names).isNull();
m = clazz.getMethod(methodName, PrintStream.class, int.class);
names = discoverer.getParameterNames(m);
assertThat(names).isNull();
}
public static void staticMethodNoLocalVars() {
}
public static long staticMethod(long x, long y) {
long u = x * y;
return u;
}
public static long staticMethod(long x, long y, long z) {
long u = x * y * z;
return u;
}
public double instanceMethod(double x, double y) {
double u = x * y;
return u;
}
public double instanceMethod(double x, double y, double z) {
double u = x * y * z;
return u;
}
public static class InnerClass {
public int waz = 0;
public InnerClass() {
}
public InnerClass(String firstArg, long secondArg, Object thirdArg) {
long foo = 0;
short bar = 10;
this.waz = (int) (foo + bar);
}
public String instanceMethod(String aa) {
return aa;
}
public String instanceMethod(String aa, String bb) {
return aa + bb;
}
public static long staticMethod(long x) {
long u = x;
return u;
}
public static long staticMethod(long x, long y) {
long u = x * y;
return u;
}
}
public static class GenerifiedClass<K, V> {
private static long date;
static {
// some custom static bloc or <clinit>
date = new Date().getTime();
}
public GenerifiedClass() {
this(null, null);
}
public GenerifiedClass(K key) {
this(key, null);
}
public GenerifiedClass(K key, V value) {
}
public static <P> long generifiedStaticMethod(P param) {
return date;
}
public <P> void generifiedMethod(P param, long x, K key, V value) {
// nothing
}
public static void voidStaticMethod(Object obj, long x, int i) {
// nothing
}
public static long nonVoidStaticMethod(Object obj, long x, int i) {
return date;
}
public static long getDate() {
return date;
}
}
}