Chapter 24. 动态语言支持

24.1. 介绍

Spring 2.0开始广泛支持在Spring中使用动态语言(如JRuby)定义的类和对象。

Spring对动态语言的支持主要有:允许你使用所支持的动态语言编写任意数目的类,Spring容器能够完全透明的实例化,配置,依赖注入其最终对象。

目前支持的动态语言列表如下:

  • JRuby

  • Groovy

  • BeanShell

Section 24.4, “场景”一节描述了一些可运行的示例,通过这些示例你可以体验到Spring对动态语言的支持。

注意只有在Spring2.0及以上版本才可获得本章所指的动态语言支持。目前Spring团队还没有计划要在以前的版本(如1.2.x)中提供对动态语言的支持。

24.2. 第一个例子

本章的大部分内容的关注点都在描述Spring对动态语言的支持的细节上。在深入到这些细节之前,首先让我们看一个使用动态语言定义的bean的快速上手的例子。

第一个bean使用的动态语言是Groovy(这个例子来自Spring的测试套件,如果你打算看看对其他语言的支持的相同的例子,请阅读相应的源码)。

首先看看Groovy bean要实现的Messenger接口。注意该接口是使用纯Java定义的。依赖的对象是通过Messenger接口的引用注入的,并不知道其实现是Groovy脚本。

package org.springframework.scripting;

public interface Messenger {

    String getMessage();
}

下面是依赖Messenger接口的类的定义。

package org.springframework.scripting;

public class DefaultBookingService implements BookingService {

    private Messenger messenger;

    public void setMessenger(Messenger messenger) {
        this.messenger = messenger;
    }

    public void processBooking() {
        // use the injected Messenger object...
    }
}

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

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

// import the Messenger interface (written in Java) that is to be implemented
import org.springframework.scripting.Messenger

// define the implementation in Groovy
class GroovyMessenger implements Messenger {

    @Property String message;
}

最后,这里的bean定义将Groovy定义的Messenger实现注入到DefaultBookingService类的实例中。

[Note]Note

要使用用户定制的动态语言标签来定义 dynamic-language-backed bean,需要在Spring XML配置文件的头部添加相应的XML Schema。同样需要Spring的ApplicationContext实现作为IoC容器。Spring支持在简单的BeanFactory实现下使用dynamic-language-backed bean,但是你需要管理Spring内部的种种细节。

关于XML Schema的配置,详情请看Appendix A, XML Schema-based configuration

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:lang="http://www.springframework.org/schema/lang"
       xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.0.xsd">

    <!-- this is the bean definition for the Groovy-backed Messenger implementation -->
    <lang:groovy id="messenger" script-source="classpath:Messenger.groovy">
        <lang:property name="message" value="I Can Do The Frug" />
    </lang:groovy>

    <!-- an otherwise normal bean that will be injected by the Groovy-backed Messenger -->
    <bean id="bookingService" class="x.y.DefaultBookingService">
        <property name="messenger" ref="messenger" />
    </bean>
</beans>

现在可以象以前一样使用bookingService bean (DefaultBookingService)的私有成员变量 messenger,因为被注入的Messenger实例确实一个真正的Messenger实例。这里也没有什么特别的地方,就是简单的Java和Groovy。

但愿你能够无需多加说明就看明白以上的XML片段,而不用太担心它是否恰当或者是否正确。请继续阅读更深层次的细节以了解以上配置的原因。

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

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

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

24.3.1. 公共概念

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

  1. 编写针对动态语言源码的测试代码(测试驱动)

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

  3. 在XML配置文件中使用相应的<lang:language/>元素定义dynamic-language-backed beans。当然你也可以使用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能够自我修正,从已改变的动态语言源文件中提取新的状态和逻辑。

[Note]Note

注意该特征默认值为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的方法被调用的时候才会检查底层源文件是否修改了。刷新脚本产生的任何异常都会在调用的代码中抛出一个fatal类型的异常。

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

24.3.1.3. 内置动态语言源文件

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

<lang:groovy id="messenger">
    <lang:property name="message" value="I Can Do The Frug" />
    <lang:inline-script>
package org.springframework.scripting.groovy;

import org.springframework.scripting.Messenger

class GroovyMessenger implements Messenger {

    @Property String message;
}
    </lang:inline-script>
</lang:groovy>

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

还有,别忘了还有比上面的例子还要复杂的多的情况,也许这种情况下你可以在源码外面包一层<![CDATA[]]>(如下例)。

下面这个例子是一个基于JRuby的bean,这个例子直接在Spring的XML配置文件中定义了源码,并使用了inline: 符号。(注意可以使用 &lt;符号来表示'<'字符)

<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 context的构造器注入

关于Spring动态语言支持有一个要点必须引起注意:目前对dynamic-language-backed bean还不可能提供构造器参数的支持(也就是说对于dynamic-language-backed bean的构造器注入无效)。

只是为了将构造器和属性的特殊处理100%说清楚,下面混合了代码和配置的例子是无法运作的。

下面是使用Groovy实现Messenger接口的例子。

// 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;
    }

    @Property String message;

    @Property 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]是用Java代码重新实现的Ruby解释器,是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'

include_class 'org.springframework.scripting.Messenger'

class RubyMessenger < Messenger

 def setMessage(message)
  @@message = message
 end

 def getMessage
  @@message
 end

end

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

