24.3. 定义动态语言支持的bean

Spring Framework

24.3. 定义动态语言支持的bean

这一节描述了如何针对Spring所支持的动态语言定义受Spring所管理的bean。

请注意本章不会解释这些支持的动态语言的语法和用法。例如,如果你想在你的某个应用中使用Groovy来编写类,我们假设你已经了解Groovy这门语言。如果你需要了解和动态语言本身有关的更多细节,请参考本章末尾第 24.6 节 “更多的资源”一节。

24.3.1. 公共概念

使用dynamic-language-backed bean要经过以下步骤:

  1. 编写针对动态语言源码的测试代码(这是理所应当的事情)

  2. 然后编写动态语言源码 :)

  3. 在XML配置文件中使用相应的<lang:language/>元素定义dynamic-language-backed bean。当然你也可以使用Spring API,以编程的方式来定义---本章并不会涉及到这种高级的配置方式,你可以直接阅读源码来获得相应的指示)。注意这是一个迭代的步骤。每一个动态语言的源文件至少对应一个bean定义(同一个动态语言的源文件当然可以在多个bean定义中引用)。

前面两步(测试并编写动态语言源文件)超出了本章的范畴。请参考你所选动态语言相关的语言规范或者参考手册,并继续开发你的动态语言的源文件。不过你应该首先阅读本章的剩下部分,因为Spring(动态语言支持)对动态语言源文件的内容有一些(小小的)要求。

24.3.1.1. <lang:language/> 元素

最后一步包括如何定义dynamic-language-backed bean定义,每一个要配置的bean对应一个定义(这和普通的Javabean配置没有什么区别)。但是,对于容器中每一个需要实例化和配置的类,普通的Javabean配置需要指定全限定名,对于dynamic language-backed bean则使用<lang:language/>元素取而代之。

每一种支持的语言都有对应的<lang:language/>元素

  • <lang:jruby/>(JRuby)

  • <lang:groovy/>(Groovy)

  • <lang:bsh/>(BeanShell)

对于配置中可用的确切的属性和子元素取决于具体定义bean的语言(后面和特定语言有关的章节会揭示全部内幕)。

24.3.1.2. Refreshable bean

Spring对动态语言支持中最引人注目的价值在于增加了对 'refreshable bean' 特征的支持。

refreshable bean是一种只有少量配置的dynamic-language-backed bean。dynamic-language-backed bean 可以监控底层源文件的变化,一旦源文件发生改变就可以自动重新加载(例如开发者编辑文件并保存修改)。

这样就允许开发者在应用程序中部署任意数量的动态语言源文件,并通过配置Spring容器来创建动态语言源文件所支持的bean(使用本章所描述的机制)。以后如果需求发生改变,或者一些外部因素起了作用,这样就可以简单的编辑动态语言源文件,而这些文件中的变化会反射为bean的变化。而这些工作不需要关闭正在运行的应用(或者重新部署web应用)。dynamic-language-backed bean能够自我修正,从已改变的动态语言源文件中提取新的状态和逻辑。

注意

注意,该特征默认值为off(关闭)

下面让我们看一个例子,体验一下使用refreshable bean是多么容易的事情。首先要启用refreshable bean特征,只需要在bean定义的 <lang:language/>元素中指定一个附加属性。假设我们继续使用前文中的 例子,那么只需要在Spring的XML配置文件中进行如下修改以启用refreshable bean:

<beans>

    <!-- this bean is now 'refreshable' due to the presence of the 'refresh-check-delay' attribute -->
    <lang:groovy id="messenger"
          refresh-check-delay="5000" <!-- switches refreshing on with 5 seconds between checks -->
          script-source="classpath:Messenger.groovy">
        <lang:property name="message" value="I Can Do The Frug" />
    </lang:groovy>

    <bean id="bookingService" class="x.y.DefaultBookingService">
        <property name="messenger" ref="messenger" />
    </bean>

</beans>

这就是所有你需要做的事情。 'messenger' bean定义中的'refresh-check-delay'属性指定了刷新bean的时间间隔,在这个时间段内的底层动态语言源文件的任何变化都会刷新到对应的bean上。通过给该属性赋一个负值即可关闭该刷新行为。注意在默认情况下,该刷新行为是关闭的。如果你不需要该刷新行为,最简单的办法就是不要定义该属性。

运行以下应用程序可以体验refreshable特征:请执行接下来这段代码中的'jumping-through-hoops-to-pause-the-execution'小把戏。System.in.read()的作用是暂停程序的执行,这个时候去修改底层的动态语言源文件,然后程序恢复执行的时候触发dynamic-language-backed bean的刷新。

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {

    public static void main(final String[] args) throws Exception {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        Messenger messenger = (Messenger) ctx.getBean("messenger");
        System.out.println(messenger.getMessage());
        // pause execution while I go off and make changes to the source file...
        System.in.read();
        System.out.println(messenger.getMessage());
    }
}

