Peeking Inside the Box: Attribute-Oriented Programming with Java 1.5, Part 1 Author: Don Schwarz

xiaoxiao2021-03-06  72

Peeking Inside The Box: attribute-oriented programming with java 1.5

I recently spent an afternoon at the London Science Museum watching steam engines, pumps, turbines, and other antique machines operate. Some were so simple that their behavior was obvious, but most had sections cut away and replaced with glass windows so their inner workings could be observed. As I walked through the exhibits, I could not help but wonder how the craftsmen and maintenance crews of those machines were able to do their jobs without the cut-away models and diagrams that accompanied the exhibits here. Software engineers have it Easy!

To a seasoned Java developer with access to source code, any program can look like the transparent models at the museum. Tools such as thread dumps, method call tracing, breakpoints, and profiling statistics can provide insight into what a program is doing right now, has done in the past, and may do in the future. But in a production environment, things are not always so obvious. These tools are not typically available, or at the very least, they are usable only by trained developers. Support teams And end users also need to be aware of what an application is doing at any given time.

To fill this void, we've invented simpler alternatives such as log files (typically for server processes) and status bars (for GUI applications). However, because these tools must capture and report only a very small subset of the information available to them and often must present this information in a way that is easy to understand, they tend to be programmed explicitly into the application. This code is intertwined with the application's business logic in such a way that developers must "work around it" when trying to debug or understand core functionality, yet remember to update this code whenever that functionality changes. What we really want to do is to centralize the status-reporting logic in a single place, and manage the individual status messages as metadata.In this article, I will CONSIER THE CASE OF A STATUS-BAR Component Embedded In A Gui Application. I WILL Explore A Number of Different Ways To Implement this Status reporter, Starting with the traditional hard-code ID Iom. Along The Way, I Will Introduce and Discuss A Number of New Features in Java 1.5, Including Annotations and Run-Time Bytecode Instrument.

The Statusmanager

..

Figure 1. Our Dynamically Generated Status Bar

Since I do not wish to reference any GUI components directly from our business logic, I'll also create a StatusManager to act as an entry point for status updates. The actual notification will be delegated out to a StatusState object so that later this can be Extended to Support Multiple Concurrent Threads. Figure 2 Shows this Arrangement.figure 2. StatusManager and Jstatusbar

Now I need to write code which that call the methods on StatusManager to report on the progress of the application. These method calls are typically scattered throughout the code in try-finally blocks, often one per method.

Public void connectiontoc (String URL) {

StatusManager.push ("Connecting to Database");

Try {

...

} finally {

Statusmanager.pop ();

}

}

................................

This code is functional, but after duplicating it dozens or even hundreds of times throughout your codebase, it can begin to look a bit messy. In addition, what if I want to access these messages in some other way? Later in this article, I 'll define a user-friendly Exception Handler That Sharees The Same Messages. The question Mething In The Implementation of Our Method, Rather Than On The Interface Where It Belongs.

Attribute-oriented programming

