TraceAspect.java
1 |
package com.goldencode.csvtrace; |
---|---|
2 |
import java.io.*; |
3 |
import java.lang.reflect.Field; |
4 |
import java.nio.file.*; |
5 |
import java.util.*; |
6 |
import java.util.concurrent.atomic.*; |
7 |
|
8 |
import org.apache.commons.lang.builder.*; |
9 |
import org.aspectj.lang.*; |
10 |
import org.aspectj.lang.annotation.*; |
11 |
|
12 |
/**
|
13 |
* To use:
|
14 |
* 1. Copy this class to the project at com/goldencode/csvtrace.
|
15 |
* 2. Rebuild with AspectJ.
|
16 |
* 3. When the app is started look for csvtrace.stop in the working dir.
|
17 |
* 4. Modify csvtrace.stop at your liking.
|
18 |
* 5. Rename csvtrace.stop to csvtrace.start to activate tracing.
|
19 |
* 6. Rename csvtrace.start to csvtrace.stop or stop the app to disable tracing.
|
20 |
* 7. The trace output is saved to csvtrace.csv in the working dir.
|
21 |
* 8. When no longer needed, delete this class from your project.
|
22 |
*
|
23 |
* This thing is WIP.
|
24 |
*/
|
25 |
@Aspect
|
26 |
public class TraceAspect |
27 |
{ |
28 |
private static final String CTRL_FILE_START = "csvtrace.start"; |
29 |
private static final String CTRL_FILE_STOP = "csvtrace.stop"; |
30 |
private static final String CSV_FILE = "csvtrace.csv"; |
31 |
|
32 |
private static boolean traceActive = false; |
33 |
private static AtomicReference<List<String>> includes = new AtomicReference<>(); |
34 |
private static AtomicReference<List<String>> excludes = new AtomicReference<>(); |
35 |
private static BufferedWriter csvFileWriter = null; |
36 |
|
37 |
static
|
38 |
{ |
39 |
start(); |
40 |
} |
41 |
|
42 |
@Before("target(o) && execution(* com.goldencode..*(..))&& !cflow(within(TraceAspect))") |
43 |
public void traceMethods(JoinPoint joinPoint, Object o) |
44 |
{ |
45 |
if (!traceActive)
|
46 |
{ |
47 |
return;
|
48 |
} |
49 |
|
50 |
List<String> incs = includes.get(); |
51 |
if (incs == null || incs.isEmpty()) |
52 |
{ |
53 |
return;
|
54 |
} |
55 |
|
56 |
List<String> excs = excludes.get(); |
57 |
|
58 |
Signature sig = joinPoint.getStaticPart().getSignature();
|
59 |
Class<?> declaring = sig.getDeclaringType();
|
60 |
String declaringName = declaring.getCanonicalName();
|
61 |
if (declaringName == null) |
62 |
{ |
63 |
return;
|
64 |
} |
65 |
|
66 |
// qualified class name + method name
|
67 |
declaringName = declaringName + "." + sig.getName();
|
68 |
|
69 |
// match the declaring class against the list of excludes
|
70 |
boolean isMatch = false; |
71 |
for (String exclude : excs) |
72 |
{ |
73 |
if (exclude.endsWith("*")) |
74 |
{ |
75 |
if (declaringName.startsWith(exclude.substring(0, exclude.length() - 1))) |
76 |
{ |
77 |
isMatch = true;
|
78 |
break;
|
79 |
} |
80 |
} |
81 |
else
|
82 |
{ |
83 |
if (declaringName.equals(exclude))
|
84 |
{ |
85 |
isMatch = true;
|
86 |
break;
|
87 |
} |
88 |
} |
89 |
} |
90 |
|
91 |
if (isMatch)
|
92 |
{ |
93 |
// exclude matched - exit
|
94 |
return;
|
95 |
} |
96 |
|
97 |
// match the declaring class against the list of includes
|
98 |
for (String include : incs) |
99 |
{ |
100 |
if (include.endsWith("*")) |
101 |
{ |
102 |
if (declaringName.startsWith(include.substring(0, include.length() - 1))) |
103 |
{ |
104 |
isMatch = true;
|
105 |
break;
|
106 |
} |
107 |
} |
108 |
else
|
109 |
{ |
110 |
if (declaringName.equals(include))
|
111 |
{ |
112 |
isMatch = true;
|
113 |
break;
|
114 |
} |
115 |
} |
116 |
} |
117 |
|
118 |
if (!isMatch)
|
119 |
{ |
120 |
return;
|
121 |
} |
122 |
|
123 |
Object[] args = joinPoint.getArgs(); |
124 |
|
125 |
createCSVEntry(declaringName, o, args); |
126 |
} |
127 |
|
128 |
public void createCSVEntry(String declaringName, Object ref, Object[] args) |
129 |
{ |
130 |
long nanos = System.nanoTime(); |
131 |
StringBuilder str = new StringBuilder(); |
132 |
str.append(nanos).append(",");
|
133 |
str.append(Thread.currentThread().getName()).append(","); |
134 |
|
135 |
if (ref != null) |
136 |
{ |
137 |
str.append(declaringName).append("(")
|
138 |
.append(ref.getClass().getSimpleName()) |
139 |
.append(")").append(","); |
140 |
} |
141 |
else
|
142 |
{ |
143 |
str.append(declaringName).append(",");
|
144 |
} |
145 |
|
146 |
str.append("@");
|
147 |
if (ref == null) |
148 |
{ |
149 |
// do the same length as a valid hash code for easier grep
|
150 |
str.append("00000000");
|
151 |
} |
152 |
else
|
153 |
{ |
154 |
str.append(Integer.toHexString(System.identityHashCode(ref))); |
155 |
} |
156 |
|
157 |
str.append(",");
|
158 |
|
159 |
// serialize the arguments
|
160 |
for (Object arg : args) |
161 |
{ |
162 |
String argStr;
|
163 |
if (arg != null) |
164 |
{ |
165 |
Class<?> argClass = arg.getClass();
|
166 |
if (isPrimitiveOrWrapper(argClass))
|
167 |
{ |
168 |
argStr = arg.toString(); |
169 |
} |
170 |
else
|
171 |
{ |
172 |
argStr = toStringReflective(arg, argClass); |
173 |
} |
174 |
} |
175 |
else
|
176 |
{ |
177 |
argStr = "<null>";
|
178 |
} |
179 |
|
180 |
// escape control chars
|
181 |
argStr = argStr.replace("\"", "\"\""); |
182 |
argStr = "\"" + argStr + "\""; |
183 |
|
184 |
str.append(argStr).append(",");
|
185 |
} |
186 |
|
187 |
String out = str.toString();
|
188 |
synchronized (this) |
189 |
{ |
190 |
try
|
191 |
{ |
192 |
csvFileWriter.write(out); |
193 |
csvFileWriter.newLine(); |
194 |
csvFileWriter.flush(); |
195 |
} |
196 |
catch (IOException e) |
197 |
{ |
198 |
e.printStackTrace(); |
199 |
} |
200 |
} |
201 |
} |
202 |
|
203 |
private static void start() |
204 |
{ |
205 |
String workDir = System.getProperty("user.dir"); |
206 |
Path path = FileSystems.getDefault().getPath(workDir, CSV_FILE); |
207 |
File csvFile = path.toFile();
|
208 |
try
|
209 |
{ |
210 |
boolean writeColumns = false; |
211 |
if (!csvFile.exists())
|
212 |
{ |
213 |
writeColumns = true;
|
214 |
} |
215 |
|
216 |
csvFileWriter = new BufferedWriter(new FileWriter(csvFile, true)); |
217 |
|
218 |
if (writeColumns)
|
219 |
{ |
220 |
csvFileWriter.write("Nanos, Thread, Method, Hashcode, Arg1, Arg2, Arg3, Arg4, Arg5");
|
221 |
csvFileWriter.newLine(); |
222 |
csvFileWriter.flush(); |
223 |
} |
224 |
} |
225 |
catch (IOException e1) |
226 |
{ |
227 |
e1.printStackTrace(); |
228 |
return;
|
229 |
} |
230 |
|
231 |
path = FileSystems.getDefault().getPath(workDir, CTRL_FILE_START); |
232 |
File startFile = path.toFile();
|
233 |
path = FileSystems.getDefault().getPath(workDir, CTRL_FILE_STOP); |
234 |
File stopFile = path.toFile();
|
235 |
if (!startFile.exists() && !stopFile.exists())
|
236 |
{ |
237 |
// create the default file
|
238 |
try (BufferedWriter writer = new BufferedWriter(new FileWriter(stopFile))) |
239 |
{ |
240 |
writer.write("# rename this file to csvtrace.start to start tracing, or csvtrace.stop to stop tracing");
|
241 |
writer.newLine(); |
242 |
writer.write("# my.name* - all methods of all classes in all the namespaces beginning with 'my.name'");
|
243 |
writer.newLine(); |
244 |
writer.write("# my.namespace.* - all methods of all classes in the namespace 'my.namespace' including subspaces");
|
245 |
writer.newLine(); |
246 |
writer.write("# my.namespace.MyClass* - all methods of class names beginning with 'my.namespace.MyClass'");
|
247 |
writer.newLine(); |
248 |
writer.write("# my.namespace.MyClass.* - all methods of class name equal to 'my.namespace.MyClass'");
|
249 |
writer.newLine(); |
250 |
writer.write("# my.namespace.MyClass.foo* - all methods of class 'my.namespace.MyClass' beginning with 'foo'");
|
251 |
writer.newLine(); |
252 |
writer.write("# my.namespace.MyClass.fooBar - the method 'fooBar' of class 'my.namespace.MyClass'");
|
253 |
writer.newLine(); |
254 |
writer.write("!java.*");
|
255 |
writer.newLine(); |
256 |
writer.write("!javax.*");
|
257 |
writer.newLine(); |
258 |
writer.write("# make sure to apply some filters in the form above and remove the asterisk");
|
259 |
writer.write("*");
|
260 |
} |
261 |
catch (IOException e) |
262 |
{ |
263 |
e.printStackTrace(); |
264 |
throw new RuntimeException(e); |
265 |
} |
266 |
} |
267 |
|
268 |
updateState(); |
269 |
|
270 |
Thread fileWatcherThread = new Thread(new Runnable() |
271 |
{ |
272 |
|
273 |
@Override
|
274 |
public void run() |
275 |
{ |
276 |
controlFileWatcher(); |
277 |
} |
278 |
}, "csv-trace");
|
279 |
|
280 |
fileWatcherThread.setDaemon(true);
|
281 |
fileWatcherThread.start(); |
282 |
} |
283 |
|
284 |
private static void controlFileWatcher() |
285 |
{ |
286 |
WatchService watcher; |
287 |
try
|
288 |
{ |
289 |
watcher = FileSystems.getDefault().newWatchService(); |
290 |
} |
291 |
catch (IOException e) |
292 |
{ |
293 |
e.printStackTrace(); |
294 |
return;
|
295 |
} |
296 |
|
297 |
String workDir = System.getProperty("user.dir"); |
298 |
Path dir = FileSystems.getDefault().getPath(workDir); |
299 |
|
300 |
try
|
301 |
{ |
302 |
dir.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, |
303 |
StandardWatchEventKinds.ENTRY_MODIFY); |
304 |
} |
305 |
catch (IOException x) |
306 |
{ |
307 |
System.err.println(x);
|
308 |
} |
309 |
|
310 |
for (;;)
|
311 |
{ |
312 |
|
313 |
// wait for key to be signaled
|
314 |
WatchKey key; |
315 |
try
|
316 |
{ |
317 |
key = watcher.take(); |
318 |
} |
319 |
catch (InterruptedException x) |
320 |
{ |
321 |
return;
|
322 |
} |
323 |
|
324 |
for (WatchEvent<?> event : key.pollEvents())
|
325 |
{ |
326 |
WatchEvent.Kind<?> kind = event.kind(); |
327 |
|
328 |
if (kind == StandardWatchEventKinds.OVERFLOW)
|
329 |
{ |
330 |
continue;
|
331 |
} |
332 |
|
333 |
@SuppressWarnings("unchecked") |
334 |
WatchEvent<Path> ev = (WatchEvent<Path>) event; |
335 |
Path filename = ev.context(); |
336 |
|
337 |
if (filename.endsWith(CTRL_FILE_START) || filename.endsWith(CTRL_FILE_STOP))
|
338 |
{ |
339 |
updateState(); |
340 |
} |
341 |
} |
342 |
|
343 |
try
|
344 |
{ |
345 |
// sleep not to consume to much CPU in case the csv file is in the same folder as the
|
346 |
// control file
|
347 |
Thread.sleep(1000); |
348 |
} |
349 |
catch (InterruptedException e) |
350 |
{ |
351 |
break;
|
352 |
} |
353 |
|
354 |
// reset the key to receive further events
|
355 |
boolean valid = key.reset();
|
356 |
if (!valid)
|
357 |
{ |
358 |
break;
|
359 |
} |
360 |
} |
361 |
} |
362 |
|
363 |
private static void updateState() |
364 |
{ |
365 |
String workDir = System.getProperty("user.dir"); |
366 |
Path path = FileSystems.getDefault().getPath(workDir, CTRL_FILE_START); |
367 |
File startFile = path.toFile();
|
368 |
path = FileSystems.getDefault().getPath(workDir, CTRL_FILE_STOP); |
369 |
File stopFile = path.toFile();
|
370 |
|
371 |
if (stopFile.exists())
|
372 |
{ |
373 |
includes.set(null);
|
374 |
return;
|
375 |
} |
376 |
|
377 |
if (startFile.exists())
|
378 |
{ |
379 |
processControlFile(startFile); |
380 |
} |
381 |
} |
382 |
|
383 |
private static void processControlFile(File file) |
384 |
{ |
385 |
List<String> tempIncludes = new ArrayList<>(); |
386 |
List<String> tempExcludes = new ArrayList<>(); |
387 |
|
388 |
try (BufferedReader br = new BufferedReader(new FileReader(file))) |
389 |
{ |
390 |
String line = null; |
391 |
while ((line = br.readLine()) != null) |
392 |
{ |
393 |
line = line.trim(); |
394 |
if (line.startsWith("#")) |
395 |
{ |
396 |
continue;
|
397 |
} |
398 |
|
399 |
// anything else than # is considered an include pattern
|
400 |
|
401 |
line = line.trim(); |
402 |
|
403 |
if (line.startsWith("!")) |
404 |
{ |
405 |
tempExcludes.add(line.substring(1));
|
406 |
} |
407 |
else
|
408 |
{ |
409 |
tempIncludes.add(line.trim()); |
410 |
} |
411 |
} |
412 |
} |
413 |
catch (IOException e) |
414 |
{ |
415 |
e.printStackTrace(); |
416 |
} |
417 |
|
418 |
excludes.set(tempExcludes); |
419 |
includes.set(tempIncludes); |
420 |
traceActive = !tempIncludes.isEmpty(); |
421 |
} |
422 |
|
423 |
private static boolean isPrimitiveOrWrapper(Class<?> clazz) |
424 |
{ |
425 |
return clazz.isPrimitive() ||
|
426 |
clazz.equals(Boolean.class) ||
|
427 |
clazz.equals(Character.class) ||
|
428 |
clazz.equals(Short.class) ||
|
429 |
clazz.equals(Integer.class) ||
|
430 |
clazz.equals(Long.class) ||
|
431 |
clazz.equals(Float.class) ||
|
432 |
clazz.equals(Double.class) ||
|
433 |
CharSequence.class.isAssignableFrom(clazz);
|
434 |
} |
435 |
|
436 |
private static String toStringReflective(Object value, Class<?> clazz) |
437 |
{ |
438 |
if (value == null) |
439 |
{ |
440 |
return "<null>"; |
441 |
} |
442 |
|
443 |
if (isPrimitiveOrWrapper(clazz))
|
444 |
{ |
445 |
return value.toString();
|
446 |
} |
447 |
|
448 |
StringBuilder b = new StringBuilder(); |
449 |
b.append(clazz.getSimpleName()); |
450 |
b.append("@");
|
451 |
b.append(System.identityHashCode(value));
|
452 |
|
453 |
b.append("[");
|
454 |
|
455 |
boolean first = true; |
456 |
Class<?> parent = clazz;
|
457 |
while(parent != null && !parent.getClass().equals(Object.class)) |
458 |
{ |
459 |
Field[] fields = parent.getDeclaredFields(); |
460 |
for(Field f : fields) |
461 |
{ |
462 |
String fName = f.getName();
|
463 |
if (fName.startsWith("ajc$")) |
464 |
{ |
465 |
// don't care for aspectj internals
|
466 |
continue;
|
467 |
} |
468 |
|
469 |
if (!first)
|
470 |
{ |
471 |
b.append(",");
|
472 |
} |
473 |
first = false;
|
474 |
f.setAccessible(true);
|
475 |
b.append(fName); |
476 |
b.append("=");
|
477 |
Object fValue;
|
478 |
try
|
479 |
{ |
480 |
fValue = f.get(value); |
481 |
if (fValue == null) |
482 |
{ |
483 |
b.append("<null>");
|
484 |
} |
485 |
else if (isPrimitiveOrWrapper(f.getType())) |
486 |
{ |
487 |
b.append(fValue.toString()); |
488 |
} |
489 |
else
|
490 |
{ |
491 |
b.append("[" + fValue.toString() + "]"); |
492 |
} |
493 |
} |
494 |
catch (Exception e) |
495 |
{ |
496 |
b.append("<error>");
|
497 |
} |
498 |
} |
499 |
parent = parent.getSuperclass(); |
500 |
} |
501 |
|
502 |
b.append("]");
|
503 |
return b.toString();
|
504 |
} |
505 |
|
506 |
@SuppressWarnings("serial") |
507 |
private static class TwoLevelsToStringStyle extends ToStringStyle |
508 |
{ |
509 |
|
510 |
private static final int MAX_DEPTH = 1; |
511 |
|
512 |
private int depth = 0; |
513 |
|
514 |
/**
|
515 |
* <p>
|
516 |
* Constructor.
|
517 |
* </p>
|
518 |
*/
|
519 |
public TwoLevelsToStringStyle()
|
520 |
{ |
521 |
super();
|
522 |
this.setUseShortClassName(true); |
523 |
this.setUseIdentityHashCode(true); |
524 |
|
525 |
} |
526 |
|
527 |
@Override
|
528 |
public void appendDetail(StringBuffer buffer, String fieldName, Object value) |
529 |
{ |
530 |
if (depth > MAX_DEPTH)
|
531 |
{ |
532 |
return;
|
533 |
} |
534 |
|
535 |
Class<?> valueClass = value.getClass();
|
536 |
|
537 |
if (valueClass.isPrimitive() ||
|
538 |
valueClass.equals(Boolean.class) ||
|
539 |
valueClass.equals(Character.class) ||
|
540 |
valueClass.equals(Short.class) ||
|
541 |
valueClass.equals(Integer.class) ||
|
542 |
valueClass.equals(Long.class) ||
|
543 |
valueClass.equals(Float.class) ||
|
544 |
valueClass.equals(Double.class) ||
|
545 |
CharSequence.class.isAssignableFrom(valueClass))
|
546 |
{ |
547 |
super.appendDetail(buffer, fieldName, value);
|
548 |
} |
549 |
else if (accept(valueClass)) |
550 |
{ |
551 |
depth++; |
552 |
try
|
553 |
{ |
554 |
buffer.append(ToStringBuilder.reflectionToString(value, this));
|
555 |
} |
556 |
finally
|
557 |
{ |
558 |
depth--; |
559 |
} |
560 |
|
561 |
} |
562 |
} |
563 |
|
564 |
|
565 |
// @Override
|
566 |
// protected void appendDetail(StringBuffer buffer, String fieldName, Collection<?> coll)
|
567 |
// {
|
568 |
// if (depth > MAX_DEPTH)
|
569 |
// {
|
570 |
// return;
|
571 |
// }
|
572 |
//
|
573 |
// appendClassName(buffer, coll);
|
574 |
// appendIdentityHashCode(buffer, coll);
|
575 |
// appendDetail(buffer, fieldName, coll.toArray());
|
576 |
// }
|
577 |
|
578 |
/**
|
579 |
* Returns whether or not to recursively format the given <code>Class</code>. By default, this
|
580 |
* method always returns {@code true}, but may be overwritten by sub-classes to filter specific
|
581 |
* classes.
|
582 |
*
|
583 |
* @param clazz
|
584 |
* The class to test.
|
585 |
* @return Whether or not to recursively format the given <code>Class</code>.
|
586 |
*/
|
587 |
protected boolean accept(final Class<?> clazz) |
588 |
{ |
589 |
// TODO: WeakHasMap is internally used in ToStringBuilder implementation
|
590 |
// and causes infinite recursion
|
591 |
return !WeakHashMap.class.isAssignableFrom(clazz); |
592 |
} |
593 |
} |
594 |
} |