译:使用Spring框架编写脚本指南 | Spring For All

1,324 阅读13分钟
原文链接: www.spring4all.com

使用Spring框架编写脚本指南

原文链接: dzone.com/articles/in…

作者: Andrey Belyaev Aleksey Stukalov

译者: bliux

脚本编写是使您的应用程序在运行时可根据客户需求进行调整的最常用方法之一。与往常一样,这种方法引入了灵活性和可管理性之间的权衡。本文介绍了在项目中采用脚本的不同方法,以及如何整合提供方便的脚本基础结构和其他有用功能的Spring库。

介绍

脚本(也称为插件体系结构)是使您的应用程序在运行时可自定义的最直接的方法。很多时候,脚本不是通过设计而是偶然地进入您的应用程序。假设您在功能规范中有一个非常不清楚的部分,所以不要在另外的业务分析中浪费另一天,我们决定创建一个扩展点并调用实现存根的脚本。我们将在稍后阐明它应该如何运作。使用这种方法有很多众所周知的优点和缺点,例如在运行时定义业务逻辑的灵活性很高,并且在重新部署时节省了大量时间。这避免了不可能执行全面测试,因此避免了安全性,性能问题等不可预测的问题。

没什么特别的,只是脚本

使用Java的JSR-233API,在Java中运行脚本是一项简单的任务。有很多应用于生产的脚本引擎实现了这些API(Nashorn,JRuby,Jython等),因此向Java代码添加一些脚本魔术并不是一个问题,如下所示:

Map<String, Object> parameters = createParametersMap();
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine scriptEngine = manager.getEngineByName("groovy");
Object result = scriptEngine.eval(script.getScriptAsString("discount.groovy"), 
                                  new SimpleBindings(parameters));

显然,当您在代码库中有多个脚本文件和一个调用时,将这些代码分散在所有应用程序上并不是一个好主意,因此您可以将此代码段提取到放置在实用程序类中的单独方法中。有时候,你甚至可能会走得更远。您可以创建一个特殊的类(或一组类),它们基于业务域(如类)对脚本化业务逻辑进行分组,像PricingScriptService。这将让我们调用包装evaluateGroovy() 成很好的强类型方法,但仍然有一些样板代码。所有方法都将包含参数映射,脚本文本加载逻辑和类似于此的脚本评估引擎调用:

public BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) {
  Map<String, Object> params = new HashMap<>();
  params.put("cust", customer);
  params.put("amount", orderAmount);
  return (BigDecimal)scripting.evalGroovy(getScriptSrc("discount.groovy"), params);
}

这种方法在了解参数类型和返回值类型方面带来了更多透明度。并且不要忘记在编码标准文档中添加禁止“解包”脚本引擎调用的规则!

脚本性能

尽管使用脚本引擎非常简单,但如果代码库中有很多脚本,则可能会遇到一些性能问题。例如,您可以使用Groovy模板同时报告和运行报告。迟早,你会发现“简单”的脚本正在成为性能瓶颈。

这就是为什么有些框架会在现有API上构建自己的脚本引擎,为更好的性能,执行监控,多语言脚本等添加一些不错的功能。

例如,在CUBA框架中,有一个非常复杂的脚本引擎,它实现了改进脚本实现和执行的功能,例如:

  1. 类缓存以避免重复的脚本编译

  2. 能够使用Groovy和Java语言编写脚本

  3. 用于脚本引擎管理的JMX bean

所有这些都提高了性能和可用性,但仍然是那些用于创建参数映射,获取脚本文本等的低级API。因此,我们仍然需要将它们分组为高阶模块,以便在应用程序中有效地使用脚本。

如果不提新的实验性GraalVM引擎及其多语言API允许我们 使用其他语言扩展Java应用程序,那将是不公平的。所以也许我们会看到Nashorn迟早会退休,并且能够在同一个源文件中编写不同的编程语言,但它在未来仍然存在。

Spring框架:一个难以拒绝的提议?

在Spring框架中,我们对JDK的API提供了内置的脚本支持。你可以在org.springframework.scripting.* 包中找到很多有用的类。有评估者,工厂和构建自己的脚本支持所需的所有工具。

除了低级API之外,Spring框架还有一个实现,可以简化应用程序中脚本的处理。您可以按照文档中的描述定义以动态语言实现的bean。

您需要做的就是使用像Groovy这样的动态语言实现一个类,并在配置XML中描述一个bean,如下所示:

<lang:groovy id="messenger" script-source="classpath:Messenger.groovy">
        <lang:property name="message" value="I Can Do The Frug" />
</lang:groovy>

之后,您可以使用XML配置将Messenger bean注入应用程序类。在底层脚本更改的情况下,可以自动“刷新”该bean,建议使用AOP等。

