Deep Bytecode operations: creating audit logs using ASM and Javassist
Original link: https://blog.newrelic.com/2014/09/29/diving-bytecode-manipulation-creating-audit-log-asm-javassist/
Using spring and hibernate on the stack, the bytecode of your application may be enhanced or handled at run time. Bytecode is the instruction set of the Java Virtual Machine (JVM), and all languages running on the JVM must eventually be compiled into bytecode. Operation bytecode causes are as follows: Program analysis:
Find class class generation that applies bug check code complexity to find specific annotations:
Lazy loading data security from the database using agents
Specific API restriction access code obfuscation no Java source class conversions
Code Analysis Code Optimization Finally, add a log
There are several tools available for manipulating bytecode, from very low-level tools such as ASM that require bytecode levels to advanced frameworks such as ASPECTJ, which allow pure Java to be written.
This blog post, I'll show you how to implement an audit log using javassist and ASM, respectively. Audit Log Examples
Suppose I don't have the following code:
public class Banktransactions {public
static void Main (string[] args) {
Banktransactions bank = new Banktransacti ONS ();
for (int i = 0; i < i++) {
String accountid = ' account ' + i;
Bank.login ("Password", AccountId, "Ashley");
Bank.unimportantprocessing (AccountId);
Bank.withdraw (AccountId, double.valueof (i));
}
System.out.println ("Transactions completed");
}
1 2 3 4 5 6 7 8 9 10 11 12 1 2 3 4 5 6 7 8 9 10 11-12
We need to record important operations and key information to determine the operation. Above, I will determine the important action of login exit. For logins, the important information will be the account ID and the user. For exits, the important information will be the account ID and the amount withdrawn. One way to record important operations is to add a log statement to every important method, but it will be tedious. Instead, we can add annotations to important methods, and then use tools to inject the log records. In this case, the tool will be a byte-code operating framework.
@ImportantLog (Fields = {"1", "2"}) public
void Login (string password, string accountid, String userName) {
//Lo Gin Logic
}
@ImportantLog (fields = {"0", "1"}) public
void Withdraw (String accountid, Double moneytoremove) {
//transaction Logic
}
1 2 3 4 5 6 7 8 1 2 3 4 5 6-7 8
@ImportantLog Note indicates that we want to log a message each time the method is invoked, whereas the fields parameter in the @importantlog annotation represents the index position of each parameter that should be logged. For example, for login, we want to record the input parameters for the 1th and 2nd digits. They are accountid and username. We will not record the No. 0 digit password parameter.
There are two main benefits of using bytecode and annotations to perform logging: Separating logging from business logic helps keep your code clean and simple. Easily delete audit log records without modifying the source code. where to actually modify the byte code.
We can manipulate bytecode using the core Java features introduced in 1.5. This feature is called the Java Proxy.
To understand the Java proxy, let's take a look at the typical Java processing process.
Use the class containing our main method to execute command Java as an input parameter. This starts the Java run-time environment, uses ClassLoader to load the input class, and calls the main method of the class. In our specific example, the Banktransactions main method is invoked, which causes some processing to occur and print "complete transaction".
Now look at the Java process using the Java Proxy.
Command Java to run two input parameters. The first is the JVM parameter-javaagent, which points to the proxy jar. The second is the class that contains our main methods. The Javaagent flag tells the JVM to load the proxy first. The agent's main class must be specified in the proxy jar's manifest. Once the class is loaded, the Premain method of the class is invoked. This premain method acts as an installation hook for the agent. It allows an agent to register a class converter. When a class converter registers in the JVM, the converter receives the bytes of each class before the class is loaded into the JVM. This provides the class converter with the opportunity to modify the bytes of the class as needed. Once the class converter modifies the byte, it returns the modified byte to the JVM. These bytes are then validated and loaded by the JVM.
In our specific example, when the banktransaction load, the byte will first enter the class converter for potential modifications. The modified byte is returned and loaded into the JVM. After loading, invoke the main method in the class, do some processing, and print "Transaction complete."
Let's take a look at the code. Below I have the Premain method of the agent:
public class Javassistagent {public
static void Premain (String Agentargs, Instrumentation inst) {
SYSTEM.OUT.PRINTLN ("starting agent");
Inst.addtransformer (New Importantlogclasstransformer ());
}
1 2 3 4 5 6 1 2 3 4 5-6
The Premain method prints out a message and registers a class converter. Class converters must implement method conversions, which are invoked by each class that is loaded into the JVM. It takes the byte array of the class as input to the method, and then returns the modified byte array. If the class converter decides not to modify the bytes of a particular class, you 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;
}
}
1 2 3 4 5 6 7 8 1 2 3 4 5 6-7 8
Now we know where to modify the byte of a class, and then we need to know how to modify the byte. How to modify bytecode using Javassist.
Javassist is a bytecode operating framework with advanced and low-level APIs. I'll focus on the Advanced Object-oriented API, starting with the interpretation of the objects in the javassist. Next, I will implement the actual code for the audit log application.
Javassist uses a Ctclass object to represent a class. These Ctclass objects can be obtained from classpool and used to modify classes. Classpool is a Ctclass object container based on the HashMap implementation, where the key is the class name 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 might want to add a classpath or class byte to Classpool.
Similar to the Java class containing fields, methods, and constructors, the Ctclass object contains Ctfields,ctconstructors and Ctmethods. All of these objects can be modified. I will focus on the method operation because the audit log application requires this behavior.
Here are a few ways to modify a method:
The above illustration shows one of the main advantages of javassist. You don't actually have to write a section code. Instead, you write Java code. A complex scenario is that Java code must be enclosed in quotes.
Now that we know the basic building blocks of javassist, let's look at the actual code for the application. The transformation method of a class converter requires the following steps: Converting a byte array to a Ctclass object checking each annotated @importantlog method in Ctclass If there is a @importantlog annotation in the method, then:
Get method Important parameter Index function start increment log statement
When writing Java code using Javassist, be aware of the following issues: The JVM uses slashes between packages, and Javassist uses points. When you insert multiple lines of Java code, the code needs to be in parentheses. When using reference method parameter values such as 1,2, you know that 0 is reserved for "this". This means that your method of