http://dev2dev.bea.com.cn/bbs/jishudata/ArticleShow.jsp?Id=17
作者:sdj21,rocksun(dev2dev ID)
摘要:
本文介绍了如何使用JDK5.0的新特性Annotation和Spring框架来实现一种简单的JavaControl框架,主要实现了成员变量的自动带入,以及方法调用的权限检查控制,同时实现了一个JavaDatabaseControl的扩展,模仿workshop中的对应功能。
目录:
环境
背景资料
JavaControl最基本的功能--声明注入
使用动态代理来增加横切
Control方法的权限检查
一个DatabaseControl的特例
关于Demo程序
框架如何的加强
感受
代码下载
参考文档
环境 (目录)
由于使用了Annotation,所以必须准备好JDK5.0
背景知识 (目录)
1)Annotation简介
在Java领域,最早的Annotation就是JavaDoc了,将文档直接写在源程序里极大的方便了文档的编写,后来出现了许多有这种同步需求的工作,大家便发明了XDoclet,使用类似于JavaDoc的语法撰写描述信息,并使用工具生成描述文件。到JDK5.0出现以前这种需求已经更多了,许多工具和框架已经通过各种方式实现自己的这种标记,.NET更是率先推出了语言级的支持,所以JDK5.0终于推出了自己的Annotation。
以下是两个简单的Annotation定义,定义的方式类似于接口,只是在interface前面多了个"@"
public @interface SampleAnnotation{
String someValue;
}
public @interface NormalAnnotation{
String value1;
int value2;
}
然后我们在程序里可以这样使用Annotation,我们的编程工具或者是运行中的框架程序可以读取这些内容,生成代码或者是添加动作。
@SampleAnnotation (someValue ="Actual Value used in the program”)
public void annotationUsingMethod(){
…
}
Annotation主要是给工具开发商和框架开发者使用的工具,一般的编程人员可能仅仅是使用其他人开发的Annotation。Workshop在两年前已经开始尝试在开发工具里运用Annotaion,但当时没有语言级的支持,所有的Annotation都是以注释的形式出现,这样虽然灵活但是不严谨,也不适于推广,所以Annotation的出现可以大大方便开发商的工作,使得许多小开发商以及一般的架构设计人员也可以利用这种方式编程。
注:本文中Annotation可能对应的名字是“注释”或者是“说明”
2)Spring和DI(IOC)简介
在我看来,Spring和Annotation能完成很多相似的工作,它们之间的区别是在解决问题的位置并不相同。
这是一个Spring的配置文件的信息,里面定义了三个bean,其中的exampleBean的属性中引用了其他的bean
[bean="yetAnotherBean"/>]
1
我们用以下方式调用
InputStream is = new FileInputStream("beans.xml");
XmlBeanFactory factory = new XmlBeanFactory(is);
Object o = factory.get("exampleBean");
通过以上方式我们可以得到一个exampleBean的实例,并且里面的一些属性已经被预先注入了。
在Spring结合了一些动态代理的以后,我们完全可以实现Annotation所能做到的许多功能。但是我们可以看出两种方式是不一样的,使用Spring面临着同步问题,维护配置文件比较的麻烦;而使用Annotation时,我们通常不容易在不改变原代码的时候改变一些特性,而且有时候也面临着代码复用的问题。本文并不讨论这些内容,本文里Spring只是一个bean的容器,用来存放JavaControl的注册信息,不会涉及依赖注入,而使用Annotation来完成相应的功能。
注:在本文里使用了两个配置文件,bean.xml放置了我们测试用的Control信息,在实际环境中可能需要不断添加新的Control。另一个配置文件是config.xml,里面是我们定义的ControlWrapper,每当我们增加一种Control的时候,如DatabaseControl的时候,就需要添加一个Wrapper。你应该首先看看这两个文件,里面有demo程序的配置以及注册的Control。
JavaControl最基本的功能--声明注入(目录)
在JavaControl里编程的时候,我们通常并不会显式的初始化JavaControl,因为具体的实现不应该在程序里绑定,而应该完全的面向接口编程,我们只是简单的在声明Control的地方加一句简单的注释:
/**
* @common:control
*/
private myJavaControl.JCSecond jCSecond;
然后在我们的Control里就可以直接使用jCSecond,如下:
/**
* @common:operation
*/
public String serviceA()
{
System.out.println("This is serviceA");
return "This is serviceA"+":"+jCSecond.serviceB();
}
Workshop>平台会根据这些注释,自动生成代码,来完成初始化jCSecond的动作,来完成注入的工作。而我们的程序会在程序运行中读取注释信息,来完成注入工作,这与workshop不同。
至于这个接口具体要使用哪个实现类,大家可以看看workshop的打包文件的META-INFjc-jar.xml,里面有所有JavaControl的说明,包括接口和实现,这说明了我们以后可以改变实现。
下面我们实现自己的声明注入,这里我们需要一个JavaControlFactory类,作为进入Control环境的接口。两个Annotation,用来说明Control的实现类和包装器。JavaControlFactory类来读取Control中的注释信息,完成包装等工作。
1)首先我们需要定义一个修饰成员变量的Annotation,所有被当作Control的成员变量都可以使用这个annotation来说明,完整的定义如下:
package net.rocksun.tiffany.annotation;
import java.lang.annotation.*;
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.FIELD})
public @interface Control {
public String name();
}
这是个简单的annotation,@Retention说明本Annotation要保留到什么时候,因为我们需要在程序运行时动态的读取,所以设置为RUNTIME。@Target说明了本annotation所针对的对象,是FIELD。
我们在Control里这样使用这个annotation:
@Control(name="second")
private SecondControl second;
name是SecondControl的实现类名称,我们在这里说明,然后通过JavaControlFactory来根据名字自动的将SecondControl实例化。
2)我们还需要一个说明类的ControlType,用来注释所有的Control类:
package net.rocksun.tiffany.annotation;
import java.lang.annotation.*;
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.TYPE})
public @interface ControlType {
public String name();
}
它也是运行级别保留,但它针对的对象是类,只有类可以使用这个annotation注释。name表示这个类需要的包装器,也就是用来对这个类进行特殊处理的类。
使用方式如下
.............
@ControlType(name="DefaultControl")
public class FirstControlImpl implements FirstControl {
.............
@ControlType(name="DefaultControl")说明这个类需要使用DefaultControl类型的包装器包装一下。
3)ControlFactory实现
我们希望从JavaControl中取得类的方式为FirstControl first = (FirstControl)factory.getControl("first");并且first中所有的标记为Control的成员变量都可以被正确的初始化。
所以我们的ControlFactory首先应该遍历类first的所有成员变量,当遇到使用Control注释的成员变量,根据它的类型进行实例化。以下是遍历所有成员变量的过程:
for (int i = 0; i < fields.length; i++) {
//判断是否为Control,也就是检查是否使用@Control注释
if(isControl(fields[i])){
Object tempBean = getControlInstance(fields[i]);
if(tempBean==null){
throw new IllegalArgumentException(bean.getClass()+":"+fields[i].getName()+"’s annotation error");
}else{
//如果检查配置没有问题,就设置这个值,同时检查这个Bean的成员是不是也需要实例化
wrappedBean.setPropertyValue(fields[i].getName(), tempBean);
initBean(tempBean);
}
}
}
需要注意的是,所有的Control应该使用JavaBean的方式,所有成员变量应该有无参的构造方法,并且成员都有get和set方法。
其中isControl方法如下:
private boolean isControl(Field field){
Annotation[] annotation = field.getDeclaredAnnotations();
for (int i = 0; i < annotation.length; i++){
if(annotation[i] instanceof Control){
return true;
}
}
return false;
}
我们遍历这个字段的所有annotation,来检查有没有Control,如果有就执行getControlInstance,来实例化这个Control。getControlInstance会检查我们在类上作的ControlType注释,来选择包装器,进行bean的包装。
4)在实例化完成后,根据类的ControlType Annotation我们可以对类进行包装,每一个包装类实现如下接口:
public interface ControlWrapper {
public Object wrapBean(Object bean);
}
这样根据指定的ControlType的不同,我们还可以另外使用我们自定义的Control包装器,进行特殊的操作,我们首先实现了一个默认的DefaultControlType,不作任何操作。
到目前,我们的JavaControl最基本框架已经完成了,我们可以测试结果,可以看到,我们通过注释说明,就可以实现对成员变量的自动初始化,我们通过外部文件的配置就可以在以后方便的修改实现类。
可以察看源代码中ControlFactoryTest的testGetControl的运行结果。
使用动态代理来增加横切(目录)
对于每一个方法被执行的时候,我们希望可以进行一种横切,如每一个方法执行前我们要记一个Log,这就需要添加一个动态代理,当然我们也可以使用其他种方式,但是动态代理是最优雅的。
我们增加DefaultControlProxy类来处理横切的动作,DefaultControlProxy是InvocationHandler的子类,实现了ivoke方法,代码如下:
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
log.info("method "+method.getName()+" invoked");
return method.invoke(getDelegate(),args);
}
我们不作额外的操作,只是log被执行的方法,这样,所有的Control方法在执行的时候都会纪录log,这里的delegate是我们原来的对象,我们保留这个还有很多用处。 为了完成这些横切,我们必须修改DefaultControlWrapper,加入代码如下
public Object wrapBean(Object bean) {
DefaultControlProxy proxy = new DefaultControlProxy();
proxy.setDelegate(bean);
System.out.println("wrap................"+bean.getClass());
return Proxy.newProxyInstance(this.getClass().getClassLoader(),bean.getClass().getInterfaces(),proxy);
}
重新运行我们的测试,结果并不影响。只是在每个Control的方法执行以前,打印了句Log信息。
Control方法的权限检查(目录)
workshop可以指定一个方法的角色,我们已经有了横切,所以这步工作也并不困难。首先定义一个Annotation,名字是Role,代码如下:
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target( {java.lang.annotation.ElementType.METHOD})
public @interface Role {
public String name();
}
没有好说的,与前面及唯一的区别就是对象变成了方法,我们可以这样声明来表示这个方法必需要角色admin来参与:
@Role(name="admin")
public String doTaskWithRole(){
return "admin";
}
对于每一个Control必须在建立的时候,告诉他所拥有的角色,所以修改所有JavaControl的基类BaseControl如下:
private String role;
public BaseControl() {
}
public void setRole(String role) {
this.role = role;
}
public String getRole() {
return role;
}
有了这些修改,我们可以在我们的横切代码那里增加检查权限的功能
Method m = delegate.getClass().getMethod(method.getName(),classes);
Annotation[] annotation = m.getAnnotations();
for (int i = 0; i < annotation.length; i++) {
if(annotation[i] instanceof Role){
log.info("Method "+method.getName()+" should check role");
if(((BaseControl)getDelegate()).getRole().equals(((Role)annotation[i]).name())){
}else{
throw new IllegalAccessException("Method "+method.getName()+" requier Role "+((Role)annotation[i]).name());
}
}
}
以上代码就是增加的检查,我们使用Method m = delegate.getClass().getMethod(method.getName())得到新的Method,因为传递给我们的Method对象是代理过的,所以我们必须使用原始的类的定义,当出现权限不足时,一个例外就会抛出。对于我们的测试用例,我们增加testHasRoleCheckFailed()>和testHasRoleCheckSuccess()>用例,分别测试在没有和有权限的时候会不会有例外发生。
一个DatabaseControl的特例 (目录)
我们可以扩展我们的JavaControl了,我们定义一个新的Annotation----DatabaseMethod
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Target({java.lang.annotation.ElementType.METHOD})
public @interface DatabaseMethod {
public String sql();
public String dataSource() default "dataSource";
}
这个Annotation有两个成员,其中dataSource()有缺省值,也就是程序里注释的时候,可以不输入dataSource参数。
@ControlType(name="DatabaseControl")
public class DatabaseControl1Impl extends BaseControl implements DatabaseControl1{
@DatabaseMethod(sql="insert into user values(:0,:1)")
public void insertUser(int id , String name) {
}
}
这就是一个使用DatabaseControl的例子,首先说明这个Control是一个DatabaseControl,需要使用net.rocksun.tiffany.wrapper.DatabaseControlWrapper来进行包装。再就是对应的我们要修改DatabaseControlWrapper类,同时参照DefaultControl,我们要对DatabaseControlProxy进行修改,使之可以处理sql:
public Object invoke(Object proxy, Method method, Object[] args) throws
Throwable {
log.info(delegate.getClass()+ "’s method "+method.toString()+" invoked");
Class[] classes = null;
if(args!=null){
classes = new Class[args.length];
for (int i = 0; i < args.length; i++) {
classes[i] = args[i].getClass();
if(args[i] instanceof Integer){
classes[i] = Integer.TYPE;
}
}
}
Method m = delegate.getClass().getMethod(method.getName(),classes);
Annotation[] annotation = m.getAnnotations();
for (int i = 0; i < annotation.length; i++) {
if(annotation[i] instanceof DatabaseMethod){
String sql = ((DatabaseMethod)annotation[i]).sql();
String dataSource = ((DatabaseMethod)annotation[i]).dataSource();
for (int j = 0; j < args.length; j++) {
if(args[j] instanceof Integer){
sql = sql.replace(":"+j,args[j].toString());
}else{
sql = sql.replace(":"+j,"’"+args[j].toString()+"’");
}
}
log.info(" will execute sql ’"+ sql +"’ for dataSource ’"+dataSource+"’");
}
}
return method.invoke(getDelegate(),args);
}
这是整个方法的定义,并没有考虑到所有的类型,也没有实际执行sql,只是告诉大家我们这个时候已经有了操作数据库的能力了。运行测试用例的testInsertUser,我们就可以看到我们把sql处理成可以执行的状态。
关于Demo程序
所有的Demo Control都在net.rocksun.tiffany.demo下,里面有FirstControl,SecondControl,DatabaseControl1三个Control和它们的实现,所有的Control实现都是BaseControl的子类,BaseControl帮助它的子类保存角色信息。
FirstControl中有SecondControl和DatabaseControl1的一个实例,FirstControl的三个方法,分别为doTask,doTaskWithRole,insertUser,其中第一个演示了成员变量的自动注入,第二个包括了权限检查,第三个是调用DatabaseControl控件。
有了这些,我们就可以使用ControlFactoryTest进行测试
框架如何的加强(目录)
作为框架程序,还有许多事情要做。如数据库控件,我们可以添加一种事物控制标示,在需要启动事物的方法前添加一种annotation,方法里所有DatabaseControl使用相同的数据库连接,并且根据抛出的Exception来选择提交和会滚。有时候建立一种通用的控件框架是比较麻烦的,但如果在一个自己写的框架下,在方法前增加注释来说明一些额外的操作也是很方便的选择。
由于没有工具,所以使用我们自己的Control框架并不容易,需要自己写接口,然后是实现,然后是配置信息,而使用workshop则可以只关心写实现,接口和配置都交给workshop自动的完成。使用Control最方便的地方就是可以很好的与编程工具结合,并且可以使代码更规范,但如果没有工具,就比较难说了。
目前这个框架还有很多问题,注册新控件太麻烦,包装程序重复工作太多,还没有很好的复用。再就是BaseControl应该更好的包装,空间声明应该使用新Annotation类型,而不应该使用名称作为参数的方式,因为使用新类型,可以在编译时检查,减少错误发生的可能。
对于页面流等技术我们已可以自己实现,但核心思想是一样的。
感受
记得看《XDoclet in Action》的时候,说XDoclet生成配置文件的时候,有两种目的,一种是直接可以用,另一种是作为模版,第一种方式更好一些,第二种通常是没有办法时的选择。Annotation和Spring其实就是XDoclet的两种状态,当Annotation是代码级的时候,我们可以用工具得到对应的Spring配置文件,但是这就意味着,我们可能最好不要直接修改Spring的配置文件,因为下一次生成就把这些修改覆盖了,但这样就失去了通过配置来灵活改变一些设置的可能性。同样,如果我们完全依赖自己配置文件,可能会很麻烦。
但我觉得使用配置虽然麻烦一些,但是确实是更清楚一些,如果有可能的话应该结合使用,实现灵活性与简单性的结合。
Annotation,DIIOC)等等,这些东西出现的目的都差不多,都是为了减少这些横切代码的重复工作。我也越来越感觉万变不离其宗了。JavaControl就是想提供这样一个环境,在这个环境里可以自动的读取Annotation来完成横切的工作,这比使用spring确实方便一些,因为没有这样一个环境,Spring只能通过配置实现,这样的重复工作也太多。
本文代码下载 (请选择目标另存为 tiffany.rar)
参考文档
这一次几乎没参考什么,只是看了些Annotation的参考,但是忘记了地址。
如果有就是这个了:oreilly_.java.1.5.tiger.a.developers.notebook.(2004)
关于作者:
sdj21,rocksun(dev2dev ID),软件工程师,
邮件地址:sdj21@sina.com , daijun@gmail.com ,sundaijun@126.com