hk2000c技术专栏

技术源于哲学,哲学来源于生活 关心生活,关注健康,关心他人

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  111 随笔 :: 1 文章 :: 28 评论 :: 0 Trackbacks

2007年12月31日 #

posted @ 2014-04-21 21:33 hk2000c 阅读(177) | 评论 (0)编辑 收藏

许多年以来,对于软件项目,企业软件开发的主流实践一直都倾向于在单一的通用编程语言上进行标准化,从而使得Java和C#成为今天编程语言的主流选择。随着越来越多的目光开始投向DSL,也许我们的前脚已经踏在了一道新的门槛之上,向前望去,我们会发现在软件项目中采用多种语言已经成为一个标准,但80年代和90年代初出现的问题不会重现。

Martin Fowler提出,也许我们正在迈进这样的一个新时期

[……]在这个时期内,我们将见证多种语言在同一个项目上的应用,人们就像现在选择框架一样,根据功能来选择相应的语言。

Fowler称:“像Hibernate、Struts和ADO这样的大型框架,给人们在学习上带来的挑战,绝不亚于学习一门语言,即便你在单一一门宿主语言上使用这些框架编程也是如此。”此外,在它们的宿主语言中表述需求的难度可能会相当大,并可能引出笨拙难用的配置文件,“这些配置文件实际上就是使用XML写的外部领域特定语言”。

在语言中嵌入DSL,而不是使用类库,可能会是一个更为合适的解决方案。Martin给出了这样的一个分析结论:“API就好比是在声明一个词汇表,而DSL则为其增加了相应的语法,使得人们能够写出条理清晰的句子。”因此,使用DSL而不是框架会使代码丰富表现力,为人们带来“更良好的抽象处理方式”,并使“阅读我们写出的代码及对我们意图的展示变得更加容易”。

Piers Cawley称,DSL的主要特性并非其可读性,而是“它们对去相应领域的高度专注”使得它们能够更加明确地表义。Cawley为了阐述他的观点举了一个例子,说明DSL不仅仅能让我们“写出读起来像领域专家说出来的话一样的程序”,也可以很技术化,用来代表一个使用它们的语法进行操控的框架。

Neal Ford也相信,被他称为多语言编程(Polyglot Programming)的势头正在兴起。在软件开发的这个新纪元中,日益明显的主要特征就是嵌入更多的语言,使人们能够“为所做的菜选择一把恰到好处的刀,并且恰如其分地使用它”。他举了一个例子,展示在Java编程语言中并行类库的使用难度,并将其与Haskell作比。Haskell是一门函数式语言,“消除了变量所带来的副作用”,并使“编写线程安全的代码”变得更容易。Ford强调说,Java和.NET平台都存在Haskell语言的实现(Jaskell和Haskell.net)。

不再使用单一语言进行开发所带来的风险之一可能让80年代末90年代初所出现的问题又再次重现,当时语言就是完全独立的平台,既不能互操作也不能放在一起良好地使用。Martin Fowler指出,现在的情况有这样的一个重要区别:

在80年代末期,人们很难让各个语言之间紧密地互操作。这些年来,人们花了很大精力创建出可以让不同语言紧密共存的环境。脚本语言在传统上与C语言有着很密切的关系。在JVM和CLR平台上也有人为互操作花费了大量精力。另外人们也在类库上投入了很多人力物力,为的是让语言忽视类库的存在。

最终,要学习并使用多种语言,对于业界乃至开发人员都可能会变成一项重要资产。《Pragmatic Programmers》这本书里面就说到,由于这样做会对人们对编程的思考方式产生影响,因此这样能帮助人们发现解决问题的新途径。

您是怎样认为的呢?在下去的五年中,我们会开始混合使用语言,并像用类库一样频繁地使用DSL吗?

posted @ 2011-11-10 05:56 hk2000c 阅读(302) | 评论 (0)编辑 收藏

     摘要: java 装载 groovy 方法  阅读全文
posted @ 2011-11-10 05:45 hk2000c 阅读(730) | 评论 (0)编辑 收藏

     摘要: java 和groovy 集成

  阅读全文
posted @ 2011-11-10 05:37 hk2000c 阅读(1053) | 评论 (0)编辑 收藏

AspectJ

 

      AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件


一、AspectJ概述

图1 :FigureEditor例子的UML

      AspectJ(也就是AOP)的动机是发现那些使用传统的编程方法无法很好处理的问题。考虑一个要在某些应用中实施安全策略的问题。安全性是贯穿于系统所有模块间的问题,每个模块都需要应用安全机制才能保证整个系统的安全性,很明显这里的安全策略的实施问题就是一个横切关注点,使用传统的编程解决此问题非常的困难而且容易产生差错,这就正是AOP发挥作用的时候了。

      传统的面向对象编程中,每个单元就是一个,而类似于安全性这方面的问题,它们通常不能集中在一个类中处理因为它们横跨多个类,这就导致了代码无法重用,可维护性差而且产生了大量代码冗余,这是我们不愿意看到的。

      面向方面编程的出现正好给处于黑暗中的我们带来了光明,它针对于这些横切关注点进行处理,就好象面向对象编程处理一般的关注点一样。而作为AOP的具体实现之一的AspectJ,它向Java中加入了连接点(Join Point)这个新概念,其实它也只是现存的一个Java概念的名称而已。它向Java语言中加入少许新结构:切点(pointcut)、通知(Advice)、类型间声明(Inter-type declaration)和方面(Aspect)。切点和通知动态地影响程序流程,类型间声明则是静态的影响程序的类等级结构,而方面则是对所有这些新结构的封装。

      一个连接点是程序流中指定的一点。切点收集特定的连接点集合和在这些点中的值。一个通知是当一个连接点到达时执行的代码,这些都是AspectJ的动态部分。其实连接点就好比是程序中的一条一条的语句,而切点就是特定一条语句处设置的一个断点,它收集了断点处程序栈的信息,而通知就是在这个断点前后想要加入的程序代码。AspectJ中也有许多不同种类的类型间声明,这就允许程序员修改程序的静态结构、名称、类的成员以及类之间的关系。AspectJ中的方面是横切关注点的模块单元。它们的行为与Java语言中的类很象,但是方面还封装了切点、通知以及类型间声明。

动态连接点模型

      任何面向方面编程的关键元素就是连接点模型。AspectJ提供了许多种类的连接点集合,但是本篇只介绍它们中的一个:方法调用连接点集(method call join points)。一个方法调用连接点捕捉对象的方法调用。每一个运行时方法调用都是一个不同的连接点,许多其他的连接点集合可能在方法调用连接点执行时运,包括方法执行时的所有连接点集合以及在方法中其他方法的调用。我们说这些连接点集合在原来调用的连接点的动态环境中执行。

 

切点

       在AspectJ中,切点捕捉程序流中特定的连接点集合。例如,切点

              call(void Point.setX(int))

