学习Spring必学的Java基础知识

引述要学习Spring框架的技术内幕,必须事先掌握一些基本的Java知识,正所谓“登高必自卑,涉远必自迩”。以下几项Java知识和Spring框架息息相关,不可不学(我将通过一个系列分别介绍这些Java基础知识,希望对大家有所帮助。):

[1] Java反射知识–>Spring IoC :http://www.iteye.com/topic/1123081
[2] Java动态代理–>Spring AOP :http://www.iteye.com/topic/1123293
[3] 属性编辑器,即PropertyEditor–>Spring IoC:http://www.iteye.com/topic/1123628
[4] XML基础知识–>Spring配置:http://www.iteye.com/topic/1123630
[5] 注解–>Spring配置:http://www.iteye.com/topic/1123823
[6] 线程本地变更,即ThreadLocal–>Spring事务管理:http://www.iteye.com/topic/1123824
[7] 事务基础知识–>Spring事务管理:http://www.iteye.com/topic/1124043
[8] 国际化信息–>MVC:http://www.iteye.com/topic/1124044
[9] HTTP报文–>MVC:http://www.iteye.com/topic/1124408

Java语言允许通过程序化的方式间接对Class进行操作,Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数、属性和方法等。Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能,这就为使用程序化方式操作Class对象开辟了途径。

简单实例

我们将从一个简单例子开始探访Java反射机制的征程,下面的Car类拥有两个构造函数、两个方法以及三个属性,如代码清单3-9所示:

代码清单3-9  Car

  1. package com.baobaotao.reflect;
  2. public class Car {
  3.     private String brand;
  4.     private String color;
  5.     private int maxSpeed;
  6.      //①默认构造函数
  7.     public Car(){}
  8.      //②带参构造函数
  9.     public Car(String brand,String color,int maxSpeed){
  10.         this.brand = brand;
  11.         this.color = color;
  12.         this.maxSpeed = maxSpeed;
  13.     }
  14.      //③未带参的方法
  15.     public void introduce() {
  16.        System.out.println(“brand:”+brand+”;color:”+color+”;maxSpeed:” +maxSpeed);
  17.     }
  18.      //省略参数的getter/Setter方法
  19.      …
  20. }

一般情况下,我们会使用如下的代码创建Car的实例:

  1. Car car = new Car();
  2. car.setBrand(“红旗CA72″);

或者:

  1. Car car = new Car(“红旗CA72″,”黑色”);

以上两种方法都采用传统方式的直接调用目标类的方法,下面我们通过Java反射机制以一种更加通用的方式间接地操作目标类:

代码清单3-10  ReflectTest

  1. package com.baobaotao. reflect;
  2. import java.lang.reflect.Constructor;
  3. import java.lang.reflect.Field;
  4. import java.lang.reflect.Method;
  5. public class ReflectTest {
  6.     public static Car  initByDefaultConst() throws Throwable
  7.     {
  8.         //①通过类装载器获取Car类对象
  9.         ClassLoader loader = Thread.currentThread().getContextClassLoader();
  10.         Class clazz = loader.loadClass(“com.baobaotao.reflect.Car”);
  11.           //②获取类的默认构造器对象并通过它实例化Car
  12.         Constructor cons = clazz.getDeclaredConstructor((Class[])null);
  13.         Car car = (Car)cons.newInstance();
  14.           //③通过反射方法设置属性
  15.         Method setBrand = clazz.getMethod(“setBrand”,String.class);
  16.         setBrand.invoke(car,”红旗CA72″);
  17.         Method setColor = clazz.getMethod(“setColor”,String.class);
  18.         setColor.invoke(car,”黑色”);
  19.         Method setMaxSpeed = clazz.getMethod(“setMaxSpeed”,int.class);
  20.         setMaxSpeed.invoke(car,200);
  21.         return car;
  22.     }
  23.     public static void main(String[] args) throws Throwable {
  24.         Car car = initByDefaultConst();
  25.         car.introduce();
  26.     }
  27. }

运行以上程序,在控制台上将打印出以下信息:

引用
brand:红旗CA72;color:黑色;maxSpeed:200

这说明我们完全可以通过编程方式调用Class的各项功能,这和直接通过构造函数和方法调用类功能的效果是一致的,只不过前者是间接调用,后者是直接调用罢了。

在ReflectTest中,使用了几个重要的反射类,分别是ClassLoader、Class、Constructor和Method,通过这些反射类就可以间接调用目标Class的各项功能了。在①处,我们获取当前线程的ClassLoader,然后通过指定的全限定类“com.baobaotao.beans.Car”装载Car类对应的反射实例。在②处,我们通过Car的反射类对象获取Car的构造函数对象cons,通过构造函数对象的newInstrance()方法实例化Car对象,其效果等同于new Car()。在③处,我们又通过Car的反射类对象的getMethod(String methodName,Class paramClass)获取属性的Setter方法对象,第一个参数是目标Class的方法名;第二个参数是方法入参的对象类型。获取方法反射对象后,即可通过invoke(Object obj,Object param)方法调用目标类的方法,该方法的第一个参数是操作的目标类对象实例;第二个参数是目标方法的入参。

在代码清单3 10中,粗体所示部分的信息即是通过反射方法操控目标类的元信息,如果我们将这些信息以一个配置文件的方式提供,就可以使用Java语言的反射功能编写一段通用的代码对类似于Car的类进行实例化及功能调用操作了。

类装载器ClassLoader

类装载器工作机制

类装载器就是寻找类的节码文件并构造出类在JVM内部表示对象的组件。在Java中,类装载器把一个类装入JVM中,要经过以下步骤:

      [1.]装载:查找和导入Class文件;

 

      [2.]链接:执行校验、准备和解析步骤,其中解析步骤是可以选择的:

 

          [2.1]校验:检查载入Class文件数据的正确性;

 

          [2.2]准备:给类的静态变量分配存储空间;

 

          [2.3]解析:将符号引用转成直接引用;

 

      [3.]初始化:对类的静态变量、静态代码块执行初始化工作。

类装载工作由ClassLoader及其子类负责,ClassLoader是一个重要的Java运行时系统组件,它负责在运行时查找和装入Class字节码文件。JVM在运行时会产生三个ClassLoader:根装载器、ExtClassLoader(扩展类装载器)和AppClassLoader(系统类装载器)。其中,根装载器不是ClassLoader的子类,它使用C++编写,因此我们在Java中看不到它,根装载器负责装载JRE的核心类库,如JRE目标下的rt.jar、charsets.jar等。ExtClassLoader和AppClassLoader都是ClassLoader的子类。其中ExtClassLoader负责装载JRE扩展目录ext中的JAR类包;AppClassLoader负责装载Classpath路径下的类包。

这三个类装载器之间存在父子层级关系,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认情况下,使用AppClassLoader装载应用程序的类,我们可以做一个实验:

代码清单3-11  ClassLoaderTest

  1. public class ClassLoaderTest {
  2.     public static void main(String[] args) {
  3.         ClassLoader loader = Thread.currentThread().getContextClassLoader();
  4.         System.out.println(“current loader:”+loader);
  5.         System.out.println(“parent loader:”+loader.getParent());
  6.         System.out.println(“grandparent loader:”+loader.getParent(). getParent());
  7.     }
  8. }

运行以上代码,在控制台上将打出以下信息:

引用
current loader:sun.misc.Launcher$AppClassLoader@131f71a
parent loader:sun.misc.Launcher$ExtClassLoader@15601ea
//①根装载器在Java中访问不到,所以返回null
grandparent loader:null

通过以上的输出信息,我们知道当前的ClassLoader是AppClassLoader,父ClassLoader是ExtClassLoader,祖父ClassLoader是根类装载器,因为在Java中无法获得它的句柄,所以仅返回null。

JVM装载类时使用“全盘负责委托机制”,“全盘负责”是指当一个ClassLoader装载一个类的时,除非显式地使用另一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入;“委托机制”是指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全角度考虑的,试想如果有人编写了一个恶意的基础类(如java.lang.String)并装载到JVM中将会引起多么可怕的后果。但是由于有了“全盘负责委托机制”,java.lang.String永远是由根装载器来装载的,这样就避免了上述事件的发生。

ClassLoader重要方法

在Java中,ClassLoader是一个抽象类,位于java.lang包中。下面对该类的一些重要接口方法进行介绍:

    •   Class loadClass(String name)
    • name参数指定类装载器需要装载类的名字,必须使用全限定类名,如com.baobaotao. beans.Car。该方法有一个重载方法loadClass(String name ,boolean resolve),resolve参数告诉类装载器是否需要解析该类。在初始化类之前,应考虑进行类解析的工作,但并不是所有的类都需要解析,如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要进行解析。

 

    • Class defineClass(String name, byte[] b, int off, int len)
    • 将类文件的字节数组转换成JVM内部的java.lang.Class对象。字节数组可以从本地文件系统、远程网络获取。name为字节数组对应的全限定类名。

 

    •   Class findSystemClass(String name)
    • 从本地文件系统载入Class文件,如果本地文件系统不存在该Class文件,将抛出ClassNotFoundException异常。该方法是JVM默认使用的装载机制。

 

    •   Class findLoadedClass(String name)
    • 调用该方法来查看ClassLoader是否已装入某个类。如果已装入,那么返回java.lang.Class对象,否则返回null。如果强行装载已存在的类,将会抛出链接错误。

 

    •   ClassLoader getParent()
    • 获取类装载器的父装载器,除根装载器外,所有的类装载器都有且仅有一个父装载器,ExtClassLoader的父装载器是根装载器,因为根装载器非Java编写,所以无法获得,将返回null。

除JVM默认的三个ClassLoader以外,可以编写自己的第三方类装载器,以实现一些特殊的需求。类文件被装载并解析后,在JVM内将拥有一个对应的java.lang.Class类描述对象,该类的实例都拥有指向这个类描述对象的引用,而类描述对象又拥有指向关联ClassLoader的引用,如图3-4所示。

ef8d6a8b-8aee-3348-aa76-66804aaa3473

每一个类在JVM中都拥有一个对应的java.lang.Class对象,它提供了类结构信息的描述。数组、枚举、注解以及基本Java类型(如int、double等),甚至void都拥有对应的Class对象。Class没有public的构造方法。Class对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构造的。

Java反射机制

Class反射对象描述类语义结构,可以从Class对象中获取构造函数、成员变量、方法类等类元素的反射对象,并以编程的方式通过这些反射对象对目标类对象进行操作。这些反射对象类在java.reflect包中定义,下面是最主要的三个反射类:

    •   Constructor:类的构造函数反射类,通过Class#getConstructors()方法可以获得类的所有构造函数反射对象数组。在JDK5.0中,还可以通过getConstructor(Class… parameterTypes)获取拥有特定入参的构造函数反射对象。Constructor的一个主要方法是newInstance(Object[] initargs),通过该方法可以创建一个对象类的实例,相当于new关键字。在JDK5.0中该方法演化为更为灵活的形式:newInstance (Object… initargs)。
    •   Method:类方法的反射类,通过Class#getDeclaredMethods()方法可以获取类的所有方法反射类对象数组Method[]。在JDK5.0中可以通过getDeclaredMethod(String name, Class… parameterTypes)获取特定签名的方法,name为方法名;Class…为方法入参类型列表。Method最主要的方法是invoke(Object obj, Object[] args),obj表示操作的目标对象;args为方法入参,代码清单3 10③处演示了这个反射类的使用方法。在JDK 5.0中,该方法的形式调整为invoke(Object obj, Object… args)。此外,Method还有很多用于获取类方法更多信息的方法:
    • 1)Class getReturnType():获取方法的返回值类型;

 

            2)Class[] getParameterTypes():获取方法的入参类型数组;

 

            3)Class[] getExceptionTypes():获取方法的异常类型数组;

 

          4)Annotation[][] getParameterAnnotations():获取方法的注解信息,JDK 5.0中的新方法;

  •   Field:类的成员变量的反射类,通过Class#getDeclaredFields()方法可以获取类的成员变量反射对象数组,通过Class#getDeclaredField(String name)则可获取某个特定名称的成员变量反射对象。Field类最主要的方法是set(Object obj, Object value),obj表示操作的目标对象,通过value为目标对象的成员变量设置值。如果成员变量为基础类型,用户可以使用Field类中提供的带类型名的值设置方法,如setBoolean(Object obj, boolean value)、setInt(Object obj, int value)等。

此外,Java还为包提供了Package反射类,在JDK 5.0中还为注解提供了AnnotatedElement反射类。总之,Java的反射体系保证了可以通过程序化的方式访问目标类中所有的元素,对于private或protected的成员变量和方法,只要JVM的安全机制允许,也可以通过反射进行调用,请看下面的例子:

代码清单3-12  PrivateCarReflect

  1. package com.baobaotao.reflect;
  2. public class PrivateCar {
  3.        //①private成员变量:使用传统的类实例调用方式,只能在本类中访问
  4.    private String color;
  5.         //②protected方法:使用传统的类实例调用方式,只能在子类和本包中访问
  6.    protected void drive(){
  7. System.out.println(“drive private car! the color is:”+color);
  8.    }
  9. }

color变量和drive()方法都是私有的,通过类实例变量无法在外部访问私有变量、调用私有方法的,但通过反射机制却可以绕过这个限制:

代码清单3-13  PrivateCarReflect

  1. public class PrivateCarReflect {
  2.    public static void main(String[] args) throws Throwable{
  3.        ClassLoader loader = Thread.currentThread().getContextClassLoader();
  4.        Class clazz = loader.loadClass(“com.baobaotao.reflect.PrivateCar”);
  5.        PrivateCar pcar = (PrivateCar)clazz.newInstance();
  6.        Field colorFld = clazz.getDeclaredField(“color”);
  7.         //①取消Java语言访问检查以访问private变量
  8.        colorFld.setAccessible(true);
  9.        colorFld.set(pcar,”红色”);
  10.        Method driveMtd = clazz.getDeclaredMethod(“drive”,(Class[])null);
  11.         //Method driveMtd = clazz.getDeclaredMethod(“drive”); JDK5.0下使用
  12.         //②取消Java语言访问检查以访问protected方法
  13.        driveMtd.setAccessible(true);
  14.         driveMtd.invoke(pcar,(Object[])null);
  15.   }
  16. }

运行该类,打印出以下信息:

引用
drive private car! the color is:红色

在访问private、protected成员变量和方法时必须通过setAccessible(boolean access)方法取消Java语言检查,否则将抛出IllegalAccessException。如果JVM的安全管理器设置了相应的安全机制,调用该方法将抛出SecurityException。

