Experience (AOP) programming in Java 1.5

Source: Internet
Author: User

For an experienced Java developer who can access source code, any program can be seen as a transparent model in the museum. Similar to thread dumping, method call tracking, breakpoint, and aspect (profiling) statistical tables and other tools let us know what operations the program is currently performing, what operations it has just done, and what operations it will do in the future. However, this is not so obvious in the product environment. These tools are generally unavailable or can only be used by trained developers at most. The Support Team and end users also need to know what operations the application is performing at a specific time point.

To fill this gap, we have created 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 available information, and usually must express this information in an easy-to-understand way, so programmers tend to write them clearly into applications. These codes are entangled in the business logic of applications. When developers try to debug or understand core functions, they must "work around these codes ", remember to update the code after the function is changed. The real function we want to achieve is to centralize the status report at a certain location and manage a single status message as metadata (metadata.

In this article, I will consider using the status bar component embedded in the GUI application. I will introduce a variety of different methods to implement this status report, starting with the traditional hard coding habits. Later, I will introduce a large number of new features of Java 1.5, including annotation and instrumentation ).

Status Manager)

My main goal is to build a JStatusBar Swing component that can embed GUI applications. Figure 1 shows the style of the status bar in a simple Jframe.
  

   Figure 1. dynamically generated status bar
Since I do not want to reference any GUI components directly in the business logic, I will create a StatusManager to act as the entry point for status updates. The actual notification will be delegated to the StatusState object, so it can be expanded 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 process of the application. In typical cases, these method calls are distributed throughout the try-finally code block. Generally, each method calls one call.

Public void connectToDB (String url ){
StatusManager. push ("Connecting to database ");
Try {
...
} Finally {
StatusManager. pop ();
}
}

These codes implement the functions we need, but after dozens or even hundreds of times of copying the code in the code library, it seems a bit messy. What if we want to access these messages in other ways? Later in this article, I will define a user-friendly exception handler that shares the same message. The problem is that I have hidden the status message in the implementation of the method, instead of putting the message in the interface to which the message belongs.

  Attribute-Oriented Programming

What I really want to achieve is to put the reference to StatusManager somewhere outside the code and simply mark this method with our message. Then I can use code-generation or introspection to execute real work. The XDoclet project summarizes this method as Attribute-Oriented Programming. It also provides a framework component that can convert custom Javadoc-like markup into source code.

However, JSR-175 contains such content, and Java 1.5 provides a more structured format to include these attributes in real code. These attributes are called "annotations" and we can use them to provide metadata for classes, methods, fields, or variable definitions. They must be explicitly declared and provide a set of name-value pairs (name-value pair) that can contain any constant value (including primitive, String, enumeration, and class ).

  Annotation (Annotations)

To process status messages, I want to define a new annotation containing string values. The annotation definition is very similar to the interface definition, but it replaces the interface with the @ interface keyword and only supports methods (although their functions are more like fields ):

Public @ interface Status {
String value ();
}

Similar to interfaces, I put @ interface in a file called Status. java and import it to any file that needs to be referenced.

For our field, value may be a strange name. Message-like names may be more suitable. However, value has special significance for Java. It allows us to use @ Status ("...") instead of @ Status (value = "...") to define annotations, which is obviously simpler.

Now I can use the following code to define my own method:

@ Status ("Connecting to database ")
Public void connectToDB (String url ){
...
}

Note that you must use the-source 1.5 option when compiling this code. If you use Ant instead of using the javac command line to create an application, you need to use Ant 1.6.1 or later.

As a supplement to classes, methods, fields, and variables, annotations can also be used to provide metadata for other annotations. In particular, Java introduces a small amount of annotations. You can use these annotations to customize the working method of your own annotations. We use the following code to redefine our Annotations:

@ Target (ElementType. METHOD)
@ Retention (RetentionPolicy. SOURCE)
Public @ interface Status {
String value ();
}

The @ Target annotation defines what the @ Status annotation can reference. Ideally, I want to mark a large piece of code, but its options are methods, fields, classes, local variables, parameters, and other annotations. I am only interested in the code, so I chose METHOD ).

@ Retention annotation allows us to specify when Java can discard messages independently. It may be SOURCE (discarded during compilation), CLASS (discarded during CLASS loading), or RUNTIME (not discarded ). Select SOURCE first, but we will update it later in this article.

Source code Reconstruction

Now all my messages are encoded into the metadata. I have to write some code to notify the status listener. Suppose at some time, I continue to save the connectToDB Method to the source code control, but there is no reference to StatusManager. However, before compiling this class, I want to add some necessary calls. That is to say, I want to automatically Insert the try-finally statement and push/pop call.

