动态语言支持
Spring 为使用动态语言(如 Groovy)定义的类和对象提供了全面的支持。这种支持允许你用支持的动态语言编写任意数量的类,并让 Spring 容器透明地实例化、配置和依赖注入生成的对象。 Spring 的脚本支持主要针对 Groovy 和 BeanShell。除了这些特别支持的语言之外,从 Spring 4.2 开始,还支持 JSR-223 脚本机制,以便与任何支持 JSR-223 的语言提供程序(例如 JRuby)集成。 你可以在 场景 中找到此动态语言支持可以立即发挥作用的完整工作示例。
第一个示例
本章的大部分内容将详细描述动态语言支持。在深入了解动态语言支持的所有细节之前,我们先看一个用动态语言定义的 bean 的快速示例。这个第一个 bean 的动态语言是 Groovy。(本示例的基础取自 Spring 测试套件。如果你想查看任何其他受支持语言的等效示例,请查看源代码)。
下一个示例显示了 Messenger 接口,Groovy bean 将实现该接口。请注意,此接口是用纯 Java 定义的。注入 Messenger 引用的依赖对象不知道底层实现是 Groovy 脚本。以下清单显示了 Messenger 接口:
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 接口:
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 in file 'Messenger.groovy'
class GroovyMessenger implements Messenger {
String message
}
|
要使用自定义动态语言标签来定义动态语言支持的 bean,你需要在 Spring XML 配置文件顶部添加 XML Schema 序言。你还需要使用 Spring |
最后,以下示例显示了 bean 定义,这些定义将 Groovy 定义的 Messenger 实现注入到 DefaultBookingService 类的实例中:
<?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 https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.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 片段不言自明,但如果不是,也不必过分担心。请继续阅读,以深入了解前面配置的原因和原理。
定义由动态语言支持的 Bean
本节将准确描述如何在任何受支持的动态语言中定义 Spring 管理的 bean。
请注意,本章不试图解释受支持动态语言的语法和惯用语。例如,如果你想使用 Groovy 编写应用程序中的某些类,我们假设你已经了解 Groovy。如果你需要有关动态语言本身的更多详细信息,请参阅本章末尾的 更多资源。
常见概念
使用动态语言支持的 bean 涉及的步骤如下:
-
编写动态语言源代码的测试(当然)。
-
然后编写动态语言源代码本身。
-
使用 XML 配置中相应的
<lang:language/>元素定义动态语言支持的 bean(你可以使用 Spring API 以编程方式定义此类 bean,但你需要查阅源代码以获取有关如何执行此操作的说明,因为本章不涵盖此类高级配置)。请注意,这是一个迭代步骤。每个动态语言源文件至少需要一个 bean 定义(尽管多个 bean 定义可以引用同一个源文件)。
前两个步骤(测试和编写动态语言源文件)超出了本章的范围。请参阅你所选动态语言的语言规范和参考手册,然后开始开发你的动态语言源文件。不过,你首先需要阅读本章的其余部分,因为 Spring 的动态语言支持确实对你的动态语言源文件的内容做了一些(小的)假设。
<lang:language/> 元素
上一节 列表中的最后一步涉及定义动态语言支持的 bean 定义,每个要配置的 bean 一个(这与普通的 JavaBean 配置没有什么不同)。但是,你可以使用 <lang:language/> 元素来定义动态语言支持的 bean,而不是指定要由容器实例化和配置的类的完全限定类名。
每个受支持的语言都有一个对应的 <lang:language/> 元素:
-
<lang:groovy/>(Groovy) -
<lang:bsh/>(BeanShell) -
<lang:std/>(JSR-223, 例如, JRuby)
可用于配置的确切属性和子元素取决于 bean 定义所用的具体语言(本章后面特定于语言的部分将详细介绍这一点)。
可刷新 Bean
Spring 中动态语言支持最引人注目(也许是唯一)的价值之一是“可刷新 bean”功能。
可刷新 bean 是一个由动态语言支持的 bean。通过少量配置,动态语言支持的 bean 可以监视其底层源文件资源的更改,然后在动态语言源文件更改时(例如,当你在文件系统上编辑并保存文件更改时)重新加载自身。
这允许你将任意数量的动态语言源文件部署为应用程序的一部分,配置 Spring 容器以创建由动态语言源文件支持的 bean(使用本章中描述的机制),然后(稍后,随着需求变化或某些其他外部因素发挥作用)编辑动态语言源文件并使所做的任何更改反映在由更改的动态语言源文件支持的 bean 中。无需关闭正在运行的应用程序(或在 Web 应用程序的情况下重新部署)。如此修改的动态语言支持的 bean 会从更改的动态语言源文件中获取新的状态和逻辑。
|
此功能默认关闭。 |
现在我们可以看一个例子,看看开始使用可刷新 bean 是多么容易。要打开可刷新 bean 功能,你必须在 bean 定义的 <lang:language/> 元素上指定一个额外的属性。因此,如果我们继续使用本章前面 的示例,以下示例显示了我们将在 Spring XML 配置中更改以实现可刷新 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 在底层动态语言源文件发生任何更改后刷新的毫秒数。你可以通过为 refresh-check-delay 属性分配负值来关闭刷新行为。请记住,默认情况下,刷新行为是禁用的。如果你不希望有刷新行为,请不要定义该属性。
如果我们然后运行以下应用程序,我们可以练习可刷新功能。(请原谅在下一段代码中“为了暂停执行而跳过障碍”的滑稽行为。)System.in.read() 调用只是为了让程序在暂停执行时,你(在此场景中的开发人员)去编辑底层动态语言源文件,以便在程序恢复执行时触发动态语言支持的 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() 方法的所有调用都必须更改,以便消息被引号括起来。以下清单显示了当程序执行暂停时,你(开发人员)应该对 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。在源文件更改并保存且程序恢复执行后,调用动态语言支持的 Messenger 实现上的 getMessage() 方法的结果是 ’I Can Do The Frug'`(注意包含额外的引号)。
如果更改发生在 refresh-check-delay 值的窗口内,则脚本更改不会触发刷新。脚本的更改实际上直到在动态语言支持的 bean 上调用方法时才会被拾取。只有当在动态语言支持的 bean 上调用方法时,它才会检查其底层脚本源是否已更改。任何与刷新脚本相关的异常(例如遇到编译错误或发现脚本文件已被删除)都会导致致命异常传播到调用代码。
前面描述的可刷新 bean 行为不适用于使用 <lang:inline-script/> 元素表示法定义的动态语言源文件(请参阅 内联动态语言源文件)。此外,它仅适用于可以实际检测到底层源文件更改的 bean(例如,通过检查文件系统上存在的动态语言源文件的最后修改日期的代码)。
内联动态语言源文件
动态语言支持还可以处理直接嵌入在 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 Validator 实现添加到 Spring MVC Controller 中。使用内联源,这只是片刻的工作。(有关此类示例,请参阅 脚本化验证器。)
理解动态语言支持的 Bean 的构造函数注入
关于 Spring 的动态语言支持,有一件非常重要的事情需要注意。那就是,你目前无法向动态语言支持的 bean 提供构造函数参数(因此,动态语言支持的 bean 不支持构造函数注入)。为了使构造函数和属性的这种特殊处理 100% 清晰,以下代码和配置的混合不起作用:
package org.springframework.scripting.groovy
import org.springframework.scripting.Messenger
// from the file 'Messenger.groovy'
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 注入是绝大多数开发人员青睐的注入方式(至于这是好是坏,我们留待以后讨论)。
Groovy Bean
本节描述如何在 Spring 中使用 Groovy 定义的 bean。
Groovy 主页包括以下描述:
“Groovy 是一种适用于 Java 2 平台的敏捷动态语言,它具有人们在 Python、Ruby 和 Smalltalk 等语言中非常喜欢的许多特性,使 Java 开发人员能够使用类似 Java 的语法。”
如果你从头开始阅读本章,你已经 看过一个示例 的 Groovy 动态语言支持的 bean。现在考虑另一个示例(再次使用 Spring 测试套件中的示例):
package org.springframework.scripting;
public interface Calculator {
int add(int x, int y);
}
以下示例使用 Groovy 实现了 Calculator 接口:
package org.springframework.scripting.groovy
// from the file 'calculator.groovy'
class GroovyCalculator implements Calculator {
int add(int x, int y) {
x + y
}
}
以下 bean 定义使用 Groovy 中定义的计算器:
<!-- 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 = ctx.getBean("calculator", Calculator.class);
System.out.println(calc.add(2, 8));
}
}
运行上述程序的结果是(不出所料)10。(有关更有趣的示例,请参阅动态语言展示项目以获取更复杂的示例,或参阅本章后面 场景 中的示例)。
每个 Groovy 源文件不得定义多个类。虽然这在 Groovy 中是完全合法的,但它(可以说)是一种不好的做法。为了保持一致的方法,你(在 Spring 团队看来)应该遵循每个源文件一个(公共)类的标准 Java 约定。
使用回调自定义 Groovy 对象
GroovyObjectCustomizer 接口是一个回调,允许你将额外的创建逻辑挂钩到创建 Groovy 支持的 bean 的过程中。例如,此接口的实现可以调用任何所需的初始化方法、设置一些默认属性值或指定自定义 MetaClass。以下清单显示了 GroovyObjectCustomizer 接口定义:
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 元编程的完整讨论超出了 Spring 参考手册的范围。请参阅 Groovy 参考手册的相关部分或在线搜索。有很多文章涉及此主题。实际上,如果你使用 Spring 命名空间支持,使用 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 命名空间支持,你仍然可以使用 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"/>
|
你还可以指定 Groovy |
BeanShell Bean
本节描述如何在 Spring 中使用 BeanShell bean。
BeanShell 主页 包含以下描述:
BeanShell 是一个用 Java 编写的小型、免费、可嵌入的 Java 源代码解释器,具有动态语言特性。BeanShell 动态运行标准 Java 语法,并用常见的脚本便利性扩展它,例如松散类型、命令和方法闭包,就像 Perl 和 JavaScript 中的那样。
与 Groovy 相比,BeanShell 支持的 bean 定义需要一些(小的)额外配置。Spring 中 BeanShell 动态语言支持的实现很有趣,因为 Spring 创建了一个 JDK 动态代理,它实现了 <lang:bsh> 元素的 script-interfaces 属性值中指定的所有接口(这就是为什么你必须在属性值中至少提供一个接口,因此,在使用 BeanShell 支持的 bean 时要面向接口编程)。这意味着对 BeanShell 支持的对象的所有方法调用都通过 JDK 动态代理调用机制。
现在我们可以展示一个使用基于 BeanShell 的 bean 的完整工作示例,该 bean 实现了本章前面定义的 Messenger 接口。我们再次展示 Messenger 接口的定义:
package org.springframework.scripting;
public interface Messenger {
String getMessage();
}
以下示例显示了 Messenger 接口的 BeanShell“实现”(我们在此处宽松地使用该术语):
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>
请参阅 场景 以了解你可能希望使用基于 BeanShell 的 bean 的一些场景。
场景
在脚本语言中定义 Spring 管理的 bean 可能有益的场景多种多样。本节描述了 Spring 中动态语言支持的两种可能用例。
脚本化的 Spring MVC 控制器
Spring MVC 控制器是可以使用动态语言支持的 bean 的一类受益类。在纯 Spring MVC 应用程序中,Web 应用程序的导航流在很大程度上由封装在 Spring MVC 控制器中的代码决定。随着 Web 应用程序的导航流和其他表示层逻辑需要更新以响应支持问题或不断变化的业务需求,通过编辑一个或多个动态语言源文件并立即看到这些更改反映在运行应用程序的状态中,可能更容易实现任何此类所需更改。
请记住,在 Spring 等项目所倡导的轻量级架构模型中,你通常旨在拥有一个非常薄的表示层,应用程序的所有核心业务逻辑都包含在域和服务层类中。将 Spring MVC 控制器开发为动态语言支持的 bean 允许你通过编辑和保存文本文件来更改表示层逻辑。对此类动态语言源文件的任何更改(取决于配置)都会自动反映在由动态语言源文件支持的 bean 中。
|
为了实现动态语言支持的 bean 的任何更改的自动“拾取”,你必须启用“可刷新 bean”功能。请参阅 可刷新 Bean 以获取此功能的完整处理。 |
以下示例显示了一个使用 Groovy 动态语言实现的 org.springframework.web.servlet.mvc.Controller:
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 jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
// from the file '/WEB-INF/groovy/FortuneController.groovy'
class FortuneController implements Controller {
@Property FortuneService fortuneService
ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse httpServletResponse) {
return new ModelAndView("tell", "fortune", this.fortuneService.tellFortune())
}
}
<lang:groovy id="fortune"
refresh-check-delay="3000"
script-source="/WEB-INF/groovy/FortuneController.groovy">
<lang:property name="fortuneService" ref="fortuneService"/>
</lang:groovy>
脚本化验证器
Spring 应用程序开发中另一个可能受益于动态语言支持的 bean 所提供的灵活性的领域是验证。使用松散类型动态语言(也可能支持内联正则表达式)表达复杂的验证逻辑比使用普通 Java 更容易。
同样,将验证器开发为动态语言支持的 bean 允许你通过编辑和保存简单的文本文件来更改验证逻辑。任何此类更改(取决于配置)都会自动反映在运行应用程序的执行中,并且不需要重新启动应用程序。
|
为了实现动态语言支持的 bean 的任何更改的自动“拾取”,你必须启用“可刷新 bean”功能。请参阅 可刷新 Bean 以获取此功能的完整详细处理。 |
以下示例显示了一个使用 Groovy 动态语言实现的 Spring org.springframework.validation.Validator(有关 Validator 接口的讨论,请参阅 使用 Spring 的 Validator 接口进行验证):
import org.springframework.validation.Validator
import org.springframework.validation.Errors
import org.springframework.beans.TestBean
class TestBeanValidator implements Validator {
boolean supports(Class clazz) {
return TestBean.class.isAssignableFrom(clazz)
}
void validate(Object bean, Errors errors) {
if(bean.name?.trim()?.size() > 0) {
return
}
errors.reject("whitespace", "Cannot be composed wholly of whitespace.")
}
}
附加细节
最后一节包含一些与动态语言支持相关的附加细节。
AOP — 建议脚本化 Bean
你可以使用 Spring AOP 框架来建议脚本化 bean。Spring AOP 框架实际上并不知道被建议的 bean 可能是一个脚本化 bean,因此你使用的(或打算使用的)所有 AOP 用例和功能都适用于脚本化 bean。当你建议脚本化 bean 时,你不能使用基于类的代理。你必须使用 基于接口的代理。
你不仅限于建议脚本化 bean。你还可以使用支持的动态语言编写切面本身,并使用这些 bean 来建议其他 Spring bean。但这确实是动态语言支持的高级用法。
作用域
如果不是立即显而易见,脚本化 bean 可以像任何其他 bean 一样进行作用域。各种 <lang:language/> 元素上的 scope 属性允许你控制底层脚本化 bean 的作用域,就像它对常规 bean 所做的那样。(默认作用域是 单例,就像对“常规”bean 一样。)
以下示例使用 scope 属性将 Groovy bean 定义为 原型:
<?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 https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd">
<lang:groovy id="messenger" script-source="classpath:Messenger.groovy" scope="prototype">
<lang:property name="message" value="I Can Do The RoboCop" />
</lang:groovy>
<bean id="bookingService" class="x.y.DefaultBookingService">
<property name="messenger" ref="messenger" />
</bean>
</beans>
lang XML 模式
Spring XML 配置中的 lang 元素处理将用动态语言(如 Groovy 或 BeanShell)编写的对象作为 bean 暴露在 Spring 容器中。
这些元素(以及动态语言支持)在 动态语言支持 中有全面介绍。有关此支持和 lang 元素的完整详细信息,请参阅该部分。
要使用 lang 模式中的元素,你需要在 Spring XML 配置文件顶部添加以下序言。以下片段中的文本引用了正确的模式,以便 lang 命名空间中的标签对你可用:
<?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 https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd">
<!-- bean definitions here -->
</beans>