这些文章摘自于我的《Spring 3.x企业应用开发实战》,我将通过连载的方式,陆续在此发出。欢迎大家讨论。

学习Spring必学的Java基础知识(2)—-动态代理
Spring AOP使用动态代理技术在运行期织入增强的代码,为了揭示Spring AOP底层的工作机理,有必要对涉及到的Java知识进行学习。Spring AOP使用了两种代理机制:一种是基于JDK的动态代理;另一种是基于CGLib的动态代理。之所以需要两种代理机制,很大程度上是因为JDK本身只提供接口的代理,而不支持类的代理。

带有横切逻辑的实例

我们通过具体化代码实现上一节所介绍例子的性能监视横切逻辑,并通过动态代理技术对此进行改造。在调用每一个目标类方法时启动方法的性能监视,在目标类方法调用完成时记录方法的花费时间。

代码清单6-2  ForumService:包含性能监视横切代码

  1. package com.baobaotao.proxy;
  2. public class ForumServiceImpl implements ForumService {
  3.     public void removeTopic(int topicId) {
  4.          //①-1开始对该方法进行性能监视
  5.         PerformanceMonitor.begin(
  6.                             ”com.baobaotao.proxy.ForumServiceImpl. removeTopic”);
  7.         System.out.println(“模拟删除Topic记录:”+topicId);
  8.         try {
  9.             Thread.currentThread().sleep(20);
  10.         } catch (Exception e) {
  11.             throw new RuntimeException(e);
  12.         }
  13.          //①-2结束对该方法进行性能监视
  14.         PerformanceMonitor.end();
  15.     }
  16.     public void removeForum(int forumId) {
  17.           //②-1开始对该方法进行性能监视
  18.         PerformanceMonitor.begin(
  19. “com.baobaotao.proxy.ForumServiceImpl. removeForum”);
  20.         System.out.println(“模拟删除Forum记录:”+forumId);
  21.         try {
  22.             Thread.currentThread().sleep(40);
  23.         } catch (Exception e) {
  24.             throw new RuntimeException(e);
  25.         }
  26.          //②-2结束对该方法进行性能监视
  27.         PerformanceMonitor.end();
  28.     }
  29. }

代码清单6-2中粗体表示的代码就是具有横切逻辑特征的代码,每个Service类和每个业务方法体的前后都执行相同的代码逻辑:方法调用前启动PerformanceMonitor,方法调用后通知PerformanceMonitor结束性能监视并给记录性能监视结果。

PerformanceMonitor是性能监视的实现类,我们给出一个非常简单的实现版本,其代码如代码清单6-3所示:

代码清单6-3  PerformanceMonitor

  1. package com.baobaotao.proxy;
  2. public class PerformanceMonitor {
  3.      //①通过一个ThreadLocal保存调用线程相关的性能监视信息
  4.     private static ThreadLocal<MethodPerformace> performanceRecord =
  5.                                 new ThreadLocal<MethodPerformance>();
  6.     //②启动对某一目标方法的性能监视
  7.      public static void begin(String method) {
  8.         System.out.println(“begin monitor…”);
  9.         MethodPerformance mp = new MethodPerformance(method);
  10.         performanceRecord.set(mp);
  11.     }
  12.     public static void end() {
  13.         System.out.println(“end monitor…”);
  14.         MethodPerformance mp = performanceRecord.get();
  15.          //③打印出方法性能监视的结果信息。
  16.         mp.printPerformance();
  17.     }
  18. }

ThreadLocal是将非线程安全类改造为线程安全类的法宝,在9.2节中我们将详细介绍这个Java基础知识。PerformanceMonitor提供了两个方法:通过调用begin(String method)方法开始对某个目标类方法的监视,method为目标类方法的全限定名;而end()方法结束对目标类方法的监视,并给出性能监视的信息。这两个方法必须配套使用。

用于记录性能监视信息的MethodPerformance类的代码如所示:

代码清单6-4  MethodPerformance

  1. package com.baobaotao.proxy;
  2. public class MethodPerformance {
  3.     private long begin;
  4.     private long end;
  5.     private String serviceMethod;
  6.     public MethodPerformance(String serviceMethod){
  7.        this.serviceMethod = serviceMethod;
  8.        //①记录目标类方法开始执行点的系统时间
  9.        this.begin = System.currentTimeMillis();
  10.     }
  11.     public void printPerformance(){
  12.         //②获取目标类方法执行完成后的系统时间,并进而计算出目标类方法执行时间
  13.         end = System.currentTimeMillis();
  14.         long elapse = end - begin;
  15.         //③报告目标类方法的执行时间
  16.         System.out.println(serviceMethod+”花费”+elapse+”毫秒。”);
  17.     }
  18. }

通过下面的代码测试拥有性能监视能力的ForumServiceImpl业务方法:

  1. package com.baobaotao.proxy;
  2. public class TestForumService {
  3.     public static void main(String[] args) {
  4.         ForumService forumService = new ForumServiceImpl();
  5.         forumService .removeForum(10);
  6.        forumService .removeTopic(1012);
  7.     }
  8. }

我们得到以下输出信息:

引用
begin monitor… ①removeForum(10)方法的性能监视报告
模拟删除Forum记录:10
end monitor…
com.baobaotao.proxy.ForumServiceImpl.removeForum花费47毫秒。

begin monitor… ①removeTopic(1012)方法的性能监视报告
模拟删除Topic记录:1012
end monitor…
com.baobaotao.proxy.ForumServiceImpl.removeTopic花费26毫秒。

正如代码清单6 2实例所示,当某个方法需要进行性能监视,就必须调整方法代码,在方法体前后分别添加上开启性能监视和结束性能监视的代码。这些非业务逻辑的性能监视代码破坏了ForumServiceImpl业务逻辑的纯粹性。我们希望通过代理的方式,将业务类方法中开启和结束性能监视的这些横切代码从业务类中完全移除。并通过JDK动态代理技术或CGLib动态代理技术将横切代码动态织入到目标方法的相应位置。

JDK动态代理

JDK 1.3以后,Java提供了动态代理的技术,允许开发者在运行期创建接口的代理实例。在Sun刚推出动态代理时,还很难想象它有多大的实际用途,现在我们终于发现动态代理是实现AOP的绝好底层技术。

JDK的动态代理主要涉及到java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态将横切逻辑和业务逻辑编织在一起。

而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。这样讲一定很抽象,我们马上着手使用Proxy和InvocationHandler这两个魔法戒对上一节中的性能监视代码进行革新。

首先,我们从业务类ForumServiceImpl中删除性能监视的横切代码,使ForumServiceImpl只负责具体的业务逻辑,如代码清单6-5所示:

代码清单6-5  ForumServiceImpl:移除性能监视横切代码

  1. package com.baobaotao.proxy;
  2. public class ForumServiceImpl implements ForumService {
  3.     public void removeTopic(int topicId) {
  4.                                 ①
  5.         System.out.println(“模拟删除Topic记录:”+topicId);
  6.         try {
  7.             Thread.currentThread().sleep(20);
  8.         } catch (Exception e) {
  9.             throw new RuntimeException(e);
  10.         }
  11.                               ①
  12.     }
  13.     public void removeForum(int forumId) {
  14.                           ②
  15.         System.out.println(“模拟删除Forum记录:”+forumId);
  16.         try {
  17.             Thread.currentThread().sleep(40);
  18.         } catch (Exception e) {
  19.             throw new RuntimeException(e);
  20.         }
  21.                           ②
  22.     }
  23. }

在代码清单6-5中的①和②处,原来的性能监视代码被移除了,我们只保留了真正的业务逻辑。

从业务类中移除的性能监视横切代码当然不能漂浮在空气中,它还得找到一个安身之所,InvocationHandler就是横切代码的安家乐园,我们将性能监视的代码安置在PerformanceHandler中,如代码清单6-6所示:

  1. 代码清单6-6  PerformanceHandler
  2. package com.baobaotao.proxy;
  3. import java.lang.reflect.InvocationHandler;
  4. import java.lang.reflect.Method;
  5. public class PerformanceHandler implements InvocationHandler {//①实现InvocationHandler
  6.     private Object target;
  7.     public PerformanceHandler(Object target){ //②target为目标的业务类
  8.         this.target = target;
  9.     }
  10.     public Object invoke(Object proxy, Method method, Object[] args) ③
  11.             throws Throwable {
  12.         PerformanceMonitor.begin(target.getClass().getName()+”.”+ method. getName());③-1
  13.         Object obj = method.invoke(target, args);// ③-2通过反射方法调用业务类的目标方法
  14.         PerformanceMonitor.end();③-1
  15.         return obj;
  16.     }
  17. }

③处invoke()方法中粗体所示部分的代码为性能监视的横切代码,我们发现,横切代码只出现一次,而不是原来那样星洒各处。③-2处的method.invoke()语句通过Java反射机制间接调用目标对象的方法,这样InvocationHandler的invoke()方法就将横切逻辑代码(③-1)和业务类方法的业务逻辑代码(③-2)编织到一起了,所以我们可以将InvocationHandler看成是一个编织器。下面,我们对这段代码做进一步的说明。

首先,我们实现InvocationHandler接口,该接口定义了一个 invoke(Object proxy, Method method, Object[] args)的方法,proxy是最终生成的代理实例,一般不会用到;method是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;args是通过被代理实例某一个方法的入参,在方法反射调用时使用。

此外,我们在构造函数里通过target传入希望被代理的目标对象,如②处所示,在InvocationHandler接口方法invoke(Object proxy, Method method, Object[] args)里,将目标实例传给method.invoke()方法,调用目标实例的方法,如③所示。
下面,我们通过Proxy结合PerformanceHandler创建ForumService接口的代理实例,如代码清单6-7所示:

代码清单6-7  TestForumService:创建代理实例

  1. package com.baobaotao.proxy;
  2. import java.lang.reflect.Proxy;
  3. public class TestForumService {
  4.     public static void main(String[] args) {
  5.                //①希望被代理的目标业务类
  6.         ForumService target = new ForumServiceImpl();
  7.                //②将目标业务类和横切代码编织到一起
  8.         PerformanceHandler handler = new PerformanceHandler(target);
  9.                 //③根据编织了目标业务类逻辑和性能监视横切逻辑的InvocationHandler实例创建代理实例
  10.         ForumService proxy = (ForumService) Proxy.newProxyInstance(
  11.                 target.getClass().getClassLoader(),
  12.                 target.getClass().getInterfaces(),
  13.                 handler);
  14.                 //④调用代理实例
  15.         proxy.removeForum(10);
  16.         proxy.removeTopic(1012);
  17.     }
  18. }

上面的代码完成业务类代码和横切代码的编织工作并生成了代理实例。在②处,我们让PerformanceHandler将性能监视横切逻辑编织到ForumService实例中,然后在③处,通过Proxy的newProxyInstance()静态方法为编织了业务类逻辑和性能监视逻辑的handler创建一个符合ForumService接口的代理实例。该方法的第一个入参为类加载器;第二个入参为创建代理实例所需要实现的一组接口;第三个参数是整合了业务逻辑和横切逻辑的编织器对象。

按照③处的设置方式,这个代理实例实现了目标业务类的所有接口,即Forum ServiceImpl的ForumService接口。这样,我们就可以按照调用ForumService接口实例相同的方式调用代理实例,如④所示。运行以上的代码,输出以下信息:

引用
begin monitor…
模拟删除Forum记录:10
end monitor…
com.baobaotao.proxy.ForumServiceImpl.removeForum花费47毫秒。

begin monitor…
模拟删除Topic记录:1012
end monitor…
com.baobaotao.proxy.ForumServiceImpl.removeTopic花费26毫秒。

我们发现,程序的运行效果和直接在业务类中编写性能监视逻辑的效果一致,但是在这里,原来分散的横切逻辑代码已经被我们抽取到PerformanceHandler中。当其他业务类(如UserService、SystemService等)的业务方法也需要使用性能监视时,我们只要按照代码清单6-7相似的方式,分别为它们创建代理对象就可以了。下面,我们通过时序图描述通过创建代理对象进行业务方法调用的整体逻辑,以进一步认识代理对象的本质,如图6-3所示。

eaafc698-ec08-3ef3-aae0-6d14514cc976

我们在上图中使用虚线的方式对通过Proxy创建的ForumService代理实例加以凸显,ForumService代理实例内部利用PerformaceHandler整合横切逻辑和业务逻辑。调用者调用代理对象的removeForum()和removeTopic()方法时,上图的内部调用时序清晰地告诉我们实际上所发生的一切。

CGLib动态代理

使用JDK创建代理有一个限制,即它只能为接口创建代理实例,这一点我们可从Proxy的接口newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)的方法签名中就看得很清楚:第二个入参interfaces就是需要代理实例实现的接口列表。虽然面向接口编程的思想被很多大师级人物(包括Rod Johnson)推崇,但在实际开发中,许多开发者也对此深感困惑:难道对一个简单业务表的操作也需要老老实实地创建5个类(领域对象类、Dao接口,Dao实现类,Service接口和Service实现类)吗?难道不能直接通过实现类构建程序吗?对于这个问题,我们很难给出一个孰好孰劣的准确判断,但我们确实发现有很多不使用接口的项目也取得了非常好的效果(包括大家所熟悉的SpringSide开源项目)。

对于没有通过接口定义业务方法的类,如何动态创建代理实例呢?JDK的代理技术显然已经黔驴技穷,CGLib作为一个替代者,填补了这个空缺。

CGLib采用非常底层的字节码技术,可以为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,并顺势织入横切逻辑。下面,我们采用CGLib技术,编写一个可以为任何类创建织入性能监视横切逻辑代理对象的代理创建器,如代码清单 6-8所示:

代码清单6-8  CglibProxy

  1. package com.baobaotao.proxy;
  2. import java.lang.reflect.Method;
  3. import net.sf.cglib.proxy.Enhancer;
  4. import net.sf.cglib.proxy.MethodInterceptor;
  5. import net.sf.cglib.proxy.MethodProxy;
  6. public class CglibProxy implements MethodInterceptor {
  7.     private Enhancer enhancer = new Enhancer();
  8.     public Object getProxy(Class clazz) {
  9.         enhancer.setSuperclass(clazz); //① 设置需要创建子类的类
  10.         enhancer.setCallback(this);
  11.         return enhancer.create(); //②通过字节码技术动态创建子类实例
  12.     }
  13.         //③拦截父类所有方法的调用
  14.     public Object intercept(Object obj, Method method, Object[] args,
  15.             MethodProxy proxy) throws Throwable {
  16.         PerformanceMonitor.begin(obj.getClass().getName()+”.”+method. getName());//③-1
  17.         Object result=proxy.invokeSuper(obj, args); ③-2
  18.         PerformanceMonitor.end();//③-1通过代理类调用父类中的方法
  19.         return result;
  20.     }
  21. }