捕捉每一个签名为void Point.setX(int)的方法调用的连接点,也就是说,调用Point对象的有一个整型参数的void setX方法。切点能与其他切点通过或(||)、与(&&)以及非(!)操作符联合。例如 call(void Point.setX(int)) || call(void Point.setY(int)) 捕捉setX或setY调用的连接点。切点还可以捕捉不同类型的连接点集合,换句话说,它们能横切类型。例如

       call(void FigureElement.setXY(int,int)) || call(void Point.setX(int))

       || call(void Point.setY(int) || call(void Line.setP1(Point))

       || call(void Line.setP2(Point));

捕捉上述五个方法调用的任意一个的连接点集合。它在本文的例子中捕捉当FigureElement移动时的所有连接点集合。AspectJ使程序员可以命名一个切点集合,以便通知的使用。例如可以为上面的那些切点命名

pointcut move():

call(void FigureElement.setXY(int,int)) || call(void Point.setX(int))

|| call(void Point.setY(int)) || call(void Line.setP1(Point)) || call(void Line.setP2(Point));

无论什么时候,程序员都可以使用move()代替捕捉这些复杂的切点。

       前面所说的切点都是基于显示的方法签名,它们称为基于名字(name-based)横切。AspectJ还提供了另一种横切,称为基于属性(property-based)的横切。它们可以使用通配符描述方法签名,例如 call(void Figure.make*(..)) 捕捉Figure对象中以make开头的参数列表任意的方法调用的连接点。而 call(public & Figure.*(..)) 则捕捉Figure对象中的任何公共方法调用的连接点。但是通配符不是AspectJ支持的唯一属性,AspectJ中还有许多其他的属性可供程序员使用。例如cflow,它根据连接点集合是否在其他连接点集合的动态环境中发生标识连接点集合。例如 cflow(move()) 捕捉被move()捕捉到的连接点集合的动态环境中发生的连接点。

 

通知

       虽然切点用来捕捉连接点集合,但是它们没有做任何事。要真正实现横切行为,我们需要使用通知机制。通知包含了切点和要在每个连连接点处执行的代码段。AspectJ有几种通知。

·前通知(Before Advice)      当到达一个连接点但是在程序进程运行之前执行。例如,前通知在方法实际调用之前运行,刚刚在方法的参数被分析之后。

       Before() : move(){ System.out.println(“物体将移动了”);}

·后通知(After Advice) 当特定连接点处的程序进程执行之后运行。例如,一个方法调用的后通知在方法体运行之后,刚好在控制返回调用者之前执行。因为Java程序有两种退出连接点的形式,正常的和抛出异常。相对的就有三种后通知:返回后通知(after returning)、抛出异常后通知(after throwing)和清楚的后通知(after),所谓清楚后通知就是指无论是正常还是异常都执行的后通知,就像Java中的finally语句。

       After() returning : move(){    System.out.println(“物体刚刚成功的移动了”);}

·在周围通知(Around Advice)    在连接点到达后,显示的控制程序进程是否执行(暂不讨论)

 

暴露切点环境

       切点不仅仅捕捉连接点,它还能暴露连接点处的部分执行环境。切点中暴露的值可以在通知体中声明以后使用。通知声明有一个参数列表(和方法相同)用来描述它所使用的环境的名称。例如后通知

       after(FigureElement fe,int x,int y) returning : somePointcuts {  someCodes  }

使用了三个暴露的环境,一个名为fe的FigureElement对象,两个整型变量x,y。通知体可以像使用方法的参数那样使用这些变量,例如

       after(FigureElement fe,int x,int y) returning : somePointcuts {

              System.out.println(fe+”移动到(”+x+”,”+y+”)”);

}

通知的切点发布了通知参数的值,三个原生切点this、target和args被用来发布这些值/所以上述例子的完整代码为

       after(FigureElement fe,int x,int y) returning : call(void FigureElement.setXY(int,int)

&& target(fe) && args(x,y) {

              System.out.println(fe+”移动到(”+x+”,”+y+”)”);

}

目标对象是FigureElement所以fe是after的第一个参数,调用的方法包含两个整型参数所以x和y为after的第二和第三个参数。所以通知打印出方法setXY调用返回后对象移动到的点x和y。当然还可以使用命名切点完成同样的工作,例如

       pointcut setXY(FigureElement fe,int x,int y):call(void FigureElement.setXY(int,int)

              && target(fe) && args(x,y);

       after(FigureElement fe,int x,int y) returning : setXY(fe,x,y){

       System.out.println(fe+”移动到(”+x+”,”+y+”)”);

       }

 

类型间声明

       AspectJ的类型间声明指的是那些跨越类和它们的等级结构的声明。这些可能是横跨多个类的成员声明或者是类之间继承关系的改变。不像通知是动态地操作,类型间声明编译时的静态操作。考虑一下,Java语言中如何向一个一些的类中加入新方法,这需要实现一个特定接口,所有类都必须在各自内部实现接口声明的方法,而使用AspectJ则可以将这些工作利用类型间声明放在一个方面中。这个方面声明方法和字段,然后将它们与需要的类联系。

假设我们想有一个Sreen对象观察Point对象的变化,当Point是一个存在的类。我们可以通过书写一个方面,由这个方面声明Point对象有一个实例字段observers,用来保存所有观察Point对象的Screen对象的引用,从而实现这个功能。

       Aspect PointObserving{

              Private    Collection Point.observers=new ArrayList();

……

}

observers字段是私有字段,只有PointObserving能使用。因此,要在aspect中加入方法管理observers聚集。

       Aspect PointObserving{

              Private Collection Point.observers=new ArrayList();

              Public static void addObserver(Point p,Screen s){

                     p.observers.add(s);

              }

              public static void removeObserver(Point p,Screen s){

                     p.observers.remove(s);

              }

              ……

}

然后我们可以定义一个切点stateChanges决定我们想要观察什么并且提供一个after通知定义当观察到变化时我们想要做什么。

       Aspect PointObserving{

              Private Collection Point.observers=new ArrayList();

              Public static void addObserver(Point p,Screen s){

                     p.observers.add(s);

              }

              public static void removeObserver(Point p,Screen s){

                     p.observers.remove(s);

              }

              pointcut stateChanges(Point p) : target(p) && call(void Point.set*(int));

              after(Point p) : stateChanges(p){

                     Iterator it=p.observers.iterator();

                     While(it.hasNext()){

                            UpdateObserver(p,(Screen)it.next()));

                     }

              }

              private static void updateObserver(Point p,Screen s){

                     s.display(p);

              }

}

注意无论是Sreen还是Point的代码都没有被修改,所有的新功能的加入都在方面中实现了,很酷吧!

 

方面

       方面以横切模块单元的形式包装了所有的切点、通知和类型间声明。这非常像Java语言的类。实际上,方面也可以定义自己的方法,字段和初始化方法。像类一样一个方面也可以用abstrace关键字声明为抽象方面,可以被子方面继承。在AspectJ中方面的设计实际上使用了单例模式,缺省情况下,它不能使用new构造,但是可以使用一个方法实例化例如方法aspectOf()可以获得方面的实例。所以在方面的通知中可以使用非静态的成员字段。

例如

       aspect Tracing {

              OutputStream trace=System.out;

              After() : move(){    trace.println(“物体成功移动”); }


二、AspectJ应用范围

       如前所述,AspectJ可以用于应用开发的不同阶段。下面讨论不同阶段的AspectJ的具体应用情况。

开发型方面(Development Aspects)

       开发方面可以很容易的从真正的产品中删除。而产品方面则被可用于开发过程和生产过程,但是仅仅影响某几个类。

       这一部分将通过几个例子说明方面在Java应用的开发阶段是如何使用的。这些方面包括调试、测试和性能检测等工作。方面定义的行为范围包括简单的代码跟踪、测试应用的内在联系等等。使用AspectJ不仅使得模块化这些功能变为可能,同时也使得根据需要打开和关闭这些功能变成可能。

 

代码跟踪(Tracing)
       首先让我们看看如何增加一个程序内部工作的可视性。我们定义一个简单的方面用于代码跟踪并且在每个方法调用时输出一些信息。在前一篇的图形编辑例子中,这样的方面可能仅仅简单的跟踪什么时候画一个点。

aspect SimpleTracing {
    pointcut tracedCall():
        call(void FigureElement.draw(GraphicsContext));
 
    before(): tracedCall() {
        System.out.println("Entering: " + thisJoinPoint);
    }
}
代码利用了thisJoinPoint变量。在所有的通知体内,这个变量将与描述当前连接点的对象绑定。所以上述代码在每次一个FigureElement对象接受到draw方法时输出如下信息:

Entering: call(void FigureElement.draw(GraphicsContext))

通常我们在调式程序时,会在特定的地方放置几条输出语句,而当调试结束时还需要找到这些代码段将它们删除,这样做不但使我们的代码很难看而且很费时间。而使用AspectJ我们可以克服以上的两个问题,我们可以通过定义切点捕捉任何想要观察的代码段,利用通知可以在方面内部书写输出语句,而不需要修改源代码,当不在需要跟踪语句的时候还可以很轻松的将方面从应用中删除并重新编译代码即可。

 

前提条件和后续条件(Pre-and Post-Conditions)
       许多的程序员使用按契约编程(Design by Contract)的形式。这种形式的编程需要显式的前提条件测试以保证方法调用是否合适,还需要显式的后续条件测试保证方法是否工作正常。AspectJ使得可以模块化地实现这两种条件测试。例如下面的代码

aspect PointBoundsChecking {

    pointcut setX(int x):

        (call(void FigureElement.setXY(int, int)) && args(x, *))

        || (call(void Point.setX(int)) && args(x));

 

    pointcut setY(int y):

        (call(void FigureElement.setXY(int, int)) && args(*, y))

        || (call(void Point.setY(int)) && args(y));

 

    before(int x): setX(x) {

        if ( x < MIN_X || x > MAX_X )

            throw new IllegalArgumentException("x is out of bounds.");

    }

 

    before(int y): setY(y) {

        if ( y < MIN_Y || y > MAX_Y )

            throw new IllegalArgumentException("y is out of bounds.");

    }

}

它实现了边界检测功能。当FigureElement对象移动时,如果x或y的值超过了定义的边界,程序将会抛出IllegalArgumentException异常。

 

合同实施(Contract Enforcement)
       基于属性的横切机制在定义更加复杂的合同实施上非常有用。一个十分强大的功能是它可以强制特定的方法调用只出现在对应的程序中,而在其他程序中不出现。例如,下面的方面实施了一个限制,使得只有在知名的工厂方法中才能向注册并添加FigureElement对象。实施这个限制的目的是为了确保没有任何一个FigureElement对象被注册多次。

static aspect RegistrationProtection {

    pointcut register(): call(void Registry.register(FigureElement));

    pointcut canRegister(): withincode(static * FigureElement.make*(..));

 

    before(): register() && !canRegister() {

        throw new IllegalAccessException("Illegal call " + thisJoinPoint);

    }

}

这个方面使用了withincode初始切点,它表示在FigureElement对象的工厂方法(以make开始的方法)体内出现的所有连接点。在before通知中声明一个异常,该通知用于捕捉任何不在工厂方法代码内部产生的register方法的调用。该通知在特定连接点处抛出一个运行时异常,但是AspectJ能做地更好。使用declare error的形式,我们可以声明一个编译时的错误。

static aspect RegistrationProtection {

    pointcut register(): call(void Registry.register(FigureElement));

    pointcut canRegister(): withincode(static * FigureElement.make*(..));

 

    declare error: register() && !canRegister(): "Illegal call"

}

当使用这个方面后,如果代码中存在定义的这些非法调用我们将无法通过编译。这种情况只出现在我们只需要静态信息的时候,如果我们需要动态信息,像上面提到的前提条件实施时,就可以利用在通知中抛出带参数的异常来实现。

 

配置管理(Configuration Management)
       AspectJ的配置管理可以使用类似于make-file等技术进行处理。程序员可以简单的包括他们想要的方面进行编译。不想要任何方面出现在产品阶段的开发者也可以通过配置他们的make-file使用传统的Java编译器编译整个应用。

 

产品型方面(Production Aspects)

       这一部分的方面例子将描述方面用于生产阶段的应用。产品方面将向应用中加入功能而不仅仅为程序的内部工作增加可视性。

改变监视(Change Monitoring)
       在第一个例子,方面的角色是用于维护一位数据标志,由它说明对象从最后一次显示刷新开始是否移动过。在方面中实现这样的功能是十分直接的,testAndClear方法被显示代码调用以便找到一个图形元素是否在最近移动过。这个方法返回标志的状态并将它设置为假。切点move捕捉所有能够是图形移动的方法调用。After通知截获move切点并设置标志位。

aspect MoveTracking {

private static boolean dirty = false;

 

    public static boolean testAndClear() {

        boolean result = dirty;

        dirty = false;

        return result;

    }

 

    pointcut move():

        call(void FigureElement.setXY(int, int)) ||

        call(void Line.setP1(Point)) ||

        call(void Line.setP2(Point)) ||

        call(void Point.setX(int)) ||

        call(void Point.setY(int));

 

    after() returning: move() {

        dirty = true;

    }

}
 

这个简单例子同样说明了在产品代码中使用AspectJ的一些好处。考虑使用普通的Java代码实现这个功能:将有可能需要包含标志位,testAndClear以及setFlag方法的辅助类。这些方法需要每个移动的图形元素包含一个对setFlag方法的调用。这些方法的调用就是这个例子中的横切关注点。

·显示的捕捉了横切关注点的结构

·功能容易拔插

·实现更加稳定

 

传递上下文(Context Passing)
横切结构的上下文传递在Java程序中是十分复杂的一部分。考虑实现一个功能,它允许客户设置所创建的图形对象的颜色。这个需求需要从客户端传入一个颜色或颜色工厂。而要在大量的方法中加入一个参数,目的仅仅是为传递上下文信息这种不方便的情况是所有的程序员都十分熟悉的。

使用AspectJ,这种上下文的传递可以使用模块化的方式实现。下面代码中的after通知仅当一个图形对象的工厂方法在客户ColorControllingClient的某个方法控制流程中调用时才运行。

aspect ColorControl {

    pointcut CCClientCflow(ColorControllingClient client):

        cflow(call(* * (..)) && target(client));

 

    pointcut make(): call(FigureElement Figure.make*(..));

 

    after (ColorControllingClient c) returning (FigureElement fe):

            make() && CCClientCflow(c) {

        fe.setColor(c.colorFor(fe));

    }

}

这个方面仅仅影响一小部分的方法,但是注意该功能的非AOP实现可能 需要编辑更多的方法。

 

提供一致的行为(Providing Consistent Behavior)
接下来的例子说明了基于属性的方面如何在很多操作中提供一致的处理功能。这个方面确保包com.bigboxco的所有公共方法记录由它们抛出的任何错误。PublicMethodCall切点捕捉包中的公共方法调用, after通知在任何一个这种调用抛出错误后运行并且记录下这个错误。

aspect PublicErrorLogging {

    Log log = new Log();

 

    pointcut publicMethodCall():

        call(public * com.bigboxco.*.*(..));

 

    after() throwing (Error e): publicMethodCall() {

        log.write(e);

    }

}

在一些情况中,这个方面可以记录一个异常两次。这在com.bigboxco包内部的代码自己调用本包中的公共方法时发生。为解决这个问题,我们可以使用cflow初始切点将这些内部调用排除:

after() throwing (Error e) : publicMethodCall() && !cflow(publicMethodCall()) {

    log.write(e);

}

 

结论

       AspectJ是对Java语言的简单而且实际的面向方面的扩展。仅通过加入几个新结构,AspectJ提供了对模块化实现各种横切关注点的有力支持。向以有的Java开发项目中加入AspectJ是一个直接而且渐增的任务。一条路径就是通过从使用开发方面开始再到产品方面当拥有了AspectJ的经验后就使用开发可重用方面。当然可以选取其他的开发路径。例如,一些开发者将从使用产品方面马上得到好处,另外的人员可能马上编写可重用的方面。

       AspectJ可以使用基于名字和基于属性这两种横切点。使用基于名字横切点的方面仅影响少数几个类,虽然它们是小范围的,但是比起普通的Java实现来说它们能够减少大量的复杂度。使用基于属性横切点的方面可以有小范围或着大范围。使用AspectJ导致了横切关注点的干净、模块化的实现。当编写AspectJ方面时,横切关注点的结构变得十分明显和易懂。方面也是高度模块化的,使得开发可拔插的横切功能变成现实。

       AspectJ提供了比这两部分简短介绍更多的功能。本系列的下一章内容,The AspectJ Language,将介绍 AspectJ语言的更多细节和特征。系列的第三章,Examples将通过一些完整的例子说明如何使用AspectJ。建议大家在仔细阅读了接下来的两章后再决定是否在项目中加入AspectJ。


三、AspectJ的高级特性

(一)、The reflection API

说到高级特性,首先要说的就是AspectJ提供的一套reflection API,主要包括JoinPoint、JoinPoint.StaticPart和Signature三个主要的接口。你可以从aspectj.jar中的javadoc来了解它们的详细情况。那它们能提供什么功能呢?其实从字面上就能大致明白:通过这三个接口能访问到Join Points的信息。譬如,调用thisJoinPoint.getArgs()就可以得到方法的参数列表。

(二)、Aspect precedence

在AspectJ中,pointcut和advice都会包含在一个aspect中。在应用系统中,对同一个join point会有多种advice(logging,caching等),这就会引出一个问题:如果系统中有很多的aspect,而这些aspect很有可能会捕获同样的join points,那这些aspect的执行顺序是如何安排的呢?

AspectJ早已为我们考虑到了这个问题,它提供了一种设置aspect precedence的方法。对三种不同的advice来说:

1、before advice是先执行higher-precedence,后执行lower-precedence;

2、around advice是higher-precedence包含lower-precedence,当higher-precedence around advice没有调用proceed()方法时,lower-precedence不会被执行;

3、after advice与before advice正好相反,先执行执行lower-precedence,然后执行higher-precedence。

那应该如何来声明aspect precedence?非常简单,只要在aspect中使用如下的语法即可:

declare precedence : TypePattern1, TypePattern2, ..;

从左往右,排在前面的是higher-precedence advice,后面的是lower-precedence。

(三)、Aspect association

在Java中,为了节省对象每次构建的耗费,增加效率,很多人会考虑使用Singleton模式,让jvm中只有一个实例存在。AspectJ当然为我们考虑到这个问题,Aspect association实际上就是aspect与advised join point object的一种关联关系,这很类似于OO中association,譬如1:1,1:m等。Aspect association能让我们能更好地控制aspect的状态信息。

在AspectJ中可以把Aspect association大致分为三类:

1、Per virtual machine (default)

一个jvm中只有一个aspect instance,AspectJ默认association。

2、Per object

每一个advised join point object都会产生一个aspect instance,不过同一个object instance只会产生一个aspect instance。

3、Per control-flow association

这种association稍微复杂一些,它主要针对程序调用的控制流,譬如:A方法调用B方法,B方法又调用C方法,这就是control-flow。

在aspect中声明这三种association非常简单,它的主要语法如下:

aspect [( )] {
... aspect body
}

Per virtual machine是aspectj的默认association,不需要你额外的声明,正常使用即可。

Per object主要有两种方式:perthis()和pertarget()。perthis()主要用于execution object,pertarget()主要用于target object,两者非常类似。

Per control-flow中也包含两种方式:percflow()和percflowbelow()。这两者也很类似,只是两者的control-flow不太一样而已。

维护aspect的状态信息还有一种方法,就是使用introduce。可以在aspect中introduce member fields,通过fields来保存状态信息。

 

四、AspectJ实例

 

使用方面的Tracing程序

       写一个具有跟踪能力的类是很简单的事情:一组方法,一个控制其开或关的布尔变量,一种可选的输出流,可能还有一些格式化输出能力。这些都是Trace类需要的东西。当然,如果程序需要的话,Trace类也可以实现的十分的复杂。开发这样的程序只是一方面,更重要的是如何在合适的时候调用它。在大型系统开发过程中,跟踪程序往往影响效率,而且在正式版本中去除这些功能十分麻烦,需要修改任何包含跟踪代码的源码。出于这些原因,开发人员常常使用脚本程序以便向源码中添加或删除跟踪代码。

       AspectJ可以更加方便的实现跟踪功能并克服这些缺点。Tracing可以看作是面向整个系统的关注点,因此,Tracing方面可以完全独立在系统之外并且在不影响系统基本功能的情况下嵌入系统。

 

应用实例

整个例子只有四个类。应用是关于Shape的。TwoShape类是Shape类等级的基类。

public abstract class TwoDShape {

    protected double x, y;

    protected TwoDShape(double x, double y) {

        this.x = x; this.y = y;

    }

    public double getX() { return x; }

    public double getY() { return y; }

    public double distance(TwoDShape s) {

        double dx = Math.abs(s.getX() - x);

        double dy = Math.abs(s.getY() - y);

        return Math.sqrt(dx*dx + dy*dy);

    }

    public abstract double perimeter();

    public abstract double area();

    public String toString() {

        return (" @ (" + String.valueOf(x) + ", " + String.valueOf(y) + ") ");

    }

}

TwoShape类有两个子类,Circle和Square  

public class Circle extends TwoDShape {

    protected double r;

    public Circle(double x, double y, double r) {

        super(x, y); this.r = r;

    }

    public Circle(double x, double y) { this(  x,   y, 1.0); }

    public Circle(double r)           { this(0.0, 0.0,   r); }

    public Circle()                   { this(0.0, 0.0, 1.0); }

    public double perimeter() {

        return 2 * Math.PI * r;

    }

    public double area() {

        return Math.PI * r*r;

    }

    public String toString() {

        return ("Circle radius = " + String.valueOf(r) + super.toString());

    }

}

public class Square extends TwoDShape {

    protected double s;    // side

    public Square(double x, double y, double s) {

        super(x, y); this.s = s;

    }

    public Square(double x, double y) { this(  x,   y, 1.0); }

    public Square(double s)           { this(0.0, 0.0,   s); }

    public Square()                   { this(0.0, 0.0, 1.0); }

    public double perimeter() {

        return 4 * s;

    }

    public double area() {

        return s*s;

    }

    public String toString() {

        return ("Square side = " + String.valueOf(s) + super.toString());

    }

}

 

Tracing版本一

首先我们直接实现一个Trace类并不使用方面。公共接口Trace.java

public class Trace {

    public static int TRACELEVEL = 0;

    public static void initStream(PrintStream s) {...}

    public static void traceEntry(String str) {...}

    public static void traceExit(String str) {...}

}

如果我们没有AspectJ,我们需要在所有需要跟踪的方法或构造子中直接调用traceEntry和traceExit方法并且初试化TRACELEVEL和输出流。以上面的例子来说,如果我们要跟踪所有的方法调用(包括构造子)则需要40次的方法调用并且还要时刻注意没有漏掉什么方法,但是使用方面我们可以一致而可靠的完成。TraceMyClasses.java

aspect TraceMyClasses {

    pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square);

    pointcut myConstructor(): myClass() && execution(new(..));

    pointcut myMethod(): myClass() && execution(* *(..));

 

    before (): myConstructor() {

        Trace.traceEntry("" + thisJoinPointStaticPart.getSignature());

    }

    after(): myConstructor() {

        Trace.traceExit("" + thisJoinPointStaticPart.getSignature());

    }

 

    before (): myMethod() {

        Trace.traceEntry("" + thisJoinPointStaticPart.getSignature());

    }

    after(): myMethod() {

        Trace.traceExit("" + thisJoinPointStaticPart.getSignature());

    }

}

这个方面在合适的时候调用了跟踪方法。根据此方面,跟踪方法在Shape等级中每个方法或构造子的入口和出口处调用,输出的是各个方法的签名。因为方法签名是静态信息,我们可以利用thisJoinPointStaticPart对象获得。运行这个方面的main方法可以获得以下输出:

  --> tracing.TwoDShape(double, double)

  <-- tracing.TwoDShape(double, double)

  --> tracing.Circle(double, double, double)

  <-- tracing.Circle(double, double, double)

  --> tracing.TwoDShape(double, double)

  <-- tracing.TwoDShape(double, double)

  --> tracing.Circle(double, double, double)

  <-- tracing.Circle(double, double, double)

  --> tracing.Circle(double)

  <-- tracing.Circle(double)

  --> tracing.TwoDShape(double, double)

  <-- tracing.TwoDShape(double, double)

  --> tracing.Square(double, double, double)

  <-- tracing.Square(double, double, double)

  --> tracing.Square(double, double)

  <-- tracing.Square(double, double)

  --> double tracing.Circle.perimeter()

  <-- double tracing.Circle.perimeter()

c1.perimeter() = 12.566370614359172

  --> double tracing.Circle.area()

  <-- double tracing.Circle.area()

c1.area() = 12.566370614359172

  --> double tracing.Square.perimeter()

  <-- double tracing.Square.perimeter()

s1.perimeter() = 4.0

  --> double tracing.Square.area()

  <-- double tracing.Square.area()

s1.area() = 1.0

  --> double tracing.TwoDShape.distance(TwoDShape)

    --> double tracing.TwoDShape.getX()

    <-- double tracing.TwoDShape.getX()

    --> double tracing.TwoDShape.getY()

    <-- double tracing.TwoDShape.getY()

  <-- double tracing.TwoDShape.distance(TwoDShape)

c2.distance(c1) = 4.242640687119285

  --> double tracing.TwoDShape.distance(TwoDShape)

    --> double tracing.TwoDShape.getX()

    <-- double tracing.TwoDShape.getX()

    --> double tracing.TwoDShape.getY()

    <-- double tracing.TwoDShape.getY()

  <-- double tracing.TwoDShape.distance(TwoDShape)

s1.distance(c1) = 2.23606797749979

  --> String tracing.Square.toString()

    --> String tracing.TwoDShape.toString()

    <-- String tracing.TwoDShape.toString()

  <-- String tracing.Square.toString()

s1.toString(): Square side = 1.0 @ (1.0, 2.0)

 

Tracing版本二

       版本二实现了可重用的tracing方面,使其不仅仅用于Shape的例子。首先定义如下的抽象方面Trace.java

abstract aspect Trace {

 

    public static int TRACELEVEL = 2;

    public static void initStream(PrintStream s) {...}

    protected static void traceEntry(String str) {...}

    protected static void traceExit(String str) {...}

abstract pointcut myClass();

 

}

为了使用它,我们需要定义我们自己的子类。

public aspect TraceMyClasses extends Trace {

    pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square);

 

    public static void main(String[] args) {

        Trace.TRACELEVEL = 2;

        Trace.initStream(System.err);

        ExampleMain.main(args);

    }

}

注意我们仅仅在类中声明了一个切点,它是超类中声明的抽象切点的具体实现。版本二的Trace类的完整实现如下

abstract aspect Trace {

 

    // implementation part

 

    public static int TRACELEVEL = 2;

    protected static PrintStream stream = System.err;

    protected static int callDepth = 0;

 

    public static void initStream(PrintStream s) {

        stream = s;

    }

    protected static void traceEntry(String str) {

        if (TRACELEVEL == 0) return;

        if (TRACELEVEL == 2) callDepth++;

        printEntering(str);

    }

    protected static void traceExit(String str) {

        if (TRACELEVEL == 0) return;

        printExiting(str);

        if (TRACELEVEL == 2) callDepth--;

    }

    private static void printEntering(String str) {

        printIndent();

        stream.println("--> " + str);

    }

    private static void printExiting(String str) {

        printIndent();

        stream.println("<-- " + str);

    }

    private static void printIndent() {

        for (int i = 0; i < callDepth; i++)

            stream.print("  ");

    }

 

    // protocol part

 

    abstract pointcut myClass();

 

    pointcut myConstructor(): myClass() && execution(new(..));

    pointcut myMethod(): myClass() && execution(* *(..));

 

    before(): myConstructor() {

        traceEntry("" + thisJoinPointStaticPart.getSignature());

    }

    after(): myConstructor() {

        traceExit("" + thisJoinPointStaticPart.getSignature());

    }

 

    before(): myMethod() {

        traceEntry("" + thisJoinPointStaticPart.getSignature());

    }

    after(): myMethod() {

        traceExit("" + thisJoinPointStaticPart.getSignature());

    }

}

它与版本一的不同包括几个部分。首先在版本一中Trace用单独的类来实现而方面是针对特定应用实现的,而版本二则将Trace所需的方法和切点定义融合在一个抽象方面中。这样做的结果是traceEntry和traceExit方法不需要看作是公共方法,它们将由方面内部的通知调用,客户完全不需要知道它们的存在。这个方面的一个关键点是使用了抽象切点,它其实与抽象方法类似,它并不提供具体实现而是由子方面实现它。

 

Tracing版本三

       在前一版本中,我们将traceEntry和traceExit方法隐藏在方面内部,这样做的好处是我们可以方便的更改接口而不影响余下的代码。

       重新考虑不使用AspectJ的程序。假设,一段时间以后,tracing的需求变了,我们需要在输出中加入方法所属对象的信息。至少有两种方法实现,一是保持traceEntry和traceExit方法不变,那么调用者有责任处理显示对象的逻辑,代码可能如下

       Trace.traceEntry("Square.distance in " + toString());

另一种方法是增强方法的功能,添加一个参数表示对象,例如

  public static void traceEntry(String str, Object obj);

  public static void traceExit(String str, Object obj);

然而客户仍然有责任传递正确的对象,调用代码如下

       Trace.traceEntry("Square.distance", this);

这两种方法都需要动态改变其余代码,每个对traceEntry和traceExit方法的调用都需要改变。

       这里体现了方面实现的另一个好处,在版本二的实现中,我们只需要改变Trace方面内部的一小部分代码,下面是版本三的Trace方面实现

abstract aspect Trace {

 

    public static int TRACELEVEL = 0;

    protected static PrintStream stream = null;

    protected static int callDepth = 0;

 

    public static void initStream(PrintStream s) {

        stream = s;

    }

 

    protected static void traceEntry(String str, Object o) {

        if (TRACELEVEL == 0) return;

        if (TRACELEVEL == 2) callDepth++;

        printEntering(str + ": " + o.toString());

    }

 

    protected static void traceExit(String str, Object o) {

        if (TRACELEVEL == 0) return;

        printExiting(str + ": " + o.toString());

        if (TRACELEVEL == 2) callDepth--;

    }

 

    private static void printEntering(String str) {

        printIndent();

        stream.println("Entering " + str);

    }

 

    private static void printExiting(String str) {

        printIndent();

        stream.println("Exiting " + str);

    }

 

    private static void printIndent() {

        for (int i = 0; i < callDepth; i++)

            stream.print("  ");

    }

 

    abstract pointcut myClass(Object obj);

 

    pointcut myConstructor(Object obj): myClass(obj) && execution(new(..));

    pointcut myMethod(Object obj): myClass(obj) &&

        execution(* *(..)) && !execution(String toString());

 

    before(Object obj): myConstructor(obj) {

        traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);

    }

    after(Object obj): myConstructor(obj) {

        traceExit("" + thisJoinPointStaticPart.getSignature(), obj);

    }

 

    before(Object obj): myMethod(obj) {

        traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);

    }

    after(Object obj): myMethod(obj) {

        traceExit("" + thisJoinPointStaticPart.getSignature(), obj);

    }

}

