Experience aspect-oriented (AOP) programming in Java 1.5

Source: Internet
Author: User
Tags format execution gz file new features variables stack trace variable thread
Programming excerpt from a long time college
For an experienced Java developer who has access to the source code, any program can be viewed as a transparent model in a museum. Tools such as thread dumps (dump), method call tracking, breakpoints, slice (profiling), and so on, let us know what the program is currently doing, what has just been done, and what will be done in the future. However, the situation in the product environment is not so obvious, these tools are generally not available, or can only be used by trained developers. The support team and end users also need to know what the application is doing at some point.

To fill this vacancy, we have invented some simple alternatives, such as log files (typically used for server processing) and status bars (for GUI applications). However, since these tools can only capture and report a small subset of the information available, and often have to show it in an easy-to-understand way, programmers tend to write them explicitly into the application. And the code wraps around the business logic of the application, and when developers try to debug or understand core functionality, they have to "work around that code," and remember to update the code when the functionality changes. The real function we want to implement is to centralize the status reports in one place and manage the individual status messages as metadata (metadata).

In this article I will consider the case of using the State Bar component in an embedded GUI application. I'll introduce a variety of different ways to implement this status report, starting with the traditional hard coding habits. I will then introduce a number of new features of Java 1.5, including annotations (annotation) and Run-time bytecode refactoring (instrumentation).
State Manager (Statusmanager)

My main goal is to build a jstatusbar swing component that can be embedded in a GUI application. Figure 1 shows the style of the status bar in a simple jframe.


Figure 1. Our dynamically generated status bar

Since I don't want to refer to any GUI component directly in the business logic, I will create a Statusmanager (state manager) to act as an entry point for the status update. The actual notification is delegated to the Statusstate object, so it can be extended later to support multiple concurrent threads. Figure 2 shows this arrangement.


Figure 2. Statusmanager and Jstatusbar

Now I have to write code to call the Statusmanager method to report the application's process. Typically, these method calls are scattered throughout the try-finally code block, usually one call per method.