在上面代码中,用户可以通过getProxy(Class clazz)为一个类创建动态代理对象,该代理对象通过扩展clazz创建代理对象。在这个代理对象中,我们织入性能监视的横切逻辑(③-1)。intercept(Object obj, Method method, Object[] args,MethodProxy proxy)是CGLib定义的Interceptor接口的方法,它拦截所有目标类方法的调用,obj表示目标类的实例;method为目标类方法的反射对象;args为方法的动态入参;而proxy为代理类实例。

下面,我们通过CglibProxy为ForumServiceImpl类创建代理对象,并测试代理对象的方法,如代码清单6-9所示:

代码清单6-9  TestForumService:测试Cglib创建的代理类

  1. package com.baobaotao.proxy;
  2. import java.lang.reflect.Proxy;
  3. public class TestForumService {
  4.     public static void main(String[] args) {
  5.       CglibProxy proxy = new CglibProxy();
  6.       ForumServiceImpl forumService = ①
  7.                 (ForumServiceImpl )proxy.getProxy(ForumServiceImpl.class);
  8.       forumService.removeForum(10);
  9.       forumService.removeTopic(1023);
  10.     }
  11. }

在①中,我们通过CglibProxy为ForumServiceImpl动态创建了一个织入性能监视逻辑的代理对象,并调用代理类的业务方法。运行上面的代码,输入以下信息:

引用
begin monitor…
模拟删除Forum记录:10
end monitor…
com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeForum花费47毫秒。
begin monitor…
模拟删除Topic记录:1023
end monitor…
com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeTopic花费16毫秒。

观察以上的输出,除了发现两个业务方法中都织入了性能监控的逻辑外,我们还发现代理类的名字是com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0,这个特殊的类就是CGLib为ForumServiceImpl动态创建的子类。

代理知识小结

Spring AOP的底层就是通过使用JDK动态代理或CGLib动态代理技术为目标Bean织入横切逻辑。在这里,我们对前面两节动态创建代理对象作一个小结。

我们虽然通过PerformanceHandler或CglibProxy实现了性能监视横切逻辑的动态织入,但这种实现方式存在三个明显需要改进的地方:

1)目标类的所有方法都添加了性能监视横切逻辑,而有时,这并不是我们所期望的,我们可能只希望对业务类中的某些特定方法添加横切逻辑;
2)我们通过硬编码的方式指定了织入横切逻辑的织入点,即在目标类业务方法的开始和结束前织入代码;
3)我们手工编写代理实例的创建过程,为不同类创建代理时,需要分别编写相应的创建代码,无法做到通用。

以上三个问题,在AOP中占用重要的地位,因为Spring AOP的主要工作就是围绕以上三点展开:Spring AOP通过Pointcut(切点)指定在哪些类的哪些方法上织入横切逻辑,通过Advice(增强)描述横切逻辑和方法的具体织入点(方法前、方法后、方法的两端等)。此外,Spring通过Advisor(切面)将Pointcut和Advice两者组装起来。有了Advisor的信息,Spring就可以利用JDK或CGLib的动态代理技术采用统一的方式为目标Bean创建织入切面的代理对象了。

JDK动态代理所创建的代理对象,在JDK 1.3下,性能强差人意。虽然在高版本的JDK中,动态代理对象的性能得到了很大的提高,但是有研究表明,CGLib所创建的动态代理对象的性能依旧比JDK的所创建的代理对象的性能高不少(大概10倍)。但CGLib在创建代理对象时所花费的时间却比JDK动态代理多(大概8倍),所以对于singleton的代理对象或者具有实例池的代理,因为无须频繁创建代理对象,所以比较适合用CGLib动态代理技术,反之适合用JDK动态代理技术。值得一提的是,由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中的final方法进行代理。

学习Spring必学的Java基础知识(3)—-PropertyEditor
在Spring配置文件里,我们往往通过字面值为Bean各种类型的属性提供设置值:不管是double类型还是int类型,在配置文件中都对应字符串类型的字面值。BeanWrapper填充Bean属性时如何将这个字面值转换为对应的double或int等内部类型呢?我们可以隐约地感觉到一定有一个转换器在其中起作用,这个转换器就是属性编辑器。

“属性编辑器”这个名字可能会让人误以为是一个带用户界面的输入器,其实属性编辑器不一定非得有用户界面,任何实现java.beans.PropertyEditor接口的类都是属性编辑器。属性编辑器的主要功能就是将外部的设置值转换为JVM内部的对应类型,所以属性编辑器其实就是一个类型转换器。

PropertyEditor是JavaBean规范定义的接口,JavaBean规范中还有其他一些PropertyEditor配置的接口。为了彻底理解属性编辑器,必须对JavaBean中有关属性编辑器的规范进行学习,相信这些知识对学习和掌握Spring中的属性编辑器会大有帮助。

JavaBean的编辑器

Sun所制定的JavaBean规范,很大程度上是为IDE准备的——它让IDE能够以可视化的方式设置JavaBean的属性。如果在IDE中开发一个可视化应用程序,我们需要通过属性设置的方式对组成应用的各种组件进行定制,IDE通过属性编辑器让开发人员使用可视化的方式设置组件的属性。

一般的IDE都支持JavaBean规范所定义的属性编辑器,当组件开发商发布一个组件时,它往往将组件对应的属性编辑器捆绑发行,这样开发者就可以在IDE环境下方便地利用属性编辑器对组件进行定制工作。

JavaBean规范通过java.beans.PropertyEditor定义了设置JavaBean属性的方法,通过BeanInfo描述了JavaBean哪些属性是可定制的,此外还描述了可定制属性与PropertyEditor的对应关系。

BeanInfo与JavaBean之间的对应关系,通过两者之间规范的命名确立:对应JavaBean的BeanInfo采用如下的命名规范:<Bean>BeanInfo。如ChartBean对应的BeanInfo为ChartBeanBeanInfo;Car对应的BeanInfo为CarBeanInfo。当JavaBean连同其属性编辑器相同的组件注册到IDE中后,当在开发界面中对JavaBean进行定制时,IDE就会根据JavaBean规范找到对应的BeanInfo,再根据BeanInfo中的描述信息找到JavaBean属性描述(是否开放、使用哪个属性编辑器),进而为JavaBean生成特定开发编辑界面。

JavaBean规范提供了一个管理默认属性编辑器的管理器:PropertyEditorManager,该管理器内保存着一些常见类型的属性编辑器,如果某个JavaBean的常见类型属性没有通过BeanInfo显式指定属性编辑器,IDE将自动使用PropertyEditorManager中注册的对应默认属性编辑器。

由于JavaBean对应的属性编辑器等IDE环境相关的资源和组件需要动态加载,所以在纯Java的IDE中开发基于组件的应用时,总会感觉IDE反应很迟钝,不像Delphi、C++Builder一样灵敏快捷。但在Eclipse开发环境中,设计包括可视化组件的应用时却很快捷,原因是Eclipse没有使用Java的标准用户界面组件库,当然也就没有按照JavaBean的规范开发设计GUI组件了。

PropertyEditor

PropertyEditor是属性编辑器的接口,它规定了将外部设置值转换为内部JavaBean属性值的转换接口方法。PropertyEditor主要的接口方法说明如下:

  •   Object getValue():返回属性的当前值。基本类型被封装成对应的封装类实例;
  •   void setValue(Object newValue):设置属性的值,基本类型以封装类传入;
  •   String getAsText():将属性对象用一个字符串表示,以便外部的属性编辑器能以可视化的方式显示。缺省返回null,表示该属性不能以字符串表示;
  •   void setAsText(String text):用一个字符串去更新属性的内部值,这个字符串一般从外部属性编辑器传入;
  •   String[] getTags():返回表示有效属性值的字符串数组(如boolean属性对应的有效Tag为true和false),以便属性编辑器能以下拉框的方式显示出来。缺省返回null,表示属性没有匹配的字符值有限集合;
  •   String getJavaInitializationString():为属性提供一个表示初始值的字符串,属性编辑器以此值作为属性的默认值。

可以看出PropertyEditor接口方法是内部属性值和外部设置值的沟通桥梁。此外,我们可以很容易地发现该接口的很多方法是专为IDE中的可视化属性编辑器提供的:如getTags()、getJavaInitializationString()以及另外一些我们未此介绍的接口方法。

Java为PropertyEditor提供了一个方便类:PropertyEditorSupport,该类实现了PropertyEditor接口并提供默认实现,一般情况下,用户可以通过扩展这个方便类设计自己的属性编辑器。

BeanInfo

BeanInfo主要描述了JavaBean哪些属性可以编辑以及对应的属性编辑器,每一个属性对应一个属性描述器PropertyDescriptor。PropertyDescriptor的构造函数有两个入参:
PropertyDescriptor(String propertyName, Class beanClass) ,其中propertyName为属性名;而beanClass为JavaBean对应的Class。

此外PropertyDescriptor还有一个setPropertyEditorClass(Class propertyEditorClass)方法,为JavaBean属性指定编辑器。BeanInfo接口最重要的方法就是:PropertyDescriptor[] getPropertyDescriptors() ,该方法返回JavaBean的属性描述器数组。

BeanInfo接口有一个常用的实现类:SimpleBeanInfo,一般情况下,可以通过扩展SimpleBeanInfo实现自己的功能。

一个实例

在本节中,我们来看一个具体属性编辑器的实例,该实例根据《Core Java Ⅱ》上的一个例子改编而成。

ChartBean是一个可定制图表组件,允许通过属性的设置定制图表的样式以得到满足各种不同使用场合要求的图表。我们忽略ChartBean的其他属性,仅关注其中的两个属性:

代码清单5-2  CharBean

  1. public class ChartBean extends JPanel{
  2.    private int titlePosition = CENTER;
  3.    private boolean inverse;
  4.    //省略get/setter方法
  5. }

下面,我们为titlePosition属性提供一个属性编辑器。我们不去直接实现PropertyEditor,而是通过扩展PropertyEditorSupport这个方便类来定义我们的属性编辑器:

代码清单5-3  TitlePositionEditor

  1. import java.beans.*;
  2.  public class TitlePositionEditor extends PropertyEditorSupport{
  3.     private String[] options = { ”Left”, ”Center”, ”Right” };
  4.     //①代表可选属性值的字符串标识数组
  5.     public String[] getTags() { return options; }
  6.     //②代表属性初始值的字符串
  7.     public String getJavaInitializationString() { return ”" + getValue(); }
  8.     //③将内部属性值转换为对应的字符串表示形式,供属性编辑器显示之用
  9.     public String getAsText(){
  10.        int value = (Integer) getValue();
  11.        return options[value];
  12.     }
  13.     //④将外部设置的字符串转换为内部属性的值
  14.     public void setAsText(String s){
  15.        for (int i = 0; i < options.length; i++){
  16.           if (options[i].equals(s)){
  17.              setValue(i);
  18.              return;
  19.           }
  20.        }
  21.     }
  22.  }

①处通过getTags()方法返回一个字符串数组,因此在IDE中该属性对应的编辑器将自动提供一个下拉框,下拉框中包含3个可选项:“Left”、“Center”、“Right”。而③和④处的两个方法分别完成属性值到字符串的双向转换功能。CharBean的inverse属性也有一个相似的编辑器InverseEditor,我们忽略不讲。

下面编写ChartBean对应的BeanInfo,根据JavaBean的命名规范,这个BeanInfo应该命名为ChartBeanBeanInfo,它负责将属性编辑器和ChartBean的属性挂钩起来:

代码清单5-4  ChartBeanBeanInfo

  1. import java.beans.*;
  2.  public class ChartBeanBeanInfo extends SimpleBeanInfo{
  3.     public PropertyDescriptor[] getPropertyDescriptors() {
  4.        try{
  5. //①将TitlePositionEditor绑定到ChartBean的titlePosition属性中
  6. PropertyDescriptor titlePositionDescriptor
  7.              = new PropertyDescriptor(“titlePosition”, ChartBean.class);
  8.           titlePositionDescriptor.setPropertyEditorClass(TitlePositionEditor.class);
  9. //②将InverseEditor绑定到ChartBean的inverse属性中
  10. PropertyDescriptor inverseDescriptor
  11.              = new PropertyDescriptor(“inverse”, ChartBean.class);
  12.           inverseDescriptor.setPropertyEditorClass(InverseEditor.class);
  13.           return new PropertyDescriptor[]{titlePositionDescriptor, inverseDescriptor};
  14.        }
  15.        catch (IntrospectionException e){
  16.           e.printStackTrace();
  17.           return null;
  18.        }
  19.     }
  20.  }

在ChartBeanBeanInfo中,我们分别为ChartBean和titlePosition和inverse属性指定对应的属性编辑器。将ChartBean连同属性编辑器以及ChartBeanBeanInfo打成JAR包,使用IDE组件扩展管理功能注册到IDE中。这样,我们就可以像使用TextField、Checkbox等这些组对ChartBean进行可视化的开发设计工作了。下面是ChartBean在NetBeans IDE中的属性编辑器效果图,如图5-5所示。

a7e8b27e-b7c9-39c2-80d1-1e94e887ea01

ChartBean可设置的属性都列在属性查看器中,当单击titlePosition属性时,下拉框中列出了我们提供的3个选项。

Spring默认属性编辑器

Spring的属性编辑器和传统的用于IDE开发时的属性编辑器不同,它们没有UI界面,仅负责将配置文件中的文本配置值转换为Bean属性的对应值,所以Spring的属性编辑器并非传统意义上的JavaBean属性编辑器。

Spring为常见的属性类型提供了默认的属性编辑器。从图5-4中,我们可以看出BeanWrapperImpl类扩展了PropertyEditorRegistrySupport类,Spring在PropertyEditor RegistrySupport中为常见属性类型提供了默认的属性编辑器,这些“常见的类型”共32个,可分为3大类,总结如下:

表5-1  Spring提供的默认属性编辑器

类    别 说    明
基础数据类型 分为几个小类: 1)基本数据类型,如:boolean、byte、short、int等; 2)基本数据类型封装类,如:Long、Character、Integer等; 3)两个基本数据类型的数组,char[]和byte[]; 4)大数类,BigDecimal和BigInteger
集合类 为5种类型的集合类Collection、Set、SortedSet、List和SortedMap提供了编辑器
资源类 用于访问外部资源的8个常见类Class、Class[]、File、InputStream、Locale、Properties、Resource[]和URL

