class: center, middle .vcenter[ ## How # Java Bytecode Instrumentation ## can be safe and user-friendly ] --- ## Java 5 (2004) ``` java -javaagent:myjavaagent.jar -jar myapp.jar ```
``` class MyClassFileTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, byte[] classBytes) { ... } } ``` ??? Java 5 introduced the concept of a "java agent". Before this, agents had to be written in C, using the JVM Profiler Interface. This opened up a whole new world of writing Java to instrument Java. The way this worked (and still works to this day) is that you need to register your java agent on the command line using the -javaagent arg. In your java agent code, you can then register ClassFileTransformers with the JVM. And any time the JVM loads a class, it gives your ClassFileTransformer a chance to modify the class bytes before it loads the class. What can you do with this? Well, you can do whatever you want, and completely manipulate an application's code in any way that you want. So writing managed Java code to instrument Java is certainly saf-er and more user-friendly compared to writing it in C. But that was a low bar. And the "do whatever you want" with the class bytes? Yeah, that's not safe *or* user-friendly. --- ## ASM (2002) ``` class MyClassFileTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, byte[] classBytes) { ClassReader reader = new ClassReader(classBytes); ClassWriter writer = new ClassWriter(); MyClassVisitor visitor = new MyClassVisitor(writer); reader.accept(visitor); return writer.toByteArray(); } } class MyClassVisitor extends ClassVisitor { ... } class MyMethodVisitor extends MethodVisitor { ... } ``` ??? So.. ASM to the rescue! Talk through the code. You may have noticed, ASM predates Java 5, because people were already playing with bytecode transformation in various ways before Java added the JVM-wide ClassFileTransformer hook. --- ## ASM (2002) ``` class MyClassVisitor extends ClassVisitor { public MethodVisitor visitMethod(String name, String descriptor) { int numArgs = Type.getArgumentTypes(desc).length; return new MyMethodVisitor(super.visitMethod(name, descriptor), numArgs); } } class MyMethodVisitor extends MethodVisitor { int numArgs; public void visitCode() { super.visitCode(); for (int i = 0; i < numArgs; i++) { visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); visitVarInsn(ALOAD, 1); visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V"); } } } ``` ??? Talk through the code. Btw, I'd hardly call this writing Java to instrument Java. It's really more like using writing Bytecode to instrument Java. Also, I promised safety. This is definitely not that either. --- ## AspectJ 5 (2004) ``` @Pointcut("execution(" + "void javax.servlet.Servlet.service(" + "javax.servlet.ServletRequest," + "javax.servlet.ServletResponse)" + ")") void servletPointcut() {} @Around("servletPointcut()" + " && !cflowbelow(servletPointcut())" + " && target(target)" + " && args(request, response)") public Object aroundTopLevelServletPointcut(ProceedingJoinPoint joinPoint, Object target, ServletRequest request, ServletResponse request) { return joinPoint.proceed(); } ``` ??? AspectJ tackled this issue of writing real Java to instrument Java. Talk through the code. --- ## Glowroot APM (2012) ``` @Pointcut(className = "javax.servlet.Servlet", methodName = "service", methodParameterTypes = { "javax.servlet.ServletRequest", "javax.servlet.ServletResponse"} nestingGroup = "servlet") class ServletAdvice { @OnBefore static Span onBefore(@BindParameter ServletRequest request, ThreadContext context) { ... } @OnReturn static void onReturn(@BindParameter ServletRequest request, @BindParameter ServletResponse response, @BindTraveler Span span, ThreadContext context) { ... } @OnThrow static void onThrow(@BindParameter ServletRequest request, @BindParameter ServletResponse response, @BindThrowable Throwable throwable, @BindTraveler Span span, ThreadContext context) { ... } } ``` ??? Full disclosure here. The other projects I'm talking about today are super successful. This project is only moderately successful at best. It's only here because I wrote it, and like to talk about it :-). And because it shows that several people were thinking about these problems. So, Glowroot is a Java APM tool, and so a big part of that is instrumentation. Talk through the code. --- ## ByteBuddy (2016) ``` builder.type(hasSuperType(named("javax.servlet.Servlet"))) .transform(new ForAdvice() .withExceptionHandler(...) .advice(named("service") .and(takesArgument(0, named("javax.servlet.ServletRequest"))) .and(takesArgument(1, named("javax.servlet.ServletResponse"))), "ServletAdvice")); class ServletAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) static Span onEnter(@Advice.Argument(0) ServletRequest request) { ... } @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) static void onExit(@Advice.Argument(1) ServletResponse response, @Advice.Thrown Throwable throwable, @Advice.Enter Span span) { ... } } ``` ??? So, ByteBuddy really moved the Java bytecode instrumentation story forward. It provided similar ease of use to Glowroot's embedded instrumentation engine, in a very flexible, general purpose instrumentation library. Talk through the code. --- class: center, middle .vcenter[ # User-friendly ☑
# But is it safe? ] --- ## What could go wrong here? ``` builder.type(hasSuperType(named("javax.servlet.Servlet"))) .transform(new ForAdvice() .withExceptionHandler(...) .advice(named("service") .and(takesArgument(0, named("javax.servlet.ServletRequest"))) .and(takesArgument(1, named("javax.servlet.ServletResponse"))), "ServletAdvice")); class ServletAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) static Span onEnter(@Advice.Argument(0) ServletRequest request) { String userAgent = request.getHeader("User-Agent"); ... } ... } ``` ??? Gets to general issue of dealing with different versions of instrumented library --- ## Library shape matcher (Datadog 2018) ``` builder.type(hasSuperType(named("javax.servlet.Servlet"))) .and(new LibraryShapeMatcher()) .transform(new ForAdvice() .withExceptionHandler(...) .advice(named("service") .and(takesArgument(0, named("javax.servlet.ServletRequest"))) .and(takesArgument(1, named("javax.servlet.ServletResponse"))), "ServletAdvice")); class ServletAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) static Span onEnter(@Advice.Argument(0) ServletRequest request) { String userAgent = request.getHeader("User-Agent"); ... } ... // auto-generated at build time static Matcher getLibraryShapeMatcher() { ... } } ``` ??? Talk through the code. auto-generated using ASM of course! --- ## OpenTelemetry Java Auto-Instrumentation (2020) .libraries[ | | | | | |---------------------------|--------------------|---------------------------|----------------------| | Akka HTTP | 10.0+ | JSP | 2.3+ | | Apache HttpAsyncClient | 4.0+ | Kafka | 0.11+ | | Apache HttpClient | 2.0+ | Lettuce | 4.0+ | | AWS SDK | 1.11.x and 2.2.0+ | Log4j | 1.1+ | | Cassandra Driver | 3.0+ | Logback | 1.0+ | | Couchbase Client | 2.0+ (not 3.x yet) | MongoDB Drivers | 3.3+ | | Dropwizard Views | 0.7+ | Netty | 3.8+ | | Elasticsearch API | 2.0+ (not 7.x yet) | OkHttp | 3.0+ | | Elasticsearch REST Client | 5.0+ | Play | 2.3+ (not 2.8.x yet) | | Finatra | 2.9+ | Play WS | 1.0+ | | Geode Client | 1.4+ | RabbitMQ Client | 2.7+ | | Google HTTP Client | 1.19+ | Ratpack | 1.5+ | | Grizzly | 2.0+ | Reactor | 3.1+ | | gRPC | 1.5+ | Rediscala | 1.8+ | | Hibernate | 3.3+ | RMI | Java 7+ | | HttpURLConnection | Java 7+ | RxJava | 1.0+ | | Hystrix | 1.4+ | Servlet | 2.3+ | | java.util.logging | Java 7+ | Spark Web Framework | 2.3+ | | JAX-RS | 0.5+ | Spring Data | 1.8+ | | JAX-RS Client | 2.0+ | Spring Scheduling | 3.1+ | | JDBC | Java 7+ | Spring Servlet MVC | 3.1+ | | Jedis | 1.4+ | Spring Webflux | 5.0+ | | Jetty | 8.0+ | Spymemcached | 2.12+ | | JMS | 1.1+ | Twilio | 6.6+ | ] ??? Testing --- class: center, middle .vcenter[ ## More topics # Test harness # Shading # Helper classes ] --- class: center, middle .vcenter[ ## (the end) # Questions ]