GluonJ

       GluonJ是一个简单的面向切面编程(AOP)的java工具.GluonJ针对面向对象的语言(OOP)结构提供了一个极其简单的扩展方式实现了AOP的特性,非常独特:不同于其他流行的AOP框架,没有采用pointcut-advice,而是采用了revisers 和 within methods. 

       Gluonj是较高级别的API,底层使用javassist进行java类文件处理.

       有意思的项目名称:Gluon+J,J指的是java,Gluon翻译后是胶子(物理名词),gluonJ的特性与胶子还真是有几分相似之处.项目的作者对物理也有研究?

Gluonj的HelloWold

原始类:Person 扩展类:SayHello
package test;
public class Person {
public void greet() {
System.out.println("Hi!");
}
public static void main(String[] args) {
new Person().greet();
}
}
package sample;
public class SayHello revises test.Person {
public void greet() {
System.out.println("Hello!");
}
}
编译&运行:

#编译类文件
java -jar GluonJCompiler.jar test/Person.java sample/SayHello.java
#织入代码
java -jar gluonj.jar test/Person.class sample/SayHello.class

#运行

java test.Person

#输出:

Hello!

注意:
revises不是java原语(在java中通过注解@revises使用gluonJ会在下文中介绍),在eclipse中会显示错误,编译需要通过GluonJCompiler内置的编译器进行解析编译.
 
理解GluonJ 
       简单来讲,GluonJ 编译器把reviser 转换为一个普通的java 子类.所有target class(原始类)实例的行为将转换(通过调用)为该子类实例的行为.(在下文反编译后的文件可看到)
 
术语
  • reviser:中文翻译为修订者或校对者
    扩展类,类似java继承.该类中使用revises( within、requires、using等 )语句对原始类(target)进行扩展。
  • target class:原始类,编译后target class的字节码将被修改.
  • compilation&post-compile transformation
    gluonJ编译包含两步:source-to-bytecode translation 和 bytecode-to-bytecode translation,我们称之为compilation 和 post-compile transformation.
    compilation 过程使用GluonJCompiler.jar完成源码(文本文件)到字节码的转换
    过程类似javac,参数(option)与javac也基本兼容.
    post-compile 过程使用gluonj.jar完成字节码修改,以实现代码织入
    该过程接受三个选项:-debug(开启调试模式),-d(输出文件夹),-cp(class path)。并注意参数是.class文件不是.java文件.
    代码织入并非将reviser中代码放入原始类中,而是在原始类中加入reviser的执行句柄.
    反编译后的Person.class文件 反编译后的SayHello.class文件
    package test;
    import java.io.PrintStream;
    import sample.SayHello;

    public class Person
    {
    public void greet()
    throws
    {
    System.out.println("Hi!");
    }
    public static void main(String[] args) throws {
    new SayHello().greet();
    }
    }
    package sample;

    import java.io.PrintStream;
    import javassist.gluonj.Reviser;
    import test.Person;

    @Reviser
    public class SayHello extends Person
    {
    public void greet()
    throws
    {
    System.out.println("Hello!");
    }

    private void _init()
    throws
    {
    }
    }
    每次都是new一个对象么?是否会影响效率,如何保持数据状态,需要做进一步实验或查看相关gluonj的实现.

 

功能&特性

  • reviser与子类(subclass)很像(但不是).可基本(出去下文中的限制部分)按照子类的方式实现reviser类.
  • 可扩展的行为(这些行为都将附加到原始类中)包括:
    添加新方法、重写方法(可通过super调用原方法),实现新的接口
  • reviser的安全性遵从java,可访问target class可见成员,不能访问私有(private)成员,不能重写final方法.
  • 可以重写static方法

限制

  • target class中的每个构造函数都需要在reviser中声明(可以理解为overriding).
  • reviser不能重载(overloading)或添加构造函数
    reviser不能声明构造函数,当你需要一个新的构造函数时,只能在reviser中使用工厂方法(factory method)
  • reviser不能声明多个within 方法
    可以通过使用多个reviser(revise相同类)的方式实现此目的。
  • reviser不能被实例化.
  • 不能定义reviser的子类(subclass)

各种详细用法:

Ant task

load-time weaving