PropertyEditorRegistrySupport中有两个用于保存属性编辑器的Map类型变量:

  •   defaultEditors:用于保存默认属性类型的编辑器,元素的键为属性类型,值为对应的属性编辑器实例;
  •   customEditors:用于保存用户自定义的属性编辑器,元素的键值和defaultEditors相同。

PropertyEditorRegistrySupport通过类似以下的代码定义默认属性编辑器:

  1. this.defaultEditors.put(char.class, new CharacterEditor(false));
  2. this.defaultEditors.put(Character.class, new CharacterEditor(true));
  3. this.defaultEditors.put(Locale.class, new LocaleEditor());
  4. this.defaultEditors.put(Properties.class, new PropertiesEditor());

这些默认的属性编辑器解决常见属性类型的注册问题,如果用户的应用包括一些特殊类型的属性,且希望在配置文件中以字面值提供配置值,那么就需要编写自定义属性编辑器并注册到Spring容器中。这样,Spring才能将配置文件中的属性配置值转换为对应的属性类型值。

自定义属性编辑器

Spring大部分默认属性编辑器都直接扩展于java.beans.PropertyEditorSupport类,用户也可以通过扩展PropertyEditorSupport实现自己的属性编辑器。比起用于IDE环境的属性编辑器来说,Spring环境下使用的属性编辑器的功能非常单一:仅需要将配置文件中字面值转换为属性类型的对象即可,并不需要提供UI界面,因此仅需要简单覆盖PropertyEditorSupport的setAsText()方法就可以了。

一个实例

我们继续使用第4章中Boss和Car的例子,假设我们现在希望在配置Boss时,不通过引用Bean的方式注入Boss的car属性,而希望直接通过字符串字面值提供配置。为了方便阅读,这里再次列出Boss和Car类的简要代码:

代码清单5-5  Car

  1. package com.baobaotao.editor;
  2. public class Car {
  3.     private int maxSpeed;
  4.     public String brand;
  5.     private double price;
  6.     //省略get/setter
  7. }

代码清单5-6  Boss

  1. package com.baobaotao.editor;
  2. public class Boss {
  3.     private String name;
  4.     private Car car = new Car();
  5.     //省略get/setter
  6. }

Boss有两个属性:name和car,分别对应String类型和Car类型。Spring拥有String类型的默认属性编辑器,因此对于String类型的属性我们不用操心。但Car类型是我们自定义的类型,要配置Boss的car属性,有两种方案:

  • 1)在配置文件中为car专门配置一个<bean>,然后在boss的<bean>中通过ref引用car Bean,这正是我们上一章中所用的方法;
  • 2)为Car类型提供一个自定义的属性编辑器,这样,我们就通过字面值为Boss的car属性提供配置值。

第一种方案是常用的方法,但是在有些情况下,这种方式需要将属性对象一步步肢解为最终可以用基本类型表示的Bean,使配置文件变得不够清晰,直接为属性类提供一个对应的自定义属性编辑器可能会是更好的替代方案。

现在,我们来为Car编写一个自定义的属性编辑器,其代码如下所示:

代码清单5-7  CustomCarEditor

  1. package com.baobaotao.editor;
  2. import java.beans.PropertyEditorSupport;
  3. public class CustomCarEditor extends PropertyEditorSupport {
  4.     //①将字面值转换为属性类型对象
  5.     public void setAsText(String text){
  6.         if(text == null || text.indexOf(“,”) == -1){
  7.             throw new IllegalArgumentException(“设置的字符串格式不正确”);
  8.         }
  9.         String[] infos = text.split(“,”);
  10.         Car car = new Car();
  11.         car.setBrand(infos[0]);
  12.         car.setMaxSpeed(Integer.parseInt(infos[1]));
  13.         car.setPrice(Double.parseDouble(infos[2]));
  14.          //②调用父类的setValue()方法设置转换后的属性对象
  15.         setValue(car);
  16.     }
  17. }

CustomCarEditor很简单,它仅覆盖PropertyEditorSupport便利类的setAsText(String text)方法,该方法负责将配置文件以字符串提供的字面值转换为Car对象。字面值采用逗号分隔的格式同时为brand、maxSpeed和price属性值提供设置值,setAsText()方法解析这个字面值并生成对应的Car对象。由于我们并不需要将Boss内部的car属性反显到属性编辑器中,因此不需要覆盖getAsText()方法。

注册自定义的属性编辑器

在IDE环境下,自定义属性编辑器在使用之前必须通过扩展组件功能进行注册,在Spring环境中也需要通过一定的方法注册自定义的属性编辑器。

如果使用BeanFactory,用户需要手工调用registerCustomEditor(Class requiredType, PropertyEditor propertyEditor)方法注册自定义属性编辑器;如果使用ApplicationContext,则只需要在配置文件通过CustomEditorConfigurer注册就可以了。CustomEditorConfigurer实现BeanFactoryPostProcessor接口,因此是一个Bean工厂后处理器。我们知道Bean工厂后处理器在Spring容器加载配置文件并生成BeanDefinition半成品后就会被自动执行。因此CustomEditorConfigurer有容器启动时有机会注入自定义的属性编辑器。下面的配置片断定义了一个CustomEditorConfigurer:

  1. <!–①配置自动注册属性编辑器的CustomEditorConfigurer –>
  2. <bean class=”org.springframework.beans.factory.config.CustomEditorConfigurer”>
  3.         <property name=”customEditors”>
  4.             <map>
  5.                    <!–②-1属性编辑器对应的属性类型–>
  6.                    <entry key=”com.baobaotao.editor.Car”>
  7.                          <!–②-2对应的属性编辑器Bean –>
  8.                     <bean class=”com.baobaotao.editor.CustomCarEditor” />
  9.                 </entry>
  10.             </map>
  11.         </property>
  12.     </bean>
  13.  <bean id=”boss” class=”com.baobaotao.editor.Boss”>
  14.        <property name=”name” value=”John”/>
  15.        <!–③该属性将使用②处的属性编辑器完成属性填充操作–>
  16.        <property name=”car” value=”红旗CA72,200,20000.00″/>
  17. </bean>

在①处,我们定义了用于注册自定义属性编辑器的CustomEditorConfigurer,Spring容器将通过反射机制自动调用这个Bean。CustomEditorConfigurer通过一个Map属性定义需要自动注册的自定义属性编辑器。在②处,我们为Car类型指定了对应属性编辑器CustomCarEditor,注意键是属性类型,而值是对应的属性编辑器Bean,而不是属性编辑器的类名。

最精彩的部分当然是③处的配置,我们原来通过一个<bean>元素标签配置好car Bean,然后在boss的<bean>中通过ref引用car Bean,但是现在我们直接通过value为car属性提供配置。BeanWrapper在设置boss的car属性时,它将检索自定义属性编辑器的注册表,当发现Car属性类型拥有对应的属性编辑器CustomCarEditor时,它就会利用CustomCarEditor将“红旗CA72,200,20000.00”转换为Car对象。

引用
按照JavaBeans的规范,JavaBeans的基础设施会在JavaBean相同类包下查找是否存在<JavaBean>Editor的类,如果存在,自动使用<JavaBean>Editor作为该JavaBean的PropertyEditor。
如com.baobaotao.domain.UserEditor会自动成为com.baobaotao.domain.User对应的PropertyEditor。Spring也支持这个规范,也即如果采用这种规约命令PropertyEditor,就无须显式在CustomEditorConfigurer中注册了,Spring将自动查找并注册这个PropertyEditor。

另:Spring 3.0除支持PropertyEditor外,还在核心包中引入了自建的ConversionService,它提供了更为强大的类型转换的能力,可以完成任意类型之间的转换,还可以在转换过程中参考目标对象所在宿主类的上下文信息。Spring的类型转换同时支持PropertyEdito和ConversionService。

学习Spring必学的Java基础知识(4)—-XML基础知识

XML的特殊字符

XML中共有5个特殊的字符,分别是:&<>“’。如果配置文件中的注入值包括这些特殊字符,就需要进行特别处理。有两种解决方法:其一,采用本例中的<![CDATA[ ]]>特殊标签,将包含特殊字符的字符串封装起来;其二,使用XML转义序列表示这些特殊的字符,这5个特殊字符所对应XML转义序列在表4-2中说明:

d8d61b82-d546-368a-83c1-6bfefa060b04

Spring在进行XML配置时,如果属性值包含了一个XML的特殊符号,因此我们特意在属性值外添加了一个<![CDATA[ ]]>的XML特殊处理标签,<![CDATA[ ]]>的作用是让XML解析器将标签中的字符串当作普通的文本对待,以防止某些字符串对XML格式造成破坏。来看一个例子:

  1. <bean id=”car” class=”com.baobaotao.attr.Car”>
  2.     <property name=”maxSpeed”>
  3.         <value>200</value>
  4.     </property>
  5.     <property name=”brand”>①
  6.         <value><![CDATA[红旗&CA72]]></value>
  7.     </property>
  8. </bean>

如果使用XML转义序列,我们可以使用以下的配置替换代码清单4-10中的配置:

  1. <property name=”brand”><value>红旗&amp;CA72</value></property>

Schema命名空间的声明

对于基于XML的配置,Spring 1.0的配置文件采用DTD格式,Spring 2.0以后使用Schema的格式,后者让不同类型的配置拥有了自己的命名空间,使配置文件更具扩展性。此外,Spring基于Schema配置方案为许多领域的问题提供了简化的配置方法,配置工作因此得到了大幅简化。

采取基于Schema配置格式,文件头的声明会复杂一些,先看一个简单的示例:

bb7d4050-6bbf-31b5-a8b9-9d88afbfd15e

要了解文件头中声明的内容,需要学习一点XML Schema的知识,Schema在文档根节点中通过xmlns对文档中的命名空间进行声明。我们在上面的代码中定义了3个命名空间:

① 默认命名空间:它没有空间名,用于Spring Bean的定义;
②  xsi命名空间:这个命名空间用于为每个文档中命名空间指定相应的Schema样式文件,是标准组织定义的标准命名空间;
③  aop命名空间:这个命名空间是Spring配置AOP的命名空间,是用户自定义的命名空间。

命名空间的定义分为两个步骤:第一步指定命名空间的名称,第二步指定命名空间的Schema文档样式文件的位置,用空格或回车换行进行分隔。

在第一步中,需要指定命名空间的缩略名和全名,请看下面配置所定义的命名空间:

  1. xmlns:aop=”http://www.springframework.org/schema/aop”

aop为命名空间的别名,一般使用简洁易记的名称,文档后面的元素可通过命名空间别名加以区分,如<aop:config/>等。而http://www.springframework.org/schema/aop为空间的全限定名,习惯上用文档发布机构的官方网站和相关网站目录作为全限定名。这种命名方式既可以标识文档所属的机构,又可以很好地避免重名的问题。但从XML Schema语法来说,别名和全限定名都可以任意命名。

如果命名空间的别名为空,则表示该命名空间为文档默认命名空间,文档中无命名空间前缀的元素都属于默认命名空间,如<beans/>、<bean/>等都属于①处定义的默认命名空间。
在第二步中,为每个命名空间指定了对应的Schema文档格式定义文件,定义的语法是:

472938ae-472e-357e-9b5d-db062eea301f

命名空间使用全限定名,每个组织机构在发布Schema文件后,都会为该Schema文件提供一个引用的URL地址,一般使用这个URL地址指定命名空间对应的Schema文件。命名空间名称和对应的Schema文件地址之间使用空格或回车分隔,不同的命名空间之间也使用这种分隔方法。

指定命名空间的Schema文件地址有两个用途:XML解析器可以获取Schema文件并对文档进行格式合法性验证;在开发环境下,IDE可以引用Schema文件对文档编辑提供诱导功能。

学习Spring必学的Java基础知识(5)—-注解
有必要对JDK 5.0新增的注解(Annotation)技术进行简单的学习,因为Spring 支持@AspectJ,而@AspectJ本身就是基于JDK 5.0的注解技术。所以学习JDK 5.0的注解知识有助于我们更好地理解和掌握Spring的AOP技术。

了解注解

对于Java开发人员来说,在编写代码时,除了源程序以外,我们还会使用Javadoc标签对类、方法或成员变量进行注释,以便使用Javadoc工具生成和源代码配套的Javadoc文档。这些@param、@return等Javadoc标签就是注解标签,它们为第三方工具提供了描述程序代码的注释信息。使用过Xdoclet的朋友,对此将更有感触,像Struts、Hibernate都提供了Xdoclet标签,使用它们可以快速地生成对应程序代码的配置文件。

JDK5.0注解可以看成是Javadoc标签和Xdoclet标签的延伸和发展。在JDK5.0中,我们可以自定义这些标签,并通过Java语言的反射机制中获取类中标注的注解,完成特定的功能。
注解是代码的附属信息,它遵循一个基本原则:注解不能直接干扰程序代码的运行,无论增加或删除注解,代码都能够正常运行。Java语言解释器会忽略这些注解,而由第三方工具负责对注解进行处理。第三方工具可以利用代码中的注解间接控制程序代码的运行,它们通过Java反射机制读取注解的信息,并根据这些信息更改目标程序的逻辑,而这正是Spring AOP对@AspectJ提供支持所采取的方法。

引用
很多东西的设计都必须遵循最基本的原则,为了防止机器人伤害人类,科幻作家阿西莫夫于1940年提出了“机器人三原则”:第一,机器人不能伤害人类;第二,机器人应遵守人类的命令,与第一条违背的命令除外;第三,机器人应能保护自己,与第一条违背的命令除外。这是给机器人赋予的伦理性纲领,机器人学术界一直将这三条原则作为机器人开发的准则。

一个简单的注解类

通常情况下,第三方工具不但负责处理特定的注解,本身还提供了这些注解的定义,所以我们通常仅需关注如何使用注解就可以了。但定义注解类本身并不困难,Java提供了定义注解的语法。下面,我们马上着手编写一个简单的注解类,如代码清单7-1所示:

代码清单7-1  NeedTest注解类

  1. package com.baobaotao.aspectj.anno;
  2. import java.lang.annotation.ElementType;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. import java.lang.annotation.Target;
  6. @Retention(RetentionPolicy.RUNTIME) //①声明注解的保留期限
  7. @Target(ElementType.METHOD)//②声明可以使用该注解的目标类型
  8. public @interface NeedTest {//③定义注解
  9.     boolean value() default true;//④声明注解成员
  10. }

