在任何无法理解的情况下-编写脚本

图片

脚本是使应用程序更灵活,能够随时修复问题的最常用方法之一。 当然,这种方法也有缺点;您必须始终记住灵活性和可管理性之间的平衡。 但是在本文中,我们不会“一般性地”讨论使用脚本的优缺点,我们将考虑实现此方法的实用方法,还将介绍一个库,该库为将脚本添加到Spring Framework中编写的应用程序提供了便利的基础结构。

几句话


当您想增加无需重新编译和后续部署即可更改应用程序中的业务逻辑的功能时,脚本是首先想到的方法之一。 通常,出现脚本不是因为它是故意的,而是因为它发生了。 例如,在规范中有一部分逻辑目前尚不完全清楚,但是为了不花费额外的几天(有时甚至更长)进行分析,您可以创建一个扩展点并调用脚本-存根。 然后,当然,当需求明确时,将重写此脚本。

该方法不是新方法,其优缺点众所周知:灵活性-您可以更改正在运行的应用程序的逻辑并节省重新安装的时间,但另一方面,脚本更难测试,因此可能存在安全性,性能等问题。

这些技术(将在后面讨论)对于已经在其应用程序中使用脚本的开发人员以及正在考虑脚本的开发人员都可能有用。

没有什么私人的,只是脚本


使用JSR-233,Java脚本变得非常简单。 有足够的基于此API的脚本引擎(Nashorn,JRuby,Jython等),因此在代码中添加一些脚本魔术不是问题:

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)); 

显然,如果这样的代码散布在整个应用程序中,那么它将变成难以理解的东西。 而且,当然,如果您的应用程序中有多个脚本调用,则需要创建一个单独的类来使用它们。 有时,您甚至可以走得更远,并制作特殊的类,这些类将使用常规类型的Java方法包装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. 能够用Java和Groovy编写脚本
  2. 类缓存以免重新编译脚本
  3. JMX bin控制引擎

所有这些当然可以提高性能和可用性,但是低级引擎仍然是低级的,您仍然需要读取脚本文本,传递参数并调用API来执行脚本。 因此,您仍然需要在每个项目中进行某种包装,以使开发更加高效。

更不用说GraalVM了-这是一个实验性引擎,可以运行不同语言(JVM和非JVM)的程序,并允许您将这些语言的模块插入Java应用程序,这是不公平的。 我希望Nashorn迟早会成为历史,我们将有机会用一种语言用不同的语言编写部分代码。 但这只是一个梦想。

Spring框架:很难拒绝的提议?


Spring具有基于JDK API的内置脚本执行支持。 在org.springframework.scripting.*包中,您可以找到许多有用的类-全部都是这样,因此您可以方便地使用低级API在应用程序中编写脚本。

此外,还有更高级别的支持,请参见文档中的详细说明 。 简而言之-您需要使用脚本语言(例如Groovy)创建一个类,并通过XML描述将其发布为bean:

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

Bean发布后,可以使用IoC将其添加到其类中。 当更改文件中的文本时,Spring提供了脚本的自动更新,您可以将方法挂起。

看起来不错,但是您需要制作“真实的”类才能发布它们;您不能在脚本中编写常规函数。 另外,脚本只能存储在文件系统中,要使用您必须在Spring内部爬升的数据库。 是的,许多人认为XML配置已过时,尤其是在应用程序已经在批注中包含所有内容的情况下。 当然,这是调味剂,但通常不予理会。

剧本:难点和想法


因此,每种解决方案都有其各自的价格,如果我们谈论Java应用程序中的脚本,那么在引入该技术时,可能会遇到一些困难:

  1. 可管理性。 通常,脚本调用分散在整个应用程序中,并且随着代码的更改,很难跟踪必要脚本的调用。
  2. 查找拨号对等体的能力。 如果在特定脚本中出了问题,那么除非您通过文件名或方法调用(例如evaluateGroovy()应用搜索,否则查找其所有拨号evaluateGroovy()将是一个问题。
  3. 透明性 编写脚本本身并不是一件容易的事,对于那些调用此脚本的人来说,甚至更加困难。 您需要记住输入参数的名称,它们具有的数据类型以及执行的结果。 或每次查看脚本源代码。
  4. 测试和更新-并非总是可以在应用程序代码的环境中测试脚本,即使将脚本上载到“战斗”服务器后,您仍需要以某种方式能够在出现问题时快速回滚所有内容。

用Java方法包装脚本调用似乎可以帮助解决上述大多数问题。 如果可以在IoC容器中发布此类,并在其服务中使用正常的有意义的名称来调用方法,而不是从某个实用工具类中调用eval(“disc_10_cl.groovy”) ,那是非常好的。 另一个优点是代码可以自我记录,开发人员不必担心文件名后面隐藏了哪种算法。

此外,如果每个脚本仅与一种方法相关联,则可以使用IDE中的“查找用法”菜单快速找到应用程序中的所有拨号对等方,并了解脚本在每种特定业务逻辑算法中的位置。

测试得到了简化-使用熟悉的框架,模拟等等,它变成了“常规”类测试。

以上所有内容与本文开头提到的想法非常吻合-脚本实现的方法的“特殊”类。 但是,如果您又迈出了一步,并且隐藏了所有相同类型的服务代码以从开发人员那里调用脚本引擎,以至于他甚至都没有考虑它,那该怎么办?

脚本存储库-概念


这个想法很简单,并且至少与Spring一起工作的人应该熟悉,特别是与Spring JPA一起工作的人。 您需要做一个Java接口,并在调用其方法时调用该脚本。 顺便说一下,在JPA中,使用了相同的方法-截取对CrudRepository的调用,根据方法名称和参数,创建请求,然后由数据库引擎执行该请求。

实施该概念需要什么?

首先,一个类级别的注释,以便您可以找到接口-存储库并基于该接口创建一个bin。

同样,此接口的方法上的注释可能会派上用场,以便存储调用该方法所需的元数据。 例如-在哪里获取脚本文本以及使用哪个引擎。

一个有用的附加功能是能够在接口中使用具有实现方式的方法(也称为默认值)-该代码将一直起作用,直到业务分析师显示算法的更完整版本,并且开发人员根据
此信息。 或者让分析师编写脚本,然后开发人员只需将其复制到服务器即可。 有很多选择:-)