The XDoclet framework component is a Java source code generation engine that uses annotations similar to the above, but stores them in the comment of Java source code. XDoclet is perfect for generating the entire Java class, configuration file, or other created parts, but it does not support modifications to existing Java classes, which limits the validity of refactoring. Instead, I can use analysis tools (such as JavaCC or anlr, which provides the syntax basis for analyzing Java source code), but this requires a lot of effort.

It seems that there is no good tool for source code reconstruction in Java code. These tools may be available in the market, but you can see later in this article that bytecode refactoring may be a more powerful technology. Rebuild bytecode

Instead of refactoring the source code and then compiling it, it is to compile the original source code and then refactor the bytecode it generates. Such operations may be easier or more complex than source code reconstruction, and depend on accurate conversion. The main advantage of bytecode reconstruction is that the code can be modified at runtime without the need for a compiler.

Although Java's bytecode format is relatively simple, I still want to use a Java class library to analyze and generate bytecode (this can isolate us from future changes in Java file formats ). I chose the Byte Code Engineering Library (bytecode engine class Library, BCEL) of Jakarta, but I can also choose CGLIB, ASM or SERP.

Since I will refactor bytecode in a variety of different ways, I will start with declaring the restructured universal interface. It is similar to a simple framework component that executes annotation-based refactoring. This framework component supports class and method Conversion Based on annotations. Therefore, this interface has the following definition:

Public interface Instrumentor
{
Public void instrumentClass (ClassGen classGen, Annotation );
Public void instrumentMethod (ClassGen classGen, MethodGen methodGen, Annotation );
}

Both ClassGen and MethodGen are BCEL classes, which use the Builder pattern ). That is to say, they provide a way to change other immutable objects, and to convert between mutable and immutable representations.

Now I need to write an implementation for the interface. It must replace the @ Status Annotation with an appropriate StatusManager call. As mentioned above, I want to include these calls in the try-finally code block. Note that to achieve this goal, the annotation we use must be marked with @ Retention (RetentionPolicy. CLASS), which instructs the Java compiler not to discard the annotation during compilation. Since I previously declared @ Status as @ Retention (RetentionPolicy. SOURCE), I must update it.

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

The following is the bytecode that represents a normal method call. 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 located in the try-finally code block. Therefore, if it produces an exception, StatusManager. pop () is called ():

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 14 any

14 15 14 any

You can find that in order to implement a try-finally, I have to copy some commands and add several jump and exception table records. Fortunately, the InstructionList class of BCEL makes this work quite simple.

  Reconstruction of bytecode during runtime

Now I have an interface for modifying classes based on annotations and the specific implementation of this interface. The next step is to compile the actual framework component that calls it. In fact, I will write a small number of framework components, starting with restructuring the framework components of all classes at runtime. Since this operation occurs during the build process, I decided to define an Ant transaction for it. The Declaration of the reconstruction target in the build. xml file should be as follows:

<Instrument class = "com. pkg. OurInstrumentor">
<Fileset dir = "$ (classes. dir)">
<Include name = "**/*. class"/>
</Fileset>
</Instrument>

To implement such transactions, I must define a class that implements the org. apache. tools. ant. Task interface. The attributes and sub-elements of our transactions are passed in through the set and add methods. We call the execute method to implement the work that the firm wants to execute-In the example, it is To refactor 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 {
Directorytransferds = fileSet. getdirectorytransfer( project );
// Java 1.5 "for" Syntax
For (String file: ds. getIncludedFiles ()){
InstrumentFile (inst, file );
}
} Catch (Exception ex ){
Throw new BuildException (ex );
}
}
...
}

The BCEL 5.1 version used for this operation has a problem-it does not support analysis annotations. I can load the class being restructured and view the annotation Using reflection. However, in this case, I have to use RetentionPolicy. RUNTIME to replace RetentionPolicy. CLASS. I must also perform some static initialization in these classes, and these operations may load the local class library or introduce other dependencies. Fortunately, BCEL provides a plug-in mechanism that allows the client to analyze bytecode attributes. I have compiled my own AttributeReader implementation (implementation). When there is an annotation, it knows how to analyze the RuntimeVisibleAnnotations and RuntimeInvisibleAnnotations attributes in the bytecode. In future BCEL versions, this function should be included rather than provided as a plug-in.

The bytecode reconstruction method at the Compilation Time is displayed in the code/02_compiletime directory of the sample code.

However, this method has many defects. First, I must add additional steps to the setup process. I cannot choose to enable or disable the refactoring operation based on the command line setting or other information not provided during compilation. If the restructured or unreconstructed code needs to be run in the product environment at the same time, you must create two separate. jars files and decide which one to use.

Refactored bytecode during class loading

A better way is to delay the bytecode reconstruction operation until the bytecode is loaded. When this method is used, the reconstructed bytecode does not need to be saved. The performance of our application may be affected at startup time, but you can control what operations are performed based on your system attributes or runtime configuration data.