Java新语法规定使用@interface修饰符定义注解类,如③所示,一个注解可以拥有多个成员,成员声明和接口方法声明类似,这里,我们仅定义了一个成员,如④所示。成员的声明有以下几点限制:

  • 成员以无入参无抛出异常的方式声明,如boolean value(String str)、boolean value() throws Exception等方式是非法的;
  • 可以通过default为成员指定一个默认值,如String level() default “LOW_LEVEL”、int high() default 2是合法的,当然也可以不指定默认值;
  • 成员类型是受限的,合法的类型包括原始类型及其封装类、String、Class、enums、注解类型,以及上述类型的数组类型。如ForumService value()、List foo()是非法的。

在①和②处,我们所看到的注解是Java预定义的注解,称为元注解(Meta-Annotation),它们被Java编译器使用,会对注解类的行为产生影响。@Retention(RetentionPolicy. RUNTIME)表示NeedTest这个注解可以在运行期被JVM读取,注解的保留期限类型在java.lang.annotation.Retention类中定义,介绍如下:

  • SOURCE:注解信息仅保留在目标类代码的源码文件中,但对应的字节码文件将不再保留;
  • CLASS:注解信息将进入目标类代码的字节码文件中,但类加载器加载字节码文件时不会将注解加载到JVM中,也即运行期不能获取注解信息;
  • RUNTIME:注解信息在目标类加载到JVM后依然保留,在运行期可以通过反射机制读取类中注解信息。
  • Target(ElementType.METHOD)表示NeedTest这个注解只能应用到目标类的方法上,注解的应用目标在java.lang.annotation.ElementType类中定义:

  • TYPE:类、接口、注解类、Enum声明处,相应的注解称为类型注解;
  • FIELD:类成员变量或常量声明处,相应的注解称为域值注解;
  • METHOD:方法声明处,相应的注解称为方法注解;
  • PARAMETER:参数声明处,相应的注解称为参数注解;
  • CONSTRUCTOR:构造函数声明处,相应的注解称为构造函数注解;
  • LOCAL_VARIABLE:局部变量声明处,相应的注解称为局域变量注解;
  • ANNOTATION_TYPE:注解类声明处,相应的注解称为注解类注解,ElementType. TYPE包括ElementType.ANNOTATION_TYPE;
  • PACKAGE:包声明处,相应的注解称为包注解。

如果注解只有一个成员,则成员名必须取名为value(),在使用时可以忽略成员名和赋值号(=),如@NeedTest(true)。注解类拥有多个成员时,如果仅对value成员进行赋值则也可不使用赋值号,如果同时对多个成员进行赋值,则必须使用赋值号,如DeclareParents (value = “NaiveWaiter”, defaultImpl = SmartSeller.class)。注解类可以没有成员,没有成员的注解称为标识注解,解释程序以标识注解存在与否进行相应的处理;此外,所有的注解类都隐式继承于java.lang.annotation.Annotation,但注解不允许显式继承于其他的接口。

我们希望使用NeedTest注解对业务类的方法进行标注,以便测试工具可以根据注解情况激活或关闭对业务类的测试。在编写好NeedTest注解类后,就可以在其他类中使用它了。

使用注解

我们在ForumService中使用NeedTest注解,标注业务方法是否需要测试,如代码清单7-2所示:

代码清单7-2  ForumService:使用注解

  1. package com.baobaotao.aspectj.anno;
  2. public class ForumService {
  3.     @NeedTest(value=true) ①
  4.     public void deleteForum(int forumId){
  5.         System.out.println(“删除论坛模块:”+forumId);
  6.     }
  7.     @NeedTest(value=false) ②
  8.     public void deleteTopic(int postId){
  9.         System.out.println(“删除论坛主题:”+postId);
  10.     }
  11. }

如果注解类和目标类不在同一个包中,需要通过import引用的注解类。在①和②处,我们使用NeedTest分别对deleteForum()和deleteTopic()方法进行标注。在标注注解时,可以通过以下格式对注解成员进行赋值:

引用
@<注解名>(<成员名1>=<成员值1>,<成员名1>=<成员值1>,…)

如果成员是数组类型,可以通过{}进行赋值,如boolean数组的成员可以设置为{true,false,true}。下面是几个注解标注的例子:

示例1,多成员的注解:

  1. @AnnoExample(id= 2868724, synopsis = ”Enable time-travel”,
  2. engineer = ”Mr. Peabody”,date = ”4/1/2007″)

示例2,一个成员的注解,成员名为value。可以省略成员名和赋值符号:

  1. @Copyright(“2011 bookegou.com All Right Reserved”)

示例3,无成员的注解:

示例4,成员为字符串数组的注解:

  1. @SuppressWarnings(value={“unchecked”,”fallthrough”})

示例5,成员为注解数组类型的注解:

  1. @Reviews({@Review(grade=Review.Grade.EXCELLENT,reviewer=”df”),
  2.            @Review(grade=Review.Grade.UNSATISFACTORY,reviewer=”eg”,
  3.                     comment=”This method needs an @Override annotation”)})

@Reviews注解拥有一个@Review注解数组类型的成员,@Review注解类型有三个成员,其中reviewer、comment都是String类型,但comment有默认值,grade是枚举类型的成员。

由于NeedTest注解的保留限期是RetentionPolicy.RUNTIME类型,因此当ForumService被加载到JVM时,仍就可通过反射机制访问到ForumService各方法的注解信息。

访问注解

前面提到过,注解不会直接影响程序的运行,但是第三方程序或工具可以利用代码中的注解完成特殊的任务,间接控制程序的运行。对于RetentionPolicy.RUNTIME保留期限的注解,我们可以通过反射机制访问类中的注解。

在JDK5.0里,Package、Class、Constructor、Method以及Field等反射对象都新增了访问注解信息的方法:<T extends Annotation>T getAnnotation(Class<T> annotationClass),该方法支持通过泛型直接返回注解对象。

下面,我们就通过反射来访问注解,得出ForumService 类中通过@NeedTest注解所承载的测试需求,如代码清单7-3所示:

代码清单7-3  TestTool:访问代码中的注解

  1. package com.baobaotao.aspectj.anno;
  2. import java.lang.reflect.Method;
  3. public class TestTool {
  4.     public static void main(String[] args) {
  5.                //①得到ForumService对应的Class对象
  6.         Class clazz = ForumService.class;
  7.                 //②得到ForumSerivce对应的Method数组
  8.         Method[] methods = clazz.getDeclaredMethods();
  9.         System.out.println(methods.length);
  10.         for (Method method : methods) {
  11.                         //③获取方法上所标注的注解对象
  12.             NeedTest nt = method.getAnnotation(NeedTest. class);
  13.             if (nt != null) {
  14.                 if (nt.value()) {
  15.                     System.out.println(method.getName() + ”()需要测试”);
  16.                 } else {
  17.                     System.out.println(method.getName() + ”()不需要测试”);
  18.                 }
  19.             }
  20.         }
  21.     }
  22. }

在③处,通过方法的反射对象,我们获取了方法上所标注的NeedTest注解对象,接着就可以访问注解对象的成员,从而得到ForumService类方法的测试需求。运行以上代码,输出以下的信息:

引用
deleteForum()需要测试
deleteTopic()不需要测试

我们知道Spring通过各种模板类降低了开发者使用各种数据持久技术的难度。这些模板类都是线程安全的,也就是说,多个DAO可以复用同一个模板实例而不会发生冲突。我们使用模板类访问底层数据,根据持久化技术的不同,模板类需要绑定数据连接或会话的资源。但这些资源本身是非线程安全的,也就是说它们不能在同一时刻被多个线程共享。虽然模板类通过资源池获取数据连接或会话,但资源池本身解决的是数据连接或会话的缓存问题,并非数据连接或会话的线程安全问题。

按照传统经验,如果某个对象是非线程安全的,在多线程环境下,对对象的访问必须采用synchronized进行线程同步。但模板类并未采用线程同步机制,因为线程同步会降低并发性,影响系统性能。此外,通过代码同步解决线程安全的挑战性很大,可能会增强好几倍的实现难度。那么模板类究竟仰仗何种魔法神功,可以在无须线程同步的情况下就化解线程安全的难题呢?答案就是ThreadLocal!::__IHACKLOG_REMOTE_IMAGE_AUTODOWN_BLOCK__::41

ThreadLocal在Spring中发挥着重要的作用,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了它们的身影,起着举足轻重的作用。要想了解Spring事务管理的底层技术,ThreadLocal是必须攻克的山头堡垒。

ThreadLocal是什么

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
ThreadLocal,顾名思义,它不是一个线程,而是线程的一个本地化对象。当工作于多线程中的对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程的角度看,这个变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。

线程局部变量并不是Java的新发明,很多语言(如IBM XL、FORTRAN)在语法层面就提供线程局部变量。在Java中没有提供语言级支持,而以一种变通的方法,通过ThreadLocal的类提供支持。所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,这也是为什么线程局部变量没有在Java开发者中得到很好普及的原因。

ThreadLocal的接口方法

ThreadLocal类接口很简单,只有4个方法,我们先来了解一下。

    • void set(Object value)
    • 设置当前线程的线程局部变量的值;

    • public Object get()
    • 该方法返回当前线程所对应的线程局部变量;

    • public void remove()
    • 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度;

    • protected Object initialValue()
    • 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的默认实现直接返回一个null。

值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:

代码清单9-3  SimpleThreadLocal

  1. public class SimpleThreadLocal {
  2.     private Map valueMap = Collections.synchronizedMap(new HashMap());
  3.     public void set(Object newValue) {
  4.                 //①键为线程对象,值为本线程的变量副本
  5.         valueMap.put(Thread.currentThread(), newValue);
  6.     }
  7.     public Object get() {
  8.         Thread currentThread = Thread.currentThread();
  9.                 //②返回本线程对应的变量
  10.         Object o = valueMap.get(currentThread);
  11.                 //③如果在Map中不存在,放到Map中保存起来
  12.                if (o == null && !valueMap.containsKey(currentThread)) {
  13.             o = initialValue();
  14.             valueMap.put(currentThread, o);
  15.         }
  16.         return o;
  17.     }
  18.     public void remove() {
  19.         valueMap.remove(Thread.currentThread());
  20.     }
  21.     public Object initialValue() {
  22.         return null;
  23.     }
  24. }

虽然代码清单9 3中这个ThreadLocal实现版本显得比较幼稚,但它和JDK所提供的ThreadLocal类在实现思路上是非常相近的。

一个TheadLocal实例

下面,我们通过一个具体的实例了解一下ThreadLocal的具体使用方法。

代码清单9-4  SequenceNumber

  1. package com.baobaotao.basic;
  2. public class SequenceNumber {
  3.         //①通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
  4.     private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
  5.         public Integer initialValue(){
  6.             return 0;
  7.         }
  8.     };
  9.         //②获取下一个序列值
  10.     public int getNextNum(){
  11.         seqNum.set(seqNum.get()+1);
  12.         return seqNum.get();
  13.     }
  14.     public static void main(String[ ] args)
  15.     {
  16.           SequenceNumber sn = new SequenceNumber();
  17.          //③ 3个线程共享sn,各自产生序列号
  18.          TestClient t1 = new TestClient(sn);
  19.          TestClient t2 = new TestClient(sn);
  20.          TestClient t3 = new TestClient(sn);
  21.          t1.start();
  22.          t2.start();
  23.          t3.start();
  24.     }
  25.     private static class TestClient extends Thread
  26.     {
  27.         private SequenceNumber sn;
  28.         public TestClient(SequenceNumber sn) {
  29.             this.sn = sn;
  30.         }
  31.         public void run()
  32.         {
  33.                         //④每个线程打出3个序列值
  34.             for (int i = 0; i < 3; i++) {
  35.             System.out.println(“thread["+Thread.currentThread().getName()+
  36. "] sn["+sn.getNextNum()+"]“);
  37.             }
  38.         }
  39.     }
  40. }

通常我们通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值,如①处所示。TestClient线程产生一组序列号,在③处,我们生成3个TestClient,它们共享同一个SequenceNumber实例。运行以上代码,在控制台上输出以下的结果:

引用
thread[Thread-2] sn[1]
thread[Thread-0] sn[1]
thread[Thread-1] sn[1]
thread[Thread-2] sn[2]
thread[Thread-0] sn[2]
thread[Thread-1] sn[2]
thread[Thread-2] sn[3]
thread[Thread-0] sn[3]
thread[Thread-1] sn[3]

考查输出的结果信息,我们发现每个线程所产生的序号虽然都共享同一个Sequence Number实例,但它们并没有发生相互干扰的情况,而是各自产生独立的序列号,这是因为我们通过ThreadLocal为每一个线程提供了单独的副本。

与Thread同步机制的比较

ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal为每一个线程提供一个独立的变量副本,从而隔离了多个线程对访问数据的冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的对象封装,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度上简化ThreadLocal的使用,代码清单9-2就使用了JDK 5.0新的ThreadLocal<T>版本。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。而ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

Spring使用ThreadLocal解决线程安全问题

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9-2所示。

4aec4a17-aebd-387b-9e79-c7f3e3e68664

这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。
下面的实例能够体现Spring对有状态Bean的改造思路:

代码清单9-5  TopicDao:非线程安全

  1. public class TopicDao {
  2.    //①一个非线程安全的变量
  3.    private Connection conn;
  4.    public void addTopic(){
  5.         //②引用非线程安全变量
  6.        Statement stat = conn.createStatement();
  7.        …
  8.    }
  9. }

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单9-6  TopicDao:线程安全

  1. import java.sql.Connection;
  2. import java.sql.Statement;
  3. public class TopicDao {
  4.   //①使用ThreadLocal保存Connection变量
  5. private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
  6. public static Connection getConnection(){
  7.         //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
  8.         //并将其保存到线程本地变量中。
  9. if (connThreadLocal.get() == null) {
  10.             Connection conn = ConnectionManager.getConnection();
  11.             connThreadLocal.set(conn);
  12.               return conn;
  13.         }else{
  14.               //③直接返回线程本地变量
  15.             return connThreadLocal.get();
  16.         }
  17.     }
  18.     public void addTopic() {
  19.         //④从ThreadLocal中获取线程对应的
  20.          Statement stat = getConnection().createStatement();
  21.     }
  22. }

不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果为null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多Dao共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个实例基本上说明了Spring对有状态类线程安全化的解决思路。在本章后面的内容中,我们将详细说明Spring如何通过ThreadLocal解决事务管理的问题。

Spring虽然提供了灵活方便的事务管理功能,但这些功能都是基于底层数据库本身的事务处理机制工作的。要深入了解Spring的事务管理和配置,有必要先对数据库事务的基础知识进行学习。

何为数据库事务

“一荣俱荣,一损俱损”这句话很能体现事务的思想,很多复杂的事物要分步进行,但它们组成一个整体,要么整体生效,要么整体失效。这种思想反映到数据库上,就是多个SQL语句,要么所有执行成功,要么所有执行失败。
数据库事务有严格的定义,它必须同时满足 4 个特性:原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durabiliy),简称为ACID。下面是对每个特性的说明。

  • 原子性:表示组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才提交,事务中任何一个数据库操作失败,已经执行的任何操作都必须撤销,让数据库返回到初始状态。
  • 一致性:事务操作成功后,数据库所处的状态和它的业务规则是一致的,即数据不会被破坏。如从A账户转账100元到B账户,不管操作成功与否,A和B的存款总额是不变的。
  • 隔离性:在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。准确地说,并非要求做到完全无干扰,数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性越好,但并发性越弱。
  • 持久性:一旦事务提交成功后,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证能够通过某种机制恢复数据。

