在任何无法理解的情况下,请编写脚本
腳本編寫(xiě)是使您的應(yīng)用程序在運(yùn)行時(shí)就可以根據(jù)客戶需求進(jìn)行調(diào)整的最流行的方法之一。 與往常一樣,此方法不僅帶來(lái)好處,例如,在靈活性和可管理性之間存在眾所周知的折衷方案。 本文不是從理論上討論優(yōu)缺點(diǎn)的文章之一,而是從實(shí)踐上展示了如何采用腳本的不同方法,并介紹了一個(gè)Spring庫(kù),該庫(kù)提供了方便的腳本基礎(chǔ)結(jié)構(gòu)和有用的功能。
介紹
腳本(也稱為插件體系結(jié)構(gòu))是使應(yīng)用程序在運(yùn)行時(shí)可自定義的最直接方法。 通常,腳本不是偶然進(jìn)入設(shè)計(jì)的,而是偶然進(jìn)入應(yīng)用程序的。 說(shuō),您在功能規(guī)范中有一個(gè)非常不清楚的部分,因此為了避免浪費(fèi)一天來(lái)進(jìn)行額外的業(yè)務(wù)分析,我們決定創(chuàng)建一個(gè)擴(kuò)展點(diǎn)并調(diào)用一個(gè)實(shí)現(xiàn)存根的腳本-將闡明以后的工作方式。
使用這種方法有很多眾所周知的利弊:例如,在運(yùn)行時(shí)定義業(yè)務(wù)邏輯的靈活性非常大,可以節(jié)省大量的重新部署時(shí)間,而無(wú)法進(jìn)行全面的測(cè)試,因此,安全性,性能問(wèn)題是無(wú)法預(yù)測(cè)的問(wèn)題等等。
對(duì)于已經(jīng)決定在其Java應(yīng)用程序中堅(jiān)持使用腳本插件的人,或者只是考慮將其添加到代碼中的人,進(jìn)一步討論的腳本編寫(xiě)方法可能會(huì)有所幫助。
沒(méi)什么特別的,只是腳本
使用Java的JSR-233 API,用Java評(píng)估腳本是一項(xiàng)簡(jiǎn)單的任務(wù)。 為此API實(shí)現(xiàn)了許多生產(chǎn)級(jí)評(píng)估引擎(Nashorn,JRuby,Jython等),因此向Java代碼中添加一些腳本魔術(shù)不是問(wèn)題,如下所示:
Map parameters = createParametersMap();ScriptEngineManager manager = new ScriptEngineManager();ScriptEngine scriptEngine = manager.getEngineByName("groovy");Object result = scriptEngine.eval(script.getScriptAsString("discount.groovy"), new SimpleBindings(parameters));顯然,當(dāng)代碼庫(kù)中有多個(gè)腳本文件和一個(gè)調(diào)用時(shí),將這樣的代碼散布在您的所有應(yīng)用程序上并不是一個(gè)好主意,因此您可以將此代碼段提取到放置到實(shí)用程序類的單獨(dú)方法中。 有時(shí),您甚至可以走得更遠(yuǎn):您可以創(chuàng)建一個(gè)特殊的類(或類集),以基于業(yè)務(wù)域?qū)⒛_本化的業(yè)務(wù)邏輯分組,例如PricingScriptService類。 這將使我們將對(duì)validateGroovy ()的調(diào)用包裝到一個(gè)不錯(cuò)的強(qiáng)類型方法中,但是仍然有一些樣板代碼,所有方法都將包含參數(shù)映射,腳本文本加載邏輯和腳本評(píng)估引擎調(diào)用,類似于:
public BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) {Map params = new HashMap<>();params.put("cust", customer);params.put("amount", orderAmount);return (BigDecimal)scripting.evalGroovy(getScriptSrc("discount.groovy"), params); }這種方法在了解參數(shù)類型和返回值類型方面帶來(lái)了更大的透明度。 并且不要忘記在您的編碼標(biāo)準(zhǔn)文檔中添加禁止“未包裝”腳本引擎調(diào)用的規(guī)則!
類固醇腳本
盡管使用腳本引擎非常簡(jiǎn)單,但是如果代碼庫(kù)中有很多腳本,您可能會(huì)遇到一些性能問(wèn)題。 舉個(gè)例子–您使用Groovy模板進(jìn)行報(bào)告并同時(shí)運(yùn)行許多報(bào)告。 遲早您會(huì)發(fā)現(xiàn)“簡(jiǎn)單”腳本正在成為性能瓶頸。
這就是為什么某些框架在現(xiàn)有API上構(gòu)建自己的腳本引擎的原因,并添加了一些不錯(cuò)的功能以提高性能,執(zhí)行監(jiān)視,多語(yǔ)言腳本等。
例如,在CUBA框架中,有一個(gè)相當(dāng)復(fù)雜的腳本引擎,該引擎實(shí)現(xiàn)了一些功能來(lái)改善腳本的實(shí)現(xiàn)和執(zhí)行,例如:
所有這些都提高了性能和可用性,但是它們?nèi)匀皇怯糜趧?chuàng)建參數(shù)映射,獲取腳本文本等的低級(jí)API,因此,我們?nèi)匀恍枰獙⑺鼈兎纸M為高階模塊,以在應(yīng)用程序中有效地使用腳本。
而且,不提及新的實(shí)驗(yàn)性GraalVM引擎及其允許使用其他語(yǔ)言擴(kuò)展Java應(yīng)用程序的多語(yǔ)言API,這是不公平的。 因此,也許我們會(huì)看到Nashorn早晚退休,并能夠在同一源文件中以不同的編程語(yǔ)言編寫(xiě),但是將來(lái)仍然如此。
Spring框架:很難拒絕的提議?
在Spring Framework中,我們對(duì)JDK的API提供了內(nèi)置的腳本支持,您可以在org.springframework.scripting。*包中找到很多有用的類。 這里有評(píng)估人員,工廠等。所有構(gòu)建您自己的腳本支持所需的工具。
除了底層API之外,Spring Framework的實(shí)現(xiàn)還應(yīng)簡(jiǎn)化應(yīng)用程序中腳本的處理-您可以按照文檔中的描述定義以動(dòng)態(tài)語(yǔ)言實(shí)現(xiàn)的bean。
您需要做的就是使用動(dòng)態(tài)語(yǔ)言(例如Groovy)實(shí)現(xiàn)一個(gè)類,并在配置XML中描述一個(gè)bean,如下所示:
<lang:groovy id="messenger" script-source="classpath:Messenger.groovy"><lang:property name="message" value="I Can Do The Frug" /> </lang:groovy>之后,您可以使用XML config將Messenger Bean注入到應(yīng)用程序類中。 可以在基礎(chǔ)腳本更改的情況下自動(dòng)“刷新”該bean,并與AOP等一起使用。
這種方法看起來(lái)不錯(cuò),但是作為開(kāi)發(fā)人員,如果您想利用動(dòng)態(tài)語(yǔ)言支持的所有功能,則應(yīng)為您的bean實(shí)現(xiàn)成熟的類。 在現(xiàn)實(shí)生活中,腳本可能是純函數(shù),因此您需要向腳本中添加一些額外的代碼,以使其與Spring兼容。 如今也有一些開(kāi)發(fā)人員認(rèn)為XML配置與注解相比已“過(guò)時(shí)”,并試圖避免使用它,因?yàn)閎ean定義和注入在Java代碼和XML代碼之間進(jìn)行了劃分。 盡管這更多是關(guān)于品味的問(wèn)題,而不是性能/兼容性/可讀性等問(wèn)題,但我們可以考慮到它。
腳本:挑戰(zhàn)和想法
因此,一切都有其代價(jià),當(dāng)您向應(yīng)用程序中添加腳本時(shí),您可能會(huì)遇到一些挑戰(zhàn):
似乎在常規(guī)Java方法下隱藏腳本化方法調(diào)用可以解決其中的大多數(shù)難題。 首選方式–注入“腳本化” bean并使用有意義的名稱調(diào)用其方法,而不是僅從實(shí)用工具類中調(diào)用另一個(gè)“ eval”方法。 因此,我們的代碼正變得自我記錄,開(kāi)發(fā)人員無(wú)需查看文件“ disc_10_cl.groovy”即可確定參數(shù)名稱,類型等。
另一個(gè)優(yōu)勢(shì)–如果所有腳本都有與之關(guān)聯(lián)的唯一Java方法,則可以使用IDE中的“查找用法”功能輕松找到應(yīng)用程序中的所有擴(kuò)展點(diǎn),以及了解該腳本的參數(shù)及其含義。返回。
這種執(zhí)行腳本的方式也使測(cè)試變得更加簡(jiǎn)單–我們不僅能夠“照常”測(cè)試這些類,而且還可以在需要時(shí)使用模擬框架。
所有這些都使我們想起了本文開(kāi)頭提到的方法–腳本方法的“特殊”類。 而且,如果我們更進(jìn)一步,并隱藏開(kāi)發(fā)人員對(duì)腳本引擎,參數(shù)創(chuàng)建等的所有調(diào)用,該怎么辦?
腳本存儲(chǔ)庫(kù)概念
這個(gè)想法非常簡(jiǎn)單,并且所有使用Spring Framework的開(kāi)發(fā)人員都應(yīng)該熟悉。 我們只是創(chuàng)建一個(gè)Java接口并將其方法以某種方式鏈接到腳本。 例如,Spring Data JPA使用類似的方法,其中接口方法根據(jù)方法名稱轉(zhuǎn)換為SQL查詢,然后由ORM引擎執(zhí)行。
我們可能需要執(zhí)行什么概念?
可能是一個(gè)類級(jí)別的注釋,它將幫助我們檢測(cè)腳本存儲(chǔ)庫(kù)接口并為其構(gòu)造一個(gè)特殊的Spring bean。
方法級(jí)別的注釋將幫助我們將方法鏈接到其腳本實(shí)現(xiàn)。
最好為該方法提供一個(gè)默認(rèn)實(shí)現(xiàn),它不是簡(jiǎn)單的存根,而是業(yè)務(wù)邏輯的有效部分。 在我們實(shí)現(xiàn)由業(yè)務(wù)分析師開(kāi)發(fā)的算法之前,該方法將一直有效。 或者我們可以讓他/她編寫(xiě)此腳本:-)
假設(shè)您需要?jiǎng)?chuàng)建一個(gè)服務(wù)來(lái)根據(jù)用戶個(gè)人資料計(jì)算折扣。 而且業(yè)務(wù)分析師說(shuō),我們可以放心地假設(shè)默認(rèn)情況下可以為所有注冊(cè)客戶提供10%的折扣。 對(duì)于這種情況,我們可能會(huì)考慮以下代碼概念:
@ScriptRepository public interface PricingRepository {@ScriptMethoddefault BigDecimal applyCustomerDiscount(Customer customer,BigDecimal orderAmount) {return orderAmount.multiply(new BigDecimal("0.9"));} }當(dāng)涉及適當(dāng)?shù)恼劭鬯惴▽?shí)現(xiàn)時(shí),groovy腳本將如下所示:
-------- file discount.groovy -------- def age = 50 if ((Calendar.YEAR - cust.birthday.year) >= age) {return amount.multiply(0.75) } --------這一切的最終目標(biāo)–讓開(kāi)發(fā)人員僅實(shí)現(xiàn)唯一的接口和折扣算法腳本,而不要對(duì)所有這些“ getEngine”和“ eval”調(diào)用感到困惑。 腳本解決方案應(yīng)該發(fā)揮所有魔力:當(dāng)方法被調(diào)用時(shí),攔截調(diào)用,查找并加載腳本文本,對(duì)其進(jìn)行評(píng)估并返回結(jié)果(或者,如果找不到腳本文本,則執(zhí)行默認(rèn)方法)。 理想用法應(yīng)與此類似:
@Service public class CustomerServiceBean implements CustomerService {@Injectprivate PricingRepository pricingRepository;//Other injected beans here@Overridepublic BigDecimal applyCustomerDiscount(Customer cust, BigDecimal orderAmnt) {if (customer.isRegistered()) {return pricingRepository.applyCustomerDiscount(cust, orderAmnt);} else {return orderAmnt;}//Other service methods here}腳本調(diào)用是可讀的,我猜想任何Java開(kāi)發(fā)人員都熟悉腳本的調(diào)用方式。
這些就是想法,它們被用來(lái)為使用Spring Framework的腳本存儲(chǔ)庫(kù)實(shí)現(xiàn)創(chuàng)建一個(gè)庫(kù)。 該庫(kù)具有用于從不同來(lái)源加載和評(píng)估腳本文本的功能,以及一些API,允許開(kāi)發(fā)人員在需要時(shí)實(shí)現(xiàn)庫(kù)的擴(kuò)展。
怎么運(yùn)行的
該庫(kù)引入了一些注釋(以及那些喜歡它的人的XML配置),這些注釋在上下文初始化期間為所有標(biāo)記有@ScriptRepository注釋的存儲(chǔ)庫(kù)接口啟動(dòng)動(dòng)態(tài)代理構(gòu)建。 這些代理以實(shí)現(xiàn)存儲(chǔ)庫(kù)接口的單例Bean的形式發(fā)布,這意味著您可以使用@Autowired或@Inject將這些代理完全注入到Bean中,如上一節(jié)中的代碼片段所示。
在應(yīng)用程序配置類之一上使用@EnableSpringRepositories批注可激活腳本存儲(chǔ)庫(kù)。 這種方法類似于其他熟悉的Spring注釋,例如@EnableJpaRepositories或@EnableMongoRepositories。 并且對(duì)于此批注,您需要指定應(yīng)類似于JPA存儲(chǔ)庫(kù)進(jìn)行掃描的軟件包名稱數(shù)組。
@Configuration @EnableScriptRepositories(basePackages = {"com.example", "com.sample"}) public class CoreConfig { //More configuration here. }如前所示,我們需要使用@ScriptMethod標(biāo)記腳本存儲(chǔ)庫(kù)中的每個(gè)方法(庫(kù)也提供@GroovyScript和@JavaScript ),以將元數(shù)據(jù)添加到這些調(diào)用中并指示這些方法已編寫(xiě)腳本。 當(dāng)然,還支持腳本方法的默認(rèn)實(shí)現(xiàn)。 解決方案的所有組件都顯示在下圖中。 藍(lán)色形狀與應(yīng)用程序代碼相關(guān),白色形狀與庫(kù)相關(guān)。 Spring Bean標(biāo)記有春天徽標(biāo)。
調(diào)用接口的腳本化方法時(shí),該代理類將攔截該代理方法,該代理類將對(duì)兩個(gè)bean進(jìn)行查找-一個(gè)提供程序以實(shí)現(xiàn)腳本文本,以及一個(gè)評(píng)估程序以獲取結(jié)果。 腳本評(píng)估后,結(jié)果將返回到調(diào)用服務(wù)。 提供程序和評(píng)估程序都可以在@ScriptMethod批注屬性以及執(zhí)行超時(shí)中指定(盡管庫(kù)為這些屬性提供了默認(rèn)值):
@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")); } }您可能會(huì)注意到@ScriptParam批注-我們需要它們?yōu)榉椒ǖ膮?shù)提供名稱。 這些名稱應(yīng)在腳本中使用,因?yàn)镴ava編譯器會(huì)在編譯時(shí)刪除實(shí)際的參數(shù)名稱。 您可以省略這些注釋,在這種情況下,您需要將腳本的參數(shù)命名為“ arg0”,“ arg1”等,這會(huì)影響代碼的可讀性。
默認(rèn)情況下,該庫(kù)具有提供程序,可以從兩種腳本語(yǔ)言的文件系統(tǒng)和基于JSR-233的評(píng)估器讀取groovy和javascript文件。 但是,您可以為不同的腳本存儲(chǔ)和執(zhí)行引擎創(chuàng)建自定義提供程序和評(píng)估程序。 所有這些功能都基于Spring框架接口( org.springframework.scripting.ScriptSource和org.springframework.scripting.ScriptEvaluator ),因此您可以重用所有基于Spring的類,例如StandardScriptEvaluator而不是默認(rèn)類。
提供程序(以及評(píng)估程序)以Spring Bean的形式發(fā)布,因?yàn)槟_本存儲(chǔ)庫(kù)代理為了靈活性而按名稱解析它們-您可以用新的executor代替默認(rèn)執(zhí)行程序,而無(wú)需更改應(yīng)用程序代碼,而是在應(yīng)用程序上下文中替換一個(gè)Bean。
測(cè)試和版本控制
由于可以輕松更改腳本,因此我們需要確保在更改腳本時(shí)不會(huì)破壞生產(chǎn)服務(wù)器。 該庫(kù)與JUnit測(cè)試框架兼容,沒(méi)有什么特別的。 由于您在基于Spring的應(yīng)用程序中使用腳本,因此可以將單元測(cè)試和集成測(cè)試作為應(yīng)用程序的一部分來(lái)測(cè)試腳本,然后再將其上傳到生產(chǎn)環(huán)境,因此還支持模擬。
另外,您可以創(chuàng)建一個(gè)腳本提供程序,以從數(shù)據(jù)庫(kù)甚至Git或其他源代碼控制系統(tǒng)中讀取不同的腳本文本版本。 在這種情況下,如果生產(chǎn)中出現(xiàn)問(wèn)題,則很容易切換到較新的腳本版本或回滾到以前的腳本版本。
結(jié)論
該庫(kù)將幫助您安排代碼中的腳本,提供以下內(nèi)容:
在此Spring Boot的基礎(chǔ)上,還支持自動(dòng)配置,并且您還可以使用熟悉的單元測(cè)試和模擬技術(shù),在將腳本部署到生產(chǎn)之前測(cè)試腳本。
該庫(kù)具有用于在運(yùn)行時(shí)獲取腳本元數(shù)據(jù)(方法名稱,參數(shù)等)的API,如果您希望避免編寫(xiě)try..catch塊來(lái)處理腳本拋出的異常,則可以獲取包裝的執(zhí)行結(jié)果,它還支持XML配置,如果您希望以這種格式存儲(chǔ)配置。
同樣,可以通過(guò)注釋中的超時(shí)參數(shù)來(lái)限制腳本執(zhí)行時(shí)間。
可以在https://github.com/cuba-rnd/spring-script-repositories找到庫(kù)資源。
翻譯自: https://www.javacodegeeks.com/2018/11/incomprehensible-situation-scripting.html
總結(jié)
以上是生活随笔為你收集整理的在任何无法理解的情况下,请编写脚本的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: java 使用本机代理_Java与本机代
- 下一篇: 多个公证员提高网络吞吐量