What I really want to do is to leave any references to StatusManager out of our code altogether and simply tag the method with our message. I can then use either code-generation or run-time introspection to do the real work. The XDoclet Project refers to this as Attribute-Oriented Programming, and provides a framework which can convert custom Javadoc-like tags into source code.However, with the inclusion of JSR-175, Java 1.5 has provided a more structured format for including these attributes inside of real code . The attributes are called "annotations" and they can be used to provide metadata for class, method, field, or variable definitions. They must be declared explicitly, and provide a sequence of name-value pairs that can contain any constant value (including Primitives, Strings, Enumerations, and classes.

Annotations

To hold our status message, I'll want to define a new annotation that contains a string value. Annotation definitions look a lot like interface definitions, but the keyword @interface is used instead of interface and only methods are supported (though they act more Like Fields.

Public @Interface status {

String value ();

}

Like an interface, I will put the @INTERFACE IN A File named status.java, And Import It Into Any Other Files That Need To Reference IT.

. Value may seem like a strange name for our field here Something like message might be more appropriate; however, value has special meaning to Java This allows us to declare annotations as @Status ( "...") rather than @Status (. Value = "..."), as a shortcut.

I can now define my method as stock:

@Status ("Connecting to Database")

Public void connectiontoc (String URL) {

...

}

Note That To Compile This Code I Needed to Use the - Source 1.5 Option. If ''re Using Ant (As Our Code Samples DO) Rather Than Building A Javac Command Line Directly, You'll Also Need Ant 1.6.1.in Addition to classes, methods, fields, and variables, annotations can also be used to provide metadata for other annotations In particular, Java comes with a few annotations that can be used to customize how your annotation works Let's redefine our annotation as follows..:

@Target (ElementType.Method)

@RetionPolicy.Source

Public @Interface status {

String value ();

}

The @Target annotation defines what the @Status annotation can reference. Ideally, I would like to mark up arbitrary blocks of code, but the only options are methods, fields, classes, local variables, parameters, and other annotations. I'm only INTERESTED IN CODE, SO i choise method.

The @Retention annotation allows us to specify when Java is free to discard this information. It can be either SOURCE (discard when compiling), CLASS (discard at class load time), or RUNTIME (do not discard). Let's start with SOURCE, But I will need to upgrade it lat.

Instrumenting Source Code

Now that my messages are encoded in metadata, I'll need some code to notify the status listeners. Let's assume for the moment that I continue to store my connectToDB method in source code control without any references to StatusManager. However, before compiling the class I want to add in the next - finals..........................

The XDoclet framework is a source code generation engine for Java that uses annotations similar to those described above, but stored in Java source code comments. XDoclet works very well for generating entire Java classes, configuration files, or other build artifacts, but does not support the modification of existing Java classes. This limits its usefulness for instrumentation. Instead, I could use a parser tool like JavaCC or ANTLR (which comes with a grammar for parsing Java source code), but that requires a fair amount of effort.There seems to be no good tool available for doing source code instrumentation of Java code. There may be a market for such a tool, but as you will see in the rest of this article, bytecode instrumentation can be a much more powerful technique.

INSTRUMENTING BYTECODE

Rather than instrumenting source code and then compiling it, I could compile the original source code and then instrument the bytecode that is produced. Depending on the exact transformation required, this can be either much easier or much more difficult than source code instrumentation. The main Advantage of Bytecode Instrumentation is this the code can be be modified at run time, WITHOUT HAVING A Compiler Available.

Although Java's bytecode format is relatively simple, I will certainly want to use a Java library to do the parsing and generation of the bytecode (eg, to insulate us from future changes in the Java class file format). I chose to use Jakarta's Byte Code Engineering Library (BCEL), But I COULD JUST AS Easily Have Picked CGLIB, ASM, or Serp.

Since I will be instrumenting bytecode in a number of different ways, I'll begin by declaring a generic interface for instrumentation. This will act as a simple framework for doing annotation-based instrumentation. This framework will support the transformation of classes and methods, Based ON Annotations, So The Interface Will Look Something Like this: 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 implement the Builder pattern. That is, they provide methods for mutating an otherwise immutable object, and for transforming between the mutable and non-mutable representations.

Now I will need to write an implementation for this interface that replaces @Status annotations with the appropriate calls to StatusManager. As described previously, I want to wrap these calls in a try-finally block. Note that for this to work, the annotations that are used must be tagged with @Retention (RetentionPolicy.CLASS), which instructs the Java compiler not to discard the annotations while compiling. Since I declared @Status as @Retention (RetentionPolicy.SOURCE) earlier, I need to upgrade it.

As it turns out, in this case, instrumenting bytecode is significantly more difficult than instrumenting source code. The reason is that try-finally is a concept that exists in source code only! The Java compiler transforms try-finally blocks into a series of try -catch block and inserts calls to the finally block before every return. thus, i will need to do something similar in order to do something, similar ,,,,,,,,,,,,

This is the bytecode what represents An Ordinary Method Call, Flanked 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

This Is The Same Method Call, But IN a Try-Finally Block, SO That StatusManager.pop () IS Called Even EXCEPTION IS THROWN.

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

As you can see, I NEOU CAN SEE, I NEED TO DUPLICATE SOME INSTRUCTION TABLE RECORDS JUST TO IMPLEMENT A SINGLE TRY-FINALLY. LUCKILY, BCEL'S INSTRUCTIONLIST CLASS MAKES THIS FAIRLY EASY.

Instrumenting bytecode At Compile Time

Now that I have an interface for modifying classes based on annotations and a concrete implementation of this interface, the last step is to write the actual framework that will call it. I'm actually going to write a few of these frameworks, starting with one that instruments all classes at a compile time. Since this is going to happen as part of my build process, I've decided to define an Ant task for it. The declaration of the instrumentation target in my build.xml file should look something like THIS:

To provide an implementation of this task, I need to define a class that realizes the org.apache.tools.ant.Task interface. Attributes and sub-elements of our task are passed in through set and add method calls. The execute method is Called to Implement The Work of The Task - In this Case, To Instrument The Class Files Given in The Specified .

Public class instructiontask 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);