在这些事务特性中,数据“一致性”是最终目标,其他的特性都是为达到这个目标的措施、要求或手段。

数据库管理系统一般采用重执行日志保证原子性、一致性和持久性,重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即可以根据重执行日志撤销已经执行的操作。此外,对于已经提交的事务,即使数据库崩溃,在重启数据库时也能够根据日志对尚未持久化的数据进行相应的重执行操作。

和Java程序采用对象锁机制进行线程同步类似,数据库管理系统采用数据库锁机制保证事务的隔离性。当多个事务试图对相同的数据进行操作时,只有持有锁的事务才能操作数据,直到前一个事务完成后,后面的事务才有机会对数据进行操作。Oracle数据库还使用了数据版本的机制,在回滚段为数据的每个变化都保存一个版本,使数据的更改不影响数据的读取。

数据并发的问题

一个数据库可能拥有多个访问客户端,这些客户端都可以并发方式访问数据库。数据库中的相同数据可能同时被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。这些问题可以归结为5类,包括3类数据读问题(脏读、不可重复读和幻象读)以及2类数据更新问题(第一类丢失更新和第二类丢失更新)。下面,我们分别通过实例讲解引发问题的场景。

脏读(dirty read)

A事务读取B事务尚未提交的更改数据,并在这个数据的基础上操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不被承认的。来看取款事务和转账事务并发时引发的脏读场景:

f3a9e5d2-f781-31be-915b-22737b31b976

在这个场景中,B希望取款500元而后又撤销了动作,而A往相同的账户中转账100元,就因为A事务读取了B事务尚未提交的数据,因而造成账户白白丢失了500元。在Oracle数据库中,不会发生脏读的情况。

引用
一个有结巴的人在饮料店柜台前转悠,老板很热情地迎上来说:“喝一瓶?”结巴连忙说:“我…喝…喝…”老板麻利地打开易拉罐递给结巴,结巴终于憋出了他的那句话:“我…喝…喝…喝不起啊!”

不可重复读(unrepeatable read)

不可重复读是指A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两次读取账户的余额发生不一致:

9327486a-634e-39b0-9954-2e889adc3e81

在同一事务中,T4时间点和T7时间点读取账户存款余额不一样。

幻象读(phantom read)

A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题。幻象读一般发生在计算统计数据的事务中,举一个例子,假设银行系统在同一个事务中,两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100元,这时,两次统计的总金额将不一致:

6645075a-24e0-3445-8c2d-bb21caf0c424

如果新增数据刚好满足事务的查询条件,这个新数据就进入了事务的视野,因而产生了两个统计不一致的情况。

幻象读和不可重复读是两个容易混淆的概念,前者是指读到了其他已经提交事务的新增数据,而后者是指读到了已经提交事务的更改数据(更改或删除),为了避免这两种情况,采取的对策是不同的,防止读取到更改数据,只需要对操作的数据添加行级锁,阻止操作中的数据发生变化,而防止读取到新增数据,则往往需要添加表级锁——将整个表锁定,防止新增数据(Oracle使用多版本数据的方式实现)。

第一类丢失更新

A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来:

59e5f0a2-562d-3df4-86cb-1c0240aab3e9

A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。

第二类丢失更新

A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:

651c23fa-7fd1-3a0b-bfd9-92c67ec97b8f

上面的例子里由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户账户将损失100元。

数据库锁机制

数据并发会引发很多问题,在一些场合下有些问题是允许的,但在另外一些场合下可能却是致命的。数据库通过锁的机制解决并发访问的问题,虽然不同的数据库在实现细节上存在差别,但原理基本上是一样的。

按锁定的对象的不同,一般可以分为表锁定和行锁定,前者对整个表进行锁定,而后者对表中特定行进行锁定。从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定既防止其他的独占锁定,也防止其他的共享锁定。为了更改数据,数据库必须在进行更改的行上施加行独占锁定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE语句都会隐式采用必要的行锁定。下面我们介绍一下Oracle数据库常用的5种锁定。

  • 行共享锁定:一般通过SELECT FOR UPDATE语句隐式获得行共享锁定,在Oracle中用户也可以通过LOCK TABLE IN ROW SHARE MODE语句显式获得行共享锁定。行共享锁定并不防止对数据行进行更改的操作,但是可以防止其他会话获取独占性数据表锁定。允许进行多个并发的行共享和行独占性锁定,还允许进行数据表的共享或者采用共享行独占锁定。
  • 行独占锁定:通过一条INSERT、UPDATE或DELETE语句隐式获取,或者通过一条LOCK TABLE IN ROW EXCLUSIVE MODE语句显式获取。这个锁定可以防止其他会话获取一个共享锁定、共享行独占锁定或独占锁定。
  • 表共享锁定:通过LOCK TABLE IN SHARE MODE语句显式获得。这种锁定可以防止其他会话获取行独占锁定(INSERT、UPDATE或DELETE),或者防止其他表共享行独占锁定或表独占锁定,它允许在表中拥有多个行共享和表共享锁定。该锁定可以让会话具有对表事务级一致性访问,因为其他会话在用户提交或者回溯该事务并释放对该表的锁定之前不能更改这个被锁定的表。
  • 表共享行独占:通过LOCK TABLE IN SHARE ROW EXCLUSIVE MODE语句显式获得。这种锁定可以防止其他会话获取一个表共享、行独占或者表独占锁定,它允许其他行共享锁定。这种锁定类似于表共享锁定,只是一次只能对一个表放置一个表共享行独占锁定。如果A会话拥有该锁定,则B会话可以执行SELECT FOR UPDATE操作,但如果B会话试图更新选择的行,则需要等待。
  • 表独占:通过LOCK TABLE IN EXCLUSIVE MODE显式获得。这个锁定防止其他会话对该表的任何其他锁定。

事务隔离级别

尽管数据库为用户提供了锁的DML操作方式,但直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制。只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加上适合的锁。此外数据库还会维护这些锁,当一个资源上的锁数目太多时,自动进行锁升级以提高系统的运行性能,而这一过程对用户来说完全是透明的。

ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别,在相同数据环境下,使用相同的输入,执行相同的工作,根据不同的隔离级别,可以导致不同的结果。不同事务隔离级别能够解决的数据并发问题的能力是不同的,如表9-1所示。

e3b7b601-4215-327a-b158-6378597f2624

事务的隔离级别和数据库并发性是对立的,两者此增彼长。一般来说,使用READ UNCOMMITED隔离级别的数据库拥有最高的并发性和吞吐量,而使用SERIALIZABLE隔离级别的数据库并发性最低。

SQL 92定义READ UNCOMMITED主要是为了提供非阻塞读的能力,Oracle虽然也支持READ UNCOMMITED,但它不支持脏读,因为Oracle使用多版本机制彻底解决了在非阻塞读时读到脏数据的问题并保证读的一致性,所以,Oracle的READ COMMITTED隔离级别就已经满足了SQL 92标准的REPEATABLE READ隔离级别。

SQL 92推荐使用REPEATABLE READ以保证数据的读一致性,不过用户可以根据应用的需要选择适合的隔离等级。

JDBC对事务支持

并不是所有的数据库都支持事务,即使支持事务的数据库也并非支持所有的事务隔离级别,用户可以通过Connection#getMetaData()方法获取DatabaseMetaData对象,并通过该对象的supportsTransactions()、supportsTransactionIsolationLevel(int level)方法查看底层数据库的事务支持情况。

Connection默认情况下是自动提交的,也即每条执行的SQL都对应一个事务,为了能够将多条SQL当成一个事务执行,必须先通过Connection#setAutoCommit(false)阻止Connection自动提交,并可通过Connection#setTransactionIsolation()设置事务的隔离级别,Connection中定义了对应SQL 92标准4个事务隔离级别的常量。通过Connection#commit()提交事务,通过Connection#rollback()回滚事务。下面是典型的JDBC事务数据操作的代码:

代码清单9-1  JDBC事务代码

Java代码  ef8d6a8b-8aee-3348-aa76-66804aaa34733
  1. Connection conn ;
  2. try{
  3.      conn = DriverManager.getConnection();//①获取数据连接
  4.      conn.setAutoCommit(false); //②关闭自动提交的机制
  5.      conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); //③设置事务隔离级别
  6.      Statement stmt = conn.createStatement();
  7.      int rows = stmt.executeUpdate( ”INSERT INTO t_topic VALUES(1,’tom’) ” );
  8.      rows = stmt.executeUpdate( ”UPDATE t_user set topic_nums = topic_nums +1 ”+
  9. “WHERE user_id = 1″);
  10.      conn.commit();//④提交事务
  11. }catch(Exception e){
  12.      …
  13.      conn.rollback();//⑤回滚事务
  14. }finally{
  15.    …
  16. }

在JDBC 2.0中,事务最终只能有两个操作:提交和回滚。但是,有些应用可能需要对事务进行更多的控制,而不是简单地提交或回滚。JDBC 3.0(JDK 1.4及以后的版本)引入了一个全新的保存点特性,Savepoint 接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点,而并非像JDBC 2.0一样只回滚到开始事务的点,如图9-1所示。

45f9a7e8-f2c3-3fd8-849d-d8287552ad1f

下面的代码使用了保存点的功能,在发生特定问题时,回滚到指定的保存点,而非回滚整个事务,如代码清单9-2所示:

代码清单9-2  使用保存点的事务代码

  1. Statement stmt = conn.createStatement();
  2. int rows = stmt.executeUpdate( ”INSERT INTO t_topic VALUES(1,’tom’)”);
  3. Savepoint svpt = conn.setSavepoint(“savePoint1″);//①设置一个保存点
  4. rows = stmt.executeUpdate( ”UPDATE t_user set topic_nums = topic_nums +1 ”+
  5.           ”WHERE user_id = 1″);
  6. //②回滚到①处的savePoint1,①之前的SQL操作,在整个事务提交后依然提交,
  7. //但①到②之间的SQL操作被撤销了
  8. conn.rollback(svpt);
  9. conn.commit();//③提交事务

并非所有数据库都支持保存点功能,用户可以通过DatabaseMetaData#supportsSavepoints()方法查看是否支持。

假设我们正在开发一个支持多国语言的Web应用程序,要求系统能够根据客户端的系统的语言类型返回对应的界面:英文的操作系统返回英文界面,而中文的操作系统则返回中文界面——这便是典型的i18n国际化问题。对于有国际化要求的应用系统,我们不能简单地采用硬编码的方式编写用户界面信息、报错信息等内容,而必须为这些需要国际化的信息进行特殊处理。简单来说,就是为每种语言提供一套相应的资源文件,并以规范化命名的方式保存在特定的目录中,由系统自动根据客户端语言选择适合的资源文件。

基础知识

“国际化信息”也称为“本地化信息”,一般需要两个条件才可以确定一个特定类型的本地化信息,它们分别是“语言类型”和“国家/地区的类型”。如中文本地化信息既有中国大陆地区的中文,又有中国台湾、中国香港地区的中文,还有新加坡地区的中文。Java通过java.util.Locale类表示一个本地化对象,它允许通过语言参数和国家/地区参数创建一个确定的本地化对象。

语言参数使用ISO标准语言代码表示,这些代码是由ISO-639标准定义的,每一种语言由两个小写字母表示。在许多网站上都可以找到这些代码的完整列表,下面的网址是提供了标准语言代码的信息:http://www.loc.gov/standards/iso639-2/php/English_list.php

国家/地区参数也由标准的ISO国家/地区代码表示,这些代码是由ISO-3166标准定义的,每个国家/地区由两个大写字母表示。用户可以从以下网址查看ISO-3166的标准代码:
http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html。

表5-2给出了一些语言和国家/地区的标准代码:

e7780646-6578-3732-bfa1-e0035e799888

Locale

java.util.Locale是表示语言和国家/地区信息的本地化类,它是创建国际化应用的基础。下面给出几个创建本地化对象的示例:

  1. //①带有语言和国家/地区信息的本地化对象
  2. Locale locale1 = new Locale(“zh”,”CN”);
  3. //②只有语言信息的本地化对象
  4. Locale locale2 = new Locale(“zh”);
  5. //③等同于Locale(“zh”,”CN”)
  6. Locale locale3 = Locale.CHINA;
  7. //④等同于Locale(“zh”)
  8. Locale locale4 = Locale.CHINESE;
  9. //⑤获取本地系统默认的本地化对象
  10. Locale locale 5= Locale.getDefault();

用户既可以同时指定语言和国家/地区参数定义一个本地化对象①,也可以仅通过语言参数定义一个泛本地化对象②。Locale类中通过静态常量定义了一些常用的本地化对象,③和④处就直接通过引用常量返回本地化对象。此外,用户还可以获取系统默认的本地化对象,如⑤所示。

引用
在测试时,如果希望改变系统默认的本地化设置,可以在启动JVM时通过命令参数指定:java -Duser.language=en -Duser.region=US MyTest。

本地化工具类

JDK的java.util包中提供了几个支持本地化的格式化操作工具类:NumberFormat、DateFormat、MessageFormat。下面,我们分别通过实例了解它们的用法:

代码清单5-13  NumberFormat

  1. Locale locale = new Locale(“zh”, ”CN”);
  2. NumberFormat currFmt = NumberFormat.getCurrencyInstance(locale);
  3. double amt = 123456.78;
  4. System.out.println(currFmt.format(amt));

上面的实例通过NumberFormat按本地化的方式对货币金额进行格式化操作,运行实例,输出以下信息:

代码清单5-14  DateFormat

  1. Locale locale = new Locale(“en”, ”US”);
  2. Date date = new Date();
  3. DateFormat df = DateFormat.getDateInstance(DateFormat.MEDIUM, locale);
  4. System.out.println(df.format(date));

通过DateFormat#getDateInstance(int style,Locale locale)方法按本地化的方式对日期进行格式化操作。该方法第一个入参为时间样式,第二个入参为本地化对象。运行以上代码,输出以下信息:

