
Skripte sind eine der häufigsten Methoden, um eine Anwendung flexibler zu gestalten und unterwegs etwas zu reparieren. Natürlich hat dieser Ansatz auch Nachteile: Sie müssen sich immer an das Gleichgewicht zwischen Flexibilität und Verwaltbarkeit erinnern. In diesem Artikel werden wir jedoch nicht „allgemein“ über die Vor- und Nachteile der Verwendung von Skripten sprechen, sondern praktische Möglichkeiten zur Implementierung dieses Ansatzes in Betracht ziehen und eine Bibliothek einführen, die eine bequeme Infrastruktur zum Hinzufügen von Skripten zu Anwendungen bietet, die im Spring Framework geschrieben wurden.
Ein paar einleitende Worte
Wenn Sie die Möglichkeit hinzufügen möchten, die Geschäftslogik in einer Anwendung ohne Neukompilierung und anschließende Bereitstellung zu ändern, sind Skripte eine der Möglichkeiten, die Ihnen in den Sinn kommen. Oft erscheinen Skripte nicht, weil es beabsichtigt war, sondern weil es passiert ist. In der Spezifikation gibt es beispielsweise einen Teil der Logik, der derzeit nicht vollständig klar ist. Um jedoch nicht einige Tage (und manchmal auch länger) für die Analyse aufzuwenden, können Sie einen Erweiterungspunkt festlegen und ein Skript aufrufen - einen Stub. Und dann wird dieses Skript natürlich neu geschrieben, wenn die Anforderungen klar werden.
Die Methode ist nicht neu und ihre Vor- und Nachteile sind bekannt: Flexibilität - Sie können die Logik einer laufenden Anwendung ändern und bei einer Neuinstallation Zeit sparen. Andererseits sind Skripte schwieriger zu testen, daher mögliche Probleme mit Sicherheit, Leistung usw.
Diese Techniken, die später erläutert werden, können sowohl für Entwickler nützlich sein, die bereits Skripte in ihrer Anwendung verwenden, als auch für diejenigen, die nur darüber nachdenken.
Nichts Persönliches, nur Skripte
Mit JSR-233 ist das Scripting in Java sehr einfach geworden. Es gibt genügend Skript-Engines, die auf dieser API basieren (Nashorn, JRuby, Jython und einige mehr), sodass es kein Problem ist, Ihrem Code ein bisschen Skript-Magie hinzuzufügen:
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));
Wenn ein solcher Code in der gesamten Anwendung verteilt ist, wird er offensichtlich zu etwas Unverständlichem. Wenn Ihre Anwendung mehr als einen Skriptaufruf enthält, müssen Sie natürlich eine separate Klasse erstellen, um mit ihnen arbeiten zu können. Manchmal können Sie sogar noch weiter gehen und spezielle Klassen erstellen, die
evaluateGroovy()
-Aufrufe in reguläre typisierte Java-Methoden einschließen. Diese Methoden haben einen ziemlich einheitlichen Dienstprogrammcode, wie im Beispiel:
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); }
Dieser Ansatz erhöht die Transparenz beim Aufrufen von Skripten aus Anwendungscode erheblich. Sie können sofort sehen, welche Parameter das Skript akzeptiert, welchen Typ sie haben und was zurückgegeben wird. Die Hauptsache ist, nicht zu vergessen, den Code-Schreibstandards ein Verbot hinzuzufügen, Skripte nicht mit typisierten Methoden aufzurufen!
Wir pumpen Skripte auf
Trotz der Tatsache, dass Skripte einfach sind, gibt es eine echte Chance, auf Leistungsprobleme zu stoßen, wenn Sie viele davon haben und sie intensiv nutzen. Wenn Sie beispielsweise eine Reihe umfangreicher Vorlagen zum Generieren von Berichten verwenden und diese gleichzeitig ausführen, wird dies früher oder später zu einem der Engpässe bei der Anwendungsleistung.
Daher stellen viele Frameworks verschiedene Add-Ons über die Standard-API her, um die Arbeitsgeschwindigkeit, das Caching, die Überwachung der Ausführung, die Verwendung verschiedener Skriptsprachen in einer Anwendung usw. zu verbessern.
Beispielsweise wurde in CUBA eine ziemlich ausgeklügelte
Skript- Engine erstellt, die zusätzliche Funktionen unterstützt, wie z.
- Fähigkeit, Skripte in Java und Groovy zu schreiben
- Klassencache, um Skripte nicht neu zu kompilieren
- JMX-Behälter zur Steuerung des Motors
All dies verbessert natürlich die Leistung und Benutzerfreundlichkeit, aber die Low-Level-Engine bleibt weiterhin Low-Level, und Sie müssen den Skripttext lesen, Parameter übergeben und die API aufrufen, um das Skript auszuführen. Sie müssen also in jedem Projekt eine Art Wrapper erstellen, um die Entwicklung noch effizienter zu gestalten.
Und es wäre unfair, GraalVM nicht zu erwähnen - eine experimentelle Engine, die Programme in verschiedenen Sprachen (JVM und Nicht-JVM) ausführen kann und es Ihnen ermöglicht,
Module in diesen Sprachen in
Java-Anwendungen einzufügen. Ich hoffe, dass Nashorn früher oder später in die Geschichte eingehen wird und wir die Möglichkeit haben werden, Teile des Codes in verschiedenen Sprachen in einer Quelle zu schreiben. Das ist aber nur ein Traum.
Spring Framework: Ein Angebot, das schwer abzulehnen ist?
Spring verfügt über eine integrierte Unterstützung für die Skriptausführung, die auf der JDK-API aufbaut. Im Paket
org.springframework.scripting.*
Finden Sie viele nützliche Klassen - alles, damit Sie die Low-Level-API bequem für die Skripterstellung in Ihrer Anwendung verwenden können.
Darüber hinaus gibt es ein höheres Maß an Unterstützung, das in der
Dokumentation ausführlich beschrieben wird. Kurz gesagt: Sie müssen eine Klasse in einer Skriptsprache (z. B. Groovy) erstellen und über eine XML-Beschreibung als Bean veröffentlichen:
<lang:groovy id="messenger" script-source="classpath:Messenger.groovy"> <lang:property name="message" value="I Can Do The Frug" /> </lang:groovy>
Sobald eine Bean veröffentlicht wurde, kann sie mithilfe von IoC zu ihren Klassen hinzugefügt werden. Spring bietet eine automatische Aktualisierung des Skripts, wenn Text in der Datei geändert wird. Sie können Aspekte an Methoden usw. hängen.
Es sieht gut aus, aber Sie müssen "echte" Klassen erstellen, um sie zu veröffentlichen. Sie können keine reguläre Funktion in ein Skript schreiben. Außerdem können Skripte nur im Dateisystem gespeichert werden, um die Datenbank zu verwenden, die Sie in Spring erklimmen müssen. Ja, und viele halten die XML-Konfiguration für veraltet, insbesondere wenn die Anwendung bereits alle Anmerkungen enthält. Das ist natürlich Aroma, aber man muss oft damit rechnen.
Skripte: Schwierigkeiten und Ideen
Jede Lösung hat ihren eigenen Preis. Wenn wir über Skripte in Java-Anwendungen sprechen, kann es bei der Einführung dieser Technologie zu einigen Schwierigkeiten kommen:
- Verwaltbarkeit. Oft sind Skriptaufrufe über die gesamte Anwendung verteilt, und bei Änderungen im Code ist es ziemlich schwierig, die Aufrufe der erforderlichen Skripte zu verfolgen.
- Fähigkeit, Anrufpunkte zu finden. Wenn in einem bestimmten Skript etwas schief geht, ist das Auffinden aller Dial
evaluateGroovy()
ein Problem, es sei denn, Sie wenden eine Suche nach Dateinamen oder Methodenaufrufen wie evaluateGroovy()
- Transparenz Das Schreiben eines Skripts ist an sich keine leichte Aufgabe, und noch schwieriger ist es für diejenigen, die dieses Skript aufrufen. Sie müssen sich merken, wie die Eingabeparameter aufgerufen werden, welche Art von Daten sie haben und was das Ergebnis der Ausführung ist. Oder schauen Sie sich jedes Mal den Skript-Quellcode an.
- Testen und Aktualisieren - Es ist nicht immer möglich, das Skript in der Umgebung des Anwendungscodes zu testen. Nach dem Hochladen auf den "Battle" -Server müssen Sie in der Lage sein, alles schnell zurückzusetzen, wenn etwas schief geht.
Es scheint, dass das Umschließen von Skriptaufrufen in Java-Methoden zur Lösung der meisten der oben genannten Probleme beiträgt. Es ist sehr gut, wenn solche Klassen im IoC-Container veröffentlicht werden können und Methoden mit normalen, aussagekräftigen Namen in ihren Diensten aufrufen können, anstatt
eval(“disc_10_cl.groovy”)
von einer Dienstprogrammklasse
eval(“disc_10_cl.groovy”)
. Ein weiteres Plus ist, dass der Code selbstdokumentierend wird und der Entwickler nicht darüber nachdenken muss, welche Art von Algorithmus hinter dem Dateinamen verborgen ist.
Wenn außerdem jedes Skript nur einer Methode zugeordnet ist, können Sie über das Menü „Verwendungen suchen“ in der IDE schnell alle Dial Peers in der Anwendung finden und die Position des Skripts in den einzelnen Geschäftslogikalgorithmen verstehen.
Das Testen wird vereinfacht - es wird zu einem „normalen“ Klassentest unter Verwendung vertrauter Frameworks, Mocks und mehr.
All dies steht im Einklang mit der am Anfang des Artikels erwähnten Idee - „spezielle“ Klassen für Methoden, die von Skripten implementiert werden. Aber was ist, wenn Sie noch einen Schritt machen und den gesamten Service-Code des gleichen Typs zum Aufrufen von Skript-Engines vor dem Entwickler verbergen, damit er nicht einmal darüber nachdenkt (na ja, fast)?
Skript-Repositorys - Konzept
Die Idee ist recht einfach und sollte denen bekannt sein, die mindestens einmal mit Spring gearbeitet haben, insbesondere mit Spring JPA. Sie müssen lediglich eine Java-Schnittstelle erstellen und das Skript aufrufen, wenn Sie seine Methoden aufrufen. In JPA wird übrigens ein identischer Ansatz verwendet - der Aufruf von CrudRepository wird abgefangen, basierend auf dem Methodennamen und den Parametern wird eine Anforderung erstellt, die dann vom Datenbankmodul ausgeführt wird.
Was wird zur Umsetzung des Konzepts benötigt?
Zuerst eine Annotation auf Klassenebene, damit Sie die Schnittstelle - das Repository - finden und darauf basierend einen Bin erstellen können.
Außerdem sind Anmerkungen zu den Methoden dieser Schnittstelle wahrscheinlich nützlich, um die zum Aufrufen der Methode erforderlichen Metadaten zu speichern. Zum Beispiel - wo man den Skripttext erhält und welche Engine verwendet werden soll.
Eine nützliche Ergänzung ist die Möglichkeit, Methoden mit Implementierung in der Benutzeroberfläche zu verwenden (auch bekannt als Standard). Dieser Code funktioniert so lange, bis der Business Analyst eine vollständigere Version des Algorithmus anzeigt und der Entwickler ein Skript basierend auf erstellt
diese Informationen. Oder lassen Sie den Analysten das Skript schreiben und der Entwickler kopiert es dann einfach auf den Server. Es gibt viele Möglichkeiten :-)
Angenommen, Sie müssen für einen Online-Shop einen Dienst einrichten, um Rabatte basierend auf dem Benutzerprofil zu berechnen. Derzeit ist nicht klar, wie dies zu tun ist, aber der Geschäftsanalyst schwört, dass alle registrierten Benutzer Anspruch auf einen Rabatt von 10% haben. Den Rest wird er innerhalb einer Woche vom Kunden erfahren. Service ist gleich morgen nötig - schließlich Saison. Wie könnte der Code für diesen Fall aussehen?
@ScriptRepository public interface PricingRepository { @ScriptMethod default BigDecimal applyCustomerDiscount(Customer customer, BigDecimal orderAmount) { return orderAmount.multiply(new BigDecimal("0.9")); } }
Und dann wird der Algorithmus selbst, der zum Beispiel in groovig geschrieben ist, rechtzeitig eintreffen, dort werden die Rabatte etwas anders sein:
def age = 50 if ((Calendar.YEAR - customer.birthday.year) >= age) { return orderAmount.multiply(0.75) } else { return orderAmount.multiply(0.9) }
Der Zweck all dessen ist es, dem Entwickler die Möglichkeit zu geben, nur den Schnittstellencode und den
getEngine
zu schreiben und nicht mit all diesen Aufrufen von
getEngine
,
eval
und anderen
getEngine
. Die Bibliothek für die Arbeit mit Skripten sollte die ganze Magie ausführen - den Aufruf der Schnittstellenmethode abfangen, den Skripttext abrufen, die Parameterwerte ersetzen, die gewünschte Skript-Engine abrufen, das Skript ausführen (oder die Standardmethode aufrufen, wenn kein Skripttext vorhanden ist) und den Wert zurückgeben. Im Idealfall sollte das Programm zusätzlich zu dem bereits geschriebenen Code Folgendes haben:
@Service public class CustomerServiceBean implements CustomerService { @Inject private PricingRepository pricingRepository;
Die Herausforderung ist lesbar, verständlich und um sie zu meistern, braucht man keine besonderen Fähigkeiten.
Dies waren die Ideen, auf deren Grundlage eine kleine Bibliothek für die Arbeit mit Skripten erstellt wurde. Es ist für Spring-Anwendungen vorgesehen. Dieses Framework wurde zum Erstellen der Bibliothek verwendet. Es bietet eine erweiterbare API zum Laden und Ausführen von Skripten aus verschiedenen Quellen, die die Routinearbeit mit Skript-Engines verbirgt.
Wie funktioniert es?
Für alle mit
@ScriptRepository
gekennzeichneten
@ScriptRepository
werden Proxy-Objekte während der Initialisierung des Spring-Kontexts mit der
newProxyInstance
Methode der
Proxy
Klasse erstellt. Diese Proxys werden im Spring-Kontext als Singleton-Beans veröffentlicht, sodass Sie ein Klassenfeld mit einem Schnittstellentyp deklarieren und die Annotation
@Autowired
oder
@Inject
darauf setzen können. Genau wie geplant.
Das Scannen und Verarbeiten von
@EnableSriptRepositories
wird mithilfe der Annotation
@EnableSriptRepositories
aktiviert, genauso wie Spring JPA oder Repositorys für MongoDB aktiviert (
@EnableJpaRepositories
bzw.
@EnableMongoRepositories
). Als Anmerkungsparameter müssen Sie ein Array mit den Namen der zu scannenden Pakete angeben.
@Configuration @EnableScriptRepositories(basePackages = {"com.example", "com.sample"}) public class CoreConfig {
Methoden müssen mit
@ScriptMethod
kommentiert
@ScriptMethod
(es gibt auch
@GroovyScript
und
@JavaScript
mit der entsprechenden Spezialisierung), um Metadaten zum Aufrufen des Skripts hinzuzufügen. Natürlich werden Standardmethoden in Schnittstellen unterstützt.
Die allgemeine Struktur der Bibliothek ist im Diagramm dargestellt. Blau hervorgehobene Komponenten, die entwickelt werden müssen, weiß - die sich bereits in der Bibliothek befinden. Das Spring-Symbol markiert Komponenten, die im Spring-Kontext verfügbar sind.
Wenn die Schnittstellenmethode aufgerufen wird (tatsächlich das Proxy-Objekt), wird der Aufruf-Handler gestartet, der im Anwendungskontext nach zwei Beans sucht: dem Provider, der nach dem Skripttext sucht, und dem Executor, der tatsächlich den gefundenen Text ausführt. Anschließend gibt der Handler das Ergebnis an die aufrufende Methode zurück.
Die Provider- und Executor-
@ScriptMethod
Namen werden in der Annotation
@ScriptMethod
angegeben, in der Sie auch die Ausführungszeit der Methode begrenzen können. Unten finden Sie einen Beispielcode für die Verwendung der Bibliothek:
@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")); } }
Sie können
@ScriptParam
Anmerkungen bemerken - sie werden benötigt, um die Parameternamen anzugeben, wenn sie an das Skript übergeben werden, da der Java-Compiler die ursprünglichen Namen aus den Quellen löscht (es gibt Möglichkeiten, dies nicht zu tun, aber es ist besser, sich nicht darauf zu verlassen). Sie können die Parameternamen weglassen, aber in diesem Fall müssen Sie "arg0", "arg1" im Skript verwenden, was die Lesbarkeit nicht wesentlich verbessert.
Standardmäßig verfügt die Bibliothek über Anbieter zum Lesen von .groovy- und .js-Dateien von der Festplatte und entsprechenden Executoren, die Wrapper über die Standard-JSR-233-API sind. Sie können Ihre eigenen Beans für verschiedene
ScriptProvider
und für verschiedene Engines
SpringEvaluator
. Dazu müssen Sie die entsprechenden Schnittstellen implementieren:
ScriptProvider
und
SpringEvaluator
. Die erste Schnittstelle verwendet
org.springframework.scripting.ScriptSource
und die zweite ist
org.springframework.scripting.ScriptEvaluator
. Die Spring-API wurde verwendet, damit vorgefertigte Klassen verwendet werden können, wenn sie bereits in der Anwendung enthalten sind.
Der Anbieter und der Künstler werden nach Namen durchsucht, um eine größere Flexibilität zu gewährleisten. Sie können die Standard-Beans aus der Bibliothek in Ihrer Anwendung ersetzen, indem Sie Ihre Komponenten mit denselben Namen benennen.
Testen und Versionieren
Da sich Skripte häufig und einfach ändern, müssen Sie sicherstellen können, dass die Änderungen nichts beschädigen. Die Bibliothek ist mit JUnit kompatibel. Das Repository kann einfach als reguläre Klasse im Rahmen eines Einheits- oder Integrationstests getestet werden. Scheinbibliotheken werden ebenfalls unterstützt. In Tests für die Bibliothek finden Sie ein Beispiel dafür, wie Sie die Skript-Repository-Methode verspotten.
Wenn eine Versionierung erforderlich ist, können Sie einen Anbieter erstellen, der beispielsweise verschiedene Versionen von Skripten aus dem Dateisystem, aus der Datenbank oder aus Git liest. Bei Problemen auf dem Hauptserver ist es daher einfach, ein Rollback auf die vorherige Version des Skripts zu organisieren.
Insgesamt
Die vorgestellte Bibliothek hilft beim Organisieren von Skripten in der Spring-Anwendung:
- Der Entwickler hat immer Informationen darüber, welche Parameter die Skripte benötigen und was zurückgegeben wird. Und wenn die Schnittstellenmethoden sinnvoll benannt sind, was das Skript dann tut.
- Anbieter und Ausführende helfen dabei, den Code für den Empfang von Skripten und die Interaktion mit der Skript-Engine an einem Ort zu halten, und diese Aufrufe werden nicht über den gesamten Anwendungscode verteilt.
- Alle Skriptaufrufe können mithilfe von Verwendungen suchen leicht gefunden werden.
Spring Boot Autokonfiguration, Unit Testing, Mock werden unterstützt. Über die API können Sie Daten zu den Skriptmethoden und ihren Parametern abrufen. Sie können das Ausführungsergebnis auch mit einem speziellen ScriptResult-Objekt umschließen, in dem ein Ergebnis oder eine Ausnahmeinstanz angezeigt wird, wenn Sie sich beim Aufrufen von Skripten nicht mit try ... catch beschäftigen möchten. Die XML-Konfiguration wird unterstützt, wenn sie aus dem einen oder anderen Grund erforderlich ist. Und schließlich können Sie bei Bedarf ein Zeitlimit für die Skriptmethode angeben.
Die Bibliotheksquellen sind hier.