因此,假设对于一家在线商店,您需要提供一项服务以根据用户个人资料计算折扣。 目前尚不清楚如何执行此操作,但是业务分析师发誓所有注册用户都可以享受10%的折扣,他将在一周内从客户那里找到其余的折扣。 明天就需要服务-毕竟是季节。 这种情况下的代码是什么样的?

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

然后算法本身(例如以常规方式编写)将及时到达,那里的折扣会略有不同:

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

所有这一切的目的是使开发人员能够只编写接口代码和脚本代码,而不会弄乱对getEngineeval和其他调用的所有这些调用。 使用脚本的库应该发挥所有作用-拦截接口方法的调用,获取脚本文本,替换参数值,获取所需的脚本引擎,执行脚本(如果没有脚本文本则调用默认方法)并返回值。 理想情况下,除了已经编写的代码外,该程序还应具有以下内容:

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

挑战是可读性,可理解性的,要做到这一点,就不需要具备任何特殊技能。

这些想法是在此基础上创建了一个用于处理脚本的小型库的。 它旨在用于Spring应用程序,该框架用于创建库。 它提供了一个可扩展的API,用于从各种来源加载脚本并执行它们,从而隐藏了脚本引擎的日常工作。

如何运作


对于标记有@ScriptRepository所有接口,在Spring上下文初始化期间使用Proxy类的newProxyInstance方法创建代理对象。 这些代理在Spring上下文中以单例bean的形式发布,因此您可以声明具有接口类型的类字段,并在其上放置@Autowired@Inject批注。 完全按计划进行。

使用@EnableSriptRepositories批注来激活脚本界面的扫描和处理,与Spring激活MongoDB的JPA或存储库(分别为@EnableJpaRepositories@EnableMongoRepositories )的方式相同。 作为注释参数,您需要指定一个数组,其中包含要扫描的软件包的名称。

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

必须使用@ScriptMethod注释方法(还有@GroovyScript@JavaScript ,以及相应的专业名称)以添加用于调用脚本的元数据。 当然,支持接口中的默认方法。

该库的一般结构如图所示。 库中已经有需要开发的蓝色突出显示的组件,白色突出显示的组件。 Spring图标标记了Spring上下文中可用的组件。


当调用接口方法(实际上是代理对象)时,将启动调用处理程序,该调用处理程序在应用程序上下文中搜索两个bean:提供程序(将查找脚本文本)和执行程序(实际上将执行找到的文本)。 然后,处理程序将结果返回给调用方法。

提供者和执行者@ScriptMethod名称在@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和.js文件的提供程序,这些执行程序是对标准JSR-233 API的包装。 您可以为不同的脚本源和不同的引擎创建自己的bean,为此,您需要实现相应的接口: ScriptProviderSpringEvaluator 。 第一个接口使用org.springframework.scripting.ScriptSource ,第二个接口是org.springframework.scripting.ScriptEvaluator 。 使用Spring API,以便可以在应用程序中使用现成的类。
通过名称搜索提供者和艺术家以提供更大的灵活性-您可以通过使用相同的名称命名组件来替换应用程序库中的标准bean。

测试和版本控制


由于脚本会频繁且轻松地进行更改,因此您需要一种方法来确保所做的更改不会破坏任何内容。 该库与JUnit兼容,可以将存储库简单地作为常规类进行测试,作为单元或集成测试的一部分。 还支持模拟库,在库测试中,您可以找到有关如何在脚本存储库方法上进行模拟的示例。

如果需要版本控制,那么您可以创建一个提供程序,以从文件系统,数据库或Git中读取不同版本的脚本。 因此,在主服务器出现问题的情况下,组织回滚到脚本的先前版本将很容易。

合计


呈现的库将帮助组织Spring应用程序中的脚本:

  1. 开发人员将始终获得有关脚本需要哪些参数以及返回什么的信息。 如果接口方法被有意义地命名,那么脚本将执行什么操作。
  2. 提供者和执行者将帮助将用于接收脚本和与脚本引擎进行交互的代码放在一个地方,并且这些调用不会分散在整个应用程序代码中。
  3. 使用“查找用法”可以轻松找到所有脚本调用。

支持Spring Boot自动配置,单元测试,模拟。 您可以通过API获取有关“脚本”方法及其参数的数据。 而且,您还可以使用特殊的ScriptResult对象包装执行结果,如果您不想在调用脚本时烦恼try ... catch,则会在其中存在结果或异常实例。 如果出于某种原因需要XML配置,则支持该配置。 最后,如果需要,您可以为脚本方法指定超时。

库资源在这里。

Source: https://habr.com/ru/post/zh-CN431084/


All Articles