MessageFormat在NumberFormat和DateFormat的基础上提供了强大的占位符字符串的格式化功能,它支持时间、货币、数字以及对象属性的格式化操作。下面的实例演示了一些常见的格式化功能:

代码清单5-15  MessageFormat

  1.  //①信息格式化串
  2. String pattern1 = ”{0},你好!你于{1}在工商银行存入{2} 元。”;
  3. String pattern2 = ”At {1,time,short} On{1,date,long},{0} paid {2,number, currency}.”;
  4. //②用于动态替换占位符的参数
  5. Object[] params = {“John”, new GregorianCalendar().getTime(),1.0E3};
  6. //③使用默认本地化对象格式化信息
  7. String msg1 = MessageFormat.format(pattern1,params);
  8. //④使用指定的本地化对象格式化信息
  9. MessageFormat mf = new MessageFormat(pattern2,Locale.US);
  10. String msg2 = mf.format(params);
  11. System.out.println(msg1);
  12. System.out.println(msg2);

pattern1是简单形式的格式化信息串,通过{n}占位符指定动态参数的替换位置索引,{0}表示第一个参数,{1}表示第二个参数,以此类推。

pattern2格式化信息串比较复杂一些,除参数位置索引外,还指定了参数的类型和样式。从pattern2中可以看出格式化信息串的语法是很灵活的,一个参数甚至可以出现在两个地方:如 {1,time,short}表示从第二个入参中获取时间部分的值,显示为短样式时间;而{1,date,long}表示从第二个入参中获取日期部分的值,显示为长样式时间。关于MessageFormat更详细的使用方法,请参见JDK的Javadoc。

在②处,定义了用于替换格式化占位符的动态参数,这里,我们使用到了JDK5.0自动装包的语法,否则必须采用封装类表示基本类型的参数值。

在③处,通过MessageFormat的format()方法格式化信息串。它使用了系统默认的本地化对象,由于我们是中文平台,因此默认为Locale.CHINA。而在④处,我们显式指定MessageFormat的本地化对象。

运行上面的代码,输出以下信息:

引用
John,你好!你于07-1-8 下午9:58在工商银行存入1,000元。
At 9:58 PM OnJanuary 8, 2007,John paid $1,000.00.
ResourceBoundle

如果应用系统中某些信息需要支持国际化功能,则必须为希望支持的不同本地化类型分别提供对应的资源文件,并以规范的方式进行命名。国际化资源文件的命名规范规定资源名称采用以下的方式进行命名:

引用
<资源名>_<语言代码>_<国家/地区代码>.properties

其中,语言代码和国家/地区代码都是可选的。<资源名>.properties命名的国际化资源文件是默认的资源文件,即某个本地化类型在系统中找不到对应的资源文件,就采用这个默认的资源文件。<资源名>_<语言代码>.properties命名的国际化资源文件是某一语言默认的资源文件,即某个本地化类型在系统中找不到精确匹配的资源文件,将采用相应语言默认的资源文件。

举一个例子:假设资源名为resource,则语言为英文,国家为美国,则与其对应的本地化资源文件命名为resource_en_US.properties。信息在资源文件以属性名/值的方式表示:

引用
greeting.common=How are you!
greeting.morning = Good morning!
greeting.afternoon = Good Afternoon!
对应语言为中文,国家/地区为中国大陆的本地化资源文件则命名为resource_zh_ CN.properties,资源文件内容如下:
greeting.common=\u60a8\u597d\uff01
greeting.morning=\u65e9\u4e0a\u597d\uff01
greeting.afternoon=\u4e0b\u5348\u597d\uff01

本地化不同的同一资源文件,虽然属性值各不相同,但属性名却是相同的,这样应用程序就可以通过Locale对象和属性名精确调用到某个具体的属性值了。

读者可能已经注意到,上面中文的本地化资源文件内容采用了特殊的编码表示中文字符,这是因为资源文件对文件内容有严格的要求:只能包含ASCII字符。所以必须将非ASCII字符的内容转换为Unicode代码的表示方式。如上面中文的resource_zh_CN.properties资源文件的三个属性值分别是“您好!”、“早上好!”和“下午好!”三个中文字符串对应的Unicode代码串。

如果在应用开发时,直接采用Unicode代码编辑资源文件是很不方便的,所以,通常我们直接使用正常的方式编写资源文件,在测试或部署时再采用工具进行转换。JDK在bin目录下为我们提供了一个完成此项功能的native2ascii工具,它可以将中文字符的资源文件转换为Unicode代码格式的文件,命令格式如下:

引用
native2ascii [-reverse] [-encoding 编码] [输入文件 [输出文件]]

resource_zh_CN.properties包含中文字符并且以UTF-8进行编码,假设将该资源文件放到d:\目录下,通过下面的命令就可以将其转换为Unicode代码的形式:

引用
D:\>native2ascii -encoding utf-8 d:\resource_zh_CN.properties
d:\resource_zh_CN_1.properties

由于原资源文件采用UTF-8编码,所以必须显式通过-encoding指定编码格式。

引用
通过native2ascii命令手工转换资源文件,不但在操作上不方便,转换后资源文件中的属性内容由于采用了ASCII编码,阅读起来也不方便。很多IDE开发工具都有属性编辑器的插件,插件会自动将资源文件内容转换为ASCII形式的编码,同时以正常的方式阅读和编辑资源文件的内容,这给开发和维护带来了很大的便利。对于MyEclipse来说,使用MyEclipse Properties Editor编辑资源属性文件;对于Intellij IDEA来说,无须安装任何插件就自然支持资源属性文件的这种编辑方式了。

如果应用程序中拥有大量的本地化资源文件,直接通过传统的File操作资源文件显然太过笨拙。Java为我们提供了用于加载本地化资源文件的方便类java.util.ResourceBoundle。

ResourceBoundle为加载及访问资源文件提供便捷的操作,下面的语句从相对于类路径的目录中加载一个名为resource的本地化资源文件:

  1. ResourceBundle rb = ResourceBundle.getBundle(“com/baobaotao/i18n/resource”, locale)

通过以下的代码即可访问资源文件的属性值:

  1. rb.getString(“greeting.common”)

来看下面的实例:

代码清单5-16  ResourceBoundle

  1. ResourceBundle rb1 = ResourceBundle.getBundle(“com/baobaotao/i18n/resource”, Locale.US);
  2. ResourceBundle rb2 = ResourceBundle.getBundle(“com/baobaotao/i18n/resource”, Locale.CHINA);
  3. System.out.println(“us:”+rb1.getString(“greeting.common”));
  4. System.out.println(“cn:”+rb2.getString(“greeting.common”));

rb1加载了对应美国英语本地化的resource_en_US.properties资源文件;而rb2加载了对应中国大陆中文的resource_zh_CN.properties资源文件。运行上面的代码,将输出以下信息:

引用
us:How are you!
cn:您好!

加载资源文件时,如果不指定本地化对象,将使用本地系统默认的本地化对象。所以,在中文系统中,ResourceBundle.getBundle(“com/baobaotao/i18n/resource”)语句也将返回和代码清单5-14中rb2相同的本地化资源。

ResourceBundle在加载资源时,如果指定的本地化资源文件不存在,它按以下顺序尝试加载其他的资源:本地系统默认本地化对象对应的资源→默认的资源。上面的例子中,假设我们使用ResourceBundle.getBundle(“com/baobaotao/i18n/resource”,Locale.CANADA)加载资源,由于不存在resource_en_CA.properties资源文件,它将尝试加载resource_zh_CN.properties的资源文件,假设resource_zh_CN.properties资源文件也不存在,它将继续尝试加载resource.properties的资源文件,如果这些资源都不存在,将抛出java.util.MissingResourceException异常。

在资源文件中使用格式化串

在上面的资源文件中,属性值都是一般的字符串,它们不能结合运行时的动态参数构造出灵活的信息,而这种需求是很常见的。要解决这个问题很简单,只须使用带占位符的格式化串作为资源文件的属性值并结合使用MessageFormat就可以满足要求了。

上面的例子中,我们仅向用户提供一般性问候,下面我们对资源文件进行改造,通过格式化串让问候语更具个性化:

引用
greeting.common=How are you!{0},today is {1}
greeting.morning = Good morning!{0},now is {1 time short}
greeting.afternoon = Good Afternoon!{0} now is {1 date long}

将该资源文件保存在fmt_resource_en_US.properties中,按照同样的方式编写对应的中文本地化资源文件fmt_resource_zh_CN.properties。

下面,我们联合使用ResourceBoundle和MessageFormat得到美国英文的本地化问候语:

代码清单5-17  资源文件格式化串处理

  1.  //①加载本地化资源
  2. ResourceBundle rb1 =
  3.              ResourceBundle.getBundle(“com/baobaotao/i18n/fmt_ resource”,Locale.US);
  4. ResourceBundle rb2 =
  5.               ResourceBundle.getBundle(“com/baobaotao/i18n/fmt_ resource”,Locale.CHINA);
  6. Object[] params = {“John”, new GregorianCalendar().getTime()};
  7. String str1 = new MessageFormat(rb1.getString(“greeting.common”),Locale. US).format(params); ②
  8. String str2 =new MessageFormat(rb2.getString(“greeting.morning”),Locale. CHINA).format(params);
  9. String str3 =new MessageFormat(rb2.getString(“greeting.afternoon”),Locale. CHINA).format(params);
  10. System.out.println(str1);
  11. System.out.println(str2);
  12. System.out.println(str3);

运行以上的代码,将输出以下信息:

引用
How are you!John,today is 1/9/07 4:11 PM
早上好!John,现在是下午4:11
下午好!John,现在是2007年1月9日

MessageSource

Spring定义了访问国际化信息的MessageSource接口,并提供了几个易用的实现类。首先来了解一下该接口的几个重要方法:

    • String getMessage(String code, Object[] args, String defaultMessage, Locale locale)
    • code表示国际化资源中的属性名;args用于传递格式化串占位符所用的运行期参数;当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;locale表示本地化对象;

    • String getMessage(String code, Object[] args, Locale locale)
    • throws NoSuchMessageException

      与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException异常;
    • String getMessage(MessageSourceResolvable resolvable, Locale locale)
    • throws NoSuchMessageException

      MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个接口方法相同。

MessageSource的类结构

MessageSource分别被HierarchicalMessageSource和ApplicationContext接口扩展,这里我们主要看一下HierarchicalMessageSource接口的几个实现类,如图5-7所示:

5507ea89-e8ed-34b3-8ce5-002718dc1694

HierarchicalMessageSource接口添加了两个方法,建立父子层级的MessageSource结构,类似于前面我们所介绍的HierarchicalBeanFactory。该接口的setParentMessageSource (MessageSource parent)方法用于设置父MessageSource,而getParentMessageSource()方法用于返回父MessageSource。

HierarchicalMessageSource接口最重要的两个实现类是ResourceBundleMessageSource和ReloadableResourceBundleMessageSource。它们基于Java的ResourceBundle基础类实现,允许仅通过资源名加载国际化资源。ReloadableResourceBundleMessageSource提供了定时刷新功能,允许在不重启系统的情况下,更新资源的信息。StaticMessageSource主要用于程序测试,它允许通过编程的方式提供国际化信息。而DelegatingMessageSource是为方便操作父MessageSource而提供的代理类。

ResourceBundleMessageSource

该实现类允许用户通过beanName指定一个资源名(包括类路径的全限定资源名),或通过beanNames指定一组资源名。在代码清单5-18中,我们通过JDK的基础类完成了本地化的操作,下面我们使用ResourceBundleMessageSource来完成相同的任务。读者可以比较两者的使用差别,并体会Spring所提供的国际化处理功能所带给我们的好处:

代码清单5-18  通过ResourceBundleMessageSource配置资源

  1. <bean id=”myResource”
  2. class=”org.springframework.context.support.ResourceBundleMessageSource”>
  3.     <!–①通过基名指定资源,相对于类根路径–>
  4.     <property name=”basenames”>
  5.        <list>
  6.           <value>com/baobaotao/i18n/fmt_resource</value>
  7.        </list>
  8.     </property>
  9.   </bean>

启动Spring容器,并通过MessageSource访问配置的国际化资源,如代码清单 5 19所示:

代码清单5-19  访问国际化消息:ResourceBundleMessageSource

  1. String[] configs = {“com/baobaotao/i18n/beans.xml”};
  2. ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
  3. //①获取MessageSource的Bean
  4. MessageSource ms = (MessageSource)ctx.getBean(“myResource”);
  5. Object[] params = {“John”, new GregorianCalendar(). getTime()};
  6. //②获取格式化的国际化信息
  7. String str1 = ms.getMessage(“greeting.common”,params,Locale.US);
  8. String str2 = ms.getMessage(“greeting.morning”,params,Locale.CHINA);
  9. String str3 = ms.getMessage(“greeting.afternoon”,params,Locale.CHINA);
  10. System.out.println(str1);
  11. System.out.println(str2);
  12. System.out.println(str3);

比较代码清单5-19中的代码,我们发现最主要的区别在于我们无须再分别加载不同语言、不同国家/地区的本地化资源文件,仅仅通过资源名就可以加载整套的国际化资源文件。此外,我们无须显式使用MessageFormat操作国际化信息,仅通过MessageSource# getMessage()方法就可以完成操作了。这段代码的运行结果与代码清单5 17的运行结果完全一样。

ReloadableResourceBundleMessageSource

前面,我们提到该实现类比之于ResourceBundleMessageSource的唯一区别在于它可以定时刷新资源文件,以便在应用程序不重启的情况下感知资源文件的变化。很多生产系统都需要长时间持续运行,系统重启会给运行带来很大的负面影响。这时,通过该实现类就可以解决国际化信息更新的问题。请看下面的配置:

代码清单5-20  通过ReloadableResourceBundleMessageSource配置资源

  1. <bean id=”myResource”
  2. lass=”org.springframework.context.support. ReloadableResourceBundleMessageSource”>
  3.    <property name=”basenames”>
  4.       <list>
  5.         <value>com/baobaotao/i18n/fmt_resource</value>
  6.       </list>
  7.    </property>
  8.    <!–① 刷新资源文件的周期,以秒为单位–>
  9.    <property name=”cacheSeconds” value=”5″/>
  10.  </bean>

在上面的配置中,我们通过cacheSeconds属性让ReloadableResourceBundleMessageSource每5秒钟刷新一次资源文件(在真实的应用中,刷新周期不能太短,否则频繁的刷新将带来性能上的负面影响,一般不建议小于30分钟)。cacheSeconds默认值为-1表示永不刷新,此时,该实现类的功能就蜕化为ResourceBundleMessageSource的功能。

