加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

Groovy 使 Spring 更出色,第 2 部分: 在运行时改变应用程序的行

发布时间:2020-12-14 16:53:27 所属栏目:大数据 来源:网络整理
导读:在这个包含 2 部分的系列的 第 1 部分 ,您看到了如何使用 Groovy bean 使 Spring 应用程序更加灵活。Spring 的 Groovy 支持使您可以使用编译后的或脚本化的 Groovy 语言 bean,并通过不同的方式配置它们,包括使用 lang XML 模式和 Grails Bean Builder。当

在这个包含 2 部分的系列的 第 1 部分,您看到了如何使用 Groovy bean 使 Spring 应用程序更加灵活。Spring 的 Groovy 支持使您可以使用编译后的或脚本化的 Groovy 语言 bean,并通过不同的方式配置它们,包括使用 lang XML 模式和 Grails Bean Builder。当把 Groovy 脚本集成到应用程序中时,就可以在 bean 创建过程中包括额外的逻辑(例如,确定当创建一个 bean 时使用哪种实现策略)。还可以使用不同的脚本化 Groovy bean 为部署和打包提供更多的灵活性。

Spring 的动态语言支持中最有趣、最强大的特性也许就是在运行应用程序时 能够监视和检测对动态语言脚本的更改,并将被更改的 bean 自动重新装载 到 Spring 应用程序上下文中。在一个正在运行的应用程序中自动刷新 Spring bean,这样的用例有很多。下面是一些例子:

  • PDF 生成(账单、发票、销售报告、投资报告、收据、日程安排等)
  • e-mail 模板
  • 报告生成
  • 外部化业务逻辑、特定领域语言(DSL)和规则引擎
  • 系统管理任务
  • 更改日志记录级别和运行时调试

相信您可以想到更多的应用。本文展示如何将 bean 刷新添加到 Spring 应用程序中,并探索它是如何工作的。本文中所有例子的完整源代码(参见 下载)都可以下载获得。

可刷新的 Groovy bean

第 1 部分 中,您定义了 PdfGenerator 接口,并在 GroovyPdfGenerator.groovy 脚本文件中用一个 Groovy 类(GroovyPdfGenerator)实现了它,这个脚本文件位于应用程序的 CLASSPATH 中。您通过指定 Groovy 脚本所在的位置,配置了基于 Groovy 的 pdfGenerator bean。清单 1 显示了这个接口、它的实现以及使用 lang XML 模式的配置:


清单 1. PdfGenerator 接口、实现和配置

C-sharp代码

  1. //?PdfGenerator.java ??
  2. public?interface?PdfGenerator?{ ??
  3. ????byte[]?pdfFor(Invoice?invoice); ??
  4. } ??
  5. ??
  6. //?GroovyPdfGenerator.groovy ??
  7. class?GroovyPdfGenerator?implements?PdfGenerator?{ ??
  8. ??
  9. ????String?companyName ??
  10. ??
  11. ????public?byte[]?pdfFor(Invoice?invoice)?{ ??
  12. ????????... ??
  13. ????} ??
  14. ??
  15. } ??
  16. ??
  17. //?applicationContext.xml ??
  18. <lang:groovy?id="pdfGenerator"??
  19. ?????????????script-source="classpath:groovierspring/GroovyPdfGenerator.groovy"> ??
  20. ????<lang:property?name="companyName"?value="Groovy?Bookstore"/> ??
  21. </lang:groovy>??

到目前为止,一切良好。您有一个名为 pdfGenerator 的 bean,它是用 Groovy 实现的,位于应用程序 CLASSPATH 中。当创建 Spring 应用程序上下文时,Spring 读取脚本,将它编译成 Java 类,并在应用程序上下文中实例化一个 GroovyPdfGenerator。任何其他依赖 pdfGenerator 的类只需将它声明为一个依赖,Spring 会将它们连在一起。

Spring 如何检测脚本修改

