A normal Java class contains fields, constructors, and methods. To locate a CtClass , you can grab it by name from the ClassPool , then grab any method from the CtClass and apply your modifications. CtMethod contains lines of code for the associated method. We can insert code at the beginning of the method using the insertBefore command.
The great thing about Javassist is that you write pure Java, albeit with one caveat: the Java must be implemented as quoted strings. Although, if you happen to like coding directly in bytecode, stay tuned for the ASM section. The JVM includes a bytecode verifier to guard against invalid bytecode. If your Javassist-coded Java is not valid, the bytecode verifier will reject it at runtime.
Similar to insertBefore , there's an insertAfter to insert code at the end of a method. You can also insert code in the middle of a method by using insertAt or add a catch statement with addCatch. Let's kick off your IDE and code your logging feature. We start with an Agent containing premain and our ClassTransformer. To add audit logging, first implement transform to convert the bytes of the class to a CtClass object.
Then, you can iterate its methods and capture ones with the ImportantLogin annotation on them, grab the input parameter indexes to log, and insert that code at the beginning of the method. Invisible annotations, which are only visible at class loading time and compile time, are declared by passing in the RententionPolicy. CLASS argument to the annotation.
Visible annotations RententionPolicy. For this example, you only need the attributes at compile time, so make them invisible. With the annotation in hand, you can retrieve the parameter indexes. You are finally in a position to insert your log statement in createJavaString. Your implementation creates a StringBuilder , appending some preamble followed by the required method name and class name.
One thing to note is that if you're inserting multiple Java statements, you need to surround them with squiggly brackets see lines 4 and That pretty much covers the code for adding audit logging using Javassist. In retrospect, the positives are:. ASM began life as a Ph.
It is actively updated, and supports Java 8 since the 5. This article will focus on the event-based library. Complete documentation can be found here. A Java class contains many components, including a superclass, interfaces, attributes, fields, and methods. Then pass the output bytes to a no-op ClassWriter to put the parsed bytes back together in the byte array, producing a rehydrated BankTransaction that as expected is virtually identical to our original class. For your logging requirement, you want to check each method for the indicative annotation and add any specified logging.
You only need to overwrite ClassVisitor visitMethod to return a MethodVisitor that supplies your implementation. Just like there are several components of a class, there are several components of a method, corresponding to the method attributes, annotations, and compiled code.
Again, the event handlers are always called in the same predefined sequence, so you always know all of the attributes and annotations on the method before you have to actually visit the code. Incidentally, you can chain together multiple instances of MethodVisitor , just like you can chain multiple instances of ClassVisitor.
Your PrintMessageMethodVisitor overrides two methods. First comes visitAnnotation , so you can check the method for your ImportantLog annotation. When visitCode executes, the presence of the annotation has already been determined and so it can add the specified logging.
The visitAnnotation code hooks in an AnnotationVisitor that exposes the field arguments on the ImportantLog annotation. Now, let's look at the visitCode method. First, it must check if the AnnotationVisitor flagged the annotation as present. If so, then add your bytecode. You have to know about the stack, local variables, etc. I recommend writing the code you need in a Java test class, compiling that, and running it though javap -c to see the exact bytecode. We'll see now a simple and yet complete example using Javassist.
We'll implement the getters and setters for the fields of the class TestData introduced here using bytecode manipulation.
Then we'll test the getter and setter for the field myString. Since these getters and setters are injected using bytecode manipulation at runtime, we'll have to use runtime reflection to call them:.
Inversion of Control is a design pattern related to lifecycle management of components in an application benefiting from a services architecture. In such an application, business components are usually implemented in the form of various services, such as business services, business managers, DAOs, etc. The main class delegates specific business concerns to business services, which delegate finer aspects in their turn to managers, which further delegate various business of technical aspects to smaller managers, or DAOs, adapters.
These various services need to know about each others to be able to call each others. Managing the construction and instantiation of these services is called components lifecycle management. Very often, business services are stateless components, not keeping any state in instance variables or else. Traditionally, for a very long time, these stateless services have been implemented as singletons. For a very long time this was a very convenient approach since the main singleton simply needs to get the other singletons it was using, which in turn simply needed to get the other singletons they were using, and so on.
By separating the instantiation of singletons and their initialization in two stages, cycles could be handled easily and everyone was happy. But with the rise of XP and unit testing, singleton-based applications were suffering from a very critical drawback:. Singletons were enforcing strict dependencies on other service implementation at compile time, making it pretty impossible to replace the dependencies by mock objects or stubs as required for efficiently unit testing a specific service.
Using singletons, testing a specific service often meant to be required to completely build and initialize the whole application, which can well turn into a nightmare.
Java EE was of course not an answer to this problem since it required to have a Java EE container, which is even more of a nightmare well let's not get me started on Java EE, shall we? Inversion of control is initially mostly an answer to this problem, as a way to increase the modularity of the application and making it more extensible, and more importantly testable in an easier way by removing the strict dependencies between components.
The key idea is to delegate the management of the lifecycle of the components and the injection of each component's dependencies to a framework, or rather a container, borrowing the term from Java EE, called here a lightweight container. Instead of every component getting each other references on the singletons or specific instances by building them, the container takes care of instantiating the components , managing their lifecycle in the required scope, and injecting their dependencies at runtime.
Injecting the dependencies at runtime, with a configurable approach, using a configuration file, annotations or even a dedicated API, opens the possibility to inject a different implementation of a service depending on the context, as long as it respects the required interface. For instance, injecting a mock object instead of the real deal for unit testing becomes straightforward.
Inversion of Control and Dependency Injection are two different things - and yet strongly related to each other - often confused in some documentation:. Inversion of Control, as a term, was popularized in by the Apache Avalon team trying to engineer a " Java Apache Server Framework " for the growing set of server side Java components and tools at Apache.
To the Avalon team, it was clear that components receiving various aspects of component assembly, configuration and lifecycle was a superior design to those components going at getting the same themselves. The need for those dependencies was declared in some accompanying XML.
That was actually the first IoC framework close to the form they have today. I myself discovered the concept in when reading that book, following the advise of Mr. Parick Gras, which I take the opportunity to thank a lot for this here. As stated above, in a usual application, the lifecycle of components starts with a main component or class that either creates the other services it requires or get their singletons.
These other components, in their turn, create or get references on their own dependencies, and so on. With IoC, a container, called lightweight container - as opposed to Java EE craps that are very heavy and very bad containers - takes care of instantiating and managing the lifecycle of the components as well as, more importantly, injecting the dependencies in every component.
The rest of this article is dedicated to present the implementation of a very simple IoC framework using Javassist, rather as a way to illustrate how easy and straightforward that is with Javassist than for any other reason :- Implementing Dependency Injection is actually a state-of-the-art use case for Javassist and a nice way to present the possibilities and whereabouts of bytecode manipulation.
We want it to implement Dependency Injection in its simplest form:. In case of getter property injection instead of field injection, SCIF is forced to generate a sub-class of the initial class and override the getter in that sub-class to implement lazy-loading.
This is a consequence of the usage of an annotation to identify the services to be enhanced: in order for the framework to be able to query the annotation on a class, that class needs unfortunately to be loaded by the Classloader.
Javassist is not able to change a class once that class has been loaded well at least not easily. Changing a class that is already loaded leads unusually to a linkage error and that is forbidden without usage of very advanced techniques, too difficult for this simple framework. The first dependency, ServiceB is declared by ServiceA on the field itself, using the annotation Resource. The second dependency, ServiceC is declared on the getter, indicating the will of the developer to benefit from lazy loading.
The example below illustrates in red the code or behaviour that should be injected at runtime by SCIF. The RegistryInitializer analyzes the classes annotated with Service and handles Resource annotations the following way:. It all starts with the method RegistryInitializer. The really interesting call here is enhancePropertyGetters wrappers ;. This is where we use bytecode manipulation to generate the subclass dynamically and override the getter of the method declaring the Resource annotation.
Below is the code representing the diagram above. The class visitor LogMethodClassVisitor has been added. Note that you can add more than one class visitor. For the audit log application, we need to examine each method on the class. Notice that the visitMethod returns a MethodVisitor. Just like a class has many components, a method also has many components, which can be considered events when the method is parsed.
The MethodVisitor provides the events on a method. For the audit logging application, we want to examine the annotations on the method. Based on the annotations, we might need to modify the actual code in the method. To make these modifications we need to chain in a methodVisitor as seen below. Note that visitAnnotation returns an AnnotationVisitor. Just like classes and methods have components, there are also multiple components to an annotation. An AnnotationVisitor allows us to visit all parts of an annotation.
One of the major differences between Javassist and ASM can be seen above. With ASM, you have to write code at the bytecode level when modifying methods, meaning you need to have a good understanding of how the JVM works. You need to know exactly what is on your stack and the local variables at a given moment of time. While writing at the bytecode level opens up the door in terms of functionality and optimization, it does mean ASM has a long developer ramp up time.
Now that you have seen how to use ASM and Javassist in one scenario, I encourage you to give a bytecode manipulation framework a try. Bytecode manipulation not only gives you a better understanding of the JVM, but there are countless applications for it. Once started, you will find that the sky's the limit. This blog is based on the technical content from the talk I gave at SpringOne 2GX on bytecode manipulation.
Be sure to check out the video on InfoQ once it is released. Ashley Puls is a principal software engineer and architect at New Relic.
She has a background in mathematics and enjoys diving into the details of language agents. When not programming, Ashley competes in triathlons and soccer games. The views expressed on this blog are those of the author and do not necessarily reflect the views of New Relic.
Any solutions offered by the author are environment-specific and not part of the commercial solutions or support offered by New Relic.
Please join us exclusively at the Explorers Hub discuss. This blog may contain links to content on third-party sites. By providing such links, New Relic does not adopt, guarantee, approve or endorse the information, views or products available on such sites.
Related articles 0. Figures 0. Information related to the author. Supplementary material 0. Result List. Previous article Next article.
0コメント