我们编写一个测试类对上面配置的ReloadableResourceBundleMessageSource进行测试:

代码清单5-21  刷新资源:ReloadableResourceBundleMessageSource

  1. String[] configs = {“com/baobaotao/i18n/beans.xml”};
  2. ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
  3. MessageSource ms = (MessageSource)ctx.getBean(“myResource”);
  4. Object[] params = {“John”, new GregorianCalendar().getTime()};
  5. for (int i = 0; i < 2; i++) {
  6.     String str1 = ms.getMessage(“greeting.common”,params,Locale.US);
  7.     System.out.println(str1);
  8.     Thread.currentThread().sleep(20000); //①模拟程序应用,在此期间,我们更改资源文件
  9. }

在①处,我们让程序睡眠20秒钟,在这期间,我们将fmt_resource_zh_CN.properties资源文件的greeting.common键值调整为:

引用
—How are you!{0},today is {1}—

我们将看到两次输出的格式化信息分别对应更改前后的内容,也即本地化资源文件的调整被自动生效了:

引用
How are you!John,today is 1/9/07 4:55 PM
—How are you!John,today is 1/9/07 4:55 PM—

容器级的国际化信息资源

在如图5-7所示的MessageSource类图结构中,我们发现ApplicationContext实现了MessageSource的接口。也就是说ApplicationContext的实现类本身也是一个MessageSource对象。

将ApplicationContext和MessageSource整合起来,乍一看挺让人费解的,Spring这样设计的意图究竟是什么呢?原来Spring认为:在一般情况下,国际化信息资源应该是容器级。我们一般不会将MessageSource作为一个Bean注入到其他的Bean中,相反MessageSource作为容器的基础设施向容器中所有的Bean开放。只要我们考察一下国际化信息的实际消费场所就更能理解Spring这一设计的用意了。国际化信息一般在系统输出信息时使用,如Spring MVC的页面标签,控制器Controller等,不同的模块都可能通过这些组件访问国际化信息,因此Spring就将国际化消息作为容器的公共基础设施对所有组件开放。

既然一般情况下我们不会直接通过引用MessageSource Bean使用国际信息,那如何声明容器级的国际化信息呢?我们其实在5.1.1节讲解Spring容器的内部工作机制时已经埋下了伏笔:在介绍容器启动过程时,我们通过代码清单5-1对Spring容器启动时的步骤进行剖析,④处的initMessageSource()方法所执行的工作就是初始化容器中的国际化信息资源:它根据反射机制从BeanDefinitionRegistry中找出名称为“messageSource”且类型为org.springframework.context.MessageSource的Bean,将这个Bean定义的信息资源加载为容器级的国际化信息资源。请看下面的配置:

代码清单5-22  容器级资源的配置

  1. <!–①注册资源Bean,其Bean名称只能为messageSource –>
  2. <bean id=”messageSource”
  3.       class=”org.springframework.context.support.ResourceBundleMessageSource”>
  4.   <property name=”basenames”>
  5.      <list>
  6.        <value>com/baobaotao/i18n/fmt_resource</value>
  7.      </list>
  8.   </property>
  9. </bean>

下面,我们通过ApplicationContext直接访问国际化信息,如代码清单5 23所示:

代码清单5-23  通过ApplicationContext访问国际化信息

  1. String[] configs = {“com/baobaotao/i18n/beans.xml”};
  2. ApplicationContext ctx = new ClassPathXmlApplicationContext(configs);
  3. //①直接通过容器访问国际化信息
  4. Object[] params = {“John”, new GregorianCalendar().getTime()};
  5. String str1 = ctx.getMessage(“greeting.common”,params,Locale.US);
  6. String str2 = ctx.getMessage(“greeting.morning”,params,Locale.CHINA);
  7. System.out.println(str1);
  8. System.out.println(str2);

运行以上代码,输出以下信息:

引用
How are you!John,today is 1/9/07 5:24 PM
早上好!John,现在是下午5:24

假设MessageSource Bean名字没有命名为“messageSource”,以上代码将抛出NoSuchMessageException异常。

引用

学习Web开发不好好学习HTTP报文,将会“打拳不练功,到老一场空”,你花在犯迷糊上的时间比你沉下心来学习HTTP的时间肯定会多很多。

HTTP请求报文解剖

HTTP请求报文由3部分组成(请求行+请求头+请求体):

1a4e7e6a-6d7b-38f1-af8a-043140034c8f

下面是一个实际的请求报文:

412b4451-2738-3ebc-b1f6-a0cc13b9697b

①是请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。不过,当前的大多数浏览器只支持GET和POST,Spring 3.0提供了一个HiddenHttpMethodFilter,允许你通过“_method”的表单参数指定这些特殊的HTTP方法(实际上还是通过POST提交表单)。服务端配置了HiddenHttpMethodFilter后,Spring会根据_method参数指定的值模拟出相应的HTTP方法,这样,就可以使用这些HTTP方法对处理方法进行映射了。

②为请求对应的URL地址,它和报文头的Host属性组成完整的请求URL,③是协议名称及版本号。

④是HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。

⑤是报文体,它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html? param1=value1&param2=value2”的方式传递请求参数。

对照上面的请求报文,我们把它进一步分解,你可以看到一幅更详细的结构图:

cdc4dbbb-f98e-31d5-8270-3c37bf1c54e5

引用
HttpWatch是强大的网页数据分析工具,安装后将集成到Internet Explorer工具栏中。它不用代理服务器或一些复杂的网络监控工具,就能抓取请求及响应的完整信息,包括Cookies、消息头、查询参数、响应报文等,是Web应用开发人员的必备工具。

HTTP请求报文头属性

报文头属性是什么东西呢?我们不妨以一个小故事来说明吧。

引用
快到中午了,张三丰不想去食堂吃饭,于是打电话叫外卖:老板,我要一份[鱼香肉丝],要12:30之前给我送过来哦,我在江湖湖公司研发部,叫张三丰。

这里,你要[鱼香肉丝]相当于HTTP报文体,而“12:30之前送过来”,你叫“张三丰”等信息就相当于HTTP的报文头。它们是一些附属信息,帮忙你和饭店老板顺利完成这次交易。

请求HTTP报文和响应HTTP报文都拥有若干个报文关属性,它们是为协助客户端及服务端交易的一些附属信息。

常见的HTTP请求报文头属性

Accept

请求报文可通过一个“Accept”报文头属性告诉服务端 客户端接受什么类型的响应。

如下报文头相当于告诉服务端,俺客户端能够接受的响应类型仅为纯文本数据啊,你丫别发其它什么图片啊,视频啊过来,那样我会歇菜的~~~:

  1. Accept:text/plain

Accept属性的值可以为一个或多个MIME类型的值,关于MIME类型,大家请参考:http://en.wikipedia.org/wiki/MIME_type

Cookie

客户端的Cookie就是通过这个报文头属性传给服务端的哦!如下所示:

  1. Cookie: $Version=1; Skin=new;jsessionid=5F4771183629C9834F8382E23BE13C4C

服务端是怎么知道客户端的多个请求是隶属于一个Session呢?注意到后台的那个jsessionid=5F4771183629C9834F8382E23BE13C4C木有?原来就是通过HTTP请求报文头的Cookie属性的jsessionid的值关联起来的!(当然也可以通过重写URL的方式将会话ID附带在每个URL的后面哦)。

Referer

表示这个请求是从哪个URL过来的,假如你通过google搜索出一个商家的广告页面,你对这个广告页面感兴趣,鼠标一点发送一个请求报文到商家的网站,这个请求报文的Referer报文头属性值就是http://www.google.com。

引用
唐僧到了西天.
如来问:侬是不是从东土大唐来啊?
唐僧:厉害!你咋知道的!
如来:呵呵,我偷看了你的Referer…

很多貌似神奇的网页监控软件(如著名的 我要啦),只要在你的网页上放上一段JavaScript,就可以帮你监控流量,全国访问客户的分布情况等报表和图表,其原理就是通过这个Referer及其它一些HTTP报文头工作的。

Cache-Control

对缓存进行控制,如一个请求希望响应返回的内容在客户端要被缓存一年,或不希望被缓存就可以通过这个报文头达到目的。

如以下设置,相当于让服务端将对应请求返回的响应内容不要在客户端缓存:

  1. Cache-Control: no-cache

其它请求报文头属性

参见:http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

如何访问请求报文头

由于请求报文头是客户端发过来的,服务端当然只能读取了,以下是HttpServletRequest一些用于读取请求报文头的API:

  1. //获取请求报文中的属性名称
  2. java.util.Enumeration<java.lang.String>   getHeaderNames();
  3. //获取指定名称的报文头属性的值
  4. java.lang.String getHeader(java.lang.String name)

由于一些请求报文头属性“太著名”了,因此HttpServletRequest为它们提供了VIP的API:

  1. //获取报文头中的Cookie(读取Cookie的报文头属性)
  2.  Cookie[]   getCookies() ;
  3. //获取客户端本地化信息(读取 Accept-Language 的报文头属性)
  4. java.util.Locale    getLocale()
  5. //获取请求报文体的长度(读取Content-Length的报文头属性)
  6. int getContentLength();

HttpServletRequest可以通过

  1. HttpSession getSession()

获取请求所关联的HttpSession,其内部的机理是通过读取请求报文头中Cookie属性的JSESSIONID的值,在服务端的一个会话Map中,根据这个JSESSIONID获取对应的HttpSession的对象。(这样,你就不会觉得HttpSession很神秘了吧,你自己也可以做一个类似的会话管理 ::__IHACKLOG_REMOTE_IMAGE_AUTODOWN_BLOCK__::83 )

HTTP响应报文解剖

响应报文结构

HTTP的响应报文也由三部分组成(响应行+响应头+响应体):

0236098f-1a98-3a4f-ba6c-4a44c6ec4ed0

以下是一个实际的HTTP响应报文:

bddb00b6-a3e1-3112-a4f4-4b3cb8687c70

①报文协议及版本;
②状态码及状态描述;
③响应报文头,也是由多个属性组成;
④响应报文体,即我们真正要的“干货”。

响应状态码

和请求报文相比,响应报文多了一个“响应状态码”,它以“清晰明确”的语言告诉客户端本次请求的处理结果。

HTTP的响应状态码由5段组成:

  • 1xx 消息,一般是告诉客户端,请求已经收到了,正在处理,别急…
  • 2xx 处理成功,一般表示:请求收悉、我明白你要的、请求已受理、已经处理完成等信息.
  • 3xx 重定向到其它地方。它让客户端再发起一个请求以完成整个处理。
  • 4xx 处理发生错误,责任在客户端,如客户端的请求一个不存在的资源,客户端未被授权,禁止访问等。
  • 5xx 处理发生错误,责任在服务端,如服务端抛出异常,路由出错,HTTP版本不支持等。

以下是几个常见的状态码:

200 OK

你最希望看到的,即处理成功!

303 See Other

我把你redirect到其它的页面,目标的URL通过响应报文头的Location告诉你。

引用
悟空:师傅给个桃吧,走了一天了::__IHACKLOG_REMOTE_IMAGE_AUTODOWN_BLOCK__::86
唐僧:我哪有桃啊!去王母娘娘那找吧::__IHACKLOG_REMOTE_IMAGE_AUTODOWN_BLOCK__::87

304 Not Modified

告诉客户端,你请求的这个资源至你上次取得后,并没有更改,你直接用你本地的缓存吧,我很忙哦,你能不能少来烦我啊!

404 Not Found

你最不希望看到的,即找不到页面。如你在google上找到一个页面,点击这个链接返回404,表示这个页面已经被网站删除了,google那边的记录只是美好的回忆。

500 Internal Server Error

看到这个错误,你就应该查查服务端的日志了,肯定抛出了一堆异常,别睡了,起来改BUG去吧!

其它的状态码参见:http://en.wikipedia.org/wiki/List_of_HTTP_status_codes

有些响应码,Web应用服务器会自动给生成。你可以通过HttpServletResponse的API设置状态码:

  1. //设置状态码,状态码在HttpServletResponse中通过一系列的常量预定义了,如SC_ACCEPTED,SC_OK
  2. void    setStatus(int sc)

常见的HTTP响应报文头属性

Cache-Control

响应输出到客户端后,服务端通过该报文头属告诉客户端如何控制响应内容的缓存。

下面,的设置让客户端对响应内容缓存3600秒,也即在3600秒内,如果客户再次访问该资源,直接从客户端的缓存中返回内容给客户,不要再从服务端获取(当然,这个功能是靠客户端实现的,服务端只是通过这个属性提示客户端“应该这么做”,做不做,还是决定于客户端,如果是自己宣称支持HTTP的客户端,则就应该这样实现)。

  1. Cache-Control: max-age=3600

ETag

一个代表响应服务端资源(如页面)版本的报文头属性,如果某个服务端资源发生变化了,这个ETag就会相应发生变化。它是Cache-Control的有益补充,可以让客户端“更智能”地处理什么时候要从服务端取资源,什么时候可以直接从缓存中返回响应。

关于ETag的说明,你可以参见:http://en.wikipedia.org/wiki/HTTP_ETag
Spring 3.0还专门为此提供了一个org.springframework.web.filter.ShallowEtagHeaderFilter(实现原理很简单,对JSP输出的内容MD5,这样内容有变化ETag就相应变化了),用于生成响应的ETag,因为这东东确实可以帮助减少请求和响应的交互。

下面是一个ETag:

  1. ETag: ”737060cd8c284d8af7ad3082f209582d”

Location

我们在JSP中让页面Redirect到一个某个A页面中,其实是让客户端再发一个请求到A页面,这个需要Redirect到的A页面的URL,其实就是通过响应报文头的Location属性告知客户端的,如下的报文头属性,将使客户端redirect到iteye的首页中:

  1. Location: http://www.iteye.com

Set-Cookie

服务端可以设置客户端的Cookie,其原理就是通过这个响应报文头属性实现的:

  1. Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1

其它HTTP响应报文头属性

更多其它的HTTP响应头报文,参见:http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

如何写HTTP请求报文头

在服务端可以通过HttpServletResponse的API写响应报文头的属性:

  1. //添加一个响应报文头属性
  2. void    setHeader(String name, String value)

象Cookie,Location这些响应都是有福之人,HttpServletResponse为它们都提供了VIP版的API:

  1. //添加Cookie报文头属性
  2. void addCookie(Cookie cookie)
  3. //不但会设置Location的响应报文头,还会生成303的状态码呢,两者天仙配呢
  4. void    sendRedirect(String location)

发表评论

电子邮件地址不会被公开。 必填项已用 * 标注

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>