Load-time weaving without a Java agent

Override static methods

Requires

Using

WithIn methods

Using a reviser in Java

--Grouping

--Accessing new members added by revisers

--Constructors of an @Reviser class

Ant task

GluonJ程序的编译过程(当前只有post-compile)可以配置为一个ant task.示例如下

<?xml version="1.0"?>
<project name="hello" basedir=".">
<taskdef name="weave" classname="javassist.gluonj.ant.taskdefs.Weave">
<classpath>
<pathelement location="./gluonj.jar"/>
</classpath>
</taskdef>

<target name="weave">
<weave destdir="./out" debug="false" >
<classpath>
<pathelement path="./classes"/>
</classpath>
<fileset dir="./classes" includes="test/**/*" />
</weave>
</target>
</project>

熟悉ant的童鞋一看就明白了,在此就不多介绍了.(debug参数同post-compile中debug参数).

 

load-time weaving 加载时织入

post-compile可以在加载时在执行.采用此方式时,需要在jvm启动参数中添加-javaagent.示例如下:

java -javaagent:gluonj.jar=sample.SayHello test.Person
注意事项:
  • "gluonj.jar=”后是reviser的名字(注意没有后缀).多个riviser时,使用英文逗号进行分割.
  • -javaagent:…是一个VM参数,在使用ecilpse或其他IDE启动时需要注意进行相应配置.
  • 如果需要查看详细的日志信息,需要使用debug参数,类似:
    java -javaagent:gluonj.jar=debug:sample.SayHello test.Person
    注意debug的位置.

Load-time weaving without a Java agent 不使用java agent在加载时织入

不适用VM参数-javaagent 而在加载时织入的办法是,写类似如下的启动类:

import javassist.gluonj.util.Loader;
public class Runner {
public static void main(String[] args) throws Throwable {
Class mainClass = test.Person.class;
Class[] revisers = { sample.SayHello.class };
Loader.run(mainClass, args, revisers);
}
}
编译及运行:
javac -cp .:gluonj.jar Runner.java 
java -cp gluonj.jar Runner
此方法的思路是编写自定义的启动类,该启动类中调用gluon的api,以实现类的织入.此方法支持在运行期间动态织入,但需要注意原始类(Person.class)需要之前没有被当前classloader加载过,否则可能会织入失败.运行期间做类的修改建议直接调用javassist或其他classworking的工具进行修改.

 

Override static methods 重写static方法

不同于普通class,reviser可重写target class的static方法,例如:

target clas
reviser
package test;
public class Fact {
public static int fact(int n) {
if (n == 1)
return 1;
else
return n * fact(n - 1);
}
public static void main(String[] args) {
System.out.println(fact(5));
}
}
package sample;
import test.Fact;
public class FactLogger revises Fact {
public static int fact(int n) {
System.out.println("* " + n);
return Fact.fact(n);
}
}

编译在反编译后:

Fact.class FactLogger.class
package test;

import java.io.PrintStream;
import sample.FactLogger;

public class Fact
{
public static int fact(int n)
throws
{
if (n == 1) {
return 1;
}
return n * FactLogger.fact(n - 1);
}
public static void main(String[] args) throws {
System.out.println(FactLogger.fact(5));
}
}
package sample;

import java.io.PrintStream;
import javassist.gluonj.Reviser;
import test.Fact;

@Reviser
public class FactLogger extends Fact
{
public static int fact(int n)
throws
{
System.out.println("* " + n);
return Fact.fact(n);
}

private void _init()
throws
{
}
}

可以看到Fact.fact方法被修改了.细心的童鞋会发现这里可能会有个疑问:main方法中的Fact.fact的调用也被改成了FactLogger.fact,如果有一个类比如TestA也调用了Fact.fact方法,由于编译的时候没有将TestA.class列入到参数列表中,那么TestA中的Fact.fact(5)调用不会被修改,那么结果将产生4条输出.log还好,那如果FactLogger在return时做了 -1操作,那就会有问题了.读者可做个测试.在实际应用中要注意此问题.(当然如果只是log不会有啥大问题).

 

Requires

gluonJ允许在同一个target class上有多个reviser,在这种情况下,需要通过requires语句指定revisers的顺序.