// java 1.5 "for" Syntax

FOR (String File: ds.getincludedfiles ()) {

Instrumentfile (inst, file);

}

} catch (exception ex) {

Throw New BuildException (ex);

}

}

...

}

The one problem with using BCEL for this purpose is that as of version 5.1, it does not support parsing annotations. I could load the classes that we're instrumenting and use reflection to view the annotations. However, then I would have had to use RetentionPolicy.RUNTIME instead of RetentionPolicy.CLASS. I'd also be executing any static initializers in those classes, which may load native libraries or introduce other dependencies. luckily, BCEL provides a plugin mechanism that allows clients to parse bytecode attributes that it does not natively support. I've written my own implementation of the AttributeReader interface that knows how to parse the RuntimeVisibleAnnotations and RuntimeInvisibleAnnotations attributes that are inserted into bytecode when annotations are present. Future versions of BCEL should include this functionality without the need for a plugin.This Compile Time Bytecode Instrumentation Approach Is Shown In The Directory Code / 02_compiletime of The Sample Code.

There are a number of disadvantages to this approach, however. For one thing, I had to add an additional step to my build process. I also can not turn the instrumentation on or off based on command-line settings or other information that is not available At Compile Time. if Both INSTRUMENTED AND NON-INSTRUMENTED CODE NEEDS to Be Run in a production setting, two separate .jars will need to be built and the decision of which to use mustness.

Instrumenting bytecode at class loading time

A better solution would be to delay the instrumentation of our bytecode until after it is loaded from the disk. This way, the instrumented bytecode does not need to be stored. The start-up performance of our application may suffer, but the trade-off is that you can control what happens based on system properties, or other runtime configuration data.Prior to Java 1.5, it was possible to do this type of class-file manipulation with a custom class loader. However, the new java.lang.instrument ................... ..

To register our ClassFileTransformer at the appropriate time (before any of our classes are loaded), I'll need to define a premain method. Java will call this before the main class is even loaded, and it is passed a reference to an Instrumentation object . I will also need to add a -javaagent option to the command line to tell Java about our premain method with Java. This argument takes the full name of your agent class (which contains the premain method) and an arbitrary string argument. In this Case, We'll Pass The Full Name of Our Instrumentor Class As The Argument (this stay all be on one line):

-JavaAgent: Boxpeeking.instrument.instrumentoradaptor =

Boxpeeking.status.instrument.StatusInstrumentor

Now That I'VE Arranged for a callback That Occurs Before ANNOTATED CLASSES ARE LOADED, AND I HAVE A Reference To The Instrumentation Object, i canreguse Our ClassFileTransformer.

Public static void premain (String ClassName,

Instrumentation I)

Throws ClassNotFoundException,

InstantiationException,

IllegaCCESSEXCEPTION {

Class Instclass = Class.Forname (classname);

INSTRUMENTOR INST = (Instrumentor) instclass.newinstance ();

I.AddTransformer (New InstrumentoraDaptor (INST);

}

..................

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);

a Annotation AN:

GetanNotations (jc.getattributes ())) {

Instrumentor.instrumentClassClass (CG, AN);

}

For (org.apache.bcel.classfile.method M:

cg.getMethods ()) {

a Annotation AN:

GetanNotations (M.GetaTRibutes ())) {

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 ("instructing"

ClassName, EX);

}

}

...

}

This Approach of Instrumenting Bytecode At Startup Is Shown In The Example Code'S / Code / 03_Startup Directory.

Conclusion

转载请注明原文地址:https://www.9cbs.com/read-121550.html

New Post(0)