在此我们必须在methods切点排除toString方法的执行。问题是toString方法在通知内部调用,因此如果我们跟踪它,我们将陷入无限循环中。这一点不明显,所以必须在写通知时格外注意。如果通知回调对象,通常都回存在循环的可能性。

       事实上,简单的排除连接点的执行并不够,如果在这之中调用了其他跟踪方法,那么就必须提供以下限制

&& !cflow(execution(String toString()))

排除toString方法的执行以及在这之下的所有连接点。

       总之,为了实现需求的改变我们必须在Trace方面中做一些改变,包括切点说明。但是实现的改变只局限于Trace方面内部,而如果没有方面,则需要更改每个应用类的实现。
(来源:http://befresh.blogbus.com/logs/2004/08/339330.html;
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=15565)

 

 

posted @ 2008-10-25 00:08 hk2000c 阅读(923) | 评论 (2)编辑 收藏

.概述

1.1 JMS与ActiveMQ特性  

   JMS始终在JavaEE五花八门的协议里,WebService满天飞的时候占一位置,是因为:

  • 它可以把不影响用户执行结果又比较耗时的任务(比如发邮件通知管理员)异步的扔给JMS 服务端去做,而尽快的把屏幕返还给用户。
  • 服务端能够多线程排队响应高并发的请求,并保证请求不丢失。
  • 可以在Java世界里达到最高的解耦。客户端与服务端无需直连,甚至无需知晓对方是谁、在哪里、有多少人,只要对流过的信息作响应就行了,在企业应用环境复杂时作用明显。

    ActiveMQ的特性:

  • 完全支持JMS1.1和J2EE 1.4规范的 JMS Provider实现,也是Apache Geronimo默认的JMS provider。
  • POJO withdout EJB Container,不需要实现EJB繁琐复杂的Message Bean接口和配置。
  • Spring Base,可以使用Spring的各种特性如IOC、AOP 。
  • Effective,基于Jencks的JCA Container实现 pool connection,control transactions and manage security。 

1.2 SpringSide 的完全POJO的JMS方案   

  SpringSide 2.0在BookStore示例中,演示了用户下订单时,将发通知信到用户邮箱的动作,通过JMS交给JMS服务端异步完成,避免了邮件服务器的堵塞而影响用户的下订。

  全部代码于examples\bookstore\src\java\org\springside\bookstore\components\activemq 目录中。

  一个JMS场景通常需要三者参与:

  • 一个POJO的的Message Producer,负责使用Spring的JMS Template发送消息。
  • 一个Message Converter,负责把Java对象如订单(Order)转化为消息,使得Producer能够直接发送POJO。
  • 一个MDP Message Consumer,负责接收并处理消息。

  SpringSide 2.0采用了ActiveMQ 4.1-incubator 与Spring 2.0 集成,对比SS1.0M3,有三个值得留意的地方,使得代码中几乎不见一丝JMS的侵入代码:

  1. 采用Spring2.0的Schema式简化配置。
  2. 实现Message Converter转化消息与对象,使得Producer能够直接发送POJO而不是JMS Message。
  3. 使用了Spring2.0的DefaultMessageListenerContainer与MessageListenerAdapter,消息接收者不用实现MessageListener 接口。
  4. 同时,Spring 2.0 的DefaultMessageListenerContainer 代替了SS1.0M3中的Jenck(JCA Container),充当MDP Container的角色。

2.引入ActiveMQ的XSD

  ActiveMQ4.1 响应Spring 2.0号召,支持了引入XML Schema namespace的简单配置语法,简化了配置的语句。 

  在ApplicationContext.xml(Spring的配置文件)中引入ActiveMQ的XML Scheam 配置文件),如下:

<beans
  xmlns="http://www.springframework.org/schema/beans"
  xmlns:amq="http://activemq.org/config/1.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://activemq.org/config/1.0 http://people.apache.org/repository/org.apache.activemq/xsds/activemq-core-4.1-incubator-SNAPSHOT.xsd">

由于ActiveMQ4.1 SnapShot的那个XSD有部分错误,因此使用的是自行修改过的XSD。

先在ClassPath根目录放一个修改过的activemq-core-4.1-incubator-SNAPSHOT.xsd。

在ClassPath 下面建立META-INF\spring.schemas 内容如下。这个spring.schemas是spring自定义scheam的配置文件,请注意"http:\://"部分写法

http\://people.apache.org/repository/org.apache.activemq/xsds/activemq-core-4.1-incubator-SNAPSHOT.xsd=/activemq-core-4.1-incubator-SNAPSHOT.xsd

3. 配置方案

3.1 基础零件 

1. 配置ActiveMQ Broker  

   暂时采用在JVM中嵌入这种最简单的模式,  当spring初始化时候,ActiveMQ embedded Broker 就会启动了。

<!--  lets create an embedded ActiveMQ Broker -->
<amq:broker useJmx="false" persistent="false">
  	<amq:transportConnectors>
    		<amq:transportConnector uri="tcp://localhost:0"/>
 	</amq:transportConnectors>
 </amq:broker>

2. 配置(A)ConnectionFactory

  由于前面配置的Broker是JVM embedded 所以URL为:vm://localhost

<!--  ActiveMQ connectionFactory to use  -->
 <amq:connectionFactory id="jmsConnectionFactory" brokerURL="vm://localhost"/>

3 配置(B)Queue

<!--  ActiveMQ destinations to use  -->
 <amq:queue name="destination" physicalName="org.apache.activemq.spring.Test.spring.embedded"/>

4. 配置(C)Converter

   配置Conveter,使得Producer能够直接发送Order对象,而不是JMS的Message对象。

<!--  OrderMessage converter  -->
 <bean id="orderMessageConverter" class="org.springside.bookstore.components.activemq.OrderMessageConverter"/>  

3.2  发送端 

1 配置JmsTemplate

   Spring提供的Template,绑定了(A)ConnectionFactory与(C)Converter。

<!--  Spring JmsTemplate config -->
 <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
  <property name="connectionFactory">
   <!--  lets wrap in a pool to avoid creating a connection per send -->
   <bean class="org.springframework.jms.connection.SingleConnectionFactory">
    <property name="targetConnectionFactory" ref="jmsConnectionFactory"/>
   </bean>
  </property>
  <!-- custom MessageConverter -->
  <property name="messageConverter" ref="orderMessageConverter"/>
 </bean>

2.Producer

   消息发送者,使用JmsTemplate发送消息,绑定了JmsTemplate (含A、C)与(B)Queue。

<!-- POJO which send Message uses  Spring JmsTemplate,绑定JMSTemplate 与Queue -->
 <bean id="orderMessageProducer" class="org.springside.bookstore.components.activemq.OrderMessageProducer">
  <property name="template" ref="jmsTemplate"/>
  <property name="destination" ref="destination"/>
 </bean>

3.3 接收端

  1.接收处理者(MDP)

    使用Spring的MessageListenerAdapter,指定负责处理消息的POJO及其方法名,绑定(C)Converter。

  <!--  Message Driven POJO (MDP),绑定Converter -->
 <bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter">
  <constructor-arg>
   <bean class="org.springside.bookstore.components.activemq.OrderMessageConsumer">
    <property name="mailService" ref="mailService"/>
   </bean>
  </constructor-arg>
  <!--  may be other method -->
  <property name="defaultListenerMethod" value="sendEmail"/>
  <!-- custom MessageConverter define -->
  <property name="messageConverter" ref="orderMessageConverter"/>
 </bean> 

2. listenerContainer

    负责调度MDP, 绑定(A) connectionFactory, (B)Queue和MDP。

<!--  this is the attendant message listener container -->
 <bean id="listenerContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer">
  <property name="connectionFactory" ref="jmsConnectionFactory"/>
  <property name="destination" ref="destination"/>
  <property name="messageListener" ref="messageListener"/>
 </bean>

  互相绑定的关系有点晕,发送端和接收端都以不同形式绑定了(A) connectionFactory, (B)Queue和 (C)Converter。

4. 下篇


1. 说明

   请先阅读ActiveMQ4.1 +Spring2.0的POJO JMS方案(上)

   本篇将补充说明了:

   1) 使用数据库持久化消息,保证服务器重启时消息不会丢失
   2) 使用Jencks作正宗的JCA Container。

2.持久化消息

2.1 给Broker加入Persistence 配置

在配置文件applicationContext-activemq-embedded-persitence.xml中的<amq:broker>节点加入  

<amq:persistenceAdapter>
<amq:jdbcPersistenceAdapter id="jdbcAdapter" dataSource="#hsql-ds" createTablesOnStartup="true" useDatabaseLock="false"/>
</amq:persistenceAdapter>

请注意MSSQL(2000/2005)和HSQL由于不支持[SELECT  * ACTIVEMQ_LOCK FOR UPDATE ]语法,因此不能使用默认的userDatabaseLock="true",只能设置成useDatabaseLock="false"

2.2 配置多种数据源

配置多种数据源,给jdbcPersistenceAdapter使用,SpringSide 中使用的内嵌HSQL

 <!-- The HSQL Datasource that will be used by the Broker -->
<bean id="hsql-ds" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:res:hsql/activemq"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
<property name="poolPreparedStatements" value="true"/>
</bean>

2. 3 说明

   笔者仅仅使用了jdbcPersistenceAdapter,其实在ActiveMQ的XSD已经描述了多种PersistenceAdapter,可以参考对应的XSD文件.

  另外对于数据库的差异主要表现在设置了userDatabaseLock="true"之后,ActiveMQ使用的[SELECT * ACTIVEMQ_LOCK FOR UPDATE] 上面,会导致一些数据库出错(测试中MSSQL2000/2005,HSQL都会导致出错)。另外HSQL的脚本请参见activemq.script。

3. Jenck(JCA Container)  

   Spring 2.0本身使用DefaultMessageListenerContainer 可以充当MDP中的Container角色,但是鉴于Jencks是JCA标准的,它不仅仅能够提供jms的jca整合,包括其他资源比如jdbc都可以做到jca管理

所以,同时完成了这个ActiveMQ+Spring+Jencks 配置演示,更多的针对生产系统的JCA特性展示,会在稍后的开发计划讨论中确定。

     此文档适用于说明使用 Jecncks 和 使用Spring 2.0(DefaultMessageListenerContainer)  充当MDP Container时的区别,同时演示Jecnks 的Spring 2.0 新配置实例。

3.1 引入ActiveMQ ResourceAdapter 和Jencks 的XSD

  在ApplicationContext.xml(Spring的配置文件)中引入ActiveMQ ResourceAdapter 和Jencks 的XML Scheam 配置文件),如下:

   ActiveMQ4.1 响应Spring 2.0号召,支持了引入XML Schema namespace的简单配置语法,简化了配置的语句。 

  在ApplicationContext.xml(Spring的配置文件)中引入ActiveMQ的XML Scheam 配置文件),如下:

<beans
xmlns="http://www.springframework.org/schema/beans"   xmlns:amq="http://activemq.org/config/1.0"   xmlns:ampra="http://activemq.org/ra/1.0"   xmlns:jencks="http://jencks.org/1.3"   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://activemq.org/config/1.0 http://people.apache.org/repository/org.apache.activemq/xsds/activemq-core-4.1-incubator-SNAPSHOT.xsd
  http://activemq.org/ra/1.0 http://people.apache.org/repository/org.apache.activemq/xsds/activemq-ra-4.1-incubator-SNAPSHOT.xsd
  http://jencks.org/1.3 http://repository.codehaus.org/org/jencks/jencks/1.3/jencks-1.3.xsd">

由于ActiveMQ RA和Jencks 那个XSD 仍然有部分错误,因此使用的是自行修改过的XSD。(是xs:any元素引起的错误)

先在ClassPath根目录放一个修改过的activemq-ra-4.1-incubator-SNAPSHOT.xsd和jencks-1.3.xsd。

同样修改 ClassPath 下面META-INF\spring.schemas 增加内容如下。这个spring.schemas是spring自定义scheam的配置文件,请注意"http:\://"部分写法

http\://people.apache.org/repository/org.apache.activemq/xsds/activemq-ra-4.1-incubator-SNAPSHOT.xsd=/activemq-ra-4.1-incubator-SNAPSHOT.xsd
http\://repository.codehaus.org/org/jencks/jencks/1.3/jencks-1.3.xsd=/jencks-1.3.xsd

3.2  配置方案

3.2.1 基础零件 

1. 配置ActiveMQ Broker  参见 ActiveMQ+Spring

2. 配置ActiveMQ Resource Adapter

<amqra:managedConnectionFactory id="jmsManagedConnectionFactory" resourceAdapter="#resourceAdapter"/><amqra:resourceAdapter id="resourceAdapter" serverUrl="vm://localhost" />

3. 配置Jencks 基础配置

   具体的配置可以参见Jencks的XSD

<!-- jencks PoolFactory config-->
<jencks:singlePoolFactory id="poolingSupport" maxSize="16" minSize="5" blockingTimeoutMilliseconds="60" idleTimeoutMinutes="60" matchOne="true" matchAll="true" selectOneAssumeMatch="true" /> <!-- jencks XATransactionFactory -->
<jencks:xATransactionFactory id="transactionSupport" useTransactionCaching="true" useThreadCaching="true" />  
<!-- jencks ConnectionManagerFactory -->
<jencks:connectionManagerFactory id="connectionManager" containerManagedSecurity="false"  poolingSupport="#poolingSupport" transactionSupport="#transactionSupport" /> <!-- jencks TransactionContextManagerFactory -->
<jencks:transactionContextManagerFactory id="transactionContextManagerFactory"/>
  

4. 配置给JmsTemplate使用的connectionFactory (主要是生成者/发送者 使用)

   这里注意下,在配置jmsTemplate的使用的targetConnectionFactory就是使用jencks配置的connectionManager

<!-- spring config jms with jca-->
 <bean id="jmsManagerConnectionFactory" class="org.springframework.jca.support.LocalConnectionFactoryBean">
  <property name="managedConnectionFactory">
   <ref local="jmsManagedConnectionFactory" />
  </property>
  <property name="connectionManager">
   <ref local="connectionManager" />
  </property>
 </bean>
 
 <!--  Spring JmsTemplate config -->
 <bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
  <property name="connectionFactory">
   <!--  lets wrap in a pool to avoid creating a connection per send -->
   <bean class="org.springframework.jms.connection.SingleConnectionFactory">
        <property name="targetConnectionFactory" ref="jmsManagerConnectionFactory" />
   </bean>
  </property>
  <!-- custom MessageConverter -->
  <property name="messageConverter" ref="orderMessageConverter" />
 </bean>  

5. 配置Spring 2.0的MessageListenerAdapter,保证不需要用户实现MessageListener

  ActiveMQ+Spring

 6.配置Jecnks 充当MDP的Container

  就是把上面的MessageListenerAdapter配置到Jencks里面,完成整个MDP的配置

 <!-- Jencks Container-->
 <jencks:jcaContainer>  	<jencks:bootstrapContext>
   		<jencks:bootstrapContextFactory threadPoolSize="25" />
  	</jencks:bootstrapContext>
  		<jencks:connectors>
   	   <!-- use jencks container (use spring MessageListenerAdapter)-->
   		<jencks:connector ref="messageListener">
    			<jencks:activationSpec>
     				<amqra:activationSpec destination="org.apache.activemq.spring.Test.spring.embedded" destinationType="javax.jms.Queue" />
    			</jencks:activationSpec>
   		</jencks:connector>  	</jencks:connectors> 		 <jencks:resourceAdapter>
   		<amqra:resourceAdapter serverUrl="vm://localhost" />
  	</jencks:resourceAdapter>
 </jencks:jcaContainer>

posted @ 2008-04-12 01:37 hk2000c 阅读(738) | 评论 (0)编辑 收藏

请参考一下马琳、王皓的主管教练吴敬平的文章

直板反胶正手拉球的基本原理和训练方法

乒乓球基本上是一项圆周运动,正手和反手拉球都是以运动员的身体重心为轴心、以身体到身体重心的连线为半径进行圆周运动。因此,不管是正手还是反手击球都必须符合这个原理,从这个意义上讲,正手拉球动作的基本原理就是一种力的传递。
正手拉球的基本要点
基本站位:两腿张开与肩的宽度为差不多相同,身体稍微前倾,重心在前脚掌上,拉球时,身体向右转(以右手为例),重心放在右脚上,在转腰的过程中,用腰控制大臂,右肩稍底,小臂自然下垂,用手腕控制板型,板型前倾(拇指用力压住球板,食指稍微放松,中指顶住球板),板型前倾的角度因来球的旋转不同而调整。
击球原理:击球的时候,以右手为例,首先是腿上发力,向左蹬腿,身体重心从右脚向左脚转换,交换重心,身体前迎,身体前迎的方向要和击球的方向一致。然后是腰上发力,用腰带动大臂转动,把力传递到前臂,在击球一瞬间,收缩前臂用力击球。从力学的原理讲,正手拉球前,小臂和大臂之间的角度越小越好,这是加大半径,半径越大,初速度就越大,在击球瞬间突然收缩前臂,使半径变小而获得加速度,使速度加快,力量加大。击球时,小臂和大臂之间角度的变化要根据来球和击球的需要进行变化。很多运动员在进行正手拉球时往往只注意了收前臂而忽略了转腰,用腰来控制手臂的发力,或者是注意了用腰的发力来带动手臂而忽略了收前臂,前臂和大臂之间的角度几乎没有变化或变化很小。总结起来正手拉球应注意四点:1、必须注意重心的交换,重心迎前的方向要和击球的方向一致。2、一定要用腰控制大臂,是腰上发力,而不是用手臂发力,注意拉球时腿、腰、大臂、前臂、手腕发力的协调。3、击球瞬间必须快速收缩前臂、手腕发力,前臂收缩的速度越快,发出的力量就越大。4、击球点必须保持在身体的右前方,击球点离身体越近,越容易控制球。有一点值得注意的是不管是什么样的拉球动作,必须和你自身具备的身体条件相符合,只要不影响动作的发力就可以,没有什么固定的动作模式。另外就是在击球前,球板和球之间要有一定的距离,尽量主动去击球,而不要让球来撞你的球拍,或者是球与球拍之间的距离太小,容易被来球顶住,影响你的发力。