这种方法看起来不错,但作为开发人员,如果您想充分利用动态语言支持的所有功能,那么您应该为bean实现完整的类。真实的脚本可能是纯粹的功能; 因此,您需要在脚本中添加一些额外的代码,以使其与Spring兼容。此外,如今,一些开发人员认为XML配置与注解相比“过时”并试图避免使用它,因为bean定义和注入在Java代码和XML代码之间分散。虽然它更多的是品味而不是性能/兼容性/可读性等,但我们可能会考虑它。

脚本: 挑战和想法

因此,一切都有其代价,当您为应用程序添加脚本时,您可能会遇到一些挑战:

  • 可管理性 - 通常脚本分散在应用程序中,因此很难管理大量evaluateGroovy(或类似)调用。

  • 可发现性 — 如果调用脚本出现问题,很难找到源代码中的实际点。我们应该能够在IDE中轻松找到所有脚本调用点。

  • 透明度 — 编写脚本扩展并不是一件容易的事,因为没有关于发送到脚本的变量的信息,也没有关于它应该返回的结果的信息。最后,脚本编写只能由开发人员完成,并且只能查看源代码。

  • 测试和更新 — 部署(更新)新脚本总是很危险的。在生产之前无法回滚并且没有工具可以对其进行测试。

在常规Java方法下隐藏脚本方法调用似乎可以解决大多数这些挑战。最好的方法是通过注入“脚本化”bean并使用有意义的名称调用它们的方法,而不是从实用程序类调用另一个“eval”方法。因此,我们的代码变得自我记录,开发人员不需要查看文件“disc_10_cl.groovy”来确定参数名称,类型等。

另一个优点 - 如果所有脚本都具有与之关联的独特Java方法,则可以使用IDE中的“查找用法”功能轻松找到应用程序中的所有扩展点,以及了解此脚本的参数是什么以及他们回来了。

这种脚本编写方式使测试更简单 - 我们不仅可以“像往常一样”测试这些类,还可以根据需要使用模拟框架。

所有这些都提醒了本文开头提到的方法 - 脚本方法的“特殊”类。如果我们更进一步隐藏开发人员对脚本引擎,参数创建等的所有调用,该怎么办?

脚本仓库概念

这个想法非常简单,对于使用Spring框架的所有开发人员来说都应该很熟悉。我们只是创建一个Java接口,并以某种方式将其方法链接到脚本。作为示例,Spring Data JPA使用类似的方法,其中接口方法基于方法的名称转换为SQL查询,然后由ORM引擎执行。

我们可能需要实施这一概念吗?

我们可能需要一个类级别的注解来帮助我们检测脚本存储库接口并为它们构造一个特殊的Spring bean。

方法级注解将帮助我们将方法链接到其脚本实现。

如果方法的默认实现不是简单的存根,而是业务逻辑的有效部分,那将是很好的。它将一直有效,直到我们实现由业务分析师开发的算法。或者我们可以让他/她尝试编写脚本!

假设您需要创建服务以根据用户的个人资料计算折扣。业务分析师表示,我们可以放心地假设默认情况下可以为所有注册客户提供10%的折扣。对于这种情况,我们可能会考虑以下代码概念:

@ScriptRepository
public interface PricingRepository {
@ScriptMethod
default BigDecimal applyCustomerDiscount(Customer customer,BigDecimal orderAmount) {
return orderAmount.multiply(new BigDecimal("0.9"));
}
}

当涉及到适当的折扣算法实现时,Groovy脚本将如下所示:

def age = 50
if ((Calendar.YEAR - cust.birthday.year) >= age) {
  return amount.multiply(0.75)
} else {
  return amount.multiply(0.9)
}

所有这一切的最终目标是让开发人员仅实现接口和折扣算法脚本。此外,他们不会在那些“getEngine”和“eval”电话中摸索。脚本解决方案应该为您完成所有的魔术 - 调用方法时,拦截调用,查找并加载脚本文本,对其进行评估,然后返回结果(如果找不到脚本文本,则执行默认方法)。理想的用法应该类似于:

