Skip to content

Commit

Permalink
Update Javassist and ASM
Browse files Browse the repository at this point in the history
  • Loading branch information
T5750 committed Feb 7, 2020
1 parent 6bc8da0 commit 79def54
Show file tree
Hide file tree
Showing 13 changed files with 345 additions and 55 deletions.
14 changes: 13 additions & 1 deletion doc/source/jdk8/asm.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ ASM is used in many projects, including:
## ASM API Basics
The ASM API provides two styles of interacting with Java classes for transformation and generation: event-based and tree-based.

### Overview

Package | Description
---|---
`org.objectweb.asm` | Provides a small and fast bytecode manipulation framework.
`org.objectweb.asm.commons` | Provides some useful class and method adapters.
`org.objectweb.asm.signature` | Provides support for type signatures.
`org.objectweb.asm.tree` | Provides an ASM visitor that constructs a tree representation of the classes it visits.
`org.objectweb.asm.tree.analysis` | Provides a framework for static code analysis based on the asm.tree package.
`org.objectweb.asm.util` | Provides ASM visitors that can be useful for programming and debugging purposes.

### Event-based API
This API is heavily **based on the Visitor pattern** and is **similar in feel to the SAX parsing model** of processing XML documents. It is comprised, at its core, of the following components:
- `ClassReader` – helps to read class files and is the beginning of transforming a class
Expand Down Expand Up @@ -142,4 +153,5 @@ java -javaagent:jdk8.jar -cp . t5750.asm.instrument.PremainTest