假设对于这个例子,所有调用Messenger实现中getMessage()方法的地方都被修改:比如将message用引号括起来。下面是我在程序执行暂停的时候对Messenger.groovy源文件所做的修改:

package org.springframework.scripting

class GroovyMessenger implements Messenger {

    private String message = "Bingo"

    public String getMessage() {
        // change the implementation to surround the message in quotes
        return "'" + this.message + "'"
    }

    public void setMessage(String message) {
        this.message = message
    }
}

在这段程序执行的时候,在输入暂停之前的输出是I Can Do The Frug。在修改并保存了源文件之后,程序恢复执行,再次调用dynamic-language-backed MessengergetMessage()方法的结果为'I Can Do The Frug' (注意新增的引号)。

有一点很重要,如果上述对脚本的修改发生在'refresh-check-delay'值的时间范围内并不会触发刷新动作。同样重要的是,修改脚本并不会马上起作用,而是要到该动态语言实现的bean的相应的方法被调用时才有效。只有动态语言实现的bean的方法被调用的时候才会检查底层源文件是否修改了。刷新脚本产生的任何异常(例如发生编译错误,或者脚本文件被删除了)都会在调用的代码中抛出一个致命异常。

前面描述的refreshable bean的行为并会作用于使用<lang:inline-script/>元素定义的动态语言源文件(请参考第 24.3.1.3 节 “内置动态语言源文件”这一节)。而且它作用于那些可以检测到底层源文件发生改变的bean。例如,检查文件系统中的动态语言源文件的最后修改日期。

24.3.1.3. 内置动态语言源文件

Spring动态语言支持还提供了直接在bean定义中直接嵌入动态语言源码的功能。通过<lang:inline-script/>元素,可以在Spring的配置文件中直接定义动态语言源文件。下面的例子或许可以将嵌入脚本特征表达的更清楚:

<lang:groovy id="messenger">
    <lang:inline-script>
package org.springframework.scripting.groovy;

import org.springframework.scripting.Messenger

class GroovyMessenger implements Messenger {

    String message
}
    </lang:inline-script>
    <lang:property name="message" value="I Can Do The Frug" />
</lang:groovy>

直接在Spring的配置文件中定义动态语言源码的是否是最佳实践这个问题先放在一边,<lang:inline-script/>元素在某些场景下还是相当有用的。例如,给Spring MVC的Controller快速添加一个Spring Validator实现。如果采用内置源码的方式只需要片刻时间就可以搞定(请参见第 24.4.2 节 “Validator的脚本化”这一节的示例)。

下面这个例子是一个基于JRuby的bean,这个例子直接在Spring的XML配置文件中定义了源码,并使用了inline: 符号。(注意其中使用 &lt;符号来表示'<'字符。这个例子中如果在内置源码周围定义一个<![CDATA[]]>就更好了。)

<lang:jruby id="messenger" script-interfaces="org.springframework.scripting.Messenger">
    <lang:inline-script>
require 'java'

include_class 'org.springframework.scripting.Messenger'

class RubyMessenger &lt; Messenger

 def setMessage(message)
  @@message = message
 end

 def getMessage
  @@message
 end
 
end
    </lang:inline-script>
    <lang:property name="message" value="Hello World!" />
</lang:jruby>

24.3.1.4. 理解dynamic-language-backed bean上下文中的构造器注入

关于Spring动态语言支持有一个要点必须引起注意:目前对dynamic-language-backed bean还不可能提供构造器参数的支持(也就是说对于dynamic-language-backed bean的构造器注入无效)。 只是为了将构造器和属性的特殊处理100%说清楚,下面混合了代码和配置的例子是无法运作的。

// from the file 'Messenger.groovy'
package org.springframework.scripting.groovy;

import org.springframework.scripting.Messenger

class GroovyMessenger implements Messenger {

    GroovyMessenger() {}

    // this constructor is not available for Constructor Injection
    GroovyMessenger(String message) {
        this.message = message;
    }

    String message

    String anotherMessage
}
<lang:groovy id="badMessenger"
    script-source="classpath:Messenger.groovy">

    <!-- this next constructor argument will *not* be injected into the GroovyMessenger -->
    <!--     in fact, this isn't even allowed according to the schema -->
    <constructor-arg value="This will *not* work" />
    
    <!-- only property values are injected into the dynamic-language-backed object -->
    <lang:property name="anotherMessage" value="Passed straight through to the dynamic-language-backed object" />

</lang>

实际上这种局限性并没有表现的那么明显,因为setter注入的方式是开发人员更青睐的方式(至于哪种注入方式更好,这个话题我们还是留到以后再讨论吧)。

