8304425: ClassHierarchyResolver from Reflection

Reviewed-by: asotona
This commit is contained in:
Chen Liang 2023-06-08 07:29:56 +00:00 committed by Adam Sotona
parent 79a4ac791c
commit ac3ce2bf75
10 changed files with 373 additions and 98 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -26,12 +26,19 @@ package jdk.internal.classfile;
import java.io.InputStream;
import java.lang.constant.ClassDesc;
import java.lang.invoke.MethodHandles;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import jdk.internal.classfile.impl.Util;
import java.util.function.Supplier;
import jdk.internal.classfile.impl.ClassHierarchyImpl;
import jdk.internal.classfile.impl.ClassHierarchyImpl.ClassLoadingClassHierarchyResolver;
import jdk.internal.classfile.impl.ClassHierarchyImpl.StaticClassHierarchyResolver;
import jdk.internal.classfile.impl.Util;
import static java.lang.constant.ConstantDescs.CD_Object;
/**
* Provides class hierarchy information for generating correct stack maps
@ -41,18 +48,14 @@ import jdk.internal.classfile.impl.ClassHierarchyImpl;
public interface ClassHierarchyResolver {
/**
* Default singleton instance of {@linkplain ClassHierarchyResolver}
* using {@link ClassLoader#getSystemResourceAsStream(String)}
* as the {@code ClassStreamResolver}
* Returns a default instance of {@linkplain ClassHierarchyResolver}
* that reads from system class loader with
* {@link ClassLoader#getSystemResourceAsStream(String)} and falls
* back to reflection if a class is not found.
*/
ClassHierarchyResolver DEFAULT_CLASS_HIERARCHY_RESOLVER
= new ClassHierarchyImpl.CachedClassHierarchyResolver(
new Function<ClassDesc, InputStream>() {
@Override
public InputStream apply(ClassDesc classDesc) {
return ClassLoader.getSystemResourceAsStream(Util.toInternalName(classDesc) + ".class");
}
});
static ClassHierarchyResolver defaultResolver() {
return ClassHierarchyImpl.DEFAULT_RESOLVER;
}
/**
* {@return the {@link ClassHierarchyInfo} for a given class name, or null
@ -61,6 +64,31 @@ public interface ClassHierarchyResolver {
*/
ClassHierarchyInfo getClassInfo(ClassDesc classDesc);
/**
* Information about a resolved class.
*/
sealed interface ClassHierarchyInfo permits ClassHierarchyImpl.ClassHierarchyInfoImpl {
/**
* Indicates that a class is a declared class, with the specific given super class.
*
* @param superClass descriptor of the super class, may be {@code null}
* @return the info indicating the super class
*/
static ClassHierarchyInfo ofClass(ClassDesc superClass) {
return new ClassHierarchyImpl.ClassHierarchyInfoImpl(superClass, false);
}
/**
* Indicates that a class is an interface.
*
* @return the info indicating an interface
*/
static ClassHierarchyInfo ofInterface() {
return new ClassHierarchyImpl.ClassHierarchyInfoImpl(CD_Object, true);
}
}
/**
* Chains this {@linkplain ClassHierarchyResolver} with another to be
* consulted if this resolver does not know about the specified class.
@ -74,30 +102,73 @@ public interface ClassHierarchyResolver {
public ClassHierarchyInfo getClassInfo(ClassDesc classDesc) {
var chi = ClassHierarchyResolver.this.getClassInfo(classDesc);
if (chi == null)
chi = other.getClassInfo(classDesc);
return other.getClassInfo(classDesc);
return chi;
}
};
}
/**
* Information about a resolved class.
* @param thisClass descriptor of this class
* @param isInterface whether this class is an interface
* @param superClass descriptor of the superclass (not relevant for interfaces)
* Returns a ClassHierarchyResolver that caches class hierarchy information from this
* resolver. The returned resolver will not update if delegate resolver returns differently.
* The thread safety of the returned resolver depends on the thread safety of the map
* returned by the {@code cacheFactory}.
*
* @param cacheFactory the factory for the cache
* @return the ClassHierarchyResolver with caching
*/
public record ClassHierarchyInfo(ClassDesc thisClass, boolean isInterface, ClassDesc superClass) {
default ClassHierarchyResolver cached(Supplier<Map<ClassDesc, ClassHierarchyInfo>> cacheFactory) {
return new ClassHierarchyImpl.CachedClassHierarchyResolver(this, cacheFactory.get());
}
/**
* Returns a {@linkplain ClassHierarchyResolver} that extracts class hierarchy
* information from classfiles located by a mapping function
* Returns a ClassHierarchyResolver that caches class hierarchy information from this
* resolver. The returned resolver will not update if delegate resolver returns differently.
* The returned resolver is not thread-safe.
* {@snippet file="PackageSnippets.java" region="lookup-class-hierarchy-resolver"}
*
* @return the ClassHierarchyResolver
*/
default ClassHierarchyResolver cached() {
record Factory() implements Supplier<Map<ClassDesc, ClassHierarchyInfo>> {
// uses a record as we cannot declare a local class static
static final Factory INSTANCE = new Factory();
@Override
public Map<ClassDesc, ClassHierarchyInfo> get() {
return new HashMap<>();
}
}
return cached(Factory.INSTANCE);
}
/**
* Returns a {@linkplain ClassHierarchyResolver} that extracts class hierarchy
* information from classfiles located by a mapping function. The mapping function
* should return null if it cannot provide a mapping for a classfile. Any IOException
* from the provided input stream is rethrown as an UncheckedIOException.
*
* @param classStreamResolver maps class descriptors to classfile input streams
* @return the {@linkplain ClassHierarchyResolver}
*/
public static ClassHierarchyResolver ofCached(Function<ClassDesc, InputStream> classStreamResolver) {
return new ClassHierarchyImpl.CachedClassHierarchyResolver(classStreamResolver);
static ClassHierarchyResolver ofResourceParsing(Function<ClassDesc, InputStream> classStreamResolver) {
return new ClassHierarchyImpl.ResourceParsingClassHierarchyResolver(classStreamResolver);
}
/**
* Returns a {@linkplain ClassHierarchyResolver} that extracts class hierarchy
* information from classfiles located by a class loader.
*
* @param loader the class loader, to find class files
* @return the {@linkplain ClassHierarchyResolver}
*/
static ClassHierarchyResolver ofResourceParsing(ClassLoader loader) {
return ofResourceParsing(new Function<>() {
@Override
public InputStream apply(ClassDesc classDesc) {
return loader.getResourceAsStream(Util.toInternalName(classDesc) + ".class");
}
});
}
/**
@ -108,8 +179,52 @@ public interface ClassHierarchyResolver {
* @param classToSuperClass a map from classes to their super classes
* @return the {@linkplain ClassHierarchyResolver}
*/
public static ClassHierarchyResolver of(Collection<ClassDesc> interfaces,
static ClassHierarchyResolver of(Collection<ClassDesc> interfaces,
Map<ClassDesc, ClassDesc> classToSuperClass) {
return new ClassHierarchyImpl.StaticClassHierarchyResolver(interfaces, classToSuperClass);
return new StaticClassHierarchyResolver(interfaces, classToSuperClass);
}
/**
* Returns a ClassHierarchyResolver that extracts class hierarchy information via
* the Reflection API with a {@linkplain ClassLoader}.
*
* @param loader the class loader
* @return the class hierarchy resolver
*/
static ClassHierarchyResolver ofClassLoading(ClassLoader loader) {
return new ClassLoadingClassHierarchyResolver(new Function<>() {
@Override
public Class<?> apply(ClassDesc cd) {
try {
return Class.forName(Util.toBinaryName(cd.descriptorString()), false, loader);
} catch (ClassNotFoundException ex) {
return null;
}
}
});
}
/**
* Returns a ClassHierarchyResolver that extracts class hierarchy information via
* the Reflection API with a {@linkplain MethodHandles.Lookup Lookup}. If the class
* resolved is inaccessible to the given lookup, it throws {@link
* IllegalArgumentException} instead of returning {@code null}.
*
* @param lookup the lookup, must be able to access classes to resolve
* @return the class hierarchy resolver
*/
static ClassHierarchyResolver ofClassLoading(MethodHandles.Lookup lookup) {
return new ClassLoadingClassHierarchyResolver(new Function<>() {
@Override
public Class<?> apply(ClassDesc cd) {
try {
return cd.resolveConstantDesc(lookup);
} catch (IllegalAccessException ex) {
throw new IllegalArgumentException(ex);
} catch (ReflectiveOperationException ex) {
return null;
}
}
});
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,17 +25,22 @@ package jdk.internal.classfile.impl;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.constant.ClassDesc;
import java.lang.constant.ConstantDescs;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
import jdk.internal.classfile.ClassHierarchyResolver;
import static java.lang.constant.ConstantDescs.CD_Object;
import static jdk.internal.classfile.Classfile.*;
/**
* Class hierarchy resolution framework is answering questions about classes assignability, common classes ancestor and whether the class represents an interface.
* All the requests are handled without class loading nor full verification, optionally with incomplete dependencies and with focus on maximum performance.
@ -43,6 +48,20 @@ import jdk.internal.classfile.ClassHierarchyResolver;
*/
public final class ClassHierarchyImpl {
public record ClassHierarchyInfoImpl(ClassDesc superClass, boolean isInterface) implements ClassHierarchyResolver.ClassHierarchyInfo {
static final ClassHierarchyResolver.ClassHierarchyInfo OBJECT_INFO = new ClassHierarchyInfoImpl(null, false);
}
public static final ClassHierarchyResolver DEFAULT_RESOLVER = ClassHierarchyResolver
.ofResourceParsing(ResourceParsingClassHierarchyResolver.SYSTEM_STREAM_PROVIDER)
.orElse(new ClassLoadingClassHierarchyResolver(ClassLoadingClassHierarchyResolver.SYSTEM_CLASS_PROVIDER))
.cached(new Supplier<>() {
@Override
public Map<ClassDesc, ClassHierarchyResolver.ClassHierarchyInfo> get() {
return new ConcurrentHashMap<>();
}
});
private final ClassHierarchyResolver resolver;
/**
@ -53,9 +72,9 @@ public final class ClassHierarchyImpl {
this.resolver = classHierarchyResolver;
}
private ClassHierarchyResolver.ClassHierarchyInfo resolve(ClassDesc classDesc) {
private ClassHierarchyInfoImpl resolve(ClassDesc classDesc) {
var res = resolver.getClassInfo(classDesc);
if (res != null) return res;
if (res != null) return (ClassHierarchyInfoImpl) res;
throw new IllegalArgumentException("Could not resolve class " + classDesc.displayName());
}
@ -79,7 +98,7 @@ public final class ClassHierarchyImpl {
//calculation of common ancestor is a robust (yet fast) way to decide about assignability in incompletely resolved class hierarchy
//exact order of symbol loops is critical for performance of the above isAssignableFrom method, so standard situations are resolved in linear time
//this method returns null if common ancestor could not be identified
if (isInterface(symbol1) || isInterface(symbol2)) return ConstantDescs.CD_Object;
if (isInterface(symbol1) || isInterface(symbol2)) return CD_Object;
for (var s1 = symbol1; s1 != null; s1 = resolve(s1).superClass()) {
for (var s2 = symbol2; s2 != null; s2 = resolve(s2).superClass()) {
if (s1.equals(s2)) return s1;
@ -101,83 +120,94 @@ public final class ClassHierarchyImpl {
}
public static final class CachedClassHierarchyResolver implements ClassHierarchyResolver {
//this instance should never appear in the cache nor leak out
//this instance should leak out, appears only in cache in order to utilize Map.computeIfAbsent
private static final ClassHierarchyResolver.ClassHierarchyInfo NOPE =
new ClassHierarchyResolver.ClassHierarchyInfo(null, false, null);
new ClassHierarchyInfoImpl(null, true);
private final Function<ClassDesc, InputStream> streamProvider;
private final Map<ClassDesc, ClassHierarchyResolver.ClassHierarchyInfo> resolvedCache;
private final Map<ClassDesc, ClassHierarchyInfo> resolvedCache;
private final Function<ClassDesc, ClassHierarchyInfo> delegateFunction;
public CachedClassHierarchyResolver(Function<ClassDesc, InputStream> classStreamProvider) {
this.streamProvider = classStreamProvider;
this.resolvedCache = Collections.synchronizedMap(new HashMap<>());
public CachedClassHierarchyResolver(ClassHierarchyResolver delegate, Map<ClassDesc, ClassHierarchyInfo> resolvedCache) {
this.resolvedCache = resolvedCache;
this.delegateFunction = new Function<>() {
@Override
public ClassHierarchyInfo apply(ClassDesc classDesc) {
var ret = delegate.getClassInfo(classDesc);
return ret == null ? NOPE : ret;
}
};
}
@Override
public ClassHierarchyInfo getClassInfo(ClassDesc classDesc) {
var ret = resolvedCache.computeIfAbsent(classDesc, delegateFunction);
return ret == NOPE ? null : ret;
}
}
public static final class ResourceParsingClassHierarchyResolver implements ClassHierarchyResolver {
public static final Function<ClassDesc, InputStream> SYSTEM_STREAM_PROVIDER = new Function<>() {
@Override
public InputStream apply(ClassDesc cd) {
return ClassLoader.getSystemClassLoader().getResourceAsStream(Util.toInternalName(cd) + ".class");
}
};
private final Function<ClassDesc, InputStream> streamProvider;
public ResourceParsingClassHierarchyResolver(Function<ClassDesc, InputStream> classStreamProvider) {
this.streamProvider = classStreamProvider;
}
// resolve method looks for the class file using <code>ClassStreamResolver</code> instance and tries to briefly scan it just for minimal information necessary
// minimal information includes: identification of the class as interface, obtaining its superclass name and identification of all potential interfaces (to avoid unnecessary future resolutions of them)
// empty ClInfo is stored in case of an exception to avoid repeated scanning failures
@Override
public ClassHierarchyResolver.ClassHierarchyInfo getClassInfo(ClassDesc classDesc) {
//using NOPE to distinguish between null value and non-existent record in the cache
//this code is on JDK bootstrap critical path, so cannot use lambdas here
var res = resolvedCache.getOrDefault(classDesc, NOPE);
if (res == NOPE) {
res = null;
var ci = streamProvider.apply(classDesc);
if (ci != null) {
try (var in = new DataInputStream(new BufferedInputStream(ci))) {
in.skipBytes(8);
int cpLength = in.readUnsignedShort();
String[] cpStrings = new String[cpLength];
int[] cpClasses = new int[cpLength];
for (int i=1; i<cpLength; i++) {
switch (in.readUnsignedByte()) {
case 1 -> cpStrings[i] = in.readUTF();
case 7 -> cpClasses[i] = in.readUnsignedShort();
case 8, 16, 19, 20 -> in.skipBytes(2);
case 15 -> in.skipBytes(3);
case 3, 4, 9, 10, 11, 12, 17, 18 -> in.skipBytes(4);
case 5, 6 -> {in.skipBytes(8); i++;}
}
var ci = streamProvider.apply(classDesc);
if (ci == null) return null;
try (var in = new DataInputStream(new BufferedInputStream(ci))) {
in.skipBytes(8);
int cpLength = in.readUnsignedShort();
String[] cpStrings = new String[cpLength];
int[] cpClasses = new int[cpLength];
for (int i = 1; i < cpLength; i++) {
int tag;
switch (tag = in.readUnsignedByte()) {
case TAG_UTF8 -> cpStrings[i] = in.readUTF();
case TAG_CLASS -> cpClasses[i] = in.readUnsignedShort();
case TAG_STRING, TAG_METHODTYPE, TAG_MODULE, TAG_PACKAGE -> in.skipBytes(2);
case TAG_METHODHANDLE -> in.skipBytes(3);
case TAG_INTEGER, TAG_FLOAT, TAG_FIELDREF, TAG_METHODREF, TAG_INTERFACEMETHODREF,
TAG_NAMEANDTYPE, TAG_CONSTANTDYNAMIC, TAG_INVOKEDYNAMIC -> in.skipBytes(4);
case TAG_LONG, TAG_DOUBLE -> {
in.skipBytes(8);
i++;
}
boolean isInterface = (in.readUnsignedShort() & 0x0200) != 0;
in.skipBytes(2);
int superIndex = in.readUnsignedShort();
var superClass = superIndex > 0 ? ClassDesc.ofInternalName(cpStrings[cpClasses[superIndex]]) : null;
res = new ClassHierarchyInfo(classDesc, isInterface, superClass);
int interfCount = in.readUnsignedShort();
for (int i=0; i<interfCount; i++) {
//all listed interfaces are cached without resolution
var intDesc = ClassDesc.ofInternalName(cpStrings[cpClasses[in.readUnsignedShort()]]);
resolvedCache.put(intDesc, new ClassHierarchyResolver.ClassHierarchyInfo(intDesc, true, null));
}
} catch (Exception ignore) {
//ignore
default -> throw new IllegalStateException("Bad tag (" + tag + ") at index (" + i + ")");
}
}
//null ClassHierarchyInfo value is also cached to avoid repeated resolution attempts
resolvedCache.put(classDesc, res);
boolean isInterface = (in.readUnsignedShort() & ACC_INTERFACE) != 0;
in.skipBytes(2);
int superIndex = in.readUnsignedShort();
var superClass = superIndex > 0 ? ClassDesc.ofInternalName(cpStrings[cpClasses[superIndex]]) : null;
return new ClassHierarchyInfoImpl(superClass, isInterface);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
return res;
}
}
public static final class StaticClassHierarchyResolver implements ClassHierarchyResolver {
private static final ClassHierarchyInfo CHI_Object =
new ClassHierarchyInfo(ConstantDescs.CD_Object, false, null);
private final Map<ClassDesc, ClassHierarchyInfo> map;
public StaticClassHierarchyResolver(Collection<ClassDesc> interfaceNames, Map<ClassDesc, ClassDesc> classToSuperClass) {
map = HashMap.newHashMap(interfaceNames.size() + classToSuperClass.size() + 1);
map.put(ConstantDescs.CD_Object, CHI_Object);
map.put(CD_Object, ClassHierarchyInfoImpl.OBJECT_INFO);
for (var e : classToSuperClass.entrySet())
map.put(e.getKey(), new ClassHierarchyInfo(e.getKey(), false, e.getValue()));
map.put(e.getKey(), ClassHierarchyInfo.ofClass(e.getValue()));
for (var i : interfaceNames)
map.put(i, new ClassHierarchyInfo(i, true, null));
map.put(i, ClassHierarchyInfo.ofInterface());
}
@Override
@ -185,4 +215,39 @@ public final class ClassHierarchyImpl {
return map.get(classDesc);
}
}
public static final class ClassLoadingClassHierarchyResolver implements ClassHierarchyResolver {
public static final Function<ClassDesc, Class<?>> SYSTEM_CLASS_PROVIDER = new Function<>() {
@Override
public Class<?> apply(ClassDesc cd) {
try {
return Class.forName(Util.toBinaryName(cd.descriptorString()), false, ClassLoader.getSystemClassLoader());
} catch (ClassNotFoundException ex) {
return null;
}
}
};
private final Function<ClassDesc, Class<?>> classProvider;
public ClassLoadingClassHierarchyResolver(Function<ClassDesc, Class<?>> classProvider) {
this.classProvider = classProvider;
}
@Override
public ClassHierarchyInfo getClassInfo(ClassDesc cd) {
if (!cd.isClassOrInterface())
return null;
if (cd.equals(CD_Object))
return ClassHierarchyInfo.ofClass(null);
var cl = classProvider.apply(cd);
if (cl == null) {
return null;
}
return cl.isInterface() ? ClassHierarchyInfo.ofInterface()
: ClassHierarchyInfo.ofClass(cl.getSuperclass().describeConstable().orElseThrow());
}
}
}

View file

@ -32,7 +32,7 @@ import jdk.internal.classfile.ClassHierarchyResolver;
import jdk.internal.classfile.Classfile;
import jdk.internal.classfile.constantpool.Utf8Entry;
import static jdk.internal.classfile.ClassHierarchyResolver.DEFAULT_CLASS_HIERARCHY_RESOLVER;
import static jdk.internal.classfile.ClassHierarchyResolver.defaultResolver;
public class Options {
@ -52,7 +52,7 @@ public class Options {
public Boolean fixJumps = true;
public Boolean patchCode = true;
public Boolean filterDeadLabels = false;
public ClassHierarchyResolver classHierarchyResolver = DEFAULT_CLASS_HIERARCHY_RESOLVER;
public ClassHierarchyResolver classHierarchyResolver = defaultResolver();
public Function<Utf8Entry, AttributeMapper<?>> attributeMapper = new Function<>() {
@Override
public AttributeMapper<?> apply(Utf8Entry k) {

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -84,9 +84,26 @@ public class Util {
return count;
}
public static String toClassString(String desc) {
//TODO: this doesn't look right L ... ;
return desc.replace('/', '.');
/**
* Convert a descriptor of classes or interfaces or arrays, or an internal
* name of a class or interface, into a fully qualified binary name, that can
* be resolved by {@link Class#forName(String) Class::forName}. Primitive type
* descriptors should never be passed into this method.
*
* @param descOrInternalName a descriptor or internal name
* @return the fully qualified binary name
*/
public static String toBinaryName(String descOrInternalName) {
if (descOrInternalName.startsWith("L")) {
// descriptors of classes or interfaces
if (descOrInternalName.length() <= 2 || !descOrInternalName.endsWith(";")) {
throw new IllegalArgumentException(descOrInternalName);
}
return descOrInternalName.substring(1, descOrInternalName.length() - 1).replace('/', '.');
} else {
// arrays, classes or interfaces' internal names
return descOrInternalName.replace('/', '.');
}
}
public static Iterator<String> parameterTypes(String s) {

View file

@ -105,7 +105,7 @@ public final class VerifierImpl {
static final int MAX_CODE_SIZE = 65535;
public static List<VerifyError> verify(ClassModel classModel, Consumer<String> logger) {
return verify(classModel, ClassHierarchyResolver.DEFAULT_CLASS_HIERARCHY_RESOLVER, logger);
return verify(classModel, ClassHierarchyResolver.defaultResolver(), logger);
}
public static List<VerifyError> verify(ClassModel classModel, ClassHierarchyResolver classHierarchyResolver, Consumer<String> logger) {

View file

@ -25,6 +25,7 @@
import java.lang.constant.ClassDesc;
import java.lang.constant.ConstantDescs;
import java.lang.constant.MethodTypeDesc;
import java.lang.invoke.MethodHandles;
import java.util.HashSet;
import java.util.Set;
@ -33,7 +34,10 @@ import java.util.LinkedList;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import jdk.internal.classfile.ClassElement;
import jdk.internal.classfile.ClassHierarchyResolver;
import jdk.internal.classfile.ClassModel;
import jdk.internal.classfile.ClassTransform;
import jdk.internal.classfile.Classfile;
@ -107,7 +111,7 @@ class PackageSnippets {
ClassModel cm = Classfile.parse(bytes);
Set<ClassDesc> dependencies =
cm.elementStream()
.flatMap(ce -> ce instanceof MethodMethod mm ? mm.elementStream() : Stream.empty())
.flatMap(ce -> ce instanceof MethodModel mm ? mm.elementStream() : Stream.empty())
.flatMap(me -> me instanceof CodeModel com ? com.elementStream() : Stream.empty())
.<ClassDesc>mapMulti((xe, c) -> {
switch (xe) {
@ -303,4 +307,11 @@ class PackageSnippets {
.andThen(instrumentorClassRemapper)))));
}
// @end
void resolverExample() {
// @start region="lookup-class-hierarchy-resolver"
MethodHandles.Lookup lookup = MethodHandles.lookup(); // @replace regex="MethodHandles\.lookup\(\)" replacement="..."
ClassHierarchyResolver resolver = ClassHierarchyResolver.ofClassLoading(lookup).cached();
// @end
}
}