package sample;
public class GoodDay requires SayHello revises test.Person {
public void greet() {
super.greet();
System.out.println("It's a good day today.");
}
}
gluon将按照顺序对target class进行扩展.方法调用的顺序采用Last In First Out(LIFO)的方式.在上例中,调用Greet方式时,GoodDay中的greet将首先被调用,由于方法中调用super.greet(),所以SayHello中的greet方法被调用.所以输出结果会是:Hello! It's a good day today. 反编译后的文件.
可以理解为:GoodDay extend SayHello,SayHello extend test.Person .(实际上也类似)
编译(post compile)时,也需要保持顺序:
java -jar gluonj.jar test/Person.class sample/SayHello.class sample/GoodDay.class 
load time weaving:
java -javaagent:gluonj.jar=sample.GoodDay test.Person
load time weaving模式下 GluonJ 会自动加载所有的required revisers,所以不必:
java -javaagent:gluonj.jar=sample.GoodDay,sample.SayHello test.Person
既然在l-t-w模式下,gluonj会自动加载所有的requires revisers,所以你可以写一个reviser,该reviser直接或间接的requires 所有必要的revisers,这样在运行程序是只需要指定该reviser做为load time weaver的参数.
Using
  • Reviser R可以访问 R requires的其他revisers添加的可见成员.例如:
    LinkNode:
    package test;
    public class LinkNode {
    protected LinkNode next = null;
    public static void main(String[] args) {
    LinkNode n = new LinkNode();
    System.out.println(n);
    }
    }
    IntNode:
    package sample;
    import test.LinkNode;
    public class IntNode revises LinkNode {
    public int value;
    public int get() { return value; }
    }
    PrintableNode:
    package sample;
    import test.LinkNode;
    public class PrintableNode requires IntNode revises LinkNode {
    public String toString() { return "Node:" + get() + " " + value; }
    }
    PrintableNode requires IntNode,就可以访问IntNode中的get方法. 通过前面的分析可知,PrintableNode在编译后继承了IntNode,当然就可以调用IntNode中的get方法了.
  • Normal class 可通过Using声明访问Reviser成员.
    package test;
    using sample.IntNode;
    public class NodeTest {
    public static void main(String[] args) {
    LinkNode n = new LinkNode();
    System.out.println(n.value);
    }
    }
    using语句类似import语句,声明了要访问的reviser.
    编译(编译后class及反编译下载)&执行语句:

    java -jar GluonJCompiler.jar test/LinkNode.java sample/IntNode.java sample/PrintableNode.java test/NodeTest.java
    java -jar gluonj.jar test/LinkNode.class sample/IntNode.class sample/PrintableNode.class test/NodeTest.class
    java -cp . test/NodeTest

    结果:0

    反编译后的程序还是挺有意思的.试想如果在编译的时候去掉printableNode,会是什么结果呢?
WithIn methods
reviser中可以声明一个within方法.声明了with的方法当调用者是指定的方法或类才生效.用法类似aspectj的pointcut.示例如下:
Position类
package test;
public class Position {
public int x;
public void rmove(int dx) { setX(x + dx); }
public void setX(int newX) {
System.out.println("setX");
x = newX;
}
}
PosTest类
package test;
public class PosTest {
public static void main(String[] args) {
Position p = new Position();
p.setX(5);
p.rmove(11);
}
}
PosLgger类
package sample;
public class PosLogger revises test.Position {
public void setX(int newX) within test.PosTest.main(String[]) {
System.out.println("x: " + x + ", newX: " + newX);
super.setX(newX);
}
}
编译&运行:
java -jar GluonJCompiler.jar test/Position.java sample/PosLogger.java  test/PosTest.java
java -jar gluonj.jar test/Position.class sample/PosLogger.class test/PosTest.class
java -cp . test/PosTest
输出:
x: 0, newX: 5
setX
setX
可发现PosTest.main方法调用的setX增加了日志输出,rmove调用的setX没有增加日志输出.
通过反编译可知,within语句在reviser和target class中增加了setX_aop1的方法,reviser中的setX_aop1方法中做了日志输出.
within只能声明一个class name,如果希望该类中所有调用该方法的方法都触发,上例中做如下修改即可:
package sample;
public class PosLogger revises test.Position {
public void setX(int newX) within test.PosTest {
System.out.println("x: " + x + ", newX: " + newX);
super.setX(newX);
}
}