24.3.2. JRuby beans

来自JRuby官方网页...

JRuby是Ruby语言的纯Java实现。

Spring一直以来的崇尚的哲学是提供选择性,因此Spring动态语言支持特征也支持使用JRuby语言定义的bean。JRuby语言当然基于Ruby语言,支持内置正则表达式,块(闭包),以及其他很多特征,这些特征对于某些域问题提供了解决方案,可以让开发变的更容易。

Spring对JRuby动态语言支持的有趣的地方在于:对于<lang:ruby>元素'script-interfaces'属性指定的接口,Spring为它们创建了JDK动态代理实现(这也是你使用JRuby实现的bean,必须为该属性指定至少一个接口并编程实现的原因)。

首先我们看一个使用基于JRuby的bean的可工作的完整示例。下面是使用JRuby实现的Messenger接口(本章之前定义过了,为了方便你阅读,下面重复定义该接口)。

package org.springframework.scripting;

public interface Messenger {

    String getMessage();
}
require 'java'

class RubyMessenger
    include org.springframework.scripting.Messenger

    def setMessage(message)
        @@message = message
    end

    def getMessage
        @@message
    end
end

# this last line is not essential (but see below)
RubyMessenger.new

下面是Spring的XML配置,其内容定义了RubyMessenger(JRuby bean)的实例。

<lang:jruby id="messageService"
            script-interfaces="org.springframework.scripting.Messenger"
            script-source="classpath:RubyMessenger.rb">
    
    <lang:property name="message" value="Hello World!" />

</lang:jruby>

注意JRuby源码的最后一行('RubyMessenger.new')。在Spring动态语言支持的上下文之下使用JRuby的时候,我们鼓励你实例化并返回一个JRuby类的实例。如果你打算将其作为你的JRuby源码的执行结果,并将其作为dynamic-language-backed bean,只需要简单的实例化你的JRuby类就可以达到这样的效果,如下面源文件的最后一行:

require 'java'

include_class 'org.springframework.scripting.Messenger'

# class definition same as above...

# instantiate and return a new instance of the RubyMessenger class
RubyMessenger.new

如果你忘记了这点,并不代表以前所有的努力白费了,不过Spring会以反射的方式扫描你的JRuby的类型表示,并找出一个类,然后进行实例化。这个过程的速度是相当快的,可能你永远都不会感觉到延迟,但是只需要象前面的例子那样在你的JRuby的脚本最后添加一行就可以避免这样的事情,何乐而不为呢?如果不提供这一行,或者如果Spring在你的JRuby脚本中无法找到可以实例化的类,JRuby的解释器执行源码结束后会立刻抛出ScriptCompilationException异常。下面的代码中可以立刻发现一些关键的文本信息,这些文本信息标识了导致异常的根本原因(如果Spring容器在创建的 dynamic-language-backed bean的时候抛出以下异常, 在相应的异常堆栈中会包括以下文本信息,希望这些信息能够帮助你更容易定位并矫正问题):

org.springframework.scripting.ScriptCompilationException: Compilation of JRuby script returned ''

为了避免这种错误,将你打算用作JRuby-dynamic-language-backed bean(如前文所示)的类进行实例化,并将其返回。请注意在JRuby脚本中实际上可以定义任意数目的类和对象,重要的是整个源文件应该返回一个对象(用于Spring的配置)。

第 24.4 节 “场景” 这一节提供了一些场景,在这些场景下也许你会打算采用基于JRuby的bean.

24.3.3. Groovy beans

来自Groovy官方网页...

Groovy是一门针对Java 2平台的敏捷的动态语言,Python、Ruby、Smalltalk这类语言中不少受人喜爱的特征都被囊括其中,并以Java风格的语法展现给Java开发者。

如果你是以从上到下的方式一直读到这一章,你应该已经看到了一个Groovy-dynamic-language-backed bean的示例。接下来我们来看另外一个例子(还是选自Spring的测试套件)。

注意

Groovy需要1.4以上的JDK。

package org.springframework.scripting;

public interface Calculator {

    int add(int x, int y);
}

下面是使用Groovy实现的Calculator接口。

// from the file 'calculator.groovy'
package org.springframework.scripting.groovy

class GroovyCalculator implements Calculator {

    int add(int x, int y) {
        x + y
    }
}
<-- from the file 'beans.xml' -->
<beans>
    <lang:groovy id="calculator" script-source="classpath:calculator.groovy"/>
</beans>

最后是一个小应用程序,用于测试上面的配置。

package org.springframework.scripting;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Main {

    public static void Main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        Calculator calc = (Calculator) ctx.getBean("calculator");
        System.out.println(calc.add(2, 8));
    }
}

