-
Notifications
You must be signed in to change notification settings - Fork 347
Expand file tree
/
Copy pathHelperScanner.java
More file actions
322 lines (285 loc) · 10.2 KB
/
Copy pathHelperScanner.java
File metadata and controls
322 lines (285 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package datadog.trace.agent.tooling;
import datadog.trace.bootstrap.Constants;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.jar.asm.ClassReader;
import net.bytebuddy.jar.asm.ClassVisitor;
import net.bytebuddy.jar.asm.FieldVisitor;
import net.bytebuddy.jar.asm.Handle;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;
import net.bytebuddy.jar.asm.Type;
/** Scans helper classes to find what classes they depend on and what order to load them. */
public final class HelperScanner extends ClassVisitor {
static final int READER_OPTIONS = ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
final ClassFileLocator locator;
final MethodScanner methodScanner = new MethodScanner();
final Consumer<String> REQUIRES = this::requiresClass;
final Consumer<String> USES = this::usesClass;
final Map<String, Set<String>> classGraph = new LinkedHashMap<>();
final Set<String> search = new HashSet<>();
final Set<String> visited = new HashSet<>();
String className;
Set<String> requires;
Set<String> uses;
HelperScanner() {
this(ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader()));
}
HelperScanner(ClassFileLocator locator) {
super(Opcodes.ASM7, null);
this.locator = locator;
}
/**
* Expands helper class names with their non-bootstrap dependencies, via the agent class loader.
*/
public static String[] withClassDependencies(String... helperClassNames) {
return new HelperScanner().simulateClassLoading(helperClassNames);
}
/**
* Same as above, but reads bytecode via the given locator (e.g. during build time where the agent
* loader is absent).
*/
public static String[] withClassDependencies(
ClassFileLocator locator, String... helperClassNames) {
return new HelperScanner(locator).simulateClassLoading(helperClassNames);
}
/**
* Simulates class-loading by finding all classes required to load the helper classes as well as
* optional classes used in method instructions that may be needed later when invoking the method.
* Classes are arranged in order of loading to satisfy the constraints of {@link HelperInjector}.
*
* <p>Bootstrap types are not included in the list.
*/
String[] simulateClassLoading(String... helperClassNames) {
Deque<String> workQueue = new ArrayDeque<>();
for (String className : helperClassNames) {
workQueue.addLast(className);
// keep root names in the final list even if they're not loadable at this point
classGraph.put(className, Collections.emptySet());
}
// scan each class in turn, adding new types to the work queue
while ((className = workQueue.pollFirst()) != null) {
if (visited.add(className)) {
try {
byte[] bytecode = locator.locate(className).resolve();
requires = new LinkedHashSet<>();
uses = new LinkedHashSet<>();
new ClassReader(bytecode).accept(this, READER_OPTIONS);
classGraph.put(className, requires);
uses.removeAll(visited);
workQueue.addAll(uses);
} catch (Throwable ignore) {
}
}
}
visited.clear();
for (String className : classGraph.keySet()) {
removeCycles(className);
}
// load types without any dependencies, then load those satisfied by what's loaded so far...
// (this assumes that the class graph has had cycles removed and is a directed acyclic graph)
Set<String> loaded = new LinkedHashSet<>();
while (!classGraph.isEmpty()) {
boolean unchanged = true;
Iterator<Map.Entry<String, Set<String>>> itr = classGraph.entrySet().iterator();
while (itr.hasNext()) {
Map.Entry<String, Set<String>> node = itr.next();
if (loaded.containsAll(node.getValue())) {
loaded.add(node.getKey());
itr.remove();
unchanged = false;
}
}
if (unchanged) {
throw new IllegalStateException("Unable to resolve load order for: " + classGraph);
}
}
return loaded.toArray(new String[0]);
}
/** Simple depth-first search to make sure we end up with a directed acyclic graph. */
void removeCycles(String className) {
if (visited.add(className)) {
search.add(className);
Iterator<String> itr = classGraph.get(className).iterator();
while (itr.hasNext()) {
String nextName = itr.next();
if (search.contains(nextName) // cycle detected, remove link to break it
|| !classGraph.containsKey(nextName) // remove any non-loadable types
|| nextName.startsWith(className + "$")) { // skip links to inner types
itr.remove();
} else {
removeCycles(nextName);
}
}
search.remove(className);
}
}
/** Types that contribute to the helper class shape/hierarchy are required at load-time. */
@Override
public void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces) {
record(superName, REQUIRES);
record(interfaces, REQUIRES);
}
@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
if (this.className.equals(name)) {
record(outerName, REQUIRES);
}
}
@Override
public FieldVisitor visitField(
final int access,
final String name,
final String descriptor,
final String signature,
final Object value) {
// Field types are resolved lazily by the JVM, not during defineClass.
// Using USES (not REQUIRES) avoids false dependency cycles that can break
// topological sort ordering for superclass/interface relationships.
record(Type.getType(descriptor), USES);
return null;
}
@Override
public MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions) {
// Method parameter/return types and declared exceptions are resolved lazily
// by the JVM, not during defineClass. Only superclass and interfaces are
// eagerly resolved, which are handled by visit().
record(Type.getMethodType(descriptor), USES);
record(exceptions, USES);
return methodScanner;
}
/** Attempts to find all types used in method instructions by the helper class. */
class MethodScanner extends MethodVisitor {
MethodScanner() {
super(Opcodes.ASM7, null);
}
@Override
public void visitFieldInsn(
final int opcode, final String owner, final String name, final String descriptor) {
record(Type.getObjectType(owner), USES);
record(Type.getType(descriptor), USES);
}
@Override
public void visitMethodInsn(
final int opcode,
final String owner,
final String name,
final String descriptor,
final boolean isInterface) {
record(Type.getObjectType(owner), USES);
record(Type.getMethodType(descriptor), USES);
}
@Override
public void visitTypeInsn(final int opcode, final String type) {
record(Type.getObjectType(type), USES);
}
@Override
public void visitInvokeDynamicInsn(
String name,
String descriptor,
Handle bootstrapMethodHandle,
Object... bootstrapMethodArguments) {
record(Type.getType(descriptor), USES);
record(bootstrapMethodHandle, USES);
for (Object value : bootstrapMethodArguments) {
if (value instanceof Type) {
record((Type) value, USES);
} else if (value instanceof Handle) {
record((Handle) value, USES);
}
}
}
@Override
public void visitLdcInsn(final Object value) {
if (value instanceof Type) {
record((Type) value, USES);
} else if (value instanceof Handle) {
record((Handle) value, USES);
}
}
}
/** Marks a class as required; the helper won't load if this class hasn't been loaded first. */
void requiresClass(String className) {
requires.add(className);
uses.add(className);
}
/** Marks a class as used; the helper doesn't need it at load time but may use it when called. */
void usesClass(String className) {
uses.add(className);
}
void record(Type type, Consumer<String> action) {
if (null != type) {
while (type.getSort() == Type.ARRAY) {
type = type.getElementType();
}
if (type.getSort() == Type.METHOD) {
record(type.getArgumentTypes(), action);
record(type.getReturnType(), action);
} else if (type.getSort() == Type.OBJECT) {
String className = type.getClassName();
// ignore types that we expect to be on the boot-class-path
if (this.className.equals(className)
|| className.startsWith("java.")
|| className.startsWith("javax.")
|| className.startsWith("jdk.")
|| className.startsWith("com.sun.")
|| className.startsWith("sun.")
|| className.startsWith("org.slf4j.")
|| className.startsWith("datadog.slf4j.")) {
return;
}
for (String prefix : Constants.BOOTSTRAP_PACKAGE_PREFIXES) {
if (className.startsWith(prefix)) {
return;
}
}
action.accept(className);
}
}
}
void record(Type[] types, Consumer<String> action) {
if (null != types) {
for (Type t : types) {
record(t, action);
}
}
}
void record(Handle handle, Consumer<String> action) {
if (null != handle) {
record(Type.getObjectType(handle.getOwner()), action);
record(Type.getType(handle.getDesc()), action);
}
}
void record(String internalName, Consumer<String> action) {
if (null != internalName) {
record(Type.getObjectType(internalName), action);
}
}
void record(String[] internalNames, Consumer<String> action) {
if (null != internalNames) {
for (String n : internalNames) {
record(Type.getObjectType(n), action);
}
}
}
}