在内部,Spring 使用一个 Spring AOP (参见 参考资料RefreshableScriptTargetSource 拦截对目标对象(pdfGenerator bean)的调用,执行重新装载检查,并获取一个更新版本的 bean。基本上,依赖可刷新 bean 的 bean 都拥有对一个 AOP 代理而不是 bean 本身的引用。

事情变得真正有趣起来。假设您经常要在应用程序正在运行时对 PDF 生成代码进行更改,并使这些更改立即生效。Spring 使得这种功能的添加变得很简单。您只需为定义 bean 的 <lang:groovy> 元素添加 refresh-check-delay 属性。该属性定义一个毫秒数,每过这么长时间,Spring 检查对底层 Groovy 脚本的更改。如果检测到对脚本的更改(例如,自上次检查后,.groovy 脚本上的时间戳被改变),那么 Spring 读取脚本,编译它,并用新的版本替换 旧的 pdfGenerator bean。Spring 这样做时,任何使用 pdfGenerator 的 bean 都不需要知道这种变化。

清单 2 显示了 pdfGenerator bean,它被配置了 10 秒(10,000 毫秒)的刷新检查延迟。添加 refresh-check-delay,之后,Spring 配置这个 bean,使之在底层 GroovyPdfGenerator.groovy 脚本文件改变时自动刷新。


清单 2. 将 refresh-check-delay 添加到脚本化的 bean 定义中

<lang:groovy id="pdfGenerator"
script-source="classpath:groovierspring/GroovyPdfGenerator.groovy"
refresh-check-delay="10000">
<lang:property name="companyName" value="Refreshable Groovy Bookstore"/>
</lang:groovy>

现在,如果在应用程序正在运行时对 GroovyPdfGenerator.groovy 脚本做出更改,Spring 将检测到这一更改,并在运行时重新装载 pdfGenerator bean,而不必重新启动。注意,只有达到规定的延迟时间,并且 可刷新 bean 上发生方法调用,才会发生刷新检查。例如,假设 pdfGenerator bean 的刷新检查延时为 10 秒,但是连续 50 秒内没有发生方法调用。在这种情况下,Spring 会在 50 秒之后(而不是每过 10 秒)检查是否需要刷新。换句话说,Spring 不会积极地轮询脚本的更改;相反,它判断自上次方法调用后经过的时间,然后计算这段时间是否超过刷新检查延时。只有当经过的时间超过刷新检查延时,Spring 才检查脚本是否被更改,进而确定是否需要刷新。另一方面,假设 pdfGenerator bean 处于较重的负载下,每一秒钟它的方法被多次调用。如果 refresh-check-delay 为 10 秒,无论这个 bean 被使用多少次,它最快只能每 10 秒重新装载一次。所以,不需要担心 Spring 是否会因为积极地轮询 Groovy 脚本而消耗系统资源,它并没有这样做。

如果 Spring 应用程序中有不止一个脚本化的 Groovy bean,您想为所有这些 bean 的刷新检查延时设置一个默认值,那么可以使用 <lang:defaults> 元素轻松做到这一点,如清单 3 所示:


清单 3. 设置默认刷新检查延时

				
<lang:defaults refresh-check-delay="20000"/>

通过使用清单 3 中显示的 <lang:defaults>所有 脚本化动态语言 bean(那些用 Groovy、JRuby、BeanShell 等编写的 bean)的刷新检查延时都被设为 20 秒。对于要使用不同值的 bean,只需添加一个 refresh-check-delay 属性覆盖默认值。甚至可以通过将 refresh-check-delay 设置为一个负值,关闭 单个脚本化的 bean 的自动刷新行为,如清单 4 所示:


清单 4. 覆盖默认的 refresh-check delay

C-sharp代码

  1. <lang:defaults?refresh-check-delay="20000"/> ??
  2. ??
  3. <lang:groovy?id="pdfGenerator"??
  4. ?????????????script-source="classpath:groovierspring/GroovyPdfGenerator.groovy"??
  5. ?????????????refresh-check-delay="60000"> ??
  6. ????<lang:property?name="companyName"?value="Refreshable?Groovy?Bookstore"/> ??
  7. </lang:groovy> ??
  8. ??
  9. <lang:groovy?id="invoiceEmailer"??
  10. ?????????????script-source="classpath:groovierspring/GroovyInvoiceEmailer.groovy"??
  11. ?????????????refresh-check-delay="-1"/>??

在清单 4 中可以看到,默认的刷新检查延时是 20 秒。但是,我已经将 pdfGenerator bean 的刷新检查延时配置为 60 秒,并且完全关闭了 invoiceEmailer bean 上的刷新检查。

使用 Grails Bean Builder 配置可刷新 Groovy bean

第 1 部分,您看到了如何使用 Grails Bean Builder (参见 参考资料)通过编程的方式定义 Spring bean。如果使用 Bean Builder,可以比较轻松地为 bean 添加自动刷新 — 不过这样一来,就会更多地暴露 Spring 内部,因为 Bean Builder 和 <lang:groovy> 语法糖不同。清单 5 展示了如何为所有脚本化 bean 添加一个默认的刷新检查,以及如何为单个 bean 设置刷新延时:


清单 5. 使用 Grails Bean Builder 配置可刷新 Groovy bean

C-sharp代码

  1. def?builder?=?new?grails.spring.BeanBuilder() ??
  2. builder.beans?{ ??
  3. ????scriptFactoryPostProcessor(ScriptFactoryPostProcessor)?{ ??
  4. ????????defaultRefreshCheckDelay?=?20000 ??
  5. ????} ??
  6. ????pdfGenerator(GroovyScriptFactory,??
  7. ?????????????????'classpath:groovierspring/GroovyPdfGenerator.groovy')?{?bean?-> ??
  8. ????????companyName?=?'Refreshable?Bean?Builder?Bookstore'??
  9. ????????bean.beanDefinition.setAttribute( ??
  10. ????????????ScriptFactoryPostProcessor.REFRESH_CHECK_DELAY_ATTRIBUTE,?60000) ??
  11. ????} ??
  12. }??

清单 5 中的 Bean Builder 配置在逻辑上等同于 清单 4 中的 pdfGenerator bean 的配置。您使用 ScriptFactoryPostProcessor bean 的 defaultRefreshCheckDelay 属性为所有脚本化 bean 设置了一个默认的刷新检查延时。在使用 Bean Builder 时,若要为单个的 bean 设置刷新检查延时,必须在底层的 Spring bean 定义上设置一个属性。如果使用 <lang:groovy> 基于 XML 的配置时,Spring 会负责底层的细节,而如果使用 Bean Builder,则需要您自己做这件事。注意,为了在 bean 定义上设置属性,还需要为 pdfGenerator bean 上的闭包声明一个 bean 参数。

定制 Groovy bean

您已经看到了如何使用 refreshable beans 特性使 Groovy bean 在运行时自动更新,并使得应用程序在运行时更加动态。为了使 Groovy bean 更加灵活,Spring 的 Groovy 支持还提供了另一种方式:定制。通过定制,可以将定制的逻辑注入到 Groovy bean 创建过程中。通过 GroovyObjectCustomizer 接口(清单 6 所示),可以在新创建的 GroovyObject 上执行定制逻辑:


清单 6. GroovyObjectCustomizer 接口

				
public interface GroovyObjectCustomizer {
    void customize(GroovyObject goo);
}

GroovyObjectCustomizer 是一个回调,Spring 在创建一个 Groovy bean 之后会调用它。可以对一个 Groovy bean 应用附加的逻辑,或者执行元编程,例如替换对象的元类(参见 参考资料)。清单 7 展示了一个实现,该实现输出执行一个 Groovy bean 上的某个方法所花的时间:


清单 7. 性能日志记录 GroovyObjectCustomizer

C-sharp代码

  1. public?class?PerformanceLoggingCustomizer?implements?GroovyObjectCustomizer?{ ??
  2. ??
  3. ????public?void?customize(GroovyObject?goo)?{ ??
  4. ????????DelegatingMetaClass?metaClass?=?new?DelegatingMetaClass(goo.getMetaClass())?{ ??
  5. ????????????@Override ??
  6. ????????????public?Object?invokeMethod(Object?object,?String?method,?Object[]?args)?{ ??
  7. ????????????????long?start?=?System.currentTimeMillis(); ??
  8. ????????????????Object?result?=?super.invokeMethod(object,?method,?args); ??
  9. ????????????????long?elapsed?=?System.currentTimeMillis()?-?start; ??
  10. ????????????????System.out.printf("%s?took?%d?millis?on?%s/n",?elapsed,?object); ??
  11. ????????????????return?result; ??
  12. ????????????} ??
  13. ????????}; ??
  14. ????????metaClass.initialize(); ??
  15. ????????goo.setMetaClass(metaClass); ??
  16. ????} ??
  17. }??

清单 7 中的 PerformanceLoggingCustomizer 替换 GroovyObject 的元类,并覆盖 invokeMethod,以便添加性能计时(performance-timing)逻辑。接下来,需要配置定制程序,以便将它应用到一个或多个 Groovy bean 上。清单 8 展示了如何使用 <lang:groovy> 中的 customizer-ref 属性将一个定制程序添加到一个已有的 Groovy bean 中:


清单 8. 配置一个 Groovy 对象定制程序

				
				<bean id="performanceLoggingCustomizer" class="groovierspring.PerformanceLoggingCustomizer"/>

<lang:groovy id="pdfGenerator"
    refresh-check-delay="60000"
    script-source="classpath:groovierspring/GroovyPdfGenerator.groovy"
    customizer-ref="performanceLoggingCustomizer">
    <lang:property name="companyName" value="Customized Groovy Bookstore"/>
</lang:groovy>

现在,当 GroovyPdfGenerator 中的任何方法被调用时,您将在标准输出中看到如下所示的输出。(如果您正考虑使用一个日志记录框架会更好,那么您的想法是对的!)

pdfFor took 18 millis on groovierspring.GroovyPdfGenerator@f491a6

为 Groovy bean 添加定制很简单;较难的部分是实现实际的定制逻辑 — 也就是说,当 Groovy bean 被创建时,您想对它们做什么。您看到了使用 <lang:groovy> 和它的 customizer-ref 属性的配置。如果您更喜欢使用 Grails Bean Builder 来构建 Spring bean,那么也很简单。清单 9 展示了如何添加 peformanceLoggingCustomizer bean:

清单 9. 使用 Grails Bean Builder 添加一个 Groovy 对象定制程序

C-sharp代码

  1. builder.beans?{ ??
  2. ????performanceLoggingCustomizer(PerformanceLoggingCustomizer) ??
  3. ????scriptFactoryPostProcessor(ScriptFactoryPostProcessor)?{ ??
  4. ????????defaultRefreshCheckDelay?=?20000 ??
  5. ????} ??
  6. ????pdfGenerator(GroovyScriptFactory,??
  7. ?????????????????'classpath:groovierspring/GroovyPdfGenerator.groovy',??
  8. ?????????????????performanceLoggingCustomizer)?{?bean?-> ??
  9. ????????companyName?=?'Refreshable?Bean?Builder?Bookstore'??
  10. ????????bean.beanDefinition.setAttribute( ??
  11. ????????????ScriptFactoryPostProcessor.REFRESH_CHECK_DELAY_ATTRIBUTE,?60000) ??
  12. ????} ??
  13. }??

更巧妙的数据库

不需要使用 JAR,Spring 提供了对内联脚本和通过 Spring Resource 抽象装载的脚本的支持(参见 参考资料)。在 第 1 部分 中,您看到了内联脚本和基于 Resource 的脚本 — 尤其是 CLASSPATH 资源。您使用 可刷新 bean 添加了更多的动态行为。Spring 装载、编译和刷新动态语言 bena 的能力依赖于 ScriptSource 接口,如清单 10 所示(不完整的 Javadocs):


清单 10. ScriptSource 接口

				
public interface ScriptSource {

    String getScriptAsString() throws IOException;

    boolean isModified();

    String suggestedClassName();
}

ScriptSource 定义 3 个方法:一个方法获取脚本源代码,一个方法确定脚本是否已被修改,还有一个方法返回一个用于脚本的建议类名。Spring 为这个接口提供了两种实现:StaticScriptSourceResourceScriptSource。当在 Spring 配置文件中定义脚本时,可以使用 StaticScriptSourceResourceScriptSource 则用于从任何 Resource 装载脚本(例如,从 CLASSPATH 上的文件中或从 URL 装载脚本)。

可插拔脚本源代码定位符

当我第一次实现将 Groovy 脚本存储在数据库中的功能时,我想到这种机制也许应该是可插拔的,以便用户可以插入不同的 ScriptSource 实现和脚本定位符策略。我就此事咨询了 SpringSource 的 Keith Donald,他表示赞同,并让我向 Spring 提交一个新的特性请求。结果,在 Spring 未来的一个版本中(目前预定为 3.1RC1),脚本源代码定位符机制将变成可插拔的(参见 参考资料)。

静态和基于 Resource 的脚本为定义脚本提供了很多位置,但是基于种种原因,您可能想使用数据库作为存放脚本的位置。例如,很多组织不允许对生产机器进行文件系统访问,或者他们可能需要 WAR 或 EAR 文件形式的部署。此外,数据库是大多数组织已经在使用并且熟悉的事务性资源。数据库还为集中式数据访问提供了一种比较简单的方式并可以保证安全性,这种方式不需要知道关于文件系统、服务器等的细节。最后,将脚本存储在数据库中意味着可以通过允许用户编辑脚本来在应用程序中更新脚本。(当然,如果将活动的代码存储在一个数据库中,那么需要考虑潜在的安全性问题,并适当地确保应用程序的安全。)

假设您希望将 Groovy 脚本存储在一个关系数据库中。从 Spring 2.5 开始,可以创建新的脚本类型,但是首先必须创建自己的 ScriptSource,并扩展一些 Spring 类。特别是,您需要定义自己的 ScriptSource 实现,并修改 Spring 的 ScriptFactoryPostProcessor,使它知道如何使用新的 ScriptSource 类型。

清单 11 实现一个 DatabaseScriptSource,它使用 Spring JDBC 从一个关系数据库装载脚本:


清单 11. DatabaseScriptSource 实现

C-sharp代码

  1. public?class?DatabaseScriptSource?implements?ScriptSource?{ ??
  2. ??
  3. ????private?final?String?scriptName; ??
  4. ????private?final?JdbcTemplate?jdbcTemplate; ??
  5. ????private?Timestamp?lastKnownUpdate; ??
  6. ??
  7. ????private?final?Object?lastModifiedMonitor?=?new?Object(); ??
  8. ??
  9. ????public?DatabaseScriptSource(String?scriptName,?DataSource?dataSource)?{ ??
  10. ????????this.scriptName?=?scriptName; ??
  11. ????????this.jdbcTemplate?=?new?JdbcTemplate(dataSource); ??
  12. ????} ??
  13. ??
  14. ????public?String?getScriptAsString()?throws?IOException?{ ??
  15. ????????synchronized?(this.lastModifiedMonitor)?{ ??
  16. ????????????this.lastKnownUpdate?=?retrieveLastModifiedTime(); ??
  17. ????????} ??
  18. ????????return?(String)?jdbcTemplate.queryForObject( ??
  19. ????????????????"select?script_source?from?groovy_scripts?where?script_name?=??",??
  20. ????????????????new?Object[]{?this.scriptName?},?String.class); ??
  21. ????} ??
  22. ??
  23. ????public?boolean?isModified()?{ ??
  24. ????????synchronized?(this.lastModifiedMonitor)?{ ??
  25. ????????????Timestamp?lastUpdated?=?retrieveLastModifiedTime(); ??
  26. ????????????return?lastUpdated.after(this.lastKnownUpdate); ??
  27. ????????} ??
  28. ????} ??
  29. ??
  30. ????public?String?suggestedClassName()?{ ??
  31. ????????return?StringUtils.stripFilenameExtension(this.scriptName); ??
  32. ????} ??
  33. ??
  34. ????private?Timestamp?retrieveLastModifiedTime()?{ ??
  35. ????????return?(Timestamp)?this.jdbcTemplate.queryForObject( ??
  36. ????????????????"select?last_updated?from?groovy_scripts?where?script_name?=??",?Timestamp.class); ??
  37. ????} ??
  38. }??

清单 11 中的 DatabaseScriptSource 非常简单,不过您可以让它要求的数据库表结构更加通用。它假设一个名为 groovy_scripts 的表有 script_namescript_source last_updated 这几个列。它支持从 groovy_scripts 表装载脚本和检查修改情况。

现在,需要教会 Spring 识别 DatabaseScriptSource。为此,必须扩展 ScriptFactoryPostProcessor 并覆盖 convertToScriptSource 方法,该方法负责将一个脚本源代码定位符(例如 classpath:groovierspring/GroovyPdfGenerator.groovy)转换成一个 ScriptSource。清单 12 显示了 ScriptFactoryPostProcessor 中的默认实现:


清单 12. ScriptFactoryPostProcessor convertToScriptSource 方法

				
protected ScriptSource convertToScriptSource(
        String beanName,String scriptSourceLocator,ResourceLoader resourceLoader) {

    if (scriptSourceLocator.startsWith(INLINE_SCRIPT_PREFIX)) {
        return new StaticScriptSource(
                scriptSourceLocator.substring(INLINE_SCRIPT_PREFIX.length()),beanName);
    }
    else {
        return new ResourceScriptSource(resourceLoader.getResource(scriptSourceLocator));
    }
}

可以看到,默认的实现只处理内联和基于资源的脚本。还可以创建 ScriptFactoryPostProcessor 的一个新的子类,并覆盖 convertToScriptSource 方法,以便使用 DatabaseScriptSource 从数据库装载脚本,如清单 13 所示:


清单 13. CustomScriptFactoryPostProcessor 实现

C-sharp代码

  1. public?class?CustomScriptFactoryPostProcessor?extends?ScriptFactoryPostProcessor?{ ??
  2. ??
  3. ????public?static?final?String?DATABASE_SCRIPT_PREFIX?=?"database:"; ??
  4. ??
  5. ????private?DataSource?dataSource; ??
  6. ??
  7. ????@Required ??
  8. ????public?void?setDataSource(DataSource?dataSource)?{ ??
  9. ????????this.dataSource?=?dataSource; ??
  10. ????} ??
  11. ??
  12. ????@Override ??
  13. ????protected?ScriptSource?convertToScriptSource(String?beanName,??
  14. ?????????????????????????????????????????????????String?scriptSourceLocator,??
  15. ?????????????????????????????????????????????????ResourceLoader?resourceLoader)?{ ??
  16. ????????if?(scriptSourceLocator.startsWith(INLINE_SCRIPT_PREFIX))?{ ??
  17. ????????????return?new?StaticScriptSource( ??
  18. ????????????????scriptSourceLocator.substring(INLINE_SCRIPT_PREFIX.length()),?beanName); ??
  19. ????????} ??
  20. ????????else?if?(scriptSourceLocator.startsWith(DATABASE_SCRIPT_PREFIX))?{ ??
  21. ????????????return?new?DatabaseScriptSource( ??
  22. ????????????????scriptSourceLocator.substring(DATABASE_SCRIPT_PREFIX.length()),??
  23. ????????????????dataSource); ??
  24. ????????} ??
  25. ????????else?{ ??
  26. ????????????return?new?ResourceScriptSource( ??
  27. ????????????????resourceLoader.getResource(scriptSourceLocator)); ??
  28. ????????} ??
  29. ????} ??
  30. ??
  31. }??

以上清单中的 CustomScriptFactoryPostProcessor 类似于 ScriptFactoryPostProcessor,但是,如果脚本源代码定位符以 database: 开始(例如 database:groovierspring/GroovyPdfGenerator.groovy),它还可以使用基于数据库的脚本。理想情况下,这种机制将更加灵活(参见 可插拔脚本源代码定位符 侧边栏)。但是,就现在而言,您已经有了在数据库中存储 Groovy 脚本所需的东西。

最后要做的就是配置 pdfGenerator bean,以便从数据库读取它。首先,需要使用 清单 13 中显示的 CustomScriptFactoryPostProcessor 定义一个 scriptFactoryPostProcessor bean。然后,使用数据库脚本源代码定位符定义 pdfGenerator bean。既可以使用单纯的 <bean/> 语法,也可以使用更清晰的 <lang:groovy> 语法来定义 pdfGenerator bean。当使用 <lang:groovy> 时,Spring 检查在名为 scriptFactoryPostProcessor 的应用程序上下文中是否有一个 ScriptFactoryPostProcessor bean,如果没有,则自动创建一个。如果 scriptFactoryPostProcessor 已经被定义,则 Spring 就使用这个 bean,这样您就可以替换自己定制的实现。清单 14 显示了新的配置:


清单 14. 数据库 pdfGenerator bean 配置

				
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/GroovierSpringDataSource"/>

<bean id="scriptFactoryPostProcessor"
      class="groovierspring.CustomScriptFactoryPostProcessor">
    <property name="dataSource" ref="dataSource"/>
</bean>

<lang:groovy id="pdfGenerator"
             refresh-check-delay="60000"
             script-source="database:groovierspring/GroovyPdfGenerator.groovy">
    <lang:property name="companyName" value="Database Groovy Bookstore"/>
</lang:groovy>

清单 14 中的代码并不比您在前面看到的代码复杂多少。scriptFactoryPostProcessor bean 要求注入一个 DataSource,所以还要定义 dataSource bean。除此之外,惟一不同的地方是基于 CLASSPATH 的脚本变成了数据库中的脚本。如果您更倾向于使用 Grails Bean Builder,那么可以轻松地用它来配置数据源和定制的 ScriptFactoryPostProcessor bean。

至此,您可以从数据库装载 Groovy 脚本,并在数据库中的脚本被更改之后刷新它们,这使得 Spring 原本已经灵活的 Groovy 支持变得更加灵活和动态。您还看到了如何添加自己的 ScriptSource 实现,以允许从选择的任何位置装载脚本。

Groovy 脚本变坏

也许每个人都同意应该彻底地对应用程序进行测试,至于如何测试,大家却各执己见。例如,100% 的代码覆盖是必需的,还是可选的,抑或纯粹是浪费时间?无论您个人的观点如何,当您突然有能力将变化部署到一个正在运行的生产系统中,并且让那些变化立即生效时(例如用 Spring 的动态语言支持就可以做到),测试就变得非常重要。

如果您决定使用 refreshable beans 特性,那么需要一个可靠的策略来确保新的代码能够正确地、符合预期地工作。至于如何有效地这样做取决于您所处的环境:

  • 系统的关键程度?
  • 如果中断某项内容,会有什么影响?
  • 修复问题的速度有多快?

您的特定环境可能还涉及更多方面的考虑,但是总而言之,bean-refresh 特性既是强大的,但是又 存在潜在危险。您需要慎重地使用它。可能遇到的两种主要问题是脚本编译错误和运行时错误。

脚本编译错误

假设您在运行时更改一个脚本,使之不能编译。当 Spring 检测到更改时,它尝试重新装载 bean,这时会抛出一个 ScriptCompilationException,后者包装原始的异常,例如一个 Groovy MultipleCompilationErrorsException。当出现这种情况时,Spring 不再尝试重新装载 bean,原始的 bean 继续使用,就像什么事情也没有发生一样。您的应用程序需要适当地对 ScriptCompilationException 作出响应。很可能,您应该显示某种错误消息,并向开发人员或操作人员发送一个通知(例如一条 e-mail 消息或即时消息)。当然,不管是谁部署脚本更改,都应该监视应用程序,以确保脚本成功编译,并且新的 bean 替换旧的 bean。

但是没有任何损失,因为没有通过编译的脚本对已经部署的 bean 没有影响。所以,您可以修复导致编译异常的问题,然后再次尝试。当 bean 编译成功时,Spring 用新的 bean 替换已有的 bean,而这对于应用程序代码来说是透明的。现在,您的更改已经生效,这一切都不需要重新部署或者重新启动应用程序。

运行时脚本错误

运行时脚本错误存在与编译代码抛出的运行时错误一样的问题:它们导致应用程序出现一个失败条件,后者很可能被传播到用户,并导致他们尝试执行的任何动作都失败。例如,假设您对 GroovyPdfGenerator 作了更改,使之可以编译,但是每当它尝试生成一个 PDF 时都会抛出一个运行时错误。在此情况下,使用 pdfGenerator 的代码必须要么处理异常,要么传播它,并且很有可能,用户将收到一个错误消息,表明不能生成一个 PDF。(并且这个问题将尽快被修复!)

但是,和脚本编译错误一样,当出现运行时脚本错误时,并没有带来什么损失。实际上,由于脚本可以在运行时更改,与编译过的代码相比,它们更容易被修复。您可以修复脚本中存在的任何问题,一旦脚本被重新装载,问题将不复存在。所以,从某种角度来说,在运行时更改代码的能力不仅在做出更改方面给予您更大的灵活性,而且也在出现错误时给予您更大的灵活性。但是,这并不意味着应该让 Spring 应用程序中的所有 bean 都是可刷新的。与很多事情一样,可刷新 bean 最好是适度地使用。

脚本安全性

最后要考虑的重要一点是安全性。保证脚本的安全,并确保只有经过授权的用户或管理员能够修改它们,这一点很重要。在某些方面,这与如何确保应用程序其他部分的安全没有区别。例如,大多数应用程序需要确保数据的完整性,并使用户只能访问特定于其角色或权限的功能。但是另一方面,这个功能也可能为黑客入侵系统并更改数据和系统行为打开新的方便之门。您当然希望减少应用程序的攻击面,因此和所有设计上的权衡一样,您必须权衡优点和缺点。

使安全性变得更加重要的是,借助 Spring 的动态语言支持,您不仅可以更改系统数据,还可以更改系统行为。在某一程度上确实如此:想想 SQL 注入攻击,这种攻击可注入恶意代码,还有 JavaScript 跨站点脚本编制,或者跨站点请求伪造攻击,这些攻击可以更改或替换系统的行为。我认为,您需要知道如何对 Groovy 脚本进行适当的控制,如果这些脚本是可刷新的,那么更应该这样做。

取决于如何使用可刷新 bean,随之产生的安全风险有可能超过在运行时更改行为所带来的好处。试想一个面向客户的销售应用程序,该应用程序需要经常更改为客户提供折扣的规则,或者想象一个保险应用程序,这个应用程序的业务规则可能经常被更改。在此情况下,可以设计一个用 Groovy 编写的 DSL,销售人员或保险代理可以对其进行更改,以适应当前的业务需要。也许您想添加一点逻辑,以便对超过 50 美元的商品提供 10% 的折扣。当然,可以通过允许用户直接在正在运行的应用程序中编辑小块的 DSL,来适应这种类型的更改。或者,也可以设计一个图形化的编辑器,以便用户用它来更改折扣策略。

结束语

您已经看到了如何使用编译过的 Groovy 类或动态编译和装载的脚本将 Groovy 集成到基于 Spring 的应用程序中。您还知道如何使脚本化的 Groovy bean 实现可刷新功能,如何在创建时定制 Groovy bean,以及如何将它们存储在关系数据库中。您了解到脚本编译和运行时错误如何以不同的方式影响正在运行的应用程序,以及可刷新 bean 如何使得在运行时修复 bug 比使用传统的架构更加容易,当使用传统架构时,需要重新部署或者重新启动应用程序。最后,我简要地谈到了脚本化的 bean 和可刷新 bean 的安全性问题,并提醒您需要充分评估应用程序所需的安全性级别。

Spring 和 Groovy 组成了一个强大的组合:Spring 提供架构和基础设施,而 Groovy 则增加动态能力。Spring 在 Groovy 脚本改变时重新装载它们的能力可以将您的应用程序带到未知的领域。但是要记住:“能力越大,责任越重。” 为应用程序增加更多的动态性当然可以使应用程序更加灵活和强大,但是这也可能带来前所未有的问题和挑战。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读