运行上面的程序最终输出结果(毫无疑问)为10。(令人激动的例子,是吧?记住我们的目的是为了阐述概念。更复杂的例子请参考动态语言的示例项目,或者参考本章最后列出的第 24.4 节 “场景”)。

有一点很重要,那就是你不要 在一个Groovy源文件中定义两个以上的class。虽然Groovy允许这样做,但是是一个很不好的实践,为了保持一致性,你应该尊重标准的Java规范(至少作者是这样认为的):一个源文件只定义一个(public)类。

24.3.3.1. 通过回调定制Groovy对象

GroovyObjectCustomizer接口实际上是一个回调, 它允许你将附属的创建逻辑添加到创建Groovy bean的过程中。 例如,该接口的实现能够调用任何需要的初始化方法, 或者设置一些缺省的属性值,或者指定自定义的MetaClass

public interface GroovyObjectCustomizer {

   void customize(GroovyObject goo);
}

Spring框架将会初始化Groovy bean示例,然后将已经创建的GroovyObject 对象传到到指定的GroovyObjectCustomizer接口。 通过GroovyObject你可以做任意想做的事情。大部分 人也许都想通过该回调传入一个定制的MetaClass,下面你将看到一个这样的例子。

public final class SimpleMethodTracingCustomizer implements GroovyObjectCustomizer {

   public void customize(GroovyObject goo) {
      DelegatingMetaClass metaClass = new DelegatingMetaClass(goo.getMetaClass()) {

         public Object invokeMethod(Object object, String methodName, Object[] arguments) {
            System.out.println("Invoking '" + methodName + "'.");
            return super.invokeMethod(object, methodName, arguments);
         }
      };
      metaClass.initialize();
      goo.setMetaClass(metaClass);
   }
}

完整讨论Groovy的元编程已经超出了本参考手册的范畴。建议直接查阅Groovy参考手册的有关章节, 或者通过在线查询:目前有大量的关于这方面的文章。 如果你使用Spring 2.0的命名空间支持功能,使用GroovyObjectCustomizer将会变得非常简单。

<!-- define the GroovyObjectCustomizer just like any other bean -->
<bean id="tracingCustomizer" class="example.SimpleMethodTracingCustomizer" />

   <!-- ... and plug it into the desired Groovy bean via the 'customizer-ref' attribute -->
   <lang:groovy id="calculator"
      script-source="classpath:org/springframework/scripting/groovy/Calculator.groovy"
      customizer-ref="tracingCustomizer" />

如果你没有使用Spring 2.0的命名空间支持,你仍然可以使用GroovyObjectCustomizer的功能。

<bean id="calculator" class="org.springframework.scripting.groovy.GroovyScriptFactory">
      <constructor-arg value="classpath:org/springframework/scripting/groovy/Calculator.groovy"/>
      <!-- define the GroovyObjectCustomizer (as an inner bean) -->
      <constructor-arg>
         <bean id="tracingCustomizer" class="example.SimpleMethodTracingCustomizer" />
      </constructor-arg>
</bean>

<bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/>

24.3.4. BeanShell beans

来自BeanShell官方网页...

BeanShell是一个用Java实现的小型免费嵌入式Java源码解释器,支持动态语言特征。BeanShell动态执行标准的Java语法,并进行了扩展,带来一些常见的脚本的便利,如在Perl和JavaScript中的宽松类型、命令、方法闭包等等。

和Groovy相比,基于BeanShell的bean定义需要的配置要多一些。Spring对BeanShell动态语言支持的有趣的地方在于:对于<lang:bsh>元素的'script-interfaces'属性指定的接口,Spring为它们创建了JDK动态代理实现(这也是你使用BeanShell实现的bean,必须为该属性指定至少一个接口并编程实现的原因)。这意味着所有调用 BeanShell-backed对象的方法,都要通过JDK动态代理调用机制。

首先我们看一个使用基于BeanShell的bean的可工作的完整示例。它实现了本章之前定义的Messenger接口(为了方便阅读,下面重复定义该接口)。

package org.springframework.scripting;

public interface Messenger {

    String getMessage();
}

Here is the BeanShell 'implementation' (the term is used loosely here) of the Messenger interface.

下面是BeanShell的实现的Messenger 接口。

String message;

String getMessage() {
    return message;
}

void setMessage(String aMessage) {
    message = aMessage;
}

下面的Spring XML定义了上述“类”的一个“实例”(这里对术语的使用非常的随意)。

<lang:bsh id="messageService" script-source="classpath:BshMessenger.bsh"
    script-interfaces="org.springframework.scripting.Messenger">

    <lang:property name="message" value="Hello World!" />
</lang:bsh>

第 24.4 节 “场景”一节中提供了一些场景,在这样的场景下你也许打算采用基于BeanShell的bean。