下面是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的配置)。

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

24.3.3. Groovy beans

来自Groovy官方网页...

Groovy是一门来自Java2平台的敏捷的动态语言,拥有很多象Python, Ruby, Smalltalk这类语言的特征,并以Java风格的语法展现给Java开发者。

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

[Note]Note

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
    }
}

下面是相应的Spring的XML配置文件。

<-- 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。令人激动的例子吧?记住我们的目的是为了阐述概念。更复杂的例子请参考动态语言的示例项目,或者参考本章最后列出的Section 24.4, “场景”

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

关于Groovy bean的部分就到此为止。Groove on!

24.3.4. BeanShell beans

来自BeanShell官方网页...

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

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

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

package org.springframework.scripting;

public interface Messenger {

    String getMessage();
}

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

String message;

String getMessage() {
    return message;
}

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

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

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

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

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

24.4. 场景

在某些可能的场景下,使用脚本语言定义Spring管理的bean的是有好处的,当然这样的场景是各式各样的。这一节描述了两个可能在Spring中使用动态语言支持特征的用例。

请注意Spring的发布包中包括了一个动态语言支持的示例项目(示例项目只是一个小项目,其范围是用于演示Spring框架的某些特定的特征)。

24.4.1. Spring MVC控制器脚本化

有一组类可以使用dynamic-language-backed bean并从中获益,那就是Spring MVC控制器。在纯Spring MVC应用中,贯穿整个web应用的导航流程,相当大的部分都封装在Spring MVC控制器的代码中。因为web应用的导航流程和其他表示层逻辑需要能够积极响应业务需求的变化和问题,通过编辑一个或多个动态语言源文件也许可以更容易响应这样那样的变化,而且通过这种方式,一个处于运行状态的应用可以立即反映出所做的改动。

象Spring这样由项目支持的轻量级架构模型中,你的目标是拥有一个真正瘦小 的表示层,而应用的所有业务逻辑都在包装在领域层和服务层的类中,将Spring MVC控制器作为dynamic-language-backed bean来进行开发,可以简单的编辑保存文本文件就可以修改表示层逻辑,这些动态语言源文件的任何变化都可以(取决于配置)自动的反射为bean(底层为动态语言源文件)的变化。

[Note]Note

请注意为了自动提取dynamic-language-backed bean的任何变化,你必须启用'refreshable beans' 功能。关于该特征的详细情况请参考Section 24.3.1.2, “Refreshable bean”一节。

下面的示例是使用Groovy动态语言实现的org.springframework.web.servlet.mvc.Controller。这个例子选自Spring发布包中提供的动态语言支持示例项目。关于该项目的详情请参考Spring发布包中的'samples/showcases/dynamvc/'目录。

<!-- from the file '/WEB-INF/groovy/FortuneController.groovy' -->
package org.springframework.showcase.fortune.web;

import org.springframework.showcase.fortune.service.FortuneService;
import org.springframework.showcase.fortune.domain.Fortune;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class FortuneController implements Controller {

    @Property FortuneService fortuneService

    public ModelAndView handleRequest(
            HttpServletRequest request, HttpServletResponse httpServletResponse) {

        return new ModelAndView("tell", "fortune", this.fortuneService.tellFortune())
    }

}

上面的Controller接口的相关的bean定义如下:

<lang:groovy id="fortune"
             refresh-check-delay="3000"
             script-source="/WEB-INF/groovy/FortuneController.groovy">
    <lang:property name="fortuneService" ref="fortuneService"/>
</lang:groovy>

24.4.2. Validator脚本化

使用Spring进行应用开发的另一个领域,也就是校验,也许会从dynamic-language-backed bean提供的柔韧性中获益。使用松散类型的动态语言(相对Java语言)也许可以很容易的表示复杂的校验逻辑(可能是通过内置的正则表达式)。

使用dynamic-language-backed bean作为校验器,可以很容易的改变校验逻辑,只要编辑修改简单的文本文件即可;任何此类修改可以自动反射(取决于配置方式),这些都是在程序运行的时候进行的,不需要重启应用程序。

[Note]Note

请注意为了自动提取dynamic-language-backed bean的任何变化,你必须启用 'refreshable beans' 功能。关于该特征的详细情况请参考Section 24.3.1.2, “Refreshable bean”一节。

下面的示例是使用Groovy动态语言实现的org.springframework.validation.Validator。(关于Validator接口的讨论请参考Section 5.2, “使用Spring的Validator接口进行校验” 一节)

import org.springframework.validation.Validator
import org.springframework.validation.Errors
import org.springframework.beans.TestBean

public class TestBeanValidator implements Validator {

    boolean supports(Class clazz) {
        return TestBean.isAssignableFrom(clazz)
    }
    
    void validate(Object bean, Errors errors) {
        String name = bean.getName()
        if(name == null || name.trim().length == 0) {
            errors.reject("whitespace", "Cannot be composed wholly of whitespace")
        }
    }
}

24.5. 更多的资源

下面的链接给出了和本章所描述的各种动态语言有关的可进一步参考的资源。

Spring社区中一些活跃分子已经添加了数量可观的动态语言支持,包括本章涉及到的以及其它的动态语言。此时此刻第三方的贡献也许已经添加到Spring主发布所支持的的语言列表中,不妨看看是否能在Spring Modules project找到你钟爱的脚本语言。