Using a reviser in Java 在java中使用reviser
我们还可以通过java注解的方式对plain java使用Gluonj.因此也可以使用一般的java编译器(javac)编译,完成源代码对字节码的转换.然后使用post-compile的方式运行.事实上之前的第一步(compiletation)就是将gluonj程序转换为使用了注解的java 字节码.
使用注解重写SayHello reviser:
package sample;
import javassist.gluonj.Reviser;

@Reviser public class SayHello extends test.Person {
public void greet() {
System.out.println("Hello!");
}
}
编译&run:
javac -cp .:gluonj.jar test/Person.java sample/SayHello.java
java -jar gluonj.jar test/Person.class sample/SayHello.class
java test.Person
--load-time weaving:
javac -cp .:gluonj.jar test/Person.java sample/SayHello.java
java -javaagent:gluonj.jar=sample.SayHello test.Person
--require:
package sample;
import javassist.gluonj.Reviser;
import javassist.gluonj.Require;

@Reviser @Require(SayHello.class)
public class GoodDay extends test.Person {
public void greet() {
super.greet();
System.out.println("It's a good day today.");
}
}
多个requires的情况(需要注意顺序):
@Require({SayHello.class, SayChao.class})
@require是个java.lang.Class的数组.
--within method:
package sample;
import javassist.gluonj.Reviser;
import javassist.gluonj.Within;
import javassist.gluonj.Code;

@Reviser public class PosLogger extends test.Position {
@Within(test.PosTest.class) @Code("main(java.lang.String[])")
public void setX(int newX) {
System.out.println("x: " + x + ", newX: " + newX);
super.setX(newX);
}
}
@Within的参数是java.lang.Class
@Code是可选的,描述一个方法的标识(signature),表明只有这个方法的调用才会被revise,如果没有此参数,标识@within描述的Class的所有方法对此方法的调用都会被revise.
注意@Code的参数必须是全路径名称(fully qualified class name).
--Grouping
一个源文件中应该存放多个关注于"同样事情"的revisers.这样做的好处是显而易见的.
可通过在注解了@Reviser的类中使用内嵌static类(也注解了@Reviser)来实现.即相关的reviser通过这样的方式分组到同一个类文件中.内嵌的reviser不必是相同的target class.例如:
package sample;
import javassist.gluonj.Reviser;

@Reviser public class Say {
@Reviser public static class SayHello extends test.Person {
public void greet() {
System.out.println("Hello!");
}
}

@Reviser public static class GoodDay extends test.Person {
public void greet() {
super.greet();
System.out.println("It's a good day today.");
}
}
}
test.Say是Person的reviser的聚合类.这些static nested classes非显示被Say所require.require的顺序是按照字母顺序(此顺序依赖编译器).值得注意的是,Say的父类(或target class)并没有显示的描述.这样一个@Reviser的类并没有revise任何类.Say只是requires其内嵌的static且注解了@Reviser的类.
内嵌的SayHello与GoodDay并须是static类.Inner class不能注解@Reviser.
运行:
javac -cp .:gluonj.jar test/Person.java sample/Say.java
java -javaagent:gluonj.jar=sample.Say test.Person
在第二行执行语句中sample.Say作为参数传入,它自动将其@Require的类织入.
--Accessing new members added by revisers
使用java注解的方式实现reviser,使访问reviser新增的成员变得繁琐。由于java编译器不解析reviser的语义,当需要访问一个由reviser添加的成员时,你必须显示的将对象实例转化为reviser类型来绕过(欺骗)java编译器.见下面的例子:
package sample;
import test.LinkNode;
import javassist.gluonj.Reviser;
import javassist.gluonj.Require;
import static javassist.gluonj.GluonJ.revise;