正手拉球的方法与技巧
正手拉球是一门很复杂的技术,有近台拉球、中近台拉球、远台拉球,有拉上旋球、下旋球,有近台快带、反拉弧圈球,拉半出台球等等。不管拉球有多么复杂,但有一点是最重要的基础,就是步法。步法的好坏,直接关系到正手拉球的命中率、力量的大小和拉球时的调节能力。要想练好正手拉球,就必须先练好步法。而在这一点上,是专业运动员和业余运动员最大的区别所在,业余运动员不可能像专业运动员那样进行大量的高强度的步法训练。但有一点是相同的,那就是击球的技巧。只要能够做到因势利导,充分发挥现有的条件,也会收到一定的效果。下面,我给大家介绍一些比较实用的训练方法和技巧:
1、拉好定点下旋球:拉冲下旋球是直板反胶最基础的基本功,在拉下旋球时除了注意前面提到的基本动作要领以外,要特别注意手腕的用力方法。在击球的瞬间是用手腕去摩擦球,击球点在来球的中上部,在用手腕摩擦球时还要根据来球旋转的强弱再加上一定的撞击。就是人们常说的又摩又打。拉冲下旋球旋转弱的来球要连摩擦带撞击,撞击可稍大于摩擦。拉冲下旋球旋转强的来球必须用力摩擦击球,用自己拉球的力量抵消来球的旋转。在击球的瞬间要特别注意击球时一定要把球往前送,不能靠力量去硬碰球。这就是我们常说的“吃球”,尽量让球在球板上停留的时间长一些。经常这样训练拉球,你对球的感觉就会越来越好,拉球就会越来越有数,慢慢达到运用自如。训练的方法,在没有多球条件的情况下可采用拉球一方发下旋球到对方的反手位让对方搓长球到侧身位,然后发力拉冲这个球。拉球时一定要注意用全力拉冲,不要考虑下一板球对方是否能够防过来。要的就是让你防不过来。经常这样训练,你的拉球力量一定会提高。在有多球的条件下,可让对方发下旋球到你的侧身位,定点发力拉冲这种球。拉球时要掌握好击球时间,在对方来球跳到最高点或下降前期击球最好。击球时间一定要相对固定,这样容易掌握拉球的命中率,好调节。出界多就向前送一点,下网多就多摩擦一点。在定点拉冲下旋球比较有数的情况下,再把来球的落点扩大到全台的定点拉冲,这样不断加大拉球的难度,拉球的水平就会不断提高。
2、拉好定点上旋球:拉上旋球和下旋球不同的是,拉上旋球击球点在来球的上部,摩擦球要大于撞击球,击球的瞬间一定要往前送。训练的方法基本和抢拉下旋球一样,只是来球的旋转不一样,是上旋球。在推挡后侧身发力拉冲这板球,或对方变你正手位后发力拉冲,反复练习。有多球训练的条件,可以由对方直接发上旋球到你的正手位和侧身位抢冲,落点可以从定点到不定点,逐步提高击球的难度。
3、练好反拉弧圈球:反拉弧圈球是一种高级技术,尤其是业余运动员掌握了这项技术就像如鱼得水,你就掌握了比赛的主动权。因为一般的业余运动员在拉弧圈球时拉高吊弧圈球的时候多,你掌握了反拉弧圈球的技术,你就站在了比对方高一挡的层次上。反拉弧圈球的要领,首先要自己发力,尽量少借对方的旋转,用自己拉球的力量去抵消对方来球的旋转。其次是在反拉时摩擦球一定要薄,摩擦球的上部甚至顶部,既要借对方来球的旋转的力,还要自己发力摩擦球。越是自己发力反拉,命中率越高。越是怕对方的旋转去碰球,越是容易吃对方的旋转。训练的方法,对方发下旋球到你的反手位,你搓球到对方侧身位,对方拉高吊弧圈球到你反手位,你侧身反拉,这样反复练习,等基本掌握了反拉弧圈球的规律以后,再把反拉扩大到全台和不定点反拉。
4、近台正手快带弧圈球:这项技术是防守中很先进的技术,也是很难掌握的技术,是90年代后期才逐渐被采用的技术。在这之前人们在正手位的防守都是平挡,借对方来球的旋转把球挡过去,因而在比赛关键的时刻就很容易因紧张而造成失误,即使不失误,防过去的球也没有威胁,很容易被对方连续进攻。到90年代后期,中国的运动员把反拉的技术运用在近台的防守上,特别是直板反胶打法的运动员运用更多,加快了攻防转换的节奏,收到了很好的效果,马林在这项技术的运用上是非常突出的。这项技术要求运动员的对来球的判断要非常快、准确,手上对球的感觉要求很高,因为有很多球是在失去身体重心或不到位的情况下,完全靠运动员手上的功夫去完成技术动作。我想虽然目前在业余运动员中能真正掌握这项技术的不多,但已经具备了一定水平的运动员可以去尝试一下,也许你会有意外的收获。
这项技术的技巧主要在于掌握好击球时间和手腕的用力,击球时间尽量在球的起跳前期(上升期),当步法实在到不了位的情况下,还可以在球刚一跳起时就击球。击球时靠腰和手腕发力,接触球的顶部。接触球时既要借对方来球旋转的力,同时自己一定要发力去摩擦球,尽量摩擦薄一点,摩擦厚就容易下网,在摩擦球的瞬间一定要把球往前顶。训练方法可采用搓下旋球到对方正手位让对方拉弧圈球到自己的正手位,然后正手近台快带。这样反复练习就会逐渐掌握击球的基本方法,在快带对方从下旋球拉起来的弧圈球比较熟练的情况下,再进行推直线让对方拉弧圈球到自己的正手位快带上旋弧圈球的训练。这样,你就会慢慢掌握在防守中正手近台快带弧圈球的技术。这项技术的关键点是在击球时一定摩擦球要薄,而且自己一定要主动发力去带球。

正手拉球的注意事项
业余选手在练习正手拉球时,要注意掌握以下几点:
1、收前臂:在正手拉球时一定要注意收前臂,大臂和小臂之间的角度一定不能固定,要根据来球来决定摆臂的大小。但要注意一点,收前臂一定要用腰来控制。
2、转腰:由于乒乓球是圆周运动,击球时用腰来控制手是非常重要的环节,击球时球拍的后引不是用手往后拉手,而是用转腰来完成,用腰固定大臂,转腰的速度要远远快于拉手。就是说,在击球前的摆臂是先转腰而不是先拉手。而我们好多球迷们在打球时都是先拉手,不知道转腰,因而在击球时经常出现身体不协调导致发力不集中或发不出力。
3、击球点:击球点的最佳位置是在身体的右前方(以右手为例),要保持最佳的击球位置就必须学好步法,保持好身体的重心,重心的高低要根据来球来决定。马林经常使用的侧身倒地爆冲是不得已而为之,对方搓过来的球又低又长,拉完以后不可能再还原,只有搏杀。马林在拉这种球的时候重心低,但是击球点是球的最高点或下降前期。正手位大角度的球击球点要根据自己步法移动的情况来决定击球点的高低。一般情况下是在球的下降中期和后期击球。
4、手腕的运用:在拉球时,手腕要相对固定,不能晃动太大,击球瞬间用中指顶住球板发力摩擦球。另外手腕还具有击球瞬间的调节功能,比如在拉球时突然感到球的旋转比自己预想的要转时就靠手腕来调节击球的力量大小和摩擦球的部位。在不到位和顶住自己的情况下,就要靠腰和手腕来调节击球点。特别是在比赛中,很多球都不是很规则,来球的落点也是你最难受的地方,这时候就要靠手腕来调节,手腕的调节主要靠大拇指和中指用力来完成。其次拉球时板型的控制也要靠手腕来完成,有很多的直板运动员正手拉球时吊腕很厉害,这影响发力,一般情况下,手腕和前臂几乎在一条直线上,球板把与手腕之间的角度在45度左右。
5、吃球:我们看一个运动员拉球的好坏,主要是看他拉球时是否吃球。吃球就是球在球板上的停留时间比较长,而不是球一碰球板就出去了。要做到拉球时吃球,就必须每一板球都主动发力去摩擦球,在平时的训练中尽量少打借力球。拉球吃球的好坏,在平时训练中不是很明显,但在比赛中就有很大的区别。很多球都是在你不到位的情况下要完成拉球的动作,就全靠你用手腕主动发力去摩擦球来调节,你习惯了主动发力拉球,就能在比赛中控制拉球时力量和击球部位的调节,拉过去很多高难度的球。
6、抢冲上旋球和下旋球的区别:动作上没有多大的区别,区别在于抢冲下旋球时击球点在球的中上部,发力的时候根据来球的旋转可带点撞击;抢冲上旋球时击球点在球的顶部,主动发力摩擦球,击球时身体重心也随之向前。特别是在反拉弧圈球时,摩擦薄反而容易过去,摩擦厚或带点撞击就容易失误。
7、微调:很多球迷朋友提出这个问题,我认为要在比赛中做到这一点是比较难的。这首先取决于你个人本身的球感,就是你手上对球的感觉。其次是在训练中不断地培养你对球的旋转的理解,要清楚地知道你打过去的球是什么样的旋转,对方回过来的球又是什么样的旋转。只有这样,你才会根据来球的不同,在很困难正常击球的情况下,在来球很不规则的情况下,在球落在边边角角很难回击的情况下,通过手上的调节把球回击过去。因此,对于业余球迷朋友们来讲,最主要的是去琢磨球的旋转变化,把这个规律基本掌握住了,你就具备了微调的能力
正手弧圈球技术是乒乓球技术中最基本也是最重要的技术之一。拉好弧圈球的三个要素就是腿、腰、手;三者要协调一致,才能发挥弧圈球的最大威力。


从弧圈球的风格上来讲,目前主要分为欧洲派和亚洲派。欧洲选手拉弧圈球的时候,撞击的成分比较多,因此球在飞行的过程中速度快,力量大,弧线低;亚洲选手拉弧圈球的时候,摩擦的成分比较多,因此球在弹起后的过程中速度快,旋转强,弧线低。随着弧圈球技术的发展,目前各国选手都在相互学习,相互借鉴,因此并没有十分明显的风格区别,而是根据不同的球运用不同的技术。

弧圈球的基本技术动作并不难,但是要想拉好弧圈球必须要勤学苦练,才能是自己的技术有大幅度的提高。如何掌握基本的弧圈球技术呢?(以右手握拍选手为例)

一、技术动作分解

1.准备动作:

拉球之前,站位一定要合理。一般来说,站位距球台边缘1.5米左右。左脚前,右脚后,两脚间距略比肩宽,右脚尖于左脚脚窝的位置平齐,以两脚前脚掌内侧着地。两腿弯曲,含胸,重心放低,身体与球台边缘的夹角大概为45度左右。

2.拉球:

拉上旋球时,右肩略微下沉,同时横向转腰,右臂自然放松,靠横向转腰动作完成引拍的过程。此时,以右脚为轴,重心放到右腿上。然后,右腿蹬地,腰部横向回转,并带动右臂,注意此时右臂仍为放松状态。待腰转到基本与球台边缘平行的时候开始收缩前臂,击球。重心由右腿转移到两腿上,两肩持平。击球时,要找好击球时间。击球时间分为上升期和下降期,上升期是指来球即将达到最高点的时候,下降期是指来球从最高点刚刚下落的时候。一般来说,来球位于右腹部前方一尺多的距离时击球感觉最好,可以发出力。击球时,要注意摩擦球,主要向前发力。击球后要注意大臂、小臂立刻放松,还原。
此主题相关图片如下:


关于击球部位,对于以拉打为主和摩擦为主是有区别的。 以拉打为主的选手,击球的部位一般为B点或B、C点之间。以摩擦为主的选手,击球部位一般为C点。
拉下旋球的动作要领与拉上旋球基本一致。只是拉下旋球时,右肩沉的更低一些,击球的部位一般为B点,且用力的方向向上多一些。

3.步法

拉球时,要根据来球的位置,时刻跑动来调节击球的最佳位置。跑动时要保证重心尽量平稳,身体不要乱晃。

二、高吊弧圈与前冲弧圈

高吊弧圈一般是针对拉下旋球而言的。高吊弧圈以旋转见长,但是弧线略高,速度较慢。高吊弧圈的击球部位一般为B点,甚至是A、B点之间,这要根据来球的旋转而定。拉高吊弧圈,右肩下沉的较低,用力方向向上的比较多,先要制造一个高过球网的弧线,然后用力方向向前,再制造一个向前的弧线。如果一味的向上硬拉,则球很容易出界。

前冲弧圈速度快,力量大,但旋转稍逊。拉前冲弧圈,击球部位一般为C点或B、C点之间。右肩略微下沉,用力方向向前比较多。若来球的下旋旋转很强,则必须增加转腰的幅度和前臂收缩的速度,以增大对球的摩擦力。

三、台内弧圈球技术

台内弧圈球的技术难度比较大。首先要判断来球的位置和高度,根据来球的高度来决定引拍的高度。拉台内弧圈球,一般引拍的高度较高,往往与台面高度持平,甚至高于台面。击球部位一般为D点。由于摩擦球的部位很薄,因为对于下旋非常强的台内球,处理起来难度很大。而对于不太转的下旋球来说,台内弧圈球给对方造成的威胁还是很大的。拉台内弧圈球,要注意用力方向向上多一些,继而向前,要把弧线拉短。

四、套胶与弧圈球

进口套胶与国产套胶的性能不同,对于拉弧圈球的风格有一定的影响。
欧洲人拉球多为拉打,因为欧洲的套胶胶皮黏性差,海绵偏软,但弹性好。使用进口套胶,球在接触到拍子之后,海绵被挤压的程度较深,海绵被压缩的行程长,这样就削减了来球的大部分旋转和力量,因此采用拉打的手法可以很好的控制来球,加之欧洲人身高马大,爆发力非常好。这样的拉球威力不小。

亚洲人拉球多摩擦,因为国产的套胶,如狂飙系列套胶,胶皮黏性强,海绵弹性非常实在,非常大。在球接触拍子的时候,胶皮给了来球很大的阻力,而海绵被压缩的程度也不大,这样就造成的脱板速度很快。因此只有多摩擦,以旋转克旋转才能拉出高质量的弧圈球。所以使用国产套胶对拉球的技术要求较高。

随着乒乓器材的发展,国内已经生产出很多新产品,兼具了国产与进口的很多优点,对于众多的乒乓球爱好者来说,又多了很多的选择。

五、拉球的常见问题
1. 重心后坐。
重心后坐,自然使腿部力量不能发挥出来,使手臂的走向多为向上,削减了拉球的速度、力量和旋转。
2. 手臂僵硬。
引手的过程中,肌肉僵硬,大大降低了控制球的能力,并锁住了力量。击球后肌肉僵硬,使力量不能全部发挥出来,并降低了还原速度。
3. 转腰不够。
只靠手臂拉球,速度、力量、旋转都有很大的损失。
4. 抬肘、抬肩。
使腿、腰、手不能协调一致,当力量从腿、腰传到手的时候,能量中断。
5.步法迟钝。
等球,使击球点太低,使全身的力量用不到球上。
posted @ 2008-03-08 01:23 hk2000c 阅读(710) | 评论 (0)编辑 收藏


拉弧圈最重要的环节是什么?是吃球。

就是尽量延长球和胶皮接触的时间,主动发力控球,把挥拍的能量充分作用到球体上。吃球就是球在球板上的停留时间比较长,而不是球一碰球板就出去了。要做到拉球时吃球,就必须每一板球都主动发力去摩擦球,在平时的训练中尽量少打借力球。

延长控球时间靠是什么?反胶、软板、灌胶、先打后摩,还有最重要的一点是在加速挥拍过程中击球。加速击球就好比在阻力较小的平面上推箱子,只有不断加速去推,才能一直不离手,力量才能充分传递到箱子上。也就是说,拉弧圈最好是不断加速追着球摩擦。

如果拉上旋来球,就是逆旋转击球,球和胶皮接触的瞬间,球和胶皮的相对速度大,来球减转的过程就是个缓冲过程,球不会很快脱板,一般不会有吃不住球的感觉。

如果拉下旋来球,则是顺旋转击球,如果挥拍向上的速度低于旋转, 便无法吃得住球。就好比玩陀螺,抓住转动的陀螺容易,因为是逆旋转,而给陀螺加转就很困难,要用比陀螺更快速度的鞭子去抽。这一点对着削球手感觉最为明显,力量还没作用到球上,球就脱板了,常会有吃不住球的情况发生。如果仔细观察录像,国手们拉削球时挥拍摩擦都极快,挥拍之所以快的就是靠发力抵消来球的旋转。对下旋来球, 挥拍速度是不能低于旋转的。

拉下旋球为保证能吃住球需掌握三个要点:

一是增大球和球板的正压力,就是“又摩又打”,增大正压力便于摩擦。

二是加快向上向前的挥拍速度,包括手腕也要加上摩擦球的动作。

三是掌握击球时间。一般是在下降期拉,一方面下旋球在空中飞行时会逐渐减转,另一方面,球在下落时由于下落速度和球的旋转方向相反,两个速度相抵,挥拍相对速度就体现的更快一些。

第一板从下旋拉起的弧圈很难防守,也是因为具有其“顺旋”的加成效果。一旦练成,对对手威慑极大。





前言:都说下旋球下降期好拉,为甚么?什么出转没出转,都是凭感觉。请看物理学的精确分析。我们对事物不仅要知其然,也要知其所以然。

如图:球为下旋,设球拍垂直向上摩擦,与球在a点接触。假设球旋转每秒30转,直径40MM,则a点线速度为:

V2=2πr*30 = 3.768m /s(即如果球原地向上转,a点的对地速度)

1、拉上升期,a点速度为转速加球速。

设球向上的分速度v0 = 2m/s.a点对于地面的速度为 v0+v2 = 5.768m/s .如果靠摩擦把球拉起,拍速必须大于a点速度。约21公里/小时。

2、拉下降期,a点速度为转速减球速。

设球向下落的分速度v0 = 2m/s.a点对于地面的速度为 v0-v2 = 1.768m/s .如果靠摩擦把球拉起,拍速必须大于a点速度。约6.3公里/小时。

可见拉上升期比下降期需要三倍的速度!

 

 

posted @ 2008-03-08 01:19 hk2000c 阅读(981) | 评论 (0)编辑 收藏

   我们在单位里调试用户系统时,单位的网络地址一般和用户的网络地址不在一个网段上,如果没有路由器则两网不能互通,那对工作会很有影响。硬路由器价格昂贵也没有必要去配,因为SOLARIS可以很容易地设成软件路由器,而不需另外花费。

  1、编辑文件/etc/hosts,为该工作站加另一个网段地址:

   #vi/etc/hosts

   127.0.0.1localhost

   192.9.200.1serverloghost;本例的主机名及地址

   192.9.201.1 anoserver;另一个对应的名称及地址

  2、编辑文件/etc/nerworks,将两个网络的地址加入:

   #vi /etc/networks

   loc 192.9.200;本网网址

   ano 192.9.201;另一个网的网址

  3、新建文件/etc/gateways,该文件只要存在没有内容也可,以使SOLARIS在启动时运行路由器服务进程。

   #cat/dev/null>/etc/gateways

  4、查询主网卡的名称:

   #ifconfig-a;列出系统中的所有网络接口

   loO:flags=849<UP,LOOPBACK,RUN-NONG,MULTICAST>mtu 8232

   inet 127.0.0.1 netmask

   ff000000

   hneO:flags=863<UP,BROADCAST,NO-TRAILRS,RUNNNHG,MULTICAST>mtu1500

   inet 192.2.200.1 netmask ffffff00 broadcast

   192.2.200.255

   ether 8:0:20:1:2:3

   hme即为工作站上所配的100M网卡名,如果你所用的是10M网卡则名为le。

  5、新建文件/etc/hostname.hme0:1,将/etc/josts中的另一个主机名填入,以使SOLARIS启动时在物理接口hme0上建立一个逻辑接口。

  6、设置完以上各步后,重启工作站

  7、效果:

   在工作站启动中,可以看到“machine is a router.”的噗显示。表明本机已成为一个路由器,会向网络上发RIP包,用接口查询命令可见:

   #ifcofig -a ;列出系统中的所有网络接口

   lo0:flags=849<UP,LOOPBACK,RUNNNG,MULTICAST> mtu8232

   inet 127.0.0 .1etmask ff00000

   hne0:flags=863<UP,BROADCAST,NOTRAILERS,RUN-NING,MULTICAST>mtu 1500

   inet 192.9.200.1 netmask ffff00 broadcast

   192.9.200.255

   hne0:1:flags=8d0<UP,BROADCAST,NOTRAULERS,RUMNNNG,MULTICAST>mtu 1500

   inet 192.9.201.1 netmask ffff00 broadcast

   192.9.201.255

  以上表明已启动了hme0上的一个逻辑接口,地址为192.9.201.1。

  在别的UNIX机器上,会根据RIP包自动将该工作站加入到路由表中,在PC机上(例如WIN95),只要在控制面板中将TCP/IPM网络的网关设置为该工作站的地址(使用与本机同一个网络的地址),就可以与另一网络的机器通迅了。