@Service
public class CustomerServiceBean implements CustomerService {
   @Inject
   private PricingRepository pricingRepository;
   //Other injected beans here
   @Override
   public BigDecimal applyCustomerDiscount(Customer cust, BigDecimal orderAmnt) {
   if (customer.isRegistered()) {
       return pricingRepository.applyCustomerDiscount(cust, orderAmnt);
   } else {
       return orderAmnt;
   }
   //Other service methods here
}

脚本调用是可读的,任何Java开发人员都应该熟悉它的调用方式。

这些是用于使用Spring框架为脚本存储库实现创建库的想法。该库具有来自不同来源和评估的脚本文本加载工具以及允许开发人员在需要时实现库扩展的API。

它是怎么工作的

该库引入了一些注解(以及那些喜欢它的XML配置),它为@ScriptRepository 在上下文初始化期间标注注解的所有存储库接口启动动态代理构造。这些代理发布为实现存储库接口的单例bean,这意味着您可以使用@Autowired或@Inject 完全按照上一节中的代码片段将这些代理注入到bean中。

@EnableSpringRepositories 用于其中一个应用程序配置类的注解,用于激活脚本存储库。这种方法类似于其他熟悉的Spring注解,如 @EnableJpaRepositories 或 @EnableMongoRepositories。对于此注解,您需要指定应与JPA存储库类似地扫描的包名。

@Configuration
@EnableScriptRepositories(basePackages = {"com.example", "com.sample"})
public class CoreConfig {
//More configuration here.
}

如前所示,我们需要在脚本存储库中标记每个方法 @ScriptMethod (库提供 @GroovyScript和 @JavaScript 同样),以向这些调用添加元数据并指示这些方法是脚本化的。当然,支持脚本方法的默认实现。

解决方案的所有组件都显示在下图中。蓝色形状与应用程序代码相关,白色与库相关。Spring bean标有Spring图标。

当调用接口的脚本方法时,它会被代理类拦截,代理类执行两个bean的查找 - 一个用于实现脚本文本的提供程序和一个用于获取结果的求值程序。在脚本执行之后,结果将返回给调用服务。

可以在@ScriptMethod 注解属性中指定提供者和赋值器以及执行超时(但库提供了这些属性的默认值):

@ScriptRepository
public interface PricingRepository {
@ScriptMethod (providerBeanName = "resourceProvider",
               evaluatorBeanName = "groovyEvaluator",
               timeout = 100)
    default BigDecimal applyCustomerDiscount(
      @ScriptParam("cust") Customer customer,
      @ScriptParam("amount") BigDecimal orderAmount) {
        return orderAmount.multiply(new BigDecimal("0.9"));
    }
}

您可能会注意到 @ScriptParam 注解。我们需要它们为方法的参数提供名称。这些名称应该在脚本中使用,因为Java编译器会删除编译中的实际参数名称。您可以省略这些注解。在这种情况下,您需要命名脚本的参数“arg0”,“arg1”等,这会影响代码的可读性。

默认情况下,库具有可以从文件系统读取Groovy和JavaScript文件的提供程序,以及用于两种脚本语言的基于JSR-233的评估程序。您可以为不同的脚本存储和执行引擎创建自定义提供程序和评估程序。所有这些工具都基于Spring框架接口(org.springframework.scripting.ScriptSource 和 org.springframework.scripting.ScriptEvaluator),因此您可以重用所有基于Spring的类,例如,StandardScriptEvaluator 而不是默认的类。

提供程序(以及评估程序)作为Spring bean发布,因为脚本存储库代理为了灵活性而按名称解析它们。您可以使用新的替换默认执行程序而不更改应用程序代码,在应用程序上下文中替换一个bean。

测试和版本控制

由于脚本可能很容易更改,因此我们需要确保在更改脚本时不会破坏生产服务器。该库与JUnit测试框架兼容。没什么特别的。由于您在基于Spring的应用程序中使用它,因此在将它们上载到生产环境之前,您可以使用单元测试和集成测试作为应用程序的一部分来测试脚本; 也支持Mocking。

此外,您可以创建一个脚本提供程序,从数据库甚至从Git或其他源代码控制系统读取不同的脚本文本版本。在这种情况下,如果生产中出现问题,最简单的方法是切换到较新的脚本版本或回滚到以前版本的脚本。

总结

该库将帮助您在代码中管理脚本,提供以下内容:

  • 通过引入Java接口,开发人员始终拥有有关脚本参数及其类型的信息。

  • 提供程序和评估程序可帮助您摆脱通过应用程序代码分散的脚本引擎调用。

  • 我们可以使用“查找用法(参考)”IDE命令或仅按方法名称进行简单的文本搜索,轻松找到应用程序代码中的所有脚本用法。

除此之外,还支持Spring Boot自动配置,您还可以在使用熟悉的单元测试和模拟技术将脚本部署到生产环境之前对其进行测试。

该库有一个用于在运行时获取脚本元数据(方法名称,参数等)的API,如果您想避免编写try.catch 块来处理脚本抛出的异常,则可以获得包装的执行结果。此外,如果您希望以此格式存储配置,它还支持XML配置。

最后,脚本执行时间可以通过注解中的超时参数进行限制。

资源可以在GitHub上找到。

关注社区公号,加入社区纯技术微信群