/*
 * Copyright 2002-2020 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.expression.spel.support;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.asm.MethodVisitor;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.Property;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.CompilablePropertyAccessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

A powerful PropertyAccessor that uses reflection to access properties for reading and possibly also for writing on a target instance.

A property can be referenced through a public getter method (when being read) or a public setter method (when being written), and also as a public field.

Author:Andy Clement, Juergen Hoeller, Phillip Webb, Sam Brannen
See Also:
Since:3.0
/** * A powerful {@link PropertyAccessor} that uses reflection to access properties * for reading and possibly also for writing on a target instance. * * <p>A property can be referenced through a public getter method (when being read) * or a public setter method (when being written), and also as a public field. * * @author Andy Clement * @author Juergen Hoeller * @author Phillip Webb * @author Sam Brannen * @since 3.0 * @see StandardEvaluationContext * @see SimpleEvaluationContext * @see DataBindingPropertyAccessor */
public class ReflectivePropertyAccessor implements PropertyAccessor { private static final Set<Class<?>> ANY_TYPES = Collections.emptySet(); private static final Set<Class<?>> BOOLEAN_TYPES; static { Set<Class<?>> booleanTypes = new HashSet<>(4); booleanTypes.add(Boolean.class); booleanTypes.add(Boolean.TYPE); BOOLEAN_TYPES = Collections.unmodifiableSet(booleanTypes); } private final boolean allowWrite; private final Map<PropertyCacheKey, InvokerPair> readerCache = new ConcurrentHashMap<>(64); private final Map<PropertyCacheKey, Member> writerCache = new ConcurrentHashMap<>(64); private final Map<PropertyCacheKey, TypeDescriptor> typeDescriptorCache = new ConcurrentHashMap<>(64); private final Map<Class<?>, Method[]> sortedMethodsCache = new ConcurrentHashMap<>(64); @Nullable private volatile InvokerPair lastReadInvokerPair;
Create a new property accessor for reading as well writing.
See Also:
  • ReflectivePropertyAccessor(boolean)
/** * Create a new property accessor for reading as well writing. * @see #ReflectivePropertyAccessor(boolean) */
public ReflectivePropertyAccessor() { this.allowWrite = true; }
Create a new property accessor for reading and possibly also writing.
Params:
  • allowWrite – whether to allow write operations on a target instance
See Also:
Since:4.3.15
/** * Create a new property accessor for reading and possibly also writing. * @param allowWrite whether to allow write operations on a target instance * @since 4.3.15 * @see #canWrite */
public ReflectivePropertyAccessor(boolean allowWrite) { this.allowWrite = allowWrite; }
Returns null which means this is a general purpose accessor.
/** * Returns {@code null} which means this is a general purpose accessor. */
@Override @Nullable public Class<?>[] getSpecificTargetClasses() { return null; } @Override public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { if (target == null) { return false; } Class<?> type = (target instanceof Class ? (Class<?>) target : target.getClass()); if (type.isArray() && name.equals("length")) { return true; } PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); if (this.readerCache.containsKey(cacheKey)) { return true; } Method method = findGetterForProperty(name, type, target); if (method != null) { // Treat it like a property... // The readerCache will only contain gettable properties (let's not worry about setters for now). Property property = new Property(type, method, null); TypeDescriptor typeDescriptor = new TypeDescriptor(property); method = ClassUtils.getInterfaceMethodIfPossible(method); this.readerCache.put(cacheKey, new InvokerPair(method, typeDescriptor)); this.typeDescriptorCache.put(cacheKey, typeDescriptor); return true; } else { Field field = findField(name, type, target); if (field != null) { TypeDescriptor typeDescriptor = new TypeDescriptor(field); this.readerCache.put(cacheKey, new InvokerPair(field, typeDescriptor)); this.typeDescriptorCache.put(cacheKey, typeDescriptor); return true; } } return false; } @Override public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { Assert.state(target != null, "Target must not be null"); Class<?> type = (target instanceof Class ? (Class<?>) target : target.getClass()); if (type.isArray() && name.equals("length")) { if (target instanceof Class) { throw new AccessException("Cannot access length on array class itself"); } return new TypedValue(Array.getLength(target)); } PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); InvokerPair invoker = this.readerCache.get(cacheKey); this.lastReadInvokerPair = invoker; if (invoker == null || invoker.member instanceof Method) { Method method = (Method) (invoker != null ? invoker.member : null); if (method == null) { method = findGetterForProperty(name, type, target); if (method != null) { // Treat it like a property... // The readerCache will only contain gettable properties (let's not worry about setters for now). Property property = new Property(type, method, null); TypeDescriptor typeDescriptor = new TypeDescriptor(property); method = ClassUtils.getInterfaceMethodIfPossible(method); invoker = new InvokerPair(method, typeDescriptor); this.lastReadInvokerPair = invoker; this.readerCache.put(cacheKey, invoker); } } if (method != null) { try { ReflectionUtils.makeAccessible(method); Object value = method.invoke(target); return new TypedValue(value, invoker.typeDescriptor.narrow(value)); } catch (Exception ex) { throw new AccessException("Unable to access property '" + name + "' through getter method", ex); } } } if (invoker == null || invoker.member instanceof Field) { Field field = (Field) (invoker == null ? null : invoker.member); if (field == null) { field = findField(name, type, target); if (field != null) { invoker = new InvokerPair(field, new TypeDescriptor(field)); this.lastReadInvokerPair = invoker; this.readerCache.put(cacheKey, invoker); } } if (field != null) { try { ReflectionUtils.makeAccessible(field); Object value = field.get(target); return new TypedValue(value, invoker.typeDescriptor.narrow(value)); } catch (Exception ex) { throw new AccessException("Unable to access field '" + name + "'", ex); } } } throw new AccessException("Neither getter method nor field found for property '" + name + "'"); } @Override public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) throws AccessException { if (!this.allowWrite || target == null) { return false; } Class<?> type = (target instanceof Class ? (Class<?>) target : target.getClass()); PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); if (this.writerCache.containsKey(cacheKey)) { return true; } Method method = findSetterForProperty(name, type, target); if (method != null) { // Treat it like a property Property property = new Property(type, null, method); TypeDescriptor typeDescriptor = new TypeDescriptor(property); method = ClassUtils.getInterfaceMethodIfPossible(method); this.writerCache.put(cacheKey, method); this.typeDescriptorCache.put(cacheKey, typeDescriptor); return true; } else { Field field = findField(name, type, target); if (field != null) { this.writerCache.put(cacheKey, field); this.typeDescriptorCache.put(cacheKey, new TypeDescriptor(field)); return true; } } return false; } @Override public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) throws AccessException { if (!this.allowWrite) { throw new AccessException("PropertyAccessor for property '" + name + "' on target [" + target + "] does not allow write operations"); } Assert.state(target != null, "Target must not be null"); Class<?> type = (target instanceof Class ? (Class<?>) target : target.getClass()); Object possiblyConvertedNewValue = newValue; TypeDescriptor typeDescriptor = getTypeDescriptor(context, target, name); if (typeDescriptor != null) { try { possiblyConvertedNewValue = context.getTypeConverter().convertValue( newValue, TypeDescriptor.forObject(newValue), typeDescriptor); } catch (EvaluationException evaluationException) { throw new AccessException("Type conversion failure", evaluationException); } } PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); Member cachedMember = this.writerCache.get(cacheKey); if (cachedMember == null || cachedMember instanceof Method) { Method method = (Method) cachedMember; if (method == null) { method = findSetterForProperty(name, type, target); if (method != null) { method = ClassUtils.getInterfaceMethodIfPossible(method); cachedMember = method; this.writerCache.put(cacheKey, cachedMember); } } if (method != null) { try { ReflectionUtils.makeAccessible(method); method.invoke(target, possiblyConvertedNewValue); return; } catch (Exception ex) { throw new AccessException("Unable to access property '" + name + "' through setter method", ex); } } } if (cachedMember == null || cachedMember instanceof Field) { Field field = (Field) cachedMember; if (field == null) { field = findField(name, type, target); if (field != null) { cachedMember = field; this.writerCache.put(cacheKey, cachedMember); } } if (field != null) { try { ReflectionUtils.makeAccessible(field); field.set(target, possiblyConvertedNewValue); return; } catch (Exception ex) { throw new AccessException("Unable to access field '" + name + "'", ex); } } } throw new AccessException("Neither setter method nor field found for property '" + name + "'"); }
Get the last read invoker pair.
Deprecated:as of 4.3.15 since it is not used within the framework anymore
/** * Get the last read invoker pair. * @deprecated as of 4.3.15 since it is not used within the framework anymore */
@Deprecated @Nullable public Member getLastReadInvokerPair() { InvokerPair lastReadInvoker = this.lastReadInvokerPair; return (lastReadInvoker != null ? lastReadInvoker.member : null); } @Nullable private TypeDescriptor getTypeDescriptor(EvaluationContext context, Object target, String name) { Class<?> type = (target instanceof Class ? (Class<?>) target : target.getClass()); if (type.isArray() && name.equals("length")) { return TypeDescriptor.valueOf(Integer.TYPE); } PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); TypeDescriptor typeDescriptor = this.typeDescriptorCache.get(cacheKey); if (typeDescriptor == null) { // Attempt to populate the cache entry try { if (canRead(context, target, name) || canWrite(context, target, name)) { typeDescriptor = this.typeDescriptorCache.get(cacheKey); } } catch (AccessException ex) { // Continue with null type descriptor } } return typeDescriptor; } @Nullable private Method findGetterForProperty(String propertyName, Class<?> clazz, Object target) { Method method = findGetterForProperty(propertyName, clazz, target instanceof Class); if (method == null && target instanceof Class) { method = findGetterForProperty(propertyName, target.getClass(), false); } return method; } @Nullable private Method findSetterForProperty(String propertyName, Class<?> clazz, Object target) { Method method = findSetterForProperty(propertyName, clazz, target instanceof Class); if (method == null && target instanceof Class) { method = findSetterForProperty(propertyName, target.getClass(), false); } return method; }
Find a getter method for the specified property.
/** * Find a getter method for the specified property. */
@Nullable protected Method findGetterForProperty(String propertyName, Class<?> clazz, boolean mustBeStatic) { Method method = findMethodForProperty(getPropertyMethodSuffixes(propertyName), "get", clazz, mustBeStatic, 0, ANY_TYPES); if (method == null) { method = findMethodForProperty(getPropertyMethodSuffixes(propertyName), "is", clazz, mustBeStatic, 0, BOOLEAN_TYPES); if (method == null) { // Record-style plain accessor method, e.g. name() method = findMethodForProperty(new String[] {propertyName}, "", clazz, mustBeStatic, 0, ANY_TYPES); } } return method; }
Find a setter method for the specified property.
/** * Find a setter method for the specified property. */
@Nullable protected Method findSetterForProperty(String propertyName, Class<?> clazz, boolean mustBeStatic) { return findMethodForProperty(getPropertyMethodSuffixes(propertyName), "set", clazz, mustBeStatic, 1, ANY_TYPES); } @Nullable private Method findMethodForProperty(String[] methodSuffixes, String prefix, Class<?> clazz, boolean mustBeStatic, int numberOfParams, Set<Class<?>> requiredReturnTypes) { Method[] methods = getSortedMethods(clazz); for (String methodSuffix : methodSuffixes) { for (Method method : methods) { if (isCandidateForProperty(method, clazz) && method.getName().equals(prefix + methodSuffix) && method.getParameterCount() == numberOfParams && (!mustBeStatic || Modifier.isStatic(method.getModifiers())) && (requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType()))) { return method; } } } return null; }
Return class methods ordered with non-bridge methods appearing higher.
/** * Return class methods ordered with non-bridge methods appearing higher. */
private Method[] getSortedMethods(Class<?> clazz) { return this.sortedMethodsCache.computeIfAbsent(clazz, key -> { Method[] methods = key.getMethods(); Arrays.sort(methods, (o1, o2) -> (o1.isBridge() == o2.isBridge() ? 0 : (o1.isBridge() ? 1 : -1))); return methods; }); }
Determine whether the given Method is a candidate for property access on an instance of the given target class.

The default implementation considers any method as a candidate, even for non-user-declared properties on the Object base class.

Params:
  • method – the Method to evaluate
  • targetClass – the concrete target class that is being introspected
Since:4.3.15
/** * Determine whether the given {@code Method} is a candidate for property access * on an instance of the given target class. * <p>The default implementation considers any method as a candidate, even for * non-user-declared properties on the {@link Object} base class. * @param method the Method to evaluate * @param targetClass the concrete target class that is being introspected * @since 4.3.15 */
protected boolean isCandidateForProperty(Method method, Class<?> targetClass) { return true; }
Return the method suffixes for a given property name. The default implementation uses JavaBean conventions with additional support for properties of the form 'xY' where the method 'getXY()' is used in preference to the JavaBean convention of 'getxY()'.
/** * Return the method suffixes for a given property name. The default implementation * uses JavaBean conventions with additional support for properties of the form 'xY' * where the method 'getXY()' is used in preference to the JavaBean convention of * 'getxY()'. */
protected String[] getPropertyMethodSuffixes(String propertyName) { String suffix = getPropertyMethodSuffix(propertyName); if (suffix.length() > 0 && Character.isUpperCase(suffix.charAt(0))) { return new String[] {suffix}; } return new String[] {suffix, StringUtils.capitalize(suffix)}; }
Return the method suffix for a given property name. The default implementation uses JavaBean conventions.
/** * Return the method suffix for a given property name. The default implementation * uses JavaBean conventions. */
protected String getPropertyMethodSuffix(String propertyName) { if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) { return propertyName; } return StringUtils.capitalize(propertyName); } @Nullable private Field findField(String name, Class<?> clazz, Object target) { Field field = findField(name, clazz, target instanceof Class); if (field == null && target instanceof Class) { field = findField(name, target.getClass(), false); } return field; }
Find a field of a certain name on a specified class.
/** * Find a field of a certain name on a specified class. */
@Nullable protected Field findField(String name, Class<?> clazz, boolean mustBeStatic) { Field[] fields = clazz.getFields(); for (Field field : fields) { if (field.getName().equals(name) && (!mustBeStatic || Modifier.isStatic(field.getModifiers()))) { return field; } } // We'll search superclasses and implemented interfaces explicitly, // although it shouldn't be necessary - however, see SPR-10125. if (clazz.getSuperclass() != null) { Field field = findField(name, clazz.getSuperclass(), mustBeStatic); if (field != null) { return field; } } for (Class<?> implementedInterface : clazz.getInterfaces()) { Field field = findField(name, implementedInterface, mustBeStatic); if (field != null) { return field; } } return null; }
Attempt to create an optimized property accessor tailored for a property of a particular name on a particular class. The general ReflectivePropertyAccessor will always work but is not optimal due to the need to lookup which reflective member (method/field) to use each time read() is called. This method will just return the ReflectivePropertyAccessor instance if it is unable to build a more optimal accessor.

Note: An optimal accessor is currently only usable for read attempts. Do not call this method if you need a read-write accessor.

See Also:
  • OptimalPropertyAccessor
/** * Attempt to create an optimized property accessor tailored for a property of a * particular name on a particular class. The general ReflectivePropertyAccessor * will always work but is not optimal due to the need to lookup which reflective * member (method/field) to use each time read() is called. This method will just * return the ReflectivePropertyAccessor instance if it is unable to build a more * optimal accessor. * <p>Note: An optimal accessor is currently only usable for read attempts. * Do not call this method if you need a read-write accessor. * @see OptimalPropertyAccessor */
public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullable Object target, String name) { // Don't be clever for arrays or a null target... if (target == null) { return this; } Class<?> clazz = (target instanceof Class ? (Class<?>) target : target.getClass()); if (clazz.isArray()) { return this; } PropertyCacheKey cacheKey = new PropertyCacheKey(clazz, name, target instanceof Class); InvokerPair invocationTarget = this.readerCache.get(cacheKey); if (invocationTarget == null || invocationTarget.member instanceof Method) { Method method = (Method) (invocationTarget != null ? invocationTarget.member : null); if (method == null) { method = findGetterForProperty(name, clazz, target); if (method != null) { TypeDescriptor typeDescriptor = new TypeDescriptor(new MethodParameter(method, -1)); method = ClassUtils.getInterfaceMethodIfPossible(method); invocationTarget = new InvokerPair(method, typeDescriptor); ReflectionUtils.makeAccessible(method); this.readerCache.put(cacheKey, invocationTarget); } } if (method != null) { return new OptimalPropertyAccessor(invocationTarget); } } if (invocationTarget == null || invocationTarget.member instanceof Field) { Field field = (invocationTarget != null ? (Field) invocationTarget.member : null); if (field == null) { field = findField(name, clazz, target instanceof Class); if (field != null) { invocationTarget = new InvokerPair(field, new TypeDescriptor(field)); ReflectionUtils.makeAccessible(field); this.readerCache.put(cacheKey, invocationTarget); } } if (field != null) { return new OptimalPropertyAccessor(invocationTarget); } } return this; }
Captures the member (method/field) to call reflectively to access a property value and the type descriptor for the value returned by the reflective call.
/** * Captures the member (method/field) to call reflectively to access a property value * and the type descriptor for the value returned by the reflective call. */
private static class InvokerPair { final Member member; final TypeDescriptor typeDescriptor; public InvokerPair(Member member, TypeDescriptor typeDescriptor) { this.member = member; this.typeDescriptor = typeDescriptor; } } private static final class PropertyCacheKey implements Comparable<PropertyCacheKey> { private final Class<?> clazz; private final String property; private boolean targetIsClass; public PropertyCacheKey(Class<?> clazz, String name, boolean targetIsClass) { this.clazz = clazz; this.property = name; this.targetIsClass = targetIsClass; } @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } if (!(other instanceof PropertyCacheKey)) { return false; } PropertyCacheKey otherKey = (PropertyCacheKey) other; return (this.clazz == otherKey.clazz && this.property.equals(otherKey.property) && this.targetIsClass == otherKey.targetIsClass); } @Override public int hashCode() { return (this.clazz.hashCode() * 29 + this.property.hashCode()); } @Override public String toString() { return "PropertyCacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + ", targetIsClass=" + this.targetIsClass + "]"; } @Override public int compareTo(PropertyCacheKey other) { int result = this.clazz.getName().compareTo(other.clazz.getName()); if (result == 0) { result = this.property.compareTo(other.property); } return result; } }
An optimized form of a PropertyAccessor that will use reflection but only knows how to access a particular property on a particular class. This is unlike the general ReflectivePropertyResolver which manages a cache of methods/fields that may be invoked to access different properties on different classes. This optimal accessor exists because looking up the appropriate reflective object by class/name on each read is not cheap.
/** * An optimized form of a PropertyAccessor that will use reflection but only knows * how to access a particular property on a particular class. This is unlike the * general ReflectivePropertyResolver which manages a cache of methods/fields that * may be invoked to access different properties on different classes. This optimal * accessor exists because looking up the appropriate reflective object by class/name * on each read is not cheap. */
public static class OptimalPropertyAccessor implements CompilablePropertyAccessor {
The member being accessed.
/** * The member being accessed. */
public final Member member; private final TypeDescriptor typeDescriptor; OptimalPropertyAccessor(InvokerPair target) { this.member = target.member; this.typeDescriptor = target.typeDescriptor; } @Override @Nullable public Class<?>[] getSpecificTargetClasses() { throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); } @Override public boolean canRead(EvaluationContext context, @Nullable Object target, String name) throws AccessException { if (target == null) { return false; } Class<?> type = (target instanceof Class ? (Class<?>) target : target.getClass()); if (type.isArray()) { return false; } if (this.member instanceof Method) { Method method = (Method) this.member; String getterName = "get" + StringUtils.capitalize(name); if (getterName.equals(method.getName())) { return true; } getterName = "is" + StringUtils.capitalize(name); if (getterName.equals(method.getName())) { return true; } } return this.member.getName().equals(name); } @Override public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { if (this.member instanceof Method) { Method method = (Method) this.member; try { ReflectionUtils.makeAccessible(method); Object value = method.invoke(target); return new TypedValue(value, this.typeDescriptor.narrow(value)); } catch (Exception ex) { throw new AccessException("Unable to access property '" + name + "' through getter method", ex); } } else { Field field = (Field) this.member; try { ReflectionUtils.makeAccessible(field); Object value = field.get(target); return new TypedValue(value, this.typeDescriptor.narrow(value)); } catch (Exception ex) { throw new AccessException("Unable to access field '" + name + "'", ex); } } } @Override public boolean canWrite(EvaluationContext context, @Nullable Object target, String name) { throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); } @Override public void write(EvaluationContext context, @Nullable Object target, String name, @Nullable Object newValue) { throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); } @Override public boolean isCompilable() { return (Modifier.isPublic(this.member.getModifiers()) && Modifier.isPublic(this.member.getDeclaringClass().getModifiers())); } @Override public Class<?> getPropertyType() { if (this.member instanceof Method) { return ((Method) this.member).getReturnType(); } else { return ((Field) this.member).getType(); } } @Override public void generateCode(String propertyName, MethodVisitor mv, CodeFlow cf) { boolean isStatic = Modifier.isStatic(this.member.getModifiers()); String descriptor = cf.lastDescriptor(); String classDesc = this.member.getDeclaringClass().getName().replace('.', '/'); if (!isStatic) { if (descriptor == null) { cf.loadTarget(mv); } if (descriptor == null || !classDesc.equals(descriptor.substring(1))) { mv.visitTypeInsn(CHECKCAST, classDesc); } } else { if (descriptor != null) { // A static field/method call will not consume what is on the stack, // it needs to be popped off. mv.visitInsn(POP); } } if (this.member instanceof Method) { Method method = (Method) this.member; boolean isInterface = method.getDeclaringClass().isInterface(); int opcode = (isStatic ? INVOKESTATIC : isInterface ? INVOKEINTERFACE : INVOKEVIRTUAL); mv.visitMethodInsn(opcode, classDesc, method.getName(), CodeFlow.createSignatureDescriptor(method), isInterface); } else { mv.visitFieldInsn((isStatic ? GETSTATIC : GETFIELD), classDesc, this.member.getName(), CodeFlow.toJvmDescriptor(((Field) this.member).getType())); } } } }