随笔 - 63  文章 - 0  trackbacks - 0
<2009年4月>
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

常用链接

留言簿(2)

随笔分类

随笔档案

搜索

  •  

最新评论

阅读排行榜

评论排行榜

UserType and CompositeUserType为Hibernate中提供的用户类型自定义接口。根据这个接口,可以实现自定义的数据类型。 

最近看<<深入浅出Hibernate>>,作者在书中介绍了Hibernate 提供的用户自定义数据类型,于是效仿书中的方法,在数据库表中添加一个字段用于记录所有好友的userid,每个userid之间用";"加以分隔,同时将该字段映射为一个特殊的List集合,利用UserType interface实现String解析后将各个userid封装在List中,将List中的记录封装成以";"分隔的String。
使用UserType接口,实现了较好的设计风格,以及更好的重用性。
/*
* 用户自定义的数据类型,对应数据库中的一个字段,在该字段中,保存了
* 多个用户需要的信息,之间用";"加以分隔.
* @Author:Paul
* @Date:April 18th,2008
*/
package com.globalhands.hibernate.userTypes;

import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.sql.Types;

import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.usertype.UserType;

public class SpecialList implements UserType {
private List specialList;

private static final char SPLITTER = ';';

private static final int[] TYPES = new int[] { Types.VARCHAR };

public String assemble(Serializable arg0, Object arg1)
throws HibernateException {
return null;
}

/*
* 将List封装为一个String对象
*/
public String assemble(List specialList) throws HibernateException {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < specialList.size() - 1; i++) {
sb.append(specialList.get(i)).append(this.SPLITTER);
}
sb.append(specialList.get(specialList.size() - 1));
return sb.toString();
}

/*
* 创建一个新的List实例,包含原有的List实例中的所有元素.
*/
public Object deepCopy(Object value) throws HibernateException {
List sourceList = (List) value;
List targetList = new ArrayList();
targetList.addAll(sourceList);
return targetList;
}

public Serializable disassemble(Object arg0) throws HibernateException {
return null;
}

/*
* 判断specialList是否发生变化
*/
public boolean equals(Object x, Object y) throws HibernateException {
if (x == y) {
return true;
}
if (x != null && y != null) {
List xList = (List) x;
List yList = (List) y;

if (xList.size() != yList.size()) {
return false;
}

for (int i = 0; i <= xList.size() - 1; i++) {
String str1 = (String) xList.get(i);
String str2 = (String) yList.get(i);
if (!xList.equals(yList)) {
return false;
}
}
return true;
}
return false;
}

public int hashCode(Object arg0) throws HibernateException {
return 0;
}

public boolean isMutable() {
return false;
}

/*
* 从resultset中取出email字段,并将其解析为List类型后返回
*/
public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
throws HibernateException, SQLException {
String value = (String) Hibernate.STRING.nullSafeGet(rs, names[0]);
if (value != null) {
return parse(value);
} else {
return null;
}
}

/*
* 将以";"分隔的字符串解析为一个字符串数组
*/
private List parse(String value) {
String[] strs = value.split(";");
List specialList = new ArrayList();
for (int i = 0; i <= strs.length - 1; i++) {
specialList.add(strs[i]);
}
return specialList;
}

/*
* 将List型的email信息组装成字符串之后保存到email字段
*/
public void nullSafeSet(PreparedStatement st, Object value, int index)
throws HibernateException, SQLException {
if (value != null) {
String str = assemble((List) value);
Hibernate.STRING.nullSafeSet(st, str, index);
} else {
Hibernate.STRING.nullSafeSet(st, value, index);
}
}

public Object replace(Object arg0, Object arg1, Object arg2)
throws HibernateException {
return null;
}

public Class returnedClass() {
return List.class;
}

public int[] sqlTypes() {
return TYPES;
}

}
同时,修改相应的[ormapping_filename].hbm.xml中相应字段的映射信息
<property name="buddy" type="com.globalhands.hibernate.userTypes.SpecialList">
            <column name="buddy" length="2000" not-null="true" />
        </property>
之后,修改POJO类中该字段的返回类型为List。
使用JUnit测试程序。
posted @ 2009-05-24 19:57 lanxin1020 阅读(571) | 评论 (0)编辑 收藏
posted @ 2009-05-21 22:13 lanxin1020 阅读(151) | 评论 (0)编辑 收藏

什么是 ASM?

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

与 BCEL 和 SERL 不同,ASM 提供了更为现代的编程模型。对于 ASM 来说,Java class 被描述为一棵树;使用 “Visitor” 模式遍历整个二进制结构;事件驱动的处理方式使得用户只需要关注于对其编程有意义的部分,而不必了解 Java 类文件格式的所有细节:ASM 框架提供了默认的 “response taker”处理这一切。

为什么要动态生成 Java 类?

动态生成 Java 类与 AOP 密切相关的。AOP 的初衷在于软件设计世界中存在这么一类代码,零散而又耦合:零散是由于一些公有的功能(诸如著名的 log 例子)分散在所有模块之中;同时改变 log 功能又会影响到所有的模块。出现这样的缺陷,很大程度上是由于传统的 面向对象编程注重以继承关系为代表的“纵向”关系,而对于拥有相同功能或者说方面 (Aspect)的模块之间的“横向”关系不能很好地表达。例如,目前有一个既有的银行管理系统,包括 Bank、Customer、Account、Invoice 等对象,现在要加入一个安全检查模块, 对已有类的所有操作之前都必须进行一次安全检查。


图 1. ASM – AOP
图 1. ASM – AOP

然而 Bank、Customer、Account、Invoice 是代表不同的事务,派生自不同的父类,很难在高层上加入关于 Security Checker 的共有功能。对于没有多继承的 Java 来说,更是如此。传统的解决方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍旧是分散的 —— 每个需要 Security Checker 的类都必须要派生一个 Decorator,每个需要 Security Checker 的方法都要被包装(wrap)。下面我们以 Account 类为例看一下 Decorator:

首先,我们有一个 SecurityChecker 类,其静态方法 checkSecurity 执行安全检查功能:

public class SecurityChecker {   public static void checkSecurity() {    System.out.println("SecurityChecker.checkSecurity ...");    //TODO real security check   }   }       

另一个是 Account 类:

public class Account {   public void operation() {    System.out.println("operation...");    //TODO real operation   }  }       

若想对 operation 加入对 SecurityCheck.checkSecurity() 调用,标准的 Decorator 需要先定义一个 Account 类的接口:

public interface Account {   void operation();   }       

然后把原来的 Account 类定义为一个实现类:

public class AccountImpl extends Account{   public void operation() {    System.out.println("operation...");    //TODO real operation   }  }        

定义一个 Account 类的 Decorator,并包装 operation 方法:

public class AccountWithSecurityCheck implements Account {    private  Account account;   public AccountWithSecurityCheck (Account account) {    this.account = account;   }   public void operation() {    SecurityChecker.checkSecurity();    account.operation();   }  }       

在这个简单的例子里,改造一个类的一个方法还好,如果是变动整个模块,Decorator 很快就会演化成另一个噩梦。动态改变 Java 类就是要解决 AOP 的问题,提供一种得到系统支持的可编程的方法,自动化地生成或者增强 Java 代码。这种技术已经广泛应用于最新的 Java 框架内,如 Hibernate,Spring 等。

为什么选择 ASM?

最直接的改造 Java 类的方法莫过于直接改写 class 文件。Java 规范详细说明了class 文件的格式,直接编辑字节码确实可以改变 Java 类的行为。直到今天,还有一些 Java 高手们使用最原始的工具,如 UltraEdit 这样的编辑器对 class 文件动手术。是的,这是最直接的方法,但是要求使用者对 Java class 文件的格式了熟于心:小心地推算出想改造的函数相对文件首部的偏移量,同时重新计算 class 文件的校验码以通过 Java 虚拟机的安全机制。

Java 5 中提供的 Instrument 包也可以提供类似的功能:启动时往 Java 虚拟机中挂上一个用户定义的 hook 程序,可以在装入特定类的时候改变特定类的字节码,从而改变该类的行为。但是其缺点也是明显的:

  • Instrument 包是在整个虚拟机上挂了一个钩子程序,每次装入一个新类的时候,都必须执行一遍这段程序,即使这个类不需要改变。
  • 直接改变字节码事实上类似于直接改写 class 文件,无论是调用 ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer),还是 Instrument.redefineClasses(ClassDefinition[] definitions),都必须提供新 Java 类的字节码。也就是说,同直接改写 class 文件一样,使用 Instrument 也必须了解想改造的方法相对类首部的偏移量,才能在适当的位置上插入新的代码。

尽管 Instrument 可以改造类,但事实上,Instrument 更适用于监控和控制虚拟机的行为。

一种比较理想且流行的方法是使用 java.lang.ref.proxy。我们仍旧使用上面的例子,给 Account 类加上 checkSecurity 功能:

首先,Proxy 编程是面向接口的。下面我们会看到,Proxy 并不负责实例化对象,和 Decorator 模式一样,要把 Account 定义成一个接口,然后在 AccountImpl 里实现 Account 接口,接着实现一个 InvocationHandler Account 方法被调用的时候,虚拟机都会实际调用这个 InvocationHandlerinvoke 方法:

class SecurityProxyInvocationHandler implements InvocationHandler {   private Object proxyedObject;   public SecurityProxyInvocationHandler(Object o) {    proxyedObject = o;   }       public Object invoke(Object object, Method method, Object[] arguments)    throws Throwable {       if (object instanceof Account && method.getName().equals("opertaion")) {     SecurityChecker.checkSecurity();    }    return method.invoke(proxyedObject, arguments);   }  }    

最后,在应用程序中指定 InvocationHandler 生成代理对象:

public static void main(String[] args) {   Account account = (Account) Proxy.newProxyInstance(    Account.class.getClassLoader(),    new Class[] { Account.class },    new SecurityProxyInvocationHandler(new AccountImpl())   );   account.function();  }   

其不足之处在于:

  • Proxy 是面向接口的,所有使用 Proxy 的对象都必须定义一个接口,而且用这些对象的代码也必须是对接口编程的:Proxy 生成的对象是接口一致的而不是对象一致的:例子中 Proxy.newProxyInstance 生成的是实现 Account 接口的对象而不是 AccountImpl 的子类。这对于软件架构设计,尤其对于既有软件系统是有一定掣肘的。
  • Proxy 毕竟是通过反射实现的,必须在效率上付出代价:有实验数据表明,调用反射比一般的函数开销至少要大 10 倍。而且,从程序实现上可以看出,对 proxy class 的所有方法调用都要通过使用反射的 invoke 方法。因此,对于性能关键的应用,使用 proxy class 是需要精心考虑的,以避免反射成为整个应用的瓶颈。

ASM 能够通过改造既有类,直接生成需要的代码。增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM 与 Proxy 编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。

相比于其他流行的 Java 字节码操纵工具,ASM 更小更快。ASM 具有类似于 BCEL 或者 SERP 的功能,而只有 33k 大小,而后者分别有 350k 和 150k。同时,同样类转换的负载,如果 ASM 是 60% 的话,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。

ASM 已经被广泛应用于一系列 Java 项目:AspectWerkz、AspectJ、BEA WebLogic、IBM AUS、OracleBerkleyDB、Oracle TopLink、Terracotta、RIFE、EclipseME、Proactive、Speedo、Fractal、EasyBeans、BeanShell、Groovy、Jamaica、CGLIB、dynaop、Cobertura、JDBCPersistence、JiP、SonarJ、Substance L&F、Retrotranslator 等。Hibernate 和 Spring 也通过 cglib,另一个更高层一些的自动代码生成工具使用了 ASM。





回页首


Java 类文件概述

所谓 Java 类文件,就是通常用 javac 编译器产生的 .class 文件。这些文件具有严格定义的格式。为了更好的理解 ASM,首先对 Java 类文件格式作一点简单的介绍。Java 源文件经过 javac 编译器编译之后,将会生成对应的二进制文件(如下图所示)。每个合法的 Java 类文件都具备精确的定义,而正是这种精确的定义,才使得 Java 虚拟机得以正确读取和解释所有的 Java 类文件。


图 2. ASM – Javac 流程
图 2. ASM – Javac 流程

Java 类文件是 8 位字节的二进制流。数据项按顺序存储在 class 文件中,相邻的项之间没有间隔,这使得 class 文件变得紧凑,减少存储空间。在 Java 类文件中包含了许多大小不同的项,由于每一项的结构都有严格规定,这使得 class 文件能够从头到尾被顺利地解析。下面让我们来看一下 Java 类文件的内部结构,以便对此有个大致的认识。

例如,一个最简单的 Hello World 程序:

public class HelloWorld {   public static void main(String[] args) {    System.out.println("Hello world");   }  }  

经过 javac 编译后,得到的类文件大致是:


图 3. ASM – Java 类文件
图 3. ASM – Java 类文件

从上图中可以看到,一个 Java 类文件大致可以归为 10 个项:

  • Magic:该项存放了一个 Java 类文件的魔数(magic number)和版本信息。一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件。
  • Version:该项存放了 Java 类文件的版本信息,它对于一个 Java 文件具有重要的意义。因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。
  • Constant Pool:该项存放了类中各种文字字符串、类名、方法名和接口名称、final 变量以及对外部类的引用信息等常量。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。
  • Access_flag:该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。
  • This Class:指向表示该类全限定名称的字符串常量的指针。
  • Super Class:指向表示父类全限定名称的字符串常量的指针。
  • Interfaces:一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。以上三项所指向的常量,特别是前两项,在我们用 ASM 从已有类派生新类时一般需要修改:将类名称改为子类名称;将父类改为派生前的类名称;如果有必要,增加新的实现接口。
  • Fields:该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。
  • Methods:该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。
  • Class attributes:该项存放了在该文件中类或接口所定义的属性的基本信息。

事实上,使用 ASM 动态生成类,不需要像早年的 class hacker 一样,熟知 class 文件的每一段,以及它们的功能、长度、偏移量以及编码方式。ASM 会给我们照顾好这一切的,我们只要告诉 ASM 要改动什么就可以了 —— 当然,我们首先得知道要改什么:对类文件格式了解的越多,我们就能更好地使用 ASM 这个利器。





回页首


ASM 3.0 编程框架

ASM 通过树这种数据结构来表示复杂的字节码结构,并利用 Push 模型来对树进行遍历,在遍历过程中对字节码进行修改。所谓的 Push 模型类似于简单的 Visitor 设计模式,因为需要处理字节码结构是固定的,所以不需要专门抽象出一种 Vistable 接口,而只需要提供 Visitor 接口。所谓 Visitor 模式和 Iterator 模式有点类似,它们都被用来遍历一些复杂的数据结构。Visitor 相当于用户派出的代表,深入到算法内部,由算法安排访问行程。Visitor 代表可以更换,但对算法流程无法干涉,因此是被动的,这也是它和 Iterator 模式由用户主动调遣算法方式的最大的区别。

在 ASM 中,提供了一个 ClassReader 类,这个类可以直接由字节数组或由 class 文件间接的获得字节码数据,它能正确的分析字节码,构建出抽象的树在内存中表示字节码。它会调用 accept 方法,这个方法接受一个实现了 ClassVisitor 接口的对象实例作为参数,然后依次调用 ClassVisitor 接口的各个方法。字节码空间上的偏移被转换成 visit 事件时间上调用的先后,所谓 visit 事件是指对各种不同 visit 函数的调用,ClassReader 知道如何调用各种 visit 函数。在这个过程中用户无法对操作进行干涉,所以遍历的算法是确定的,用户可以做的是提供不同的 Visitor 来对字节码树进行不同的修改。ClassVisitor 会产生一些子过程,比如 visitMethod 会返回一个实现 MethordVisitor 接口的实例,visitField 会返回一个实现 FieldVisitor 接口的实例,完成子过程后控制返回到父过程,继续访问下一节点。因此对于 ClassReader 来说,其内部顺序访问是有一定要求的。实际上用户还可以不通过 ClassReader 类,自行手工控制这个流程,只要按照一定的顺序,各个 visit 事件被先后正确的调用,最后就能生成可以被正确加载的字节码。当然获得更大灵活性的同时也加大了调整字节码的复杂度。

各个 ClassVisitor 通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。

ClassAdaptor 类实现了 ClassVisitor 接口所定义的所有函数,当新建一个 ClassAdaptor 对象的时候,需要传入一个实现了 ClassVisitor 接口的对象,作为职责链中的下一个访问者 (Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。当用户需要对字节码进行调整时,只需从 ClassAdaptor 类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递下去。这样,用户无需考虑字节偏移,就可以很方便的控制字节码。

每个 ClassAdaptor 类的派生类可以仅封装单一功能,比如删除某函数、修改字段可见性等等,然后再加入到职责链中,这样耦合更小,重用的概率也更大,但代价是产生很多小对象,而且职责链的层次太长的话也会加大系统调用的开销,用户需要在低耦合和高效率之间作出权衡。用户可以通过控制职责链中 visit 事件的过程,对类文件进行如下操作:

  1. 删除类的字段、方法、指令:只需在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回 null,而不是返回由 visitMethod 方法返回的 MethodVisitor 对象。

    class DelLoginClassAdapter extends ClassAdapter {   public DelLoginClassAdapter(ClassVisitor cv) {    super(cv);   }     public MethodVisitor visitMethod(final int access, final String name,    final String desc, final String signature, final String[] exceptions) {    if (name.equals("login")) {     return null;    }    return cv.visitMethod(access, name, desc, signature, exceptions);   }  }           

  2. 修改类、字段、方法的名字或修饰符:在职责链传递过程中替换调用参数。

    class AccessClassAdapter extends ClassAdapter {   public AccessClassAdapter(ClassVisitor cv) {    super(cv);   }     public FieldVisitor visitField(final int access, final String name,          final String desc, final String signature, final Object value) {          int privateAccess = Opcodes.ACC_PRIVATE;          return cv.visitField(privateAccess, name, desc, signature, value);      }  }           

  3. 增加新的类、方法、字段

ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类 —— ClassWriter。它实现了 ClassVisitor 接口,而且含有一个 toByteArray() 函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有 visit 事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后),如下例:

ClassWriter  classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);  ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter);  ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor);     ClassReader classReader = new ClassReader(strFileName);  classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);           

综上所述,ASM 的时序图如下:


图 4. ASM – 时序图
图 4. ASM – 时序图




回页首


使用 ASM3.0 进行 AOP 编程

我们还是用上面的例子,给 Account 类加上 security check 的功能。与 proxy 编程不同,ASM 不需要将 Account 声明成接口,Account 可以仍旧是一个实现类。ASM 将直接在 Account 类上动手术,给 Account 类的 operation 方法首部加上对 SecurityChecker.checkSecurity 的调用。

首先,我们将从 ClassAdapter 继承一个类。ClassAdapter 是 ASM 框架提供的一个默认类,负责沟通 ClassReaderClassWriter。如果想要改变 ClassReader 处读入的类,然后从 ClassWriter 处输出,可以重写相应的 ClassAdapter 函数。这里,为了改变 Account 类的 operation 方法,我们将重写 visitMethdod 方法。

class AddSecurityCheckClassAdapter extends ClassAdapter{     public AddSecurityCheckClassAdapter(ClassVisitor cv) {    //Responsechain 的下一个 ClassVisitor,这里我们将传入 ClassWriter,    //负责改写后代码的输出    super(cv);   }      //重写 visitMethod,访问到 "operation" 方法时,   //给出自定义 MethodVisitor,实际改写方法内容   public MethodVisitor visitMethod(final int access, final String name,    final String desc, final String signature, final String[] exceptions) {    MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);    MethodVisitor wrappedMv = mv;    if (mv != null) {     //对于 "operation" 方法     if (name.equals("operation")) {       //使用自定义 MethodVisitor,实际改写方法内容      wrappedMv = new AddSecurityCheckMethodAdapter(mv);      }     }    return wrappedMv;   }  }     

下一步就是定义一个继承自 MethodAdapterAddSecurityCheckMethodAdapter,在“operation”方法首部插入对 SecurityChecker.checkSecurity() 的调用。

class AddSecurityCheckMethodAdapter extends MethodAdapter {   public AddSecurityCheckMethodAdapter(MethodVisitor mv) {    super(mv);   }     public void visitCode() {    visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker",     "checkSecurity", "()V");   }  }      

其中,ClassReader 读到每个方法的首部时调用 visitCode(),在这个重写方法里,我们用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V"); 插入了安全检查功能。

最后,我们将集成上面定义的 ClassAdapterClassReaderClassWriter 产生修改后的 Account 类文件:

import java.io.File;  import java.io.FileOutputStream;  import org.objectweb.asm.*;        public class Generator{   public static void main() throws Exception {    ClassReader cr = new ClassReader("Account");    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);    ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);    cr.accept(classAdapter, ClassReader.SKIP_DEBUG);    byte[] data = cw.toByteArray();    File file = new File("Account.class");    FileOutputStream fout = new FileOutputStream(file);    fout.write(data);    fout.close();   }  }   

执行完这段程序后,我们会得到一个新的 Account.class 文件,如果我们使用下面代码:

public class Main {   public static void main(String[] args) {    Account account = new Account();    account.operation();   }  }   

使用这个 Account,我们会得到下面的输出:

SecurityChecker.checkSecurity ...  operation...   

也就是说,在 Account 原来的 operation 内容执行之前,进行了 SecurityChecker.checkSecurity() 检查。

将动态生成类改造成原始类 Account 的子类

上面给出的例子是直接改造 Account 类本身的,从此 Account 类的 operation 方法必须进行 checkSecurity 检查。但事实上,我们有时仍希望保留原来的 Account 类,因此把生成类定义为原始类的子类是更符合 AOP 原则的做法。下面介绍如何将改造后的类定义为 Account 的子类 Account$EnhancedByASM。其中主要有两项工作:

  • 改变 Class Description, 将其命名为 Account$EnhancedByASM,将其父类指定为 Account
  • 改变构造函数,将其中对父类构造函数的调用转换为对 Account 构造函数的调用。

AddSecurityCheckClassAdapter 类中,将重写 visit 方法:

public void visit(final int version, final int access, final String name,    final String signature, final String superName,    final String[] interfaces) {   String enhancedName = name + "$EnhancedByASM";  //改变类命名   enhancedSuperName = name; //改变父类,这里是”Account”   super.visit(version, access, enhancedName, signature,   enhancedSuperName, interfaces);  }   

改进 visitMethod 方法,增加对构造函数的处理:

public MethodVisitor visitMethod(final int access, final String name,   final String desc, final String signature, final String[] exceptions) {   MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);   MethodVisitor wrappedMv = mv;   if (mv != null) {    if (name.equals("operation")) {     wrappedMv = new AddSecurityCheckMethodAdapter(mv);    } else if (name.equals("<init>")) {     wrappedMv = new ChangeToChildConstructorMethodAdapter(mv,      enhancedSuperName);    }   }   return wrappedMv;  }   

这里 ChangeToChildConstructorMethodAdapter 将负责把 Account 的构造函数改造成其子类 Account$EnhancedByASM 的构造函数:

class ChangeToChildConstructorMethodAdapter extends MethodAdapter {   private String superClassName;     public ChangeToChildConstructorMethodAdapter(MethodVisitor mv,    String superClassName) {    super(mv);    this.superClassName = superClassName;   }     public void visitMethodInsn(int opcode, String owner, String name,    String desc) {    //调用父类的构造函数时    if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) {      owner = superClassName;    }    super.visitMethodInsn(opcode, owner, name, desc);//改写父类为superClassName   }  }  

最后演示一下如何在运行时产生并装入产生的 Account$EnhancedByASM。 我们定义一个 Util 类,作为一个类工厂负责产生有安全检查的 Account 类:

public class SecureAccountGenerator {     private static AccountGeneratorClassLoader classLoader =     new AccountGeneratorClassLoade();   private static Class secureAccountClass;     public Account generateSecureAccount() throws ClassFormatError,     InstantiationException, IllegalAccessException {    if (null == secureAccountClass) {                 ClassReader cr = new ClassReader("Account");     ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);     ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);     cr.accept(classAdapter, ClassReader.SKIP_DEBUG);     byte[] data = cw.toByteArray();     secureAccountClass = classLoader.defineClassFromClassFile(      "Account$EnhancedByASM",data);    }    return (Account) secureAccountClass.newInstance();   }     private static class AccountGeneratorClassLoader extends ClassLoader {    public Class defineClassFromClassFile(String className,     byte[] classFile) throws ClassFormatError {     return defineClass("Account$EnhancedByASM", classFile, 0, classFile.length());    }   }  }  

静态方法 SecureAccountGenerator.generateSecureAccount() 在运行时动态生成一个加上了安全检查的 Account 子类。著名的 Hibernate 和 Spring 框架,就是使用这种技术实现了 AOP 的“无损注入”。





回页首


小结

最后,我们比较一下 ASM 和其他实现 AOP 的底层技术:

posted @ 2009-05-12 12:43 lanxin1020 阅读(240) | 评论 (0)编辑 收藏

以下是一份完整的struts-config.xml文件,配置元素的说明详见注释.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
"http://jakarta.apache.org/struts/dtds/struts-config.dtd">
<!-- struts-config.xml中的元素必须按照上述doc指令中的dtd文档定义顺序书写,本例即遵从了dtd定义顺序 -->
<!-- struts-config是整个xml的根元素,其他元素必须被包含其内 -->
<struts-config>
<!--
   名称:data-sources
   描述:data-sources元素定义了web App所需要使用的数据源
   数量:最多一个
   子元素:data-source
-->
<data-sources>
   <!--
    名称:data-source
    描述:data-source元素定义了具体的数据源
    数量:任意多个
    属性:
     @key:当需要配置多个数据源时,相当于数据源的名称,用来数据源彼此间进行区别
     @type:可以使用的数据源实现的类,一般来自如下四个库
      Poolman,开放源代码软件
      Expresso,Jcorporate
      JDBC Pool,开放源代码软件
      DBCP,Jakarta
   -->
   <data-source key="firstOne" type="org.apache.commons.dbcp.BasicDataSource">
    <!--
     名称:set-property
     描述:用来设定数据源的属性
     属性:
      @autoCommit:是否自动提交 可选值:true/false
      @description:数据源描述
      @driverClass:数据源使用的类
      @maxCount:最大数据源连接数
      @minCount:最小数据源连接数
      @user:数据库用户
      @password:数据库密码
      @url:数据库url
    -->
    <set-property property="autoCommit" value="true"/>
    <set-property property="description" value="Hello!"/>
    <set-property property="driverClass" value="com.mysql.jdbc.Driver"/>
    <set-property property="maxCount" value="10"/>
    <set-property property="minCount" value="2"/>
    <set-property property="user" value="root"/>
    <set-property property="password" value=""/>
    <set-property property="url" value="jdbc:mysql://localhost:3306/helloAdmin"/>
   </data-source>
</data-sources>

<!--
   名称:form-beans
   描述:用来配置多个ActionForm Bean
   数量:最多一个
   子元素:form-bean
-->
<form-beans>
   <!--
    名称:form-bean
    描述:用来配置ActionForm Bean
    数量:任意多个
    子元素:form-property
    属性:
     @className:指定与form-bean元素相对应的配置类,一般默认使用org.apaceh.struts.config.FormBeanConfig,如果自定义,则必须继承 FormBeanConfig
     @name:必备属性!为当前form-bean制定一个全局唯一的标识符,使得在整个Struts框架内,可以通过该标识符来引用这个ActionForm Bean。
     @type:必备属性!指明实现当前ActionForm Bean的完整类名。
   -->
   <form-bean name="Hello" type="myPack.Hello">
    <!--
     名称:form-property
     描述:用来设定ActionForm Bean的属性
     数量:根据实际需求而定,例如,ActionForm Bean对应的一个登陆Form中有两个文本框,name和password,ActionForm Bean中也有这两个字段,则此处编写两个form-property来设定属性
     属性:
      @className:指定与form-property相对应的配置类,默认是org.apache.struts.config.FormPropertyConfig,如果自定义,则必须继承FormPropertyConfig类
      @name:所要设定的ActionForm Bean的属性名称
      @type:所要设定的ActionForm Bean的属性值的类
      @initial:当前属性的初值
    -->
    <form-property name="name" type="java.lang.String"/>
    <form-property name="number" type="java.lang.Iteger" initial="18"/>
   </form-bean>
</form-beans>

<!--
   名称:global-exceptions
   描述:处理异常
   数量:最多一个
   子元素:exception
-->
<global-exceptions>
   <!--
    名称:exception
    描述:具体定义一个异常及其处理
    数量:任意多个
    属性:
     @className:指定对应exception的配置类,默认为org.apache.struts.config.ExceptionConfig
     @handler:指定异常处理类,默认为org.apache.struts.action.ExceptionHandler
     @key:指定在Resource Bundle种描述该异常的消息key
     @path:指定当发生异常时,进行转发的路径
     @scope:指定ActionMessage实例存放的范围,默认为request,另外一个可选值是session
     @type:必须要有!指定所需要处理异常类的名字。
     @bundle:指定资源绑定
   -->
   <exception
    key=""hello.error
    path="/error.jsp"
    scope="session"
    type="hello.HandleError"/>
</global-exceptions>

<!--
   名称:global-forwards
   描述:定义全局转发
   数量:最多一个
   子元素:forward
-->
<global-forwards>
   <!--
    名称:forward
    描述:定义一个具体的转发
    数量:任意多个
    属性:
     @className:指定和forward元素对应的配置类,默认为org.apache.struts.action.ActionForward
     @contextRelative:如果为true,则指明使用当前上下文,路径以“/”开头,默认为false
     @name:必须配有!指明转发路径的唯一标识符
     @path:必须配有!指明转发或者重定向的URI。必须以"/"开头。具体配置要与contextRelative相应。
     @redirect:为true时,执行重定向操作,否则执行请求转发。默认为false
   -->
   <forward name="A" path="/a.jsp"/>
   <forward name="B" path="/hello/b.do"/>
</global-forwards>

<!--
   名称:action-mappings
   描述:定义action集合
   数量:最多一个
   子元素:action
-->
<action-mappings>
   <!--
    名称:action
    描述:定义了从特定的请求路径到相应的Action类的映射
    数量:任意多个
    子元素:exception,forward(二者均为局部量)
    属性:
     @attribute:制定与当前Action相关联的ActionForm Bean在request和session范围内的名称(key)
     @className:与Action元素对应的配置类。默认为org.apache.struts.action.ActionMapping
     @forward:指名转发的URL路径
     @include:指名包含的URL路径
     @input:指名包含输入表单的URL路径,表单验证失败时,请求会被转发到该URL中
     @name:指定和当前Acion关联的ActionForm Bean的名字。该名称必须在form-bean元素中定义过。
     @path:指定访问Action的路径,以"/"开头,没有扩展名
     @parameter:为当前的Action配置参数,可以在Action的execute()方法中,通过调用ActionMapping的getParameter()方法来获取参数
     @roles:指定允许调用该Aciton的安全角色。多个角色之间用逗号分割。处理请求时,RequestProcessor会根据该配置项来决定用户是否有调用该Action的权限
     @scope:指定ActionForm Bean的存在范围,可选值为request和session。默认为session
     @type:指定Action类的完整类名
     @unknown:值为true时,表示可以处理用户发出的所有无效的Action URL。默认为false
     @validate:指定是否要先调用ActionForm Bean的validate()方法。默认为true
    注意:如上属性中,forward/include/type三者相斥,即三者在同一Action配置中只能存在一个。
   -->
   <action path="/search"
    type="addressbook.actions.SearchAction"
    name="searchForm"
    scope="request"
    validate="true"
    input="/search.jsp">
    <forward name="success" path="/display.jsp"/>
   </action>  
</action-mappings>

<!--
   名称:controller
   描述:用于配置ActionServlet
   数量:最多一个
   属性:
    @bufferSize:指定上传文件的输入缓冲的大小.默认为4096
    @className:指定当前控制器的配置类.默认为org.apache.struts.config.ControllerConfig
    @contentType:指定相应结果的内容类型和字符编码
    @locale:指定是否把Locale对象保存到当前用户的session中,默认为false
    @processorClass:指定负责处理请求的Java类的完整类名.默认org.apache.struts.action.RequestProcessor
    @tempDir:指定文件上传时的临时工作目录.如果没有设置,将才用Servlet容器为web应用分配的临时工作目录.
    @nochache:true时,在相应结果中加入特定的头参数:Pragma ,Cache-Control,Expires防止页面被存储在可数浏览器的缓存中,默认为false
-->
<controller
   contentType="text/html;charset=UTF-8"
   locale="true"
   processorClass="CustomRequestProcessor">
</controller>
<!--
   名称:message-resources
   描述:配置Resource Bundle.
   数量:任意多个
   属性:
    @className:指定和message-resources对应的配置类.默认为org.apache.struts.config.MessageResourcesConfig
    @factory:指定资源的工厂类,默认为org.apache.struts.util.PropertyMessageResourcesFactory
    @key:
    @null:
    @parameter:
-->
<message-resources
   null="false"
   parameter="defaultResource"/>
<message-resources
   key="images"
   null="false"
   parameter="ImageResources"/>
  
<!--
   名称:plug-in
   描述:用于配置Struts的插件
   数量:任意多个
   子元素:set-property
   属性:
    @className:指定Struts插件类.此类必须实现org.apache.struts.action.PlugIn接口
-->
<plug-in
   className="org.apache.struts.validator.ValidatorPlugIn">
   <!--
    名称:set-property
    描述:配置插件的属性
    数量:任意多个
    属性:
     @property:插件的属性名称
     @value:该名称所配置的值
   -->
   <set-property
    property="pathnames"
    value="/WEB-INF/validator-rules.xml,/WEB-INF/vlaidation.xml"/>
</plug-in>

</struts-config>

posted @ 2009-05-11 16:26 lanxin1020 阅读(265) | 评论 (0)编辑 收藏
 过滤器是一个程序,它先于与之相关的servlet或JSP页面运行在服务器上。过滤器可附加到一个或多个servlet或JSP页面上,并且可以检查进入这些资源的请求信息。在这之后,过滤器可以作如下的选择:

1. 以常规的方式调用资源(即,调用servlet或JSP页面)。个人理解为请求通过过滤执行其他的操作
2.利用修改过的请求信息调用资源。对请求的信息加以修改,然后继续执行
3. 调用资源,但在发送响应到客户机前对其进行修改
4. 阻止该资源调用,代之以转到其他的资源,返回一个特定的状态代码或生成替换输出。个人理解为请求被拦截时强制执行(跳转)的操作

过滤器提供了几个重要好处 :
        首先,它以一种模块化的或可重用的方式封装公共的行为。你有30个不同的serlvet或JSP页面,需要压缩它们的内容以减少下载时间吗?没问题:构造一个压缩过滤器,然后将它应用到30个资源上即可。

       其次,利用它能够将高级访问决策与表现代码相分离。这对于JSP特别有价值,其中一般希望将几乎整个页面集中在表现上,而不是集中在业务逻辑上。例如,希 望阻塞来自某些站点的访问而不用修改各页面(这些页面受到访问限制)吗?没问题:建立一个访问限制过滤器并把它应用到想要限制访问的页面上即可。

       最后,过滤器使你能够对许多不同的资源进行批量性的更改。你有许多现存资源,这些资源除了公司名要更改外其他的保持不变,能办到么?没问题:构造一个串替换过滤器,只要合适就使用它。

      但要注意,过滤器只在与servlet规范2.3版兼容的服务器上有作用。如果你的Web应用需要支持旧版服务器,就不能使用过滤器。

1.   建立基本过滤器

建立一个过滤器涉及下列五个步骤:
1)建立一个实现Filter接口的类。这个类需要三个方法,分别是:doFilter、init和destroy
       doFilter方法包含主要的过滤代码(见第2步),init方法建立设置操作,而destroy方法进行清楚。

2)在doFilter方法中放入过滤行为。doFilter方法的第一个参数为ServletRequest对象。此对象给过滤器提供了对进入的信息 (包括表单数据、cookie和HTTP请求头)的完全访问。第二个参数为ServletResponse,通常在简单的过滤器中忽略此参数。最后一个参 数为FilterChain,如下一步所述,此参数用来调用servlet或JSP页。

3)调用FilterChain对象的doFilter方法。Filter接口的doFilter方法取一个FilterChain对象作为它的一个参 数。在调用此对象的doFilter方法时,激活下一个相关的过滤器。如果没有另一个过滤器与servlet或JSP页面关联,则servlet或JSP 页面被激活。

4)对相应的servlet和JSP页面注册过滤器。在部署描述符文件(web.xml)中使用filter和filter-mapping元素。

5)禁用激活器servlet。防止用户利用缺省servlet URL绕过过滤器设置。

1.1   建立一个实现Filter接口的类
      所有过滤器都必须实现javax.servlet.Filter。这个接口包含三个方法,分别为doFilter、init和destroy。

public void doFilter(ServletRequset request,
                     ServletResponse response,
                     FilterChain chain)
     thows ServletException, IOException

每当调用一个过滤器(即,每次请求与此过滤器相关的servlet或JSP页面)时,就执行其doFilter方法。正是这个方法包含了大部分过滤逻辑。 第一个参数为与传入请求有关的ServletRequest。对于简单的过滤器,大多数过滤逻辑是基于这个对象的。如果处理HTTP请求,并且需要访问诸 如getHeader或getCookies等在ServletRequest中无法得到的方法,就要把此对象构造成 HttpServletRequest。

第二个参数为ServletResponse。除了在两个情形下要使用它以外,通常忽略这个参数。首先,如果希望完全阻塞对相关servlet或JSP页 面的访问。可调用response.getWriter并直接发送一个响应到客户机。其次,如果希望修改相关的servlet或JSP页面的输出,可把响 应包含在一个收集所有发送到它的输出的对象中。然后,在调用serlvet或JSP页面后,过滤器可检查输出,如果合适就修改它,之后发送到客户机。

DoFilter的最后一个参数为FilterChain对象。对此对象调用doFilter以激活与servlet或JSP页面相关的下一个过滤器。如果没有另一个相关的过滤器,则对doFilter的调用激活servlet或JSP本身。

public void init(FilterConfig config)   thows ServletException

init方法只在此过滤器第一次初始化时执行,不是每次调用过滤器都执行它。对于简单的过滤器,可提供此方法的一个空体,但有两个原因需要使用init。 首先,FilterConfig对象提供对servlet环境及web.xml文件中指派的过滤器名的访问。因此,普遍的办法是利用init将 FilterConfig对象存放在一个字段中,以便doFilter方法能够访问servlet环境或过滤器名.其次,FilterConfig对象具 有一个getInitParameter方法,它能够访问部署描述符文件(web.xml)中分配的过滤器初始化参数。

public void destroy( )
     大多数过滤器简单地为此方法提供一个空体,不过,可利用它来完成诸如关闭过滤器使用的文件或数据库连接池等清除任务。

1.2   将过滤行为放入doFilter方法
     doFilter方法为大多数过滤器地关键部分。每当调用一个过滤器时,都要执行doFilter。对于大多数过滤器来说,doFilter执行的步骤是 基于传入的信息的。因此,可能要利用作为doFilter的第一个参数提供的ServletRequest。这个对象常常构造为 HttpServletRequest类型,以提供对该类的更特殊方法的访问。

1.3   调用FilterChain对象的doFilter方法
     Filter接口的doFilter方法以一个FilterChain对象作为它的第三个参数。在调用该对象的doFilter方法时,激活下一个相关的 过滤器。这个过程一般持续到链中最后一个过滤器为止。在最后一个过滤器调用其FilterChain对象的doFilter方法时,激活servlet或 页面自身。
但是,链中的任意过滤器都可以通过不调用其FilterChain的doFilter方法中断这个过程。在这样的情况下,不再调用JSP页面的serlvet,并且中断此调用过程的过滤器负责将输出提供给客户机。

1.4   对适当的servlet和JSP页面注册过滤器
     部署描述符文件的2.3版本引入了两个用于过滤器的元素,分别是:filter和filter-mapping。filter元素向系统注册一个过滤对象,filter-mapping元素指定该过滤对象所应用的URL。

1.filter元素
filter元素位于部署描述符文件(web.xml)的前部,所有filter-mapping、servlet或servlet-mapping元素之前。filter元素具有如下六个可能的子元素:

1、 icon   这是一个可选的元素,它声明IDE能够使用的一个图象文件。
2、filter-name   这是一个必需的元素,它给过滤器分配一个选定的名字。
3、display-name   这是一个可选的元素,它给出IDE使用的短名称。
4、 description   这也是一个可选的元素,它给出IDE的信息,提供文本文档。
5、 filter-class   这是一个必需的元素,它指定过滤器实现类的完全限定名。
6、 init-param   这是一个可选的元素,它定义可利用FilterConfig的getInitParameter方法读取的初始化参数。单个过滤器元素可包含多个init-param元素。

请注意,过滤是在serlvet规范2.3版中初次引入的。因此,web.xml文件必须使用DTD的2.3版本。下面介绍一个简单的例子:

<xml version="1.0" encoding="ISO-8859-1"?>
    DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
    <web-app>
     <filter>
        <filter-name>MyFilterfilter-name>
        <filter-class>myPackage.FilterClassfilter-class>
      filter>
     
      <filter-mapping>...filter-mapping>
     <web-app>


2.filter-mapping元素
    filter-mapping元素位于web.xml文件中filter元素之后serlvet元素之前。它包含如下三个可能的子元素:

1、 filter-name   这个必需的元素必须与用filter元素声明时给予过滤器的名称相匹配。

2、 url-pattern   此元素声明一个以斜杠(/)开始的模式,它指定过滤器应用的URL。所有filter-mapping元素中必须提供url-pattern或 servlet-name。但不能对单个filter-mapping元素提供多个url-pattern元素项。如果希望过滤器适用于多个模式,可重复 整个filter-mapping元素。

3、 servlet-name   此元素给出一个名称,此名称必须与利用servlet元素给予servlet或JSP页面的名称相匹配。不能给单个filter-mapping元素提供 多个servlet-name元素项。如果希望过滤器适合于多个servlet名,可重复这个filter-mapping元素。
下面举一个例子:

xml version="1.0" encoding="ISO-8859-1"?>
    DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd">
    <web-app>
      <filter>
        <filter-name>MyFilterfilter-name>
        <filter-class>myPackage.FilterClassfilter-class>
      filter>
     
      <filter-mapping>
        <filter-name>MyFilterfilter-name>
        <url-pattern>/someDirectory/SomePage.jspurl-pattern>
      filter-mapping>
    web-app>

 

1.5   禁用激活器servlet
     在对资源应用过滤器时,可通过指定要应用过滤器的URL模式或servlet名来完成。如果提供servlet名,则此名称必须与web.xml的 servlet元素中给出的名称相匹配。如果使用应用到一个serlvet的URL模式,则此模式必须与利用web.xml的元素servlet- mapping指定的模式相匹配。但是,多数服务器使用“激活器servlet”为servlet体统一个缺省的URL:http: //host/WebAppPrefix/servlet/ServletName。需要保证用户不利用这个URL访问servlet(这样会绕过过滤器 设置)。
例如,假如利用filter和filter-mapping指示名为SomeFilter的过滤器应用到名为SomeServlet的servlet,则如下:

<filter>
      <filter-name>SomeFilterfilter-name>
      <filter-class>somePackage.SomeFilterClassfilter-class>
   <filter>
   
    <filter-mapping>
      <filter-name>SomeFilterfilter-name>
      <servlet-name>SomeServletservlet-name>
     <filter-mapping>

 


接着,用servlet和servlet-mapping规定URL   http://host/webAppPrefix/Blah 应该调用SomeSerlvet,如下所示:

<filter>
      <filter-name>SomeFilterfilter-name>
      <filter-class>somePackage.SomeFilterClassfilter-class>
    filter>
   
    <filter-mapping>
      <filter-name>SomeFilterfilter-name>
      <servlet-name>/Blahservlet-name>
     <filter-mapping>

 


现在,在客户机使用URL   http://host/webAppPrefix/Blah 时就会调用过滤器。过滤器不应用到
http://host/webAppPrefix/servlet/SomePackage.SomeServletClass
尽管有关闭激活器的服务器专用方法。但是,可移植最强的方法时重新映射Web应用钟的/servlet模式,这样使所有包含此模式的请求被送到相同的 servlet中。为了重新映射此模式,首先应该建立一个简单的servlet,它打印一条错误消息,或重定向用户到顶层页。然后,使用servlet和 servlet-mapping元素发送包含/servlet模式的请求到该servlet。程序清单9-1给出了一个简短的例子。

程序清单9-1 web.xml(重定向缺省servlet URL的摘录)

xml version="1.0" encoding="ISO-8859-1"?>
    DOCTYPE web-app PUBLIC
         "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
         "http://java.sun.com/dtd/web-app_2_3.dtd">
    <web-app>
   
    <servlet>
      <servlet-name>Errorservlet-name>
      <servlet-class>somePackage.ErrorServletservlet-class>
    servlet>
   
    <servlet-mapping>
      <servlet-name>Errorservlet-name>
      <url-pattern>/servlet/*url-pattern>
    servlet-mapping>
   
    <web-app>

本文参考:http://www.javaeye.com/topic/140553

posted @ 2009-05-10 09:33 lanxin1020 阅读(240) | 评论 (0)编辑 收藏

struts的validator的客户端验证,不能进行多表单页面的验证,原因是由<html:script>标签生成的javascipt是根据每个表单,生成一段代码。例如:

<html:javascript formName="searchSgbySjForm" dynamicJavascript="true" staticJavascript="false"/>
生成  :
             var bCancel = false; 
             function validateSearchSgbySjForm(form)
            {                                                                         
                if (bCancel)       return true;        
                else        return validateRequired(form) && validateDate(form);   
            }
            function required ()
           {     
               this.aa = new Array("sgfssjq", "事故发生时间起 不可为空.", new Function ("varName", "this.datePatternStrict='yyy-MM-dd'; return thisvarName];"));
               this.ab = new Array("sgfssjz", "事故发生时间止 不可为空.", new Function ("varName", "this.datePatternStrict='yyy-MM-dd';  return this[varName];"));   
           }

           function DateValidations ()
          {
              this.aa = new Array("sgfssjq", "事故发生时间起 不是有效的日期类型.", new Function ("varName", "this.datePatternStrict='yyy-MM-dd';  return this  [varName];"));
             this.ab = new Array("sgfssjz", "事故发生时间止 不是有效的日期类型.", new Function ("varName", "this.datePatternStrict='yyy-MM-dd';  return this[varName];"));   
         }

如果有多个的话required和DateValidations 都会重复的,而javascript是只认最后一个函数的。所以,会导致验证出错。

再写一个标签 ,主要根据原来的代码修改,代码如下:

package com.tmri.acd.tag;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.tagext.BodyTagSupport;

import org.apache.commons.validator.Field;
import org.apache.commons.validator.Form;
import org.apache.commons.validator.ValidatorAction;
import org.apache.commons.validator.ValidatorResources;
import org.apache.commons.validator.util.ValidatorUtils;
import org.apache.commons.validator.Var;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.config.ModuleConfig;
import com.tmri.acd.tag.TagUtils;
import org.apache.struts.util.MessageResources;
import org.apache.struts.validator.Resources;
import org.apache.struts.validator.ValidatorPlugIn;
import java.util.StringTokenizer;

public class JavascriptValidatorTag extends BodyTagSupport
{
 private static final Comparator actionComparator = new Comparator()
{       
            public int compare(Object o1, Object o2)
           {
                 ValidatorAction va1 = (ValidatorAction) o1;           
                 ValidatorAction va2 = (ValidatorAction) o2;
                 if ((va1.getDepends() == null || va1.getDepends().length() == 0) && (va2.getDepends() == null || va2.getDepends().length() == 0))
                {               
                    return 0;
                }
                else if (  (va1.getDepends() != null && va1.getDepends().length() > 0)  && (va2.getDepends() == null || va2.getDepends().length() == 0))
               {  
                   return 1;
               }
}

 

posted @ 2009-05-09 22:59 lanxin1020 阅读(172) | 评论 (0)编辑 收藏
HTML 注释 

在客户端显示一个注释. 

JSP 语法
<!-- comment [ <%= expression %> ] --> 
例子 1
<!-- This file displays the user login screen --> 
在客户端的HTML源代码中产生和上面一样的数据: 

<!-- This file displays the user login screen --> 
例子 2
<!-- This page was loaded on <%= (new java.util.Date()).toLocaleString() %> --> 
在客户端的HTML源代码中显示为: 

<!-- This page was loaded on January 1, 2000 --> 
描述
这种注释和HTML中很像,也就是它可以在"查看源代码"中看到. 

唯一有些不同的就是,你可以在这个注释中用表达式(例子2所示).这个表达示是不定的,由页面不同而不同,你能够使用各种表达式,只要是合法的就行。更多的请看表达式



隐藏注释 


写在JSP程序中,但不是发给客户。 

JSP 语法
<%-- comment --%> 
例子:

<%@ page language="java" %> <html> <head><title>A Comment Test</title></head> <body> <h2>A Test of Comments</h2> <%-- This comment will not be visible in the page source --%> </body> </html> 

描述
用隐藏注释标记的字符会在JSP编译时被忽略掉。这个注释在你希望隐藏或注释你的JSP程序时是很有用的。JSP编译器不是会对<%--and--%>之间的语句进行编译的,它不会显示在客户的浏览器中,也不会在源代码中看到 

声明 


在JSP程序中声明合法的变量和方法 

JSP 语法
<%! declaration; [ declaration; ]+ ... %> 
例子
<%! int i = 0; %> 
<%! int a, b, c; %> 
<%! Circle a = new Circle(2.0); %> 
描述
声明你将要在JSP程序中用到的变量和方法。你也必须这样做,不然会出错. 

你可以一次性声明多个变量和方法,只要以";"结尾就行,当然这些声明在Java中要是合法的。 

当你声明方法或变量时,请注意以下的一些规则: 

声明必须以";"结尾(Scriptlet有同样的规则,但是 表达式就不同了). 
你可以直接使用在<% @ page %>中被包含进来的已经声明的变量和方法,不需要对它们重新进行声明. 
一个声明仅在一个页面中有效。如果你想每个页面都用到一些声明,最好把它们写成一个单独的文件,然后用<%@ include %>或<jsp:include >元素包含进来。 


表达式 


包含一个符合JSP语法的表达式 

JSP 语法
<%= expression %> 
例子
<font color="blue"><%= map.size() %></font> 
<b><%= numguess.getHint() %></b>. 
描述
表达式元素表示的是一个在脚本语言中被定义的表达式,在运行后被自动转化为字符串,然后插入到这个表达示在JSP文件的位置显示。因为这个表达式的值已经被转化为字符串,所以你能在一行文本中插入这个表达式(形式和ASP完全一样). 

当你在JSP中使用表达式时请记住以下几点: 

你不能用一个分号(";")来作为表达式的结束符.但是同样的表达式用在scriptlet中就需要以分号来结尾了!查看Scriptlet 
这个表达式元素能够包括任何在Java Language Specification中有效的表达式. 
有时候表达式也能做为其它JSP元素的属性值.一个表达式能够变得很复杂,它可能由一个或多个表达式组成,这些表达式的顺序是从左到右。 


[/b]Scriptlet [/b]


包含一个有效的程序段. 

JSP 语法
<% code fragment %> 
例子
<% 
String name = null; 
if (request.getParameter("name") == null) { 
%> 
<%@ include file="error.html" %> 
<% 
} else { 
foo.setName(request.getParameter("name")); 
if (foo.getName().equalsIgnoreCase("integra")) 
name = "acura"; 
if (name.equalsIgnoreCase( "acura" )) { 
%> 
描述
一个scriptlet能够包含多个jsp语句,方法,变量,表达式 

因为scriptlet,我们便能做以下的事: 

声明将要用到的变量或方法(参考 声明). 
编写JSP表达式(参考 表达式). 
使用任何隐含的对象和任何用<jsp:useBean>声明过的对象 
编写JSP语句 (如果你在使用Java语言,这些语句必须遵从Java Language Specification,). 
任何文本,HTML标记,JSP元素必须在scriptlet之外 
当JSP收到客户的请求时,scriptlet就会被执行,如果scriptlet有显示的内容,这些显示的内容就被存在out对象中。 


Include 指令


在JSP中包含一个静态的文件,同时解析这个文件中的JSP语句. 

JSP 语法
<%@ include file="relativeURL" %> 
例子
include.jsp: 

<html> <head><title>An Include Test</title></head> <body bgcolor="white"> <font color="blue"> The current date and time are <%@ include file="date.jsp" %> </font> </body> </html> 

date.jsp:

 <%@ page import="java.util.*" %> <%= (new java.util.Date() ).toLocaleString() %> 

Displays in the page: 
The current date and time are 
Aug 30, 1999 2:38:40 

描述
<%@include %>指令将会在JSP编译时插入一个包含文本或代码的文件,当你使用<%@ include %>指命时,这个包含的过程就当是静态的。静态的包含就是指这个被包含的文件将会被插入到JSP文件中去,这个包含的文件可以是JSP文件,HTML文件,文本文件。如果包含的是JSP文件,这个包含的JSP的文件中代码将会被执行。 

如果你仅仅只是用include 来包含一个静态文件。那么这个包含的文件所执行的结果将会插入到JSP文件中放<% @ include %>的地方。一旦包含文件被执行,那么主JSP文件的过程将会被恢复,继续执行下一行. 

这个被包含文件可以是html文件,jsp文件,文本文件,或者只是一段Java代码,但是你得注意在这个包含文件中不能使用<html>,</html>,<body>,</body>标记,因为这将会影响在原JSP文件中同样的标记 ,这样做有时会导致错误. 

有一些<%@ include %>指命的行为是以特殊的JSP编译条件为基础,比如: 

这个被包含的文件必须对所有客户都有开放且必须f有效,或者它有安全限制 
如果这个包含文件被改变,包含此文件的JSP文件将被重新编译 
属性:
file="relativeURL"
这个包含文件的路径名一般来说是指相对路径,不需要什么端口,协议,和域名,如下: 

"error.jsp""templates/onlinestore.html""/beans/calendar.jsp" 

如果这个路径以"/"开头,那么这个路径主要是参照JSP应用的上下关系路径,如果路径是以文件名或目录名开头,那么这个路径就是正在使用的JSP文件的当前路径.


Page 指令


定义JSP文件中的全局属性. 

JSP 语法
<%@ page 
[ language="java" ] 
[ extends="package.class" ] 
[ import="{package.class | package.*}, ..." ] 
[ session="true | false" ] 
[ buffer="none | 8kb | sizekb" ] 
[ autoFlush="true | false" ] 
[ isThreadSafe="true | false" ] 
[ info="text" ] 
[ errorPage="relativeURL" ] 
[ contentType="mimeType [ ;charset=characterSet ]" | "text/html ; charset=ISO-8859-1" ] 
[ isErrorPage="true | false" ] 
%> 
例子
<%@ page import="java.util.*, java.lang.*" %> 
<%@ page buffer="5kb" autoFlush="false" %> 
<%@ page errorPage="error.jsp" %> 
描述
<%@ page %>指令作用于整个JSP页面,同样包括静态的包含文件。但是<% @ page %>指令不能作用于动态的包含文件,比如 <jsp:include> 

你可以在一个页面中用上多个<% @ page %>指令,但是其中的属性只能用一次,不过也有个例外,那就是import属性。因为import属性和Java中的import语句差不多(参照Java Language),所以你就能多用此属性几次了. 

无论你把<% @ page %>指令放在JSP的文件的哪个地方,它的作用范围都是整个JSP页面。不过,为了JSP程序的可读性,以及好的编程习惯,最好还是把它放在JSP文件的顶部. 

属性
language="java"
声明脚本语言的种类,暂时只能用"java" 

extends="package.class"
标明JSP编译时需要加入的Java Class的全名,但是得慎重的使用它,它会限制JSP的编译能力. 

import="{package.class | package.* }, ..."
需要导入的Java包的列表,这些包就作用于程序段,表达式,以及声明。 

下面的包在JSP编译时已经导入了,所以你就不需要再指明了: 

java.lang.* 
javax.servlet.* 
javax.servlet.jsp.* 
javax.servlet.http.* 

session="true | false"
设定客户是否需要HTTP Session.(学过ASP的人,应该对它不陌生)如果它为true,那么Session是有用的。 

如果它有false,那么你就不能使用session对象,以及定义了scope=session的<jsp:useBean>元素。这样的使用会导致错误. 

缺省值是true. 

buffer="none | 8kb | sizekb"
buffer的大小被out对象用于处理执行后的JSP对客户浏览器的输出。缺省值是8kb 

autoFlush="true | false"
设置如果buffer溢出,是否需要强制输出,如果其值被定义为true(缺省值),输出正常,如果它被设置为false,如果这个buffer溢出,就会导致一个意外错误的发生.如果你把buffer设置为none,那么你就不能把autoFlush设置为false. 

isThreadSafe="true | false"
设置Jsp文件是否能多线程使用。缺省值是true,也就是说,JSP能够同时处理多个用户的请求,如果设置为false,一个jsp只能一次处理一个请求 

info="text"
一个文本在执行JSP将会被逐字加入JSP中,你能够使用Servlet.getServletInfo方法取回。 

errorPage="relativeURL"
设置处理异常事件的JSP文件。 

isErrorPage="true | false"
设置此页是否为出错页,如果被设置为true,你就能使用exception对象. 

contentType="mimeType [ ;charset=characterSet ]" | "text/html;charset=ISO-8859-1"
设置MIME类型 。缺省MIME 类型是: text/html, 缺省字符集为 ISO-8859-1.


<jsp:forward>


重定向一个HTML文件,JSP文件,或者是一个程序段. 

JSP 语法
<jsp:forward page={"relativeURL" | "<%= expression %>"} /> 
or 
<jsp:forward page={"relativeURL" | "<%= expression %>"} > 
<jsp:param name="parameterName" 
value="{parameterValue | <%= expression %>}" />+ 
</jsp:forward> 
例子
<jsp:forward page="/servlet/login" /> 
<jsp:forward page="/servlet/login"> 
<jsp:param name="username" value="jsmith" /> 
</jsp:forward> 
描述
<jsp:forward>标签从一个JSP文件向另一个文件传递一个包含用户请求的request对象.<jsp:forward>标签以下的代码,将不能执行. 

你能够向目标文件传送参数和值,在这个例子中我们传递的参数名为username,值为scott,如果你使用了<jsp:param>标签的话,目标文件必须是一个动态的文件,能够处理参数. 

如果你使用了非缓冲输出的话,那么使用<jsp:forward>时就要小心。如果在你使用<jsp:forward>之前,jsp文件已经有了数据,那么文件执行就会出错. 

属性
page="{relativeURL | <%= expression %>}"
这里是一个表达式或是一个字符串用于说明你将要定向的文件或URL.这个文件可以是JSP,程序段,或者其它能够处理request对象的文件(如asp,cgi,php). 

<jsp:param name="parameterName" value="{parameterValue | <%= expression %>}" />+
向一个动态文件发送一个或多个参数,这个文件一定是动态文件. 

如果你想传递多个参数,你可以在一个JSP文件中使用多个<jsp:param>。name指定参数名,value指定参数值.


<jsp:getProperty>


获取Bean的属性值,用于显示在页面中 

JSP 语法
<jsp:getProperty name="beanInstanceName" property="propertyName" /> 
例子
<jsp:useBean id="calendar" scope="page" class="employee.Calendar" /> 
<h2> 
Calendar of <jsp:getProperty name="calendar" property="username" /> 
</h2> 
描述
这个<jsp:getProperty>元素将获得Bean的属性值,并可以将其使用或显示在JSP页面中.在你使用<jsp:getProperty>之前,你必须用<jsp:useBean>创建它. 

<jsp:getProperty>元素有一些限制: 

你不能使用<jsp:getProperty>来检索一个已经被索引了的属性 
你能够和JavaBeans组件一起使用<jsp:getProperty>,但是不能与Enterprise Bean一起使用。 
属性
name="beanInstanceName"
bean的名字,由<jsp:useBean>指定 

property="propertyName"
所指定的Bean的属性名。 

技巧:
在sun的JSP参考中提到,如果你使用<jsp:getProperty>来检索的值是空值,那么NullPointerException将会出现,同时如果使用程序段或表达式来检索其值,那么在浏览器上出现的是null(空).


<jsp:include>


包含一个静态或动态文件. 

JSP 语法
<jsp:include page="{relativeURL | <%= expression%>}" flush="true" /> 
or 
<jsp:include page="{relativeURL | <%= expression %>}" flush="true" > 
<jsp:param name="parameterName" value="{parameterValue | <%= expression %>}" />+ 
</jsp:include> 
Examples
<jsp:include page="scripts/login.jsp" /> 
<jsp:include page="copyright.html" /> 
<jsp:include page="/index.html" /> 
<jsp:include page="scripts/login.jsp"> 
<jsp:param name="username" value="jsmith" /> 
</jsp:include> 

描述
<jsp:include>元素允许你包含动态文件和静态,这两种包含文件的结果是不同的。如果文件仅是静态文件,那么这种包含仅仅是把包含文件的内容加到jsp文件中去,而如果这个文件动态的,那么这个被包含文件也会被Jsp编译器执行(这一切与asp相似) 

你不能从文件名上判断一个文件是动态的还是静态的,比如aspcn.asp 就有可能只是包含一些信息而已,而不需要执行。<jsp:include>能够同时处理这两种文件,因此你就不需要包含时还要判断此文件是动态的还是静态的. 

如果这个包含文件是动态的,那么你还可以用<jsp:param>还传递参数名和参数值。 

属性
page="{relativeURL | <%= expression %>}"
参数为一相对路径,或者是代表相对路径的表达式. 

flush="true"
这里你必须使用flush="true",你不能使用false值.缺省值为false 

<jsp:param name="parameterName" value="{parameterValue | <%= expression %> }" />+
<jsp:param>子句能让你传递一个或多个参数给动态文件 

你能在一个页面中使用多个<jsp:param>来传递多个参数,


<jsp:plugin>


执行一个applet或Bean,有可能的话还要下载一个Java插件用于执行它. 

JSP 语法
<jsp:plugin 
type="bean | applet" 
code="classFileName" 
codebase="classFileDirectoryName" 
[ name="instanceName" ] 
[ archive="URIToArchive, ..." ] 
[ align="bottom | top | middle | left | right" ] 
[ height="displayPixels" ] 
[ width="displayPixels" ] 
[ hspace="leftRightPixels" ] 
[ vspace="topBottomPixels" ] 
[ jreversion="JREVersionNumber | 1.1" ] 
[ nspluginurl="URLToPlugin" ] 
[ iepluginurl="URLToPlugin" ] > 
[ <jsp:params> 
[ <jsp:param name="parameterName" value="{parameterValue | <%= expression %>}" /> ]+ 
</jsp:params> ] 

[ <jsp:fallback> text message for user </jsp:fallback> ] 

</jsp:plugin> 

例子
<jsp:plugin type=applet code="Molecule.class" codebase="/html"> 
<jsp:params> 
<jsp:param name="molecule" value="molecules/benzene.mol" /> 
</jsp:params> 
<jsp:fallback> 
<p>Unable to load applet</p> 
</jsp:fallback> 
</jsp:plugin> 
描述
<jsp:plugin>元素用于在浏览器中播放或显示一个对象(典型的就是applet和Bean),而这种显示需要在浏览器的java插件。 

当Jsp文件被编译,送往浏览器时,<jsp:plugin>元素将会根据浏览器的版本替换成<object>或者<embed>元素。注意,<object>用于HTML 4.0 ,<embed>用于HTML 3.2. 

一般来说,<jsp:plugin>元素会指定对象是Applet还是Bean,同样也会指定class的名字,还有位置,另外还会指定将从哪里下载这个Java插件。具体如下: 

属性
type="bean | applet"
.将被执行的插件对象的类型,你必须得指定这个是Bean还是applet,因为这个属性没有缺省值. 

code="classFileName"
将会被Java插件执行的Java Class的名字,必须以.class结尾。这个文件必须存在于codebase属性指定的目录中. 

codebase="classFileDirectoryName"
将会被执行的Java Class文件的目录(或者是路径),如果你没有提供此属性,那么使用<jsp:plugin>的jsp文件的目录将会被使用. 

name="instanceName"
这个Bean或applet实例的名字,它将会在Jsp其它的地方调用. 

archive="URIToArchive, ..."
一些由逗号分开的路径名,这些路径名用于预装一些将要使用的class,这会提高applet的性能. 

align="bottom | top | middle | left | right"
图形,对象,Applet的位置,有以下值: 

bottom 
top 
middle 
left 
right 
height="displayPixels" width="displayPixels"
Applet或Bean将要显示的长宽的值,此值为数字,单位为象素. 

hspace="leftRightPixels" vspace="topBottomPixels"
Applet或Bean显示时在屏幕左右,上下所需留下的空间,单位为象素. 

jreversion="JREVersionNumber | 1.1" 
Applet或Bean运行所需的Java Runtime Environment (JRE) 的版本. 缺省值是 1.1. 

nspluginurl="URLToPlugin" 
Netscape Navigator用户能够使用的JRE的下载地址,此值为一个标准的URL,如http://www.aspcn.com/jsp 

iepluginurl="URLToPlugin"
IE用户能够使用的JRE的下载地址,此值为一个标准的URL,如http://www.aspcn.com/jsp 

<jsp:params> [ <jsp:param name="parameterName" value="{parameterValue | <%= expression %>}" /> ]+ </jsp:params>
你需要向applet或Bean传送的参数或参数值。 

<jsp:fallback> text message for user </jsp:fallback>
一段文字用于Java 插件不能启动时显示给用户的,如果插件能够启动而applet或Bean不能,那么浏览器会有一个出错信息弹出.


<jsp:setProperty>


设置Bean中的属性值. 

JSP语法
<jsp:setProperty 
name="beanInstanceName" 

property= "*" | 
property="propertyName" [ param="parameterName" ] | 
property="propertyName" value="{string | <%= expression %>}" 

/> 
例子
<jsp:setProperty name="mybean" property="*" /> 
<jsp:setProperty name="mybean" property="username" /> 
<jsp:setProperty name="mybean" property="username" value="Steve" /> 
描述
<jsp:setProperty>元素使用Bean给定的setter方法,在Bean中设置一个或多个属性值。你在使用这个元素之前必须得使用<jsp:useBean>声明此Bean.因为,<jsp:useBean>和<jsp:setProperty>是联系在一起的,同时这他们使用的Bean实例的名字也应当相匹配(就是说,在<jsp:setProperty>中的name的值应当和<jsp:useBean>中id的值相同) 

你能使用多种方法利用<jsp:setProperty>来设定属性值 : 

通过用户输入的所有值(被做为参数储存中request对象中)来匹配Bean中的属性 
通过用户输入的指定的值来匹配Bean中指定的属性 
在运行时使用一个表达式来匹配Bean的属性 
每一种设定属性值的方法都有其特定的语法,下面我们会来讲解 

属性及其用法
name="beanInstanceName"
表示已经在<jsp:useBean>中创建的Bean实例的名字. 

property="*"
储存用户在Jsp输入的所有值,用于匹配Bean中的属性。在Bean中的属性的名字必须和request对象中的参数名一致. 

从客户传到服器上的参数值一般都是字符类型 ,这些字符串为了能够在Bean中匹配就必须转换成其它的类型,下面的表中列出了Bean属性的类型以及他们的转换方法.

把字符串转化为其它类型的方法. Property 类型 
方法 
boolean or Boolean 
java.lang.Boolean.valueOf(String) 
byte or Byte 
java.lang.Byte.valueOf(String) 
char or Character 
java.lang.Character.valueOf(String) 
double or Double 
java.lang.Double.valueOf(String) 
integer or Integer 
java.lang.Integer.valueOf(String) 
float or Float 
java.lang.Float.valueOf(String) 
long or Long 
java.lang.Long.valueOf(String) 

如果request对象的参数值中有空值,那么对应的Bean属性将不会设定任何值。同样的,如果Bean中有一个属性没有与之对应的Request参数值,那么这个属性同样也不会设定. 

property="propertyName" [ param="parameterName" ]
使用request中的一个参数值来指定Bean中的一个属性值。在这个语法中,property指定Bean的属性名,param指定request中的参数名. 

如果bean属性和request参数的名字不同,那么你就必须得指定property和param ,如果他们同名,那么你就只需要指明property就行了. 

如查参数值为空(或未初始化),对应的Bean属性不被设定. 

property="propertyName" value="{string | <%= expression %>}"
使用指定的值来设定Bean属性。这个值可以是字符串,也可以是表达式。如果这个字符串,那么它就会被转换成Bean属性的类型(查看上面的表).如果它是一个表达式,那么它类型就必须和它将要设定的属性值的类型一致。 

如果参数值为空,那么对应的属性值也不会被设定。另外,你不能在一个<jsp:setProperty>中同时使用param和value 

技巧
如果你使用了property="*",那么Bean的属性没有必要按Html表单中的顺序排序 


<jsp:useBean>


创建一个Bean实例并指定它的名字和作用范围. 

JSP 语法
<jsp:useBean 
id="beanInstanceName" 
scope="page | request | session | application" 

class="package.class" | 
type="package.class" | 
class="package.class" type="package.class" | 
beanName="{package.class | <%= expression %>}" type="package.class" 


/> | 
> other elements </jsp:useBean> 

例子
<jsp:useBean id="cart" scope="session" class="session.Carts" /> 
<jsp:setProperty name="cart" property="*" /> 
<jsp:useBean id="checking" scope="session" class="bank.Checking" > 
<jsp:setProperty name="checking" property="balance" value="0.0" /> 
</jsp:useBean> 

描述
<jsp:useBean>用于定位或示例一个JavaBeans组件。<jsp:useBean>首先会试图定位一个Bean实例,如果这个Bean不存在,那么<jsp:useBean>就会从一个class或模版中进行示例。 

为了定位或示例一个Bean,<jsp:useBean>会进行以下步聚,顺序如下: 

通过给定名字和范围试图定位一个Bean. 
对这个Bean对象引用变量以你指定的名字命名. 
如果发现了这个Bean,将会在这个变量中储存这个引用。如果你也指定了类型,那么这个Bean也设置为相应的类型. 
如果没有发现这个Bean,将会从你指定的class中示例,并将此引用储存到一个新的变量中去。如果这个class的名字代表的是一个模版,那么这个Bean被java.beans.Beans.instantiate示例. 
如果<jsp:useBean>已经示例(不是定位)了Bean,同时<jsp:useBean>和</jsp:useBean>中有元素,那么将会执行其中的代码. 
<jsp:useBean>元素的主体通常包含有<jsp:setProperty>元素,用于设置Bean的属性值。正如上面第五步所说的,<jsp:useBean>的主体仅仅只有在<jsp:useBean>示例Bean时才会被执行,如果这个Bean已经存在,<jsp:useBean>能够定位它,那么主体中的内容将不会起作用 

属性以及用法
id="beanInstanceName"
在你所定义的范围中确认Bean的变量,你能在后面的程序中使用此变量名来分辨不同的Bean 

这个变量名对大小写敏感,必须符合你所使用的脚本语言的规定,在Java Programming Language中,这个规定在Java Language 规范已经写明。如果这个Bean已经在别的<jsp:useBean>中创建,那么这个id的值必须与原来的那个id值一致. 

scope="page | request | session | application"
Bean存在的范围以及id变量名的有效范围。缺省值是page,以下是详细说明: 

page - 你能在包含<jsp:useBean>元素的JSP文件以及此文件中的所有静态包含文件中使用Bean,直到页面执行完毕向客户端发回响应或转到另一个文件为止。
   
request - 你在任何执行相同请求的Jsp文件中使用Bean,直到页面执行完毕向客户端发回响应或转到另一个文件为止。你能够使用Request对象访问Bean,比如request.getAttribute(beanInstanceName)

session - 从创建Bean开始,你就能在任何使用相同session的Jsp文件中使用Bean.这个Bean存在于整个Session生存周期内,任何在分享此Session的Jsp文件都能使用同一Bean.注意在你创建Bean的Jsp文件中<% @ page %>指令中必须指定session=true

application - 从创建Bean开始,你就能在任何使用相同application的Jsp文件中使用Bean.这个Bean存在于整个application生存周期内,任何在分享此application的Jsp文件都能使用同一Bean. 
class="package.class"
使用new关键字以及class构造器从一个class中示例一个bean.这个class不能是抽象的,必须有一个公用的,没有参数的构造器.这个package的名字区别大小写。 

type="package.class"
如果这个Bean已经在指定的范围中存在,那么写这个Bean一个新的数据库类型 。如果你没有使用class或beanName指定type,Bean将不会被示例.package和class的名字,区分大小写. 

beanName="{package.class | <%= expression %>}" type="package.class"
使用java.beans.Beans.instantiate方法来从一个class或连续模版中示例一个Bean,同时指定Bean的类型。 

beanName可以是package和class也可以是表达式,它的值会传给Beans.instantiate.tupe的值可以和Bean相同。 

package 和 class 名字区分大小写.
posted @ 2009-05-09 11:58 lanxin1020 阅读(177) | 评论 (0)编辑 收藏

1 引子
try…catch…finally恐怕是大家再熟悉不过的语句了,而且感觉用起来也是很简单,逻辑上似乎也是很容易理解。不过,我亲自体验的“教训”告诉我,这个东西可不是想象中的那么简单、听话。不信?那你看看下面的代码,“猜猜”它执行后的结果会是什么?不要往后看答案、也不许执行代码看真正答案哦。如果你的答案是正确,那么这篇文章你就不用浪费时间看啦。
public class TestException
{
    public TestException()
    {
    }
    boolean testEx() throws Exception
    {
        boolean ret = true;
        try
        {
            ret = testEx1();
        }
        catch (Exception e)
        {
            System.out.println("testEx, catch exception");
            ret = false;
            throw e;
        }
        finally
        {
            System.out.println("testEx, finally; return value=" + ret);
            return ret;
        }
    }
    boolean testEx1() throws Exception
    {
        boolean ret = true;
        try
        {
            ret = testEx2();
            if (!ret)
            {
                return false;
            }
            System.out.println("testEx1, at the end of try");
            return ret;
        }
        catch (Exception e)
        {
            System.out.println("testEx1, catch exception");
            ret = false;
            throw e;
        }
        finally
        {
            System.out.println("testEx1, finally; return value=" + ret);
            return ret;
        }
    }
    boolean testEx2() throws Exception
    {
        boolean ret = true;
        try
        {
            int b = 12;
            int c;
            for (int i = 2; i >= -2; i--)
            {
                c = b / i;
                System.out.println("i=" + i);
            }
            return true;
        }
        catch (Exception e)
        {
            System.out.println("testEx2, catch exception");
            ret = false;
            throw e;
        }
        finally
        {
            System.out.println("testEx2, finally; return value=" + ret);
            return ret;
        }
    }
    public static void main(String[] args)
    {
        TestException testException1 = new TestException();
        try
        {
            testException1.testEx();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}
你的答案是什么?是下面的答案吗?
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, catch exception
testEx1, finally; return value=false
testEx, catch exception
testEx, finally; return value=false
如果你的答案真的如上面所说,那么你错啦。^_^,那就建议你仔细看一看这篇文章或者拿上面的代码按各种不同的情况修改、执行、测试,你会发现有很多事情不是原来想象中的那么简单的。
现在公布正确答案:
i=2
i=1
testEx2, catch exception
testEx2, finally; return value=false
testEx1, finally; return value=false
testEx, finally; return value=false

2 基础知识

2.1 相关概念
例外是在程序运行过程中发生的异常事件,比如除0溢出、数组越界、文件找不到等,这些事件的发生将阻止程序的正常运行。为了加强程序的鲁棒性,程序设计时,必须考虑到可能发生的异常事件并做出相应的处理。C语言中,通过使用if语句来判断是否出现了例外,同时,调用函数通过被调用函数的返回值感知在被调用函数中产生的例外事件并进行处理。全程变量ErroNo常常用来反映一个异常事件的类型。但是,这种错误处理机制会导致不少问题。
Java通过面向对象的方法来处理例外。在一个方法的运行过程中,如果发生了例外,则这个方法生成代表该例外的一个对象,并把它交给运行时系统,运行时系统寻找相应的代码来处理这一例外。我们把生成例外对象并把它提交给运行时系统的过程称为抛弃(throw)一个例外。运行时系统在方法的调用栈中查找,从生成例外的方法开始进行回朔,直到找到包含相应例外处理的方法为止,这一个过程称为捕获(catch)一个例外。
2.2 Throwable类及其子类
 用面向对象的方法处理例外,就必须建立类的层次。类 Throwable位于这一类层次的最顶层,只有它的后代才可以做为一个例外被抛弃。图1表示了例外处理的类层次。
从图中可以看出,类Throwable有两个直接子类:Error和Exception。Error类对象(如动态连接错误等),由Java虚拟机生成并抛弃(通常,Java程序不对这类例外进行处理);Exception类对象是Java程序处理或抛弃的对象。它有各种不同的子类分别对应于不同类型的例外。其中类RuntimeException代表运行时由Java虚拟机生成的例外,如算术运算例外ArithmeticException(由除0错等导致)、数组越界例外ArrayIndexOutOfBoundsException等;其它则为非运行时例外,如输入输出例外IOException等。Java编译器要求Java程序必须捕获或声明所有的非运行时例外,但对运行时例外可以不做处理。
 

2.3  异常处理关键字
Java的异常处理是通过5个关键字来实现的:try,catch,throw,throws,finally。JB的在线帮助中对这几个关键字是这样解释的:
Throws:  Lists the exceptions a method could throw.
Throw:   Transfers control of the method to the exception handler.
Try:    Opening exception-handling statement.
Catch:  Captures the exception.
Finally: Runs its code before terminating the program.
2.3.1 try语句 
try语句用大括号{}指定了一段代码,该段代码可能会抛弃一个或多个例外。
2.3.2 catch语句 
catch语句的参数类似于方法的声明,包括一个例外类型和一个例外对象。例外类型必须为Throwable类的子类,它指明了catch语句所处理的例外类型,例外对象则由运行时系统在try所指定的代码块中生成并被捕获,大括号中包含对象的处理,其中可以调用对象的方法。
catch语句可以有多个,分别处理不同类的例外。Java运行时系统从上到下分别对每个catch语句处理的例外类型进行检测,直到找到类型相匹配的catch语句为止。这里,类型匹配指catch所处理的例外类型与生成的例外对象的类型完全一致或者是它的父类,因此,catch语句的排列顺序应该是从特殊到一般。
也可以用一个catch语句处理多个例外类型,这时它的例外类型参数应该是这多个例外类型的父类,程序设计中要根据具体的情况来选择catch语句的例外处理类型。 
2.3.3 finally语句 
try所限定的代码中,当抛弃一个例外时,其后的代码不会被执行。通过finally语句可以指定一块代码。无论try所指定的程序块中抛弃或不抛弃例外,也无论catch语句的例外类型是否与所抛弃的例外的类型一致,finally所指定的代码都要被执行,它提供了统一的出口。通常在finally语句中可以进行资源的清除工作。如关闭打开的文件等。
2.3.4 throws语句 
throws总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常。对大多数Exception子类来说,Java 编译器会强迫你声明在一个成员函数中抛出的异常的类型。如果异常的类型是Error或 RuntimeException, 或它们的子类,这个规则不起作用, 因为这在程序的正常部分中是不期待出现的。 如果你想明确地抛出一个RuntimeException,你必须用throws语句来声明它的类型。
2.3.5 throw语句 
throw总是出现在函数体中,用来抛出一个异常。程序会在throw语句后立即终止,它后面的语句执行不到,然后在包含它的所有try块中(可能在上层调用函数中)从里向外寻找含有与其匹配的catch子句的try块。

3 关键字及其中语句流程详解

3.1 try的嵌套
你可以在一个成员函数调用的外面写一个try语句,在这个成员函数内部,写另一个try语句保护其他代码。每当遇到一个try语句,异常的框架就放到堆栈上面,直到所有的try语句都完成。如果下一级的try语句没有对某种异常进行处理,堆栈就会展开,直到遇到有处理这种异常的try语句。下面是一个try语句嵌套的例子。
class MultiNest {
    static void procedure() {
        try {
            int a = 0;
            int b = 42/a;
        } catch(java.lang.ArithmeticException e) {
            System.out.println("in procedure, catch ArithmeticException: " + e);
        }
    }
    public static void main(String args[]) {
        try {
            procedure();
        } catch(java.lang. Exception e) {
            System.out.println("in main, catch Exception: " + e);
        }
    }
}
这个例子执行的结果为:
in procedure, catch ArithmeticException: java.lang.ArithmeticException: / by zero
成员函数procedure里有自己的try/catch控制,所以main不用去处理 ArrayIndexOutOfBoundsException;当然如果如同最开始我们做测试的例子一样,在procedure中catch到异常时使用throw e;语句将异常抛出,那么main当然还是能够捕捉并处理这个procedure抛出来的异常。例如在procedure函数的catch中的System.out语句后面增加throw e;语句之后,执行结果就变为:
in procedure, catch ArithmeticException: java.lang.ArithmeticException: / by zero
in main, catch Exception: java.lang.ArithmeticException: / by zero

3.2 try-catch程序块的执行流程以及执行结果
相对于try-catch-finally程序块而言,try-catch的执行流程以及执行结果还是比较简单的。
首先执行的是try语句块中的语句,这时可能会有以下三种情况:
    1.如果try块中所有语句正常执行完毕,那么就不会有其他的“动做”被执行,整个try-catch程序块正常完成。
    2.如果try语句块在执行过程中碰到异常V,这时又分为两种情况进行处理:
-->如果异常V能够被与try相应的catch块catch到,那么第一个catch到这个异常的catch块(也是离try最近的一个与异常V匹配的catch块)将被执行;如果catch块执行正常,那么try-catch程序块的结果就是“正常完成”;如果该catch块由于原因R突然中止,那么try-catch程序块的结果就是“由于原因R突然中止(completes abruptly)”。
-->如果异常V没有catch块与之匹配,那么这个try-catch程序块的结果就是“由于抛出异常V而突然中止(completes abruptly)”。
    3. 如果try由于其他原因R突然中止(completes abruptly),那么这个try-catch程序块的结果就是“由于原因R突然中止(completes abruptly)”。

3.3 try-catch-finally程序块的执行流程以及执行结果
try-catch-finally程序块的执行流程以及执行结果比较复杂。
首先执行的是try语句块中的语句,这时可能会有以下三种情况:
1.如果try块中所有语句正常执行完毕,那么finally块的居于就会被执行,这时分为以下两种情况:
-->如果finally块执行顺利,那么整个try-catch-finally程序块正常完成。
-->如果finally块由于原因R突然中止,那么try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”
2.如果try语句块在执行过程中碰到异常V,这时又分为两种情况进行处理:
-->如果异常V能够被与try相应的catch块catch到,那么第一个catch到这个异常的catch块(也是离try最近的一个与异常V匹配的catch块)将被执行;这时就会有两种执行结果:
-->如果catch块执行正常,那么finally块将会被执行,这时分为两种情况:
-->如果finally块执行顺利,那么整个try-catch-finally程序块正常完成。
-->如果finally块由于原因R突然中止,那么try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”
-->如果catch块由于原因R突然中止,那么finally模块将被执行,分为两种情况:
-->如果如果finally块执行顺利,那么整个try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”。
-->如果finally块由于原因S突然中止,那么整个try-catch-finally程序块的结局是“由于原因S突然中止(completes abruptly)”,原因R将被抛弃。
(注意,这里就正好和我们的例子相符合,虽然我们在testEx2中使用throw e抛出了异常,但是由于testEx2中有finally块,而finally块的执行结果是complete abruptly的(别小看这个用得最多的return,它也是一种导致complete abruptly的原因之一啊——后文中有关于导致complete abruptly的原因分析),所以整个try-catch-finally程序块的结果是“complete abruptly”,所以在testEx1中调用testEx2时是捕捉不到testEx1中抛出的那个异常的,而只能将finally中的return结果获取到。
如果在你的代码中期望通过捕捉被调用的下级函数的异常来给定返回值,那么一定要注意你所调用的下级函数中的finally语句,它有可能会使你throw出来的异常并不能真正被上级调用函数可见的。当然这种情况是可以避免的,以testEx2为例:如果你一定要使用finally而且又要将catch中throw的e在testEx1中被捕获到,那么你去掉testEx2中的finally中的return就可以了。
这个事情已经在OMC2.0的MIB中出现过啦:服务器的异常不能完全被反馈到客户端。)
-->如果异常V没有catch块与之匹配,那么finally模块将被执行,分为两种情况:
-->如果finally块执行顺利,那么整个try-catch-finally程序块的结局就是“由于抛出异常V而突然中止(completes abruptly)”。
-->如果finally块由于原因S突然中止,那么整个try-catch-finally程序块的结局是“由于原因S突然中止(completes abruptly)”,异常V将被抛弃。
3.如果try由于其他原因R突然中止(completes abruptly),那么finally块被执行,分为两种情况:
-->如果finally块执行顺利,那么整个try-catch-finally程序块的结局是“由于原因R突然中止(completes abruptly)”。
-->如果finally块由于原因S突然中止,那么整个try-catch-finally程序块的结局是“由于原因S突然中止(completes abruptly)”,原因R将被抛弃。
3.4 try-catch-finally程序块中的return
从上面的try-catch-finally程序块的执行流程以及执行结果一节中可以看出无论try或catch中发生了什么情况,finally都是会被执行的,那么写在try或者catch中的return语句也就不会真正的从该函数中跳出了,它的作用在这种情况下就变成了将控制权(语句流程)转到finally块中;这种情况下一定要注意返回值的处理。
例如,在try或者catch中return false了,而在finally中又return true,那么这种情况下不要期待你的try或者catch中的return false的返回值false被上级调用函数获取到,上级调用函数能够获取到的只是finally中的返回值,因为try或者catch中的return语句只是转移控制权的作用。
3.5 如何抛出异常
如果你知道你写的某个函数有可能抛出异常,而你又不想在这个函数中对异常进行处理,只是想把它抛出去让调用这个函数的上级调用函数进行处理,那么有两种方式可供选择:
第一种方式:直接在函数头中throws SomeException,函数体中不需要try/catch。比如将最开始的例子中的testEx2改为下面的方式,那么testEx1就能捕捉到testEx2抛出的异常了。
    boolean testEx2() throws Exception{
        boolean ret = true;
        int b=12;
        int c;
        for (int i=2;i>=-2;i--){
            c=b/i;
            System.out.println("i="+i);
        }
        return true;   
}
第二种方式:使用try/catch,在catch中进行一定的处理之后(如果有必要的话)抛出某种异常。例如上面的testEx2改为下面的方式,testEx1也能捕获到它抛出的异常:
    boolean testEx2() throws Exception{
        boolean ret = true;
        try{
            int b=12;
            int c;
            for (int i=2;i>=-2;i--){
                c=b/i;
                System.out.println("i="+i);
            }
            return true;
        }catch (Exception e){
            System.out.println("testEx2, catch exception");
            Throw e;
        }
    }
第三种方法:使用try/catch/finally,在catch中进行一定的处理之后(如果有必要的话)抛出某种异常。例如上面的testEx2改为下面的方式,testEx1也能捕获到它抛出的异常:
    boolean testEx2() throws Exception{
        boolean ret = true;
        try{
            int b=12;
            int c;
            for (int i=2;i>=-2;i--){
                c=b/i;
                System.out.println("i="+i);
                throw new Exception("aaa");
            }
            return true;
        }catch (java.lang.ArithmeticException e){
            System.out.println("testEx2, catch exception");
            ret = false;
            throw new Exception("aaa");
        }finally{
            System.out.println("testEx2, finally; return value="+ret);
        }
    }
4  关于abrupt completion
前面提到了complete abruptly(暂且理解为“突然中止”或者“异常结束”吧),它主要包含了两种大的情形:abrupt completion of expressions and statements,下面就分两种情况进行解释。
4.1 Normal and Abrupt Completion of Evaluation
每一个表达式(expression)都有一种使得其包含的计算得以一步步进行的正常模式,如果每一步计算都被执行且没有异常抛出,那么就称这个表达式“正常结束(complete normally)”;如果这个表达式的计算抛出了异常,就称为“异常结束(complete abruptly)”。异常结束通常有一个相关联的原因(associated reason),通常也就是抛出一个异常V。
与表达式、操作符相关的运行期异常有:
-->A class instance creation expression, array creation expression , or string concatenation operatior expression throws an OutOfMemoryError if there is insufficient memory available.
-->An array creation expression throws a NegativeArraySizeException if the value of any dimension expression is less than zero.
-->A field access throws a NullPointerException if the value of the object reference  expression is null.
-->A method invocation expression that invokes an instance method throws a NullPointerException if the target reference is null.
-->An array access throws a NullPointerException if the value of the array reference  expression is null.
-->An array access throws an ArrayIndexOutOfBoundsException if the value of the array index expression is negative or greater than or equal to the length of the array.
-->A cast throws a ClassCastException if a cast is found to be impermissible at run time.
-->An integer division or integer remainder operator throws an ArithmeticException if the value of the right-hand operand expression is zero.
-->An assignment to an array component of reference type throws an ArrayStoreException when the value to be assigned is not compatible with the component type of the array.
4.2 Normal and Abrupt Completion of Statements
正常情况我们就不多说了,在这里主要是列出了abrupt completion的几种情况:
-->break, continue, and return 语句将导致控制权的转换,从而使得statements不能正常地、完整地执行。
-->某些表达式的计算也可能从java虚拟机抛出异常,这些表达式在上一小节中已经总结过了;一个显式的的throw语句也将导致异常的抛出。抛出异常也是导致控制权的转换的原因(或者说是阻止statement正常结束的原因)。
如果上述事件发生了,那么这些statement就有可能使得其正常情况下应该都执行的语句不能完全被执行到,那么这些statement也就是被称为是complete abruptly.
导致abrupt completion的几种原因:
-->A break with no label
-->A break with a given label
-->A continue with no label
-->A continue with a given label
-->A return with no value
-->A return with a given value A
-->throw with a given value, including exceptions thrown by the Java virtual machine
5 关于我们的编程的一点建议
弄清楚try-catch-finally的执行情况后我们才能正确使用它。
如果我们使用的是try-catch-finally语句块,而我们又需要保证有异常时能够抛出异常,那么在finally语句中就不要使用return语句了(finally语句块的最重要的作用应该是释放申请的资源),因为finally中的return语句会导致我们的throw e被抛弃,在这个try-catch-finally的外面将只能看到finally中的返回值(除非在finally中抛出异常)。(我们需要记住:不仅throw语句是abrupt completion 的原因,return、break、continue等这些看起来很正常的语句也是导致abrupt completion的原因。)

posted @ 2009-05-08 15:23 lanxin1020 阅读(198) | 评论 (0)编辑 收藏
 
    1.详解

    1)DOM(JAXP Crimson解析器)
    DOM是用与平台和语言无关的方式表示XML文档的官方W3C标准。DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才能做任何工作。由于它是基于信息层次的,因而DOM被认为是基于树或基于对象的。DOM以及广义的基于树的处理具有几个优点。首先,由于树在内存中是持久的,因此可以修改它以便应用程序能对数据和结构作出更改。它还可以在任何时候在树中上下导航,而不是像SAX那样是一次性的处理。DOM使用起来也要简单得多。

    2)SAX

    SAX处理的优点非常类似于流媒体的优点。分析能够立即开始,而不是等待所有的数据被处理。而且,由于应用程序只是在读取数据时检查数据,因此不需要将数据存储在内存中。这对于大型文档来说是个巨大的优点。事实上,应用程序甚至不必解析整个文档;它可以在某个条件得到满足时停止解析。一般来说,SAX还比它的替代者DOM快许多。

    选择DOM还是选择SAX? 对于需要自己编写代码来处理XML文档的开发人员来说, 选择DOM还是SAX解析模型是一个非常重要的设计决策。 DOM采用建立树形结构的方式访问XML文档,而SAX采用的事件模型。

    DOM解析器把XML文档转化为一个包含其内容的树,并可以对树进行遍历。用DOM解析模型的优点是编程容易,开发人员只需要调用建树的指令,然后利用navigation APIs访问所需的树节点来完成任务。可以很容易的添加和修改树中的元素。然而由于使用DOM解析器的时候需要处理整个XML文档,所以对性能和内存的要求比较高,尤其是遇到很大的XML文件的时候。由于它的遍历能力,DOM解析器常用于XML文档需要频繁的改变的服务中。

    SAX解析器采用了基于事件的模型,它在解析XML文档的时候可以触发一系列的事件,当发现给定的tag的时候,它可以激活一个回调方法,告诉该方法制定的标签已经找到。SAX对内存的要求通常会比较低,因为它让开发人员自己来决定所要处理的tag.特别是当开发人员只需要处理文档中所包含的部分数据时,SAX这种扩展能力得到了更好的体现。但用SAX解析器的时候编码工作会比较困难,而且很难同时访问同一个文档中的多处不同数据。

    3)JDOM http://www.jdom.org

    JDOM的目的是成为Java特定文档模型,它简化与XML的交互并且比使用DOM实现更快。由于是第一个Java特定模型,JDOM一直得到大力推广和促进。正在考虑通过“Java规范请求JSR-102”将它最终用作“Java标准扩展”。从2000年初就已经开始了JDOM开发。

    JDOM与DOM主要有两方面不同。首先,JDOM仅使用具体类而不使用接口。这在某些方面简化了API,但是也限制了灵活性。第二,API大量使用了Collections类,简化了那些已经熟悉这些类的Java开发者的使用。

    JDOM文档声明其目的是“使用20%(或更少)的精力解决80%(或更多)Java/XML问题”(根据学习曲线假定为20%)。JDOM对于大多数Java/XML应用程序来说当然是有用的,并且大多数开发者发现API比DOM容易理解得多。JDOM还包括对程序行为的相当广泛检查以防止用户做任何在XML中无意义的事。然而,它仍需要您充分理解XML以便做一些超出基本的工作(或者甚至理解某些情况下的错误)。这也许是比学习DOM或JDOM接口都更有意义的工作。

    JDOM自身不包含解析器。它通常使用SAX2解析器来解析和验证输入XML文档(尽管它还可以将以前构造的DOM表示作为输入)。它包含一些转换器以将JDOM表示输出成SAX2事件流、DOM模型或XML文本文档。JDOM是在Apache许可证变体下发布的开放源码。

    4)DOM4J http://dom4j.sourceforge.net

    虽然DOM4J代表了完全独立的开发结果,但最初,它是JDOM的一种智能分支。它合并了许多超出基本XML文档表示的功能,包括集成的XPath支持、XML Schema支持以及用于大文档或流化文档的基于事件的处理。它还提供了构建文档表示的选项,它通过DOM4J API和标准DOM接口具有并行访问功能。从2000下半年开始,它就一直处于开发之中。

    为支持所有这些功能,DOM4J使用接口和抽象基本类方法。DOM4J大量使用了API中的Collections类,但是在许多情况下,它还提供一些替代方法以允许更好的性能或更直接的编码方法。直接好处是,虽然DOM4J付出了更复杂的API的代价,但是它提供了比JDOM大得多的灵活性。

    在添加灵活性、XPath集成和对大文档处理的目标时,DOM4J的目标与JDOM是一样的:针对Java开发者的易用性和直观操作。它还致力于成为比JDOM更完整的解决方案,实现在本质上处理所有Java/XML问题的目标。在完成该目标时,它比JDOM更少强调防止不正确的应用程序行为。

    DOM4J是一个非常非常优秀的Java XML API,具有性能优异、功能强大和极端易用使用的特点,同时它也是一个开放源代码的软件。如今你可以看到越来越多的Java软件都在使用DOM4J来读写XML,特别值得一提的是连Sun的JAXM也在用DOM4J.

    2。比较

    1)DOM4J性能最好,连Sun的JAXM也在用DOM4J.目前许多开源项目中大量采用DOM4J,例如大名鼎鼎的Hibernate也用DOM4J来读取XML配置文件。如果不考虑可移植性,那就采用DOM4J.

    2)JDOM和DOM在性能测试时表现不佳,在测试10M文档时内存溢出。在小文档情况下还值得考虑使用DOM和JDOM.虽然JDOM的开发者已经说明他们期望在正式发行版前专注性能问题,但是从性能观点来看,它确实没有值得推荐之处。另外,DOM仍是一个非常好的选择。DOM实现广泛应用于多种编程语言。它还是许多其它与XML相关的标准的基础,因为它正式获得W3C推荐(与基于非标准的Java模型相对),所以在某些类型的项目中可能也需要它(如在JavaScript中使用DOM)。

    3)SAX表现较好,这要依赖于它特定的解析方式-事件驱动。一个SAX检测即将到来的XML流,但并没有载入到内存(当然当XML流被读入时,会有部分文档暂时隐藏在内存中)。
   
    3. 四种xml操作方式的基本使用方法
    xml文件:
Xml代码
  1. <?xml version="1.0" encoding="gbk"?><list><node><name>weidewei</name><space>http://wishlife.javaeye.com</space></node><node><name>flying</name><space>http://user.qzone.qq.com/94611981</space></node></list>  

   
     程序代码:
Java代码
  1. import java.io.File;   
  2. import java.util.Iterator;   
  3. import java.util.List;   
  4.   
  5. import javax.xml.parsers.DocumentBuilder;   
  6. import javax.xml.parsers.DocumentBuilderFactory;   
  7. import javax.xml.parsers.SAXParser;   
  8. import javax.xml.parsers.SAXParserFactory;   
  9.   
  10. import org.dom4j.io.SAXReader;   
  11. import org.jdom.Element;   
  12. import org.jdom.input.SAXBuilder;   
  13. import org.w3c.dom.Document;   
  14. import org.w3c.dom.NodeList;   
  15. import org.xml.sax.Attributes;   
  16. import org.xml.sax.InputSource;   
  17. import org.xml.sax.SAXException;   
  18. import org.xml.sax.helpers.DefaultHandler;   
  19.   
  20. public class MyXMLReader extends DefaultHandler {   
  21.   
  22.     java.util.Stack tags = new java.util.Stack();   
  23.     public MyXMLReader() {   
  24.         super();   
  25.     }   
  26.     /**  
  27.      * DOM方式  
  28.     * @since V2.0  
  29.     * @author David.Wei  
  30.     * @date 2008-4-11  
  31.     * @return void  
  32.      */  
  33.     public void DOM() {   
  34.         long lasting = System.currentTimeMillis();   
  35.   
  36.         try {   
  37.             File f = new File("F:/xmltest.xml");   
  38.             DocumentBuilderFactory factory = DocumentBuilderFactory   
  39.                     .newInstance();   
  40.             DocumentBuilder builder = factory.newDocumentBuilder();   
  41.             Document doc = builder.parse(f);   
  42.             NodeList nl = doc.getElementsByTagName("node");   
  43.             for (int i = 0; i < nl.getLength(); i++) {   
  44.                 System.out.println("|| Name:  |"  
  45.                         + doc.getElementsByTagName("name").item(i)   
  46.                                 .getFirstChild().getNodeValue());   
  47.                 System.out.println("||Space:  |"  
  48.                         + doc.getElementsByTagName("space").item(i)   
  49.                                 .getFirstChild().getNodeValue());   
  50.                 System.out.println("-------------------------------------------------");            }   
  51.         } catch (Exception e) {   
  52.             e.printStackTrace();   
  53.         }   
  54.         System.out.println("DOM RUNTIME:"  
  55.                 + (System.currentTimeMillis() - lasting) + " MS");   
  56.     }   
  57.   
  58.        
  59.   
  60.     /**  
  61.      * SAX方式  
  62.     * @since V2.0  
  63.     * @author David.Wei  
  64.     * @date 2008-4-11  
  65.     * @return void  
  66.      */  
  67.     public void SAX() {   
  68.   
  69.         long lasting = System.currentTimeMillis();   
  70.         try {   
  71.             SAXParserFactory sf = SAXParserFactory.newInstance();   
  72.             SAXParser sp = sf.newSAXParser();   
  73.             MyXMLReader reader = new MyXMLReader();   
  74.             sp.parse(new InputSource("F:/xmltest.xml"), reader);   
  75.         } catch (Exception e) {   
  76.             e.printStackTrace();   
  77.         }   
  78.         System.out.println("SAX RUNTIME:"  
  79.                 + (System.currentTimeMillis() - lasting) + " MS");   
  80.     }   
  81.   
  82.     public void startElement(String uri, String localName, String qName,   
  83.             Attributes attrs) {   
  84.         tags.push(qName);   
  85.     }   
  86.   
  87.     public void characters(char ch[], int start, int length)   
  88.             throws SAXException {   
  89.         String tag = (String) tags.peek();   
  90.         if (tag.equals("name")) {   
  91.             System.out.println("|| Name:  |" + new String(ch, start, length));   
  92.         }   
  93.         if (tag.equals("space")) {   
  94.             System.out.println("||Space:  |" + new String(ch, start, length));   
  95.         }   
  96.         System.out.println("-------------------------------------------------");   
  97.     }   
  98.   
  99.     /**  
  100.      * JDOM方式  
  101.     * @since V2.0  
  102.     * @author David.Wei  
  103.     * @date 2008-4-11  
  104.     * @return void  
  105.      */  
  106.     public void JDOM() {   
  107.         long lasting = System.currentTimeMillis();   
  108.         try {   
  109.             SAXBuilder builder = new SAXBuilder();   
  110.             org.jdom.Document doc = builder.build(new File("F:/xmltest.xml"));   
  111.             Element foo = doc.getRootElement();   
  112.             List allChildren = foo.getChildren();   
  113.             for (int i = 0; i < allChildren.size(); i++) {   
  114.                 System.out.println("|| Name:  |"  
  115.                         + ((Element) allChildren.get(i)).getChild("name")   
  116.                                 .getText());   
  117.                 System.out.println("||Space:  |"  
  118.                         + ((Element) allChildren.get(i)).getChild("space")   
  119.                                 .getText());   
  120.                 System.out.println("-------------------------------------------------");            }   
  121.         } catch (Exception e) {   
  122.             e.printStackTrace();   
  123.         }   
  124.         System.out.println("JDOM RUNTIME:"  
  125.                 + (System.currentTimeMillis() - lasting) + " MS");   
  126.     }   
  127.   
  128.     /**  
  129.      * DOM4J方式  
  130.     * @since V2.0  
  131.     * @author David.Wei  
  132.     * @date 2008-4-11  
  133.     * @return void  
  134.      */  
  135.     public void DOM4J() {   
  136.         long lasting = System.currentTimeMillis();   
  137.         try {   
  138.             File f = new File("F:/xmltest.xml");   
  139.             SAXReader reader = new SAXReader();   
  140.             org.dom4j.Document doc = reader.read(f);   
  141.             org.dom4j.Element root = doc.getRootElement();   
  142.             org.dom4j.Element foo;   
  143.             for (Iterator i = root.elementIterator("node"); i.hasNext();) {   
  144.                 foo = (org.dom4j.Element) i.next();   
  145.                 System.out.println("|| Name:  |" + foo.elementText("name"));   
  146.                 System.out.println("||Space:  |" + foo.elementText("space"));   
  147.                 System.out.println("-------------------------------------------------");   
  148.             }   
  149.         } catch (Exception e) {   
  150.             e.printStackTrace();   
  151.         }   
  152.         System.out.println("DOM4J RUNTIME:"  
  153.                 + (System.currentTimeMillis() - lasting) + " MS");   
  154.     }   
  155.   
  156.     public static void main(String arge[]) {   
  157.         MyXMLReader myXML = new MyXMLReader();   
  158.         System.out.println("=====================DOM=========================");   
  159.         myXML.DOM();   
  160.         System.out.println("=====================SAX=========================");   
  161.         myXML.SAX();   
  162.         System.out.println("=====================JDOM========================");   
  163.         myXML.JDOM();   
  164.         System.out.println("=====================DOM4J=======================");   
  165.         myXML.DOM4J();   
  166.         System.out.println("=================================================");   
  167.     }   
  168. }  


运行结果:
Html代码
  1. =====================DOM=========================   
  2. || Name:  |weidewei   
  3. ||Space:  |http://wishlife.javaeye.com   
  4. -------------------------------------------------   
  5. || Name:  |flying   
  6. ||Space:  |http://user.qzone.qq.com/94611981   
  7. -------------------------------------------------   
  8. DOM RUNTIME:62 MS  
  9. =====================SAX=========================   
  10. || Name:  |weidewei   
  11. -------------------------------------------------   
  12. ||Space:  |http://wishlife.javaeye.com   
  13. -------------------------------------------------   
  14. || Name:  |flying   
  15. -------------------------------------------------   
  16. ||Space:  |http://user.qzone.qq.com/94611981   
  17. -------------------------------------------------   
  18. SAX RUNTIME:16 MS  
  19. =====================JDOM========================   
  20. || Name:  |weidewei   
  21. ||Space:  |http://wishlife.javaeye.com   
  22. -------------------------------------------------   
  23. || Name:  |flying   
  24. ||Space:  |http://user.qzone.qq.com/94611981   
  25. -------------------------------------------------   
  26. JDOM RUNTIME:78 MS  
  27. =====================DOM4J=======================   
  28. || Name:  |weidewei   
  29. ||Space:  |http://wishlife.javaeye.com   
  30. -------------------------------------------------   
  31. || Name:  |flying   
  32. ||Space:  |http://user.qzone.qq.com/94611981   
  33. -------------------------------------------------   
  34. DOM4J RUNTIME:78 MS  
  35. =================================================  
posted @ 2009-05-08 10:52 lanxin1020 阅读(161) | 评论 (0)编辑 收藏
Java中的instanceof关键字 收藏
 instanceof是Java的一个二元操作符,和==,>,<是同一类东东。由于它是由字母组成的,所以也是Java的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回boolean类型的数据。举个例子:

  String s = "I AM an Object!";
  boolean isObject = s instanceof Object;

  我们声明了一个String对象引用,指向一个String对象,然后用instancof来测试它所指向的对象是否是Object类的一个实例,显然,这是真的,所以返回true,也就是isObject的值为True。
  instanceof有一些用处。比如我们写了一个处理账单的系统,其中有这样三个类:

  public class Bill {//省略细节}
  public class PhoneBill extends Bill {//省略细节}
  public class GasBill extends Bill {//省略细节}

  在处理程序里有一个方法,接受一个Bill类型的对象,计算金额。假设两种账单计算方法不同,而传入的Bill对象可能是两种中的任何一种,所以要用instanceof来判断:

  public double calculate(Bill bill) {
  if (bill instanceof PhoneBill) {
  //计算电话账单
  }
  if (bill instanceof GasBill) {
  //计算燃气账单
  }
  ...
  }
  这样就可以用一个方法处理两种子类。

  然而,这种做法通常被认为是没有好好利用面向对象中的多态性。其实上面的功能要求用方法重载完全可以实现,这是面向对象变成应有的做法,避免回到结构化编程模式。只要提供两个名字和返回值都相同,接受参数类型不同的方法就可以了:

  public double calculate(PhoneBill bill) {
  //计算电话账单
  }

  public double calculate(GasBill bill) {
  //计算燃气账单
  }

  所以,使用instanceof在绝大多数情况下并不是推荐的做法,应当好好利用多态。
posted @ 2009-05-07 13:14 lanxin1020 阅读(166) | 评论 (0)编辑 收藏
  1. package cn.edu.tongji.cims.wade.system;   
  2.   
  3. import java.io.*;   
  4.   
  5. public class FileOperate {   
  6.   public FileOperate() {   
  7.   }   
  8.   
  9.   /**  
  10.    * 新建目录  
  11.    * @param folderPath String 如 c:/fqf  
  12.    * @return boolean  
  13.    */  
  14.   public void newFolder(String folderPath) {   
  15.     try {   
  16.       String filePath = folderPath;   
  17.       filePath = filePath.toString();   
  18.       java.io.File myFilePath = new java.io.File(filePath);   
  19.       if (!myFilePath.exists()) {   
  20.         myFilePath.mkdir();   
  21.       }   
  22.     }   
  23.     catch (Exception e) {   
  24.       System.out.println("新建目录操作出错");   
  25.       e.printStackTrace();   
  26.     }   
  27.   }   
  28.   
  29.   /**  
  30.    * 新建文件  
  31.    * @param filePathAndName String 文件路径及名称 如c:/fqf.txt  
  32.    * @param fileContent String 文件内容  
  33.    * @return boolean  
  34.    */  
  35.   public void newFile(String filePathAndName, String fileContent) {   
  36.   
  37.     try {   
  38.       String filePath = filePathAndName;   
  39.       filePath = filePath.toString();   
  40.       File myFilePath = new File(filePath);   
  41.       if (!myFilePath.exists()) {   
  42.         myFilePath.createNewFile();   
  43.       }   
  44.       FileWriter resultFile = new FileWriter(myFilePath);   
  45.       PrintWriter myFile = new PrintWriter(resultFile);   
  46.       String strContent = fileContent;   
  47.       myFile.println(strContent);   
  48.       resultFile.close();   
  49.   
  50.     }   
  51.     catch (Exception e) {   
  52.       System.out.println("新建目录操作出错");   
  53.       e.printStackTrace();   
  54.   
  55.     }   
  56.   
  57.   }   
  58.   
  59.   /**  
  60.    * 删除文件  
  61.    * @param filePathAndName String 文件路径及名称 如c:/fqf.txt  
  62.    * @param fileContent String  
  63.    * @return boolean  
  64.    */  
  65.   public void delFile(String filePathAndName) {   
  66.     try {   
  67.       String filePath = filePathAndName;   
  68.       filePath = filePath.toString();   
  69.       java.io.File myDelFile = new java.io.File(filePath);   
  70.       myDelFile.delete();   
  71.   
  72.     }   
  73.     catch (Exception e) {   
  74.       System.out.println("删除文件操作出错");   
  75.       e.printStackTrace();   
  76.   
  77.     }   
  78.   
  79.   }   
  80.   
  81.   /**  
  82.    * 删除文件夹  
  83.    * @param filePathAndName String 文件夹路径及名称 如c:/fqf  
  84.    * @param fileContent String  
  85.    * @return boolean  
  86.    */  
  87.   public void delFolder(String folderPath) {   
  88.     try {   
  89.       delAllFile(folderPath); //删除完里面所有内容   
  90.       String filePath = folderPath;   
  91.       filePath = filePath.toString();   
  92.       java.io.File myFilePath = new java.io.File(filePath);   
  93.       myFilePath.delete(); //删除空文件夹   
  94.   
  95.     }   
  96.     catch (Exception e) {   
  97.       System.out.println("删除文件夹操作出错");   
  98.       e.printStackTrace();   
  99.   
  100.     }   
  101.   
  102.   }   
  103.   
  104.   /**  
  105.    * 删除文件夹里面的所有文件  
  106.    * @param path String 文件夹路径 如 c:/fqf  
  107.    */  
  108.   public void delAllFile(String path) {   
  109.     File file = new File(path);   
  110.     if (!file.exists()) {   
  111.       return;   
  112.     }   
  113.     if (!file.isDirectory()) {   
  114.       return;   
  115.     }   
  116.     String[] tempList = file.list();   
  117.     File temp = null;   
  118.     for (int i = 0; i < tempList.length; i++) {   
  119.       if (path.endsWith(File.separator)) {   
  120.         temp = new File(path + tempList[i]);   
  121.       }   
  122.       else {   
  123.         temp = new File(path + File.separator + tempList[i]);   
  124.       }   
  125.       if (temp.isFile()) {   
  126.         temp.delete();   
  127.       }   
  128.       if (temp.isDirectory()) {   
  129.         delAllFile(path+"/"+ tempList[i]);//先删除文件夹里面的文件   
  130.         delFolder(path+"/"+ tempList[i]);//再删除空文件夹   
  131.       }   
  132.     }   
  133.   }   
  134.   
  135.   /**  
  136.    * 复制单个文件  
  137.    * @param oldPath String 原文件路径 如:c:/fqf.txt  
  138.    * @param newPath String 复制后路径 如:f:/fqf.txt  
  139.    * @return boolean  
  140.    */  
  141.   public void copyFile(String oldPath, String newPath) {   
  142.     try {   
  143.       int bytesum = 0;   
  144.       int byteread = 0;   
  145.       File oldfile = new File(oldPath);   
  146.       if (oldfile.exists()) { //文件存在时   
  147.         InputStream inStream = new FileInputStream(oldPath); //读入原文件   
  148.         FileOutputStream fs = new FileOutputStream(newPath);   
  149.         byte[] buffer = new byte[1444];   
  150.         int length;   
  151.         while ( (byteread = inStream.read(buffer)) != -1) {   
  152.           bytesum += byteread; //字节数 文件大小   
  153.           System.out.println(bytesum);   
  154.           fs.write(buffer, 0, byteread);   
  155.         }   
  156.         inStream.close();   
  157.       }   
  158.     }   
  159.     catch (Exception e) {   
  160.       System.out.println("复制单个文件操作出错");   
  161.       e.printStackTrace();   
  162.   
  163.     }   
  164.   
  165.   }   
  166.   
  167.   /**  
  168.    * 复制整个文件夹内容  
  169.    * @param oldPath String 原文件路径 如:c:/fqf  
  170.    * @param newPath String 复制后路径 如:f:/fqf/ff  
  171.    * @return boolean  
  172.    */  
  173.   public void copyFolder(String oldPath, String newPath) {   
  174.   
  175.     try {   
  176.       (new File(newPath)).mkdirs(); //如果文件夹不存在 则建立新文件夹   
  177.       File a=new File(oldPath);   
  178.       String[] file=a.list();   
  179.       File temp=null;   
  180.       for (int i = 0; i < file.length; i++) {   
  181.         if(oldPath.endsWith(File.separator)){   
  182.           temp=new File(oldPath+file[i]);   
  183.         }   
  184.         else{   
  185.           temp=new File(oldPath+File.separator+file[i]);   
  186.         }   
  187.   
  188.         if(temp.isFile()){   
  189.           FileInputStream input = new FileInputStream(temp);   
  190.           FileOutputStream output = new FileOutputStream(newPath + "/" +   
  191.               (temp.getName()).toString());   
  192.           byte[] b = new byte[1024 * 5];   
  193.           int len;   
  194.           while ( (len = input.read(b)) != -1) {   
  195.             output.write(b, 0, len);   
  196.           }   
  197.           output.flush();   
  198.           output.close();   
  199.           input.close();   
  200.         }   
  201.         if(temp.isDirectory()){//如果是子文件夹   
  202.           copyFolder(oldPath+"/"+file[i],newPath+"/"+file[i]);   
  203.         }   
  204.       }   
  205.     }   
  206.     catch (Exception e) {   
  207.       System.out.println("复制整个文件夹内容操作出错");   
  208.       e.printStackTrace();   
  209.   
  210.     }   
  211.   
  212.   }   
  213.   
  214.   /**  
  215.    * 移动文件到指定目录  
  216.    * @param oldPath String 如:c:/fqf.txt  
  217.    * @param newPath String 如:d:/fqf.txt  
  218.    */  
  219.   public void moveFile(String oldPath, String newPath) {   
  220.     copyFile(oldPath, newPath);   
  221.     delFile(oldPath);   
  222.   
  223.   }   
  224.   
  225.   /**  
  226.    * 移动文件到指定目录  
  227.    * @param oldPath String 如:c:/fqf.txt  
  228.    * @param newPath String 如:d:/fqf.txt  
  229.    */  
  230.   public void moveFolder(String oldPath, String newPath) {   
  231.     copyFolder(oldPath, newPath);   
  232.     delFolder(oldPath);   
  233.   
  234.   }   
  235. }  
posted @ 2009-05-07 11:16 lanxin1020 阅读(390) | 评论 (0)编辑 收藏

        如果在Java程序中你使用Java Native Interface(JNI) 来调用某个特定平台下的本地库文件,你就会发现这个过程很单调、乏味。Jeff Friesen一直在介绍一个知名度很低的Java开源项目:Java Native Access---它能够避免因使用JNI导致的错误和乏味,同时它还能让你通过编程的方式调用C语言库。

        在Java语言没有提供必要的APIs的情况下,Java程序使用Java Native Interface (JNI)来调用特定平台下的本地库是必要的。例如:在Windows XP平台中,我使用过JNI来调用通用串行总线和基于TWAIN的扫描仪器的库;在更古老的Windows NT平台中,调用过智能卡的库。

        我按照一个基本的、乏味的流程来解决这些问题:首先,我创建一个Java类用来载入JNI-friendly库(这个库能过访问其他的库)并且声明这个类的本地方法。然后,在使用JDK中的javah工具为JNI-friendly库中的函数---函数和这个类中的本地方法一一对应---创建一个代理。最后,我使用C语言写了一个库并用C编译器编译了这些代码。

        尽管完成这些流程并不是很困难,但是写C代码是一个很缓慢的过程---例如: C语言中的字符串处理是通过指针来实现的,这会很复杂的。而且,使用JNI很容易出现错误,导致内存泄漏、很难找到程序崩溃的原因。

        在Java开源系列的第二篇文章中,我要介绍一个更简单、更安全的解决方法:Todd Fast and Timothy Wall的Java Native Access (JNA) 项目。JNA能够让你在Java程序中调用本地方法时避免使用C和Java Native Interface。在这篇文章中,让我以简要的介绍        JNA和运行示例必需的软件来开始下面的内容。然后,向你展示如何使用JNA将3个Windows本地库中的有用代码移植到Java程序中。

Get started with JNA(JNA入门)

Java Native Access 项目 在Java.net上,你可以到这个网站上现在这个项目的代码和在线帮助文档。虽然在下载有5个相关的jar文件,在本文中你仅仅需要下载其中的jna.jar和example.jar。

Jna.jar提供基本的、运行这些示例文件必需的jna运行环境。这个jna.jar文件除了有Unix、Linux、Windows和Mac OS X平台相关的JNT-friendly本地库外,还包含其他几个类包。每一个本地库都是用来访问相对应平台下的本地方法的。

        example.jar包含了不同的示例来表明JNA的用途。其中的一个例子是使用JNA来实现一个在不同平台下的透明视窗技术的API。在文章最后的示例中将要展示如何使用这个API修复上个月的文章关于VerifyAge2应用中辨认透明效果的问题。

获取本地时间(Get local time)

如果你在Java Native Access 首页 看过“JNA如何入门”,你就会知道一个很简单的关于调用Windows 平台下的API函数:GetSystemTime() 的JNA示例。这个不完整的例子只是展示了JNA的基本特点。(在例子的基础上,我做了一个更完整的基于Windows的例子来介绍JNA)我在Windows平台下完善了这个例子来介绍JNA。

第一例子基于Windows GetLocalTime() API函数返回本地当前的时间和日期。和GetSystemTime()不同的是,返回的时间/日期是协调通用时间(UTC)格式的,GetLocalTime()返回的时间/日期信息的格式是根据当前时区来表示。

在一个Java程序中使用JNA调用GetLocalTime,你需要知道这个函数所在的Windows平台下的动态链接库(DLL)的名称(和可能所在的地理区域)。我们发现GetLocalTime()和GetSystemTime在同一个DLL文件中:kernel32.dll。你还需要知道GetLocalTime()在C语言环境中的申明。申明如下Listing 1:

Listing 1. GetLocalTime在C语言中的申明

typedef struct
{
   WORD wYear;
   WORD wMonth;
   WORD wDayOfWeek;
   WORD wDay;
   WORD wHour;
   WORD wMinute;
   WORD wSecond;
   WORD wMilliseconds;
}
SYSTEMTIME, *LPSYSTEMTIME;

VOID GetLocalTime(LPSYSTEMTIME lpst);


这个基于C语言的申明表明传到这个函数的参数数目和类型。在这个例子中,只有一个参数---一个指向Windows SYSTEMTIME结构体的指针。而且,每个结构体成员的类型是16bit长度的无符号整型。根据这些信息,你能够创建一个完全描述GetLocalTime()函数的接口,如Listing 2中所示:

Listing 2. Kernel32.java

// Kernel32.java

import com.sun.jna.*;
import com.sun.jna.win32.*;

public interface Kernel32 extends StdCallLibrary
{
   public static class SYSTEMTIME extends Structure
   {
      public short wYear;
      public short wMonth;
      public short wDayOfWeek;
      public short wDay;
      public short wHour;
      public short wMinute;
      public short wSecond;
      public short wMilliseconds;
   }

   void GetLocalTime (SYSTEMTIME result);
}


Kernel32 接口(The Kernel32 interface)

因为JNA使用通过一个接口来访问某个库中的函数,Listing 2表示了一个描述GetLocalTime()的接口。根据约定,我把接口命名为Kernel32是因为GetLocalTime()在Windows的kernel32.dll库。

这个接口必须继承com.sun..jna.Library接口。因为Windows API函数遵循stdcall调用协议(stdcall calling convention),为Windows API申明的接口也必须继承com.sun.jna.win32. StdCallLibrary接口。因此这个接口共继承了Library 和 com.sun.jna.win32.StdCall两个接口。

在前面,你已经知道了GetLocalTime() 需要一个指向SYSTEMTIME结构体的指针作为它唯一的参数。因为Java不支持指针,JNA是通过申明一个com.sun.jna.Structure的子类来代替的。根据java文档中抽象类的概念,在参数环境中,Structure相当于C语言的struct*。

在SYSTEMTIME类中的字段和C结构体中的相对应的属性字段的顺序是一一对应的。保证字段顺序的一致性是非常重要的。例如,我发现交换wYear和wMonth会导致wYear和wMonth值互换。

每个字段在java中是short integer类型的。按照JNA首页上 “默认类型映射”章节给出的提示,这个short integer分配类型是正确。然而,我们应该知道一个重要的区别:Windows平台下的WORD类型等同于C语言环境中的16-bit的无符号的short integer,而java中short integer是16-bit有符号的short integer。

一个类型映射的问题

通过比较一个API 函数返回的整型值,你会发现Windows/C 语言的无符号整型和Java语言的有符号整型的JNA类型映射是有问题的。在比较的过程中,如果你不细心,那么错误的执行过程可能导致决定性情况。导致这种后果是因为忘记任何数值的符号位的确定是根据:在无符号整型的情况下会被解释为正号,而在有符号整型的进制中被理解为负号的。

通过Kernel32获取本地时间(Access the local time with Kernel32)

JNA首页上的GetSystemTime()示例已经表明必须使用预先申明的接口为本地库分配一个实例对象。你可以通过com.sun.jna.Native类中静态公用方法loadLibrary(String name, Class interfaceClass)来完成上述的目标。Listing 3 所示:

Listing 3. LocalTime.java

// LocalTime.java

import com.sun.jna.*;

public class LocalTime
{
   public static void main (String [] args)
   {
      Kernel32 lib = (Kernel32) Native.loadLibrary ("kernel32",
                                                    Kernel32.class);
      Kernel32.SYSTEMTIME time = new Kernel32.SYSTEMTIME ();
      lib.GetLocalTime (time);
      System.out.println ("Year is "+time.wYear);
      System.out.println ("Month is "+time.wMonth);
      System.out.println ("Day of Week is "+time.wDayOfWeek);
      System.out.println ("Day is "+time.wDay);
      System.out.println ("Hour is "+time.wHour);
      System.out.println ("Minute is "+time.wMinute);
      System.out.println ("Second is "+time.wSecond);
      System.out.println ("Milliseconds are "+time.wMilliseconds);
   }
}


Listing 3 执行Kernel32 lib = (Kernel32) Native.loadLibrary ("kernel32", Kernel32.class);来分配一个Kernel32实例对象并且装载kernel32.dll。因为kernel32.dll是Windows平台下标准的dll文件,所以不要指定访问这个库的路径。然而,如果找不到这个dll文件,loadLibrary()会抛出一个UnsatisfiedLinkError异常。

Kernel32.SYSTEMTIME time = new Kernel32.SYSTEMTIME ();创建了一个SYSTEMTIME结构体的示例。初始化后下面是lib.GetLocalTime (time);,这句话使用本地的时间/日期来给这个实例赋值。几个System.out.println()语句是输出这些值。

编译和运行这个应用(Compile and run the application)

这部分很容易。假设jna.jar、Kernel32.java和LocalTime.java是放在当前文件夹中,调用java –cp jna.jar;. LocalTime.java来编译这个应用的源代码。如果在Windows平台下,调用invoke java –cp jna.jar;. LocalTime 来运行这个应用。你可以得到类似与Listing 4的输出结果:

Listing 4. 从LocalTime.java生成的输出

Year is 2007
Month is 12
Day of Week is 3
Day is 19
Hour is 12
Minute is 35
Second is 13
Milliseconds are 156


获取操纵杆信息(Accessing joystick device info)

上面的例子已经介绍了JNA,但是这个获取本地时间和日期的例子并没有很好的利用这个技术,甚至也没有体现JNI的价值。Java语言中的System.currentTimeMillis()函数已经以毫秒的格式返回了这些信息。因为Java语言没有为游戏控制器提供API,所以获取操纵杆的信息更适合JNA的使用。

例如,你要构建一个平台无关的Java库,而且这些库使用JNA调用Linux, Mac OS X, Windwos和Unix平台中本地的操纵杆API。为了简洁和方便起见,这个例子仅仅是调用Windows平台下的操纵杆API。而且我将重点介绍这个API很小的一部分。

类似GetLocalTime(),第一步是辨别出操作杆API的DLL,这个DLL是winmm.dll,和kernel32.dll在同一个文件夹中,它包含了操作杆的API和其他的多媒体APIs。还需知道要被使用的操作杆函数基于C语言的声明。这些函数声明已经在Listing 5中列出来了。

Listing 5. C-based declarations for some Joystick API functions

#define MAXPNAMELEN 32

typedef struct
{
   WORD  wMid;                  // manufacturer identifier
   WORD  wPid;                  // product identifier
   TCHAR szPname  MAXPNAMELEN ; // product name
   UINT  wXmin;                 // minimum x position
   UINT  wXmax;                 // maximum x position
   UINT  wYmin;                 // minimum y position
   UINT  wYmax;                 // maximum y position
   UINT  wZmin;                 // minimum z position
   UINT  wZmax;                 // maximum z position
   UINT  wNumButtons;           // number of buttons
   UINT  wPeriodMin;            // smallest supported polling interval when captured
   UINT  wPeriodMax;            // largest supported polling interval when captured
}
JOYCAPS, *LPJOYCAPS;

MMRESULT joyGetDevCaps(UINT IDDevice, LPJOYCAPS lpjc, UINT cbjc);

UINT joyGetNumDevs(VOID);


操作杆API的函数(Functions of the Joystick API)

在Windows平台下是通过以joy作为函数名开始的函数以及被各种函数调用的结构体来实现操作杆API的。例如,joyGetNumDevs()返回的是这个平台下支持的操作杆设备最多的数目;joyGetDevCaps()返回的是每个连接上的操纵杆的质量。

joyGetDevCaps()函数需要3个参数:
* 处在0到joyGetNumDevs()-1之间的操作杆ID
* 保存返回的质量信息的JOYCAPS结构体的地址
* JOYCAPS结构体的字节大小
虽然它的结果不同,这个函数返回的是一个32位的无符号整型结果,而且0表示一个已经连接的操纵杆。

JOYCAPS结构体有3种类型。Windows平台下的WORD(16位无符号短整型)类型对应的是Java语言中16位有符号短整型。除此之外,Windows下的UINT(32位无符号整型)类型是和Java语言中32位有符号整型相对应的。而Windows平台上的text character就是TCHAR类型。

微软通过TCHAR类型使开发人员能够从ASCII类型的函数参数平滑的转移到Unicode字符类型的函数参数上。而且,拥有text类型参数的函数的实现是通过宏转变为对应的ASCII或者wide-character的函数。例如,joyGetDevCaps()是一个对应joyGetDevCapsA() 和 joyGetDevCapsW()的宏。

使用TCHAR(Working with TCHAR)

使用TCHAR和将TCHAR转变的宏会导致基于C语言的申明向基于JNA接口的转换
变得有点复杂—你在使用ASCII或者wide-character版本的操纵杆函数吗?两种版本都在如下的接口中展示了:

Listing 6. WinMM.java

// WinMM.java

import com.sun.jna.*;
import com.sun.jna.win32.*;

public interface WinMM extends StdCallLibrary
{
   final static int JOYCAPSA_SIZE = 72;

   public static class JOYCAPSA extends Structure
   {
      public short wMid;
      public short wPid;
      public byte szPname [] = new byte [32];
      public int wXmin;
      public int wXmax;
      public int wYmin;
      public int wYmax;
      public int wZmin;
      public int wZmax;
      public int wNumButtons;
      public int wPeriodMin;
      public int wPeriodMax;
   }

   int joyGetDevCapsA (int id, JOYCAPSA caps, int size);

   final static int JOYCAPSW_SIZE = 104;

   public static class JOYCAPSW extends Structure
   {
      public short wMid;
      public short wPid;
      public char szPname [] = new char [32];
      public int wXmin;
      public int wXmax;
      public int wYmin;
      public int wYmax;
      public int wZmin;
      public int wZmax;
      public int wNumButtons;
      public int wPeriodMin;
      public int wPeriodMax;
   }

   int joyGetDevCapsW (int id, JOYCAPSW caps, int size);

   int joyGetNumDevs ();
}


Listing 6没有介绍JNA的新特性。实际上,JNA强调了对本地库的接口命名规则。同时,还展示了如何将TCHAR映射到Java语言中的byte和char数组。最后,它揭示了以常量方式声明的结构体的大小。Listing 7展示了当调用joyGetDevCapsA() 和 joyGetDevCapsW()时如何使用这些常量。

Listing 7. JoystickInfo.java

// JoystickInfo.java

import com.sun.jna.*;

public class JoystickInfo
{
   public static void main (String [] args)
   {
      WinMM lib = (WinMM) Native.loadLibrary ("winmm", WinMM.class);
      int numDev = lib.joyGetNumDevs ();

      System.out.println ("joyGetDevCapsA() Demo");
      System.out.println ("---------------------\n");

      WinMM.JOYCAPSA caps1 = new WinMM.JOYCAPSA ();
      for (int i = 0; i < numDev; i++)
           if (lib.joyGetDevCapsA (i, caps1, WinMM.JOYCAPSA_SIZE) == 0)
           {
               String pname = new String (caps1.szPname);
               pname = pname.substring (0, pname.indexOf ('\0'));
               System.out.println ("Device #"+i);
               System.out.println ("  wMid = "+caps1.wMid);
               System.out.println ("  wPid = "+caps1.wPid);
               System.out.println ("  szPname = "+pname);
               System.out.println ("  wXmin = "+caps1.wXmin);
               System.out.println ("  wXmax = "+caps1.wXmax);
               System.out.println ("  wYmin = "+caps1.wYmin);
               System.out.println ("  wYmax = "+caps1.wYmax);
               System.out.println ("  wZmin = "+caps1.wZmin);
               System.out.println ("  wZmax = "+caps1.wZmax);
               System.out.println ("  wNumButtons = "+caps1.wNumButtons);
               System.out.println ("  wPeriodMin = "+caps1.wPeriodMin);
               System.out.println ("  wPeriodMax = "+caps1.wPeriodMax);
               System.out.println ();
           }

      System.out.println ("joyGetDevCapsW() Demo");
      System.out.println ("---------------------\n");

      WinMM.JOYCAPSW caps2 = new WinMM.JOYCAPSW ();
      for (int i = 0; i < numDev; i++)
           if (lib.joyGetDevCapsW (i, caps2, WinMM.JOYCAPSW_SIZE) == 0)
           {
               String pname = new String (caps2.szPname);
               pname = pname.substring (0, pname.indexOf ('\0'));
               System.out.println ("Device #"+i);
               System.out.println ("  wMid = "+caps2.wMid);
               System.out.println ("  wPid = "+caps2.wPid);
               System.out.println ("  szPname = "+pname);
               System.out.println ("  wXmin = "+caps2.wXmin);
               System.out.println ("  wXmax = "+caps2.wXmax);
               System.out.println ("  wYmin = "+caps2.wYmin);
               System.out.println ("  wYmax = "+caps2.wYmax);
               System.out.println ("  wZmin = "+caps2.wZmin);
               System.out.println ("  wZmax = "+caps2.wZmax);
               System.out.println ("  wNumButtons = "+caps2.wNumButtons);
               System.out.println ("  wPeriodMin = "+caps2.wPeriodMin);
               System.out.println ("  wPeriodMax = "+caps2.wPeriodMax);
               System.out.println ();
           }
   }
}

尽管和LocalTime这个示例类似,JoystickInfo执行WinMM lib = (WinMM) Native.loadLibrary ("winmm", WinMM.class);这句话来获取一个WinMM的实例,并且载入winmm.dll。它还执行WinMM.JOYCAPSA caps1 = new WinMM.JOYCAPSA (); 和 WinMM.JOYCAPSW caps2 = new WinMM.JOYCAPSW ();初始化必需的结构体实例。

编译和运行这个程序(Compile and run the application)

假如jna.jar,WinMM.java和JoystickInfo.java在同一个文件夹中,调用 javac -cp jna.jar;. JoystickInfo.java 来编译这个应用的源代码。
在windows平台下,调用java -cp jna.jar;. JoystickInfo就可以运行这个应用程序了。如果没有操纵杆设备,你应该得到Listing 8中的输出。

将C语言中的string类型转换为Java语言的String类型

pname = pname.substring (0, pname.indexOf ('\0')); 这段代码将一个C string 转换成了Java string. 如果不使用这个转换,C语言的string结束符’\0’和string后面的无用字符都会成为Java语言中String实例对象的内容。

Listing 8. 输出操纵杆信息(Output of JoystickInfo)

joyGetDevCapsA() Demo
---------------------

joyGetDevCapsW() Demo
---------------------


上面的输出是因为每次调用joyGetDevCap()返回的是一个非空值,这表示没有操纵杆/游戏控制器设备或者是出现错误。为了获取更多有意思的输出,将一个设备连接到你的平台上并且再次运行JoystickInfo。如下,将一个微软SideWinder即插即用游戏触摸板设备联上之后我获取了如下的输出:

Listing 9. 操纵杆连接上之后的运行结果(Output after running JoystickInfo with a joystick attached)

joyGetDevCapsA() Demo
---------------------

Device #0
  wMid = 1118
  wPid = 39
  szPname = Microsoft PC-joystick driver
  wXmin = 0
  wXmax = 65535
  wYmin = 0
  wYmax = 65535
  wZmin = 0
  wZmax = 65535
  wNumButtons = 6
  wPeriodMin = 10
  wPeriodMax = 1000

joyGetDevCapsW() Demo
---------------------

Device #0
  wMid = 1118
  wPid = 39
  szPname = Microsoft PC-joystick driver
  wXmin = 0
  wXmax = 65535
  wYmin = 0
  wYmax = 65535
  wZmin = 0
  wZmax = 65535
  wNumButtons = 6
  wPeriodMin = 10
  wPeriodMax = 1000


窗口透明度(Transparent windows)

在这系列文章中上篇文章是关于Bernhard Pauler's 气泡提示(balloontip)工程的。我构建了一个叫做VerifyAge的、包含有一个气泡提示的GUI应用。Figure 1中显示了这个GUI应用的一个小问题:这个气泡提示的没有经过修饰的对话框部分遮住了应用窗口的一部分边框,导致了无法点击这个边框的最小化和最大化按钮,并且使整个GUI很难看.
image
尽管未修饰部分的对话框不能显示气泡提示的透明度,java语言不支持窗口透明度。幸运的是,我们可以通过使用com.sun.jna.examples.WindowUtils类调用JNA的examples.jar文件来解决这个问题。
WindowUtils提供在Unix,Linux,Mac OS X和Windows平台上使用JNA’s来实现窗口透明的工具方法。例如, public static void setWindowMask(final Window w, Icon mask) 让你根据像素而不是通过预定的掩罩(mask)参数来选取某部分的窗口。这个功能将在Listing 10中展示:

Listing 10. Using JNA to render a window transparent


// Create a mask for this dialog. This mask has the same shape as the
// dialog's rounded balloon tip and ensures that only the balloon tip
// part of the dialog will be visible. All other dialog pixels will
// disappear because they correspond to transparent mask pixels.

// Note: The drawing code is based on the drawing code in
// RoundedBalloonBorder.

Rectangle bounds = getBounds ();
BufferedImage bi = new BufferedImage (bounds.width, bounds.height,
                                      BufferedImage.TYPE_INT_ARGB);
Graphics g = bi.createGraphics ();
g.fillRoundRect (0, 0, bounds.width, bounds.height-VERT_OFFSET,
                 ARC_WIDTH*2, ARC_HEIGHT*2);
g.drawRoundRect (0, 0, bounds.width-1, bounds.height-VERT_OFFSET-1,
                 ARC_WIDTH*2, ARC_HEIGHT*2);
int [] xPoints = { HORZ_OFFSET, HORZ_OFFSET+VERT_OFFSET, HORZ_OFFSET };
int [] yPoints = { bounds.height-VERT_OFFSET-1, bounds.height-VERT_OFFSET
                   -1, bounds.height-1 };
g.fillPolygon (xPoints, yPoints, 3);
g.drawLine (xPoints [0], yPoints [0], xPoints [2], yPoints [2]);
g.drawLine (xPoints [1], yPoints [1], xPoints [2], yPoints [2]);
g.dispose ();
WindowUtils.setWindowMask (this, new ImageIcon (bi));


在Listing 10中的代码段是从本文代码文档(code archive)里的加强版的VerifyAge2 应用中的TipFrame的构造函数结尾部分摘录的。这个构造函数定义了围绕提示气泡的掩罩(mask)的形状,在这个形状范围里描绘不透明的像素。
假如你当前文件夹中有examples.jar, jna.jar, 和 VerifyAge2.java,调用 javac -cp examples.jar;balloontip.jar VerifyAge2.java 来编译源文件.然后调用java -Dsun.java2d.noddraw=true -cp examples.jar;balloontip.jar;. VerifyAge2运行这个应用. Figure 2 展示了透明示例.
image

总结(In conclusion)

JNA项目有很长的历史了(追溯到1999年),但是它第一次发布是在2006年11月。从此以后它慢慢的被需要将本地C代码整合到Java工程中的开发者注意到了。因为JNA能够用来解决JuRuby中常见一个问题:缺乏对POSIX调用的支持(lack of support for POSIX calls),它也在JRuby程序员中掀起些波浪。JNA也同样被作为实现用低级C代码继承Ruby的一种解决方案(extending Ruby with low-level C code)。
我喜欢使用JNA来工作,相信你也会发现它比使用JNI来访问本地代码更简单、更安全。无需多言,JNA还有更多的特性在本文中没有体现出来。查阅它的资源部分:获取这个开源java项目更多的信息(learn more about this open source Java project)。用它做demo,而且在论坛(discussion forum )上共享你的经验。 下一个月我会带着另一个开源项目回来的,这个开源项目会给你每天的java开发带来益处。

附录:WindowUtils.setWindowMask()的替代品

在刚刚写完这篇文章后,我发现java语言支持在6u10版本中支持窗口的透明和形状定制。读完Kirill Grouchnikov的博客后,我用WindowUtils.setWindowMask()的替代品修改了VerifyAge2,如下:
// Create and install a balloon tip shape to ensure that only this part
// of the dialog will be visible.

Rectangle bounds = getBounds ();
GeneralPath gp;
gp = new GeneralPath (new RoundRectangle2D.Double (bounds.x, bounds.y,
                                                   bounds.width,
                                                   bounds.height-
                                                   VERT_OFFSET,
                                                   ARC_WIDTH*2-1,
                                                   ARC_HEIGHT*2-1));
gp.moveTo (HORZ_OFFSET, bounds.height-VERT_OFFSET);
gp.lineTo (HORZ_OFFSET, bounds.height);
gp.lineTo (HORZ_OFFSET+VERT_OFFSET+1, bounds.height-VERT_OFFSET);
AWTUtilities.setWindowShape (this, gp);


这段代码使用新类AWTUtilities(在com.sun.awt包中),而且public void setWindowShape(Window w, Shape s)函数将TipFrame和JDialog窗口设置气泡形状。
posted @ 2009-05-07 11:08 lanxin1020 阅读(806) | 评论 (0)编辑 收藏
Java代码
  1. 简单介绍及应用如下:    
  2.   一、JAVA中所需要做的工作    
  3.   在JAVA程序中,首先需要在类中声明所调用的库名称,如下:    
  4.   
  5. static {    
  6. System.loadLibrary(“goodluck”);    
  7. }   
  8.   
  9.   
  10.   在这里,库的扩展名字可以不用写出来,究竟是DLL还是SO,由系统自己判断。    
  11.   
  12.   还需对将要调用的方法做本地声明,关键字为native。且只需要声明,而不需要具体实现。如下:    
  13.   
  14. public native static void set(int i);    
  15. public native static int get();   
  16.   
  17.   
  18.   然后编译该JAVA程序文件,生成CLASS,再用JAVAH命令,JNI就会生成C/C++的头文件。    
  19.   
  20.   例如程序testdll.java,内容为:    
  21.   
  22. public class testdll    
  23. {    
  24. static    
  25. {    
  26. System.loadLibrary("goodluck");    
  27. }    
  28. public native static int get();    
  29. public native static void set(int i);    
  30. public static void main(String[] args)    
  31. {    
  32. testdll test = new testdll();    
  33. test.set(10);    
  34. System.out.println(test.get());    
  35. }    
  36. }   
  37.   
  38.   
  39.   用javac testdll.java编译它,会生成testdll.class。    
  40.   再用javah testdll,则会在当前目录下生成testdll.h文件,这个文件需要被C/C++程序调用来生成所需的库文件。    
  41.   
  42.   二、C/C++中所需要做的工作    
  43.   对于已生成的.h头文件,C/C++所需要做的,就是把它的各个方法具体的实现。然后编译连接成库文件即可。再把库文件拷贝到JAVA程序的路径下面,就可以用JAVA调用C/C++所实现的功能了。    
  44.   接上例子。我们先看一下testdll.h文件的内容:    
  45.   
  46. /* DO NOT EDIT THIS FILE - it is machine generated */    
  47. #include    
  48. /* Header for class testdll */    
  49. #ifndef _Included_testdll    
  50. #define _Included_testdll    
  51. #ifdef __cplusplus    
  52. extern "C" {    
  53. #endif    
  54. /*   
  55. * Class: testdll   
  56. * Method: get   
  57. * Signature: ()I   
  58. */    
  59. JNIEXPORT jint JNICALL Java_testdll_get (JNIEnv *, jclass);    
  60. /*   
  61. * Class: testdll   
  62. * Method: set   
  63. * Signature: (I)V   
  64. */    
  65. JNIEXPORT void JNICALL Java_testdll_set (JNIEnv *, jclass, jint);    
  66. #ifdef __cplusplus    
  67. }    
  68. #endif    
  69. #endif   
  70.   
  71.   
  72.   在具体实现的时候,我们只关心两个函数原型    
  73.   JNIEXPORT jint JNICALL Java_testdll_get (JNIEnv *, jclass); 和    
  74.   JNIEXPORT void JNICALL Java_testdll_set (JNIEnv *, jclass, jint);   
  75.   
  76.   这里JNIEXPORT和JNICALL都是JNI的关键字,表示此函数是要被JNI调用的。而jint是以JNI为中介使JAVA的int类型与本 地的int沟通的一种类型,我们可以视而不见,就当做int使用。函数的名称是JAVA_再加上java程序的package路径再加函数名组成的。参数 中,我们也只需要关心在JAVA程序中存在的参数,至于JNIEnv*和jclass我们一般没有必要去碰它。    
  77.   
  78.   好,下面我们用testdll.cpp文件具体实现这两个函数:    
  79.   
  80. #include "testdll.h"    
  81. int i = 0;    
  82. JNIEXPORT jint JNICALL Java_testdll_get (JNIEnv *, jclass)    
  83. {    
  84. return i;    
  85. }    
  86. JNIEXPORT void JNICALL Java_testdll_set (JNIEnv *, jclass, jint j)    
  87. {    
  88. i = j;    
  89. }   
  90.   
  91.   
  92.   编译连接成库文件,本例是在WINDOWS下做的,生成的是DLL文件。并且名称要与JAVA中需要调用的一致,这里就是goodluck.dll 。把goodluck.dll拷贝到testdll.class的目录下,java testdll运行它,就可以观察到结果了。    
  93.   我的项目比较复杂,需要调用动态链接库,这样在JNI传送参数到C程序时,需要对参数进行处理转换。才可以被C程序识别。   
  94.   大体程序如下:   
  95.   
  96. public class SendSMS {    
  97. static    
  98. {    
  99.   
  100.   
  101.     
  102. System.out.println(System.getProperty("java.library.path"));    
  103. System.loadLibrary("sms");    
  104. }    
  105. public native static int SmsInit();    
  106. public native static int SmsSend(byte[] mobileNo, byte[] smContent);    
  107. }    
  108.   
  109.   在这里要注意的是,path里一定要包含类库的路径,否则在程序运行时会抛出异常:   
  110.   java.lang.UnsatisfiedLinkError: no sms in java.library.path   
  111.   at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1491)   
  112.   at java.lang.Runtime.loadLibrary0(Runtime.java:788)   
  113.   at java.lang.System.loadLibrary(System.java:834)   
  114.   at com.mobilesoft.sms.mobilesoftinfo.SendSMS.(SendSMS.java:14)   
  115.   at com.mobilesoft.sms.mobilesoftinfo.test.main(test.java:18)   
  116.   Exception in thread "main"  
  117.   
  118.   指引的路径应该到.dll文件的上一级,如果指到.dll,则会报:   
  119.   java.lang.UnsatisfiedLinkError: C:\sms.dll: Can't find dependent libraries   
  120.   at java.lang.ClassLoader$NativeLibrary.load(Native Method)   
  121.   at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1560)   
  122.   at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1485)   
  123.   at java.lang.Runtime.loadLibrary0(Runtime.java:788)   
  124.   at java.lang.System.loadLibrary(System.java:834)   
  125.   at com.mobilesoft.sms.mobilesoftinfo.test.main(test.java:18)   
  126.   Exception in thread "main"  
  127.   
  128.   通过编译,生成com_mobilesoft_sms_mobilesoftinfo_SendSMS.h头文件。(建议使用Jbuilder进行编 译,操作比较简单!)这个头文件就是Java和C之间的纽带。要特别注意的是方法中传递的参数jbyteArray,这在接下来的过程中会重点介绍。   
  129.   
  130. /* DO NOT EDIT THIS FILE - it is machine generated */    
  131. #include    
  132. /* Header for class com_mobilesoft_sms_mobilesoftinfo_SendSMS */    
  133. #ifndef _Included_com_mobilesoft_sms_mobilesoftinfo_SendSMS    
  134. #define _Included_com_mobilesoft_sms_mobilesoftinfo_SendSMS    
  135. #ifdef __cplusplus    
  136. extern "C" {    
  137. #endif    
  138. /*   
  139. * Class: com_mobilesoft_sms_mobilesoftinfo_SendSMS   
  140. * Method: SmsInit   
  141. * Signature: ()I   
  142. */    
  143. JNIEXPORT jint JNICALL Java_com_mobilesoft_sms_mobilesoftinfo_SendSMS_SmsInit    
  144. (JNIEnv *, jclass);    
  145. /*   
  146. * Class: com_mobilesoft_sms_mobilesoftinfo_SendSMS   
  147. * Method: SmsSend   
  148. * Signature: ([B[B)I   
  149. */    
  150. JNIEXPORT jint JNICALL Java_com_mobilesoft_sms_mobilesoftinfo_SendSMS_SmsSend    
  151. (JNIEnv *, jclass, jbyteArray, jbyteArray);    
  152. #ifdef __cplusplus    
  153. }    
  154. #endif    
  155. #endif   
  156.   
  157.   
  158.   
  159.   对于我要调用的C程序的动态链接库,C程序也要提供一个头文件,sms.h。这个文件将要调用的方法罗列了出来。   
  160.   
  161. /*   
  162. * SMS API   
  163. * Author: yippit   
  164. * Date: 2004.6.8   
  165. */    
  166. #ifndef MCS_SMS_H    
  167. #define MCS_SMS_H    
  168. #define DLLEXPORT __declspec(dllexport)    
  169. /*sms storage*/    
  170. #define SMS_SIM 0    
  171. #define SMS_MT 1    
  172. /*sms states*/    
  173. #define SMS_UNREAD 0    
  174. #define SMS_READ 1    
  175. /*sms type*/    
  176. #define SMS_NOPARSE -1    
  177. #define SMS_NORMAL 0    
  178. #define SMS_FLASH 1    
  179. #define SMS_MMSNOTI 2    
  180. typedef struct tagSmsEntry {    
  181. int index; /*index, start from 1*/    
  182. int status; /*read, unread*/    
  183. int type; /*-1-can't parser 0-normal, 1-flash, 2-mms*/    
  184. int storage; /*SMS_SIM, SMS_MT*/    
  185. char date[24];    
  186. char number[32];    
  187. char text[144];    
  188. } SmsEntry;    
  189. DLLEXPORT int SmsInit(void);    
  190. DLLEXPORT int SmsSend(char *phonenum, char *content);    
  191. DLLEXPORT int SmsSetSCA(char *sca);    
  192. DLLEXPORT int SmsGetSCA(char *sca);    
  193. DLLEXPORT int SmsSetInd(int ind);    
  194. DLLEXPORT int SmsGetInd(void);    
  195. DLLEXPORT int SmsGetInfo(int storage, int *max, int *used);    
  196. DLLEXPORT int SmsSaveFlash(int flag);    
  197. DLLEXPORT int SmsRead(SmsEntry *entry, int storage, int index);    
  198. DLLEXPORT int SmsDelete(int storage, int index);    
  199. DLLEXPORT int SmsModifyStatus(int storage, int index); /*unread -> read*/    
  200. #endif   
  201.   
  202.   
  203.   在有了这两个头文件之后,就可以进行C程序的编写了。也就是实现对JNI调用的两个方法。在网上的资料中,由于调用的方法实现的都比较简单,(大多是打印字符串等)所以避开了JNI中最麻烦的部分,也是最关键的部分,参数的传递。     
  204. 由于Java和C的编码是不同的,所以传递的参数是要进行再处理,否则C程序是会对参数在编译过程中提出警告,例如;warning C4024: 'SmsSend' : different types for formal and actual parameter 2等。   
  205.   Sms.c的程序如下:   
  206.   
  207. #include "sms.h"    
  208. #include "com_mobilesoft_sms_mobilesoftinfo_SendSMS.h"    
  209. JNIEXPORT jint JNICALL Java_com_mobilesoft_sms_mobilesoftinfo_SendSMS_SmsInit(JNIEnv * env, jclass jobject)    
  210. {    
  211. return SmsInit();    
  212. }    
  213.   
  214. JNIEXPORT jint JNICALL Java_com_mobilesoft_sms_mobilesoftinfo_SendSMS_SmsSend(JNIEnv * env, jclass jobject, jbyteArray mobileno, jbyteArray smscontent)    
  215. {    
  216. char * pSmscontent ;    
  217. //jsize theArrayLengthJ = (*env)->GetArrayLength(env,mobileno);    
  218. jbyte * arrayBody = (*env)->GetByteArrayElements(env,mobileno,0);    
  219. char * pMobileNo = (char *)arrayBody;    
  220. printf("[%s]\n ", pMobileNo);    
  221. //jsize size = (*env)->GetArrayLength(env,smscontent);    
  222. arrayBody = (*env)->GetByteArrayElements(env,smscontent,0);    
  223. pSmscontent = (char *)arrayBody;    
  224. printf("<%s>\n", pSmscontent);    
  225. return SmsSend(pMobileNo,pSmscontent);    
  226. }   
  227.   
  228.   
  229.   
  230.   对于C或C++,在程序上是会有稍微的不同,这可以由读者对其进行适当的修改。这里要注意的是GetArrayLength,GetByteArrayElements等这些JNI中已经包含的方法,这些方法是专门对转换参数类型而提供的。具体的方法有很多,在下一篇中会做专门的介绍。   
  231.   在完成了上述的文件后,可以对sms.c进行编译,生成.dll文件(建议在release中编译,这样动态链接库的容积会比较小!)   
  232.   完成.dll文件的编译后,就可以在Java中调用C程序中的方法了。例如文件test.java   
  233.   
  234. public class test {    
  235. public test() {    
  236. }    
  237. public static void main(String[] args) {    
  238. byte[] mobileno = {    
  239. 0x310x330x360x360x310x360x330x300x360x360x370x00};    
  240. String smscontentemp = "早上好";    
  241. byte[] temp = {0};    
  242. try {    
  243. byte[] smscontentdb = smscontentemp.getBytes("gbk");    
  244. byte[] smscontent = new byte[smscontentdb.length + temp.length];    
  245. System.arraycopy(smscontentdb, 0, smscontent, 0, smscontentdb.length);    
  246. System.arraycopy(temp, 0, smscontent, smscontentdb.length, temp.length);    
  247. SendSMS sendSMS = new SendSMS();    
  248. sendSMS.SmsInit();    
  249. if (sendSMS.SmsSend(mobileno, smscontent) >= 0) {    
  250. System.out.println("chenggong !");    
  251. }    
  252. else {    
  253. System.out.println("shibai !");    
  254. }    
  255. }catch (Exception ex) {}    
  256. }    
  257. }   
  258.   
  259.   
  260.   
  261.   在这个文件中要注意的有一点,就是在传递字节数组到C程序中时,最后的结尾一定要以0结束。这是一个偷懒的做法,不过是个有效的做法。因为大多数情况 下,接口是由第三方提供的。所以我们一般是不知道在C的方法里,具体是怎么处理参数的。而C又是要求数组是有长度。所以,在Java中,如果你不想写程序 传数组的长度,那么在数组中以0结尾就是最方便的方法了。当然,如果有更好的方法也希望大家提出。   
  262.   
  263.   到这里,一个完整的Java通过JNI调用动态链接库的程序就完成了。实际上也不是很复杂。只要多注意一下细节,是很容易得出来的。  
posted @ 2009-05-07 11:04 lanxin1020 阅读(161) | 评论 (0)编辑 收藏
Properties 基本知识
如果不熟悉 java.util.Properties 类,那么现在告诉您它是用来在一个文件中存储键-值对的,其中键和值是用等号分隔的,如清单 1 所示。

清单 1. 一组属性示例
foo=bar
fu=baz

将清单 1 装载到 Properties 对象中后,您就可以找到两个键( foo 和 fu )和两个值( foo 的 bar 和 fu 的 baz )了。这个类支持带 \u 的嵌入 Unicode 字符串,但是这里重要的是每一项内容都当作 String 。

清单 2 显示了如何装载属性文件并列出它当前的一组键和值。只需传递这个文件的 InputStream 给 load() 方法,就会将每一个键-值对添加到 Properties 实例中。然后用 list() 列出所有属性或者用 getProperty() 获取单独的属性。
清单 2. 装载属性
Java代码
  1. import java.util.*;   
  2. import java.io.*;   
  3.   
  4. public class LoadSample {   
  5.     public static void main(String args[]) throws Exception {   
  6.       Properties prop = new Properties();   
  7.       FileInputStream fis =    
  8.         new FileInputStream("sample.properties");   
  9.       prop.load(fis);   
  10.       prop.list(System.out);   
  11.       System.out.println("\nThe foo property: " +   
  12.           prop.getProperty("foo"));   
  13.     }   
  14. }  

运行 LoadSample 程序生成如清单 3 所示的输出。注意 list() 方法的输出中键-值对的顺序与它们在输入文件中的顺序不一样。 Properties 类在一个散列表(hashtable,事实上是一个 Hashtable 子类)中储存一组键-值对,所以不能保证顺序。

清单 3. LoadSample 的输出

-- listing properties --
fu=baz
foo=bar

The foo property: bar

XML 属性文件
这里没有什么新内容。 Properties 类总是这样工作的。不过,新的地方是从一个 XML 文件中装载一组属性。它的 DTD 如清单 4 所示。

清单 4. 属性 DTD

dtd 写道
Java代码
  1. <?xml version="1.0" encoding="UTF-8"?>    
  2. <!-- DTD for properties -->    
  3. <!ELEMENT properties ( comment?, entry* ) >    
  4. <!ATTLIST properties version CDATA #FIXED "1.0">    
  5. <!ELEMENT comment (#PCDATA) >    
  6. <!ELEMENT entry (#PCDATA) >    
  7. <!ATTLIST entry key CDATA #REQUIRED>  



如果不想细读 XML DTD,那么可以告诉您它其实就是说在外围 <properties> 标签中包装的是一个 <comment> 标签,后面是任意数量的 <entry> 标签。对每一个 <entry> 标签,有一个键属性,输入的内容就是它的值。清单 5 显示了 清单 1中的属性文件的 XML 版本是什么样子的。

清单 5. XML 版本的属性文件
Java代码
  1. <?xml version="1.0" encoding="UTF-8"?>   
  2. <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">   
  3. <properties>   
  4. <comment>Hi</comment>   
  5. <entry key="foo">bar</entry>   
  6. <entry key="fu">baz</entry>   
  7. </properties>  

如果清单 6 所示,读取 XML 版本的 Properties 文件与读取老格式的文件没什么不同。
清单 6. 读取 XML Properties 文件
Java代码
  1. import java.util.*;   
  2. import java.io.*;   
  3.   
  4. public class LoadSampleXML {   
  5.     public static void main(String args[]) throws Exception {   
  6.       Properties prop = new Properties();   
  7.       FileInputStream fis =   
  8.         new FileInputStream("sampleprops.xml");   
  9.       prop.loadFromXML(fis);   
  10.       prop.list(System.out);   
  11.       System.out.println("\nThe foo property: " +   
  12.           prop.getProperty("foo"));   
  13.     }   
  14. }  

关于资源绑定的说明
虽然 java.util.Properties 类现在除了支持键-值对,还支持属性文件作为 XML 文件,不幸的是,没有内置的选项可以将 ResourceBundle 作为一个 XML 文件处理。是的, PropertyResourceBundle 不使用 Properties 对象来装载绑定,不过装载方法的使用是硬编码到类中的,而不使用较新的 loadFromXML() 方法。

运行清单 6 中的程序产生与原来的程序相同的输出,如 清单 2所示。

保存 XML 属性
新的 Properties 还有一个功能是将属性存储到 XML 格式的文件中。虽然 store() 方法仍然会创建一个类似 清单 1 所示的文件,但是现在可以用新的 storeToXML() 方法创建如 清单 5 所示的文件。只要传递一个 OutputStream 和一个用于注释的 String 就可以了。清单 7 展示了新的 storeToXML() 方法。

清单 7. 将 Properties 存储为 XML 文件
Java代码
  1. import java.util.*;   
  2. import java.io.*;   
  3.   
  4. public class StoreXML {   
  5.     public static void main(String args[]) throws Exception {   
  6.       Properties prop = new Properties();   
  7.       prop.setProperty("one-two""buckle my shoe");   
  8.       prop.setProperty("three-four""shut the door");   
  9.       prop.setProperty("five-six""pick up sticks");   
  10.       prop.setProperty("seven-eight""lay them straight");   
  11.       prop.setProperty("nine-ten""a big, fat hen");   
  12.       FileOutputStream fos =   
  13.         new FileOutputStream("rhyme.xml");   
  14.       prop.storeToXML(fos, "Rhyme");   
  15.       fos.close();   
  16.     }   
  17. }  

运行清单 7 中的程序产生的输出如清单 8 所示。

清单 8. 存储的 XML 文件
Java代码
  1. <?xml version="1.0" encoding="UTF-8"?>   
  2. <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">   
  3. <properties>   
  4. <comment>Rhyme</comment>   
  5. <entry key="seven-eight">lay them straight</entry>   
  6. <entry key="five-six">pick up sticks</entry>   
  7. <entry key="nine-ten">a big, fat hen</entry>   
  8. <entry key="three-four">shut the door</entry>   
  9. <entry key="one-two">buckle my shoe</entry>   
  10. </properties>  

结束语
使用 XML 文件还是使用老式的 a=b 类型的文件完全取决于您自己。老式文件从内存的角度看肯定是轻量级的。不过,由于 XML 的普遍使用,人们会期望 XML 格式流行起来,因为它已经被广泛使用了,只不过没有用到 Properties 对象。选择完全在您。分析软件包 private XMLUtils 类的源代码以获得关于所使用的 XML 解析的更多信息。
Java代码
  1. import java.io.FileInputStream;   
  2. import java.io.IOException;   
  3. import java.io.InputStream;   
  4. import java.util.Properties;   
  5. /**  
  6. * 实现properties文件的读取  
  7. * @author bbflyerwww  
  8. * @date 2006-08-02  
  9. */  
  10. public class PTest {   
  11.       public static void main(String[] args) {   
  12.           try {   
  13.               long start = System.currentTimeMillis();   
  14.               InputStream is = new FileInputStream("conf.properties");   
  15.               Properties p = new Properties();   
  16.               p.load(is);   
  17.               is.close();   
  18.               System.out.println("SIZE : " + p.size());   
  19.               System.out.println("homepage : " + p.getProperty("homepage"));   
  20.               System.out.println("author : " + p.getProperty("author"));   
  21.               System.out.println("school : " + p.getProperty("school"));   
  22.               System.out.println("date : " + p.getProperty("date"));   
  23.               long end = System.currentTimeMillis();    
  24.               System.out.println("Cost : " + (end - start));   
  25.           } catch (IOException ioe) {   
  26.               ioe.printStackTrace();   
  27.           }   
  28.       }   
  29. }  

conf.properties
Java代码
  1. # Configuration fileauthor = bbflyerwww   
  2. school = WuHan University   
  3. date = 2006-08-02  


Result

SIZE:4
author : bbflyerwww
school : WuHan University
date : 2006-08-02
Cost : 0
posted @ 2009-05-06 22:52 lanxin1020 阅读(457) | 评论 (0)编辑 收藏

一、LOG4J组成

    LOG4J主要由三大组件组成:
    . Logger: 决定什么日志信息应该被输出、什么日志信息应该被忽略;
    . Appender: 指定日志信息应该输出到什么地方, 这些地方可以是控制台、文件、网络设备;
    . Layout: 指定日志信息的输出格式;

    一个Logger可以有多个Appender,也就是说日志信息可以同时输出到多个设备上,每个Appender对应
    一种Layout(示例见下图)。

              ↗  Appender1  →  Layout
     /    
    Logger
     ﹨  
              ↘  Appender2  →  Layout


二、Logger组件

    1. Logger组件提供的方法:

       Logger组件是LOG4J的核心组件,它代表了Log4J的日志记录器,它能够对日志信息进行分类筛选。它由org.apache.log4j.Logger类实现,提供了如下方法:

 

java 代码
  1. package org.apache.log4j;   
  2.   
  3. public class Logger {   
  4.   
  5.            // Creation & retrieval methods:   
  6.            public static Logger getRootLogger();   
  7.            public static Logger getLogger(String name);   
  8.   
  9.            // printing methods:   
  10.            public void debug(Object message);   
  11.            public void info(Object message);   
  12.            public void warn(Object message);   
  13.            public void error(Object message);   
  14.            public void fatal(Object message);   
  15.       
  16.            // generic printing method:   
  17.            public void log(Priority p, Object message);   
  18. }   

    2. 在配置文件中配置Logger组件

       可在Log4J配置文件中配置自己的Logger组件,示例:

       log4j.logger.myLogger=WARN

       以上代码定义了一个Logger组件,名称为myLogger,日志级别为WARN。
 
    3. 日志级别种类:

       一共有五种,级别由高到低依次是:fatal、error、warn、info、debug。获得Logger实例后,我们可调用以下方法之一输出日志信息:

       public void debug(Object message);          //输出debug级别的日志信息;
       public void info(Object message);           //输出info级别的日志信息;
       public void warn(Object message);           //输出warn级别的日志信息;
       public void error(Object message);          //输出error级别的日志信息;
       public void fatal(Object message);          //输出fatal级别的日志信息;
       public void log(Priority p, Object message);//输出参数Priority指定级别的日志信息;

       以上方法只有当它的级别大于或等于Logger组件配置的日志级别时才调用。以前面我们配置的myLogger为例,它的日志级别为WARN, 那么在程序中,它的warn()、error()、fatal()方法会被执行。对于log()方法,只有当它的参数Priority指定的日志级别大于或等于WARN时,它才会被执行。

    4. 为什么需要对日志进行分级?
   
       在写程序的时候,为了调试程序,我们会在很多出错的地方输出大量的日志信息。当程序调试完,不需要这些信息时,将程序中这些输出日志信息代码删除吗?这样费时费力,对于大型程序几乎不可行。通过对日志分级,假如不想输出WARN级别的日志信息,则Logger组件的级别调高即可,省时省心。

    5. Logger组件的继承性

       Log4J提供了一个root Logger,它是所有Logger组件的“祖先”,它永远存在,且不能通过名字检索或引用,通过Logger.getRootLogger()方法取得它。配置root Logger代码:

       log4j.rootLogger=INFO,console

       可在配置文件中方便地配置存在继承关系的Logger组件,凡是在符号“.”后面的组件都会成为在符号“.”前面的Logger组件的子类。例如:

       log4j.apache.myLogger=WARN
       log4j.apache.myLogger.mySonLogger=,file

       以上代码中, mySonLogger是myLogger的子类Logger组件。Logger组件的继承关系:
       . 如果子类Logger组件没有定义日志级别,则将继承父类的日志级别;
       . 如果子类Logger组件定义了日志级别,就不会继承父类的日志级别;
       . 黙认情况下,子类Logger组件会继承父类所有的Appender,把它们加入到自己的Appener;
       . 如果把子类Logger组件的additivity标志设为false,那么它就不会继承父类Appender。additivity标志 默认值为false;

       以上配置的三个Logger继承关系示例如图:
    
       root Logger: 日志级别=INFO  appender清单=console
                            ↑
       myLogger: 日志级别=WARN appender清单=null
                            ↑
       mySonLogger: 日志级别=null appender清单=file

       这三个Logger组件实际日志级别和Appender如下表:

       Logger组件          日志级别          Appender清单
       root Logger         INFO              console
       myLogger            WARN              console(继承)
       mySonLogger         WARN(继承)        file,console(继承)
      
三、Appender组件

    Appender组件决定将日志信息输出到什么地方。支持以下目的地:
    . 控制台(Console);
    . 文件(File);
    . GUI组件(GUI component);
    . 套接口服务器(Remote socket server);
    . NT的事件记录器(NT Event Logger);
    . UNIX Syslog守护进程(Remote UNIX Syslog daemon);

    一个Logger可同时对应多个Appender,示例:myLogger配置二个Appender: 一个file, 一个是console:

    log4j.logger.myAppender=WARN,file,console

    log4j.appender.file=org.apache.log4j.RollingFileAppender
    log4j.appender.file.File=log.txt

    log4j.apender.console=org.apache.log4j.ConsoleAppender

四、Layout组件

    Layout组件决定日志输出格式,有以下几种类型:
    . org.apache.log4j.HTMLLayout(以HTML表格形式布局);
    . org.apache.log4j.PatternLayout(可以灵活地指定布局模式);
    . org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串);
    . org.apache.log4j.TTCCLayout(包含日志产生的时间、线程和类别等信息);
   
    为名称为console的Appender配置SimpleLayout,代码如下:

    log4j.appender.console.layout=org.apache.log4j.SimpleLayout

    输出日志格式如下:

    WARN - This is a log message from the myLogger
   
    为名称为file的Appender配置PatternLayout,代码如下:

    log4j.appender.file.layout=org.apache.log4j.PatternLayout
    log4j.appender.file.layout.ConversionPattern=%t %p - %m%n

    输出日志格式如下:

    THREAD-1 WARN - This is a log message from the myLogger

    PatternLayout让开发者依照ConversionPattern定义输出格式。ConversionPattern中一些指定日志内容和格式的预定义符号说明如下:

    符号         描述
    %r           自程序开始后消耗的毫秒数
    %t           表示日志记录请求生成的线程
    %p           表示日专语句的优先级
    %r           与日志请求相关的类别名称
    %c           日志信息所在的类名
    %m%n         表示日志信息的内容

五、Log4J的基本用法

    1. 定义配置文件
       Log4J支持二种配置文件格式:XML和Java属性文件(采用“键=值”形式)。以下为Java属性文件
       格式配置文件:
     
       . 配置Logger组件
        
         配置root Logger语法为:log4j.rootLogger=[priority],appenderName,appenderName,...
         配置自定义Logger组件语法为:log4j.logger.loggerName=[priority],appenderName,appenderName,...

         其中:priority为日志级别,可选值包括FATAL、ERROR、WARN、INFO、DEBUG、ALL;
               appenderName指定Appender组件,可指定多个;        

       . 配置Appender组件

         配置日志信息输出目的地Appender, 语法为:
         log4j.appender.appenderName=fully.ualified.name.of.appender.class
         log4j.appender.appenderName.option1=value1
         ...
         log4j.appender.appenderName.optionN=valueN

         Log4J提供的Appender有以下几种:

         a. org.apache.log4j.ConsoleAppender(控制台);
         b. org.apache.log4j.FileAppender(文件);
         c. org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件);
         d. org.apache.log4j.RollingFileAppender(文件大小到指定尺寸产生一个新的文件);
         e. org.apache.log4j.WriteAppender(将日志信息以流格式发送到任意指定地方);

       . 配置Layout组件

         配置Layout组件语法为:
         log4j.appender.appenderName.layout=fully.ualified.name.of.appender.class
         log4j.appender.appenderName.layout.option1=value1
         ...
         log4j.appender.appenderName.layout.optionN=valueN

         下面为一配置文件示例,文件名为log4j.properties:

         ## LOGGERS ##

         #configure root logger
         log4j.rootLogger=INFO,console
         #define a logger named myLogger
         log4j.logger.myLogger=WARN
         #define a second logger that is a child to myLogger
         log4j.logger.myLogger.mySonLogger=,file

         ## APPENDERS ##

         #define an appender named console, which is set to be a ConsoleAppender
         log4j.appender.console=org.apache.log4j.ConsoleAppender

         # define an appender named file, which is set to be a RollingFileAppender
         log4j.appender.file=org.apache.log4j.FileAppender
         log4j.appender.file.File=log.txt

         ## LAYOUTS ##
         # assian a SimpleLayout to console appender
         log4j.appender.console.layout=org.apache.log4j.SimpleLayout

         # assian a PatternLayout to file appender
         log4j.appender.file.layout=org.apache.log4j.PatternLayout
         log4j.appender.file.layout.ConversionPattern=%t%p-%m%n
        
    2. 程序中使用Log4j

       . 获得日志记录器:

         获得rootLogger:Logger rootLogger=Logger.getRootLogger();
         获得自定义Logger:Logger myLogger = Logger.getLogger("log4j.logger.myLogger");

       . 读取日志记录器,配置Log4J环境;

         a. BasicConfigurator.configure(): 自动快速地使用默认Log4J环境;
         b. Property.configurator.configure(String configFilename): 读取使用Java属性格式的配置文件并配置Log4J环境;
         c. DOMConfigurator.configure(String filename): 读取XML形式的配置文件并配置LOG4J环境;

       . 输出日志信息;

         在程序代码中需要生成日志的地方,调用Logger的各种输出日志方法输出不同级别的日志,例如:
        
         myLogger.debug("Thie is a log message from the " + myLogger.getName());

         下面为一使用Log4J的程序,程序名为Test.java:

java 代码
  1.  import org.apache.log4j.Logger;   
  2.  import org.apache.log4j.PropertyConfigurator;   
  3.     
  4.  public class Test {   
  5.   
  6.    public static void main(String[] args) {   
  7.      //Get an instance of the myLogger   
  8.      Logger myLogger = Logger.getLogger("myLogger");   
  9.       
  10.      //Get an instance of the childLogger   
  11.      Logger mySonLogger = Logger.getLogger("myLogger.mySonLogger");   
  12.      //Load the proerties using the PropertyConfigurator   
  13.      PropertyConfigurator.configure("log4j.properties");   
  14.   
  15.      //Log Messages using the Parent Logger   
  16.      myLogger.debug("Thie is a log message from the " + myLogger.getName());   
  17.      myLogger.info("Thie is a log message from the " + myLogger.getName());   
  18.      myLogger.warn("Thie is a log message from the " +  myLogger.getName());   
  19.      myLogger.error("Thie is a log message from the " + myLogger.getName());   
  20.      myLogger.fatal("Thie is a log message from the " + myLogger.getName());   
  21.   
  22.      mySonLogger.debug("Thie is a log message from the " + mySonLogger.getName());   
  23.      mySonLogger.info("Thie is a log message from the " + mySonLogger.getName());   
  24.      mySonLogger.warn("Thie is a log message from the " +  mySonLogger.getName());   
  25.      mySonLogger.error("Thie is a log message from the " + mySonLogger.getName());   
  26.      mySonLogger.fatal("Thie is a log message from the " + mySonLogger.getName());   
  27.    }   
  28. }   

        程序运行结果为:

        WARN - Thie is a log message from the myLogger
        ERROR - Thie is a log message from the myLogger
        FATAL - Thie is a log message from the myLogger
        WARN - Thie is a log message from the myLogger.mySonLogger
        ERROR - Thie is a log message from the myLogger.mySonLogger
        FATAL - Thie is a log message from the myLogger.mySonLogger

        另在Test.class所在的目录下看到一个log.txt文件,内容如下:

        WARN - Thie is a log message from the myLogger.mySonLogger
        ERROR - Thie is a log message from the myLogger.mySonLogger
        FATAL - Thie is a log message from the myLogger.mySonLogger

        如将配置文件log4j.properties中语句

 log4j.logger.myLogger.mySonLogger=,file

 改为

 log4j.logger.myLogger.mySonLogger=,file,console

 再次运行程序,结果如下:

        WARN - Thie is a log message from the myLogger
        ERROR - Thie is a log message from the myLogger
        FATAL - Thie is a log message from the myLogger
        WARN - Thie is a log message from the myLogger.mySonLogger
        WARN - Thie is a log message from the myLogger.mySonLogger
        ERROR - Thie is a log message from the myLogger.mySonLogger
        ERROR - Thie is a log message from the myLogger.mySonLogger
        FATAL - Thie is a log message from the myLogger.mySonLogger         
        FATAL - Thie is a log message from the myLogger.mySonLogger

        mySonLogger的日志在控制台上输出了二次,这是因为mySonLogger继承了父类console Appender,
        本身又定义了一个console Appender, 因而有二个console Appender。

六、在web应用中使用Log4J

    创建一个Servlet,在它初始化方法中读取Log4J配置文件并配置Log4J环境,这个Servlet在Web应用启
    动时候被加载和初始化,然后就可在其它Web组件中获取Logger对象并输出日志。

    1. 创建用于配置Log4J环境的Servlet

java 代码
  1. import javax.servlet.*;   
  2. import javax.servlet.http.*;   
  3. import java.io.*;   
  4. import java.util.*;   
  5.   
  6. import org.apache.log4j.PropertyConfigurator;   
  7.   
  8. public class Log4JServlet extends HttpServlet {   
  9.       public void init() throws ServletException {   
  10.            String path = getServletContext().getRealPath("/");   
  11.            //getInitParameter("propfile")方法从web.xml文件中读取Log4J配置文件的名字"profile"。   
  12.            String propfile = path + getInitParameter("propfile");   
  13.            PropertyConfigurator.configure(propfile);   
  14.       }   
  15. }   
  16.   

      该Servlet在web.xml中的配置如下:

xml 代码
  1. <servlet>  
  2.   <servlet-name>log4jServlet</servlet-name>  
  3.   <servlet-class>Log4JServlet</servlet-class>  
  4.   <init-param>  
  5.     <param-name>propfile</param-name>  
  6.     <param-value>/WEB-INF/log4j.properties</param-value>  
  7.   </init-param>  
  8.   <load-on-startup>1</load-on-startup>  
  9. </servlet>  

2. 在login.jsp中输出日志
       <%@page import="org.apache.log4j.Logger"%>
       <html>
         <head>
           <title>login</title>
         </head>
         <body>
           <%
             Logger myLogger = Logger.getLogger("myLogger");
             Logger mySonLogger = Logger.getLogger("myLogger.mySonLogger");
             myLogger.debug("Thie is a log message from the " + myLogger.getName());
             myLogger.info("Thie is a log message from the " + myLogger.getName());
             myLogger.warn("Thie is a log message from the " +  myLogger.getName());
             myLogger.error("Thie is a log message from the " + myLogger.getName());
             myLogger.fatal("Thie is a log message from the " + myLogger.getName());

             mySonLogger.debug("Thie is a log message from the " + mySonLogger.getName());
             mySonLogger.info("Thie is a log message from the " + mySonLogger.getName());
             mySonLogger.warn("Thie is a log message from the " +  mySonLogger.getName());
             mySonLogger.error("Thie is a log message from the " + mySonLogger.getName());
             mySonLogger.fatal("Thie is a log message from the " + mySonLogger.getName());
           %>
           <br>
             <form name="loginForm" method="post" action="dispatcher">
             username: <input type="text" name="username">
             <br>
             password: <input type="text" name="password">
             <br>
             <input type="submit" name="submit" value="submit">
           </form>
         </body>
       </html>
              
    3. 发布运行使用Log4J的web应用
       1) 将Log4J的JAR文件拷贝至目录:<WEB应用所在目录>/WEB-INF/lib
       2) 创建Log4J的配置文件log4j.properties, 存放目录为:<WEB应用所在目录>/WEB-INF。内容同前面配置文件示例。
       3) 编译Log4JServlet, 存放至目录: <WEB应用所在目录>/WEB-INF/classes
       4) 修改web.xml文件,加入以下内容:

xml 代码
  1. <servlet>  
  2.   <servlet-name>log4jServlet</servlet-name>  
  3.   <servlet-class>Log4JServlet</servlet-class>  
  4.   <init-param>  
  5.     <param-name>profile</param-name>  
  6.     <param-value>/WEB-INF/log4j.properties</param-value>  
  7.   </init-param>  
  8.   <load-on-startup>1</load-on-startup>  
  9. </servlet>  


       5) 启动服务器,访问login.jsp页面,在服务器控制台上看到如下日志:
          WARN - Thie is a log message from the myLogger
          ERROR - Thie is a log message from the myLogger
          FATAL - Thie is a log message from the myLogger
          WARN - Thie is a log message from the myLogger.mySonLogger
          ERROR - Thie is a log message from the myLogger.mySonLogger
          FATAL - Thie is a log message from the myLogger.mySonLogger

          另在<WEB应用所在目录>/WEB-INF目录下看到一个log.txt文件,内容如下:

          WARN - Thie is a log message from the myLogger.mySonLogger
          ERROR - Thie is a log message from the myLogger.mySonLogger
          FATAL - Thie is a log message from the myLogger.mySonLogger

posted @ 2009-05-04 17:19 lanxin1020 阅读(156) | 评论 (0)编辑 收藏

在实际编程时,要使Log4j真正在系统中运行事先还要对配置文件进行定义。定义步骤就是对Logger、Appender及Layout的分别使用。
  Log4j支持两种配置文件格式,一种是XML格式的文件,一种是java properties(key=value)【Java特性文件(键=值)】。下面我们介绍使用Java特性文件做为配置文件的方法
  具体如下:
  
  1、配置根Logger 其语法为:
  log4j.rootLogger = [ level ] , appenderName1, appenderName2, …
  level : 是日志记录的优先级,分为OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者您定义的级别。Log4j建议只使用四个级别,优先级从高到低分别是ERROR、WARN、INFO、DEBUG。通过在这里定义的级别,您可以控制到应用程序中相应级别的日志信息的开关。比如在这里定 义了INFO级别,则应用程序中所有DEBUG级别的日志信息将不被打印出来。
  appenderName:就是指定日志信息输出到哪个地方。您可以同时指定多个输出目的地。
  例如:log4j.rootLogger=info,A1,B2,C3
  
  2、配置日志信息输出目的地 ,其语法为:
  log4j.appender.appenderName = fully.qualified.name.of.appender.class   //
     "fully.qualified.name.of.appender.class" 可以指定下面五个目的地中的一个:
      1.org.apache.log4j.ConsoleAppender(控制台)
      2.org.apache.log4j.FileAppender(文件)
      3.org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
      4.org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
      5.org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
        1.ConsoleAppender选项
              Threshold=WARN:指定日志消息的输出最低层次。
              ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
              Target=System.err:默认情况下是:System.out,指定输出控制台
        2.FileAppender 选项
              Threshold=WARN:指定日志消息的输出最低层次。
              ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
              File=mylog.txt:指定消息输出到mylog.txt文件。
              Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
        3.DailyRollingFileAppender 选项
              Threshold=WARN:指定日志消息的输出最低层次。
              ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
              File=mylog.txt:指定消息输出到mylog.txt文件。
              Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
              DatePattern='.'yyyy-ww:每周滚动一次文件,即每周产生一个新的文件。当然也可以指定按月、周、天、时和分。即对应的格式如下:
              1)'.'yyyy-MM: 每月
              2)'.'yyyy-ww: 每周
              3)'.'yyyy-MM-dd: 每天
              4)'.'yyyy-MM-dd-a: 每天两次
              5)'.'yyyy-MM-dd-HH: 每小时
              6)'.'yyyy-MM-dd-HH-mm: 每分钟
        4.RollingFileAppender 选项
              Threshold=WARN:指定日志消息的输出最低层次。
              ImmediateFlush=true:默认值是true,意谓着所有的消息都会被立即输出。
              File=mylog.txt:指定消息输出到mylog.txt文件。
              Append=false:默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
              MaxFileSize=100KB: 后缀可以是KB, MB 或者是 GB. 在日志文件到达该大小时,将会自动滚动,即将原来的内容移到mylog.log.1文件。
              MaxBackupIndex=2:指定可以产生的滚动文件的最大数。 实际应用:
  log4j.appender.A1=org.apache.log4j.ConsoleAppender //这里指定了日志输出的第一个位置A1是控制台ConsoleAppender
  
  3、配置日志信息的格式 其语法为:
  A. log4j.appender.appenderName.layout = fully.qualified.name.of.layout.class
          "fully.qualified.name.of.layout.class" 可以指定下面4个格式中的一个:
        1.org.apache.log4j.HTMLLayout(以HTML表格形式布局),
       2.org.apache.log4j.PatternLayout(可以灵活地指定布局模式),
       3.org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串),
       4.org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)
            1.HTMLLayout 选项
              LocationInfo=true:默认值是false,输出java文件名称和行号
              Title=my app file: 默认值是 Log4J Log Messages.
            2.PatternLayout 选项
              ConversionPattern=%m%n :指定怎样格式化指定的消息。
            3.XMLLayout   选项
              LocationInfo=true:默认值是false,输出java文件和行号
  实际应用:
    log4j.appender.A1.layout=org.apache.log4j.PatternLayout
    B . log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n
        这里需要说明的就是日志信息格式中几个符号所代表的含义:
         -X号: X信息输出时左对齐;
            %p: 输出日志信息优先级,即DEBUG,INFO,WARN,ERROR,FATAL,
            %d: 输出日志时间点的日期或时间,默认格式为ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
            %r: 输出自应用启动到输出该log信息耗费的毫秒数
            %c: 输出日志信息所属的类目,通常就是所在类的全名
            %t: 输出产生该日志事件的线程名
            %l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
            %x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
            %%: 输出一个"%"字符
            %F: 输出日志消息产生时所在的文件名称
            %L: 输出代码中的行号
            %m: 输出代码中指定的消息,产生的日志具体信息
            %n: 输出一个回车换行符,Windows平台为"\r\n",Unix平台为"\n"输出日志信息换行
        可以在%与模式字符之间加上修饰符来控制其最小宽度、最大宽度、和文本的对齐方式。如:
              1)%20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,默认的情况下右对齐。
              2)%-20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,"-"号指定左对齐。
              3)%.30c:指定输出category的名称,最大的宽度是30,如果category的名称大于30的话,就会将左边多出的字符截掉,但小于30的话也不会有空格。
              4)%20.30c:如果category的名称小于20就补空格,并且右对齐,如果其名称长于30字符,就从左边交远销出的字符截掉。
  这里上面三个步骤是对前面Log4j组件说明的一个简化;下面给出一个具体配置例子,在程序中可以参照执行:
  log4j.rootLogger=INFO,A1,B2
  log4j.appender.A1=org.apache.log4j.ConsoleAppender
  log4j.appender.A1.layout=org.apache.log4j.PatternLayout
  log4j.appender.A1.layout.ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n
  根据上面的日志格式,某一个程序的输出结果如下:
  0  INFO 2003-06-13 13:23:46968 ClientWithLog4j Client socket: Socket[addr=localhost/127.0.0.1,port=8002,localport=2014]
  16  DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server says: 'Java server with log4j, Fri Jun 13 13:23:46 CST 2003'
  16  DEBUG 2003-06-13 13:23:46984 ClientWithLog4j GOOD
  16  DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server responds: 'Command 'HELLO' not understood.'
  16  DEBUG 2003-06-13 13:23:46984 ClientWithLog4j HELP
  16  DEBUG 2003-06-13 13:23:46984 ClientWithLog4j Server responds: 'Vocabulary: HELP QUIT'
  16  DEBUG 2003-06-13 13:23:46984 ClientWithLog4j QUIT

  4. # 当输出信息于回滚文件时
      log4j.appender.ROLLING_FILE=org.apache.log4j.RollingFileAppender   //指定以文件的方式输出日志
        log4j.appender.ROLLING_FILE.Threshold=ERROR
        log4j.appender.ROLLING_FILE.File=rolling.log   //文件位置,也可以用变量${java.home}、rolling.log
        log4j.appender.ROLLING_FILE.Append=true
        log4j.appender.ROLLING_FILE.MaxFileSize=10KB   //文件最大尺寸
        log4j.appender.ROLLING_FILE.MaxBackupIndex=1   //备份数
        log4j.appender.ROLLING_FILE.layout=org.apache.log4j.PatternLayout
        log4j.appender.ROLLING_FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n     
××××××××××××××××××××××××××××××××××××××××××××××××

附:Log4j比较全面的配置
LOG4J的配置之简单使它遍及于越来越多的应用中了:Log4J配置文件实现了输出到控制台、文件、回滚文件、发送日志邮件、输出到数据库日志表、自定义标签等全套功能。择其一二使用就够用了,
log4j.rootLogger=DEBUG,CONSOLE,A1,im
log4j.addivity.org.apache=true
# 应用于控制台
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.Threshold=DEBUG
log4j.appender.CONSOLE.Target=System.out
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
#log4j.appender.CONSOLE.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[THREAD] n%c[CATEGORY]%n%m[MESSAGE]%n%n
#应用于文件
log4j.appender.FILE=org.apache.log4j.FileAppender
log4j.appender.FILE.File=file.log
log4j.appender.FILE.Append=false
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
# Use this layout for LogFactor 5 analysis
# 应用于文件回滚
log4j.appender.ROLLING_FILE=org.apache.log4j.RollingFileAppender
log4j.appender.ROLLING_FILE.Threshold=ERROR
log4j.appender.ROLLING_FILE.File=rolling.log   //文件位置,也可以用变量${java.home}、rolling.log
log4j.appender.ROLLING_FILE.Append=true     //true:添加   false:覆盖
log4j.appender.ROLLING_FILE.MaxFileSize=10KB   //文件最大尺寸
log4j.appender.ROLLING_FILE.MaxBackupIndex=1   //备份数
log4j.appender.ROLLING_FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.ROLLING_FILE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n

#应用于socket
log4j.appender.SOCKET=org.apache.log4j.RollingFileAppender
log4j.appender.SOCKET.RemoteHost=localhost
log4j.appender.SOCKET.Port=5001
log4j.appender.SOCKET.LocationInfo=true
# Set up for Log Facter 5
log4j.appender.SOCKET.layout=org.apache.log4j.PatternLayout
log4j.appender.SOCET.layout.ConversionPattern=[start]%d{DATE}[DATE]%n%p[PRIORITY]%n%x[NDC]%n%t[THREAD]%n%c[CATEGORY]%n%m[MESSAGE]%n%n

# Log Factor 5 Appender
log4j.appender.LF5_APPENDER=org.apache.log4j.lf5.LF5Appender
log4j.appender.LF5_APPENDER.MaxNumberOfRecords=2000
# 发送日志给邮件
log4j.appender.MAIL=org.apache.log4j.net.SMTPAppender
log4j.appender.MAIL.Threshold=FATAL
log4j.appender.MAIL.BufferSize=10
log4j.appender.MAIL.From=web@www.wuset.com
log4j.appender.MAIL.SMTPHost=www.wusetu.com
log4j.appender.MAIL.Subject=Log4J Message
log4j.appender.MAIL.To=web@www.wusetu.com
log4j.appender.MAIL.layout=org.apache.log4j.PatternLayout
log4j.appender.MAIL.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n
# 用于数据库
log4j.appender.DATABASE=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.DATABASE.URL=jdbc:mysql://localhost:3306/test
log4j.appender.DATABASE.driver=com.mysql.jdbc.Driver
log4j.appender.DATABASE.user=root
log4j.appender.DATABASE.password=
log4j.appender.DATABASE.sql=INSERT INTO LOG4J (Message) VALUES ('[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n')
log4j.appender.DATABASE.layout=org.apache.log4j.PatternLayout
log4j.appender.DATABASE.layout.ConversionPattern=[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n

log4j.appender.A1=org.apache.log4j.DailyRollingFileAppender
log4j.appender.A1.File=SampleMessages.log4j
log4j.appender.A1.DatePattern=yyyyMMdd-HH'.log4j'
log4j.appender.A1.layout=org.apache.log4j.xml.XMLLayout
#自定义Appender
log4j.appender.im = net.cybercorlin.util.logger.appender.IMAppender
log4j.appender.im.host = mail.cybercorlin.net
log4j.appender.im.username = username
log4j.appender.im.password = password
log4j.appender.im.recipient = corlin@cybercorlin.net
log4j.appender.im.layout=org.apache.log4j.PatternLayout
log4j.appender.im.layout.ConversionPattern =[framework] %d - %c -%-4r [%t] %-5p %c %x - %m%n

posted @ 2009-05-04 14:40 lanxin1020 阅读(114) | 评论 (0)编辑 收藏
Log4j下载
在apache网站:jakarta.apache.org/log4j 可以免费下载到Log4j最新版本的软件包。

Log4j使用
Log4j的包下载完成后,解压,将其中打包好的的log4j-1.x.x.jar导入你的工程LIB中。
Log4j之所以受欢迎的原因之一是它的灵活性。Log4j提供了灵活的配置方法,默认是调用BasicConfigurator.configure()来进行配置,但如果只是简单的调用BasicConfigurator.configure()来进行配置工作,那么所有的配置都是固定的,不方便以后修改配置。另一种是动态配置,Log4j提供了PropertyConfigurator.configure(……)来动态配置,参数可以是一个properties文件所在路径的String对象,可以是一个properties文件所在路径的URL对象,也可以是一个properties对象。如果要用XML文件来配置信息,则可用类型的DOMConfigurator()函数来从一个XML文件中加载配置信息。这种方式更方便修改配置。

动态配置

Java代码
  1. package http;       
  2.       
  3. import org.apache.log4j.BasicConfigurator;       
  4. import org.apache.log4j.Logger;       
  5. import org.apache.log4j.PropertyConfigurator;       
  6. import org.apache.log4j.xml.DOMConfigurator;       
  7.       
  8. public class Log4jDemo {       
  9.     static Logger log = Logger.getLogger(Log4jDemo.class.getClass());       
  10.     /**     
  11.      * main     
  12.      * @param args     
  13.      */      
  14.     public static void main(String[] args) {       
  15.         BasicConfigurator.configure();//默认配置       
  16.         PropertyConfigurator.configure("c:/log4j.properties");       
  17.         //动态配置,参数可以是一个properties文件所在路径的String对象       
  18.         //可以是一个properties文件所在路径的URL对象,也可以是一个properties对象       
  19.                
  20.         DOMConfigurator.configure("c:/log4j.xml");//XML配置文件       
  21.       
  22.         //PropertyConfigurator.configure()的参数还可以是XML、Properties对象       
  23.                
  24.         //下面就可使用log4j       
  25.         log.info("info");       
  26.         log.debug("debug");       
  27.         log.error("error");       
  28.         log.warn("warn");       
  29.     }       
  30.       
  31. }      


J2EE应用log4j
上面代码描述了Log4j的简单应用,其实使用Log4j也就是这样简单方便。当然除了上面的配置方法,还有其它,比如做一个J2EE应用,在J2EE应用使用Log4j,必须先在启动服务时加载Log4j的配置文件进行初始化,可以在web.xml中进行。


java 代码
Java代码
  1. import java.io.IOException;       
  2. import java.io.PrintWriter;       
  3.       
  4. import javax.servlet.ServletException;       
  5. import javax.servlet.http.HttpServlet;       
  6. import javax.servlet.http.HttpServletRequest;       
  7. import javax.servlet.http.HttpServletResponse;       
  8.       
  9.       
  10.       
  11. public class J2eeLog4jDemo extends HttpServlet {       
  12.       
  13.     public void destroy() {       
  14.         super.destroy();        
  15.     }          
  16.     public void doGet(HttpServletRequest request, HttpServletResponse response)       
  17.             throws ServletException, IOException {       
  18.     }       
  19.     public void doPost(HttpServletRequest request, HttpServletResponse response)       
  20.             throws ServletException, IOException {             
  21.     }       
  22.     public void init() throws ServletException {       
  23.         //通过web.xml来动态取得配置文件       
  24.         String prefix = getServletContext().getRealPath("/");       
  25.         String file = getInitParameter("log4j");       
  26.       
  27.         //如果没有给出相应的配置文件,则不进行初始化       
  28.         if(file != null)        
  29.         {       
  30.             PropertyConfigurator.configure(prefix+file);       
  31.         }       
  32.     }       
  33.       
  34. }      

Web.xml 代码
Java代码
  1. <servlet>      
  2. <servlet-name>j2eelog4jdemoservlet-name>      
  3. <servlet-class>J2eeLog4jDemoservlet-class>      
  4. <init-param>      
  5. <param-name>log4jparam-name>      
  6. <param-value>log4j.propertiesparam-value>      
  7. init-param>      
  8. <load-on-startup>1load-on-startup>  //设为1时,Web容器启动即进行加载    
  9. servlet  


Spring配置Log4j
Spring中配置Log4j只要配置applicationContext.xml文件,Log4j的配置文件放在Web工程的根目录下,默认是objectname/root下,可以在web.xml中设置工程根目录.

设置根目录


web.xml 代码
Java代码
  1. <!--不定义webAppRootKey参数,webAppRootKey就是缺省的"webapp.root"-->      
  2.  <context-param>      
  3.   <param-name>webAppRootKeyparam-name>      
  4.   <param-value>webapp.rootparam-value>      
  5.  context-param>  


配置applicationContext.xml


applicationContext.xml 代码
Java代码
  1. <!--由Sprng载入的Log4j配置文件位置-->      
  2. <context-param>      
  3.  <param-name>log4jConfigLocationparam-name>      
  4.  <param-value>/WEB-INF/log4j.propertiesparam-value>      
  5.  <!--在这里定位配置文件,需要的是从root开始的绝对路径-->      
  6. context-param>      
  7.       
  8.       
  9.       
  10. <!--Spring默认刷新Log4j配置文件的间隔,单位为millisecond-->      
  11. <context-param>      
  12.  <param-name>log4jRefreshIntervalparam-name>      
  13.  <param-value>60000param-value>      
  14. context-param>      
  15.       
  16. <!--Spring log4j 监听器-->      
  17. <listener>      
  18.  <listener-class>org.springframework.web.util.Log4jConfigListenerlistener-class>      
  19. listener>     


同时使用commons-logging和Log4j


1)首先在classpath下寻找自己的配置文件commons-logging.properties,如果找到,则使用其中定义的Log实现类
2)如果找不到commons-logging.properties文件,则在查找是否已定义系统环境变量org.apache.commons.logging.Log,找到则使用其定义的Log实现类
3)否则,查看classpath中是否有Log4j的包,如果发现,则自动使用Log4j作为日志实现类
4)否则,使用JDK自身的日志实现类(JDK1.4以后才有日志实现类)
5)否则,使用commons-logging自己提供的一个简单的日志实现类SimpleLog


将commons-logging和Log4j的jar包都放置到classpath下,同时也将Log4j的配置文件放到classpath中,两者就可以很好的合作,实现如下:
Java代码
  1. package com.doctorcom.model;       
  2.       
  3. import org.apache.commons.logging.Log;       
  4.       
  5. public class LogFactorySupport {           
  6.           
  7.     public Log getLog(){       
  8.         Log log = org.apache.commons.logging.LogFactory.getLog(LogFactorySupport.class);       
  9.         log.info("");       
  10.         log.debug("");       
  11.     }       
  12.            
  13. }   


java 代码

Log4j配置内容
看一个简单的java属性配置文件log4j.properties:

properties 代码
Java代码
  1. #指定根Logger,及日志输出级别,大于等于该级别的日志将被输出( DEBUG < INFO < WARN < ERROR < FATAL ) 设为OFF可以关闭日志       
  2. log4j.rootLogger=DEBUG, A1,A2       
  3. #指定log输出目的,这里设为输出日志到指定目录的文件my.log中       
  4. log4j.appender.A1=org.apache.log4j.FileAppender       
  5. log4j.appender.A1.File=\\logs\\my.log   #当前根目录下    
  6. #指定日志信息的格式       
  7. log4j.appender.A1.layout=org.apache.log4j.PatternLayout        
  8. log4j.appender.A1.layout.ConversionPattern=%r %d{yyyy-MM-dd HH:mm:ss} %c %p -%m%n       
  9.       
  10. #把A2输出到控制台       
  11. log4j.appender.A2=org.apache.log4j.ConsoleAppender       
  12. log4j.appender.A2.layout=org.apache.log4j.SimpleLayout        
  13.       
  14. #还可以单独指定输出某个包的日志级别       
  15. #log4j.logger.com.study.HelloLog4j=INFO   


1、配置根Logger,其语法为:
log4j.rootLogger = [ level ] , appenderName, appenderName2
level:日志的级别,指定这条日志信息的重要性。分为ALL < DEBUG < INFO < WARN <error fatal=""></error>一般常用的为 DEBUG , INFO ,WARN ,ERROR四种,分别对应Logger类的四种方法
debug(Object message ) ;
info(Object message ) ;
warn(Object message ) ;
error(Object message ) ;
如果设置级别为INFO,则优先级大于等于INFO级别(如:INFO、WARN、ERROR)的日志信息将可以被输出,小于该级别的如:DEBUG将不会被输出
appenderName :就是指定日志信息输出目的地,比如(打印到控制台,输出到文件等)。同一条日志信息可以配置多个输出目的地。

2、配置log输出目的地
Log4j提供以下几种:
org.apache.log4j.ConsoleAppender(控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
3、log信息的格式
org.apache.log4j.HTMLLayout(HTML表格形式)
org.apache.log4j.SimpleLayout(简单格式的日志,只包括日志信息的级别和指定的信息字符串 ,如:DEBUG - Hello)
org.apache.log4j.TTCCLayout(日志的格式包括日志产生的时间、线程、类别等等信息)
org.apache.log4j.PatternLayout(灵活地自定义日志格式)

当使用org.apache.log4j.PatternLayout来自定义信息格式时,可以使用
log4j.appender.A1.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %p -%m%n 来格式化信息
%c    输出所属类的全名,可写为 %c{Num} ,Num类名输出的范围  如:"com.sun.aaa.classB", %C{2}将使日志输出输出范围为:aaa.classB
%d    输出日志时间其格式为 可指定格式 如 %d{HH:mm:ss}等
%l    输出日志事件发生位置,包括类目名、发生线程,在代码中的行数
%n    换行符
%m    输出代码指定信息,如info(“message”),输出message
%p    输出日志的优先级,即 FATAL ,ERROR 等
%r    输出从启动到显示该条日志信息所耗费的时间(毫秒数)
%t    输出产生该日志事件的线程名
posted @ 2009-05-04 14:37 lanxin1020 阅读(151) | 评论 (0)编辑 收藏

HashMap 与 TreeMap的区别

HashMap通过hashcode对其内容进行快速查找,而TreeMap中所有的元素都保持着某种固定的顺序,如果你需要得到一个有序的结果你就应该使用TreeMap(HashMap中元素的排列顺序是不固定的)。

集合框架”提供两种常规的Map实现:HashMapTreeMap (TreeMap实现SortedMap接口)。在Map 中插入、删除和定位元素,HashMap 是最好的选择。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。使用HashMap要求添加的键类明确定义了hashCode()equals()的实现。  这个TreeMap没有调优选项,因为该树总处于平衡状态。

2、两个对象值相同(x.equals(y) == true),但却可有不同的hash code,这句话对不对?hash code是什么意思

 hashcode是给一系列hash算法用的,比如hashtable。不同的对象应该有不同的hashcode,同一个对象应该有同样的hashcode

更正,不是同一个对象,而是相等的对象,应该有相同的hashcode

hash算法是什么啊,作用? hash算法基本就是为了将一个对象和一个整数对应起来,不同的对象对应不同的整数。
(x.equals(y) == true)那这个的话就是去比较它们所对应的整数?
不是。有一个equals()函数,和一个hashcode()函数

3、String a="abc";String b=new String("abc");String c="abc";

System.out.println(a==b);f
System.out.println(a==c);t
System.out.println(b==c);f
System.out.println(a.equals(b));
输出结果是什么?
为什么?

4、a=0;b=0;
if((a=3)>0|(b=3)>0){}
if((a=3)>0||(b=3)>0){}分别说出a,b的值

posted @ 2009-04-18 09:42 lanxin1020 阅读(202) | 评论 (0)编辑 收藏
     摘要: TREEMAP的排序机制   1package com.sf;  2  3import java.text.CollationKey;  4import java.text.Collator;  5import java.util.Comparator;  6import ...  阅读全文
posted @ 2009-04-18 09:36 lanxin1020 阅读(1076) | 评论 (0)编辑 收藏

内部类:定义在其他类里面的类。
使用内部类的理由:
1.内部类方法能够访问外部类的任何数据成员包括私有成员。
2.对同一个包的其他类,内部类是不可见的。
3.匿名内部类能够方便的定义回调而不用写太多方法。

非静态内部类没有默认的构造函数,非静态内部类的构造函数都有一个外围类对象的引用。
内部类的特殊语法规则:
1.相对内部类,引用其外部类隐式对象的形式:OuterClass.this
2.调用内部类的构造函数:outerObject.new InnerClass(construction parameters);
3.外部类外面引用内部类:OuterClass.InnerClass

内部类是一种编译器现象与虚拟机无关。编译器将内部类翻译为用$分隔外部类名和内部类名的常规类文件,虚拟机对此并无所知。
使用javap -private OuterClass$InnerClass。javap这个工具确实挺不错的,对分析字节码和源码都有很大的帮助。
可以看出详细的内部类源码清单,其中包括了编译器自动添加的部分:
public class Outer
{
 public class Inner
 {
 }
}
当内部类是非静态内部类时相应的内部类的详细源码如下:
Compiled from "Outer.java"
public class Outer$Inner extends java.lang.Object{
    final Outer this$0;  //编译器自动在内部类里面添加了指向外部类对象的引用
    public Outer$Inner(Outer);  //内部类的构造函数默认有一个外部类对象作为参数。
}

当内部类是静态内部类时:
Compiled from "Outer.java"
public class Outer$Inner extends java.lang.Object{
    public Outer$Inner(); //没有了对外部类对象的引用
}


如下代码模拟了上面内部类的情形唯一不同的是这里的Inner没有访问Outer私有数据的权限:
class Outer{
   Inner in = new Inner(this);
}

class Inner{
   public Inner(Outer outer){
      this.outer = outer;
   }
  
   private Outer outer;
}

 

//那么权限是如何控制的呢?当内部类中的方法访问到外部类的私有数据时(注意如果内部类没有方法去访问外部类的私有数据不会生成该静态方法static int access$000(Outer);)
public class Outer
{
 private int i;
 public void methodOne()
 {
 }

 class Inner
 {
  public void print(){
   System.out.println(Outer.this.i);
  }
 }
}

相应的外部类详细源码如下:
public class Outer
{
   public Outer();

   public void methodOne();
   static int access$000(Outer); //由编译器合成的用于内部类对外部类进行特殊访问权限的控制:这也是
                                //为什么内部类能够访问外部类中的私有数据的原因。
   private int i;
}

内部类访问外部类的private数据的这种方式很可能导致危险,虽然access$000不是一个合法的Java方法名,但是熟悉class文件结构的黑客可以使用十六进制编辑器轻松创建一个用虚拟机指令调用那个方法的类文件。由于隐秘的访问方法需要拥有包可见性,所以攻击代码需要与被攻击类放在同一个包中。总之,如果内部类访问了外部类的私有数据域,就有可能通过附加在外部类所在包中的其他类访问私有数据。

局部内部类:定义在方法之中,因此局部类没有public和private修饰符。对外界是完全隐藏的。
局部类不仅能够访问外部类,还能访问方法中的局部变量(或方法的参数)。不过那些局部变量要声明为final的。


匿名内部类:用外部类+$+数字的形式表示。一个外部类中有多个匿名内部类时,其生成的class文件对应有Outer$(1、2、3)的形式表示。注意到该匿名内部类是final的。
final class Outer$1
{
   Outer$1(Outer);

   public void method();

   final Outer this$0;
}

嵌套类:当内部类不需要访问外部类的数据成员时应该使用嵌套类。注意只有内部类可以声明为static的其他不行。
在接口中声明的内部类自动转换为public static的,在接口中定义的成员自动都是public static的。

内部类构造函数的可见性与其类的可见性相同。

 

posted @ 2009-04-16 14:06 lanxin1020 阅读(137) | 评论 (0)编辑 收藏
1.Struts 2的基本流程
  Struts 2框架由3个部分组成:核心控制器FilterDispatcher、业务控制器和用户实现的业务逻辑组件。在这3个部分里,Struts 2框架提供了核心控制器FilterDispatcher,而用户需要实现业务控制器和业务逻辑组件。
2.核心控制器:FilterDispatcher
  FilterDispatcher是Struts 2框架的核心控制器,该控制器作为一个Filter运行在Web应用中,它负责拦截所有的用户请求,当用户请求到达时,该Filter会过滤用户请求。如果用户请求以action结尾,该请求将被转入Struts 2框架处理。

Struts 2框架获得了*.action请求后,将根据*.action请求的前面部分决定调用哪个业务逻辑组件,例如,对于login.action请求,Struts 2调用名为login的Action来处理该请求。

Struts 2应用中的Action都被定义在struts.xml文件中,在该文件中定义Action时,定义了该Action的name属性和class属性,其中name属性决定了该Action处理哪个用户请求,而class属性决定了该Action的实现类。

Struts 2用于处理用户请求的Action实例,并不是用户实现的业务控制器,而是Action代理——因为用户实现的业务控制器并没有与Servlet API耦合,显然无法处理用户请求。而Struts 2框架提供了系列拦截器,该系列拦截器负责将HttpServletRequest请求中的请求参数解析出来,传入到Action中,并回调Action 的execute方法来处理用户请求。

显然,上面的处理过程是典型的AOP(面向切面编程)处理方式。图3.19显示了这种处理模型。



图3.19  Struts 2的拦截器和Action

从图3.19中可以看出,用户实现的Action类仅仅是Struts 2的Action代理的代理目标。用户实现的业务控制器(Action)则包含了对用户请求的处理。用户的请求数据包含在 HttpServletRequest对象里,而用户的Action类无需访问HttpServletRequest对象。拦截器负责将 HttpServletRequest里的请求数据解析出来,并传给业务逻辑组件Action实例。
3.业务控制器
  正如从图3.19所看到的,业务控制器组件就是用户实现Action类的实例,Action类里通常包含了一个execute方法,该方法返回一个字符串——该字符串就是一个逻辑视图名,当业务控制器处理完用户请求后,根据处理结果不同,execute方法返回不同字符串   ——每个字符串对应一个视图名。

程序员开发出系统所需要的业务控制器后,还需要配置Struts 2的Action,即需要配置Action的如下三个部分定义:

—  Action所处理的URL。

—  Action组件所对应的实现类。

—  Action里包含的逻辑视图和物理资源之间的对应关系。

每个Action都要处理一个用户请求,而用户请求总是包含了指定URL。当Filter Dispatcher拦截到用户请求后,根据请求的URL和Action处理URL之间的对应关系来处理转发。
4.Struts 2的模型组件
  实际上,模型组件已经超出了MVC框架的覆盖范围。对于Struts 2框架而言,通常没有为模型组件的实现提供太多的帮助。

文本框: 图3.20 控制器调用模型组件Java EE应用里的模型组件,通常指系统的业务逻辑组件。而隐藏在系统的业务逻辑组件下面的,可能还包含了DAO、领域对象等组件。

通常,MVC框架里的业务控制器会调用模型组件的方法来处理用户请求。也就是说,业务逻辑控制器不会对用户请求进行任何实际处理,用户请求最终由模型组件负责处理。业务控制器只是中间负责调度的调度器,这也是称Action为控制器的原因。


图3.20显示了这种处理流程。

提示  在图3.20中看到Action调用业务逻辑组件的方法。当控制器需要获得业务逻辑组件实例时,通常并不会直接获取业务逻辑组件实例,而是通过工厂模式来获得业务逻辑组件的实例;或者利用其他IoC容器(如Spring容器)来管理业务逻辑组件的实例。
5.Struts 2的视图组件
  Struts 2已经改变了Struts 1只能使用JSP作为视图技术的现状,Struts 2允许使用其他的模板技术,如FreeMarker、Velocity作为视图技术。

当Struts 2的控制器返回逻辑视图名时,逻辑视图并未与任何的视图技术关联,仅仅是返回一个字符串,该字符串作为逻辑视图名。

当我们在struts.xml文件中配置 Action时,不仅需要指定Action的name属性和class属性,还要为Action元素指定系列result子元素,每个result子元素定义一个逻辑视图和物理视图之间的映射。前面所介绍的应用都使用了JSP技术作为视图,故配置result子元素时没有指定type属性,默认使用JSP 作为视图资源。

如果需要在Struts 2中使用其他视图技术,则可以在配置result子元素时,指定相应的type属性即可。例如,如果需要使用FreeMarker,则为result指定值为freemarker的type属性;如果想使用Velocity模板技术作为视图资源,则为result指定值为velocity的type属性……
6.Struts 2的运行流程
  经过上面介绍,我们发现Struts 2框架的运行流程非常类似于WebWork框架的流程。

提示  在Struts 2的官方站点,我们可以找到如下说法:Essentially,Struts 2.0 is the technical equivalent of WebWork 2.3。Aside from the package and property renaming,it isn't much different than,say,migrating from WebWork 2.1 to 2.2——意思是说:Struts 2.0技术等同于WebWork 2.3框架,除了包和属性被改名外。从WebWork 2.2迁移到Struts 2不会比从WebWork 2.1迁移到WebWork 2.2更复杂。

这里我们可以看到,Struts 2其实就是WebWork 2.2的升级版,这也就不难理解:为什么WebWork和Struts 2如此相似!

因此,Struts 2的运行流程与WebWork的运行流程完全相同,读者可以参看图1.8来了解Struts 2的运行流程。
posted @ 2009-04-15 14:03 lanxin1020 阅读(1429) | 评论 (0)编辑 收藏
     摘要: 问题:         struts2 使用jakarta 上传文件时,如果上传文件的大小超出commons fileupload(jakarta上传文件还是依赖commons-fileupload)设置的大小就会在进入action以前抛出异常.       &nb...  阅读全文
posted @ 2009-04-14 12:50 lanxin1020 阅读(2212) | 评论 (0)编辑 收藏

 

下文书中包的版本:commons-fileupload-1.2.1.jar、struts2-core-2.1.2.jar

孙鑫的书《Struts2 深入详解》509页是关于限制上传文件的最大长度的内容。
其中谈到fileUpload拦截器只是当文件上传到服务器上之后,才进行的文件类型和大小判断。
Struts2框架底层默认用的是apache的commons-fileupload组件对上传文件进行接受处理。
通过struts.multipart.maxSize属性来对文件大小进行限定时,将直接影响到commons-fileupload组件的文件大小设定,默认是2M。当上传文件超过了这个尺寸时,将从commons-fileupload组件中抛出SizeLimitExceededException异常。上传文件拦截器捕获到这个异常后,将直接把该异常信息设置为Action级别的错误信息。

经过我的测试和对源代码的Debug,发现确实如孙鑫书中所言,如果上传文件大于2M时,在页面上就出现了一堆英文的错误信息,大致是:the request was rejected because its size....exceeds the configured maximum...并且在fieUpload中将来自MultiPartRequestWrapper型request对象的错误信息给加到了Action的错误中。

这时候,你在ApplicationResources.properties中自定义的上传文件过大的错误信息根本不起作用。原因就如书上所言,在底层commons-fileupload组件中就把异常给抛出来了文件根本没被上传,所以到了fileUpload拦截器时,根据取不到文件,当然也就没法对文件的类型和大小进行判断了。

然而,这个异常直接带来两个问题:

1、在页面上显示了英文的错误信息。这样的信息显然不是我们想要的。
2、由于错误的产生,原来页面上输入的其他文本内容也都不见了,也就是说params注入失败。


带着这两个问题,我们来探寻一下Struts2对于请求的处理过程。
注:这并不是一篇关于Struts2请求过程的介绍,主要是为了解决以上两个问题,才引起的简单分析。

首先当然我们要拿FilterDispatcher开刀。

在doFilter方法中调用了prepareDispatcherAndWrapRequest方法,为了包装出Struts2自己的request对象,在prepareDispatcherAndWrapRequest方法中调用Dispatcher类的wrapRequest方法,在这个方法里,会根据请求内容的类型(提交的是文本的,还是multipart/form-data格式),决定是使用tomcat的HttpServletRequestWrapper类分离出请求中的数据,还是使用Struts2的MultiPartRequestWrapper来分离请求中的数据。
注:向服务器请求时,数据是以流的形式向服务器提交,内容是一些有规则东东,我们平时在jsp中用request内置对象取parameter时,实际上是由tomcat的HttpServletRequestWrapper类分解好了的,无需我们再分解这些东西了。

当然,在这里,我们研究的是上传文件的情况,所以,由于form中设定的提交内容是媒体格式的,所以,Dispatcher类的wrapRequest方法会将请求交由MultiPartRequestWrapper类来处理。

MultiPartRequestWrapper这个类是Struts2的类,并且继承了tomcat的HttpServletRequestWrapper类,也是我们将用来代替HttpServletRequest这个类的类,看名字也知道,是对多媒体请求的包装类。
Struts2本身当然不会再造个轮子,来解析请求,而是交由Apache的commons-fileupload组件来解析了。
在MultiPartRequestWrapper的构造方法中,会调用MultiPartRequest(默认为JakartaMultiPartRequest类)的parse方法来解析请求。

在Struts2的JakartaMultiPartRequest类的parse方法中才会真正来调用commons-fileupload组件的ServletFileUpload类对请求进行解析,至此,Struts2已经实现了将请求转交commons-fileupload组件对请求解析的全过程。剩下的就是等commons-fileupload组件对请求解析完毕后,拿到分解后的数据,根据field名,依次将分解后的field名和值放到params(HashMap类型)里,同时JakartaMultiPartRequest类重置了HttpServletRequest的好多方法,比如熟知的getParameter、getParameterNames、getParameterValues,实际上都是从解析后得到的那个params对象里拿数据,在这个过程,commons-fileupload组件也乖乖的把上传的文件分析好了,JakartaMultiPartRequest也毫不客气的把分解后的文件一个一个的放到了files(HashMap类型)中,实际上此时,commons-fileupload组件已经所有要上传的文件上传完了。至此,Struts2实现了对HttpServletRequest类的包装,当回到MultiPartRequestWrapper类后,再取一下上述解析过程中发生的错误,然后把错误加到了自己的errors列表中了。同样我们会发现在MultiPartRequestWrapper类中,也把HttpServletRequest类的好多方法重载了,毕竟是个包装类嘛,实际上对于上传文件的请求,在Struts2后期的处理中用到的request都是MultiPartRequestWrapper类对象,比如我们调用getParameter时,直接调用的是MultiPartRequestWrapper的getParameter方法,间接调的是JakartaMultiPartRequest类对象的getParameter方法。
注:从这里,我们就可以看出,JakartaMultiPartRequest是完全设计成可以替换的类了。

然后继续向回返,到了Dispatcher类的wrapRequest方法,直接把MultiPartRequestWrapper对象返回了,我们就终于回到了FilterDispatcher类的prepareDispatcherAndWrapRequest方法,此时,我们拿到了完全解析好了的request对象(MultiPartRequestWrapper类),该对象又进一步被返回到了FilterDispatcher类的doFilter方法,也就是回到了出发点,至此,doFilter中拿到的request对象就是一个将请求中的数据分解好的了HttpServletRequest对象,我们完全可以用getParameter方法取其中的数据了,同时,我们也可以用getFiles得到文件数组了。
doFilter方法中,会进一步调用actionMapper的getMapping方法对url进行解析,找出命名空间和action名等,以备后面根据配置文件调用相应的拦截器和action使用。

关于doFilter方法中下一步对Dispatcher类的serviceAction方法的调用,不再描述,总之在action被调用之前,会首先走到fileUpload拦截器(对应的是FileUploadInterceptor类),在这个拦截器中,会先看一下request是不是 MultiPartRequestWrapper,如果不是,就说明不是上传文件用的request,fildUpload拦截器会直接将控制权交给下一个拦截器;如果是,就会把request对象强转为MultiPartRequestWrapper对象,然后调用hasErrors方法,看看有没有上传时候产生的错误,有的话,就直接加到了Action的错误(Action级别的)中了。另外,在fileUpload拦截器中会将MultiPartRequestWrapper对象中放置的文件全取出来,把文件、文件名、文件类型取出来,放到request的parameters中,这样到了params拦截器时,就可以轻松的将这些内容注入到Action中了,这也就是为什么fileUpload拦截器需要放在params拦截器前面的理由。在文件都放到request的parameters对象里之后,fileUpload拦截器会继续调用其他拦截器直到Action等执行完毕,他还要做一个扫尾的工作:把临时文件夹中的文件删除(这些文件是由commons-fileupload组件上传的,供你在自己的Action中将文件copy到指定的目录下,当action执行完了后,这些临时文件当然就没用了)。

你好,你还在看吗?呵呵,是不是太多了,也太乱了,没办法,Struts2就是这样的调用的。也不知道Struts2有没有公开其Sequence图,我是想画一个,不过,太懒,还是看着代码说说吧。

如果上面看烦了,也完全可以不看了,直接看下面的。

在上面一番分析之后,文件上传的全过程就结束了。
我们回到我们的问题上来。

先看第一个:
1、在页面上显示了英文的错误信息。这显然不是我们想要的。

没办法了,commons-fileupload组件没想到国际化,在FileUploadInterceptor拦载器中,也没想着国际化,直接放到Action的错误中了,就没他事了,三种做法:
  (1)在错误显示之前,把这条错误给换掉,应该难度不大,我没做留给你做了。
  (2)或者重写一下JakartaMultiPartRequest这个类,把捕捉到的异常信息换成自己的,然后,通过Struts2的配置文件,把我们重写的这个parser换上去用。
  (3)直接改commons-fileupload组件的类,换成中文的。
我具体说一下第(3)种做法:找到FileUploadBase类,把902行~908行改一下。
FileUploadException ex =
    new SizeLimitExceededException(
        "the request was rejected because"
        + " its size (" + pCount
        + ") exceeds the configured maximum"
        + " (" + pSizeMax + ")",
        pCount, pSizeMax);
=>
FileUploadException ex = new SizeLimitExceededException(
"服务器拒绝了您的请求,原因可能是向服务器提交的数据发生了丢失。", pCount, pSizeMax);

把914行~918行改一下。
throw new SizeLimitExceededException(
        "the request was rejected because its size ("
        + requestSize
        + ") exceeds the configured maximum ("
        + sizeMax + ")",
=>
throw new SizeLimitExceededException("服务器拒绝了您的请求,原因是提交数据量过大(通常是由于上传文件过大),请返回上页重试。"
+ " (最大字节数:" + sizeMax / 1024
+ "K)", requestSize, sizeMax);


再看一下第二个问题。
2、由于错误的产生,原来页面上输入的内容也全部不见了,也就是说params注入失败。
关于这个问题我在javaeye上搜索到一篇文章(使用的commons-fileupload组件的jar包似乎比较老)。
http://www.javaeye.com/topic/197345

虽然按照此文,当上传失败时,能够将其他输入内容显示出来,但是这样做的结果是全部的文件肯定会上传到服务器上,也就是说,虽然是页面上报了文件因为太大,请求被拒绝的错,但是文件依然会被上传到服务器上,commons-fileupload组件根本没会去拦文件的上传。
在这里要说明一下,如果你不抛出这个异常,请求的流会继续向服务器上传,只有当整个流上传完了之后,commons-fileupload组件才能正确的分析出文件部分、文本部分。所以,在这里抛出异常是不得已的作法,如果不抛异常,后果是虽然页面报错,但文件还是会被传到服务器的上,这一步根本没挡住输入流的上传,如果没挡住的话,大家想想会有什么后果?
所以,综上所述,对于第二个问题,如果出现了这个异常,我们根本无法让原来输入的内容还显示出来的,因为commons-fileupload组件并没有解析全部的输入内容,直接给出异常了,到了params拦截器中,request里就是空的,根本取不到parameter,所以也就无法注入到Action中了。这种情况下,只能显示一个告知用户由于提交数据量过大,服务器拒绝了请求的错误信息,比较好的方法是,直接跳到一个专门的页面,提示用户,然后让用户点返回来再次输入,否则用户会感觉上传文件大就大吧,怎么连我输入的其他一些内容也没给保存住。当然,如果能用Ajax来上传文件,对客户的操作体验可能是最好的,但是,这样可能会导致服务器上有些挂空的文件(上传后从来没被用过),需要想法清除的。

整个分析下来,我们说第二个问题基本上是无法避免的。
posted @ 2009-04-14 12:46 lanxin1020 阅读(358) | 评论 (0)编辑 收藏
     摘要: Web Service概述 Web Service的定义 W3C组织对其的定义如下,它是一个软件系统,为了支持跨网络的机器间相互操作交互而设计。Web Service服务通常被定义为一组模块化的API,它们可以通过网络进行调用,来执行远程系统的请求服务。 这里我们从一个程序员的视角来观察web service。在传统的程序编码中,存在这各种的函数方法调用。通常,我们知道一个程序...  阅读全文
posted @ 2009-04-13 18:37 lanxin1020 阅读(223) | 评论 (0)编辑 收藏
技术要点
本节代码详细说明文件上传功能的开发流程,介绍知识点如下:
 文件上传页面和显示上传成功页面代码内容。
 UploadAction类中实现上传功能方法和上传文件属性介绍。
 struts.xml中UploadAction配置,以及字符编码、文件临时存放路径配置。
 上传后所处路径和最终上传成功后效果展示。

演示代码
上传文件页面,这里笔者定义的是多个文件上传。
Java代码
  1. <!---------------------文件名:upload.jsp----------------->   
  2. <%@taglib prefix="s" uri="/struts-tags"%>   
  3. <html>   
  4.     <head>   
  5.         <meta http-equiv="Content-Type" content="text/html; charset=gb2312">   
  6.         <title>上传文件</title>   
  7.     </head>   
  8.     <body>   
  9.     <!-- 上传文件表单定义 -->   
  10.     <s:form action="upload" method="post" enctype="multipart/form-data">   
  11.         <tr>   
  12.     <!-- 上传文件标签定义 -->   
  13.     <td>上传文件:<s:file name="file"></s:file></td>   
  14.     </tr>   
  15.     <tr>   
  16.     <td>再次上传文件:<s:file name="file"></s:file></td>   
  17.     </tr>   
  18.     <tr>   
  19.     <td align="left"><s:submit name="submit" value="提交"></s:submit></td>   
  20.     </tr>   
  21.     </s:form>   
  22.     </body>   
  23. </html>  

上传文件成功后结果页面
Java代码
  1. <!-------------------文件名:result.jsp ----------------->   
  2. <%@taglib prefix="s" uri="/struts-tags"%>   
  3. <html>   
  4.     <head>   
  5.         <meta http-equiv="Content-Type" content="text/html; charset=gb2312">   
  6.         <title>上传结果</title>   
  7.     </head>   
  8.     <body>   
  9.         上传文件:   
  10.         <!-- 显示上传成功文件名 -->   
  11.         <s:property value="fileFileName" />   
  12.     </body>   
  13. </html>  

UploadAction类代码
Java代码
  1. <!------------------文件名:UploadAction.java ------------------>   
  2. import java.io.File;   
  3. import java.io.FileInputStream;   
  4. import java.io.FileNotFoundException;   
  5. import java.io.FileOutputStream;   
  6. import java.io.IOException;   
  7. import java.io.InputStream;   
  8. import java.io.OutputStream;   
  9. import java.util.List;   
  10.   
  11. import org.apache.struts2.ServletActionContext;   
  12. import com.opensymphony.xwork2.ActionSupport;   
  13.   
  14. //文件上传Action   
  15. public class UploadAction extends ActionSupport {   
  16.     //上传文件存放路径   
  17.     private final static String UPLOADDIR = "/upload";   
  18.     //上传文件集合   
  19.     private List<File> file;   
  20.     //上传文件名集合   
  21.     private List<String> fileFileName;   
  22.     //上传文件内容类型集合   
  23.     private List<String> fileContentType;   
  24.   
  25.     public List<File> getFile() {   
  26.         return file;   
  27.     }   
  28.   
  29.     public void setFile(List<File> file) {   
  30.         this.file = file;   
  31.     }   
  32.   
  33.     public List<String> getFileFileName() {   
  34.         return fileFileName;   
  35.     }   
  36.   
  37.     public void setFileFileName(List<String> fileFileName) {   
  38.         this.fileFileName = fileFileName;   
  39.     }   
  40.   
  41.     public List<String> getFileContentType() {   
  42.         return fileContentType;   
  43.     }   
  44.   
  45.     public void setFileContentType(List<String> fileContentType) {   
  46.         this.fileContentType = fileContentType;   
  47.     }   
  48.   
  49.     public String execute() throws Exception {   
  50.         for (int i = 0; i < file.size(); i++) {   
  51.             //循环上传每个文件   
  52.             uploadFile(i);   
  53.         }   
  54.         return "success";   
  55.     }   
  56.   
  57.     //执行上传功能   
  58.     private void uploadFile(int i) throws FileNotFoundException, IOException {   
  59.         try {   
  60.             InputStream in = new FileInputStream(file.get(i));   
  61.             String dir = ServletActionContext.getRequest().getRealPath(UPLOADDIR);   
  62.             File uploadFile = new File(dir, this.getFileFileName().get(i));   
  63.             OutputStream out = new FileOutputStream(uploadFile);   
  64.             byte[] buffer = new byte[1024 * 1024];   
  65.             int length;   
  66.             while ((length = in.read(buffer)) > 0) {   
  67.                 out.write(buffer, 0, length);   
  68.             }   
  69.   
  70.             in.close();   
  71.             out.close();   
  72.         } catch (FileNotFoundException ex) {   
  73.             ex.printStackTrace();   
  74.         } catch (IOException ex) {   
  75.             ex.printStackTrace();   
  76.         }   
  77.     }   
  78. }  

struts.xml配置文件中有关文件上传的配置:
Java代码
  1. <!--------------------文件名:struts.xml------------------->   
  2. <struts>   
  3.     <!-- 系统常量定义,定义上传文件字符集编码 -->   
  4.     <constant name="struts.i18n.encoding" value="gb2312"></constant>   
  5.     <!-- 系统常量定义,定义上传文件临时存放路径 -->   
  6.     <constant name="struts.multipart.saveDir" value="c:\"></constant>   
  7.     <!-- Action所在包定义 -->   
  8.     <package name="C04.4" extends="struts-default">   
  9.         <!-- Action名字,类以及导航页面定义 -->   
  10.         <!-- 通过Action类处理才导航的的Action定义 -->   
  11.         <action name="upload" class="action.UploadAction">   
  12.             <result name="input">/jsp/upload.jsp</result>   
  13.             <result name="success">/jsp/result.jsp</result>   
  14.         </action>   
  15.     </package>   
  16. </struts>   

(1):文件上传页面如图4.8所示。

图4.8  文件上传
(2):选择文件如图4.9所示。

图4.9  选择上传的文件
(3):单击“提交”按钮后文件上传成功页面,并显示上传文件名,如图4.10所示。

图4.10  上传文件成功后效果
(4):两个被上传文件最终在服务器上存放路径效果如图4.11所示。

图4.11  上传文件存放路径图
代码解释
(1)在upload.jsp中通过Form标签和File标签定义了两个上传文件。Struts2标签笔者会在之后章节里具体介绍,这里只是让读者知道是如何使用标签显示图4.8显示的内容。如果上传成功,笔者在result.jsp中“[”和“]”之间显示上传文件的文件名,如果是多个文件,以“,”相隔。这些显示格式都是用Property标签定义的。
注意:如果上传文件,在JSP的Form中一定要定义如upload.jsp文件中黑体表示的部分。method和enctype属性都必须要如代码中所示。这样Form中上传文件才会起作用。
(2)UploadAction文件中先定义了常量UPLOADDIR,它是上传文件上传后存放的文件夹名字。比如笔者使用的是JBoss(附录中有安装和在MyEclipse中部署的操作说明),则在它的已部署Web项目下的upload文件夹中,会有所有上传成功的文件。如图4.11读者也可以看见它的上传文件最终存放路径。
注意:在MyEclipse中开发的“WebRoot”目录下也要新建一个upload文件夹,否则部署后在JBoss的已部署Web项目下将没有upload文件夹。因为部署的时候会将所有“WebRoot”目录下的文件夹和文件都部署到JBoss的已部署Web项目下。
定义好UPLOADDIR后,在定义上传文件的属性变量。也许其中的“fileFileName”和“fileContentType”读者看了有点别扭,尤其是“fileFileName”感觉不符合Java命名规范,但是这两个属性变量是4.1小节中介绍的“fileUpload”拦截器类中的类公有变量名字,只有这样定义,UploadAction执行时候会把在页面上选择的上传文件的属性值放在这两个变量里面,否则调试UploadAction时候会发现这两个变量都会是“null”即空值。不相信的读者可以自行改变这两个变量名再执行上传文件功能进行调试看一下这两个变量得到的值。
注意:因为这里笔者是进行多个文件上传功能开发,因此“file”、“fileFileName”、“fileFileName”属性变量都设定为List类型,其实还可以设定为数组类型。个人觉得没有啥大区别。完全凭个人喜好而定。还有如果读者自己开发单个文件上传,就没必要把它们设定为List类型或数组类型。直接把“file”定义为Java的IO包中的File类型,“fileFileName”、“fileFileName”定义为普通的String类型即字符串类型。
之后在execute方法中,写一个循环,对所有页面中选择的上传文件一个个进行上传。这里笔者运用了重构中的“抽取方法”的方式,将上传文件的功能封装成一个私有方法,名字为“uploadFile”。其中运用了Java的IO包中很多API方法。有对重构和Java的IO功能不了解的读者可以去查阅相关资料去理解掌握,这里不是本书以及本节重点,因此不再具体记述。
(3)struts.xml中定义了<constant>标签,主要定义了文件名和文件内容显示的字符编码集以及这些被上传文件临时存放路径。
先说明一下<constant>标签,顾名思义这是定义整个Web项目的一些常量属性值,如果不定义则在Struts2自带的default.properties(读者们可到自己安装Struts2的文件路径src\core\src\main\resources\org\apache\struts2\下找到)文件中有这些常量的定义,比如在本节struts.xml文件中的“struts.i18n.encoding”和“struts.multipart.saveDir”在default.properties定义代码如下:
Java代码
  1. <!--------------------文件名:default.properties---------------->   
  2. ### This can be used to set your default locale and encoding scheme   
  3. # struts.locale=en_US   
  4. struts.i18n.encoding=UTF-8  
  5.   
  6. ### Parser to handle HTTP POST requests, encoded using the MIME-type multipart/form-data   
  7. # struts.multipart.parser=cos   
  8. # struts.multipart.parser=pell   
  9. struts.multipart.parser=jakarta   
  10. # uses javax.servlet.context.tempdir by default  
  11. struts.multipart.saveDir=  

如果不在struts.xml文件中定义,则Web项目会缺省使用default.properties文件中这两个常量属性的定义。一个将使字符编码集变为“UTF-8”,另一个干脆没有任何文件路径指定。而笔者开发的该Web项目缺省支持的字符编码集是“gb2312”,而且需要指定临时上传文件存放路径。(当然如果读者开发的Web项目缺省编码集就是“UTF-8”,而且也并不需要指定临时路径时候,就没必要在struts.xml中定义这两个<constant>),因此有必要定义这两个属性符合项目开发要求。
注意:也可以如第3章那样,把这两个属性定义在自定义的struts.properties文件中,具体代码可以如下:
Java代码
  1. <!------------------------文件名:struts.properties------------------>   
  2. struts.i18n.encoding =gb2312   
  3. struts.multipart.saveDir= c:\  

笔者个人认为比在struts.xml中定义更加好,毕竟Struts2自己也是定义在properties属性文件中,而不是定义在自己的xml配置文件中。(Struts2自带的xml配置文件为struts-default.xml,在4.1小节中已记述)。这里是为了让读者知道struts.xml配置文件也可以配置这些属性,因此写在struts.xml配置文件中。从3.2小节笔者说明struts.xml配置文件时并没有介绍<constant>标签这点也可以知道笔者个人其实是不赞同这样的配置手段即在struts.xml中配置<constant>标签。
在<Action>标签中配置“result”,和第3章类似,将这两个JSP文件的导航流程配置好即可。
(4)开始进行文件上传功能展示,按照如上记述的步骤执行即可。笔者在桌面上新建了两个文本文件,将它们上传到JBoss已部署的Web项目中展示文件上传的upload文件夹下。如图4.11所示。
其实还可以指定上传文件的格式,让它只上传特定类型的文件。比如只能上传文本和xml文件,则在struts.xml需要显示配置“uploadFile”拦截器。如下代码:
Java代码
  1. <!-----------------------文件名:struts.xml------------------>   
  2. <struts>   
  3.     <!-- Action所在包定义 -->   
  4.     <package name="C04.4" extends="struts-default">   
  5.         <!-- Action名字,类以及导航页面定义 -->   
  6.         <!-- 通过Action类处理才导航的的Action定义 -->   
  7.         <action name="upload" class="action.UploadAction">   
  8.             <result name="input">/jsp/upload.jsp</result>   
  9.             <result name="success">/jsp/result.jsp</result>   
  10.         </action>   
  11. <!—显示配置文件上传拦截器 -->   
  12. <interceptor-ref name=”fileUpload”>   
  13. <!—指定特定类型的上传文件 -->   
  14. <param name =”allowedTypes”>text/plain,application/xml</param>   
  15. </ interceptor-ref >   
  16. <interceptor-ref name=”defaultStack”></ interceptor-ref >   
  17.     </package>   
  18. </struts>   

定义了一个名为“allowedTypes”的参数,其中在<param></param>之间的是文件类型,也可以用“,”间隔,表示允许上传多个文件类型。这里允许上传文件类型为txt、xml格式的文件。如果读者不知道各个文件类型的定义,可在自己的JBoss安装目录中的server\default\deploy\jboss-web.deployer\conf\下的web.xml文件中找到(搜索<mime-mapping>即可)。
注意:如果显示配置Struts2自己的缺省拦截器一定要写在“defaultStack”前,否则“fileUpload”拦截器不会执行拦截。因为Struts2中如果某个拦截器执行拦截时候发现自己已经执行过,第二个乃至之后同名的拦截器都不会执行。这里因为“defaultStack”拦截器栈中包含了“fileUpload”拦截器,而“fileUpload”拦截器已经执行拦截了,则不会再执行拦截。如果把“defaultStack”拦截器栈放在“fileUpload”拦截器前配置,则只执行“defaultStack”拦截器栈中的“fileUpload”拦截器,这里是没有定义“allowedTypes”的,Struts2缺省默认的是支持所有文件类型。因此它会支持所有文件类型的文件上传。因此再设定“allowedTypes”就没有任何意义了。
posted @ 2009-04-13 15:33 lanxin1020 阅读(222) | 评论 (0)编辑 收藏
Struts2文件下载功能开发
技术要点
本节代码详细说明文件下载功能的开发流程,介绍知识点如下:
 上传成功页面重修改后支持文件下载代码内容。
 DownloadAction文件下载功能开发。
 struts.xml中DownloadAction配置,以及支持文件名为中文字符的文件下载。
 下载文件流程展示。

演示代码
上传成功页面,这里笔者让其在每个上传文件后提供“下载”链接。
Java代码
  1. <!------------------------文件名:result.jsp------------------->   
  2. <%@taglib prefix="s" uri="/struts-tags"%>   
  3.     <body>   
  4.         上传文件:   
  5.         <table>   
  6.         <!-- 循环显示上传成功文件名 -->   
  7.         <s:iterator value="fileFileName" status="fn">   
  8.         <tr>   
  9.         <td>   
  10.         <!-- 上传成功文件名 -->   
  11.         <s:property />           
  12.         </td>   
  13.         <td>   
  14.         <!-- 下载文件链接内容为定义的下载Action -->   
  15.         <!-- 下载文件名作为链接参数fileName值,用OGNL表达式表达 -->     
  16.         <a href="<s:url value='download.action'>                  
  17.                     <s:param name='fileName'  
  18.  value='fileFileName[#fn.getIndex()]'/>     
  19.                  </s:url>">下载</a>   
  20.         </td>   
  21.         </tr>   
  22.         </s:iterator>        
  23.         </table>         
  24.     </body>  

DownLoadAction类代码
Java代码
  1. <!------------文件名:DownLoadAction.java ------------------>   
  2. import java.io.InputStream;   
  3. import java.io.UnsupportedEncodingException;   
  4.   
  5. import org.apache.struts2.ServletActionContext;   
  6. import com.opensymphony.xwork2.ActionSupport;   
  7.   
  8. public class DownLoadAction extends ActionSupport {   
  9.     //下载文件原始存放路径   
  10.     private final static String DOWNLOADFILEPATH="/upload/";   
  11.     //文件名参数变量   
  12.     private String fileName;   
  13.   
  14.     public String getFileName() {   
  15.         return fileName;   
  16.     }   
  17.   
  18.     public void setFileName(String fileName) {   
  19.         this.fileName = fileName;   
  20.     }   
  21.   
  22.     //从下载文件原始存放路径读取得到文件输出流   
  23.     public InputStream getDownloadFile() {   
  24.         return    
  25. ServletActionContext.getServletContext().getResourceAsStream(DOWNLOADFILEPATH+fileName);   
  26.     }   
  27.     //如果下载文件名为中文,进行字符编码转换   
  28.     public String getDownloadChineseFileName() {   
  29.         String downloadChineseFileName = fileName;   
  30.   
  31.         try {   
  32.             downloadChineseFileName = new String(downloadChineseFileName.getBytes(), "ISO8859-1");   
  33.         } catch (UnsupportedEncodingException e) {   
  34.             e.printStackTrace();   
  35.         }   
  36.   
  37.         return downloadChineseFileName;   
  38.     }   
  39.   
  40.     public String execute() {   
  41.         return SUCCESS;   
  42.     }   
  43. }  

struts.xml配置文件中有关文件下载的配置:
Java代码
  1. <!------------------文件名:struts.xml----------------->   
  2. <struts>   
  3.     <!-- 下载文件的Action定义 -->   
  4.         <action name="download" class="action.DownLoadAction">   
  5.             <!-- 设置文件名参数,由页面上传入 -->   
  6.             <param name="fileName"></param>   
  7.             <result name="success" type="stream">   
  8.                 <!-- 下载文件类型定义 -->   
  9.                 <param name="contentType">text/plain</param>   
  10.                 <!-- 下载文件处理方法 -->   
  11.                 <param name="contentDisposition">   
  12.                     attachment;filename="${downloadChineseFileName}"  
  13.                 </param>   
  14.                 <!-- 下载文件输出流定义 -->   
  15.                 <param name="inputName">downloadFile</param>   
  16.             </result>   
  17.         </action>   
  18. </struts>   

(1):文件开始下载页面如图4.12所示。

图4.12  文件下载
(2):单击“下载”链接,比如点“下载文件1.txt”文件右边“下载”链接,出现对话框如图4.13所示。

图4.13  下载文件处理方式
(3):单击“保存”按钮后选择下载文件存放路径,如图4.14所示。

图4.14  下载文件选择存放路径
代码解释
(1)在result.jsp中通过iterator标签和url标签定义了“fileFileName”的循环显示以及链接。其中有关“status”和OGNL表达式笔者会在之后章节里具体介绍,这里只是让读者知道是如何使用标签显示图4.12显示的内容。特别指出<param>标签为downloadAction定义了一个参数,该参数名为“fileName”,因为在4.4.1小节中笔者定义的“fileFileName”是个List类型的数据集合,因此利用OGNL表达式将文件名作为“fileName”参数值传入downloadAction中。
(2)DownLoadAction文件中先定义了常量DOWNLOADFILEPATH,它是下载文件在服务器存放的路径名,也就是4.4.1小节中上传文件在服务器存放的路径名。
定义好DOWNLOADFILEPATH后,在定义DownLoadAction的属性变量。因为在result.jsp中定义了参数“fileName”,则它作为DownLoadAction的属性变量,需要定义相应的getter、setter方法。
然后定义了getDownloadFile方法,它返回的是一个文件流,表明将被下载文件转换为输出流,方便下载。利用Struts2自带的“ServletActionContext”类的API把下载文件存放路径作为方法参数,读取下载文件,将其转换为文件流。
还有一个getDownloadChineseFileName方法,该方法主要作用是将文件名为中文字符的文件进行文件名的字符编码集合转换。因为在Web系统中由JSP等视图页面传入的变量值,特别是中文字符的变量。缺省的字符编码集合都是“ISO8859-1”,因此利用Java的字符串类的API,将字符编码转成开发需要的字符编码集。防止中文字符乱码问题发生。
(3)struts.xml中定义了名为“download”的Action。其中它自己的参数“fileName”因为在这里它的值会从JSP页面上传入,所以这里只是定义,没有具体给它赋任何值
在<result>标签中定义了type属性,值为“stream”。如果是下载文件功能开发,DownLoadAction一定要设置type属性,而且值为“stream”。这是因为在Struts2自带的xml配置文件为struts-default.xml中有关于“stream”的result返回类型的定义,代码如下:
Java代码
  1. <!-------------------文件名:struts-default.xml-------------->   
  2. <result-type name="stream" class="org.apache.struts2.dispatcher.StreamResult"/>  

这里Struts2定义了result返回类型为“stream”,这个result类型主要是处理文件的输入和输出流时候需要使用的。因为下载文件就是把文件转换成输入输出流,将其从一个文件路径放到另外一个文件路径中去。所以肯定要设置这个result类型的。
“contentType”、“contentDisposition”、“inputName”都是这个result的属性。“contentType”就是文件类型。这里因为下载的文件是文本文件,因此设定的值为文本文件类型,具体各个文件类型如何定义,4.4.1小节已经介绍过,这里不再做说明。“contentDisposition”是指定下载文件处理方式,如图4.13就是处理方式的效果。特别指出如果“contentDisposition”定义的值把前面的“attachment”去掉,则下载方式不是以附件方式下载,如果单击“下载”链接,则会把下载文件的内容显示在浏览器中。读者可以去试验一下。这里有个“${downloadChineseFileName}”,这就是在DownLoadAction中定义getDownloadChineseFileName方法的目的,${downloadChineseFileName}是OGNL的表达式,它显示了“downloadChineseFileName”变量的具体值,因为在DownLoadAction中定义getDownloadChineseFileName方法,则把已经转换成符合需要字符编码集的下载文件名作为下载文件方式对话框中显示的名称,不会造成任何乱码问题。“inputName”是最关键的一个属性,也是一定要定义的属性,“inputName”参数中定义的值“downloadFile”就是DownLoadAction中getDownloadFile方法返回的文件流名字。在Struts2中Acion用前缀名为get的方法得到各种属性的值,这些属性有些是在Action中定义,有些就像本示例在配置文件中利用OGNL表达式或直接定义。
(4)开始进行文件下载功能展示,按照如上记述的步骤执行即可。笔者将两个文本文件上传上去,然后在上传成功页面对具体的文件进行下载。在图4.13中单击“保存”按钮就显示图4.14,选择在本机上存放下载文件的路径即可完成下载文件功能。
posted @ 2009-04-13 15:30 lanxin1020 阅读(756) | 评论 (0)编辑 收藏
Struts2标签使用原理解疑
在笔者下载的Struts2的包中,读者可以在/lib下找到struts2-core-2.0.11.1.jar包,解压该包在其根目录下的/META-INF文件夹下可以看到一个名字为“struts-tags.tld”文件。该文件就是Struts2中所有自带的标签库定义。本节通过对该文件代码的介绍来让读者知晓Struts2内部是如何使用这些标签来进行工作。并简单说明JSP中是如何用其来书写标签代码。
技术要点
本节代码说明Struts2内部定义标签的格式和在JSP中使用方式。
 struts-tags.tld文件定标签定义配置格式。
 JSP中使用标签功能介绍。

演示代码
Java代码
  1. <!------------------文件名: struts-tags.tld----------------->   
  2. <taglib>   
  3.   <tlib-version>2.2.3</tlib-version>   
  4.   <jsp-version>1.2</jsp-version>   
  5.   <short-name>s</short-name>   
  6.   <uri>/struts-tags</uri>   
  7.   <display-name>"Struts Tags"</display-name>   
  8.   <description>………………</description>   
  9.   <tag>   
  10.     <name>action</name>   
  11.     <tag-class>org.apache.struts2.views.jsp.ActionTag</tag-class>   
  12.     <body-content>JSP</body-content>   
  13.     <description><![CDATA[Execute an action from within a view]]></description>   
  14.     <attribute>   
  15.       <name>executeResult</name>   
  16.       <required>false</required>   
  17.       <rtexprvalue>false</rtexprvalue>   
  18.       <description><![CDATA[Whether the result of this action (probably a view) should be executed/rendered]]></description>   
  19.     </attribute>   
  20.    …………………………   
  21.     <attribute>   
  22.       <name>namespace</name>   
  23.       <required>false</required>   
  24.       <rtexprvalue>false</rtexprvalue>   
  25.       <description><![CDATA[Namespace for action to call]]></description>   
  26.     </attribute>   
  27.   </tag>   
  28. </taglib>  

代码解释
(1)struts-tags.tld是Struts2自标签定义文件。所有标签定义都是在<tablib>和</taglib>之间定义。以<tag></tag>用来定义一个具体标签。每个标签因为都可以有很多自己的属性。这些属性定义都是以<attribute></attribute>来定义。
(2)<tlib-version></tlib-version>之间定义的是标签库的版本。<jsp-version></jsp-version>定义的是标签库这些标签是支持JSP的哪个版本。<short-name> </short-name>其实是标签库的默认名,也可以认为是其昵称。<uri> </uri>定义的是标签库的URI,在JSP中会使用到。<display-name></display-name>是显示名。<description></description>是标签库的记述,记述标签库的使用用途等等。
(3)<attribute>中<name></name>是属性名称定义。<required></required>表示的该属性是否是必须的属性,如果是必须的则<required></required>之间为true,否则为false。<rtexprvalue></rtexprvalue>表示的是可否使用表达式,大多数标签都是为false。这里不是不能使用表达式,而是恰恰相反表示可以使用表达式。<description></description>定义和前面介绍相同。
(4)在JSP中,如之前章节的演示代码所示,都是在文件头有个使用标签的声明,代码如下。
Java代码
  1. <!---------------------文件名: *.jsp------------------------->   
  2. <%@taglib prefix="s" uri="/struts-tags"%>  

有了这个声明,在JSP文件中就可以使用Struts2的标签。比如form标签定义要像如下代码所示。
Java代码
  1. <s:form action="upload" ………>  

记住一定要用“s”,它是Struts2中标签的默认名也是相当于一个昵称,当然读者也可以把它改为自己想取的名字,不过在标签声明中的“prefix”中就要改成那个自己取的名字。
注意:因为笔者使用的Servlet版本是2.3之上的版本,因此没必要在web.xml中定义标签库。如果读者使用的Servlet版本比较低,则在web.xml文件中需要定义如下的代码:
Java代码
  1. <!----------------------文件名:web.xml----------------------->   
  2. <taglib>   
  3.     <!- 定义URI - ->   
  4.     <taglib-uri>/Struts 2-tags</taglib-uri>   
  5. <!- 定义标签库支持的jar包位置- ->   
  6.     <taglib-location>/WEB-INF/lib/struts2-core-2.0.11.1.jar</taglib-location>   
  7. </taglib>  

只有这样标签库才会在Servlet版本比较低的情况下使用有效果。
posted @ 2009-04-13 15:28 lanxin1020 阅读(449) | 评论 (0)编辑 收藏
--sql structured query language

--DML--Data Manipulation Language--数据操作语言

query information (SELECT),
add new rows (INSERT),
modify existing rows (UPDATE),
delete existing rows (DELETE),
perform a conditional update or insert operation (MERGE),
see an execution plan of SQL (EXPLAIN PLAN),
and lock a table to restrict access (LOCK TABLE).

--DDL--Data Definition Language--数据定义语言
create, modify,drop, or rename objects (CREATE,ALTER,DROP,RENAME),
remove all rows from a database object without dropping the structure (TRUNCATE),
manage access privileges (GRANT,REVOKE),
audit database use (AUDIT,NOAUDIT)
and add a description about an object to the dictionary (COMMENT).

--Transaction Control事务控制语句
save the changes(COMMIT)
or discard the changes (ROLLBACK) made by DML statements.
Also included in the transaction-control statements are statements to set a point or marker in the transaction for possible rollback (SAVEPOINT)
and to define the properties for the transaction (SET TRANSACTION).
Used to manage the properties of the database.
There isonly one statement in this category (ALTER SYSTEM).

--DCL--Data Control Language--与开发关系不是很密切,用于权限的分配与回收
grant,revoke,data control

--Session Control
control the session properties (ALTER SESSION)
and to enable/disable roles (SET ROLE).

--System Control


--------------------------------------------------------
select的用法

--每个员工的所有信息
select * from emp
--每个人的部门编号,姓名,薪水
select deptno,ename,sal from emp;
--每个人的年薪
select ename,sal*12 from emp;
--计算2*3的值
select 2*3 from emp;
--计算2*3的值(dual)
select 2*3 from dual;
select * from dual;
--得到当前时间
select sysdate from dual
--可以给列起别名,比如求每个人的年薪
select ename,sal*12  salperyear from emp;
--如果别名中有空格,需要用双引号
select ename,sal*12  "sal per year" from emp;
--如果没有内容,则为空
select comm from emp;
--当空字段参与计算,则结果是null
--例如:计算每个人的全年的收入包括月薪和年终奖
select ename,sal*12+comm from emp;
--可以将多个字符串拼在一起。比如:求每个人的薪水,格式为smith-sal-123
select ename||'-sal-'||sal from emp;
--如果字符串中有单引号,需要用另外一个单引号转义,比如:这样一个字符串: he's friend
select ename||'''s sal is'||sal from emp;


--------------------------------------------------------
--distinct 关键词的用法
--求有哪些个部门
select distinct deptno from emp
--可以用来修饰多个字段。比如:求有哪些个部门和job的组合
select distinct deptno,job from emp

--------------------------------------------------------
where关键词的用法
--可以是数值类型的等值判断。比如:求10这个部门的所有员工
select * from emp where deptno=20
--可以是字符串类型的等值判断。比如:求叫KING的这个人的信息
select * from emp where ename = 'KING'
--也可以是不等值判断。比如:求薪水小于2000的员工信息
select * from emp where sal<2000;
--字符串也可以做不等值判断,比如:求所有ename大于'CBA'的员工信息。
select * from emp where ename>'CBA';
--求部门不是10的员工
select * from emp where deptno <> 10;
--求薪水在800和1500之间的员工信息
select * from emp where sal >=800 and sal <=1500;
--也可以写成
select * from emp where sal between 800 and 1500
--这样写则不可以
-----------------------------select * from emp where 800<=sal<=1500
--where...in..的用法。比如:求薪水是800或者1500或正2000的员工信息
select * from emp where sal=800 or sal=1500 or sal=2000
--相当于写成这样
select * from emp where sal in(1500,800,2000,1500,1500,1500,1500);
--再比如求姓名是KING,SMITH,AA的员工信息
select * from emp where ename in ('KING','SMITH','AA')
--求入职时间在20-2月-81之后的员工信息
select * from emp where hiredate < '23-5月 -87';

--------------------------------------------------------
--and or not的用法
--求薪水大于1000或者部门在20这个部门的员工信息
select * from emp where sal>1000 and deptno=20
--求薪水不是800或者不是1500或者不是3000的员工信息
select * from emp where sal not in (800,1500,3000)
--也可以这样来写
select * from emp where sal <>800 and sal <> 1500 and sal<>3000

--------------------------------------------------------
--like的用法
--求名字中包含ALL这三个字符的员工信息
select * from emp where ename like '%E%';
--求名字中的第二个字母是A的员工
select * from emp where ename like '_A%';
--特殊字符需要转义。比如:求员工中包含特殊字符%的员工信息
select * from emp where ename like '%\%%' escape '\'

--------------------------------------------------------
--null的用法
--求没有年终奖的员工
select * from emp where comm is null
--求有年终奖的员工
select * from emp where comm is not null


--------------------------------------------------------
--order by的用法
--员工信息按照姓名正序排列
select * from emp order by ename asc;
--员工信息按照倒叙排列
select * from emp order by ename desc;
--也可以是多个字段组合排列。例如:员工信息按照部门正序排列,并且按照姓名倒叙排列
select * from emp order by deptno asc,ename desc

--------------------------------------------------------
--function的用法
--把所有姓名变成小写
select lower(ename) from emp;
--把所有姓名变成大写
select upper(ename) from emp;
--求所有人名中包含'a'的员工信息不区分大小写
select * from emp where lower(ename) like '%a%'
--截取子字符串,比如求Hello的一部分
select substr('hello',2,2) from dual;
select substr(ename,2,2) from emp;
--求Hello的一部分,并指明长度

--求ascii码对应的字符
select chr(65) from dual
--求字符对应的ascii码
select ascii('中')from dual
--四舍五入
select round(12.456,2) from dual
select round(12.456,-1) from dual
--四舍五入小数点后面多少位

--四舍五入小数点前面多少位


--------------------------------------------------------
--important!日期转换函数
--------------------------------------------------------
--将当前日期转换成1981-03-12 12:00:00这种形式的字符串
select to_char(sysdate,'YYYY-MM-DD HH24:MI:SS') from dual;

--将1981-03-12 12:00:00字符串转换成日期
select to_date('1981-03-12 12:00:00','YYYY-MM-DD HH24:MI:SS') from dual;

--将每个人的薪水转换成固定格式的字符串
select to_char(sal,'$999,999,999.99') from emp;
--将固定格式的字符串转换成数值
select to_number('$8,000.00','$999,999,999.99') from dual;

--当null参与计算时候,可以用nvl这个函数。比如求每个人一年总共的收入
select ename,sal*12+comm from emp

--------------------------------------------------------
--group function组函数
--求所有人的薪水的总和,平均值,最大值,最小值
select sum(sal),avg(sal),max(sal) ,min(sal) from emp;
--求总的行数
select count(*) from emp;
--求总的行树,(可以指定具体的字段)但如果字段有null值的时候需要小心使用
select count(comm) from emp;

--也可以过滤掉重复的行之后统计行数

select count(distinct deptno) from emp
--可以指明按照哪个字段进行分组.比如;分部门统计最高薪水
select deptno,max(sal) from emp where deptno is not null group by deptno
--也可以按照多个字段来分组统计,比如:分部门和岗位,统计最高薪水和行数
select deptno,job,max(sal),count(*) from emp group by deptno,job

--------------------------------------------------------
--重要:出现在select列表中的字段,如果没有在组函数中,那么必须出现在group by 子句中。
--------------------------------------------------------
select ename,deptno,job,max(sal),count(*) from emp group by deptno,job

--求薪水最高的员工姓名
select * from emp where sal=(select max(sal) from emp);
delete from emp where ename='TEST2'
update emp set deptno=10 where deptno=99
select * from dept
insert into dept (deptno,dname,loc) values('10','ACCOUNTING','NEW YORK');
--having从句的用法
--求平均薪水是2000以上的部门
select deptno,avg(sal) as avg_sal from emp group by deptno
having avg(sal) >2000
--------------------------------------------------------
--总结一下select语法
select
from
where
group by
having
order by
--------------------------------------------------------
-- 执行顺序very important!
-- 首先执行where语句将原有记录过滤;
-- 第二执行group by 进行分组;
-- 第三执行having过滤分组;
-- 然后将select 中的字段值选出来;
-- 最后执行order by 进行排序;

--------------------------------------------------------
/*
按照部门分组统计,求最高薪水,平均薪水
只有薪水是1200以上的才参与统计
并且分组结果中只包括平均薪水在1500以上的部门
而且按照平均薪水倒叙排列
*/
select max(sal),avg(sal) from emp
where sal>1200
group by deptno
having avg(sal) >1500
order by avg(sal) desc

--------------------------------------------------------
/*
把雇员按部门分组,
求最高薪水, 部门号,
过滤掉名字中第二个字母是'A'的,
要求分组后的平均薪水>1500,
按照部门编号倒序排列
*/
select max(sal) ,deptno from emp where ename not like '_A%'group by deptno
having avg(sal) >1500
order by deptno desc

/* very very important! */
select ename, deptno from emp;
select deptno, dname from dept;
--------------------------------------------------------------------------------------
--老语法:----------------------------------------------------------------------------
--------------------------------------------------------------------------------------
--等值连接:求员工姓名以及员工所在部门的名字同时显示出来
select ename,emp.deptno,dname,dept.deptno from emp,dept
where emp.deptno = dept.deptno

select ename,e.deptno,dname,d.deptno from emp e,dept d
where e.deptno = d.deptno

--非等值连接:要求每位雇员的薪水等级
select * from salgrade
select ename,sal,grade,losal,hisal from emp,salgrade
where sal >=losal and sal <=hisal
--跨3个表:求工作职位是’PRESIDENT’的雇员姓名,部门名称和薪水等级时
select ename,dname,grade from emp,dept,salgrade
where emp.deptno = dept.deptno
and sal >=losal and sal <=hisal
and job ='PRESIDENT'


--也可以同一个表做跨表连接:求每位员工的姓名,及其上级经理的姓名

select e1.ename,e2.ename from emp e1,emp e2
where e1.mgr = e2.empno

--------------------------------------------------------------------------------------
--新语法------------------------------------------------------------------------------
--在SQL1992的语法规则中,语句过滤的条件和表连接的条件都被放在了where子句中,当条件过多时,容易造成混淆,
--SQL1999修正了这个缺点,将连接条件和数据过滤条件区分开来,
--------------------------------------------------------------------------------------

--交叉连接
--结果会产生这两张表的笛卡尔乘积

select * from emp cross join dept
--要用deptno作为等值连接条件,我们可以这样写
select * from emp join dept using (deptno)
select ename, dname from emp join dept using(deptno);
--相当于
select ename, dname from emp join dept on emp.deptno = dept.deptno
--也可以写成这样


--也可以用于非等值连接
--求每位雇员的薪水等级
select * from emp join salgrade on (sal >=losal and sal<= hisal)


--多个join,where组合使用
--(求工作职位是’PRESIDENT’的雇员姓名,部门名称和薪水等级时)

select * from emp join dept on emp.deptno = dept.deptno
join salgrade on (sal >=losal and sal<= hisal)
where job = 'PRESIDENT'


--外连接--取出表中连接不到一起的多余的数据
--没有全内连接,没有右内连接

--其中outer也可以省略,简写为left join , right join , full join
--left inner join可以缩写成inner join 也可以缩写成join,意思是左内。
--update emp set deptno=20 where ename='SMITH';
--commit;
select * from emp;
select * from dept;
delete from dept where deptno=99;
--左内,从左往右找,匹配不上的记录不显示
select ename,emp.deptno from emp  join dept on emp.deptno = dept.deptno;
select ename,emp.deptno from emp  inner join dept on emp.deptno = dept.deptno;
--没有这种语法:select ename,emp.deptno from emp  left inner join dept on emp.deptno = dept.deptno;
--左外连接,从左往右找,匹配不上的记录也显示一行
select ename,dept.deptno from emp left /*outer*/ join dept on emp.deptno = dept.deptno;

--右外连接,从右往左找,匹配不上的记录,也显示一行
select ename,dept.deptno from emp right /*outer*/ join dept on emp.deptno = dept.deptno;
--没有右内连接:select ename,dept.deptno from emp right inner join dept on emp.deptno = dept.deptno;

--全外连接
select ename,dept.deptno from emp full /*outer*/ join dept on emp.deptno = dept.deptno;

--左外,右外的区别






--什么时候用外连接呢?比如领导向你要所有学生的列表,顺便把所属的班级也列出来,就需要外连接

--在Where语句中使用子查询
-----------------------------------------------------------------
--雇员中最高薪水的人员名称
--1,先求出最高薪水
--2,再求雇员中最高薪水的人员名称
select ename from emp where sal=(select max(sal) from emp)



--有哪些人的薪水是在整个雇员的平均薪水之上的
select ename,sal from emp where sal >(select avg(sal) from emp)

-----------------------------------------------------------------
--雇员中哪些人是经理人
--1,首先查询mgr中有哪些号码
--2,再看有哪些人员的号码在此出现
select distinct mgr from emp where mgr is not null order by mgr

select ename
  from emp
where empno in (select distinct mgr from emp where mgr is not null )
--where in 中不让写orderby
select ename
  from emp
where empno in (select distinct mgr from emp where mgr is not null order by mgr)

-----------------------------------------------------------------
--在From子句中使用子查询
------------------------------------------------------------------

--部门平均薪水的等级
--1,首先将每个部门的平均薪水求出来
--2,然后把结果当成一张表,再用这张结果表和salgrade表做连接,以此求得薪水等级
select deptno,avg(sal) from emp group by deptno
select * from (select deptno,avg(sal) avg_sal from emp group by deptno) t join salgrade
on avg_sal between losal and hisal;


-----------------------------------------------------------------
--每个部门最高薪水的人员名称
--1,首先将每个部门的最高薪水求出来
--2,然后把结果当成一张表,再用emp和这张结果表做连接,以此求得每个部门最高薪水的人员名称

select deptno,max(sal) from emp where deptno is not null group by deptno

select ename from emp e join
(select deptno,max(sal) max_sal from emp where deptno is not null group by deptno ) t
on sal = max_sal and e.deptno = t.deptno

-----------------------------------------------------------------
--哪些人的薪水在部门的平均薪水之上
--1,首先将每个部门的平均薪水求出来
--2,然后把结果当成一张表,再用emp和这张结果表做连接,以此求得哪些人的薪水在部门的平均薪水之上
select deptno,avg(sal) avg_sal from emp group by deptno

select * from emp join (select deptno,avg(sal) avg_sal from emp group by deptno)t
on (sal>avg_sal and emp.deptno=t.deptno)
-----------------------------------------------------------------
--求部门中(所有人的)平均的薪水等级,形式如:
--  deptno  avg_grade
--  10      3.67
--  20      2.8
--  30      2.5
--1,先求每个人的薪水等级
--2,再按照部门分组,求平均数
select deptno,sal,grade from emp join salgrade on sal between losal and hisal
select deptno,avg(grade) from (select deptno,sal,grade from emp join salgrade on sal between losal and hisal)t group by deptno




------------------------------------------------------------------------------------------
--使用伪字段:rownum,----------------------
------------------------------------------------------------------------------------------
--用来标识每条记录的行号,行号从1开始,每次递增1
select rownum,emp.* from emp;
--oracle下rownum只能使用 < <=, 不能使用 = > >= 等比较操作符,
select rownum,emp.* from emp where rownum<5;
--当rownum和order by 一起使用时,会首先选出符合rownum条件的记录,然后再排序
--(错误的写法)例如,当我们要求薪水最高的前5个人时,最直接的想法可以这样写:
select * from emp where rownum<5 order by sal desc
--(正确的写法)可以这样写

select * from
(select * from emp order by sal desc) t
where rownum<=5




--------------------------------------------------------
--不准用组函数(即MAX()),求薪水的最高值(面试题)
--第一种解决办法:
--1,先把所有薪水按照倒序排列
--2,再取第一行
select * from
(select sal from emp order by sal desc) t
where rownum=1



--第二种解决办法:
--1,先跨表查询自己,先求出的结果中,e1.sal不可能出现最大数
--2,然后再not in
select e2.sal from emp e1,emp e2 where e1.sal>e2.sal
select sal from emp where sal not in(select e2.sal from emp e1,emp e2 where e1.sal>e2.sal)


-----------------------------------------------------------------
--求平均薪水最高的部门的部门编号
--第一种解决办法:
--1,先求出每个部门的平均薪水,
select deptno,avg(sal) avg_sal from emp group by deptno
--2,再求每个部门的平均薪水的最高值,
select max(avg_sal) from (1111111111111111111111111)
--3,最后再求第一步结果中avg_sal = 最高薪水的记录.

select deptno from (111111111111) where avg_sal = (22222222)


select deptno
  from (select deptno,avg(sal) avg_sal from emp group by deptno)
where avg_sal =
       (select max(avg_sal)
          from (select deptno,avg(sal) avg_sal from emp group by deptno))


--没法考虑并列第一的情况
select deptno from
(select deptno,avg(sal) avg_sal from emp group by deptno order by avg(sal) desc)
where rownum<=1


--第二种解决办法:
--1,将上面的第一步第二步合并,先求最高平均薪水,用max(avg(sal))的办法
--不能写成select deptno,max(avg(sal)) from emp group by deptno
select max(avg(sal)) from emp group by deptno
--2,求出每个部门的平均薪水
select deptno,avg(sal) avg_sal from emp group by deptno
--3,最后再求第二步结果中(即每个部门的平均薪水),avg_sal = (第一步结果)的记录.即avg_sal =最高薪水的记录.
select deptno from (select deptno,avg(sal) avg_sal from emp group by deptno)
where avg_sal =(select max(avg(sal)) from emp group by deptno)


--第三种解决办法:
--1,先求出每个部门的平均薪水,
select avg(sal) avg_sal from emp group by deptno
--2,求最高平均薪水,用max(avg(sal))的办法
select max(avg(sal)) from emp group by deptno
--3,再使用having语句, avg(sal) = 第二步的结果
注意:为组函数起的别名在having中不能用

select deptno from emp group by deptno
having avg(sal) = (select max(avg(sal)) from emp group by deptno)



-----------------------------------------------------------------
--求平均薪水最高的部门的部门名称
--1,部门平均最高薪水
--2,得到部门编号列表,注意用group by deptno
--3,再应用having子句, having avg(sal) = (第一步的结果)
--4,得到平均最高薪水的那个部门的编号
--5,再得到部门名称



select dname from dept where deptno in

(
select deptno
  from (select deptno,avg(sal) avg_sal from emp group by deptno)
where avg_sal =
       (select max(avg_sal)
          from (select deptno,avg(sal) avg_sal from emp group by deptno))

)

-----------------------------------------------------------------
--求平均薪水的等级最低的部门的部门名称
--第一步:部门平均薪水的等级,分成两个小步骤,第一小步是求部门平均薪水

select * from
(select deptno,avg(sal) avg_sal from emp group by deptno) t
join salgrade on avg_sal between losal and hisal

--第二步:最低的等级值

select min(grade) from (1111111111111111111111111)

--第三步:等于最低值的部门编号
------------有错误,应该是grade=
select deptno from (111111111111) where grade = (22222222222222)
--第四步:求名称
select dname from dept where deptno in(33333333333)




select dname
  from dept
where deptno in
       (
       
        select deptno
          from (select *
                   from (select deptno, avg(sal) avg_sal
                           from emp
                          group by deptno) t
                   join salgrade on avg_sal between losal and hisal)
         where grade =
               (select min(grade)
                  from (select *
                          from (select deptno, avg(sal) avg_sal
                                  from emp
                                 group by deptno) t
                          join salgrade on avg_sal between losal and hisal)))









--也可以用视图的方式来解决
--conn sys/bjsxt as sysdba
--grant create table, create view, create sequence to scott
--根据第一步的结果,建立一个view

create or replace view v1 as
--必须明确定义列
select deptno, avg_sal, grade from
(select deptno,avg(sal) avg_sal from emp group by deptno) t
join salgrade on avg_sal between losal and hisal


--查看一下
select * from v1
--查询一下

--带入view

select dname from dept where deptno in
(select deptno from (v1) where grade = (select min(grade) from v1))








-------------------------------------------------------------
--为什么in的后面不能order by ?







---------------------------------------------------------------
--求部门经理人中平均薪水最低的部门名称 (思考题)
第一步,求部门经理的雇员编号
select distinct mgr from emp where mgr is not null
第二步,按部门统计,求部门经理的平均薪水
select deptno,avg(sal) avg_sal from emp where empno in (select distinct mgr from emp where mgr is not null)group by deptno
第三步,求最低值
select min(avg(sal)) from emp where empno in (select distinct mgr from emp where mgr is not null)group by deptno
第四步,求部门经理人中平均薪水最低的部门名称
select deptno from (2222222222222) where avg_sal =(333333333333333333333333)


select dname from dept where deptno in (select deptno from (select deptno,avg(sal) avg_sal from emp where empno in (select distinct mgr from emp where mgr is not null)group by deptno) where avg_sal =(select min(avg(sal)) from emp where empno in (select distinct mgr from emp where mgr is not null)group by deptno))
----------------------------------------------------------------------------
--求比普通员工的最高薪水还要高的经理人名称
--1,求所有经理的编号
create or replace view v1 as
select distinct mgr from emp where mgr is not null
select * from v1
--2,普通员工的最高薪水
select max(sal) from emp where empno not in (select distinct mgr from emp where mgr is not null)
--3,

select ename from emp where empno in (select * from v1)
and sal > (select max(sal) from emp where empno not in (select distinct mgr from emp where mgr is not null))

--即:


select ename from emp where empno in (select distinct mgr from emp where mgr is not null)
and sal > (select max(sal) from emp where empno not in (select distinct mgr from emp where mgr is not null))








------------------------------------------------------------------------------
--求薪水最高的前5名雇员
--1,先观察一下

--2,看看rownum的作用

--3,不是我们想要的结果
select ename,sal  from emp where rownum<=5 order by sal desc
--4,先order by,再rownum
select * from
(select ename,sal  from emp  order by sal desc ) t
where rownum<=5

--------------------------------------------------------------------------------
--求薪水最高的第6到第10名雇员(重点掌握)
--这种没法实现,oracle下rownum只能使用 < <=, 不能使用 = > >= 等比较操作符
--注意里面的rownum和外面的rownum的区别,外面要想访问里面的rownum,必须取得一个别名。

select * from
(select ename,sal  from emp  order by sal desc ) t
where rownum>=5
and rownum<=10

--所以再套一层select
select * from
(select t.*,rownum r from
(select ename,sal  from emp  order by sal desc ) t
)
where r>=5
and r<=10

--还有一种排序方式
select * from
(select * from emp  order by sal desc)where rownum<=10
minus
select * from
(select * from emp  order by sal desc)where rownum<=5
--------------------------------------------------------------------
--练习: 求最后入职的5名员工
--1,每个人的入职时间

--2,取前5行





-----------------------------------------------------------------
--求每个部门中薪水最高的前两名雇员

--1,每个员工的姓名,部门,工资,按部门和工资(倒序)排列
select ename,deptno,sal from emp order by deptno,sal desc
--2,套一层,加上个r
select ename,deptno,sal,rownum r from
(select ename,deptno,sal from emp order by deptno,sal desc) t
--3,创建试图
create or replace view v1
as
select ename,deptno,sal,rownum r from
(select ename,deptno,sal from emp order by deptno,sal desc) t
--观察一下
select * from v1

--每个部门中,薪水最高的第一行,并创建试图
create or replace view v2 as
select deptno,min(r) min_r from v1 group by deptno

--两个view跨表连接,大于薪水最高的行数,小于最高的行数+1,并且部门编号要匹配
select ename from v1 join v2
on ( v1.deptno = v2.deptno and v1.r >=v2.min_r and v1.r<=v2.min_r+1)


-------------------------------------------------------------------------------
--面试题: 比较效率
  select * from emp where deptno = 10 and ename like '%A%';
  select * from emp where ename like '%A%' and deptno = 10;
 
---------------------------------------------------------
--使用union、minus

--使用union、minus可以用来实现结果集的合并和去除(可以理解为加和减),例如:
select * from emp where deptno=10
union
select * from emp where deptno=20;

--相当于
select * from emp where deptno=10 or deptno=20

--而下面的语句
select * from emp where deptno in (10,20)
minus
select * from emp where sal < 1500;
--相当于
select * from emp where deptno in(10,20) and sal>=1500

--求分段显示薪水的个数



如:
scale      total
<800    0
801-1000 2
1001-2000 3
2001-5000 6
>5000    8

select '<800' as scale ,count(*) as total from emp where sal<800
union
select '<801-1000' as scale ,count(*) as total from emp where sal<=1000 and sal>=801



--或者显示成为
--注意:使用between .. and .. 的时候,包含了最大和最小值。
800-1000 1001-2000 2001-5000
2       3       6
select * from
(select count(*) as "800-1000" from emp where sal >=800 and sal <= 1000),
(select count(*) as "1001-2000" from emp where sal >=1001 and sal <= 2000),
(select count(*) as "2001-5000" from emp where sal >=2001 and sal <= 5000)



--或显示成为
DEPTNO   800-2000  2001-5000
------ ---------- ----------
    30          5          1
    20          2          3
    10          1          2
select t.deptno,"800-2000","2001-5000" from
(select deptno,count(*) as "800-2000" from emp where sal between 800 and 2000 group by deptno) t
join
(select deptno,count(*) as "2001-5000" from emp where sal between 2001 and 5000 group by deptno) t1
on t.deptno = t1.deptno








  
-----------------------------------------------------------------------------------
--每个薪水等级有多少名雇员 ?
--1,先求出每个雇员的薪水等级

--2,再group一下
posted @ 2009-04-13 10:40 lanxin1020 阅读(180) | 评论 (0)编辑 收藏
(转)Hibernate支持两种锁机制:
即通常所说的“悲观锁(Pessimistic Locking)”和
“乐观锁(OptimisticLocking)”。
悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
Hibernate的加锁模式有:
Ø LockMode.NONE : 无锁机制。
Ø LockMode.WRITE :Hibernate在Insert和Update记录的时候会自动
获取。
Ø LockMode.READ : Hibernate在读取记录的时候会自动获取。
以上这三种锁机制一般由Hibernate内部使用,如Hibernate为了保证Update
过程中对象不会被外界修改,会在save方法实现中自动为目标对象加上WRITE锁。
Ø LockMode.UPGRADE :利用数据库的for update子句加锁。
Ø LockMode. UPGRADE_NOWAIT :Oracle的特定实现,利用Oracle的for
update nowait子句实现加锁。
乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个“version”字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
悲观锁与乐观锁的比较:
悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受;
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在
系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。
Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,利用Hibernate提供的透明化乐观锁实现,将大大提升我们的生产力。
Hibernate中可以通过class描述符的optimistic-lock属性结合version描述符指定。
optimistic-lock属性有如下可选取值:
Ø none
无乐观锁
Ø version
通过版本机制实现乐观锁
Ø dirty
通过检查发生变动过的属性实现乐观锁
Ø all
通过检查所有属性实现乐观锁
其中通过version实现的乐观锁机制是Hibernate官方推荐的乐观锁实现,同时也是Hibernate中,目前唯一在数据对象脱离 Session发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都选择version方式作为Hibernate乐观锁实现机制。
posted @ 2009-04-12 16:12 lanxin1020 阅读(169) | 评论 (0)编辑 收藏

使用Hibernate自带的工具hbm2ddl,建立根据你的对象建立数据库:

首先建好POJO object, XML Mapping File(也可以使用工具根据POJO class建立),配置文件(hibernate.cfg.xml)

Java代码


import org.hibernate.cfg.Configuration;
import org.hibernate.tool.hbm2ddl.SchemaExport;

public class SchemaUtil {
public static void main(String[] args) {

Configuration cfg = new Configuration().configure(); SchemaExport schemaExport= new SchemaExport(cfg); schemaExport.create(false, true); } }
posted @ 2009-04-11 16:31 lanxin1020 阅读(149) | 评论 (0)编辑 收藏

在您撰寫好*.hbm.xml映射文件之後,您可以使用 net.sf.hibernate.tool.hbm2ddl.SchemaExportTask來自動建立資料庫表格,這邊所使用的方式是結合Ant進行自動化建構,首先我們假設將使用以下的User.hbm.xml:

User.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping
PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
    "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>
<class name="onlyfun.caterpillar.User" table="USER">
<id name="id" type="string" unsaved-value="null">
<column name="user_id" sql-type="char(32)"/>
<generator class="uuid.hex"/>
</id>
<property name="name" type="string" not-null="true">
<column name="name" length="16" not-null="true"/>
</property>
<property name="sex" type="char" />
<property name="age" type="int"/>
</class>
</hibernate-mapping>

 在這個映射文件中,<column/>標籤用於指定建立表格時的一些資訊,例如映射的表格欄位名稱,或是sql-type或 length等屬性,如果不指定這些資訊時,SchemaExportTask將自動使用Hibernate的類型至SQL類型等資訊來建立表格;sql -type用於指定表格欄位型態,not-null表示欄位不能為null,length則用於指定表格文字欄位長度,這些屬性的說明,都可以在 Hibernate參考手冊的表15.1找到。

 下面的build.xml用於Ant自動化建構時,生成資料庫表格之用:

build.xml
<project name="Hibernate" default="schema" basedir=".">
<property name="source.root" value="src"/>
<property name="class.root" value="classes"/>
<property name="lib.dir" value="lib"/>
<property name="data.dir" value="data"/>
<path id="project.class.path">
<!-- Include our own classes, of course -->
<pathelement location="${class.root}" />
<!-- Include jars in the project library directory -->
<fileset dir="${lib.dir}">
<include name="*.jar"/>
</fileset>
<pathelement path ="${classpath}"/>
</path>
<target name="schema" description="Generate DB schema from the O/R mapping files">
<!-- Teach Ant how to use Hibernate's schema generation tool -->
<taskdef name="schemaexport"
classname="net.sf.hibernate.tool.hbm2ddl.SchemaExportTask"
classpathref="project.class.path"/>
<schemaexport properties="${source.root}/hibernate.properties"
quiet="no" text="no" drop="no" delimiter=";">
<fileset dir="${source.root}">
<include name="**/*.hbm.xml"/>
</fileset>
</schemaexport>
</target>
</project>

 <taskdef/>標籤定義一個新的任務schemaexport,相關的屬性設定是根據參考手冊的建議設定的,我們在這邊使用 hibernate.properties來告訴SchemaExportTask相關的JDBC資訊,quiet、text等屬性的定義,可以看參考手冊的表15.2。

 這個Ant建構檔案,會找尋src目錄下包括子目錄中有的*.hbm.xml,並自動根據映射資訊建立表格,我們還必須提供hibernate.properties(置於src下)來告知JDBC連接的相關訊息:

hibernate.properties
hibernate.dialect=net.sf.hibernate.dialect.MySQLDialect
hibernate.connection.driver_class=com.mysql.jdbc.Driver
hibernate.connection.url=jdbc:mysql://localhost/HibernateTest
hibernate.connection.username=caterpillar
hibernate.connection.password=123456

 這邊使用的是MySQL,請實際根據您所使用的資料庫設定dialect、驅動程式等資訊,在開始運行Ant使用SchemaExportTask進行自動表格建立之前,您要先建立資料庫,這邊的例子則是在MySQL中先建立HibernateTest:

mysql> create database HibernateTest;
Query OK, 1 row affected (0.03 sec)

 接著就可以運行Ant了,執行結果如下:

ant
Buildfile: build.xml
schema:
[schemaexport] log4j:WARN No appenders could be found for logger (net.sf.hiberna
te.cfg.Environment).
[schemaexport] log4j:WARN Please initialize the log4j system properly.
[schemaexport] drop table if exists USER;
[schemaexport] create table USER (
[schemaexport]    user_id char(32) not null,
[schemaexport]    name varchar(16) not null,
[schemaexport]    sex char(1),
[schemaexport]    age integer,
[schemaexport]    primary key (user_id)
[schemaexport] );
BUILD SUCCESSFUL
Total time: 5 seconds

 運行的過程中,我們可以看到建立表格的SQL語句,而自動建立好的資料庫表格資訊如下:

mysql> DESCRIBE user;
+---------+-------------+------+-----+---------+-------+
| Field   | Type        | Null | Key | Default | Extra |
+---------+-------------+------+-----+---------+-------+
| user_id | varchar(32) |      | PRI |         |       |
| name    | varchar(16) |      |     |         |       |
| sex     | char(1)     | YES  |     | NULL    |       |
| age     | int(11)     | YES  |     | NULL    |       |
+---------+-------------+------+-----+---------+-------+
4 rows in set (0.04 sec)

更多有關SchemaExportTask的資訊,可以看看參考手冊的第15章工具箱指南的部份。

posted @ 2009-04-11 11:37 lanxin1020 阅读(245) | 评论 (0)编辑 收藏
延迟加载:
  延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。在Hibernate中提供了对实体对象的延迟加载以及对集合的延迟加载,另外在Hibernate3中还提供了对属性的延迟加载。下面我们就分别介绍这些种类的延迟加载的细节。
A、实体对象的延迟加载:
如果想对实体对象使用延迟加载,必须要在实体的映射配置文件中进行相应的配置,如下所示:
<hibernate-mapping>
<class name=”com.neusoft.entity.User” table=”user” lazy=”true”>
   ……
</class>
</hibernate-mapping>
通过将class的lazy属性设置为true,来开启实体的延迟加载特性。如果我们运行下面的代码:
User user=(User)session.load(User.class,”1”);(1)
System.out.println(user.getName());(2)
当运行到(1)处时,Hibernate并没有发起对数据的查询,如果我们此时通过一些调试工具(比如JBuilder2005的Debug工具),观察此时user对象的内存快照,我们会惊奇的发现,此时返回的可能是User$EnhancerByCGLIB$$bede8986类型的对象,而且其属性为null,这是怎么回事?还记得前面我曾讲过session.load()方法,会返回实体对象的代理类对象,这里所返回的对象类型就是User对象的代理类对象。在Hibernate中通过使用CGLIB,来实现动态构造一个目标对象的代理类对象,并且在代理类对象中包含目标对象的所有属性和方法,而且所有属性均被赋值为null。通过调试器显示的内存快照,我们可以看出此时真正的User对象,是包含在代理对象的CGLIB$CALBACK_0.target属性中,当代码运行到(2)处时,此时调用user.getName()方法,这时通过CGLIB赋予的回调机制,实际上调用CGLIB$CALBACK_0.getName()方法,当调用该方法时,Hibernate会首先检查CGLIB$CALBACK_0.target属性是否为null,如果不为空,则调用目标对象的getName方法,如果为空,则会发起数据库查询,生成类似这样的SQL语句:select * from user where id=’1’;来查询数据,并构造目标对象,并且将它赋值到CGLIB$CALBACK_0.target属性中。
   这样,通过一个中间代理对象,Hibernate实现了实体的延迟加载,只有当用户真正发起获得实体对象属性的动作时,才真正会发起数据库查询操作。所以实体的延迟加载是用通过中间代理类完成的,所以只有session.load()方法才会利用实体延迟加载,因为只有session.load()方法才会返回实体类的代理类对象。
B、        集合类型的延迟加载:
在Hibernate的延迟加载机制中,针对集合类型的应用,意义是最为重大的,因为这有可能使性能得到大幅度的提高,为此Hibernate进行了大量的努力,其中包括对JDK Collection的独立实现,我们在一对多关联中,定义的用来容纳关联对象的Set集合,并不是java.util.Set类型或其子类型,而是net.sf.hibernate.collection.Set类型,通过使用自定义集合类的实现,Hibernate实现了集合类型的延迟加载。为了对集合类型使用延迟加载,我们必须如下配置我们的实体类的关于关联的部分:
<hibernate-mapping>
   <class name=”com.neusoft.entity.User” table=”user”>
…..
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
<key column=”user_id”/>
<one-to-many class=”com.neusoft.entity.Arrderss”/>
</set>
   </class>
</hibernate-mapping>
通过将<set>元素的lazy属性设置为true来开启集合类型的延迟加载特性。我们看下面的代码:
User user=(User)session.load(User.class,”1”);
Collection addset=user.getAddresses();      (1)
Iterator it=addset.iterator();               (2)
while(it.hasNext()){
Address address=(Address)it.next();
System.out.println(address.getAddress());
}
当程序执行到(1)处时,这时并不会发起对关联数据的查询来加载关联数据,只有运行到(2)处时,真正的数据读取操作才会开始,这时Hibernate会根据缓存中符合条件的数据索引,来查找符合条件的实体对象。
这里我们引入了一个全新的概念——数据索引,下面我们首先将接一下什么是数据索引。在Hibernate中对集合类型进行缓存时,是分两部分进行缓存的,首先缓存集合中所有实体的id列表,然后缓存实体对象,这些实体对象的id列表,就是所谓的数据索引。当查找数据索引时,如果没有找到对应的数据索引,这时就会一条select SQL的执行,获得符合条件的数据,并构造实体对象集合和数据索引,然后返回实体对象的集合,并且将实体对象和数据索引纳入Hibernate的缓存之中。另一方面,如果找到对应的数据索引,则从数据索引中取出id列表,然后根据id在缓存中查找对应的实体,如果找到就从缓存中返回,如果没有找到,在发起select SQL查询。在这里我们看出了另外一个问题,这个问题可能会对性能产生影响,这就是集合类型的缓存策略。如果我们如下配置集合类型:
<hibernate-mapping>
   <class name=”com.neusoft.entity.User” table=”user”>
…..
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
<cache usage=”read-only”/><key column=”user_id”/>
<one-to-many class=”com.neusoft.entity.Arrderss”/>
</set>
   </class>
</hibernate-mapping>
这里我们应用了<cache usage=”read-only”/>配置,如果采用这种策略来配置集合类型,Hibernate将只会对数据索引进行缓存,而不会对集合中的实体对象进行缓存。如上配置我们运行下面的代码:
User user=(User)session.load(User.class,”1”);
Collection addset=user.getAddresses();     
Iterator it=addset.iterator();              
while(it.hasNext()){
Address address=(Address)it.next();
System.out.println(address.getAddress());
}
System.out.println(“Second query……”);
User user2=(User)session.load(User.class,”1”);
Collection it2=user2.getAddresses();
while(it2.hasNext()){
Address address2=(Address)it2.next();
System.out.println(address2.getAddress());
}
运行这段代码,会得到类似下面的输出:
Select * from user where id=’1’;
Select * from address where user_id=’1’;
Tianjin
Dalian
Second query……
Select * from address where id=’1’;
Select * from address where id=’2’;
Tianjin
Dalian
我们看到,当第二次执行查询时,执行了两条对address表的查询操作,为什么会这样?这是因为当第一次加载实体后,根据集合类型缓存策略的配置,只对集合数据索引进行了缓存,而并没有对集合中的实体对象进行缓存,所以在第二次再次加载实体时,Hibernate找到了对应实体的数据索引,但是根据数据索引,却无法在缓存中找到对应的实体,所以Hibernate根据找到的数据索引发起了两条select SQL的查询操作,这里造成了对性能的浪费,怎样才能避免这种情况呢?我们必须对集合类型中的实体也指定缓存策略,所以我们要如下对集合类型进行配置:
<hibernate-mapping>
   <class name=”com.neusoft.entity.User” table=”user”>
…..
<set name=”addresses” table=”address” lazy=”true” inverse=”true”>
<cache usage=”read-write”/><key column=”user_id”/>
<one-to-many class=”com.neusoft.entity.Arrderss”/>
</set>
   </class>
</hibernate-mapping>
此时Hibernate会对集合类型中的实体也进行缓存,如果根据这个配置再次运行上面的代码,将会得到类似如下的输出:
Select * from user where id=’1’;
Select * from address where user_id=’1’;
Tianjin
Dalian
Second query……
Tianjin
Dalian
这时将不会再有根据数据索引进行查询的SQL语句,因为此时可以直接从缓存中获得集合类型中存放的实体对象。
C、       属性延迟加载:
  在Hibernate3中,引入了一种新的特性——属性的延迟加载,这个机制又为获取高性能查询提供了有力的工具。在前面我们讲大数据对象读取时,在User对象中有一个resume字段,该字段是一个java.sql.Clob类型,包含了用户的简历信息,当我们加载该对象时,我们不得不每一次都要加载这个字段,而不论我们是否真的需要它,而且这种大数据对象的读取本身会带来很大的性能开销。在Hibernate2中,我们只有通过我们前面讲过的面性能的粒度细分,来分解User类,来解决这个问题(请参照那一节的论述),但是在Hibernate3中,我们可以通过属性延迟加载机制,来使我们获得只有当我们真正需要操作这个字段时,才去读取这个字段数据的能力,为此我们必须如下配置我们的实体类:
<hibernate-mapping>
<class name=”com.neusoft.entity.User” table=”user”>
……
<property name=”resume” type=”java.sql.Clob” column=”resume” lazy=”true”/>   </class>
</hibernate-mapping>
通过对<property>元素的lazy属性设置true来开启属性的延迟加载,在Hibernate3中为了实现属性的延迟加载,使用了类增强器来对实体类的Class文件进行强化处理,通过增强器的增强,将CGLIB的回调机制逻辑,加入实体类,这里我们可以看出属性的延迟加载,还是通过CGLIB来实现的。CGLIB是Apache的一个开源工程,这个类库可以操纵java类的字节码,根据字节码来动态构造符合要求的类对象。根据上面的配置我们运行下面的代码:
String sql=”from User user where user.name=’zx’ ”;
Query query=session.createQuery(sql);   (1)
List list=query.list();
for(int i=0;i<list.size();i++){
User user=(User)list.get(i);
System.out.println(user.getName());
System.out.println(user.getResume());   (2)
}
当执行到(1)处时,会生成类似如下的SQL语句:
Select id,age,name from user where name=’zx’;
这时Hibernate会检索User实体中所有非延迟加载属性对应的字段数据,当执行到(2)处时,会生成类似如下的SQL语句:
Select resume from user where id=’1’;
这时会发起对resume字段数据真正的读取操作。
posted @ 2009-04-11 10:35 lanxin1020 阅读(107) | 评论 (0)编辑 收藏
1.1.1.         基本的缓存原理
Hibernate缓存分为二级,第一级存放于session中称为一级缓存,默认带有且不能卸载。



第二级是由sessionFactory控制的进程级缓存。是全局共享的缓存,凡是会调用二级缓存的查询方法 都

会从中受益。只有经正确的配置后二级缓存才会发挥作用。同时在进行条件查询时必须使用相应的方法

才能从缓存中获取数据。比如Query.iterate()方法、load、get方法等。必须注意的是session.find方

法永远是从数据库中获取数据,不会从二级缓存中获取数据,即便其中有其所需要的数据也是如此。



查询时使用缓存的实现过程为:首先查询一级缓存中是否具有需要的数据,如果没有,查询二级缓存,

如果二级缓存中也没有,此时再执行查询数据库的工作。要注意的是:此3种方式的查询速度是依次降低

的。

1.2.   存在的问题
1.2.1.      一级缓存的问题以及使用二级缓存的原因
     因为Session的生命期往往很短,存在于Session内部的第一级最快缓存的生命期当然也很短,所以

第一级缓存的命中率是很低的。其对系统性能的改善也是很有限的。当然,这个Session内部缓存的主要

作用是保持Session内部数据状态同步。并非是hibernate为了大幅提高系统性能所提供的。

为了提高使用hibernate的性能,除了常规的一些需要注意的方法比如:

使用延迟加载、迫切外连接、查询过滤等以外,还需要配置hibernate的二级缓存。其对系统整体性能的

改善往往具有立竿见影的效果!

(经过自己以前作项目的经验,一般会有3~4倍的性能提高)



1.2.2.      N+1次查询的问题
执行条件查询时,iterate()方法具有著名的 “n+1”次查询的问题,也就是说在第一次查询时

iterate方法会执行满足条件的查询结果数再加一次(n+1)的查询。但是此问题只存在于第一次查询时

,在后面执行相同查询时性能会得到极大的改善。此方法适合于查询数据量较大的业务数据。

但是注意:当数据量特别大时(比如流水线数据等)需要针对此持久化对象配置其具体的缓存策略,比

如设置其存在于缓存中的最大记录数、缓存存在的时间等参数,以避免系统将大量的数据同时装载入内

存中引起内存资源的迅速耗尽,反而降低系统的性能!!!



1.3.   使用hibernate二级缓存的其他注意事项:
1.3.1.      关于数据的有效性
另外,hibernate会自行维护二级缓存中的数据,以保证缓存中的数据和数据库中的真实数据的一致性!

无论何时,当你调用save()、update()或 saveOrUpdate()方法传递一个对象时,或使用load()、 get()

、list()、iterate() 或scroll()方法获得一个对象时, 该对象都将被加入到Session的内部缓存中。

当随后flush()方法被调用时,对象的状态会和数据库取得同步。



也就是说删除、更新、增加数据的时候,同时更新缓存。当然这也包括二级缓存!



只要是调用hibernate API执行数据库相关的工作。hibernate都会为你自动保证 缓存数据的有效性!!



但是,如果你使用了JDBC绕过hibernate直接执行对数据库的操作。此时,Hibernate不会/也不可能自行

感知到数据库被进行的变化改动,也就不能再保证缓存中数据的有效性!!



这也是所有的ORM产品共同具有的问题。幸运的是,Hibernate为我们暴露了Cache的清除方法,这给我们

提供了一个手动保证数据有效性的机会!!

一级缓存,二级缓存都有相应的清除方法。



其中二级缓存提供的清除方法为:

按对象class清空缓存

                按对象class和对象的主键id清空缓存

                清空对象的集合中的缓存数据等。

   

1.3.2.      适合使用的情况
并非所有的情况都适合于使用二级缓存,需要根据具体情况来决定。同时可以针对某一个持久化对象配

置其具体的缓存策略。



适合于使用二级缓存的情况:

1、数据不会被第三方修改;



一般情况下,会被hibernate以外修改的数据最好不要配置二级缓存,以免引起不一致的数据。但是如果

此数据因为性能的原因需要被缓存,同时又有可能被第3方比如SQL修改,也可以为其配置二级缓存。只

是此时需要在sql执行修改后手动调用cache的清除方法。以保证数据的一致性



  2、数据大小在可接收范围之内;



     如果数据表数据量特别巨大,此时不适合于二级缓存。原因是缓存的数据量过大可能会引起内存资

源紧张,反而降低性能。



如果数据表数据量特别巨大,但是经常使用的往往只是较新的那部分数据。此时,也可为其配置二级缓

存。但是必须单独配置其持久化类的缓存策略,比如最大缓存数、缓存过期时间等,将这些参数降低至

一个合理的范围(太高会引起内存资源紧张,太低了缓存的意义不大)。



  3、数据更新频率低;



     对于数据更新频率过高的数据,频繁同步缓存中数据的代价可能和 查询缓存中的数据从中获得的

好处相当,坏处益处相抵消。此时缓存的意义也不大。





  4、非关键数据(不是财务数据等)



  财务数据等是非常重要的数据,绝对不允许出现或使用无效的数据,所以此时为了安全起见最好不要

使用二级缓存。

  因为此时 “正确性”的重要性远远大于 “高性能”的重要性。



2.     目前系统中使用hibernate缓存的建议
1.4.   目前情况
一般系统中有三种情况会绕开hibernate执行数据库操作:

1、多个应用系统同时访问一个数据库

   此种情况使用hibernate二级缓存会不可避免的造成数据不一致的问题,

   此时要进行详细的设计。比如在设计上避免对同一数据表的同时的写入操作,

   使用数据库各种级别的锁定机制等。



2、动态表相关

   所谓“动态表”是指在系统运行时根据用户的操作系统自动建立的数据表。

   比如“自定义表单”等属于用户自定义扩展开发性质的功能模块,因为此时数据表是运行时建立的,

所以不能进行hibernate的映射。因此对它的操作只能是绕开hibernate的直接数据库JDBC操作。

      如果此时动态表中的数据没有设计缓存,就不存在数据不一致的问题。

posted @ 2009-04-11 10:24 lanxin1020 阅读(499) | 评论 (0)编辑 收藏
缓存是介于应用程序和物理数据源之间,其作用是为了降低应用程序对物理数据源访问的频次,从而提高了应用的运行性能。缓存内的数据是对物理数据源中的数据的复制,应用程序在运行时从缓存读写数据,在特定的时刻或事件会同步缓存和物理数据源的数据。

  缓存的介质一般是内存,所以读写速度很快。但如果缓存中存放的数据量非常大时,也会用硬盘作为缓存介质。缓存的实现不仅仅要考虑存储的介质,还要考虑到管理缓存的并发访问和缓存数据的生命周期。

  Hibernate的缓存包括Session的缓存和SessionFactory的缓存,其中SessionFactory的缓存又可以分为两类:内置缓存和外置缓存。Session的缓存是内置的,不能被卸载,也被称为Hibernate的第一级缓存。SessionFactory的内置缓存和Session的缓存在实现方式上比较相似,前者是SessionFactory对象的一些集合属性包含的数据,后者是指Session的一些集合属性包含的数据。SessionFactory的内置缓存中存放了映射元数据和预定义SQL语句,映射元数据是映射文件中数据的拷贝,而预定义SQL语句是在Hibernate初始化阶段根据映射元数据推导出来,SessionFactory的内置缓存是只读的,应用程序不能修改缓存中的映射元数据和预定义SQL语句,因此SessionFactory不需要进行内置缓存与映射文件的同步。SessionFactory的外置缓存是一个可配置的插件。在默认情况下,SessionFactory不会启用这个插件。外置缓存的数据是数据库数据的拷贝,外置缓存的介质可以是内存或者硬盘。SessionFactory的外置缓存也被称为Hibernate的第二级缓存。

  Hibernate的这两级缓存都位于持久化层,存放的都是数据库数据的拷贝,那么它们之间的区别是什么呢?为了理解二者的区别,需要深入理解持久化层的缓存的两个特性:缓存的范围和缓存的并发访问策略。

持久化层的缓存的范围

  缓存的范围决定了缓存的生命周期以及可以被谁访问。缓存的范围分为三类。

  1 事务范围:缓存只能被当前事务访问。缓存的生命周期依赖于事务的生命周期,当事务结束时,缓存也就结束生命周期。在此范围下,缓存的介质是内存。事务可以是数据库事务或者应用事务,每个事务都有独自的缓存,缓存内的数据通常采用相互关联的的对象形式。

  2 进程范围:缓存被进程内的所有事务共享。这些事务有可能是并发访问缓存,因此必须对缓存采取必要的事务隔离机制。缓存的生命周期依赖于进程的生命周期,进程结束时,缓存也就结束了生命周期。进程范围的缓存可能会存放大量的数据,所以存放的介质可以是内存或硬盘。缓存内的数据既可以是相互关联的对象形式也可以是对象的松散数据形式。松散的对象数据形式有点类似于对象的序列化数据,但是对象分解为松散的算法比对象序列化的算法要求更快。

  3 集群范围:在集群环境中,缓存被一个机器或者多个机器的进程共享。缓存中的数据被复制到集群环境中的每个进程节点,进程间通过远程通信来保证缓存中的数据的一致性,缓存中的数据通常采用对象的松散数据形式。

  对大多数应用来说,应该慎重地考虑是否需要使用集群范围的缓存,因为访问的速度不一定会比直接访问数据库数据的速度快多少。

  持久化层可以提供多种范围的缓存。如果在事务范围的缓存中没有查到相应的数据,还可以到进程范围或集群范围的缓存内查询,如果还是没有查到,那么只有到数据库中查询。事务范围的缓存是持久化层的第一级缓存,通常它是必需的;进程范围或集群范围的缓存是持久化层的第二级缓存,通常是可选的。

持久化层的缓存的并发访问策略

  当多个并发的事务同时访问持久化层的缓存的相同数据时,会引起并发问题,必须采用必要的事务隔离措施。

  在进程范围或集群范围的缓存,即第二级缓存,会出现并发问题。因此可以设定以下四种类型的并发访问策略,每一种策略对应一种事务隔离级别。

  事务型:仅仅在受管理环境中适用。它提供了Repeatable Read事务隔离级别。对于经常被读但很少修改的数据,可以采用这种隔离类型,因为它可以防止脏读和不可重复读这类的并发问题。

  读写型:提供了Read Committed事务隔离级别。仅仅在非集群的环境中适用。对于经常被读但很少修改的数据,可以采用这种隔离类型,因为它可以防止脏读这类的并发问题。

  非严格读写型:不保证缓存与数据库中数据的一致性。如果存在两个事务同时访问缓存中相同数据的可能,必须为该数据配置一个很短的数据过期时间,从而尽量避免脏读。对于极少被修改,并且允许偶尔脏读的数据,可以采用这种并发访问策略。

  只读型:对于从来不会修改的数据,如参考数据,可以使用这种并发访问策略。

  事务型并发访问策略是事务隔离级别最高,只读型的隔离级别最低。事务隔离级别越高,并发性能就越低。
posted @ 2009-04-11 10:18 lanxin1020 阅读(112) | 评论 (0)编辑 收藏
1.Hibernate 的初始化.
读取Hibernate 的配置信息-〉创建Session Factory
1)创建Configeration类的实例。
它的构造方法:将配置信息(Hibernate config.xml)读入到内存。
一个Configeration 实例代表Hibernate 所有Java类到Sql数据库映射的集合。
2)创建SessionFactory实例
把Configeration 对象中的所有配置信息拷贝到SessionFactory的缓存中。
SessionFactory的实例代表一个数据库存储员源,创建后不再与Configeration 对象关联。
缓存(cache):指Java对象的属性(通常是一些集合类型的属性--占用内存空间。
     SessionFactory的缓存中:Hibernate 配置信息。OR映射元数据。
缓存-大:重量级对象 小:轻量级对象
3)调用SessionFactory创建Session的方法
1】用户自行提供JDBC连接。
   Connection con=dataSource.getConnection();
   Session s=sessionFactory.openSession(con);
2】让SessionFactory提供连接
   Session s=sessionFactory.openSession();
4)通过Session 接口提供的各种方法来操纵数据库访问。

Hibernate 的缓存体系
一级缓存:
Session 有一个内置的缓存,其中存放了被当前工作单元加载的对象。
每个Session 都有自己独立的缓存,且只能被当前工作单元访问。
二级缓存:
SessionFactory的外置的可插拔的缓存插件。其中的数据可被多个Session共享访问。
SessionFactory的内置缓存:存放了映射元数据,预定义的Sql语句。

Hibernate 中Java对象的状态
1.临时状态 (transient)
特征:
   1】不处于Session 缓存中
   2】数据库中没有对象记录
Java如何进入临时状态
   1】通过new语句刚创建一个对象时
   2】当调用Session 的delete()方法,从Session 缓存中删除一个对象时。

2.持久化状态(persisted)
特征:
   1】处于Session 缓存中
   2】持久化对象数据库中设有对象记录
   3】Session 在特定时刻会保持二者同步
Java如何进入持久化状态
   1】Session 的save()把临时-》持久化状态
   2】Session 的load(),get()方法返回的对象
   3】Session 的find()返回的list集合中存放的对象
   4】Session 的update(),saveOrupdate()使游离-》持久化
3.游离状态(detached)
特征:
   1】不再位于Session 缓存中
   2】游离对象由持久化状态转变而来,数据库中可能还有对应记录。
Java如何进入持久化状态-》游离状态
   1】Session 的close()方法
   2】Session 的evict()方法,从缓存中删除一个对象。提高性能。少用。

posted @ 2009-04-09 23:54 lanxin1020 阅读(116) | 评论 (0)编辑 收藏
也许你听说过Hibernate的大名,但可能一直不了解它,也许你一直渴望使用它进行开发,那么本文正是你所需要的!在本文中,我向大家重点介绍Hibernate的核心API调用库,并讲解一下它的基本配置。

  看完本文后,我相信你对什么是ORM(对像/关系映射)以及它的优点会有一个深刻的认识,我们先通过一个简单的例子开始来展现它的威力。

  正如一些传统的经典计算机文章大都会通过一个“hello,world”的例子开始讲解一样,我们也不例外,我们也将从一个相对简单的例子来阐述Hibernate的开发方法,但如果要真正阐述Hibernate的一些重要思想,仅仅靠在屏幕上打印一些字符是远远不够的,在我们的示例程序中,我们将创建一些对象,并将其保存在数据库中,然后对它们进行更新和查询。

  阅读导航

  “Hello World”“Hello world”示例程序让您对Hibernate有一个简单的认识。
  理解Hibernate的架构介绍Hibernate接口的主要功能。
  核心接口Hibernate有5个核心接口,通过这几个接口开发人员可以存储和获得持久对象,并且能够进行事务控制
  一个重要的术语:TypeType是Hibernate发明者发明的一个术语,它在整个构架中是一个非常基础、有着强大功能的元素,一个Type对象能将一个Java类型映射到数据库中一个表的字段中去。
  策略接口Hibernate与某些其它开源软件不同的还有一点――高度的可扩展性,这通过它的内置策略机制来实现。
  基础配置Hibernate可以配置成可在任何Java环境中运行,一般说来,它通常被用在2-3层的C/S模式的项目中,并被部署在服务端。
  创建一个SessionFactory对象要创建一个SessionFactory对象,必须在Hibernate初始化时创建一个Configuration类的实例,并将已写好的映射文件交由它处理。

  “Hello World”

  Hibernate应用程序定义了一些持久类,并且定义了这些类与数据库表格的映射关系。在我们这个“Hello world”示例程序中包含了一个类和一个映射文件。让我们看看这个简单的持久类包含有一些什么?映射文件是怎样定义的?另外,我们该怎样用Hibernate来操作这个持久类。

  我们这个简单示例程序的目的是将一些持久类存储在数据库中,然后从数据库取出来,并将其信息正文显示给用户。其中Message正是一个简单的持久类:,它包含我们要显示的信息,其源代码如下:

  列表1 Message.Java 一个简单的持久类

  package hello;
  public class Message {
  private Long id;
  private String text;
  private Message nextMessage;
  private Message() {}
  public Message(String text) {
  this.text = text;
  }
  public Long getId() {
  return id;
  }
  private void setId(Long id) {
  this.id = id;
  }
  public String getText() {
  return text;
  }
  public void setText(String text) {
  this.text = text;
  }
  public Message getNextMessage() {
  return nextMessage;
  }
  public void setNextMessage(Message nextMessage) {
  this.nextMessage = nextMessage;
  }
  }

  Message类有三个属性:Message的id 、消息正文、以及一个指向下一条消息的指针。其中id属性让我们的应用程序能够唯一的识别这条消息,通常它等同于数据库中的主键,如果多个Message类的实例对象拥有相同的id,那它们代表数据库某个表的同一个记录。在这里我们选择了长整型作为我们的id值,但这不是必需的。Hibernate允许我们使用任意的类型来作为对象的id值,在后面我们会对此作详细描述。

  你可能注意到Message类的代码类似于JavaBean的代码风格,并且它有一个没有参数的构造函数,在我们以后的代码中我将继续使用这种风格来编写持久类的代码。

  Hibernate会自动管理Message类的实例,并通过内部机制使其持久化,但实际上Message对象并没有实现任何关于Hibernate的类或接口,因此我们也可以将它作为一个普通的Java类来使用:

  Message message = new Message("Hello World");
  System.out.println( message.getText() );

  以上这段代码正是我们所期望的结果:它打印“hello world”到屏幕上。但这并不是我们的最终目标;实际上Hibernate与诸如EJB容器这样的环境在持久层实现的方式上有很大的不同。我们的持久类(Message类)可以用在与容器无关的环境中,不像EJB必须要有EJB容器才能执行。为了能更清楚地表现这点,以下代码将我们的一个新消息保存到数据库中去:

  Session session = getSessionFactory().openSession();
  Transaction tx = session.beginTransaction();
  Message message = new Message("Hello World");
  session.save(message);
  tx.commit();
  session.close();

  以上这段代码调用了Hibernate的Session和Transaction接口(关于getSessionFactory()方法我们将会马上提到)。它相当于我们执行了以下SQL语句:

  insert into MESSAGES (MESSAGE_ID, MESSAGE_TEXT, NEXT_MESSAGE_ID)
  values (1, 'Hello World', null)

  在以上的SQL语句中,MESSAGE_ID字段到底被初始化成了什么值呢?由于我们并没有在先前的代码中为message对象的id属性赋与初始值,那它是否为null呢?实际上Hibernate对id属性作了特殊处理:由于它是一个对象的唯一标识,因此当我们进行save()调用时,Hibernate会为它自动赋予一个唯一的值(我们将在后面内容中讲述它是如何生成这个值的)。

  我们假设你已经在数据库中创建了一个名为MESSAGE的表,那么既然前面这段代码让我们将Message对象存入了数据库中,那么现在我们就要将它们一一取出来。下面这段代码将按照字母顺序,将数据库中的所有Message对象取出来,并将它们的消息正文打印到屏幕上:

  Session newSession = getSessionFactory().openSession();
  Transaction newTransaction = newSession.beginTransaction();
  List messages =newSession.find("from Message as m order by m.text asc");
  System.out.println( messages.size() + " message(s) found:" );
  for ( Iterator iter = messages.iterator(); iter.hasNext(); ) {
  Message message = (Message) iter.next();
  System.out.println( message.getText() );
  }
  newTransaction.commit();
  newSession.close();

  在以上这段代码中,你可能被find()方法的这个参数困扰着:"from Message as m order by m.text asc",其实它是Hibernate自己定义的查询语言,全称叫Hibernate Query Language(HQL)。通俗地讲HQL与SQL的关系差不多就是方言与普通话之间的关系,咋一看,你会觉得它有点类似于SQL语句。其实在find()调用时,Hibernate会将这段HQL语言翻译成如下的SQL语句:

  select m.MESSAGE_ID, m.MESSAGE_TEXT, m.NEXT_MESSAGE_ID
  from MESSAGES m
  order by m.MESSAGE_TEXT asc

  以下就是运行结果:

  1 message(s) found:
  Hello World

  如果你以前没有ORM(对象-关系映射)的开发经验,那你可能想在代码的某个地方去寻找这段SQL语句,但在Hibernate中你可能会失望:它根本不存在!所有就SQL语句都是Hibernate动态生成的。

  也许你会觉得还缺点什么,对!仅凭以上代码Hibernate是无法将我们的Message类持久化的。我们还需要一些更多的信息,这就是映射定义表!这个表在Hibernate中是以XML格式来体现的,它定义了Message类的属性是怎样与数据库中的MESSAGES表的字段进行一一对应的,列表2是这个示例程序的映射配置文件清单:

  列表2:示例程序的对象-关系映射表

  <?xml version="1.0"?>
  <!DOCTYPE hibernate-mapping PUBLIC
  "-//Hibernate/Hibernate Mapping DTD//EN"
  "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">
  <hibernate-mapping>
  <class name="hello.Message" table="MESSAGES">
  <id name="id" column="MESSAGE_ID">
  <generator class="increment"/>
  </id>
  <property name="text" column="MESSAGE_TEXT"/>
  <many-to-one name="nextMessage" cascade="all" column="NEXT_MESSAGE_ID"/>
  </class>
  </hibernate-mapping>

  以上这个文档告诉Hibernate怎样将Message类映射到MESSAGES表中,其中Message类的id属性与表的MESSAGE_ID字段对应,text属性与表的MESSAGE_TEXT字段对应,nextMessage属性是一个多对一的关系,它与表中的NEXT_MESSAGE_ID相对应。

  相对于有些开源项目来说,Hibernate的配置文件其实是很容易理解的。你可以轻松地修改与维护它。只要你定义好了持久类与数据库中表字段的对应关系就行了,Hibernate会自动帮你生成SQL语句来对Message对象进行插入、更新、删除、查找工作,你可以不写一句SQL语句,甚至不需要懂得SQL语言!

  现在让我们做一个新的试验,我们先取出第一个Message对象,然后修改它的消息正文,最后我们再生成一个新的Message对象,并将它作为第一个Message对象的下一条消息,其代码如下:

  列表3 更新一条消息

  Session session = getSessionFactory().openSession();
  Transaction tx = session.beginTransaction();
  // 1 is the generated id of the first message
  Message message =(Message) session.load( Message.class, new Long(1) );
  message.setText("Greetings Earthling");
  Message nextMessage = new Message("Take me to your leader (please)");
  message.setNextMessage( nextMessage );
  tx.commit();
  session.close();

  以上这段代码在调用时,Hibernate内部自动生成如下的SQL语句:

  select m.MESSAGE_ID, m.MESSAGE_TEXT, m.NEXT_MESSAGE_ID
  from MESSAGES m
  where m.MESSAGE_ID = 1

  insert into MESSAGES (MESSAGE_ID, MESSAGE_TEXT, NEXT_MESSAGE_ID)
  values (2, 'Take me to your leader (please)', null)

  update MESSAGES
  set MESSAGE_TEXT = 'Greetings Earthling', NEXT_MESSAGE_ID = 2
  where MESSAGE_ID = 1

  当第一个Message对象的text属性和nextMessage被程序修改时,请注意Hibernate是如何检测到这种变化,并如何在数据库中自动对它更新的。这实际上是Hibernate的一个很有价值的特色,我们把它称为“自动脏数据检测”,Hibernate的这个特色使得当我们修改一个持久对象的属性后,不必显式地通知Hibernate去将它在数据库中进行更新。同样的,当第一个Message对象调用setNextMessage()方法将第二个Message对象作为它的下一条消息的引用时,第二条消息会无需调用save()方法,便可以自动地保存在数据库中。这种特色被称为“级联保存”,它也免去了我们显式地对第二个Message对象调用save()方法之苦。

  如果我们再运行先前的那段将数据库中所有的Message对象都打印出来的代码,那它的运行结果如下:

  2 message(s) found:
  Greetings Earthling
  Take me to your leader (please)


  “Hello world”示例程序现在介绍完毕。我们总算对Hibernate有了一个简单的认识,下面我们将回过头来,对Hibernate的主要API调用作一下简要的介绍:
  

  理解Hibernate的架构

  当你想用Hibernate开发自己的基于持久层的应用时,第一件事情应当是熟悉它的编程接口。Hibernate的API接口设计得尽量简洁明了,以方便开发人员。然而实际上由于ORM的复杂性,它的API一般都不可能设计得很简单。但是别担心,你没有必要一下子了解所有的Hibernate的API接口。

  我们将应用层放在了持久层的上部,实际上在传统的项目中,应用层充当着持久层的一个客户端角色。但对于一些简单的项目来说,应用层和持久层并没有区分得那么清楚,这也没什么,在这种情况下你可以将应用层和持久层合并成了一层。

  Hibernate的接口大致可以分为以下几种类型:

  · 一些被用户的应用程序调用的,用来完成基本的创建、读取、更新、删除操作以及查询操作的接口。这些接口是Hibernate实现用户程序的商业逻辑的主要接口,它们包括Session、Transaction和Query。

  · Hibernate用来读取诸如映射表这类配置文件的接口,典型的代表有Configuration类。

  · 回调(Callback)接口。它允许应用程序能对一些事件的发生作出相应的操作,例如Interceptor、Lifecycle和Validatable都是这一类接口。

  · 一些可以用来扩展Hibernate的映射机制的接口,例如UserType、CompositeUserType和IdentifierGenerator。这些接口可由用户程序来实现(如果有必要)。

  Hibernate使用了J2EE架构中的如下技术:JDBC、JTA、JNDI。其中JDBC是一个支持关系数据库操作的一个基础层;它与JNDI和JTA一起结合,使得Hibernate可以方便地集成到J2EE应用服务器中去。

  在这里,我们不会详细地去讨论Hibernate API接口中的所有方法,我们只简要讲一下每个主要接口的功能,如果你想了解得更多的话,你可以在Hibernate的源码包中的net.sf.hibernate子包中去查看这些接口的源代码。下面我们依次讲一下所有的主要接口:

  核心接口

  以下5个核心接口几乎在任何实际开发中都会用到。通过这些接口,你不仅可以存储和获得持久对象,并且能够进行事务控制。

  Session接口

  Session接口对于Hibernate 开发人员来说是一个最重要的接口。然而在Hibernate中,实例化的Session是一个轻量级的类,创建和销毁它都不会占用很多资源。这在实际项目中确实很重要,因为在客户程序中,可能会不断地创建以及销毁Session对象,如果Session的开销太大,会给系统带来不良影响。但值得注意的是Session对象是非线程安全的,因此在你的设计中,最好是一个线程只创建一个Session对象。

  在Hibernate的设计者的头脑中,他们将session看作介于数据连接与事务管理一种中间接口。我们可以将session想象成一个持久对象的缓冲区,Hibernate能检测到这些持久对象的改变,并及时刷新数据库。我们有时也称Session是一个持久层管理器,因为它包含这一些持久层相关的操作,诸如存储持久对象至数据库,以及从数据库从获得它们。请注意,Hibernate 的session不同于JSP应用中的HttpSession。当我们使用session这个术语时,我们指的是Hibernate中的session,而我们以后会将HttpSesion对象称为用户session。

  SessionFactory 接口

  这里用到了一个设计模式――工厂模式,用户程序从工厂类SessionFactory中取得Session的实例。

  令你感到奇怪的是SessionFactory并不是轻量级的!实际上它的设计者的意图是让它能在整个应用中共享。典型地来说,一个项目通常只需要一个SessionFactory就够了,但是当你的项目要操作多个数据库时,那你必须为每个数据库指定一个SessionFactory。
  SessionFactory在Hibernate中实际起到了一个缓冲区的作用,它缓冲了Hibernate自动生成的SQL语句和一些其它的映射数据,还缓冲了一些将来有可能重复利用的数据。

  Configuration 接口

  Configuration接口的作用是对Hibernate进行配置,以及对它进行启动。在Hibernate的启动过程中,Configuration类的实例首先定位映射文档的位置,读取这些配置,然后创建一个SessionFactory对象。

  虽然Configuration接口在整个Hibernate项目中只扮演着一个很小的角色,但它是启动hibernate时你所遇到的每一个对象。

  Transaction 接口

  Transaction接口是一个可选的API,你可以选择不使用这个接口,取而代之的是Hibernate的设计者自己写的底层事务处理代码。 Transaction接口是对实际事务实现的一个抽象,这些实现包括JDBC的事务、JTA中的UserTransaction、甚至可以是CORBA事务。之所以这样设计是能让开发者能够使用一个统一事务的操作界面,使得自己的项目可以在不同的环境和容器之间方便地移值。

  Query和Criteria接口

  Query接口让你方便地对数据库及持久对象进行查询,它可以有两种表达方式:HQL语言或本地数据库的SQL语句。Query经常被用来绑定查询参数、限制查询记录数量,并最终执行查询操作。

  Criteria接口与Query接口非常类似,它允许你创建并执行面向对象的标准化查询。

  值得注意的是Query接口也是轻量级的,它不能在Session之外使用。

  Callback 接口

  当一些有用的事件发生时――例如持久对象的载入、存储、删除时,Callback接口会通知Hibernate去接收一个通知消息。一般而言,Callback接口在用户程序中并不是必须的,但你要在你的项目中创建审计日志时,你可能会用到它。

   一个重要的术语:Type

  Hibernate的设计者们发明了一个术语:Type,它在整个构架中是一个非常基础、有着强大功能的元素。一个Type对象能将一个Java类型映射到数据库中一个表的字段中去(实际上,它可以映射到表的多个字段中去)。持久类的所有属性都对应一个type。这种设计思想使用Hibernate有着高度的灵活性和扩展性。

  Hibernate内置很多type类型,几乎包括所有的Java基本类型,例如Java.util.Currency、Java.util.calendar、byte[]和Java.io.Serializable。

  不仅如此,Hibernate还支持用户自定义的type,通过实现接口UserType和接口CompositeUserType,你可以加入自己的type。你可以利用这种特色让你的项目中使用自定义的诸如Address、Name这样的type,这样你就可以获得更大的便利,让你的代码更优雅。自定义type在Hibernate中是一项核心特色,它的设计者鼓励你多多使用它来创建一个灵活、优雅的项目!

  策略接口

  Hibernate与某些其它开源软件不同的还有一点――高度的可扩展性,这通过它的内置策略机制来实现。当你感觉到Hibernate的某些功能不足,或者有某些缺陷时,你可以开发一个自己的策略来替换它,而你所要做的仅仅只是继承它的某个策略接口,然后实现你的新策略就可以了,以下是它的策略接口:

  · 主键的生成 (IdentifierGenerator 接口)

  · 本地SQL语言支持 (Dialect 抽象类)

  · 缓冲机制 (Cache 和CacheProvider 接口)

  · JDBC 连接管理 (ConnectionProvider接口)

  · 事务管理 (TransactionFactory, Transaction, 和 TransactionManagerLookup 接口)

  · ORM 策略 (ClassPersister 接口)

  · 属性访问策略 (PropertyAccessor 接口)

  · 代理对象的创建 (ProxyFactory接口)

  Hibernate为以上所列的机制分别创建了一个缺省的实现,因此如果你只是要增强它的某个策略的功能的话,只需简单地继承这个类就可以了,没有必要从头开始写代码。

  以上就是Hibernate的一些核心接口,但当我们真正开始用它进行开发时,你的脑海里可能总会有一个疑问:我是通过什么方式,并从哪里取得Session的呢?以下我们就解答这个问题。

  基础配置

  现在回顾一下我们先前的内容:我们写出了一个示例程序,并简要地讲解了Hibernate的一些核心类。但要真正使你的项目运行起来,还有一件事必须要做:配置。Hibernate可以配置成可在任何Java环境中运行,一般说来,它通常被用在2-3层的C/S模式的项目中,并被部署在服务端。在这种项目中,Web浏览器、或Java GUI程序充当者客户端。尽管我们的焦点主要是集中在多层web应用,但实际上在一些基于命令行的应用中也可以使用Hibernate。并且,对Hibernate的配置在不同的环境下都会不同,Hibernate运行在两种环境下:可管理环境和不可管理环境

  · 可管理环境――这种环境可管理如下资源:池资源管理,诸如数据库连接池和,还有事务管理、安全定义。一些典型的J2EE服务器(JBoss、Weblogic、WebSphere)已经实现了这些。

  · 不可管理环境――只是提供了一些基本的功能,诸如像Jetty或Tomcat这样的servlet容器环境。一个普通的Java桌面应用或命令行程序也可以认为是处于这种环境下。这种环境不能提供自动事务处理、资源管理或安全管理,这些都必须由应用程序自己来定义。

  Hibernate的设计者们将这两种环境设计了一个统一的抽象界面,因此对于开发者来说只有一种环境:可管理环境。如果实际项目是建立在诸如Tomcat这类不可管理的环境中时,那Hibernate将会使用它自己的事务处理代码和JDBC连接池,使其变为一个可管理环境。
  对于可管理的环境而言,Hibernate会将自己集成在这种环境中。对于开发者而言,你所要做的工作非常简单:只需从一个Configuration类中创建一个SessionFactory类就可以了。
   创建一个SessionFactory对象

  为了能创建一个SessionFactory对象,你必须在Hibernate初始化时创建一个Configuration类的实例,并将已写好的映射文件交由它处理。这样,Configuration对象就可以创建一个SessionFactory对象,当SessionFactory对象创建成功后,Configuration对象就没有用了,你可以简单地抛弃它。如下是示例代码:

  Configuration cfg = new Configuration();
  cfg.addResource("hello/Message.hbm.xml");
  cfg.setProperties( System.getProperties() );
  SessionFactory sessions = cfg.buildSessionFactory();

  在以上代码中,Message.hb.xml这个映射文件的位置比较特殊,它与当前的classpath相关。例如classpath包含当前目录,那在上述代码中的Message.hbm.xml映射文件就可以保存在当前目录下的hello目录中。

  作为一种约定,Hibernate的映射文件默认以.htm.xml作为其扩展名。另一个约定是坚持为每一个持久类写一个配置文件,想一想如果你将所有持久类的映射写入一个单独的配置文件中的话,那这个配置文件肯定非常庞大,不易维护。但这里又出现了一个新问题:如果为每个类写一个配置文件的话,这么多的配置文件应该存放在哪里呢?

  Hibernate推荐你将每个映射文件保存在与持久类相同的目录下,并且与持久类同名。例如我们第一个示例程序中的Message持久类放在hello目录下,那你必须在这个目录下存放名为Message.hbm.xml的映射文件。这样一个持久类都有自己的一个映射文件,避免了出现像struts项目中的“struts-config.xml地狱”的情况。如果你不遵循这种规定,那你必须手动地用addResource()方法将一个个的映射文件载入;但你如果遵循这种规定,那你可以方便地用addClass()方法同时将持久类和它的映射文件载入,以下是体现这种便利性的示例代码:

  SessionFactory sessions = new Configuration()
  .addClass(org.hibernate.auction.model.Item.class)
  .addClass(org.hibernate.auction.model.Category.class)
  .addClass(org.hibernate.auction.model.Bid.class)
  .setProperties( System.getProperties() )
  .buildSessionFactory();

  当然,Hibernate的映射文件还有很多其它的配置选项,比如数据库连接的设定,或是能够改变Hibernate运行时行为的一些设定。所有的设置可能是非常庞杂的,足以让你喘不过气来,但是不必担心,因为Hibernate为绝大多数值都设定了一个合理缺省值,你只需要修改这些配置文件中的极小一部分值。

  你可以通过以下几种方式来修改Hibernate的系统配置参数:

  · 将一个Java.util.Properties实例作为参数传给Configuration类的setProperties()方法。

  · 在Hibernate启动时用Java –Dproperty=value的方式设置值。

  · 在classpath可以找到的路径下创建一个名为hibernate.properties的配置文件。

  · 在classpath可以找到的路径下创建一个名为hibernate.cfg.xml的文件,并在其<property>标签中定义属性值。

  以上就是对Hibernate的一个大致介绍,如果你想知道得更多,那本文还是远远不够的,我将陆续推出更多关于Hibernate的资料。但有一点是毫无疑问的:它的确是一个非常优秀的持久层解决方案!


posted @ 2009-04-09 23:50 lanxin1020 阅读(122) | 评论 (0)编辑 收藏

Hibernate实体对象的生命周期

关键字: hibernate学习笔记
在用Hibernate的时候,时不时会因为没有处理好实体对象的状态而犯一些莫名其妙的异常,在这里对实体对象的各种状态整理一下,希望能有所帮助。

Hibernate实体对象,即指Hibernate O/R影射关系中的域对象 即O/R中的"O"。在Hibrenate实体对象的生命周期中存在着三中状态,即:
1:自由状态(Transient)。
2:持久状态(Persistent)。
3:游离状态(Detached)。

1:自由状态(Transient)
自由状态(Transient),是指实体对象在内存中自由存在,他与数据库的记录无关。如:
Java代码
  1. TUser user = new TUser();   
  2. user.setName("MyName");  

这里的user对象只是一个非常普通的java对象,与数据库中的记录没有任何关系。

2:持久状态(Persistent)
持久状态(Persistent),即实体对象处于Hibernate框架的管理状态,实体对象被纳入Hibernate的实体容器中管理。处于持久状态的对象,其更变将由Hibernate固化到数据库中。如:
Java代码
  1. //创建两个处于自由状态的实体对象。   
  2. TUser user_1 = new TUser();   
  3. TUser user_2 = new TUser();   
  4.   
  5. user_1.setName("Name_1");   
  6. user_2.setName("Name_2");   
  7.   
  8. Transaction tx = session.begintransaction();   
  9. session.save(user_1);   
  10. //通过session的save方法,user_1对象已经被纳入Hibernate的实体管理容器,处于持久化状   
  11. //态(Persistent),这时候对user_1对象的任何修改都将被同步到数据库中。   
  12.   
  13. tx.commit();   
  14.   
  15. //而user_2仍然才处于自由状态(Transient),不受Hibernate框架的管理。  

从上面看到,处于自由状态的实体对象,可以通过Hibernate的Session.sava方法转化为持久状态
除了用Session.save方法外,还可以通过其他方法来获取一个持久状态的对象,那就是直接通过Hibernate加载的对象,通过Session.load方法,可以直接加载一个处于持久状态的实体对象。如下:
Java代码
  1. TUser user = Session.load(TUser.class,new Integer(1));   
  2. //在load方法没返回之前,就已经先把对象纳入Hibernate的管理范围,所以这里的user   
  3. //已经处于持久状态。  

从上面的代码可以看出,处于持久状态的实体对象一定要和Session关联,并处于该Session的有效期内。

3:游离状态(Detached)
处于持久状态的实体对象,在其关联的Session关闭以后,此实体对象就处于游离状态
Java代码
  1. TUser user = new TUser();   
  2. user.setName("name_1");   
  3. Transaction tx = session.begintransaction();   
  4. session.save(user);//把自由状态的实体对象user转为持久状态,   
  5. tx.commit();   
  6. session.close();   
  7. //session关闭以后,处于持久状态的实体对象user将转为游离状态。   
  8. //因为此时user已经和session脱离关系。  


由上面可以看到实体对象的游离状态是在对象和它所寄宿的Session脱离关系后形成的,但处于自由状态的实体对象也没有和任何session有关联,那么他们两者有什么区别呢?关键的就在我们对自由状态的实体对象执行了Session.save方法,
当我们执行
Java代码
  1. TUser user = new TUser();  

时,我们只是创建了一个普通的对象,他并没有和数据库里的任何一条记录对应,当我们执行
Session.save以后,Hibernate就为user设置了一个主键,就是user.Id属性,通过这个属性,Hibernate就把user对象和数据库里的记录关联起来,所以自由状态游离状态的基本区别就是 处于游离状态的实体对象,在数据库里有对应的记录,因此它可以通过和session关联再次转为持久状态

三种状态的转化
自由状态-->持久状态:可以通过Session.sava方法来转换。
持久状态-->游离状态:可以通过Session.close方法来关闭session,获取游离状态的对象
持久状态-->自由状态:可以通过Session.delete方法来删除实体对象对应的数据库记录,使实体对象处于自由状态。

补充一下,有时可能会想,可以通过认为的个处于自由状态的实体对象设置一个Id值
Java代码
  1. user.Id=1  
,来人为的创建一个游离状态的对象。
这里注意一点,我们可以人为地给实体对象设置Id值,我我们无法知道这个ID值在数据库里有没有对应的记录,如果没有,就算我们人为地设置了Id,也不能说一个有ID的实体对象就是一个游离状态的对象。
posted @ 2009-04-08 12:02 lanxin1020 阅读(244) | 评论 (0)编辑 收藏
Introducing to Spring Framework

作者:Rod Johnson
译者:yanger,taowen
校对:taowen

关于Spring Framework,今年夏天你可能已经听见很多的议论。在本文中,我将试图解释Spring能完成什么,和我怎么会认为它能帮助你开发J2EE应用程序。

又来一个framework?

你可能正在想“不过是另外一个的framework”。当已经有许多开放源代码(和专有) J2EE framework时,为什么你还要耐下心子读这篇文章或去下载Spring Framework?

我相信Spring是独特的,有几个原因:



它关注的领域是其他许多流行的Framework未曾关注的。Spring要提供的是一种管理你的业务对象的方法。

Spring既是全面的又是模块化的。Spring有分层的体系结构,这意味着你能选择仅仅使用它任何一个独立的部分,而它的架构又是内部一致。因此你能从你的学习中,得到最大的价值。例如,你可能选择仅仅使用Spring来简单化JDBC的使用,或用来管理所有的业务对象。

它的设计从一开始就是要帮助你编写易于测试的代码。Spring是使用测试驱动开发的工程的理想框架。


Spring不会给你的工程添加对其他的框架依赖。Spring也许称得上是个一站式解决方案,提供了一个典型应用所需要的大部分基础架构。它还涉及到了其他framework没有考虑到的内容。

尽管它仅仅是一个从2003年2月才开始的开源项目,但Spring有深厚的历史根基。这个开源工程是起源自我在2002年晚些时候出版的《Expert One-on-One J2EE设计与开发》书中的基础性代码。这本书展示了Spring背后的基础性架构思想。然而,对这个基础架构的概念可以追溯到2000年的早些时候,并且反映了我为一系列商业工程开发基础结构的成功经验。

2003年1月,Spring已经落户于SourceForge上了。现在有10个开发人员,其中6个是高度投入的积极分子。

Spring架构上的好处

在我们进入细节之前,让我们来看看Spring能够给工程带来的种种好处:



Spring能有效地组织你的中间层对象,不管你是否选择使用了EJB。如果你仅仅使用了Struts或其他为J2EE的 API特制的framework,Spring致力于解决剩下的问题。

Spring能消除在许多工程中常见的对Singleton的过多使用。根据我的经验,这是一个很大的问题,它降低了系统的可测试性和面向对象的程度。

通过一种在不同应用程序和项目间一致的方法来处理配置文件,Spring能消除各种各样自定义格式的属性文件的需要。曾经对某个类要寻找的是哪个魔法般的属性项或系统属性感到不解,为此不得不去读Javadoc甚至源编码?有了Spring,你仅仅需要看看类的JavaBean属性。 Inversion of Control的使用(在下面讨论)帮助完成了这种简化。

通过把对接口编程而不是对类编程的代价几乎减少到没有,Spring能够促进养成好的编程习惯。

Spring被设计为让使用它创建的应用尽可能少的依赖于他的APIs。在Spring应用中的大多数业务对象没有依赖于Spring。

使用Spring构建的应用程序易于单元测试。

Spring能使EJB的使用成为一个实现选择,而不是应用架构的必然选择。你能选择用POJOs或local EJBs来实现业务接口,却不会影响调用代码。

Spring帮助你解决许多问题而无需使用EJB。Spring能提供一种EJB的替换物,它们适用于许多web应用。例如,Spring能使用AOP提供声明性事务管理而不通过EJB容器,如果你仅仅需要与单个数据库打交道,甚至不需要一个JTA实现。

Spring为数据存取提供了一个一致的框架,不论是使用的是JDBC还是O/R mapping产品(如Hibernate)。


Spring确实使你能通过最简单可行的解决办法来解决你的问题。而这是有有很大价值的。

Spring做了些什么?

Spring提供许多功能,在此我将依次快速地展示其各个主要方面。

任务描述

首先,让我们明确Spring范围。尽管Spring覆盖了许多方面,但我们对它应该涉什么,什么不应该涉及有清楚的认识。

Spring的主要目的是使J2EE易用和促进好编程习惯。

Spring不重新轮子。因此,你发现在Spring中没有logging,没有连接池,没有分布式事务调度。所有这些东西均有开源项目提供(例如我们用于处理所有日志输出的Commons Logging以及Commons DBCP),或由你的应用程序服务器提供了。出于同样的的原因,我们没有提供 O/R mapping层。对于这个问题已经有了像Hibernate和JDO这样的优秀解决方案。

Spring的目标就是让已有的技术更加易用。例如,尽管我们没有底层事务协调处理,但我们提供了一个抽象层覆盖了JTA或任何其他的事务策略。

Spring没有直接和其他的开源项目竞争,除非我们感到我们能提供新的一些东西。例如,象许多开发人员一样,我们从来没有对Struts感到高兴过,并且觉得到在MVC web framework中还有改进的余地。在某些领域,例如轻量级的IoC容器和AOP框架,Spring确实有直接的竞争,但是在这些领域还没有已经较为流行的解决方案。(Spring在这些领域是开路先锋。)

Spring也得益于内在的一致性。所有的开发者都在唱同样的的赞歌,基础想法依然与Expert One-on-One J2EE设计与开发中提出的差不多。 并且我们已经能够在多个领域中使用一些中心的概念,例如Inversion of Control。

Spring在应用服务器之间是可移植的。当然保证可移植性总是一种挑战,但是我们避免使用任何平台特有或非标准的东西,并且支持在WebLogic,Tomcat,Resin,JBoss,WebSphere和其他的应用服务器上的用户。

Inversion of Control 容器

Spring设计的核心是 org.springframework.beans 包, 它是为与JavaBeans一起工作而设计的。 这个包一般不直接被用户使用,而是作为许多其他功能的基础。

下一个层面高一些的抽象是"Bean Factory"。一个Spring bean factory 是一个通用的Factory,它使对象能够按名称获取,并且能管理对象之间的关系。

Bean factories 支持两种模式的对象:



Singleton:在此模式中,有一个具有特定名称的共享对象实例,它在查找时被获取。这是默认的,而且是最为经常使用的。它对于无状态对象是一种理想的模式。

Prototype:在此模式中,每次获取将创建一个独立的对象。例如,这可以被用于让用户拥有他们自己的对象。



由于 org.springframwork.beans.factory.BeanFactory是一个简单的接口,它能被大量底层存储方法实现。你能够方便地实现你自己的BeanFactory,尽管很少用户需要这么做。最为常用的BeanFactory定义是:



XmlBeanFactory: 可解析简单直观的定义类和命名对象属性的XML结构。 我们提供了一个DTD来使编写更容易。

ListableBeanFactoryImpl:提供了解析存放在属性文件中的bean定义的能力,并且可通过编程创建BeanFactories。


每个bean定义可能是一个POJO(通过类名和JavaBean初始属性定义),或是一个FactoryBean。FactoryBean接口添加了一个间接层。通常,这用于创建使用AOP或其他方法的代理对象:例如,添加声明性事务管理的代理。(这在概念上和EJB的interception相似,但实现得更简单。)

BeanFactories能在一个层次结构中选择性地参与,继承ancestor(祖先)的定义。这使得在整个应用中公共配置的共享成为可能,虽然个别资源,如controller servlets,还拥有他们自己的独立的对象集合。

这种使用JavaBeans的动机在《Expert One-on-One J2EE Design and Development》的第四章中有描述,在TheServerSide网站上的有免费的PDF版本(http://www.theserverside.com/resources/article.jsp?l=RodJohnsonInterview)。

通过BeanFactory概念,Spring成为一个Inversion of Control的容器。(我不怎么喜欢container这个词,因为它使人联想到重量级容器,如EJB容器。Spring的BeanFactory是一个可通过一行代码创建的容器,并且不需要特殊的部署步骤。)

Inversion of Control背后的概念经常表述为Hollywood原则的:“Don’t call me,  I’ll call you。” IoC将控制创建的职责搬进了框架中,并把它从应用代码脱离开来。涉及到配置的地方,意思是说在传统的容器体系结构中,如EJB,一个组件可以调用容器并问“我需要它给我做工作的对象X在哪里?”;使用IoC容器则只需指出组件需要X对象,在运行时容器会提供给它。容器是通过查看方法的参数表(例如JavaBean的属性)做到的,也可能根据配置数据如XML。

IoC有几个重要的好处,例如:



因为组件不需要在运行时间寻找合作者,所以他们可以更简单的编写和维护。在Spring版的IoC里,组件通过暴露JavaBean的setter方法表达他们依赖的其他组件。这相当于EJB通过JNDI来查找,EJB查找需要开发人员编写代码。

同样原因,应用代码更容易测试。JavaBean属性是简单的,属于Java核心的,并且是容易测试的:仅编写一个自包含的Junit测试方法用来创建对象和设置相关属性即可。

一个好的IoC实现保留了强类型。如果你需要使用一个通用的factory来寻找合作者,你必须通过类型转换将返回结果转变为想要的类型。这不是一个大不了的问题,但是不雅观。使用IoC,你在你的代码中表达了强类型依赖,框架将负责类型转换。这意味着在框架配置应用时,类型不匹配将导致错误;在你的代码中,你无需担心类型转换异常。

大部分业务对象不依赖于IoC容器的APIs。这使得很容易使用遗留下来的代码,且很容易的使用对象无论在容器内或不在容器内。例如,Spring用户经常配置Jakarta Commons DBCP数据源为一个Spring bean:不需要些任何定制代码去做这件事。我们说一个IoC容器不是侵入性的:使用它并不会使你的代码依赖于它的APIs。任何JavaBean在Spring bean factory中都能成为一个组件。


最后应该强调的是,IoC 不同于传统的容器的体系结构,如EJB,应用代码最小程度地依靠于容器。这意味着你的业务对象可以潜在的被运行在不同的IoC 框架上——或者在任何框架之外——不需要任何代码的改动。

以我和其他Spring用户的经验来说,再怎么强调IoC给应用程序代码带来的好处也不为过。

IoC不是一个新概念,但是它在J2EE团体里面刚刚到达黄金时间。 有一些可供选择的IoC 容器: 例如 Apache Avalon,  PicoContainer 和 HiveMind。Avalon 从没怎么流行,尽管它很强大而且有很长的历史。Avalon相当的重和复杂,并且看起来比新的IoC解决方案更具侵入性。 PicoContainer是一个轻量级而且更强调通过构造函数表达依赖性而不是JavaBean 属性。 与 Spring不同,它的设计允许每个类型一个对象的定义(可能是因为它拒绝任何Java代码外的元数据导致的局限性)。在Spring,  PicoContainer 和其他 IoC frameworks之间做比较,可参看文章Spring网站上的 "The Spring Framework - A Lightweight Container"位于http://www.springframework.org/docs/lightweight_container.html。这个页面里面包含了PicoContainer站点的链接 。

Spring BeanFactories 是非常轻量级的。用户已经成功地将他们应用在applets和单独的Swing应用中。(它们也很好地工作在 EJB容器中。) 没有特殊的部署步骤和察觉得到的启动时间。这个能力表明一个容器在应用的任何层面几乎立即可以发挥非常大的价值。

Spring BeanFactory 概念贯穿于Spring始终, 而且是Spring如此内在一致的关键原因。在IoC容器中,Spring也是唯一的,它使用IoC作为基础概念贯穿于整个功能丰富的框架。

对应用开发人员,最重要的是,一个或多个BeanFactory提供了一个定义明确的业务对象层。这类似于local session bean层,但比它更简单。与EJBs不同,在这个层中的对象可能是相关的,并且他们的关系被拥有它们的factory管理。有一个定义明确的业务对象层对于成功的体系结构是非常重要的。

Spring ApplicationContext 是BeanFactory的子接口,为下列东西提供支持:



信息查找,支持着国际化

事件机制,允许发布应用对象以及可选的注册以接收到事件

可移植的文件和资源访问


XmlBeanFactory 例子

Spring用户通常在XML的“bean定义”文件中配置他们的应用。Spring的XML bean定义文档的根是&lt;beans&gt; 元素。该元素包含一个或多个 &lt;bean&gt;定义。我们一般给每个bean定义的指定类和属性。我们还必须指定ID作为标识,这将成为在代码中使用该bean的名字。

让我们来看一个简单的例子,它配置了三个应用程序对象,之间的关系在J2EE应用中常常能够看到:



J2EE DataSource

使用DataSource的DAO

在处理过程中使用DAO的业务对象


在下面的例子中,我们使用一个来自Jakarta Commons DBCP项目的BasicDataSource。这个class(和其他许多已有的 class一样)可以简单地被应用在Spring bean factory中,只要它提供了JavaBean格式的配置。需要在shutdown时被调用的Close方法可通过Spring的"destroy-method"属性被注册,以避免BasicDataSource需要实现任何Spring  的接口。

代码:
&lt;beans&gt;

  &lt;bean id="myDataSource"
class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"&gt;
    &lt;property name="driverClassName"&gt;&lt;value&gt;com.mysql.jdbc.Driver&lt;/value&gt;&lt;/property&gt;
    &lt;property name="url"&gt;&lt;value&gt;jdbc:mysql://localhost:3306/mydb&lt;/value&gt;&lt;/property&gt;
    &lt;property name="username"&gt;&lt;value&gt;root&lt;/value&gt;&lt;/property&gt;
  &lt;/bean&gt;

BasicDataSource中我们感兴趣的所有属性都是String类型的,因此我们用&lt;value&gt;元素来指定他们的值。如果必要的话,Spring使用标准的 JavaBean属性编辑器机制来把String转换为其他的类型。

现在,我们定义DAO,它有一个对DataSource的bean引用。Bean间关系通过&lt;ref&gt;元素来指定:

代码:
&lt;bean id="exampleDataAccessObject"
      class="example.ExampleDataAccessObject"&gt;
    &lt;property name="dataSource"&gt;&lt;ref bean="myDataSource"/&gt;&lt;/property&gt;
  &lt;/bean&gt;

The business object has a reference to the DAO, and an int property (exampleParam):
&lt;bean id="exampleBusinessObject"
      class="example.ExampleBusinessObject"&gt;
    &lt;property name="dataAccessObject"&gt;&lt;ref bean="exampleDataAccessObject"/&gt;&lt;/property&gt;
    &lt;property name="exampleParam"&gt;&lt;value&gt;10&lt;/value&gt;&lt;/property&gt;
  &lt;/bean&gt;

&lt;/beans&gt;

对象间的关系一般在配置中明确地设置,象这个例子一样。我们认为这样做是件好事情。然而Spring还提供了我们称做"autowire"的支持, 一个 la PicoContainer,其中它指出了bean间的依赖关系。这样做的局限性——PicoContainer也是如此——是如果有一个特殊类型的多个Bean,要确定那个类型所依赖的是哪个实例是不可能。好的方面是,不满足的依赖可以在factory初始化后被捕获到。(Spring 也为显式的配置提供了一种可选的依赖检查,它可以完成这个目的)

在上面的例子中,如果我们不想显式的编写他们的关系,可使用如下的autowire特性:

代码:
&lt;bean id="exampleBusinessObject"
   class="example.ExampleBusinessObject"
   autowire="byType"&gt;

    &lt;property name="exampleParam"&gt;&lt;value&gt;10&lt;/value&gt;&lt;/property&gt;
&lt;/bean&gt;


使用这个特性,Spring会找出exampleBusinessObject的dataSource属性应该被设置为在当前BeanFactory中找到的DataSource实现。在当前的BeanFactory中,如果所需要类型的bean不存在或多于一个,将产生一个错误。我们依然要设置 exampleParam属性,因为它不是一个引用。

Autowire支持和依赖检查刚刚加入CVS并将在Spring 1.0 M2(到10/20,2003)中提供。本文中所讨论的所有其他特性都包含在当前1.0 M1版本中。

把管理从Java代码中移出来比硬编码有很大的好处,因为这样可以只改变XML文件而无需改变一行Java代码。例如,我们可以简单地改变 myDataSource的bean定义引用不同的bean class以使用别的连接池,或者一个用于测试的数据源。 XML节变成另一种,我们可以用 Spring的JNDI location FactoryBean从应用服务器获取一个数据源。

现在让我们来看看例子中业务对象的java 代码。注意下面列出的代码中没有对Spring的依赖。不像EJB容器,Spring BeanFactory不具有侵入性:在应用对象里面你通常不需要对Spring的存在硬编码。

代码:
public class ExampleBusinessObject implements MyBusinessObject {

   private ExampleDataAccessObject dao;
   private int exampleParam;

   public void setDataAccessObject(ExampleDataAccessObject dao) {
      this.dao = dao;
   }

   public void setExampleParam(int exampleParam) {
      this.exampleParam = exampleParam;
   }

   public void myBusinessMethod() {
      // do stuff using dao
   }
}

注意那些property setter,它们对应于bean定义文档中的XML引用。这些将在对象被使用之前由Spring调用。

这些应用程序的bean不需要依赖于Spring:他们不需要实现任何Spring的接口或者继承Spring的类。他们只需要遵守JavaBeans的命名习惯。在Spring 应用环境之外重用它们是非常简单的,例如,在一个测试环境中。只需要用它们的缺省构造函数实例化它们,并且通过调用 setDataSource()和setExampleParam()手工设置它的属性。如果你想以一行代码支持程序化的创建,只要你有一个无参数的构造器,你就可以自由定义其他需要多个属性的构造函数。

注意在业务接口中没有声明将会一起使用的JavaBean属性。 他们是一个实现细节。我们可以“插入”带有不同bean属性的不同的实现类而不影响连接着的对象或者调用的代码。

当然,Spring XML bean factories 有更多的功能没有在这里描述,但是,应当让你对基本使用有了一些感觉。以及,简单的属性,有 JavaBean属性编辑器的属性,Spring可以自动处理lists,maps和java.util.Properties。

Bean factories 和application contexts 通常和J2EE server定义的一个范围相关联,例如:



Servlet context.:在spring 的MVC 框架里, 每一个包含common objects的web 应用都定义有一个应用程序的 context。Spring提供了通过listener或者servlet实例化这样的context的能力而不需要依赖于Spring 的MVC 框架,因而它也可以用于Struts,WebWork 或者其他的web框架之中。

A Servlet:在Spring MVC 框架里每一个servlet控制器都有它自己的应用程序context,派生于根(全应用程序范围的)应用程序context。在Struts或者其他MVC框架中实现这些也很容意。

EJB:Spring 为EJB提供方便的超类,它们简化了EJB的创建并且提供了一个从EJB Jar 文件中的XML文档载入的BeanFactory。


这些J2EE规范提供的hook通常避免了使用Singleton来创造一个bean factory。

然而,如果我们愿意的话可以用代码创建一个BeanFactory,虽然是没有什么意义的。例如,我们在以下三行代码中可以创建bean factory并且得到一个业务对象的引用:

代码:
InputStream is = getClass().getResourceAsStream("myFile.xml");
XmlBeanFactory bf = new XmlBeanFactory(is);
MyBusinessObject mbo = (MyBusinessObject) bf.getBean("exampleBusinessObject");



这段代码将能工作在一个应用服务器之外:甚至不依赖J2EE,因为Spring 的IoC容器是纯java的。

JDBC 抽象和数据存储异常层次

数据访问是Spring 的另一个闪光点。

JDBC 提供了还算不错的数据库抽象,但是需要用痛苦的API。这些问题包括:



需要冗长的错误处理代码来确保ResultSets,Statements以及(最重要的)Connections在使用后关闭。这意味着对JDBC的正确使用可以快速地导致大量的代码量。它还是一个常见的错误来源。Connection leak可以在有负载的情况下快速宕掉应用程序。

SQLException相对来说不能说明任何问题。JDBC不提供异常的层次,而是用抛出SQLException来响应所有的错误。找出到底哪里出错了——例如,问题是死锁还是无效的SQL?——要去检查SQLState或错误代码。这意味着这些值在数据库之间是变化的。

Spring用两种方法解决这些问题:



提供API,把冗长乏味和容易出错的异常处理从程序代码移到框架之中。框架处理所有的异常处理;程序代码能够集中精力于编写恰当的SQL和提取结果上。

为你本要处理SQLException程序代码提供有意义的异常层次。当Spring第一次从数据源取得一个连接时,它检查元数据以确定数据库。它使用这些信息把SQLException映射为自己从org.springframework.dao.DataAccessException派生下来的类层次中正确的异常。因而你的代码可以与有意义的异常打交道,并且不需要为私有的SQLState或者错误码担心。Spring的数据访问异常不是JDBC特有的,因而你的DAO并不一定会因为它们可能抛出的异常而绑死在JDBC上。

Spring提供两层JDBC API。第一个时,在org.springframework.jdbc.core包中,使用回调机制移动控制权——并且因而把错误处理和连接获取和释放——从程序的代码移到了框架之中。这是一种不同的Inversion of Control,但是和用于配置管理的几乎有同等重要的意义。

Spring使用类似的回调机制关注其他包含特殊获取和清理资源步骤的API,例如JDO(获取和释放是由PersistenceManager完成的),事务管理(使用JTA)和JNDI。Spring中完成这些回调的类被称作template。

例如,Spring的JdbcTemplate对象能够用于执行SQL查询并且在如下的列表中保存结果:

代码:
JdbcTemplate template = new JdbcTemplate(dataSource);
final List names = new LinkedList();
template.query("SELECT USER.NAME FROM USER",
   new RowCallbackHandler() {
      public void processRow(ResultSet rs) throws SQLException {
         names.add(rs.getString(1));
      }
   });


注意回调中的程序代码是能够自由抛出SQLException的:Spring将会捕捉到这些异常并且用自己的类层次重新抛出。程序的开发者可以选择哪个异常,如果有的话,被捕捉然后处理。

JdbcTemplate提供许多支持不同情景包括prepared statements和批量更新的方法。Spring的JDBC抽象有比起标准JDBC来说性能损失非常小,甚至在当应用中需要的结果集数量很大的时候。

在org.springframework.jdbc.object包中是对JDBC的更高层次的抽象。这是建立在核心的JDBC回调功能基础纸上的,但是提供了一个能够对RDBMS操作——无论是查询,更新或者是存储过程——使用Java对象来建模的API。这个API部分是受到JDO查询API的影响,我发现它直观而且非常有用。

一个用于返回User对象的查询对象可能是这样的:

代码:

class UserQuery extends MappingSqlQuery {

   public UserQuery(DataSource datasource) {
      super(datasource, "SELECT * FROM PUB_USER_ADDRESS WHERE USER_ID = ?");
      declareParameter(new SqlParameter(Types.NUMERIC));
      compile();
   }

   // Map a result set row to a Java object
   protected Object mapRow(ResultSet rs, int rownum) throws SQLException {
      User user = new User();
      user.setId(rs.getLong("USER_ID"));
      user.setForename(rs.getString("FORENAME"));
      return user;
   }

   public User findUser(long id) {
      // Use superclass convenience method to provide strong typing
      return (User) findObject(id);
   }
}


这个类可以在下面用上:
代码:

User user = userQuery.findUser(25);


这样的对象经常可以用作DAO的inner class。它们是线程安全的,除非子类作了一些超出常规的事情。

在org.springframework.jdbc.object包中另一个重要的类是StoredProcedure类。Spring让存储过程通过带有一个业务方法的Java类进行代理。如果你喜欢的话,你可以定义一个存储过程实现的接口,意味着你能够把你的程序代码从对存储过程的依赖中完全解脱出来。

Spring数据访问异常层次是基于unchecked(运行时)exception的。在几个工程中使用了Spring之后,我越来越确信这个决定是正确的。

数据访问异常一般是不可恢复的。例如,如果我们不能链接到数据库,某个业务对象很有可能就不能完成要解决的问题了。一个可能的异常是 optimistic locking violation,但是不是所有的程序使用optimistic locking。强制编写捕捉其无法有效处理的致命的异常通常是不好的。让它们传播到上层的handler,比如servlet或者EJB 容器通常更加合适。所有的Spring对象访问异常都是 DataAccessException的子类,因而如果我们确实选择了捕捉所有的Spring数据访问异常,我们可以很容易做到这点。

注意如果我们确实需要从unchecked数据访问异常中恢复,我们仍然可以这么做。我们可以编写代码仅仅处理可恢复的情况。例如,如果我们认为只有optimistic locking violation是可恢复的,我们可以在Spring的DAO中如下这么写:

代码:

try {
   // do work
}
catch (OptimisticLockingFailureException ex) {
   // I'm interested in this
}


如果Spring的数据访问异常是checked的,我们需要编写如下的代码。注意我们还是可以选择这么写:
代码:

try {
   // do work
}
catch (OptimisticLockingFailureException ex) {
   // I'm interested in this
}
catch (DataAccessException ex) {
   // Fatal; just rethrow it
}


第一个例子的潜在缺陷是——编译器不能强制处理可能的可恢复的异常——这对于第二个也是如此。因为我们被强制捕捉base exception (DataAccessException),编译器不会强制对子类(OptimisticLockingFailureException)的检查。因而编译器可能强制我们编写处理不可恢复问题的代码,但是对于强制我们处理可恢复的问题并未有任何帮助。

Spring对于数据访问异常的unchecked使用和许多——可能是大多数——成功的持久化框架是一致的。(确实,它部分是受到JDO的影响。) JDBC是少数几个使用checked exception的数据访问API之一。例如TopLink和JDO大量使用 unchecked exception。Gavin King现在相信Hibernate也应该选择使用unchecked exception。

Spring的JDBC能够用以下办法帮助你:




你决不需要在使用JDBC时再编写finally block。

总的来说你需要编写的代码更少了

你再也不需要挖掘你的RDBMS的文档以找出它为错误的列名称返回的某个罕见的错误代码。你的程序不再依赖于RDBMS特有的错误处理代码。

无论使用的是什么持久化技术,你都会发现容易实现DAO模式,让业务代码无需依赖于任何特定的数据访问API。


在实践中,我们发现所有这些都确实有助于生产力的提高和更少的bug。我过去常常厌恶编写JDBC代码;现在我发现我能够集中精力于我要执行的SQL,而不是烦杂的JDBC资源管理。

如果需要的话Spring的JDBC抽象可以独立使用——不强迫你把它们用作Spring的一部分。
O/R mapping 集成

当然你经常需要使用O/R mapping,而不是使用关系数据访问。你总体的应用程序框架也必须支持它。因而提供了对Hibernate 2.x和 JDO的集成支持。它的数据访问架构使得它能和任何底层的数据访问技术集成。Spring和Hibernate集成得尤其好。

为什么你要使用Hibernate加Spring,而不是直接使用Hibernate?




Session 管理 Spring提供有效率的,简单的以并且是安全的处理Hibernate Session。使用Hibernate的相关代码为了效率和恰当的事务处理一般需要使用相同的Hibernate “Session”对象。Spring让它容易透明地创建和绑定Session到当前的线程,要么使用声明式,AOP的method interceptor方法,要么在Java代码层面使用显式的,“template”包装类。因而 Spring解决了在Hibernate论坛上经常出现的用法问题。

资源管理 Spring的应用程序context能够处理Hiberante SessionFactories的位置和配置,JDBC数据源和其他相关资源。这使得这些值易于管理和改变。

集成的事务管理 Spring让你能够把你的Hibernate代码包装起来,要么使用声明式,AOP风格的method interceptor,要么在Java代码层面显式使用“template”包装类。在两种做法中,事务语义都为你处理了,并且在异常时也做好了恰当的事务处理(回滚,等)。如下面讨论的,你还获得了能够使用和替换不同transaction manager,而不会让你相关Hibernate代码受到影响的能力。额外的,JDBC 相关的代码能够完全事务性的和Hibernate代码集成。这对于处理没有在Hibernate实现的功能很有用。

如上描述的异常包装 Spring能够包装Hibernate异常,把它们从私有的,checked异常转换为一套抽象的运行时异常。这使得你能够仅仅在恰当的层面处理大部分不可恢复的持久化异常,而不影响样板catch/throw,和异常声明。你仍然能够在任何你需要的地方捕捉和处理异常。记住 JDBC异常(包括DB特有的方言)也被转换到相同的层次中,意味着你能在一致的编程模型中对JDBC执行相同的操作。

为了避免和厂商绑定 Hibernate是强大的,灵活的,开放源代码并且免费,但是它仍然使用私有的API。给出了一些选择,使用标准或者抽象API实现主要的程序功能通常是你想要的,当你需要因为功能,性能,或者其他考虑要转换到使用其他实现时。

让测试变简单 Spring的Inversion of Control方法使得改变Hibernate的session factories,数据源, transaction manager的实现和位置很容易,如果需要的话还能改变mapper object的实现。这使得更加容易分离和测试持久化相关的代码。
事务管理
抽象出一个数据访问的API是不够的;我们还需要考虑事务管理。JTA是显而易见的选择,但是它是一个直接用起来很笨重的API,因而许多J2EE开发者感到EJB CMT是对于事务管理唯一合理的选择。

Spring提供了它自己对事务管理的抽象。Spring提供了这些:



通过类似于JdbcTemplate的回调模板编程管理事务,比起直接使用JTA要容易多了

类似于EJB CMT的声明式事务管理,但是不需要EJB容器


Spring的事务抽象式唯一的,它不绑定到JTA或者任何其他事务管理技术。Spring使用事务策略的概念把程序代码和底层的事务架构(例如JDBC)解藕。

为什么你要关心这些?JTA不是所有事务管理的最好答案吗?如果你正在编写仅仅使用一个数据库的程序,你不需要JTA的复杂度。你不关心XA事务或者两阶段提交。你甚至不需要提供这些东西的高端应用服务器。但是另一方面,你不会希望在需要和多个数据源打交道的时候重写你的代码。

假定你决定通过直接使用JDBC或者Hibernate的事务以避免JTA带来的额外负担。一旦你需要处理多个数据源,你必须剥开所有的事务管理代码并且使用JTA事务来替代。这不是非常有吸引力的并且导致大部分J2EE程序员,包括我自己,推荐只使用全局JTA事务。然而使用Spring事务抽象,你只需要重新配置Spring让它使用JTA,而不是JDBC或者Hibernate的事务策略,就一切OK了。这是一个配置上的改变,而不是代码的改动。因而,Spring使得你能够自由缩放应用。
AOP

最近在应用AOP来解决企业关注点方面大家有了很大的兴趣,例如事务管理,这些都是EJB所要解决的。

Spring的AOP支持的首要目标是要给POJOs提供J2EE服务。这类似于JBoss 4的目标,Spring AOP由它能够在应用服务器之间移植的优势,因而没有绑死在厂商身上的风险。它既可以在web或者EJB容器中使用,也能够在WebLogic,Tomcat,JBoss,Resin, Jetty,Orion和许多其他应用服务器和web容器上使用。

Spring AOP支持method interception。所支持关键的AOP概念包括:



Interception:自定义行为能够在对接口和类的调用之前和之后插入。这类似于AspectJ术语中类似的“around advice”。

Introduction:指定advice会导致对象实现额外的接口。这混乱了继承。

静态和动态的pointcuts:在interception发生的程序执行处指定points。静态pointcuts concern函数签名;动态 pointcuts也可以在point被求值的地方考虑函数的参数。Pointcuts独立interceptors单独定义,使得标准 interceptor可以应用于不同应用程序和代码上下文。


Spring既支持有状态(一个advised对象一个实例)也支持无状态的interceptors(所有advice使用一个实例)。

Spring不支持field interception。这是一个经过深思熟虑的设计决定。我总是感觉field interception违反了封装。我比较倾向于把AOP作为补全物,而不是与OOP冲突的东西。如果在5年或者10年后,我们在AOP学习曲线上走得更远了并且觉得应该在程序设计的桌面上给AOP一个位置,我不会惊讶的。(然而在那个时候基于语言的解决方案例如AspectJ可能比它们今天看来更加具有吸引力。)

Spring使用动态代理实现AOP(其中存在一个接口)或者在运行时使用CGLIB生成字节码(这使得能够代理类)。两种方法都能够在任何应用服务器中使用。

Spring是第一个实现AOP Alliance interfaces的AOP 框架(www.sourceforge.net/projects/aopalliance)。这些是定义在不同AOP框架中能够互操作interceptors的尝试。

在TheServerSide和其他地方有一个正在进行但是不是那么引人注目的争论,就是这种interception是不是“true AOP”。我倒不怎么在意它叫什么;仅仅需要知道它是否在实践中有用就好了。我也乐于称它为“declarative middleware”(声明式中间件)。把 Spring AOP认做简单,轻量级的无状态beans的替代物,这样就不需要monolithic EJB容器了,而这些仅仅是让你能够构建有你需要的服务的容器。我不推荐advising任何一个POJO,对local SLSBs的类比有助于你理解推荐的粒度。(然而,与EJB不同的是,在恰当但是少见的情况下,你可以自由地把Spring的AOP应用到粒度更好的对象上。)

因为Spring在实例上advises 对象,而不是在class loader层面上,使用有不同advice的同一个类的多个实例是可能的,或者与advised实例一道使用unadvised 实例。

可能Spring AOP最常见的应用是声明式事务管理。这是基于前面描述的TansactionTemplate抽象上的,并且可以给任何POJO提供声明式事务管理。取决于事务策略,底层的机制可以是JTA,JDBC,Hibernate或者任何其他提供事务管理的API。

Spring的声明式事务管理类似于EJB CMT,在以下方面有些不同:



事务管理能够应用于任何POJO。我们推荐业务对象实现接口,但是这只是一个好的编程习惯的问题,而不是由框架强制的。

通过使用Spring的事务API能够在事务性POJO中实现编程回调。我们为此提供静态的方法,使用ThreadLoacal变量,因而你不需要传播诸如EJBContext这样的context对象来确保回滚。

你可以声明式地定义“回滚规则”。EJB不会在未捕捉程序异常的时候自动回滚(仅仅在unchecked exceptions和其他 Throwables的时候),应用程序开发者经常需要在任何异常发生时回滚。Spring事务管理让你能够声明式地指定什么异常什么子类能够导致自动回滚。缺省的行为和EJB是一致的,但是你能够在checked和unchecked异常时自动回滚。这个在最少化自编程回调代码方面有很大好处,而回调依赖于Spring的事务API(因为EJB的编程回调时在EJBContext中完成的)。

事务管理不绑定于JTA。如前面解释过的,Spring的事务管理能够在不同事务策略中使用。


当然还可以使用Spring AOP实现程序特有的aspects。取决于你对AOP概念的接受程度,决定你是否选择这么做,而不是Spring的能力,但是它确实非常有用。我们所见过的成功例子包括:



自定义的security interception,当安全检查的复杂度超出了J2EE安全架构的能力的时候

在开发中使用的调试和profiling aspects

发送email通知管理员用户不寻常的举动的Interceptors


程序自定的aspects能够成为消除需要许多函数的样板代码的有利武器。

Spring AOP透明地与Spring BeanFactory概念集成。包含一个来自Spring BeanFactory对象地代码不需要知道它是还是不是advised。和任何对象一样,契约实在接口和对象实现中定义的。

下面的XML片断展示了如何定义一个AOP代理:
代码:

&lt;bean id="myTest"
   class="org.springframework.aop.framework.ProxyFactoryBean"&gt;
   &lt;property name="proxyInterfaces"&gt;
      &lt;value&gt;org.springframework.beans.ITestBean&lt;/value&gt;
   &lt;/property&gt;
   &lt;property name="interceptorNames"&gt;
      &lt;list&gt;
         &lt;value&gt;txInterceptor&lt;/value&gt;
         &lt;value&gt;target&lt;/value&gt;
      &lt;/list&gt;
   &lt;/property&gt;
&lt;/bean&gt;


注意bean类的定义总是AOP框架的ProxyFactoryBean,虽然bean的类型在引用中使用或者由BeanFactory的getBean ()方法返回时依赖的是代理接口。(多个代理方法是被支持的。)ProxyFactoryBean的“interceptorNames”属性需要一个字符串列表。(因为如果代理是一个“prototype”而不是singleton,有状态interceptors可能需要创建新的实例,所以必须使用 Bean的名字而不是bean的引用。)列表中的名字可以是interceptor或者pointcuts(interceptors和有关它们合适被使用的信息)。列表中的“target”值自动创建一个“invoker interceptor”封装target对象。实现代理接口的是在 factory中的bean的名字。这个例子中的myTest可以和其他bean factory中的bean一样使用。例如,其他对象可以使用&lt; ref&gt;元素引用它而且这些引用是由Spring IoC设置的。

还可以不用BeanFactory,编程构建AOP代理,虽然这个很少用得上:

代码:

TestBean target = new TestBean();
DebugInterceptor di = new DebugInterceptor();
MyInterceptor mi = new MyInterceptor();
ProxyFactory factory = new ProxyFactory(target);
factory.addInterceptor(0, di);
factory.addInterceptor(1, mi);
// An "invoker interceptor" is automatically added to wrap the target
ITestBean tb = (ITestBean) factory.getProxy();


我们相信最好把程序装配从Java代码中移出来,而AOP也不例外。

Spring在它的AOP能力方面的直接竞争者是Jon Tirsen的Nanning Aspects(http://nanning.codehaus.org)。

我觉得AOP作为EJB的替代无提供企业服务这个用法方面的进步是重要的。随着时间,这将成为Spring很重要的关注点。
MVC web 框架

Spring包括一个强大而且高度可配置的MVC web 框架。

Spring的MVC model类似于Struts。在多线程服务对象这点上,Spring的Controller类似于Struts Action,只有一个实例处理所有客户的请求。然而,我们相信Spring的MVC比起Struts有很多优点,例如:



Spring在controllers,JavaBean,models和views提供了一个非常清晰的划分。

Spring的MVC是非常灵活的。不像Struts,它强制你的Action和Form对象进入固化的层次之中(因而你迫使你使用Java的实体继承),Spring MVC完全是基于接口的。而且,通过插入你自己的接口几乎Spring MVC 框架的所有部分都是可配置的。当然我们也提供了方便的类作为实现选择。

Spring MVC是真正的view无关的。你不会被强制使用JSP,如果你不想那么做的话。你可以使用Velocity,XSLT或其他view技术。如果你想要使用自定义的view机制——例如,你自己的模板语言——你可以简单实现Spring的View接口并且把它集成进来。

和其他对象一样,Spring的Controllers是通过IoC配置的。着使得它们易于测试,并且完美地和其他由Spring管理的对象集成。

Web层变成了业务对象层之上的薄薄一层。这鼓励了好的习惯。Struts和其他专门的web框架让你去实现你自己的业务对象;Spring提供了你应用程序所有层的集成。


如在Struts 1.1中所见的,你可以有和你在Spring MVC 应用程序中所需要的一样多的dispatcher servlets。

下面的例子展示了一个简单的Spring Controller如何能够访问定义在应用程序context中的业务对象。这个controller在它的handleRequest()方法中执行了Google搜索:

代码:

public class GoogleSearchController
      implements Controller {

   private IGoogleSearchPort google;

   private String googleKey;

   public void setGoogle(IGoogleSearchPort google) {
      this.google = google;
   }

   public void setGoogleKey(String googleKey) {
      this.googleKey = googleKey;
   }

   public ModelAndView handleRequest(
            HttpServletRequest request, HttpServletResponse response)
      throws ServletException, IOException {
      String query = request.getParameter("query");
      GoogleSearchResult result =
         // Google property definitions omitted...

         // Use google business object
         google.doGoogleSearch(this.googleKey, query,
            start, maxResults, filter, restrict,
            safeSearch, lr, ie, oe);

      return new ModelAndView("googleResults", "result", result);
   }
}


这段代码使用的prototype中,IGoogleSearchPort是一个GLUE web services代理,由 Spring FActoryBean返回。然而,Spring把controller从底层web service库中分离出来。接口可以使用普通的 Java对象,test stub,mock对象或者如下面要讨论的EJB代理实现。这个contorller不包括资源查找;除了支持它的web交互的必要代码之外没有别的什么了。

Spring还提供了数据绑定,forms,wizards和更复杂的工作流的支持。

对Spring MVC 框架的优秀简介是Thomas Risberg的Spring MVC 教程(http://www.springframework.org/docs/MVC-step-by-step/Spring-MVC-step-by-step.html)。还可以参见“Web MVC with the Spring Framework”(http://www.springframework.org/docs/web_mvc.html)。

如果你乐于使用你钟情的MVC框架,Spring的分层架构使得你能够使用Spring的其他部分而不用MVC层。我们有使用Spring做中间层管理和数据访问,但是在web层使用Struts,WebWork或者Tapestry的用户。
实现EJB

如果你选择使用EJB,Spring能在EJB实现和客户端访问EJB两方面都提供很大的好处。

对业务逻辑进行重构,把它从EJB facades中取出到POJO已经得到了广泛的认同。(不讲别的,这使得业务逻辑更容易单元测试,因为EJB严重依赖于容器而难于分离测试。)Spring为session bean和message driver bean提供了方便的超类,使得通过自动载入基于包含在EJB Jar文件中的XML文档BeanFactory让这变得很容易。

这意味着stateless session EJB可以这么获得和使用所需对象:

代码:

import org.springframework.ejb.support.AbstractStatelessSessionBean;

public class MyEJB extends AbstractStatelessSessionBean
         implements MyBusinessInterface {
   private MyPOJO myPOJO;

   protected void onEjbCreate() {
      this.myPOJO = getBeanFactory().getBean("myPOJO");
   }

   public void myBusinessMethod() {
      this.myPOJO.invokeMethod();
   }
}


假定MyPOJO是一个接口,它的实现类——以及任何它需要的配置,注入基本的属性和更多的合作者——在XML bean factory 定义中隐藏。

我们通过在ejb-jar.xmldeployment descriptor中名为ejb/BeanFactoryPath的环境变量定义告诉Spring去哪儿装载XML文档。如下:

代码:

&lt;session&gt;
   &lt;ejb-name&gt;myComponent&lt;/ejb-name&gt;
   &lt;local-home&gt;com.test.ejb.myEjbBeanLocalHome&lt;/local-home&gt;
   &lt;local&gt;com.mycom.MyComponentLocal&lt;/local&gt;
   &lt;ejb-class&gt;com.mycom.MyComponentEJB&lt;/ejb-class&gt;
   &lt;session-type&gt;Stateless&lt;/session-type&gt;
   &lt;transaction-type&gt;Container&lt;/transaction-type&gt;

   &lt;env-entry&gt;
      &lt;env-entry-name&gt;ejb/BeanFactoryPath&lt;/env-entry-name&gt;
      &lt;env-entry-type&gt;java.lang.String&lt;/env-entry-type&gt;
      &lt;env-entry-value&gt;/myComponent-ejb-beans.xml&lt;/env-entry-value&gt;&lt;/env-entry&gt;
   &lt;/env-entry&gt;
&lt;/session&gt;


myComponent-ejb-beans.xml 文件将会从classpath装载:在本例中,是EJB Jar文件的根目录。每个EJB都能指定自己的XML文档,因而这个机制能在每个EJB Jar文件中使用多次。

Spring 的超类实现了EJB中诸如setSessionContext()和ejbCreate()的生命周期管理的方法,让应用程序开发者只需选择是否实现Spring的onEjbCreate()方法。
使用EJB

Spring还让实现EJB变得更加容易。许多EJB程序使用Service Locator和Business Delegate模式。这些比在客户代码中遍布JNDI查找强多了,但是它们常见的实现方式有显著的缺点,例如:



使用EJB的典型代码依赖Service Locator或者Business Delegate singletons,使得测试难于进行。

在Service Locator模式没有使用Business Delegate的情况下,程序代码还要在EJB home中调用create()方法,并且处理可能导致的异常。因而仍然绑定在EJB API身上,忍受着EJB 编程模型的复杂度。

实现Business Delegate模式通常导致显著的代码重复,其中我们必须编写大量仅仅是调用EJB同等方法的方法。


基于这些和其他原因,传统的EJB访问,如在Sun Adventure Builder和OTN J2EE Virtual Shopping Mall中展示的那样,会降低生产率并且带来显著的复杂度。

Spring通过引入codeless business delegate前进了一步。有了Spring,你不再需要再编写另一个 Service Locator,另一个JNDI查找,或者在硬编码的Business Delegate中重复代码,除非你肯定这增加了价值。

例如,假定我们有使用local EJB的web controller。我们将遵循最佳实践,使用 EJB Business Methods Interface模式,EJB的local interface extend非EJB专有的业务方法接口。(这么做的主要的一个原因是确保在本地接口和bean实现类中方法签名的自动同步。)让我们调用这个业务方法接口MyComponent。当然我们还需要实现local home接口并且提供实现SessionBean和MyComponent业务方法的bean的实现类。

用了Spring EJB 访问,我们把我们的web层controller和EJB实现挂接上所需要进行的Java编码仅仅是在我们的controller中暴露一个类型MyComponent的setter方法。这将如下保存作为实例变量的引用:

代码:

private MyComponent myComponent;

public void setMyComponent(MyComponent myComponent) {
   this.myComponent = myComponent;
}


我们随后在任何业务方法中使用这个实例变量。

Spring自动完称剩下的工作,通过像这样的XML bean定义。LocalStatelessSessionProxyFactoryBean是一个可以用于任何EJB的通用factory bean。它创建的对象能够自动被Spring转型为MyComponent类型。

代码:

&lt;bean id="myComponent"
class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"&gt;

   &lt;property name="jndiName"&gt;&lt;value&gt;myComponent&lt;/value&gt;&lt;/property&gt;
   &lt;property name="businessInterface"&gt;&lt;value&gt;com.mycom.MyComponent&lt;/value&gt;&lt;/property&gt;
&lt;/bean&gt;

&lt;bean id="myController"
   class = "com.mycom.myController"
&gt;
   &lt;property name="myComponent"&gt;&lt;ref bean="myComponent"/&gt;&lt;/property&gt;
&lt;/bean&gt;


在幕后有许多魔法般的事情发生,Spring AOP framework的殷勤,虽然不强迫你使用AOP的概念享受这些结果。 “myComponent”bean定义为EJB创建一个代理,它实现了业务方法的接口。EJB local home在启动的时候被缓存,因而只需要一次JNDI查找。每次EJB被调用的时候,代理调用local EJB中的create()方法并且调用EJB中对应的业务方法。

myController bean定义为这个代理设置controller类的myController属性。

这个EJB访问机制极大简化了应用程序的代码:



Web层的代码不依赖于EJB的使用。如果你想要使用POJO,mock object或者其他test stub替代EJB引用,我们可以简单地改动一下myComponent bean定义而不影响一行Java代码

我们还不需要写一行JNDI查找或者其他EJB plumbing code。


在实际程序中的性能测试和经验标明这种方法(包括对目标EJB的反射调用)的性能影响是很小的,在典型的应用中检测不出。记住无论如何我们都不希望使用fine-grained的EJB调用,因为会有有关应用服务器上的EJB的底层架构方面的代价。

我们可以把相同方法应用于远程EJB,通过类似 org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean factory bean 的方法。然而我们无法隐藏远程EJB的业务方法接口中的RemoteException。
测试

如你可能已经注意到的,我和其他Spring开发这是全面单元测试重要性的坚定支持者。我们相信框架被彻底单元测试过的是非常重要的,而且我们框架设计的主要目标是让建立在框架之上的程序易于单元测试。

Spring自身有一个极好的单元测试包。我们的1.0 M1的单元测试覆盖率是75%,而且我们希望在1.0 RC1的时候能够达到80%的单元测试覆盖率。我们发现在这个项目中测试优先的开发带来的好处是实实在在的。例如,它使得作为国际化分布式开发团队的工作极端有效率,而且用户评论 CVS snapshots趋向于稳定和使用安全。

因为以下理由,我们相信用Spring构建的应用程序是非常易于测试的:



IoC推动了单元测试

应用程序不包括直接使用注入JNDI的J2EE服务的plumbing code,这些代码一般让测试难于进行

Spring bean factories和contexts能够在容器外设置


在容器外可以设置Spring bean factory的能力提供了对开发过程有趣的可选项。在几个使用Spring的web应用中,工作是从定义业务接口和在web容器外集成测试开始的。在业务功能已经足够完整之后,web接口不过是添加在其上的薄薄一层。
谁在使用Spring

虽然相对来说Spring还是一个新的项目,但是我们已经有了一个令人印象深刻并且不断增长的用户群。它们已经有许多产品使用着Spring。用户包括一个主要的全球投资银行(做大型项目的),一些知名的网络公司,几个web开发顾问机构,卫生保健公司,以及学院机构。

许多用户完整地使用Spring,但是一些只单独使用一些组件。例如,大量用户使用我们地JDBC或者其他数据访问功能。
Roadmap

在今年晚些时候我们主要要做的是让Spring发布release 1.0。然而,我们还有一些更长远的目标。

为1.0 final规划地主要改进式源代码级地元数据支持,它主要用于(但不局限于)AOP框架。这将使得C#风格的attribute驱动的事务管理,并且让声明式企业服务在典型应用情况下非常容易配置。Attribute支持将会在Spring的1.0 final release支持中加入,并且设计的是在发布的那个时候能与JSR-175集成。

1.0之后,一些可能的改进地方包括:



通过对我们的JDBC和事务支持的一个相当抽象来支持JMS

支持bean factories的动态重配置

提供web services的能力

IDE和其他工具支持


作为一个敏捷项目,我们主要是受到用户需求的驱动。因而我们不会开发没有一个用户需要的特性,并且我们会仔细倾听来自用户群的声音。
总结

Spring是一个解决了许多在J2EE开发中常见的问题的强大框架。

Spring提供了管理业务对象的一致方法并且鼓励了注入对接口编程而不是对类编程的良好习惯。Spring的架构基础是基于使用JavaBean属性的 Inversion of Control容器。然而,这仅仅是完整图景中的一部分:Spring在使用IoC容器作为构建完关注所有架构层的完整解决方案方面是独一无二的。

Spring提供了唯一的数据访问抽象,包括简单和有效率的JDBC框架,极大的改进了效率并且减少了可能的错误。Spring的数据访问架构还集成了Hibernate和其他O/R mapping解决方案。

Spring还提供了唯一的事务管理抽象,它能够在各种底层事务管理技术,例如JTA或者JDBC纸上提供一个一致的编程模型。

Spring提供了一个用标准Java语言编写的AOP框架,它给POJOs提供了声明式的事务管理和其他企业事务——如果你需要——还能实现你自己的aspects。这个框架足够强大,使得应用程序能够抛开EJB的复杂性,同时享受着和传统EJB相关的关键服务。

Spring还提供了可以和总体的IoC容器集成的强大而灵活的MVC web框架。
更多信息

参见以下资源获得关于Spring的更多信息:



Expert One-on-One J2EE Design and Development(Rod Johnson,Wrox,2002)。虽然Spring在书出版之后已经极大地进步和改进了,它仍然是理解Spring动机的极佳途径。

Spring的主页:http://www.springframework.org。这里包括Javadoc和几个教程。

在Sourceforge上的论坛和下载

Spring用户和Spring开发者的邮件列表


我们正在尽我们可能去改进Spring的文档和示例。我们还为在信件和邮件列表中极好的回复率自豪。我们希望你能快速融入我们的社区!
关于作者

Rod Johnson 作为Java开发者和架构师已经有了7年的经验了并且在J2EE平台出现之初就在其上进行开发了。他是《Expert One- on-One J2EE Design and Development》(Wrox,2002)的作者并且贡献了其他好几本关于J2EE的书。他当前正在为Wiley撰写另外一本有关J2EE架构的书。Rod在两个Java标准委员会服务并且经常师大会发言人。现在他在UK做一个咨询顾问。
posted @ 2009-04-07 23:30 lanxin1020 阅读(125) | 评论 (0)编辑 收藏
Spring的AbstractApplicationContext是ApplicationContext抽象实现类,该抽象类的refresh()方法定义了Spring容器在加载配置文件后的各项处理过程,这些处理过程清晰刻画了Spring容器启动时所执行的各项操作。下面,我们来看一下refresh()内部定义了哪些执行逻辑:
1.初始化BeanFactory:根据配置文件实例化BeanFactory,getBeanFactory()方法由具体子类实现。在这一步里,Spring将配置文件的信息装入到容器的Bean定义注册表(BeanDefinitionRegistry)中,但此时Bean还未初始化;
2.调用工厂后处理器:根据反射机制从BeanDefinitionRegistry中找出所有BeanFactoryPostProcessor类型的Bean,并调用其postProcessBeanFactory()接口方法;
3.注册Bean后处理器:根据反射机制从BeanDefinitionRegistry中找出所有BeanPostProcessor类型的Bean,并将它们注册到容器Bean后处理器的注册表中;
4.初始化消息源:初始化容器的国际化信息资源;
5.初始化应用上下文事件广播器;
6.初始化其他特殊的Bean:这是一个钩子方法,子类可以借助这个钩子方法执行一些特殊的操作:如AbstractRefreshableWebApplicationContext就使用该钩子方法执行初始化ThemeSource的操作;
7.注册事件监听器;
8.初始化singleton的Bean:实例化所有singleton的Bean,并将它们放入Spring容器的缓存中;
9.发布上下文刷新事件:创建上下文刷新事件,事件广播器负责将些事件广播到每个注册的事件监听器中。
在第3.4节中,我们观摩了Bean从创建到销毁的生命历程,这些过程都可以在上面的过程中找到对应的步骤。Spring协调多个组件共同完成这个复杂的工程流程,图5-1描述了Spring容器从加载配置文件到创建出一个完整Bean的作业流程以及参与的角色。
图5-1  IoC的流水线
1.ResourceLoader从存储介质中加载Spring配置文件,并使用Resource表示这个配置文件的资源;
2.BeanDefinitionReader读取Resource所指向的配置文件资源,然后解析配置文件。配置文件中每一个<bean>解析成一个BeanDefinition对象,并保存到BeanDefinitionRegistry中;
3.容器扫描BeanDefinitionRegistry中的BeanDefinition,使用Java的反射机制自动识别出Bean工厂后处理器(实现BeanFactoryPostProcessor接口)的Bean,然后调用这些Bean工厂后处理器对BeanDefinitionRegistry中的BeanDefinition进行加工处理。主要完成以下两项工作:
1)对使用到占位符的<bean>元素标签进行解析,得到最终的配置值,这意味对一些半成品式的BeanDefinition对象进行加工处理并得到成品的BeanDefinition对象;
2)对BeanDefinitionRegistry中的BeanDefinition进行扫描,通过Java反射机制找出所有属性编辑器的Bean(实现java.beans.PropertyEditor接口的Bean),并自动将它们注册到Spring容器的属性编辑器注册表中(PropertyEditorRegistry);
4.Spring容器从BeanDefinitionRegistry中取出加工后的BeanDefinition,并调用InstantiationStrategy着手进行Bean实例化的工作;
5.在实例化Bean时,Spring容器使用BeanWrapper对Bean进行封装,BeanWrapper提供了很多以Java反射机制操作Bean的方法,它将结合该Bean的BeanDefinition以及容器中属性编辑器,完成Bean属性的设置工作;
6.利用容器中注册的Bean后处理器(实现BeanPostProcessor接口的Bean)对已经完成属性设置工作的Bean进行后续加工,直接装配出一个准备就绪的Bean。
Spring容器确实堪称一部设计精密的机器,其内部拥有众多的组件和装置。Spring的高明之处在于,它使用众多接口描绘出了所有装置的蓝图,构建好Spring的骨架,继而通过继承体系层层推演,不断丰富,最终让Spring成为有血有肉的完整的框架。所以查看Spring框架的源码时,有两条清晰可见的脉络:
1)接口层描述了容器的重要组件及组件间的协作关系;
2)继承体系逐步实现组件的各项功能。
接口层清晰地勾勒出Spring框架的高层功能,框架脉络呼之欲出。有了接口层抽象的描述后,不但Spring自己可以提供具体的实现,任何第三方组织也可以提供不同实现, 可以说Spring完善的接口层使框架的扩展性得到了很好的保证。纵向继承体系的逐步扩展,分步骤地实现框架的功能,这种实现方案保证了框架功能不会堆积在某些类的身上,造成过重的代码逻辑负载,框架的复杂度被完美地分解开了。
Spring组件按其所承担的角色可以划分为两类:
1)物料组件:Resource、BeanDefinition、PropertyEditor以及最终的Bean等,它们是加工流程中被加工、被消费的组件,就像流水线上被加工的物料;
2)加工设备组件:ResourceLoader、BeanDefinitionReader、BeanFactoryPostProcessor、InstantiationStrategy以及BeanWrapper等组件像是流水线上不同环节的加工设备,对物料组件进行加工处理。
我们在第3章中已经介绍了Resource和ResourceLoader这两个组件。在本章中,我们将对其他的组件进行讲解。
posted @ 2009-04-07 23:06 lanxin1020 阅读(270) | 评论 (0)编辑 收藏