posted @ 2008-03-05 16:36 hk2000c 阅读(440) | 评论 (0)编辑 收藏

乓球意识,是指运动员在乒乓球教学训练和比赛中的一种具有明确目的性和方向性的自觉的心理活动。乒乓球意识的最显著特点是它的能动性。
  近年来,意识一词的使用日趋广泛,如:“树立首都意识”、“树立奥运意识”等。这里所说的意识概念,和我们乒乓球运动中所说的意识概念是一致的。就是要你想到北京是祖国的首都,想到奥运会,并以此来指导我们的思想和行动。
  这样,意识又可以理解为是一个思路或观点,它只是让你自觉地想着这个问题。至于怎样想、用什么方法去解决这个问题哪是技术问题。以判断意识为例,它只是要运动员想着判断球,注意区别来球的不同特点。至于怎样判断来球,那是技术方法问题,不属判断意识的范畴了。如有人打球时,不看对方打的是什么球,一律愣头愣脑地抽,结果失误频频。这是缺乏判断意识的典型表现。另一人懂得应该判断对方来球,实践中也在紧紧地盯着球,但由于对方发球质量高,结果接发球时还是“吃”了。这就不是判断意识的问题,而是还未掌握好接发球的方法。
  科学意识。一般人都能打乒乓球,但却不是谁都能打好。乒乓球运动有其自身的客观规律。欲打好,则必须使自己的行为和心理符合乒乓球运动的客观规律。为此,我们必须不断地总结自己和他人的训练和比赛经验,不断地学习科学文化知识(采用时代可能提供的先进思想和先进的科学技术方法、手段),不断地探求乒乓球运动的规律,并用这些规律来指导自己的实践。
  苦练与巧练相结合的意识。没有一名优秀的乒乓球运动员是不苦练的,但却不是所有苦练者都能成为优秀运动员。乒乓球运动有其自身的规律,只有当人们的行动符合其规律时,才能获得成功。探索规律,并用此来指导自己,这就是巧。所以,谁要想打好乒乓球,就必须苦练与巧练相结合。没有巧练的苦练,是傻练,甚至在一定意义上可说是白练;没有苦练做基础的巧练,也称不上是巧,因为它违反了训练的最基本规律。
  立志意识。志向,是一种巨大的力量,它能使人产生坚强的意志和毅力,推动人们的实践活动。志向总是同毅力相伴而行。一名运动员没有坚定不移的志向,就不可能有坚强的意志和毅力,也就不可能实现自己的志向。少儿学打乒乓球,一般多从兴趣开始,而教练员则应随其进步不断地培养他们立志的意识。否则,就会如同古人所云:“志不立,天下无可成之事。”

  判断意识。对付不同的来球,应用不同的打法。若想打好球,首先应对来球作出及时、准确的判断。这是正确还击来球的前提。
  盯球意识。盯球,是正确判断的基础。不少人对来球判断不及时或错误,都是因为盯球不够。运动员每打完一板球后,都应随球密切注视对方击球的动作(尤其是击球瞬间的动作),并紧盯对方击出球的弧线。
  移步意识。对方来球落点和节奏不定,为确保在最佳的位置和时间击球,或最大限度地发挥个人的特长技术(如反手位用侧身攻),必须移步击球。应明确,打乒乓球绝不是单纯的手法问题,随技术水平的提高,脚步移动的重要性将越来越明;显、它是争取主动、抢先进攻的有力保证。

  探索合理击球点位置的意识。所谓的击球点位置,即击球点与身体的相对位置。各种技术动作,都有一个最适宜的击球位置。它虽有个一般的规律,但因人而宜十分重要。所以,运动员在打球的实践中必须不断地琢磨与研究自己击球时最适宜的位置…
  打、摩结合意识。打乒乓球有两个最基本的力,一个是撞击球的力,简称为打;另一个是摩擦球之力,简称为摩。除近网大高球,可以用单纯的打外,打其它的球,都必须是打与摩的结合。细究起来,这里还有两层意思。
  1、快抽时,以打为主,摩擦为辅。打,可增加球的速度和力量;摩,可使球产生上旋,上旋有利于制造合理的击球弧线。
  2、制造旋转时(如拉弧圈球),应以摩擦球为主。但是一味追求摩擦,势必物极必反。擦球太薄,反而用不上力,自然难以打出旋转强烈的球来。应先打后摩,即以打的动作将球近似粘于拍面,然后再加力摩擦。
  调节意识。无论哪种技术动作,在还击不同性能的来球时,都必须自觉地调节动作。具体可细分为:
  1、力量调节意识:根据来球情况,适当调节自己的发力。来球慢且高,发大力;攻对方搓过来的下旋球,自己发力为主,稍借对方来球之力;对方拉冲不特别凶、球略向前拱时,借力中发力;对方发力抽或冲时,自己应借力挡一板或对付一板,不宜发大力。
  2、拍形调节意识:应视来球旋转与高低,适当调节拍形。来球低或带强烈下旋时,拍形稍后仰;来球不转或与可网高时,拍形与台面垂直;来球上旋或高于球时,拍形前倾;
  3、引拍调节意识:应视来球的快慢、高低、旋转等变化,相应调整引拍动作的快慢、大小和高低,切忌习惯性引拍(即不看来球,打完一板球后就习惯地将球拍引至原来位置)。如,对方拉过强烈上旋的弧圈球来,应高手引拍,并及时向前迎球(不要等球,更不能有向后的拉拍动作);对方来球下旋且低,应低手引拍;对方来球很快,应减小引拍动作幅度,加快引拍速度;来球慢且高,应适当加大引拍幅度,以利加力抽杀。
  4、手指调节意识:打乒乓球,无论身体何部位发力,最后都要通过手指作用于球拍。手指是身体发力时离球最近的部位,感觉最敏锐。在发力时,手指有如长鞭之梢儿,往往起到画龙点睛的作用。尤其是在发球时,触球瞬间的技巧全在手腕、手指的发力上。
  5、调节用力方向意识:打球时,应视不同来球,注意调节用力方向。如,攻下旋低球,应多向上用力;攻不转球,以向前打为主;攻打上旋强烈的加转弧圈球时,应向前并稍向下用力。
  还原意识。每打完一板球后,应迅速调整重心,将身体尽量还原至接近准备姿势,以为还击下一板球做好准备。有些人因缺乏此意识,打完一板球后,身体重心、手臂和球拍较长时间地停留在结束动作上,待对方将球还击过来,往往有来不及的感觉。
  体会击球动作的意识。每打一板球,都要对整个击球动作有清晰的肌肉感觉和表象,尤其是拍触球瞬间的发力情况应该清清楚楚。打丢一板球,应立刻回忆动作,哪儿错了?怎样才算正确?随着技术水平的提高,动脑筋还应越来越细。如攻球出界了,出界多少?刚才的击球动作为什么把球打出界这么远?有了这种意识,练技术才会有收获。否则,一点体会没有,技术怎么能进步?
  掌握击球动作实质的意识。研究技术动作,要注意它的外形,但尤为重要的是应分析击球动作的实质。摆速快、能发力、打摩结合好、命中率高、适应来球的范围广(即能依打不同来球的要求相应调整动作),这样的动作,就是好动作。

  击球动作的时空意识。分析技术动作,应从两方面入手。一是时间节奏方面,如快带弧圈,上升期击球;攻打弧圈球,上升后期击球;拉下旋球,下降期击球。二是空间位置(或几何图形的变化)。如挥拍路线、拍形、用力方法等。在时间节奏上,还要特别讲究从引拍到向前挥拍击球这段时间与来球的节奏合拍,这样才能打出又快又狠的球来。即在自己发力的同时,又充分借用了对方来球之力。研究与掌握这个节奏非常重要。
  动作不断分化的意识。在技术训练中,应不断将对方的来球总结分类,并明确回击每一类来球的方法和注意事项。不少运动员乐于打顺手球,来球突变,失误一球,甚为不快。其实,这时应好好想想,此球与顺手球有什么不同,长了、短了?旋转强了?还是节奏变了?弄清楚此球特点,继而明确打此类球的方法。这样不断将对方来球区分、归类,并明确打每一类球的不同方法,就可以使自己的技术越来越精细,水平亦越来越高。
  动作定与变的辩证意识。来球变了,打球动作应随之而变;但打同类球的动作,则是越固定越好。掌握与提高技术的整个过程是,对来球的区分越来越细,相应打球的动作也越分化越细;但打同类球的动作,又越来越固定。这“固定”与“变化”的统一,就促进了技术水平的不断提高。
  战术意识。实践中有两层含义。一是注意研究在比赛中运用战术的方法。因为只有合理地运用战术,才能使技术充分发挥。二是在训练中应带着战术意识练技术。拿最简单的右方斜线对攻作例。有人在练习右斜对攻时,能把比赛中的打大角和攻追身的战术联系起来,有意打大角度或时而打对方中路一板。另一人只是一味地盲目攻斜线。很明显,前者带着战术意识练习技术的效果要好。
  战略意识。在训练中,尤其是在比赛中,要有一个全局观念。如一年中有几次比赛?哪个比赛最重要?每个比赛的目的和任务都是什么?有时为参加某次有决定意义的大赛,还会有意放弃一些小的比赛。又如,对参加一次大赛而言,确立参赛人员和明确重点突破口(是团体、单打,还是双打?),则属带全局性的战略问题,必须认真对待。如果这些大题目未解决好,尽管你费了很大的气力,其结果也难以如愿。
  落点意识。训练中,特别是在比赛中,要注意击球的落点。一般情况下,大角度球、追身球、近网小球、底线长球和似出台未出台的球的落点较好。但不同对手,还会因其打法和个人掌握技术的情况,有其特殊点。如左推右攻者,一般最怕反手底线下旋球和调右压左的落点变化。比赛中,既要研究自己打球的落点,对方最怕什么落点,又要注意总结对方回球落点的规律。
  旋转意识。充分认识到旋转是乒乓球的重要制胜因素之一。在训练中,要自觉地提高发球、搓球、拉球、放高球等技术的旋转强度和变化。在比赛中,要善于利用旋转变化来扰乱以至战胜对方。
  速度意识。应充分认识到速度是我国快攻打法的灵魂。中国选手要战胜外国选手,主要靠的仍是速度——提早击球时间,重视手腕、手指的力量,能快则快,不能快时,先过渡一板,争取机会再转入快。
  变化意识。应充分认识到变化乃是乒乓球的重要制胜因素之一,自觉、主动地变化击球的速度、旋转、力量、落点和弧线。比赛中,双方都在为形成利我、不利对方的战局而变化着战术,谁的观察能力强,能及时察觉对方的战术意图,迅速变换相应的战术,谁就容易获取胜利。
  变化击球节奏的意识。比赛中,不仅应主动变化落点、旋转等,而且应主动变化击球的节奏。如原来都是上升或高点期的抢冲,现主动将击球时间后移,拉一板上旋强烈的加转弧圈球。对方已熟悉了你原来快冲的节奏,突然变成慢一拍的加转弧圈,往往就会上当,又如,同一类发球,有节奏较慢的,有节奏特快的,若再能保持拍触球前的动作尽量一致,则效果更好,这都是有变化节奏意识的表现。
  抢攻意识。这是积极主动的指导思想,力争抢攻在先(即平常说的先上手),能抢则抢、实在不能抢时,控制一板,争取下板抢。这种意识很重要。如有人的侧身攻球技术很不错,但就因缺乏抢攻意识,所以使他的侧身攻球技术英雄无用武之地。兵书讲:“两强相遇,勇者胜。”这“勇”字的一个重要含义就是先发制人。在弧圈球风靡世界的今天,快攻者只搓一板就攻攻的打法,即是抢攻意识强的表现。
  抢先发力的意识。近年来,世界乒乓球技术朝着更加积极主动的方向发展,不仅要求抢攻在先,而且应该尽量争取先发力,以使自己更加主动。
  连续进攻的意识。发起进攻后,应连续进攻,乘胜追击,直至得分。切忌攻一板后,再无继续进攻的准备,将已到手的主动又变成了相持、甚至被动。
  控、防、反意识。在不能主动进攻或对方抢攻意识极强时,应注意控制对方,并做好防守的准备。在防守或相持中,一旦有机会,应立即转入进攻。
  争取局部优势的意识。我的所有技术都比对方强,这叫绝对优势。比赛中自然怎么打都行。但在实践中,这种情况比较少见。多数情况是相对优势。这就要求运动员在比赛中应自觉地争取局部优势。能以己之长,打对方之短,当然最好。但有时不一定能实现此策。如,我发球的特长是转与不转,而对方的特短是接高抛发球。我之特长对不上对方的特短。高抛发球虽为我之特短,但与对方接此球相比,我还占便宜。此时用我之特短打对方特短就是最好的战术。因为我获得了局部优势。
  记球意识。比赛时,要有意记双方战术变化的过程,对方发的什么球,我怎么回的,他又怎么打的……从一个球,到整个战局的变化,要自觉地记。时间长了,就会大大提高自己的战术意识。
  互怕意识。比赛中“怕”是相互的。你怕他,他也怕你。谁能透过现象看到本质,谁能想得好一点,谁就容易主动。在紧张时,应多想对自己有利的方面,多想对方是多么怕你,多想战术,这样就可以长自己志气,灭对方威风。
  战略上藐视对手、战术上重视对手的意识。战略者,战争的全局也;战术者,战争的局部也。运动员应树立在全局或总体上藐视对手、藐视困难,而在具体的局部上应重视对手、重视困难的指导思想。如对某某比赛,首先应相信自己能战胜对方,并认真地分析对手的技术、战术、身体和心理特点。在此基础上制订自己的战术,如发什么球,怎么抢攻,接发球注意什么,相持、领先或落后怎么打等等,一步一步、详详细细。
  树立技术风格的意识。在技、战术训练中、应特别强调树立正确的技术风格。技术风格,常被喻为运动员的技术“灵魂”。培养什么样的技术风格,将直接关系到运动员的发展方向和可能达到的水平。无数事实证明,一个没有鲜明技术风格的选手,要攀登世界乒坛的高峰是不可能的!
  全面训练的意识。运动员应明确决定其竞技能力的诸因素(形态、机能、素质、技术、战术、心理和智力等),并自觉地提高之。这里,应反对狭隘的技术论。有人以为,要提高乒乓球运动成绩,就应该全力抓技术,什么身体素质、心理等统统不问。结果,尽管训练时间练的都是技术,但技术水平的提高却难以如愿。运动员一定要树立全面训练的观点。
  抓主要矛盾的意识。每一名运动员在某一时期必有一个主要矛盾影响着他整体水平的提高。谁善于捕捉之,并设法解决,谁就会获得明显的进步。如中国乒乓队在1957年参加第二十三届世乒赛后,发现自己的打法因缺乏准确性而使快速、凶狠的优点难以发挥作用。之后,狠抓了提高击球准确性的训练,很快就见到了明显的效果。不善于抓主要矛盾的人,不是漫无边际,什么都抓,就是虽有重点,但抓得不准。其结果都是一样——费力不讨好!
  凶稳结合的意识。乒乓球的技术各种各样,每个人的打法又各具特点,但凡在比赛中有实用价值的技术,无不同时具备威胁性(凶)和准确性(稳)。威胁性,即打出的球给对方回球造成困难,甚至使对方失误。准确性,即击球不失误。二者相互依存,并在一定的条件下可以互相转化。我们在训练或比赛中,一定要注意二者的结合,决不可以偏盖全。
  练绝招的意识。一个运动员的技术一定要有特长,即绝招,否则往往难以给对方造成威胁,也难以攀登技术高峰。绝招,可根据个人打法的特点、身体素质的特点、心理特点和所用球拍的性能等因素有目的地确立。“伤其十指,不如断其一指。”绝招就是起码能断其一指的技术。
  表现意识。明确训练是为了比赛。运动员在平日训练就应有一种要在比赛中强烈表现自己的欲望。这如同演员,排练节目就是为了上台表演;不但要上台,还要演得呱呱叫。平时这种意识强烈,演出时才可能发挥出色。当运动员亦是同样道理。
  重视理论的意识。运动员应充分认识到理论对实践的指导作用,自觉地学习和钻研乒乓球运动的理论,并注意理论与实践的结合。
  创新意识,无论是教练员还是运动员,都要十分重视创新。乒乓球运动的创新,应包括技术、战术、打法、训练、管理、器材设备和理论等七个方面。运动员主要是前四个方面。
  超前意识。教练员和运动员应能预测出未来的技术发展趋势,并以此来指导训练,使自己的训练能走在现实技术的前面。
  定量意识。运动员在训练或比赛中,应自觉地注意数量的变化。如,1500米跑多少秒?其中的每一圈(400米)又跑几秒?一周到底练多少时间?个人的竞技状态周期规律是怎样的?个人晨脉的规律是怎样的?某技术在训练中的命中率是多少,比赛中又是多少?一场、一局比赛的比分起伏是怎样的?切忌什么都是含含糊糊。
  档案意识。教练员和运动员应自觉地建立业务档案,坚持写好训练日记、阶段小结及年终总结。每年比赛胜负各多少场?身体素质或技术测验的具体成绩是多少,都应分门别类记录清楚。
posted @ 2008-02-28 22:48 hk2000c 阅读(316) | 评论 (0)编辑 收藏