@Reviser @Require(IntNode.class)
public class PrintableNode extends LinkNode {
public String toString() {
IntNode in = (IntNode)revise(this);
return "Node:" + in.get() + " " + in.value;
}
}
为了访问IntNode添加的get方法(PrintableNode继承了LinkNode,get方法对其是不可见的),toString方法中将this从PrintableNode转换为IntNode。
revise是GluonJ中的静态方法,将对象实例转做类型转换.revise方法等同于:
public String toString() {
IntNode in = (IntNode)(Object)this;
return "Node:" + in.get() + " " + in.value;
}
在jdk1.6的java编译器中支持这种转换方式.(先转换为Object,在转换为IntNode来绕过方法的继承检验?)
注意:
  • @Require注解仍然是需要的,以描述reviser的优先顺序.如果不指定IntNode与PrintableNode的顺序,将会报错.
  • using语句没有对应的java注解,using只是控制reviser添加成员的可见性.

--Constructors of an @Reviser class

在上文reviser的限制中介绍了关于构造函数的限制:不能有子类,不能实例化,构造函数必须实现target class中所有的构造函数等.下面用实例来说明:

target class Counter reviser:Incrementable
package test;
import sample.Incrementable;
import static javassist.gluonj.GluonJ.revise;

public class Counter {
protected int counter;
public Counter(int c) { counter = c; }
public int get() { return counter; }
public void decrement() {
if (--counter <= 0)
throw new RuntimeException("Bang!");
}
public static void main(String[] args) {
Counter c = new Counter(1);
((Incrementable)revise(c)).increment();
c.decrement();
System.out.println(c.get());
}
}
package sample;
import javassist.gluonj.Reviser;

@Reviser public class Incrementable extends test.Counter {
private int delta;
public Incrementable(int c) {
super(c);
delta = 1;
}
public void increment() {
counter += delta;
}
}
Counter中定义了一个构造函数,所以reviser中也定义了一个构造函数.如果Counter中定义了多个构造函数,那Incrementable中也需要定义同样数量(请同样参数)的构造函数.
为了方便,如果target class定义了多一个构造函数,Reviser可以只定义一个默认的无参的构造函数.如果target class没有定义无参构造函数,那么reviser必须调用(call)target class的其他构造函数,然而,当reviser织入后,这个调用将被忽略。 示例:
package sample;
import javassist.gluonj.Reviser;

@Reviser public class Incrementable extends test.Counter {
private int delta;
public Incrementable() {
super(0);
delta = 1;
}
public void increment() {
counter += delta;
}
}
当Incrementable织入后,Incrementable构造函数的方法体将被添加到Counter的构造函数中,同时super(0)的调用也被删除了.例如上例中Counter的构造函数将被修改为:
public Counter(int c) {
counter = c;
delta = 1; // copied from the @Reviser class
}
编译代码:
javac -cp .:GluonJCompiler.jar:gluonj.jar test/Counter.java sample/Incrementable.java 
java -jar gluonj.jar test/Counter.class sample/Incrementable.class
反编译后的的class是这样的:
Counter.class Incrementable.class
package test;

import java.io.PrintStream;
import javassist.gluonj.GluonJ;
import sample.Incrementable;

public class Counter
{
protected int counter;

public Counter(int paramInt)
{
this.counter = paramInt; }
public int get() { return this.counter; }
public void decrement() {
if (--this.counter <= 0)
throw new RuntimeException("Bang!");
}

public static void main(String[] paramArrayOfString) {
Incrementable localIncrementable = new Incrementable(1);
((Incrementable)GluonJ.revise(localIncrementable)).increment();
localIncrementable.decrement();
System.out.println(localIncrementable.get());
}
}
package sample;

import javassist.gluonj.Reviser;
import test.Counter;

@Reviser
public class Incrementable extends Counter
{
private int delta;

public void increment()
{
this.counter += this.delta;
}

private void _init()
{
0;
this.delta = 1;
}

public Incrementable(int paramInt)
{
super(paramInt);
Object localObject = null;
super._init();
}
}
实在是有意思,和官方文档描述的差别还是挺大的:
  • Counter构造函数没有发生变化,调用构造函数的地方被修改成了调用reviser的构造函数
  • Reviser中的无参构造函数改成了有参构造函数.

童鞋们在使用的时候一定要注意此处,特别是有其他代码调用构造函数的情况.


posted @ 2011-06-03 15:05 redcreen 阅读(...) 评论(...) 编辑 收藏