Before Java 1.5, we may use a custom class loader to maintain these types of files. However, the newly added Java. lang. instrument package in java 1.5 provides a few additional tools. Specifically, it defines the ClassFileTransformer concept, and we can use it to refactor a class during the standard loading process.

To register ClassFileTransformer at an appropriate time (before loading any class), I need to define a premain method. Java will call this method before loading the main class, and it is passed in to reference the Instrumentation object. I must also add the-javaagent parameter option to the command line to tell Java about our premain method. This parameter option takes the full name of our agent class (which includes the premain method) and any string as the parameter. In this example, we take the full name of the Instrumentor class as a parameter (it must be in the same line ):

-Javaagent: boxpeeking. instrument. InstrumentorAdaptor =
Boxpeeking. status. instrument. StatusInstrumentor

Now I have assigned a callback, which will occur before loading any classes containing annotations, and I have a reference to the Instrumentation object. You 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 adapter we registered here serves as a bridge between the Instrumentor interface provided above and the ClassFileTransformer interface of Java.

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, );
}
For (org. apache. bcel. classfile. Method m: cg. getMethods ()){
For (Annotation an: getAnnotations (m. getAttributes ())){
ConstantPoolGen CPP = cg. getConstantPool ();
MethodGen mg = new MethodGen (m, className, BMP );
Instrumentor. instrumentMethod (cg, mg, );
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 for restructuring bytecode at startup is located in the/code/03_startup directory in the example.

  Exception Handling

As mentioned above, I want to write additional code using @ Status annotations for different purposes. Let's consider some additional requirements: Our application must capture all the unprocessed exceptions and display them to users. However, we do not provide Java stack tracing, but display methods with @ Status annotations, and should not display any code (class or method name or row number, etc ).

For example, consider the following stack trace information:

Java. lang. RuntimeException: cocould 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 cause the GUI pop-up box shown in Figure 1. The above example assumes your YourCode. loadData (), YourCode. go () and YourCode. connectToDB () contains the @ Status annotation. Note that the exception order is the opposite. Therefore, the most detailed information is obtained first.


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

To implement these functions, I must slightly modify the existing code. First, to ensure that the @ Status annotation is visible at RUNTIME, I must update @ Retention again and set it to @ Retention (RetentionPolicy. RUNTIME ). Remember, @ Retention controls when the JVM discards annotation information. This setting means that the annotation can not only be inserted into the bytecode by the compiler, but also be accessed through reflection using the new Method. getAnnotation (Class) Method.

Now I need to arrange to receive notifications of any exceptions not explicitly handled in the code. In Java 1.4, the best way to handle unhandled exceptions on any specific thread is to use the ThreadGroup subclass and add a new thread to the ThreadGroup of this type. However, Java 1.5 provides additional functions. I can define an instance of the UncaughtExceptionHandler interface and register it for any specific thread (or all threads.

Note that it may be better to register for a specific exception in the example, but there is a bug in Java 1.5.0beta1 (#4986764), which makes this operation impossible. However, setting a handler for all threads can work, so I am doing this.

Now we have a method to intercept unhandled exceptions, and these exceptions must be reported to users. In a GUI application, in typical cases, such an operation is implemented by a mode dialog box containing the entire stack trace information or simple messages. In this example, I want to display a message when an exception occurs, but I want to provide the stack's @ Status description instead of the class and method name. To achieve this purpose, I simply query the StackTraceElement array of the Thread, find the java. lang. reflect. Method object related to each framework, and query its stack annotation list. Unfortunately, it only provides the method name and does not provide the signature of the method. Therefore, this technology does not support the Same Name (but the @ Status annotation is different) overload method.

The sample code can be found in the/code/04_exceptions directory of the peekinginside-pt2.tar.gz file.

Sampling)

I have a way to convert the StackTraceElement array to the @ Status annotation stack. This operation is more useful than that shown. Another new feature in Java 1.5-introspection-enables us to obtain an accurate StackTraceElement array from the currently running thread. With these two pieces of information, we can construct another Implementation of JstatusBar. StatusManager does not receive notifications when a method call occurs. Instead, it simply starts an additional thread to capture stack trace information and the status of each step during normal intervals. As long as the interval is short enough, the user will not feel the update delay.

The code behind the "sampler" thread follows the process of another thread:

Class StatusSampler implements Runnable
{
Private Thread watchThread;

Public StatusSampler (Thread watchThread)
{
This. watchThread = watchThread;
}

Public void run ()
{
While (watchThread. isAlive ()){
// Obtain stack trace information from the thread
StackTraceElement [] stackTrace = watchThread. getStackTrace ();
// Extract status messages from stack trace information
List <Status> statusList = StatusFinder. getStatus (stackTrace );
Collections. reverse (statusList );
// Use the status message to establish a certain State
StatusState state = new StatusState ();
For (Status s: statusList ){
String message = s. value ();
State. push (message );
}

// Update the current status

StatusManager. setState (watchThread, state );

// Sleep to the next cycle

Try {
Thread. sleep (SAMPLING_DELAY );
} Catch (InterruptedException ex ){}
}
// Status Reset
StatusManager. setState (watchThread, new StatusState ());
}
}

Compared with adding method calls, manual or refactoring, sampling is less invasive to programs. I don't need to change the setup process or command line parameters, or modify the startup process at all. It also allows me to adjust SAMPLING_DELAY to control the overhead. Unfortunately, when a method call starts or ends, there is no explicit callback for this method. In addition to the delay of status update, there is no reason for this code to receive callbacks at that time. However, in the future, I will be able to add some additional code to track the exact runtime of each method. You can precisely perform this operation by checking StackTraceElement.

The jstatusbarcode can be found in the/code/05_sampling directory of the peekinginside-pt2.tar.gz file.

  Refactored bytecode during execution

By combining the sampling method and reconstruction, I can form a final implementation that provides the best features of various methods. Sampling can be used by default, but the most time-consuming method of the application can be reconstructed individually. ClassTransformer is not installed in this implementation, but instead, ClassTransformer reconstructs the method one by one to respond to the data collected during the sampling process.

To implement this function, I will create a new class of InstrumentationManager, which can be used for refactoring and independent methods without refactoring. It can use the new Instrumentation. redefineClasses method to modify idle classes, and the code can be executed continuously. The StatusSampler thread added in the previous section now has additional responsibilities. It adds any "Discover" @ Status Method to the collection. It periodically identifies the worst offenders and provides them to the InstrumentationManager for refactoring. This allows the application to more accurately track the start and end times of each method.

One problem with the sampling method mentioned above is that it cannot distinguish between long-running methods and methods called multiple times in a loop. Since refactoring will increase the overhead for each method call, it is necessary to ignore frequently called methods. Fortunately, we can use refactoring to solve this problem. In addition to updating StatusManager, we maintain the number of times each refactoring method is called. If the value exceeds a certain limit (meaning that the overhead for maintaining the information of this method is too large), the sampling thread will permanently cancel refactoring of this method.

Ideally, I will store the number of calls to each method in the new fields of the class added during the refactoring process. Unfortunately, the class conversion mechanism added in Java 1.5 does not allow this operation; it cannot add or delete any fields. Instead, I will store the information in the static ing 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.

  Summary

Figure 4 provides a rectangle that shows the features and costs of the example I have provided.


   Figure 4. Analysis of Reconstruction Methods

You can find that the Dynamic (Dynamic) method is a good combination of various solutions. Similar to all examples using refactoring, it provides clear callbacks for method start or end times, so your application can accurately track runtime and provide feedback to users immediately. However, it can also cancel refactoring of a method (it is called too frequently), so it will not be affected by performance problems encountered by other refactoring solutions. It does not include the compile-time step, and it does not add additional work in the class loading process.

  Future trends

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

This may seem insignificant with the help of source code refactoring, because the Formatter. format method I use uses variable parameters (the "magic" function added in Java 1.5 ). The restructured version is similar to the following:

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" function is fully implemented in the compiler. In bytecode, Formatter. format uses Object [] as a parameter. 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.

Because it can only be used for refactoring (in this case, the method parameters are available) but not for sampling, you may want to refactor these methods at startup, or, at least, the dynamic implementation is biased towards refactoring of any method, and the alternative mode can be used in messages.

You can also track the number of times each restructured method call starts, so you can more accurately report the number of times each method runs. You can even save historical statistics of these times and use them to form a real progress bar (instead of an uncertain version I used ). This capability will give you a good reason to refactor a method at runtime, because the overhead of tracking any independent method is very obvious.

You can add the "debug" mode to the progress bar, regardless of whether the method call contains the @ Status annotation, to report all method calls during the sampling process. This is invaluable to any developer who wants to debug deadlocks or performance issues. In fact, Java 1.5 also provides a programmable API for the deadlock Detection. When the application is locked, we can use this API to change the process bar to red.

The annotation-based refactoring framework component established in this article may be very market-oriented. A bytecode is allowed during compilation (through Ant transaction ),

Tools for Dynamic Time (using ClassTransformer) and Instrumentation (using Instrumentation) refactoring are undoubtedly valuable to a small number of other new projects.

  Summary

In these examples, you can see that meta-programming may be a very powerful technology. 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. We can see that many new features provided in Java 1.5 provide enhanced support for metadata programming. In particular, the combination of annotations and runtime refactoring provides a real dynamic form for Attribute-oriented programming. We can further use these technologies to make their functions go beyond the existing framework components (for example, the functions 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.