乒乓球基本技术动作口诀

  一、头诀
  口诀不能罗万象,仅把要点来提供, 速度旋转多变化,有赖触类能旁通。
  二、准备姿势
  立足肩宽微提踵,屈膝弯腰莫挺胸, 拍置腹前眼注视,准备移动体放松。
  三、发球
  伸掌抛球向上空,球落击法有多种, 上下侧旋擦球面,长短轻急力不同。
  四、接发球
  “准备姿势”接发球,来球旋转反向送, 上旋推挡下旋搓,长抽短吊争抢攻。
  五、正手攻球
  切忌抬肘握拍松,前臂向前向上动, 左脚稍前体右转,倾拍一般击球中。
  六、反手攻球
  前臂搁腹臂贴胸,肘为轴心臂腕动, 左脚移后腰左转,倾拍斜击球上中。
  七、推挡球
   推挡多用反手方,动作犹似反手攻, 前臂发力向前下,倾拍推挡球上中。
  八、搓球
  拍先稍仰后平送,向前摩擦球下中, 手腕配合小臂动,球转不转靠腕动。
  九、削球
  判断来球先移动,屈腿转体引拍送, 先仰后平擦球底,向前下作弧形动。
  十、弧圈球
  挥拍力向前上冲,薄擦球面位上中, 越薄越转成弧圈,擦面要宽力加重。
  十一、回击弧圈球
  倾拍盖住球上中、及时调拍心放松, 削击移拍上而下,短促截球位偏中。
  十二、放高球
  离台抖切球下中,高抛物线前上送, 远吊对方左右角,越高、远、转越成功。
  十三、杀高球
  球弹高起莫着急,移位待落额上空, 手臂环转向前压,亦可斜砍球侧中。  
  十四、滑板球
  拍向右前似击球,手腕突转向左送, 斜擦球左侧力抽,声东击西奏奇功。
  十五、短球
  对方离台宜短吊,貌似长拍宜轻送, 拍平减力轻递球,竖板轻挡亦可用。
  十六、步法
  练球切忌丢步法,击球应先步移动, 先动后打是关键,步法混乱手法空

posted @ 2008-02-28 22:37 hk2000c 阅读(372) | 评论 (0)编辑 收藏

(一)正手发奔球
  1、 特点 球速急、落点长、冲力大,发至对方右大角或中左位置,对对方威胁较大。
  2、 要点①抛球不宜太高;②提高击球瞬间的挥拍速度;③第一落点要靠近本方台面的端线;④击球点与网同高或稍低于网。

  (二)反手发急球与发急下旋球
  1、 特点 球速快、弧线低,前冲大,迫使对方后退接球,有利于抢攻,常与发急下旋球配合使用。
  2、 要点①击球点应在身体的左前侧与网同高或比网稍低;②注意手腕的抖动发力;③第一落点在本方台区的端线附近。

  (三)发短球
  1、 特点 击球动作小,出手快,球落到对方台面后的第二跳下不出台,使对方不易发力抢拉、冲或抢攻。
  2、 要点 ①抛球不宜太高;②击球时,手腕的力量大于前臂的力量;③发球的第一落点在球台中区,不要离网太近;④发球动作尽可能与发长球相似,使对方不易判断。

  (四)正手发转与不转球
  1、 特点 球速较慢,前冲力小,主要用相似的发球动作,制造旋转变化去迷惑对方,造成对方接发球失误或为自己抢攻创造机会。
  2、 要点①抛球不宜太高;②发转球时,拍面稍后抑,切球的中下部;越是加转球,越应注意手臂的前送动作;③发不转球时,击球瞬间减小拍面后仰角度,增加前推的力量。

  (五)正手发左侧上(下)旋球
  1、 特点 左侧上(下)旋转力较强,对方挡球时向其右侧上(下)方反弹,一般站在中线偏左或侧身发球。
  2、 要点:①发球时要收腹,击球点不可远离身体;②尽量加大由右向左挥动的幅度和弧线,以增强侧旋强度。③发左侧上旋时,击球瞬间手腕快速内收,球拍从球的正中向左上方摩擦。④发左侧下旋时,拍面稍后仰,球拍从球的中下部向左下方摩擦。

  (六) 反手发右侧上(下)旋球
  1. 特点 右侧上(下)旋球力强,对方挡住后,向其左侧上(下)反弹。发球落点以左方斜线长球配合中右近网短球为佳。
  2. 要点 ①注意收腹和转腰动作;②充分利用手腕转动配合前臂发力;③发右侧上旋球时,击球瞬间球拍从球的中部向右上方摩擦,手腕有一个上勾动作;④发右侧下旋球时,拍面稍后仰,击球瞬间球拍从球的中下部向右侧下摩擦。

  (七)下蹲发球
  1.特点 下蹲发球属于上手类发球,我国运动员早在50年代就开始使用。横拍选手发下蹲球比直拍选手方便些,直拍选手发球时需变化握拍方法,即将食指移放到球拍的背面。下蹲发球可以发出左侧旋和右侧旋,在对方不适应的情况下,威胁很大,关键时候发出高质量的球,往往能直接得分。
  2. 要点①注意抛球和挥拍击球动作的配合,掌握好击球时间。②发球要有质量,发球动作要利落,以防在还未完全站起时已被对方抢攻③发下蹲右侧上、下旋球时,左脚稍前,身体略向右偏转,挥拍路线为从左后方向右前方。拍触球中部向右侧上摩擦为右侧上旋;从球中下部向右侧下摩擦为右侧下旋。④发下蹲左侧上、下旋球时,站位稍平,身体基本正对球台,挥拍路线为从右后方向左前方。拍触球右中部向左上方摩擦为左侧上旋;从球中部向左下部摩擦为左侧下旋。⑤发左(右)侧上、下旋球时,要特别注意快速做半圆形摩擦球的动作。

  (八)正手高抛发球
  1、 特点 最显著的特点是抛球高,增大了球下降时对拍的正压力,发出的球速度快,冲力大,旋转变化多,着台后拐弯飞行。但高抛发球动作复杂,有一定的难度。
  2、 要点:①抛球勿离台及身体太远。②击球点与网同高或比网稍低,在近腰的中右处(15厘米)为好③尽量加大向内摆动的幅度和弧线。④发左侧上、下旋球与低抛发球同。⑤触球后,附加一个向右前方的回收动作,可增加对方的判断(结合发右侧旋球,更有威力)。

posted @ 2008-02-05 13:13 hk2000c 阅读(289) | 评论 (0)编辑 收藏

 接连看了几场中欧女子乒乓球对抗赛,对欧洲女队鲍罗斯的反手拉球在这次系列比赛中的运用效果印象颇深。尽管原来也见过她的拉球,感觉很有一些男子运动员的味道,但每当与中国人对抗,由于对中国运动员的节奏适应不好,其力量和速度都难以发挥,能用上的时候也很少。

  仔细观察鲍罗斯的反手拉球,大致上主要用于以下三种情况:一是反手位的第一板上手,二是正手变反手位时,三是反手位的机会比较好时。而这三种球也恰恰是中国运动员,特别是水平不高的运动员常感到很别扭的球。

  中国队的打法历来讲究积极主动,体现在打法上的最明显特征就是突出正手单面进攻的使用率和杀伤力。曾有一段时间,欧洲人在比较清楚地把握了中国人的惯性套路后,以接发球“晃撇”反手底线长球和“调右压左”,从正手突破的战术,一度使中国队陷入非常被动的境地,常常令中国队“拨”不出手来,难以形成有效的正手单面攻,造成失利。所以,无论是横板快攻还是弧圈打法,在训练过程中,掌握一板比较高质量的反手拉是非常有必要的,可以为发挥正手的杀伤力创造更多的机会。

  谈横板反手拉球,不能不说一说它的基本要领。常有业余爱好者问,正手拉球时,一脚在前,一脚在后,那么反手拉球时,是否需要进行重心交换呢?可以说,其基本的道理是差不多的。但反手拉球由于受到身体解剖结构的限制,不能像正手拉球时过于侧向的站位,一般要接近于两脚的平行,只要能够完成基本的重心交换即可。在拉球引拍时,也不能像正手拉一样有很大的幅度,适当地把肘关节抬起来一点,以增加手臂的发力距离,这是与正手拉本质的区别;在接触球的一瞬间,重心往上拨,尽可能向前发力,以使球体现足够的向前质量。

  在练习和运用反手拉球过程中,要注意,作为具有中国人特点打法的运动员,其进攻和上手的指导思想都应建立在正手的基础上,再运用反手,千万不能形成左来左打,右来右打的打法风格,有失积极意义,毕竟正手和反手的杀伤力是不同的,这也是中国队在长期的实践中摸索的打法风格的成功经验之一。但作为辅助技术手段,鲍罗斯反手拉球的运用和所产生的效果是值得我们借鉴和学习的。
posted @ 2008-02-05 13:10 hk2000c 阅读(1109) | 评论 (0)编辑 收藏

这样可以做到对所有对象的安全性控制。
可以扩展到一切的业务对象。
包括基础方法,操作。
甚至安全对象本身
有点神学的味道。

可能是因为采用其他被创造者不知道的机制,所以被创造者无法理解和知晓。



posted @ 2008-01-24 00:04 hk2000c 阅读(254) | 评论 (0)编辑 收藏

您认为把 NIO 和 Servlet API 组合在一起是不可能的?请再好好想一下。在本文中,Java 开发人员 Taylor Cowan 向您展示了如何把生产者/消费者模型应用到消费者非阻塞 I/O,从而轻松地让 Servlet API 全新地兼容 NIO。在这个过程中,您将会看到采用了什么来创建实际的基于 Servlet 并实现了 NIO 的 Web 服务器;您也将发现在企业环境中,那个服务器是如何以标准的 Java I/O 服务器(Tomcat 5.0)为基础而创建的。

NIO 是带有 JDK 1.4 的 Java 平台的最有名(如果不是最出色的)的添加部分之一。下面的许多文章阐述了 NIO 的基本知识及如何利用非阻塞通道的好处。但它们所遗漏的一件事正是,没有充分地展示 NIO 如何可以提高 J2EE Web 层的可伸缩性。对于企业开发人员来说,这些信息特别密切相关,因为实现 NIO 不像把少数几个 import 语句改变成一个新的 I/O 包那样简单。首先,Servlet API 采用阻塞 I/O 语义,因此默认情况下,它不能利用非阻塞 I/O。其次,不像 JDK 1.0 中那样,线程不再是“资源独占”(resource hog),因此使用较少的线程不一定表明服务器可以处理更多的客户机。

在本文中,为了创建基于 Servlet 并实现了 NIO 的 Web 服务器,您将学习如何解决 Servlet API 与非阻塞 I/O 的不配合问题。我们将会看到在多元的 Web 服务器环境中,这个服务器是如何针对标准 I/O 服务器(Tomcat 5.0)进行伸缩的。为符合企业中生存期的事实,我们将重点放在当保持 socket 连接的客户机数量以指数级增长时,NIO 与标准 I/O 相比较的情况如何。

注意,本文针对某些 Java 开发人员,他们已经熟悉了 Java 平台上 I/O 编程的基础知识。有关非阻塞 I/O 的介绍,请参阅 参考资料 部分。

线程不再昂贵

大家都知道,线程是比较昂贵的。在 Java 平台的早期(JDK 1.0),线程的开销是一个很大负担,因此强制开发人员自定义生成解决方案。一个常见的解决方案是使用 VM 启动时创建的线程池,而不是按需创建每个新线程。尽管最近在 VM 层上提高了线程的性能,但标准 I/O 仍然要求分配惟一的线程来处理每个新打开的 socket。就短期而言,这工作得相当不错,但当线程的数量增加超过了 1K,标准 I/O 的不足就表现出来了。由于要在线程间进行上下文切换,因此 CPU 简直变成了超载。

由于 JDK 1.4 中引入了 NIO,企业开发人员最终有了“单线程”模型的一个内置解决方案:多元 I/O 使得固定数量的线程可以服务不断增长的用户数量。

多路复用(Multiplexing)指的是通过一个载波来同时发送多个信号或流。当使用手机时,日常的多路复用例子就发生了。无线频率是稀有的资源,因此无线频率提供商使用多路复用技术通过一个频率发送多个呼叫。在一个例子中,把呼叫分成一些段,然后给这些段很短的持续时间,并在接收端重新装配。这就叫做 时分多路复用(time-division multiplexing),即 TDM。

在 NIO 中,接收端相当于“选择器”(参阅 java.nio.channels.Selector )。不是处理呼叫,选择器是处理多个打开的 socket。就像在 TDM 中那样,选择器重新装配从多个客户机写入的数据段。这使得服务器可以用单个线程管理多个客户机。





回页首


Servlet API 和 NIO

对于 NIO,非阻塞读写是必要的,但它们并不是完全没有麻烦。除了不会阻塞之外,非阻塞读不能给呼叫方任何保证。客户机或服务器应用程序可能读取完整信息、部分消息或者根本读取不到消息。另外,非阻塞读可能读取到太多的消息,从而强制为下一个呼叫准备一个额外的缓冲区。最后,不像流那样,读取了零字节并不表明已经完全接收了消息。

这些因素使得没有轮询就不可能实现甚至是简单的 readline 方法。所有的 servlet 容器必须在它们的输入流上提供 readline 方法。因此,许多开发人员放弃了创建基于 Servlet 并实现了 NIO 的 Web 应用程序服务器。不过这里有一个解决方案,它组合了 Servlet API 和 NIO 的多元 I/O 的能力。

在下面的几节中,您将学习如何使用 java.io.PipedInputPipedOutputStream 类来把生产者/消费者模型应用到消费者非阻塞 I/O。当读取非阻塞通道时,把它写到正由第二个线程消费的管道。注意,这种分解映射线程不同于大多数基于 Java 的客户机/服务器应用程序。这里,我们让一个线程单独负责处理非阻塞通道(生产者),让另一个线程单独负责把数据作为流消费(消费者)。管道也为应用程序服务器解决了非阻塞 I/O 问题,因为 servlet 在消费 I/O 时将采用阻塞语义。





回页首


示例服务器

示例服务器展示了 Servlet API 和 NIO 不兼容的生产者/消费者解决方案。该服务器与 Servlet API 非常相似,可以为成熟的基于 NIO 应用程序服务器提供 POC (proof of concept),是专门编写来衡量 NIO 相对于标准 Java I/O 的性能的。它处理简单的 HTTP get 请求,并支持来自客户机的 Keep-Alive 连接。这是重要的,因为多路复用 I/O 只证明在要求服务器处理大量打开的 scoket 连接时是有意的。

该服务器被分成两个包: org.sse.serverorg.sse.http 包中有提供主要 服务器 功能的类,比如如下的一些功能:接收新客户机连接、阅读消息和生成工作线程以处理请求。 http 包支持 HTTP 协议的一个子集。详细阐述 HTTP 超出了本文的范围。有关实现细节,请从 参考资料 部分下载代码示例。

现在让我们来看一下 org.sse.server 包中一些最重要的类。





回页首


Server 类

Server 类拥有多路复用循环 —— 任何基于 NIO 服务器的核心。在清单 1 中,在服务器接收新客户机或检测到正把可用的字节写到打开的 socket 前, select() 的调用阻塞了。这与标准 Java I/O 的主要区别是,所有的数据都是在这个循环中读取的。通常会把从特定 socket 中读取字节的任务分配给一个新线程。使用 NIO 选择器事件驱动方法,实际上可以用单个线程处理成千上万的客户机,不过,我们还会在后面看到线程仍有一个角色要扮演。

每个 select() 调用返回一组事件,指出新客户机可用;新数据准备就绪,可以读取;或者客户机准备就绪,可以接收响应。server 的 handleKey() 方法只对新客户机( key.isAcceptable() )和传入数据 ( key.isReadable() ) 感兴趣。到这里,工作就结束了,转入 ServerEventHandler 类。


清单 1. Server.java 选择器循环
public void listen() {
            SelectionKey key = null;
            try {
            while (true) {
            selector.select();
            Iterator it = selector.selectedKeys().iterator();
            while (it.hasNext()) {
            key = (SelectionKey) it.next();
            handleKey(key);
            it.remove();
            }
            }
            } catch (IOException e) {
            key.cancel();
            } catch (NullPointerException e) {
            // NullPointer at sun.nio.ch.WindowsSelectorImpl, Bug: 4729342
            e.printStackTrace();
            }
            }
            





回页首


ServerEventHandler 类

ServerEventHandler 类响应服务器事件。当新客户机变为可用时,它就实例化一个新的 Client 对象,该对象代表了那个客户机的状态。数据是以非阻塞方式从通道中读取的,并被写到 Client 对象中。 ServerEventHandler 对象也维护请求队列。为了处理(消费)队列中的请求,生成了不定数量的工作线程。在传统的生产者/消费者方式下,为了在队列变为空时线程会阻塞,并在新请求可用时线程会得到通知,需要写 Queue

为了支持等待的线程,在清单 2 中已经重写了 remove() 方法。如果列表为空,就会增加等待线程的数量,并阻塞当前线程。它实质上提供了非常简单的线程池。


清单 2. Queue.java
public class Queue extends LinkedList
            {
            private int waitingThreads = 0;
            public synchronized void insert(Object obj)
            {
            addLast(obj);
            notify();
            }
            public synchronized Object remove()
            {
            if ( isEmpty() ) {
            try	{ waitingThreads++; wait();}
            catch (InterruptedException e)  {Thread.interrupted();}
            waitingThreads--;
            }
            return removeFirst();
            }
            public boolean isEmpty() {
            return 	(size() - waitingThreads <= 0);
            }
            }
            

工作线程的数量与 Web 客户机的数量无关。不是为每个打开的 socket 分配一个线程,相反,我们把所有请求放到一个由一组 RequestHandlerThread 实例所服务的通用队列中。理想情况下,线程的数量应该根据处理器的数量和请求的长度或持续时间进行调整。如果请求通过资源或处理需求花了很长时间,那么通过添加更多的线程,可以提高感知到的服务质量。

注意,这不一定提高整体的吞吐量,但确实改善了用户体验。即使在超载的情况下,也会给每个线程一个处理时间片。这一原则同样适用于基于标准 Java I/O 的服务器;不过这些服务器是受到限制的,因为会 要求 它们为每个打开的 socket 连接分配一个线程。NIO 服务器完全不用担心这一点,因此它们可以扩展到大量用户。最后的结果是 NIO 服务器仍然需要线程,只是不需要那么多。





回页首


请求处理

Client 类有两个用途。首先,通过把传入的非阻塞 I/O 转换成可由 Servlet API 消费的阻塞 InputStream ,它解决了阻塞/非阻塞问题。其次,它管理特定客户机的请求状态。因为当全部读取消息时,非阻塞通道没有给出任何提示,所以强制我们在协议层处理这一情况。 Client 类在任意指定的时刻都指出了它是否正在参与进行中的请求。如果它准备处理新请求, write() 方法就会为请求处理而将该客户机排到队列中。如果它已经参与了请求,它就只是使用 PipedInputStreamPipedOutputStream 类把传入的字节转换成一个 InputStream

图 1 展示了两个线程围绕管道进行交互。主线程把从通道读取的数据写到管道中。管道把相同的数据作为 InputStream 提供给消费者。管道的另一个重要特性是:它是进行缓冲处理的。如果没有进行缓冲处理,主线程在尝试写到管道时就会阻塞。因为主线程单独负责所有客户机间的多路复用,因此我们不能让它阻塞。


图 1. PipedInput/OutputStream
关系的图形表示