public void Connecttodb (String URL) {
Statusmanager.push ("Connecting to Database");
try {
...
finally {
Statusmanager.pop ();
}
}

The code implements the functionality we need, but it looks a bit confusing after dozens of or even hundreds of of times in the code base. Besides, what if we want to access the messages in some other way? In the later part of this article, I will define a user-friendly exception handler that shares the same message. The problem is that I hide the status message in the implementation of the method without putting the message in the interface to which the message belongs.

Attribute-oriented Programming

What I really want to do is put a reference to statusmanager somewhere outside the code and simply tag this method with our message. I can then use code generation (code-generation) or run-time introspection (introspection) to perform the real work. The Xdoclet project concludes this approach to attribute-oriented programming (Attribute-oriented programming), which also provides a framework component that converts custom-like Javadoc tags into source code.

However, JSR-175 contains such content that Java 1.5 provides a more structured format to contain these attributes in real-world code. These properties are called "annotations" (annotations), which we can use to provide metadata for a class, method, field, or variable definition. They must be explicitly declared and provide a set of name-value pairs (Name-value pair) that can contain arbitrary constant values, including primitives, strings, enumerations, and classes.

Annotations (annotations)

To handle the status message, I want to define a new annotation that contains a string value. The definition of annotations is very similar to the definition of an interface, but it replaces interface with the @interface keyword and supports only methods (although they function more like fields):

Public @interface Status {
String value ();
}

Like the interface, I put @interface into a file called Status.java and import it into any file that needs to be referenced.

For our field, value may be a strange name. A name like a message might be more appropriate, but value is of particular interest to Java. It allows us to use @status ("...") instead of @status (value= "...") to define annotations, which is significantly more straightforward.

I can now define my own method by using the following code:

@Status ("Connecting to Database")
public void Connecttodb (String URL) {
...
}

Please note that we must use the-source 1.5 option when compiling this code. If you use Ant instead of using the Javac command line to build your application, you need to use ant version 1.6.1.

As a complement to classes, methods, fields, and variables, annotations can also be used to provide metadata for other annotations. In particular, Java introduces a small number of annotations that you can use to customize the way your annotations work. We redefine our annotations with the following code:

@Target (Elementtype.method)
@Retention (Retentionpolicy.source)
Public @interface Status {
String value ();
}

@Target annotations define what the @status annotation can refer to. Ideally, I would like to mark large chunks of code, but its options are only methods, fields, classes, local variables, parameters, and other annotations. I was only interested in the code, so I chose method (methods).
 
@Retention annotations allow us to specify when Java can discard messages autonomously. It may be source (discarded at compile time), class (Discarded when class is loaded), or runtime (not discarded). We first select source, but we will update it at the back of this article.
Refactoring source code

Now that my messages are encoded into metadata, I have to write some code to notify the state listener. Suppose at some point I continue to keep the Connecttodb method in the source control, but there is no reference to the Statusmanager. However, before compiling this class, I want to add some necessary calls. That is, I want to automatically insert try-finally statements and Push/pop calls.

The Xdoclet framework component is a Java source code generation engine that uses similar annotations, but stores them in comments (comment) in Java source code. Xdoclet is perfect for generating entire Java classes, configuration files, or other established parts, but it does not support modifications to existing Java classes, which limits the effectiveness of refactoring. Instead, I can use profiling tools (such as JAVACC or ANTLR, which provide a syntax basis for parsing Java source code), but it takes a lot of effort.

There seems to be no good tool for code refactoring in Java code. Such tools may have a market, but as you can see later in this article, bytecode refactoring may be a more powerful technique. Refactoring byte code

Instead of refactoring the source code and compiling it, you compile the original source code, and then refactor the bytecode that it produces. Such an operation may be easier or more complex than source code refactoring, and this relies on the exact conversion required. The main advantage of bytecode refactoring is that code can be modified at run time without the need to use compilers.

Although the Java bytecode format is relatively simple, I would like to use a Java class library to perform bytecode analysis and generation (this can isolate us from future changes to the Java class file format). I chose the byte Code Engineering library (bytecode engine class library, BCEL) using Jakarta, but I can also choose Cglib, ASM, or SERP.

Since I'm going to refactor bytecode in a number of different ways, I'll start with a generic interface that declares refactoring. It is similar to performing a simple framework component based on annotation refactoring. This framework component, based on annotations, will support the conversion of classes and methods, so the interface has a definition similar to the following:

public interface Instrumentor
{
public void Instrumentclass (Classgen classgen,annotation a);
public void Instrumentmethod (Classgen classgen,methodgen methodgen,annotation a);
}

Classgen and Methodgen are bcel classes that use the builder pattern. In other words, they provide a way to change other immutable (immutable) objects, as well as transformations between mutable and immutable manifestations (representation).

Now I need to write an implementation for the interface, which must replace the @status annotation with the appropriate Statusmanager call. As mentioned earlier, I want to include these calls in the Try-finally code block. Note that in order to achieve this goal, the annotations we use must be marked with @retention (Retentionpolicy.class), which instructs the Java compiler not to discard annotations during compilation. Because in front of me to declare @status as @retention (Retentionpolicy.source), I have to update it.

In this case, the refactoring bytecode is more complex than the source code. The reason is that try-finally is a concept that exists only in source code. The Java compiler converts the try-finally code block into a series of Try-catch code blocks and inserts a call to the finally code block before each return. Therefore, in order to add the Try-finally code block to the existing bytecode, I must also perform a similar transaction.

The following is the bytecode that behaves as a normal method call, and it is surrounded by statusmanager updates:

0:LDC #2; String message
2:invokestatic #3; Method Statusmanager.push: (lstring;) V
5:invokestatic #4; Method DoSomething: () V
8:invokestatic #5; Method Statusmanager.pop: () V
11:return

The following is the same method call, but it is in the try-finally code block, so if it produces an exception, it calls Statusmanager.pop ():

0:LDC #2; String message
2:invokestatic #3; Method Statusmanager.push: (lstring;) V
5:invokestatic #4; Method DoSomething: () V
8:invokestatic #5; Method Statusmanager.pop: () V
11:goto 20
14:astore_0
15:invokestatic #5; Method Statusmanager.pop: () V
18:aload_0
19:athrow
20:return

Exception table:
From to target type
5 8 any
Any

You can see that in order to implement a try-finally, I have to copy some instructions and add a few jumps and exception table records. Fortunately, Bcel's instructionlist class makes this kind of work quite simple.

Refactoring bytecode at run time

Now that I have an interface based on the annotation modification class and a concrete implementation of the interface, the next step is to write the actual framework component that calls it. In fact, I'm going to write a small number of framework components that start by refactoring the skeleton components of all classes from the runtime. Since this happens during build, I decided to define an ant transaction for it. The declaration of the refactoring target in the Build.xml file should read as follows:

<instrument class= "Com.pkg.OurInstrumentor"
<fileset dir= "$ (classes.dir)" >
<include name= "**/*.class"/>
</fileset>
</instrument>

To implement this transaction, I must define a class that implements the Org.apache.tools.ant.Task interface. The attributes and child elements of our transaction (sub-elements) are passed in through the set and Add method calls. We call the Execute method to implement the work that the transaction is going to do--in the example, refactoring the class file specified in <fileset>.

public class Instrumenttask extends Task {
...
public void SetClass (String className) {...}
public void Addfileset (Fileset fileset) {...}

public void execute () throws Buildexception {
Instrumentor Inst = Getinstrumentor ();

try {
DirectoryScanner DS =fileset.getdirectoryscanner (Project);
"For" syntax for Java 1.5
For (String file:ds.getIncludedFiles ()) {
Instrumentfile (inst, file);
}
catch (Exception ex) {
throw new Buildexception (ex);
}
}
...
}

There is a problem with the Bcel version 5.1 for this operation-it does not support parsing annotations. I can load the class that is being refactored and use reflection (reflection) to view the annotations. However, if so, I would have to use retentionpolicy.runtime instead of Retentionpolicy.class. I must also perform some static initialization in these classes, which may load the local class library or introduce other dependencies. Fortunately, Bcel provides a plug-in (plugin) mechanism that allows clients to parse bytecode properties. I wrote my own Attributereader implementation (implementation), in the presence of annotations, It knows how to parse the Runtimevisibleannotations and Runtimeinvisibleannotations properties in the caret code. Bcel Future versions should include this functionality rather than being provided as plug-ins.

The compile-time bytecode refactoring method is displayed in the Code/02_compiletime directory of the sample code.

But there are many flaws in this approach. First, I have to add extra steps to the build process. I cannot decide to turn on or off a refactoring operation based on command-line settings or other information not provided at compile time. If the refactoring or refactoring code needs to run in the production environment at the same time, you must establish two separate. jars files, and you must decide which one to use.
To refactor a byte code when class is loaded

A better approach might be to delay the bytecode refactoring operation until the bytecode is loaded to refactor. When using this method, the refactored bytecode is not saved. The performance of our application startup time may be affected, but you can control what you are doing based on your own system attributes or Run-time configuration data.

Before Java 1.5, we used custom class loading programs to implement this kind of file maintenance operations. But the newly added java.lang.instrument package in Java 1.5 provides a few additional tools. Specifically, it defines the concept of classfiletransformer, which we can use to refactor a class during the standard loading process.

In order to register Classfiletransformer at the appropriate time (before loading any class), I need to define a Premain method. Java calls this method before loading the main class (main Class), and it passes in a reference to the instrumentation object. I must also add the-javaagent parameter option to the command line, telling Java our Premain method information. This parameter option takes the full name of our Agent class (which contains the Premain method) and any string as arguments. In the example we take the full name of the Instrumentor class as an argument (it must be in the same row):

-javaagent:boxpeeking.instrument.instrumentoradaptor=
Boxpeeking.status.instrument.StatusInstrumentor

Now I've arranged a callback (callback) that will occur before loading any class that contains annotations, and I have a reference to the instrumentation object that can register our Classfiletransformer:

public static void Premain (String className,
Instrumentation i)
Throws ClassNotFoundException,
Instantiationexception,
Illegalaccessexception
{
Class Instclass = Class.forName (className);
Instrumentor Inst = (instrumentor) instclass.newinstance ();
I.addtransformer (New Instrumentoradaptor (inst));
}

The adapters we register here will act as a bridge between the Instrumentor interface given above and the Java Classfiletransformer interface.

public class Instrumentoradaptor
Implements Classfiletransformer
{
Public byte[] Transform (ClassLoader cl,string classname,class classbeingredefined,
Protectiondomain protectiondomain,byte[] classfilebuffer)
{
try {
Classparser CP =new Classparser (new Bytearrayinputstream (Classfilebuffer), ClassName + ". Java");
Javaclass JC = Cp.parse ();

Classgen CG = new Classgen (JC);

For (Annotation an:getannotations (Jc.getattributes ())) {
Instrumentor.instrumentclass (CG, an);
}

For (Org.apache.bcel.classfile.Method m:cg.getmethods ()) {
For (Annotation an:getannotations (M.getattributes ())) {
Constantpoolgen CPG =cg.getconstantpool ();
Methodgen mg =new Methodgen (M, ClassName, CPG);
Instrumentor.instrumentmethod (CG, MG, an);
Mg.setmaxstack ();
Mg.setmaxlocals ();
Cg.replacemethod (M, Mg.getmethod ());
}
}
Javaclass jcnew = Cg.getjavaclass ();
return Jcnew.getbytes ();
catch (Exception ex) {
throw new RuntimeException ("instrumenting" + ClassName, ex);
}
}
...
}

This method of refactoring bytecode at startup is located in the/code/03_startup directory of the sample.

Handling of exceptions

As mentioned earlier in the article, I want to write additional code using @status annotations for different purposes. Let's consider some additional requirements: our application must catch all unhandled exceptions and display them to the user. Instead of providing a Java stack trace, however, we display methods that have @status annotations and should not display any code (the name or line number of a class or method, and so on).

For example, consider the following stack trace information:

Java.lang.RuntimeException:Could not load data for symbol IBM
At Boxpeeking.code.YourCode.loadData (Unknown Source)
At Boxpeeking.code.YourCode.go (Unknown Source)
At Boxpeeking.yourcode.ui.main+2.run (Unknown Source)
At Java.lang.Thread.run (thread.java:566)
caused by:java.lang.RuntimeException:Timed out
At Boxpeeking.code.YourCode.connectToDB (Unknown Source)
... More information

This will result in the GUI pop-up shown in Figure 1, which assumes that your yourcode.loaddata (), Yourcode.go () and Yourcode.connecttodb () all contain @status annotations. Note that the order of exceptions is the opposite, so the user gets the most detailed information first.

           
Figure 3. Stack trace information displayed in the error dialog box

To implement these features, I have to make a slight change to the existing code. First, to make sure that @status annotations are visible at run time, I have to update @retention again and set it to @retention (Retentionpolicy.runtime). Keep in mind that @Retention controls when the JVM discards annotation information. This setting means that annotations can be accessed not only by the compiler inserting bytecode, but also by reflection using the new Method.getannotation (Class) method.

Now I need to arrange for notification of any exceptions that are not explicitly handled in the receiving code. In Java 1.4, the best way to handle unhandled exceptions on any particular thread is to use the Threadgroup subclass and add your own new thread to the type of threadgroup. But Java 1.5 provides additional functionality. I can define an instance of the Uncaughtexceptionhandler interface and register it for any particular thread (or all threads).

Note that it may be better to register for a particular exception in the example, but there is a bug in the Java 1.5.0beta1 (#4986764) that makes it impossible to do so. But setting up a handler for all the threads is working, so I'm doing it.

Now we have a way to intercept unhandled exceptions, and these exceptions must be reported to the user. In a GUI application, this is typically done by popping up a modal dialog box that contains the entire stack trace information or simple message. In the example, I want to display a message when an exception is generated, but I want to provide a @status description of the stack instead of the name of the class and method. To do this, I simply query in the stacktraceelement array of thread, find the Java.lang.reflect.Method object associated with each frame, and query its stack annotation list. Unfortunately, it provides only the name of the method and does not provide the method's characteristic (signature), so this technique does not support overloaded methods with the same name (but @status annotations).

The sample code that implements this method can be found in the/code/04_exceptions directory of the peekinginside-pt2.tar.gz file.
Sampling (sampling)

I now have a way to convert the stacktraceelement array to the @status annotation stack. This operation is more useful than what is shown. Another new feature in Java 1.5-thread introspection (introspection)-enables us to get an accurate stacktraceelement array from the currently running thread. With these two pieces of information, we can construct another implementation of Jstatusbar. Instead of receiving a notification when a method call occurs, Statusmanager simply initiates an additional thread that is responsible for grabbing the stack trace information and the state of each step during the normal interval. As long as the interval is short enough, the user will not feel the delay of the update.

Here's the code behind the "sampler" thread, which tracks the passing of another thread:

Class Statussampler implements Runnable
{
Private Thread Watchthread;

Public Statussampler (Thread watchthread)
{
This.watchthread = Watchthread;
}

public void Run ()
{
while (Watchthread.isalive ()) {
Get stack trace information from thread
Stacktraceelement[] StackTrace =watchthread.getstacktrace ();
Extracting status messages from stack trace information
List <Status> statuslist =statusfinder.getstatus (stacktrace);
Collections.reverse (statuslist);
To establish a state with a State message
Statusstate state = new Statusstate ();
for (Status s:statuslist) {
String message = S.value ();
State.push (message);
}

Update the current state
Statusmanager.setstate (watchthread,state);
Hibernate to the next cycle
try {
Thread. Sleep (Sampling_delay);
catch (Interruptedexception ex) {}
}

State Reset
Statusmanager.setstate (Watchthread,new statusstate ());
}
}

Sampling is less invasive (invasive) for programs than adding method calls, manual or through refactoring. I don't have to change the build process or command-line arguments, or modify the startup process. It also allows me to control the overhead by adjusting the sampling_delay. Unfortunately, this method has no explicit callback when the method call starts or ends. In addition to the delay in status updates, there is no reason to require that the code receive callbacks at that time. However, in the future I can add some extra code to track the exact runtime of each method. This can be done precisely by examining the stacktraceelement.

Code that implements Jstatusbar through thread sampling can be found in the/code/05_sampling directory of peekinginside-pt2.tar.gz files.

Refactoring byte code during execution

By combining the sampling method with the refactoring, I was able to form a final implementation that provided the best features of the various methods. Sampling can be used by default, but the most time-consuming method of application can be refactored individually. This implementation does not install Classtransformer at all, but instead, it reconstructs the method one at a time in response to the data collected during the sampling process.

To achieve this, I will create a new class Instrumentationmanager that can be used to refactor and not refactor independent methods. It can use the new Instrumentation.redefineclasses method to modify the idle class, while the code can be executed without interruption. The Statussampler thread added in the previous section now has an additional responsibility to add any @status method of its own discovery to the collection. It periodically finds the worst offenders and provides them to the Instrumentationmanager for refactoring. This allows applications to more accurately track the start and end times of each method.

One problem with the sampling method mentioned earlier is that it cannot distinguish between a long-running method and a method that is invoked multiple times in a loop. Because refactoring adds a certain amount of overhead to each method invocation, it is necessary to ignore frequently invoked methods. Luckily, we can use refactoring to solve this problem. In addition to simply updating Statusmanager, we will maintain the number of times each refactoring method is invoked. If this number exceeds a certain limit (meaning that the information used to maintain the method is too expensive), the sampling thread will permanently cancel the refactoring of the method.

Ideally, I will store the number of calls to each method in a new field that is added to the class during the refactoring process. Unfortunately, the addition of the class conversion mechanism in Java 1.5 does not allow this, and it cannot add or remove any fields. Instead, I will store this information in a static mapping of the method object of the new Callcounter class.

This hybrid method can be found in the/code/06_dynamic directory of the sample code.

Summarized

Figure 4 provides a rectangle that shows the characteristics and costs associated with the example I give.


Fig. 4. Analysis of Reconstruction methods

You can see that the dynamic method is a good combination of various schemes. Like all examples of using refactoring, it provides a clear callback at the start or end of a method, so your application can accurately track the runtime and provide immediate feedback to the user. However, it can also eliminate the refactoring of a method (which is called too frequently), so it will not be affected by the performance problems encountered by other refactoring scenarios. It does not contain compile-time steps, and it does not add extra work to the class loading process.

Trends in the future

We can add a lot of attachment features to this project to make it more applicable. One of the most useful features may be dynamic state information. We can use the new Java.util.Formatter class to replace pattern substitution, which is similar to printf, for @status messages. For example, the @status ("Connecting to%s") annotation in our connecttodb (String URL) method can report the URL as part of the message to the user.

This may seem trivial with the help of source refactoring, because the Formatter.format method I will use uses the variable parameters (the "Magic" feature added in Java 1.5). The refactoring version is similar to the following scenario:

public void Connecttodb (String URL) {
Formatter f = new Formatter ();
String message = F.format ("Connecting to%s", url);

Statusmanager.push (message);
try {
...
finally {
Statusmanager.pop ();
}
}

Unfortunately, this "magic" feature is fully implemented in the compiler. In bytecode, Formatter.format takes object[] as an argument, and the compiler explicitly adds code to wrap each original type and assemble the array. If Bcel does not make up for it, and I need to use bytecode refactoring, I will have to implement this logic again.

Since it can only be used for refactoring (where method parameters are available) and not for sampling, you may want to refactor these methods at startup, or at least make the dynamic implementations biased towards any method refactoring, and you can use alternative mode in the message.

You can also track the number of times each refactoring method call starts, so you can also more accurately report how many times each method is run. You can even save historical statistics of these times and use them to form a real progress bar (instead of the indeterminate version I use). This ability will give you a good reason to refactor a method at run time, because the overhead of tracking any independent method can be very obvious.

You can add "Debug" mode to the progress bar, regardless of whether the method call contains @status annotations and report all method calls that occur during the sampling process. This is invaluable to any developer who wants to debug deadlocks or performance problems. In fact, Java 1.5 also provides a programmable API for deadlock (deadlock) detection, which we can use to turn the process bar red when the application is locked.

The annotated refactoring framework component that is established in this article may be very market-based. A tool that allows bytecode refactoring at compile time (through ant transactions), at startup (using Classtransformer), and during execution (using instrumentation) is undoubtedly invaluable for a small number of other new projects.

Summarize

As you can see in these examples, metadata programming (meta-programming) can be a very powerful technique. The process of reporting long-running operations is only one of the applications of this technology, and our jstatusbar is only a medium for communicating this information. As we can see, many of the new features provided in Java 1.5 provide enhanced support for metadata programming. In particular, combining annotations with Run-time refactoring provides a truly dynamic form of attribute-oriented programming. We can further use these techniques to extend its functionality beyond the existing framework components (such as the functionality of the framework components provided by Xdoclet).



Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.