Java Annotations have played a role in integrating various apis since their introduction to Java, especially for large application frameworks. Both Spring and Hibernate are good examples of Java annotation applications in this respect — you can implement very complex program logic by adding just a few lines of simple Java Annotation code. Although there is some debate about these apis, most programmers agree that declarative programming can be expressive in form when used properly. However, only a few programmers write framework apis, or application middleware, based on Java Annotations. One of the main reasons for this is that Java annotations reduce the readability of code. In this article, I want to show you that implementing these annotation based apis isn’t completely useless, and that you don’t need to know much about Java’s internal functions if you use the right tools.
One obvious problem with implementing annotation based apis is that they are not handled by the JVM at Java runtime. As a result, you can’t give an annotation a specific meaning. For example, if we define an @log annotation, then we expect a Log record to be formed every time an @log is called.
class Service { @Log void doSomething() { // do something ... }}Copy the code
It is impossible to execute the program logic just by writing the @log annotation where it is written, so it is up to the annotation user to initiate the Log generation task. Obviously, this way of working makes the annotation look meaningless, because when we call the doSomething method again, there is no way to observe the corresponding state in the generated log. Therefore, an annotation exists only as a marker and contributes nothing to the program logic.
Fill in the pit
In order to overcome the above functional limitations, many annotation-based frameworks adopt the subclass overrides class methods pattern to give specific annotation-related program logic functionality. This approach generally uses an object-oriented integration mechanism. For the @log annotation we mentioned above, the subclass implementation mechanism produces a class similar to the following LoggingService:
class LoggingService extends Service { @Override void doSomething() { Logger.log("doSomething() was called"); super.doSomething(); }}Copy the code
Of course, the code that defines these classes above is usually not written by a programmer, but generated automatically at Java runtime from a library such as Cglib or Javassst. Both libraries mentioned above provide simple apis that can be used to generate enhanced subclasses. A nice side effect of putting class-defined procedures into the runtime is that the logging framework can be effectively implemented without specifying application specifications or modifying existing user code. This avoids the “explicit creation style” of creating code by hand by creating a new Java source file.
But is the scalability good?
However, the above solution introduces another disadvantage. We implement the program logic of the annotations by automatically subclassing them and must ensure that the constructor of the parent class is not used when instantiating. Otherwise, when you call the annotation method, you still won’t be able to complete the add-log function: the reason is obvious: instantiating an object with a constructor of a parent class doesn’t create the correct instance of a method that contains subclasses. Even worse — when using the above method for runtime code generation — the LoggingService class cannot be instantiated directly because the Java compiler generates the class code during runtime when it compiles.
For these reasons, frameworks such as Spring or Hibernate use the “object factory” pattern. Within the scope of its framework logic, objects are not allowed to be instantiated directly (through constructors), but instead are created through factory classes. This approach was adopted from the beginning of Spring’s design to manage beans. Hibernates follows a similar approach. Most Hibernates instances are treated as the result of a query and therefore are not explicitly instantiated. However, when attempting to store an object instance that does not already exist in the database, a Hibernates user needs to replace the previously stored object instance with a Hibernates returned object. Judging from the Hibernates issue in this example, ignoring the above substitution would result in a common beginner’s error. In addition, thanks to these factory classes, subclassing methods are transparent to framework users, because Java’s type system can substitute instances of subclasses for their parent classes. Therefore, an instance of LoggingService can be used anywhere a user needs to invoke a custom service.
Unfortunately, this method of creating objects using factory classes has proved to be possible, but it is still very difficult to implement the logic of the @log annotation, because it requires each annotation class to build a corresponding factory class method. Obviously, doing so would increase the size of our code template significantly. What’s more, we even create more than one code template for the logging annotation class to avoid writing logic dead (hard-coding) in the logging method. Also, if someone accidentally calls the constructor, there can be subtle bugs, because the resulting object instance may not handle the annotation as expected. Again, factory design is not easy. What if we want to add an @log flag to a class that is already defined as a Hibernates bean? This may sound trivial, but it is necessary to design additional configurations to integrate the factory classes we define with the framework’s own factory classes. Finally, and in conclusion, the bloated code written in the factory model is too expensive to make both code readable and the framework used fit together perfectly. This is why we introduced the Java Agent. The Underrated Java Agent pattern provides an excellent alternative to the subclassing approach we want.
A simple Agent
The Java Agent is represented as a simple JAR file. Much like a normal Java program, the Java Agent defines classes as entry points. These classes, which act as entry points, need to contain static methods that are called before the main method of your Java program is called:
class MyAgent { public static void premain(String args, Instrumentation inst) { // implement agent here ... }}Copy the code
The most interesting part about working with Java Agents is the second parameter in the premain method. This parameter is in the form of an implementation class instance of the Instrumentation interface. This interface provides a mechanism to intervene in the loading of Java classes by defining a ClassFileTransformer. With this transfer facility, we can implement enhancements to the class logic before Java classes are used.
Using this API may not seem intuitive at first, and is likely to be a new (programming mode) challenge. Class files are converted by modifying the compiled Java Class bytecode. In fact, the JVM doesn’t know what the Java language is, it only knows what bytecode is. It is the abstract nature of bytecode that enables the JVM to run multiple languages, such as Groovy, Scala, and so on. In this way, a registered class file converter is only responsible for converting one bytecode sequence into another.
There are libraries like ASM and BCEL that provide simple apis for manipulating compiled Java classes, but the barriers to using these libraries are high and require a good understanding of how the original byte code works. To make matters worse, manipulating bytecode directly without problems is a tedious process, and the JVM throws a stinky VerifierError for even the slightest error. Fortunately, there are better, simpler options for manipulating bytecodes.
Byte Buddy this is a library of tools that I write and maintain. This library provides a concise API for manipulating compiled Java bytecode, as well as creating Java Agents. In some ways, Byte Buddy is also a code-generating library, similar to the capabilities of Cglib and Javassit. Unlike them, however, Byte Buddy also provides a unified API for subclassing, and the ability to redefine existing classes. In this article, we will only examine how to redefine a class using the Java Agent. For those of you who are more interested, refer to Byte Buddy’s webpage which offers a detailed tutorial, which describes the webpage in detail.
Create a Simple Agent using Byte Buddy
One definition provided by Byte Buddy takes a dependency injection approach. It works like this: An interceptor class — which is a POJO — is used to get the information needed for the annotation parameters. For example, by using Byte Buddy’s @origin annotation on a parameter of type Method, Byte Buddy implies that the interceptor is currently intercepting the Method variable. This way, we can define a generic interceptor that will be intercepted whenever a method appears.
class LogInterceptor { static void log(@Origin Method method) { Logger.log(method + " was called"); }}Copy the code
Of course, Byte Buddy can work on multiple annotations.
But how can these interceptors represent the code logic needed for our proposed logging framework? So far, we’ve just defined an interceptor that intercepts our method calls. Also missing is the call to the original code sequence in which Method is located. Fortunately, Byte Buddy’s tools are composed. First we define a MethodDelegation class and combine it into the LogInterceptor. This interceptor class defaults to calling the interceptor’s static method every time a method is called. Using this as a starting point, we can combine the proxy class with the code that originally called method in a sequential call, just as the Super MethodCall represents:
class LogAgent {
MethodDelegation.to(LogInterceptor.class)
.andThen(SuperMethodCall.INSTANCE)
}Copy the code
Finally, we need to notify Byte Buddy to bind the intercepted method to the specific logic. As we explained earlier, we want to apply a piece of logic to every @Log notation. In Byte Buddy, methods are identified using the ElementMatcher method, much like Java 8’s assertion mechanism. In ElementMatcher static utility classes, we can use the corresponding matcher to identify our (@ Log) tagging method: after ElementMatchers. IsAnnotatedWith (the class).
Through the above method, we can realize the definition of an agent, which can fulfill the requirements of our proposed logging framework. As described in the previous section, Byte Buddy provides a set of utility apis to build Java Agents based on (JavaEE native) apis that can modify (compiled) classes. The API, like the one below, is similar in design to a domain-oriented language and is easy to read literally from the code. Clearly, defining an agent requires only a few lines of code:
class LogAgent { public static void premain(String args, Instrumentation inst) { new AgentBuilder.Default() .rebase(ElementMatchers.any()) .transform( builder -> return builder .method(ElementMatchers.isAnnotatedWith(Log.class)) .intercept(MethodDelegation.to(LogInterceptor.class) .andThen(SuperMethodCall.INSTANCE)) ) .installOn(inst); }}Copy the code
Note that this minimal Java Agent code does not interfere with the original code. For existing code, the additional logical code is as if it were hardcoded directly into the annotated method.
What is the reality?
Of course, the Agent-based Logger we show here is just an educational example. Often, frameworks with broad coverage provide similar features that can be invoked directly. A similar feature like Spring or Dropwizard works well. However, the functionality provided by these frameworks is largely focused on addressing (specific) programming problems. For most software applications, this is probably not a bad idea. Again, the thinking behind these frameworks is sometimes on a large scale. As a result, using these frameworks to do things can create a lot of problems, often resulting in flawed abstract logic, and can further cause an explosion in software operation and maintenance costs. This assumption is by no means an exaggeration, especially as your application grows in size, requirements change frequently and bifurcated, and you use the functionality provided by the framework to solve the problem.
Instead, we could build a more targeted framework or library, using a “cherry-and-fit” style, replacing problematic components one at a time with complete ones. If that doesn’t solve the problem, we can create a custom solution and make sure that the new solution doesn’t affect the existing code in the application. As far as we know, this second approach is a bit difficult for JVMS to implement, mainly because of Java’s strong typing mechanism. However, it is not entirely impossible to overcome these type limitations by using Java Agents.
In general, I think that all concepts that involve horizontal operations should be implemented in an Agent-driven manner and should be implemented using targeted frameworks rather than the built-in methods that scary big frameworks give you. I wish more apps would consider adopting this approach. In general, using an Agent to register and implement a listener for a particular method is perfectly adequate. Based on my observations of large-volume Java application code, this indirect modular approach avoids strong coupling. As a sweet side effect, this approach makes it easy to test your code. As with testing, you can turn off application features (such as the logging example in this article) on demand by starting the application without loading agents. All of this doesn’t change a single line of code, and it doesn’t crash the program because the JVM automatically ignores annotations that can’t be resolved at run time. Security, logging, caching, and many other aspects need to be addressed in the manner of this article for many reasons. So, we’re going to say, agent, not frame.
References: Check out our JCG partner Rafael Winterhalter’s article on My Daily Java blog: Make Agents, Not Framework.
javacodegeeks
ImportNew.com
Huang Xiaofei
www.importnew.com/15768.html
Please keep the source, translator and translation link for reprinting.
About the author:Huang Xiaofei
Code scientist, amateur art lover, logic and principle. (Sina Weibo: @Huang Xiaofei)
See more articles by Huang Xiaofei >>