Client 自己排队后,工作线程就可以消费它了。 RequestHandlerThread 类承担了这个角色。至此,我们已经看到主线程是如何连续地循环的,它要么接受新客户机,要么读取新的 I/O。工作线程循环等待新请求。当客户机在请求队列上变为可用时,它就马上被 remove() 方法中阻塞的第一个等待线程所消费。


清单 3. RequestHandlerThread.java
public void run() {
            while (true) {
            Client client = (Client) myQueue.remove();
            try {
            for (; ; ) {
            HttpRequest req = new HttpRequest(client.clientInputStream,
            myServletContext);
            HttpResponse res = new HttpResponse(client.key);
            defaultServlet.service(req, res);
            if (client.notifyRequestDone())
            break;
            }
            } catch (Exception e) {
            client.key.cancel();
            client.key.selector().wakeup();
            }
            }
            }
            

然后该线程创建新的 HttpRequestHttpResponse 实例,并调用 defaultServlet 的 service 方法。注意, HttpRequest 是用 Client 对象的 clientInputStream 属性构造的。 PipedInputStream 就是负责把非阻塞 I/O 转换成阻塞流。

从现在开始,请求处理就与您在 J2EE Servlet API 中期望的相似。当对 servlet 的调用返回时,工作线程在返回到池中之前,会检查是否有来自相同客户机的另一个请求可用。注意,这里用到了单词 池 (pool)。事实上,线程会对队列尝试另一个 remove() 调用,并变成阻塞,直到下一个请求可用。





回页首


运行示例

示例服务器实现了 HTTP 1.1 协议的一个子集。它处理普通的 HTTP get 请求。它带有两个命令行参数。第一个指定端口号,第二个指定 HTML 文件所驻留的目录。在解压文件后, 切换到项目目录,然后执行下面的命令,注意要把下面的 webroot 目录替换为您自己的目录:

java -cp bin org.sse.server.Start 8080
            "C:\mywebroot"
            

还请注意,服务器并没有实现目录清单,因此必须指定有效的 URL 来指向您的 webroot 目录下的文件。





回页首


性能结果

示例 NIO 服务器是在重负载下与 Tomcat 5.0 进行比较的。选择 Tomcat 是因为它是基于标准 Java I/O 的纯 Java 解决方案。为了提高可伸缩性,一些高级的应用程序服务器是用 JNI 本机代码优化的,因此它们没有提供标准 I/O 和 NIO 之间的很好比较。目标是要确定 NIO 是否给出了大量的性能优势,以及是在什么条件下给出的。

如下是一些说明:

  • Tomcat 是用最大的线程数量 2000 来配置的,而示例服务器只允许用 4 个工作线程运行。
  • 每个服务器是针对相同的一组简单 HTTP get 测试的,这些 HTTP get 基本上由文本内容组成。

  • 把加载工具(Microsoft Web Application Stress Tool)设置为使用“Keep-Alive”会话,导致了大约要为每个用户分配一个 socket。然后它导致了在 Tomcat 上为每个用户分配一个线程,而 NIO 服务器用固定数量的线程来处理相同的负载。

图 2 展示了在不断增加负载下的“请求/秒”率。在 200 个用户时,性能是相似的。但当用户数量超过 600 时,Tomcat 的性能开始急剧下降。这最有可能是由于在这么多的线程间切换上下文的开销而导致的。相反,基于 NIO 的服务器的性能则以线性方式下降。记住,Tomcat 必须为每个用户分配一个线程,而 NIO 服务器只配置有 4 个工作线程。


图 2. 请求/秒
关系的图形表示

图 3 进一步显示了 NIO 的性能。它展示了操作的 Socket 连接错误数/分钟。同样,在大约 600 个用户时,Tomcat 的性能急剧下降,而基于 NIO 的服务器的错误率保持相对较低。


图 3. Socket 连接错误数/分钟
关系的图形表式




回页首



结束语

在本文中您已经学习了,实际上可以使用 NIO 编写基于 Servlet 的 Web 服务器,甚至可以启用它的非阻塞特性。对于企业开发人员来说,这是好消息,因为在企业环境中,NIO 比标准 Java I/O 更能够进行伸缩。不像标准的 Java I/O,NIO 可以用固定数量的线程处理许多客户机。当基于 Servlet 的 NIO Web 服务器用来处理保持和拥有 socket 连接的客户机时,会获得更好的性能。