## References
- [A Guide to Java Bytecode Manipulation with ASM](https://www.baeldung.com/java-asm)
- [ASM](https://asm.ow2.io/)
- [ASM](https://asm.ow2.io/)
- [ASM 6 Developer Guide](https://asm.ow2.io/developer-guide.html)
3 changes: 2 additions & 1 deletion doc/source/jdk8/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ Java™ Platform Standard Ed. 8
Lambda
javap
asm
javassistTutorial
javassistTutorial
javassistLog
109 changes: 109 additions & 0 deletions doc/source/jdk8/javassistLog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Javassist Audit Log

With Spring and Hibernate on your stack, your application’s bytecode is likely enhanced or manipulated at runtime. Bytecode is the instruction set of the Java Virtual Machine (JVM), and all languages that run on the JVM must eventually compile down to bytecode. Bytecode is manipulated for a variety of reasons:

**Program analysis:**
- find bugs in your application
- examine code complexity
- find classes with a specific annotation

**Class generation:**
- lazy load data from a database using proxies

**Security:**
- restrict access to certain APIs
- code obfuscation

**Transforming classes without the Java source code:**
- code profiling
- code optimization

**And finally, adding logging to applications.**

There are several tools that can be used to manipulate bytecode, ranging from very low-level tools such as ASM, which require you to work at the bytecode level, to high level frameworks such as AspectJ, which allow you to write pure Java.

![javassist_several_tools](https://s1.wailian.download/2020/02/07/javassist_several_tools-min.png)

## Audit Log Example
- `BankTransactions`
- `ImportantLog`

There are two main advantages of using bytecode and annotations to perform the logging:
1. The logging is separated from the business logic, which helps keep the code clean and simple.
2. It is easy to remove the audit logging without modifying the source code.

## Where do we actually modify the bytecode?
We can use a core Java feature introduced in 1.5 to manipulate the bytecode. This feature is called a [Java agent](http://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html).

### A typical Java process
![javassist_typical_Java_process](https://s1.wailian.download/2020/02/07/javassist_typical_Java_process-min.png)

The command `java` is executed with the class containing our main method as the one input parameter. This starts a Java runtime environment, uses a `ClassLoader` to load the input class, and invokes the main method on the class.

### Java agent
![javassist_Java_agent](https://s1.wailian.download/2020/02/07/javassist_Java_agent-min.png)

The command `java` is run with two input parameters.
- The first is the JVM argument `-javaagent`, pointing to the agent jar.
- The second is the class containing our main method.

The `javaagent` flag tells the JVM to first load the agent. The agent’s main class must be specified in the manifest of the agent jar. Once the class is loaded, the premain method on the class is invoked. This premain method acts as a setup hook for the agent. It allows the agent to register a class transformer. When a class transformer is registered with the JVM, that transformer will receive the bytes of every class prior to the class being loaded in the JVM. This provides the class transformer with the opportunity to modify the bytes of a class as needed. Once the class transformer has modified the bytes, it returns the modified bytes back to the JVM. These bytes are then verified and loaded by the JVM.

```
public class JavassistAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Starting the agent");
inst.addTransformer(new ImportantLogClassTransformer());
}
}
```
The `premain` method prints out a message and then registers a class transformer. The class transformer must implement the method `transform`, which is invoked for every class loaded into the JVM. It provides the byte array of the class as input to the method, which then returns the modified byte array. If the class transformer decides not to modify the bytes of the specific class, then it can return `null`.
```
public class ImportantLogClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className,
Class classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
// manipulate the bytes here
return modified bytes;
}
}
```

## How do we modify the bytes using Javassist?
[Javassist](http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/) is a bytecode manipulation framework with both a high level and low level API.

![javassist_diagram](https://s1.wailian.download/2020/02/07/javassist_diagram-min.png)

Javassist uses a `CtClass` object to represent a class. These `CtClass` objects can be obtained from a `ClassPool` and are used to modify Classes. The `ClassPool` is a container of `CtClass` objects implemented as a `HashMap` where the key is the name of the class and the value is the `CtClass` object representing the class. The default `ClassPool` uses the same classpath as the underlying JVM. Therefore, in some cases, you may need to add classpaths or class bytes to a `ClassPool`.

Similar to a Java class which contains fields, methods, and constructors, a `CtClass` object contains `CtFields`, `CtConstructors`, and `CtMethods`. All of these objects can be modified.

Below are a few of the ways to modify a method:

![javassist_modify_method](https://s1.wailian.download/2020/02/07/javassist_modify_method-min.png)

The transform method of the Class transformer needs to perform the following steps:
1. Convert byte array to a `CtClass` object
2. Check each method of `CtClass` for the annotation `@ImportantLog`
3. If `@ImportantLog` annotation is present on the method, then
- Get important parameter method indexes
- Add logging statement to beginning of the method

### Tips
As you write Java code using Javassist, be wary of the following gotchas:
- The JVM uses slashes(`/`) between packages while Javassist uses dots(`.`).
- When inserting more than one line of Java code, the code needs to go inside brackets.
- When referencing method parameter values using `$1`, `$2`, etc, know that `$0` is reserved for `this`. This means the value of the first parameter to your method is `$1`.
- Annotations are given a visible and invisible tag. Invisible annotations cannot be seen at runtime.

```
java -javaagent:jdk8.jar -cp .;C:\Users\hero\.m2\repository\org\javassist\javassist\3.26.0-GA\javassist-3.26.0-GA.jar t5750.module.log.BankTransactions
```

### Summary
`ImportantLogClassTransformer`
- On the positive side, the amount of code written is pretty minimal and we did not actually have to write bytecode to use Javassist.
- The big drawback is that writing Java code in quotes can become tedious.

## References
- [Diving Into Bytecode Manipulation: Creating an Audit Log With ASM and Javassist](https://blog.newrelic.com/engineering/diving-bytecode-manipulation-creating-audit-log-asm-javassist/)
2 changes: 2 additions & 0 deletions jdk8/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
9. Boxing/Unboxing
10. Debug

[Javassist Audit Log](../doc/source/jdk8/javassistLog.md)

## Runtime Environment
- [Java 8](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)
- [Javassist 3.26.x](https://github.com/jboss-javassist/javassist)
Expand Down
2 changes: 1 addition & 1 deletion jdk8/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>t5750.asm.instrument.Premain</Premain-Class>
<Premain-Class>t5750.instrument.Premain</Premain-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
Expand Down
36 changes: 0 additions & 36 deletions jdk8/src/main/java/t5750/asm/instrument/Premain.java

This file was deleted.

111 changes: 111 additions & 0 deletions jdk8/src/main/java/t5750/instrument/ImportantLogClassTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package t5750.instrument;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javassist.ByteArrayClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.annotation.Annotation;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.StringMemberValue;

public class ImportantLogClassTransformer implements ClassFileTransformer {
private static final String METHOD_ANNOTATION = "t5750.module.log.annotation.ImportantLog";
private static final String ANNOTATION_ARRAY = "fields";
private ClassPool pool;

public ImportantLogClassTransformer() {
pool = ClassPool.getDefault();
}

public byte[] transform(ClassLoader loader, String className,
Class classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
try {
pool.insertClassPath(
new ByteArrayClassPath(className, classfileBuffer));
CtClass cclass = pool.get(className.replaceAll("/", "."));
if (!cclass.isFrozen()) {
for (CtMethod currentMethod : cclass.getDeclaredMethods()) {
Annotation annotation = getAnnotation(currentMethod);
if (annotation != null) {
List parameterIndexes = getParamIndexes(annotation);
currentMethod.insertBefore(createJavaString(
currentMethod, className, parameterIndexes));
}
}
return cclass.toBytecode();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

private Annotation getAnnotation(CtMethod method) {
MethodInfo mInfo = method.getMethodInfo();
// the attribute we are looking for is a runtime invisible attribute
// use Retention(RetentionPolicy.RUNTIME) on the annotation to make it
// visible at runtime
AnnotationsAttribute attInfo = (AnnotationsAttribute) mInfo
.getAttribute(AnnotationsAttribute.invisibleTag);
if (attInfo != null) {
// this is the type name meaning use dots instead of slashes
return attInfo.getAnnotation(METHOD_ANNOTATION);
}
return null;
}

private List getParamIndexes(Annotation annotation) {
ArrayMemberValue fields = (ArrayMemberValue) annotation
.getMemberValue(ANNOTATION_ARRAY);
if (fields != null) {
MemberValue[] values = (MemberValue[]) fields.getValue();
List parameterIndexes = new ArrayList();
for (MemberValue val : values) {
parameterIndexes.add(((StringMemberValue) val).getValue());
}
return parameterIndexes;
}
return Collections.emptyList();
}

private String createJavaString(CtMethod currentMethod, String className,
List indexParameters) {
StringBuilder sb = new StringBuilder();
sb.append("{StringBuilder sb = new StringBuilder");
sb.append("(\"A call was made to method '\");");
sb.append("sb.append(\"");
sb.append(currentMethod.getName());
sb.append("\");sb.append(\"' on class '\");");
sb.append("sb.append(\"");
sb.append(className);
sb.append("\");sb.append(\"'.\");");
sb.append("sb.append(\"\\n Important params:\");");
for (Object index : indexParameters) {
try {
// add one because 0 is "this" for instance variable
// if were a static method 0 would not be anything
int localVar = Integer.parseInt(String.valueOf(index)) + 1;
sb.append("sb.append(\"\\n Index \");");
sb.append("sb.append(\"");
sb.append(index);
sb.append("\");sb.append(\" value: \");");
sb.append("sb.append($" + localVar + ");");
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
sb.append("System.out.println(sb.toString());}");
return sb.toString();
}
}
31 changes: 31 additions & 0 deletions jdk8/src/main/java/t5750/instrument/Premain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package t5750.instrument;

import java.lang.instrument.Instrumentation;

/**
* 5. Using the Modified Class
*/
public class Premain {
/**
* 5.2. Using Java Instrumentation
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Premain");
// inst.addTransformer(new ClassFileTransformer() {
// @Override
// public byte[] transform(ClassLoader l, String name, Class c,
// ProtectionDomain d, byte[] b)
// throws IllegalClassFormatException {
// if (name.equals("java/lang/Integer")) {
// CustomClassWriter cr = new CustomClassWriter(b);
// return cr.addField();
// }
// if (name.equals(AsmUtil.PATH_POINT)) {
// System.out.println("transform: " + name);
// }
// return b;
// }
// });
inst.addTransformer(new ImportantLogClassTransformer());
}
}
26 changes: 26 additions & 0 deletions jdk8/src/main/java/t5750/module/log/BankTransactions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package t5750.module.log;

import t5750.module.log.annotation.ImportantLog;

public class BankTransactions {
@ImportantLog(fields = { "1", "2" })
public void login(String password, String accountId, String userName) {
// login logic
}

@ImportantLog(fields = { "0", "1" })
public void withdraw(String accountId, Double moneyToRemove) {
// transaction logic
}

public static void main(String[] args) {
BankTransactions bank = new BankTransactions();
for (int i = 0; i < 100; i++) {
String accountId = "account" + i;
bank.login("password", accountId, "T5750");
// bank.unimportantProcessing(accountId);
bank.withdraw(accountId, Double.valueOf(i));
}
System.out.println("Transactions completed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package t5750.module.log.annotation;

public @interface ImportantLog {
String[] fields();
}
13 changes: 0 additions & 13 deletions jdk8/src/test/java/t5750/asm/instrument/PremainTest.java

This file was deleted.

0 comments on commit 79def54

Please sign in to comment.