参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.

  • 下载本文中使用的 源代码



  • 看看“ Merlin 给 Java 平台带来了非阻塞 I/O”( developerWorks,2002 年 3 月),获得 NIO 语义的进一步知识。



  • 综合的 developerWorks 教程“ NIO 入门”( developerWorks,2003 年 11 月)从高级的概念到底层的编程细节,详细论及了 NIO 库。



  • Merlin Hughes 的由两部分组成的文章“ Turning streams inside out”( developerWorks,2002 年 9 月)为 Java I/O(标准版和 NIO 版)的一些普遍的挑战提供了制作精巧的设计解决方案。



  • 为获取有关 Java I/O 问题的一些背景知识,请参阅 Allen Holub 的“ 关于解决 Java 编程语言线程问题的建议 ”( developerWorks,2000 年 10 月)。



  • 访问 NIO 主页,从资源中学习非阻塞 I/O。



  • JavaNIO.info 是查找有关 NIO 的资源的理想地方。



  • 为从书本系统学习 NIO,请参阅该领域的经典著作:Ron Hitchens 的 Java NIO(O'Reilly & Associates,2002 年)。



  • 在 developerWorks Java 技术专区可以找到有关 Java 编程各个方面的文章。



  • 访问 Developer Bookstore,获取技术书籍的完整列表,其中包括数百本 Java 相关的图书



  • 也请参阅 Java 技术专区教程页,从 developerWorks 获取免费的 Java 专门教程的完整列表。




关于作者

 

Taylor Cowan 是一位软件工程师,也是一位专攻 J2EE 的自由撰稿人。他从 North Texas 大学的计算机科学专业获得了硕士学位,另外,他还从 Jazz Arranging 获得了音乐学士学位。

posted @ 2008-01-03 22:58 hk2000c 阅读(413) | 评论 (0)编辑 收藏


    一种是继承自Thread类.Thread 类是一个具体的类,即不是抽象类,该类封装了线程的行为。要创建一个线程,程序员必须创建一个从 Thread 类导出的新类。程序员通过覆盖 Thread 的 run() 函数来完成有用的工作用户并不直接调用此函数;而是通过调用 Thread 的 start() 函数,该函数再调用 run()。
   
    例如:

 

    public class Test extends Thread{
      public Test(){
      }
      public static void main(String args[]){
        Test t1 = new Test();
        Test t2 = new Test();
        t1.start();
        t2.start();
      }
      public void run(){
        //do thread's things
      }
    }

 



    
    另一种是实现Runnable接口,此接口只有一个函数,run(),此函数必须由实现了此接口的类实现。
   
    例如:

 

    public class Test implements Runnable{
      Thread thread1;
      Thread thread2;
      public Test(){
        thread1 = new Thread(this,"1");
        thread2 = new Thread(this,"2");
      }
      public static void main(String args[]){
        Test t = new Test();
        t.startThreads();
      }
      public void run(){
        //do thread's things
      }
      public void startThreads(){
        thread1.start();
        thread2.start();
      }
    }

    两种创建方式看起来差别不大,但是弄不清楚的话,也许会将你的程序弄得一团糟。两者区别有以下几点:

1.当你想继承某一其它类时,你只能用后一种方式.

2.第一种因为继承自Thread,只创建了自身对象,但是在数量上,需要几个线程,就得创建几个自身对象;第二种只创建一个自身对象,却创建几个Thread对象.而两种方法重大的区别就在于此,请你考虑:如果你在第一种里创建数个自身对象并且start()后,你会发现好像synchronized不起作用了,已经加锁的代码块或者方法居然同时可以有几个线程进去,而且同样一个变量,居然可以有好几个线程同时可以去更改它。(例如下面的代码)这是因为,在这个程序中,虽然你起了数个线程,可是你也创建了数个对象,而且,每个线程对应了每个对象也就是说,每个线程更改和占有的对象都不一样,所以就出现了同时有几个线程进入一个方法的现象,其实,那也不是一个方法,而是不同对象的相同的方法。所以,这时候你要加锁的话,只能将方法或者变量声明为静态,将static加上后,你就会发现,线程又能管住方法了,同时不可能有两个线程进入同样一个方法,那是因为,现在不是每个对象都拥有一个方法了,而是所有的对象共同拥有一个方法,这个方法就是静态方法。

    而你如果用第二种方法使用线程的话,就不会有上述的情况,因为此时,你只创建了一个自身对象,所以,自身对象的属性和方法对于线程来说是共有的。

    因此,我建议,最好用后一种方法来使用线程。

public class mainThread extends Thread{
  int i=0;
  public static void main(String args[]){
    mainThread m1 = new mainThread();
    mainThread m2 = new mainThread();
    mainThread m3 = new mainThread();
    mainThread m4 = new mainThread();
    mainThread m5 = new mainThread();
    mainThread m6 = new mainThread();
    m1.start();
    m2.start();
    m3.start();
    m4.start();
    m5.start();
    m6.start();
  }
  public synchronized void t1(){
    i=++i;
    try{
      Thread.sleep(500);
    }
    catch(Exception e){}
    //每个线程都进入各自的t1()方法,分别打印各自的i
    System.out.println(Thread.currentThread().getName()+" "+i);
  }
  public void run(){
    synchronized(this){
      while (true) {
        t1();
      }
    }
  }
}

 

 


 

 

    下面我们来讲synchronized的4种用法吧:

    1.方法声明时使用,放在范围操作符(public等)之后,返回类型声明(void等)之前.即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入.
 
      例如:

      public synchronized void synMethod() {
        //方法体
      }

    2.对某一代码块使用,synchronized后跟括号,括号里是变量,这样,一次只有一个线程进入该代码块.例如:

      public int synMethod(int a1){
        synchronized(a1) {
          //一次只能有一个线程进入
        }
      }
    3.synchronized后面括号里是一对象,此时,线程获得的是对象锁.例如:

public class MyThread implements Runnable {
  public static void main(String args[]) {
    MyThread mt = new MyThread();
    Thread t1 = new Thread(mt, "t1");
    Thread t2 = new Thread(mt, "t2");
    Thread t3 = new Thread(mt, "t3");
    Thread t4 = new Thread(mt, "t4");
    Thread t5 = new Thread(mt, "t5");
    Thread t6 = new Thread(mt, "t6");
    t1.start();
    t2.start();
    t3.start();
    t4.start();
    t5.start();
    t6.start();
  }

  public void run() {
    synchronized (this) {
      System.out.println(Thread.currentThread().getName());
    }
  }
}


 
    对于3,如果线程进入,则得到对象锁,那么别的线程在该类所有对象上的任何操作都不能进行.在对象级使用锁通常是一种比较粗糙的方法。为什么要将整个对象都上锁,而不允许其他线程短暂地使用对象中其他同步方法来访问共享资源?如果一个对象拥有多个资源,就不需要只为了让一个线程使用其中一部分资源,就将所有线程都锁在外面。由于每个对象都有锁,可以如下所示使用虚拟对象来上锁:

class FineGrainLock {

   MyMemberClass x, y;
   Object xlock = new Object(), ylock = new Object();

   public void foo() {
      synchronized(xlock) {
         //access x here
      }

      //do something here - but don't use shared resources

      synchronized(ylock) {
         //access y here
      }
   }

   public void bar() {
      synchronized(this) {
         //access both x and y here
      }
      //do something here - but don't use shared resources
   }
}

 

    4.synchronized后面括号里是类.例如:

class ArrayWithLockOrder{
  private static long num_locks = 0;
  private long lock_order;
  private int[] arr;

  public ArrayWithLockOrder(int[] a)
  {
    arr = a;
    synchronized(ArrayWithLockOrder.class) {//-----------------------------------------这里
      num_locks++;             // 锁数加 1。
      lock_order = num_locks;  // 为此对象实例设置唯一的 lock_order。
    }
  }
  public long lockOrder()
  {
    return lock_order;
  }
  public int[] array()
  {
    return arr;
  }
}

class SomeClass implements Runnable
{
  public int sumArrays(ArrayWithLockOrder a1,
                       ArrayWithLockOrder a2)
  {
    int value = 0;
    ArrayWithLockOrder first = a1;       // 保留数组引用的一个
    ArrayWithLockOrder last = a2;        // 本地副本。
    int size = a1.array().length;
    if (size == a2.array().length)
    {
      if (a1.lockOrder() > a2.lockOrder())  // 确定并设置对象的锁定
      {                                     // 顺序。
        first = a2;
        last = a1;
      }
      synchronized(first) {              // 按正确的顺序锁定对象。
        synchronized(last) {
          int[] arr1 = a1.array();
          int[] arr2 = a2.array();
          for (int i=0; i<size; i++)
            value += arr1[i] + arr2[i];
        }
      }
    }
    return value;
  }
  public void run() {
    //...
  }
}

 

    对于4,如果线程进入,则线程在该类中所有操作不能进行,包括静态变量和静态方法,实际上,对于含有静态方法和静态变量的代码块的同步,我们通常用4来加锁.

以上4种之间的关系:

    锁是和对象相关联的,每个对象有一把锁,为了执行synchronized语句,线程必须能够获得synchronized语句中表达式指定的对象的锁,一个对象只有一把锁,被一个线程获得之后它就不再拥有这把锁,线程在执行完synchronized语句后,将获得锁交还给对象。
    在方法前面加上synchronized修饰符即可以将一个方法声明为同步化方法。同步化方法在执行之前获得一个锁。如果这是一个类方法,那么获得的锁是和声明方法的类相关的Class类对象的锁。如果这是一个实例方法,那么此锁是this对象的锁。

 


 

  下面谈一谈一些常用的方法:

  wait(),wait(long),notify(),notifyAll()等方法是当前类的实例方法,
   
        wait()是使持有对象锁的线程释放锁;
        wait(long)是使持有对象锁的线程释放锁时间为long(毫秒)后,再次获得锁,wait()和wait(0)等价;
        notify()是唤醒一个正在等待该对象锁的线程,如果等待的线程不止一个,那么被唤醒的线程由jvm确定;
        notifyAll是唤醒所有正在等待该对象锁的线程.
        在这里我也重申一下,我们应该优先使用notifyAll()方法,因为唤醒所有线程比唤醒一个线程更容易让jvm找到最适合被唤醒的线程.

    对于上述方法,只有在当前线程中才能使用,否则报运行时错误java.lang.IllegalMonitorStateException: current thread not owner.

 


 

    下面,我谈一下synchronized和wait()、notify()等的关系:

1.有synchronized的地方不一定有wait,notify

2.有wait,notify的地方必有synchronized.这是因为wait和notify不是属于线程类,而是每一个对象都具有的方法,而且,这两个方法都和对象锁有关,有锁的地方,必有synchronized。

另外,请注意一点:如果要把notify和wait方法放在一起用的话,必须先调用notify后调用wait,因为如果调用完wait,该线程就已经不是current thread了。如下例:

/**
 * Title:        Jdeveloper's Java Projdect
 * Description:  n/a
 * Copyright:    Copyright (c) 2001
 * Company:      soho  http://www.ChinaJavaWorld.com
 * @author jdeveloper@21cn.com
 * @version 1.0
 */
import java.lang.Runnable;
import java.lang.Thread;

public class DemoThread
    implements Runnable {

  public DemoThread() {
    TestThread testthread1 = new TestThread(this, "1");
    TestThread testthread2 = new TestThread(this, "2");

    testthread2.start();
    testthread1.start();

  }

  public static void main(String[] args) {
    DemoThread demoThread1 = new DemoThread();

  }

  public void run() {

    TestThread t = (TestThread) Thread.currentThread();
    try {
      if (!t.getName().equalsIgnoreCase("1")) {
        synchronized (this) {
          wait();
        }
      }
      while (true) {

        System.out.println("@time in thread" + t.getName() + "=" +
                           t.increaseTime());

        if (t.getTime() % 10 == 0) {
          synchronized (this) {
            System.out.println("****************************************");
            notify();
            if (t.getTime() == 100)
              break;
            wait();
          }
        }
      }
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }

}

class TestThread
    extends Thread {
  private int time = 0;
  public TestThread(Runnable r, String name) {
    super(r, name);
  }

  public int getTime() {
    return time;
  }

  public int increaseTime() {
    return++time;
  }

}

    下面我们用生产者/消费者这个例子来说明他们之间的关系:

    public class test {
  public static void main(String args[]) {
    Semaphore s = new Semaphore(1);
    Thread t1 = new Thread(s, "producer1");
    Thread t2 = new Thread(s, "producer2");
    Thread t3 = new Thread(s, "producer3");
    Thread t4 = new Thread(s, "consumer1");
    Thread t5 = new Thread(s, "consumer2");
    Thread t6 = new Thread(s, "consumer3");
    t1.start();
    t2.start();
    t3.start();
    t4.start();
    t5.start();
    t6.start();
  }
}

class Semaphore
    implements Runnable {
  private int count;
  public Semaphore(int n) {
    this.count = n;
  }

  public synchronized void acquire() {
    while (count == 0) {
      try {
        wait();
      }
      catch (InterruptedException e) {
        //keep trying
      }
    }
    count--;
  }

  public synchronized void release() {
    while (count == 10) {
      try {
        wait();
      }
      catch (InterruptedException e) {
        //keep trying
      }
    }
    count++;
    notifyAll(); //alert a thread that's blocking on this semaphore
  }

  public void run() {
    while (true) {
      if (Thread.currentThread().getName().substring(0,8).equalsIgnoreCase("consumer")) {
        acquire();
      }
      else if (Thread.currentThread().getName().substring(0,8).equalsIgnoreCase("producer")) {
        release();
      }
      System.out.println(Thread.currentThread().getName() + " " + count);
    }
  }
}

       生产者生产,消费者消费,一般没有冲突,但当库存为0时,消费者要消费是不行的,但当库存为上限(这里是10)时,生产者也不能生产.请好好研读上面的程序,你一定会比以前进步很多.

      上面的代码说明了synchronized和wait,notify没有绝对的关系,在synchronized声明的方法、代码块中,你完全可以不用wait,notify等方法,但是,如果当线程对某一资源存在某种争用的情况下,你必须适时得将线程放入等待或者唤醒.

 
posted @ 2008-01-02 18:14 hk2000c 阅读(355) | 评论 (0)编辑 收藏

2003 年 11 月 24 日

尽管 SSL 阻塞操作――当读写数据的时候套接字的访问被阻塞――与对应的非阻塞方式相比提供了更好的 I/O 错误通知,但是非阻塞操作允许调用的线程继续运行。本文中,作者同时就客户端和服务器端描述了如何使用Java Secure Socket Extensions (JSSE) 和 Java NIO (新 I/O)库创建非阻塞的安全连接,并且介绍了创建非阻塞套接字的传统方法,以及使用JSSE 和 NIO 的一种可选的(必需的)方法。

阻塞,还是非阻塞?这就是问题所在。无论在程序员的头脑中多么高贵……当然这不是莎士比亚,本文提出了任何程序员在编写 Internet 客户程序时都应该考虑的一个重要问题。通信操作应该是阻塞的还是非阻塞的?

许多程序员在使用 Java 语言编写 Internet 客户程序时并没有考虑这个问题,主要是因为在以前只有一种选择――阻塞通信。但是现在 Java 程序员有了新的选择,因此我们编写的每个客户程序也许都应该考虑一下。

非阻塞通信在 Java 2 SDK 的 1.4 版被引入 Java 语言。如果您曾经使用该版本编过程序,可能会对新的 I/O 库(NIO)留下了印象。在引入它之前,非阻塞通信只有在实现第三方库的时候才能使用,而第三方库常常会给应用程序引入缺陷。

NIO 库包含了文件、管道以及客户机和服务器套接字的非阻塞功能。库中缺少的一个特性是安全的非阻塞套接字连接。在 NIO 或者 JSSE 库中没有建立安全的非阻塞通道类,但这并不意味着不能使用安全的非阻塞通信。只不过稍微麻烦一点。

要完全领会本文,您需要熟悉:

  • Java 套接字通信的概念。您也应该实际编写过应用程序。而且不只是打开连接、读取一行然后退出的简单应用程序,应该是实现 POP3 或 HTTP 之类协议的客户机或通信库这样的程序。
  • SSL 基本概念和加密之类的概念。基本上就是知道如何设置一个安全连接(但不必担心 JSSE ――这就是关于它的一个“紧急教程”)。
  • NIO 库。
  • 在您选择的平台上安装 Java 2 SDK 1.4 或以后的版本。(我是在 Windows 98 上使用 1.4.1_01 版。)

如果需要关于这些技术的介绍,请参阅 参考资料部分。

那么到底什么是阻塞和非阻塞通信呢?

阻塞和非阻塞通信

阻塞通信意味着通信方法在尝试访问套接字或者读写数据时阻塞了对套接字的访问。在 JDK 1.4 之前,绕过阻塞限制的方法是无限制地使用线程,但这样常常会造成大量的线程开销,对系统的性能和可伸缩性产生影响。java.nio 包改变了这种状况,允许服务器有效地使用 I/O 流,在合理的时间内处理所服务的客户请求。

没有非阻塞通信,这个过程就像我所喜欢说的“为所欲为”那样。基本上,这个过程就是发送和读取任何能够发送/读取的东西。如果没有可以读取的东西,它就中止读操作,做其他的事情直到能够读取为止。当发送数据时,该过程将试图发送所有的数据,但返回实际发送出的内容。可能是全部数据、部分数据或者根本没有发送数据。

阻塞与非阻塞相比确实有一些优点,特别是遇到错误控制问题的时候。在阻塞套接字通信中,如果出现错误,该访问会自动返回标志错误的代码。错误可能是由于网络超时、套接字关闭或者任何类型的 I/O 错误造成的。在非阻塞套接字通信中,该方法能够处理的唯一错误是网络超时。为了检测使用非阻塞通信的网络超时,需要编写稍微多一点的代码,以确定自从上一次收到数据以来已经多长时间了。

哪种方式更好取决于应用程序。如果使用的是同步通信,如果数据不必在读取任何数据之前处理的话,阻塞通信更好一些,而非阻塞通信则提供了处理任何已经读取的数据的机会。而异步通信,如 IRC 和聊天客户机则要求非阻塞通信以避免冻结套接字。





回页首


创建传统的非阻塞客户机套接字

Java NIO 库使用通道而非流。通道可同时用于阻塞和非阻塞通信,但创建时默认为非阻塞版本。但是所有的非阻塞通信都要通过一个名字中包含 Channel 的类完成。在套接字通信中使用的类是 SocketChannel, 而创建该类的对象的过程不同于典型的套接字所用的过程,如清单 1 所示。


清单 1. 创建并连接 SocketChannel 对象
SocketChannel sc = SocketChannel.open();
            sc.connect("www.ibm.com",80);
            sc.finishConnect();
            

必须声明一个 SocketChannel 类型的指针,但是不能使用 new 操作符创建对象。相反,必须调用 SocketChannel 类的一个静态方法打开通道。打开通道后,可以通过调用 connect() 方法与它连接。但是当该方法返回时,套接字不一定是连接的。为了确保套接字已经连接,必须接着调用 finishConnect()

当套接字连接之后,非阻塞通信就可以开始使用 SocketChannel 类的 read()write() 方法了。也可以把该对象强制转换成单独的 ReadableByteChannelWritableByteChannel 对象。无论哪种方式,都要对数据使用 Buffer 对象。因为 NIO 库的使用超出了本文的范围,我们不再对此进一步讨论。

当不再需要套接字时,可以使用 close() 方法将其关闭:

sc.close();
            

这样就会同时关闭套接字连接和底层的通信通道。





回页首


创建替代的非阻塞的客户机套接字

上述方法比传统的创建套接字连接的例程稍微麻烦一点。不过,传统的例程也能用于创建非阻塞套接字,不过需要增加几个步骤以支持非阻塞通信。

SocketChannel 对象中的底层通信包括两个 Channel 类: ReadableByteChannelWritableByteChannel。 这两个类可以分别从现有的 InputStreamOutputStream 阻塞流中使用 Channels 类的 newChannel() 方法创建,如清单 2 所示:


清单 2. 从流中派生通道
ReadableByteChannel rbc = Channels.newChannel(s.getInputStream());
            WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());
            

Channels 类也用于把通道转换成流或者 reader 和 writer。这似乎是把通信切换到阻塞模式,但并非如此。如果试图读取从通道派生的流,读方法将抛出 IllegalBlockingModeException 异常。

相反方向的转换也是如此。不能使用 Channels 类把流转换成通道而指望进行非阻塞通信。如果试图读从流派生的通道,读仍然是阻塞的。但是像编程中的许多事情一样,这一规则也有例外。

这种例外适合于实现 SelectableChannel 抽象类的类。 SelectableChannel 和它的派生类能够选择使用阻塞或者非阻塞模式。 SocketChannel 就是这样的一个派生类。

但是,为了能够在两者之间来回切换,接口必须作为 SelectableChannel 实现。对于套接字而言,为了实现这种能力必须使用 SocketChannel 而不是 Socket

回顾一下,要创建套接字,首先必须像通常使用 Socket 类那样创建一个套接字。套接字连接之后,使用 清单 2中的两行代码把流转换成通道。


清单 3. 创建套接字的另一种方法
Socket s = new Socket("www.ibm.com", 80);
            ReadableByteChannel rbc = Channels.newChannel(s.getInputStream());
            WriteableByteChannel wbc = Channels.newChannel(s.getOutputStream());
            

如前所述,这样并不能实现非阻塞套接字通信――所有的通信仍然在阻塞模式下。在这种情况下,非阻塞通信必须模拟实现。模拟层不需要多少代码。让我们来看一看。

从模拟层读数据

模拟层在尝试读操作之前首先检查数据的可用性。如果数据可读则开始读。如果没有数据可用,可能是因为套接字被关闭,则返回表示这种情况的代码。在清单 4 中要注意仍然使用了 ReadableByteChannel 读,尽管 InputStream 完全可以执行这个动作。为什么这样做呢?为了造成是 NIO 而不是模拟层执行通信的假象。此外,还可以使模拟层与其他通道更容易结合,比如向文件通道内写入数据。


清单 4. 模拟非阻塞的读操作
/* The checkConnection method returns the character read when
            determining if a connection is open.
            */
            y = checkConnection();
            if(y <= 0) return y;
            buffer.putChar((char ) y);
            return rbc.read(buffer);
            

向模拟层写入数据

对于非阻塞通信,写操作只写入能够写的数据。发送缓冲区的大小和一次可以写入的数据多少有很大关系。缓冲区的大小可以通过调用 Socket 对象的 getSendBufferSize() 方法确定。在尝试非阻塞写操作时必须考虑到这个大小。如果尝试写入比缓冲块更大的数据,必须拆开放到多个非阻塞写操作中。太大的单个写操作可能被阻塞。


清单 5. 模拟非阻塞的写操作
int x, y = s.getSendBufferSize(), z = 0;
            int expectedWrite;
            byte [] p = buffer.array();
            ByteBuffer buf = ByteBuffer.allocateDirect(y);
            /* If there isn't any data to write, return, otherwise flush the stream */
            if(buffer.remaining() == 0) return 0;
            os.flush()
            for(x = 0; x < p.length; x += y)
            {
            if(p.length - x < y)
            {
            buf.put(p, x, p.length - x);
            expectedWrite = p.length - x;
            }
            else
            {
            buf.put(p, x, y);
            expectedWrite = y;
            }
            /* Check the status of the socket to make sure it's still open */
            if(!s.isConnected()) break;
            /* Write the data to the stream, flushing immediately afterward */
            buf.flip();
            z = wbc.write(buf); os.flush();
            if(z < expectedWrite) break;
            buf.clear();
            }
            if(x > p.length) return p.length;
            else if(x == 0) return -1;
            else return x + z;
            

与读操作类似,首先要检查套接字是否仍然连接。但是如果把数据写入 WritableByteBuffer 对象,就像清单 5 那样,该对象将自动进行检查并在没有连接时抛出必要的异常。在这个动作之后开始写数据之前,流必须立即被清空,以保证发送缓冲区中有发送数据的空间。任何写操作都要这样做。发送到块中的数据与发送缓冲区的大小相同。执行清除操作可以保证发送缓冲不会溢出而导致写操作被阻塞。

因为假定写操作只能写入能够写的内容,这个过程还必须检查套接字保证它在每个数据块写入后仍然是打开的。如果在写入数据时套接字被关闭,则必须中止写操作并返回套接字关闭之前能够发送的数据量。

BufferedOutputReader 可用于模拟非阻塞写操作。如果试图写入超过缓冲区两倍长度的数据,则直接写入缓冲区整倍数长度的数据(缓冲余下的数据)。比如说,如果缓冲区的长度是 256 字节而需要写入 529 字节的数据,则该对象将清除当前缓冲区、发送 512 字节然后保存剩下的 17 字节。

对于非阻塞写而言,这并非我们所期望的。我们希望分次把数据写入同样大小的缓冲区中,并最终把全部数据都写完。如果发送的大块数据留下一些数据被缓冲,那么在所有数据被发送的时候,写操作就会被阻塞。

模拟层类模板

整个模拟层可以放到一个类中,以便更容易和应用程序集成。如果要这样做,我建议从 ByteChannel 派生这个类。这个类可以强制转换成单独的 ReadableByteChannelWritableByteChannel 类。

清单 6 给出了从 ByteChannel 派生的模拟层类模板的一个例子。本文后面将一直使用这个类表示通过阻塞连接执行的非阻塞操作。


清单 6. 模拟层的类模板
public class nbChannel implements ByteChannel
            {
            Socket s;
            InputStream is; OutputStream os;
            ReadableByteChannel rbc;
            WritableByteChannel wbc;
            public nbChannel(Socket socket);
            public int read(ByteBuffer dest);
            public int write(ByteBuffer src);
            public void close();
            protected int checkConnection();
            }
            

使用模拟层创建套接字

使用新建的模拟层创建套接字非常简单。只要像通常那样创建 Socket 对象,然后创建 nbChannel 对象就可以了,如清单 7 所示:


清单 7. 使用模拟层
Socket s = new Socket("www.ibm.com", 80);
            nbChannel socketChannel = new nbChannel(s);
            ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
            WritableByteChannel wbc = (WritableByteChannel)socketChannel;
            





回页首


创建传统的非阻塞服务器套接字

服务器端的非阻塞套接字和客户端上的没有很大差别。稍微麻烦一点的只是建立接受输入连接的套接字。套接字必须通过从服务器套接字通道派生一个阻塞的服务器套接字绑定到阻塞模式。清单 8 列出了需要做的步骤。


清单 8. 创建非阻塞的服务器套接字(SocketChannel)
ServerSocketChannel ssc = ServerSocketChannel.open();
            ServerSocket ss = ssc.socket();
            ss.bind(new InetSocketAddress(port));
            SocketChannel sc = ssc.accept();
            

与客户机套接字通道相似,服务器套接字通道也必须打开而不是使用 new 操作符或者构造函数。在打开之后,必须派生服务器套接字对象以便把套接字通道绑定到一个端口。一旦套接字被绑定,服务器套接字对象就可以丢弃了。

通道使用 accept() 方法接收到来的连接并把它们转给套接字通道。一旦接收了到来的连接并转给套接字通道对象,通信就可以通过 read()write() 方法开始进行了。





回页首


创建替代的非阻塞服务器套接字

实际上,并非真正的替代。因为服务器套接字通道必须使用服务器套接字对象绑定,为何不完全绕开服务器套接字通道而仅使用服务器套接字对象呢?不过这里的通信不使用 SocketChannel ,而要使用模拟层 nbChannel。


清单 9. 建立服务器套接字的另一种方法
ServerSocket ss = new ServerSocket(port);
            Socket s = ss.accept();
            nbChannel socketChannel = new nbChannel(s);
            ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
            WritableByteChannel wbc = (WritableByteChannel)socketChannel;
            





回页首


创建 SSL 连接

创建SSL连接,我们要分别从客户端和服务器端考察。

从客户端

创建 SS L连接的传统方法涉及到使用套接字工厂和其他一些东西。我将不会详细讨论如何创建SSL连接,不过有一本很好的教程,“Secure your sockets with JSSE”(请参阅 参考资料),从中您可以了解到更多的信息。

创建 SSL 套接字的默认方法非常简单,只包括几个很短的步骤:

  1. 创建套接字工厂。
  2. 创建连接的套接字。
  3. 开始握手。
  4. 派生流。
  5. 通信。

清单 10 说明了这些步骤:


清单 10. 创建安全的客户机套接字
SSLSocketFactory sslFactory =
            (SSLSocketFactory)SSLSocketFactory.getDefault();
            SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
            ssl.startHandshake();
            InputStream is = ssl.getInputStream();
            OutputStream os = ssl.getOutputStream();
            

默认方法不包括客户验证、用户证书和其他特定连接可能需要的东西。

从服务器端

建立SSL服务器连接的传统方法稍微麻烦一点,需要加上一些类型转换。因为这些超出了本文的范围,我将不再进一步介绍,而是说说支持SSL服务器连接的默认方法。

创建默认的 SSL 服务器套接字也包括几个很短的步骤:

  1. 创建服务器套接字工厂。
  2. 创建并绑定服务器套接字。
  3. 接受传入的连接。
  4. 开始握手。
  5. 派生流。
  6. 通信。

尽管看起来似乎与客户端的步骤相似,要注意这里去掉了很多安全选项,比如客户验证。

清单 11 说明这些步骤:


清单 11. 创建安全的服务器套接字
SSLServerSocketFactory sslssf =
            (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
            SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
            SSLSocket ssls = (SSLSocket)sslss.accept();
            ssls.startHandshake();
            InputStream is = ssls.getInputStream();
            OutputStream os = ssls.getOutputStream();
            





回页首


创建安全的非阻塞连接

要精心实现安全的非阻塞连接,也需要分别从客户端和服务器端来看。

从客户端

在客户端建立安全的非阻塞连接非常简单:

  1. 创建并连接 Socket 对象。
  2. Socket 对象添加到模拟层上。
  3. 通过模拟层通信。

清单 12 说明了这些步骤:


清单 12. 创建安全的客户机连接
/* Create the factory, then the secure socket */
            SSLSocketFactory sslFactory =
            (SSLSocketFactory)SSLSocketFactory.getDefault();
            SSLSocket ssl = (SSLSocket)sslFactory.createSocket(host, port);
            /* Start the handshake.  Should be done before deriving channels */
            ssl.startHandshake();
            /* Put it into the emulation layer and create separate channels */
            nbChannel socketChannel = new nbChannel(ssl);
            ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
            WritableByteChannel wbc = (WritableByteChannel)socketChannel;
            

利用前面给出的 模拟层类 就可以实现非阻塞的安全连接。因为安全套接字通道不能使用 SocketChannel 类打开,而 Java API 中又没有完成这项工作的类,所以创建了一个模拟类。模拟类可以实现非阻塞通信,无论使用安全套接字连接还是非安全套接字连接。

列出的步骤包括默认的安全设置。对于更高级的安全性,比如用户证书和客户验证, 参考资料 部分提供了说明如何实现的文章。

从服务器端

在服务器端建立套接字需要对默认安全稍加设置。但是一旦套接字被接收和路由,设置必须与客户端的设置完全相同,如清单 13 所示:


清单 13. 创建安全的非阻塞服务器套接字
/* Create the factory, then the socket, and put it into listening mode */
            SSLServerSocketFactory sslssf =
            (SSLServerSocketFactory)SSLServerSocketFactory.getDefault();
            SSLServerSocket sslss = (SSLServerSocket)sslssf.createServerSocket(port);
            SSLSocket ssls = (SSLSocket)sslss.accept();
            /* Start the handshake on the new socket */
            ssls.startHandshake();
            /* Put it into the emulation layer and create separate channels */
            nbChannel socketChannel = new nbChannel(ssls);
            ReadableByteChannel rbc = (ReadableByteChannel)socketChannel;
            WritableByteChannel wbc = (WritableByteChannel)socketChannel;
            

同样,要记住这些步骤使用的是默认安全设置。





回页首


集成安全的和非安全的客户机连接

多数 Internet 客户机应用程序,无论使用 Java 语言还是其他语言编写,都需要提供安全和非安全连接。Java Secure Socket Extensions 库使得这项工作非常容易,我最近在编写一个 HTTP 客户库时就使用了这种方法。

SSLSocket 类派生自 Socket。 您可能已经猜到我要怎么做了。所需要的只是该对象的一个 Socket 指针。如果套接字连接不使用SSL,则可以像通常那样创建套接字。如果要使用 SSL,就稍微麻烦一点,但此后的代码就很简单了。清单 14 给出了一个例子:


清单 14. 集成安全的和非安全的客户机连接
Socket s;
            ReadableByteChannel rbc;
            WritableByteChannel wbc;
            nbChannel socketChannel;
            if(!useSSL) s = new Socket(host, port);
            else
            {
            SSLSocketFactory sslsf = SSLSocketFactory.getDefault();
            SSLSocket ssls = (SSLSocket)SSLSocketFactory.createSocket(host, port);
            ssls.startHandshake();
            s = ssls;
            }
            socketChannel = new nbChannel(s);
            rbc = (ReadableByteChannel)socketChannel;
            wbc = (WritableByteChannel)socketChannel;
            ...
            s.close();
            

创建通道之后,如果套接字使用了SSL,那么就是安全通信,否则就是普通通信。如果使用了 SSL,关闭套接字将导致握手中止。

这种设置的一种可能是使用两个单独的类。一个类负责处理通过套接字沿着与非安全套接字的连接进行的所有通信。一个单独的类应该负责创建安全的连接,包括安全连接的所有必要设置,无论是否是默认的。安全类应该直接插入通信类,只有在使用安全连接时被调用。

posted @ 2007-12-31 05:07 hk2000c 阅读(1508) | 评论 (0)编辑 收藏