Dans toute situation incompréhensible - écrire des scripts

image

Les scripts sont l'un des moyens les plus courants de rendre une application plus flexible, avec la possibilité de corriger quelque chose immédiatement. Bien sûr, cette approche présente également des inconvénients; vous devez toujours vous rappeler l'équilibre entre la flexibilité et la gérabilité. Mais dans cet article, nous ne discuterons pas «de manière générale» des avantages et des inconvénients de l'utilisation de scripts, nous examinerons des moyens pratiques de mettre en œuvre cette approche et présenterons également une bibliothèque qui fournit une infrastructure pratique pour ajouter des scripts aux applications écrites dans Spring Framework.

Quelques mots d'introduction


Lorsque vous souhaitez ajouter la possibilité de modifier la logique métier dans une application sans recompilation ni déploiement ultérieur, les scripts sont l'un des moyens qui viennent à l'esprit en premier lieu. Souvent, les scripts n'apparaissent pas parce que c'était prévu, mais parce que c'est arrivé. Par exemple, dans la spécification, il y a une partie de la logique qui n'est pas complètement claire en ce moment, mais afin de ne pas passer quelques jours supplémentaires (et parfois plus) pour l'analyse, vous pouvez créer un point d'extension et appeler un script - un stub. Et puis, bien sûr, ce script sera réécrit lorsque les exigences deviendront claires.

La méthode n'est pas nouvelle et ses avantages et inconvénients sont bien connus: flexibilité - vous pouvez changer la logique d'une application en cours d'exécution et gagner du temps lors d'une réinstallation, mais, d'un autre côté, les scripts sont plus difficiles à tester, d'où les éventuels problèmes de sécurité, de performances, etc.

Ces techniques, qui seront discutées plus loin, peuvent être utiles à la fois aux développeurs qui utilisent déjà des scripts dans leur application et à ceux qui y pensent.

Rien de personnel, juste des scripts


Avec JSR-233, l'écriture de scripts en Java est devenue très simple. Il existe suffisamment de moteurs de script basés sur cette API (Nashorn, JRuby, Jython et bien d'autres), donc ajouter un peu de magie de script à votre code n'est pas un problème:

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

De toute évidence, si un tel code est dispersé dans l'application, il se transformera en quelque chose d'incompréhensible. Et, bien sûr, si vous avez plusieurs appels de script dans votre application, vous devez créer une classe distincte pour travailler avec eux. Parfois, vous pouvez aller encore plus loin et créer des classes spéciales qui encapsuleront evaluateGroovy() appels evaluateGroovy() dans des méthodes Java typées normales. Ces méthodes auront un code utilitaire assez uniforme, comme dans l'exemple:

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

Cette approche augmente considérablement la transparence lors de l'appel de scripts à partir du code d'application - vous pouvez immédiatement voir quels paramètres le script accepte, quel type ils sont et ce qui est retourné. L'essentiel est de ne pas oublier d'ajouter aux normes d'écriture du code une interdiction d'appeler des scripts non issus de méthodes typées!

Nous pompons des scripts


Malgré le fait que les scripts soient simples, si vous en avez beaucoup et que vous les utilisez intensivement, il y a de réelles chances de rencontrer des problèmes de performances. Par exemple, si vous utilisez un tas de modèles groovy pour générer des rapports et que vous les exécutez en même temps, tôt ou tard, cela deviendra l'un des goulots d'étranglement dans les performances des applications.
Par conséquent, de nombreux frameworks proposent différents modules complémentaires sur l'API standard pour améliorer la vitesse de travail, la mise en cache, la surveillance de l'exécution, l'utilisation de différents langages de script dans une seule application, etc.

Par exemple, un moteur de script assez ingénieux a été créé dans CUBA qui prend en charge des fonctionnalités supplémentaires, telles que:

  1. Capacité à écrire des scripts en Java et Groovy
  2. Cache de classe pour ne pas recompiler les scripts
  3. Bac JMX pour contrôler le moteur

Tout cela, bien sûr, améliore les performances et la convivialité, mais le moteur de bas niveau reste bas, et vous devez toujours lire le texte du script, passer les paramètres et appeler l'API pour exécuter le script. Vous devez donc toujours faire une sorte de wrapper dans chaque projet pour rendre le développement encore plus efficace.

Et il serait injuste de ne pas mentionner GraalVM - un moteur expérimental qui peut exécuter des programmes dans différentes langues (JVM et non-JVM) et vous permet d'insérer des modules dans ces langues dans des applications Java . J'espère que Nashorn restera tôt ou tard dans l'histoire, et nous aurons l'occasion d'écrire des parties du code dans différentes langues en une seule source. Mais ce n'est qu'un rêve.

Spring Framework: une offre difficile à refuser?


Spring a une prise en charge intégrée de l'exécution des scripts basée sur l'API JDK. Dans le org.springframework.scripting.* , Vous pouvez trouver de nombreuses classes utiles - toutes afin que vous puissiez facilement utiliser l'API de bas niveau pour les scripts dans votre application.

De plus, le niveau de support est plus élevé, il est décrit en détail dans la documentation . En bref - vous devez créer une classe dans un langage de script (par exemple, Groovy) et la publier en tant que bean via une description XML:

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

Une fois qu'un bean est publié, il peut être ajouté à ses classes à l'aide d'IoC. Spring fournit une mise à jour automatique du script lors du changement de texte dans le fichier, vous pouvez accrocher des aspects sur les méthodes, etc.

Cela a l'air bien, mais vous devez créer de «vraies» classes pour les publier, vous ne pouvez pas écrire une fonction régulière dans un script. De plus, les scripts ne peuvent être stockés que dans le système de fichiers, pour utiliser la base de données que vous devez monter dans Spring. Oui, et beaucoup considèrent la configuration XML comme obsolète, surtout si l'application a déjà tout sur les annotations. Bien sûr, cela aromatise, mais il faut souvent en tenir compte.

Scripts: difficultés et idées


Ainsi, chaque solution a son propre prix, et si nous parlons de scripts dans les applications Java, alors lors de l'introduction de cette technologie, on peut rencontrer quelques difficultés:

  1. Gérabilité. Souvent, les appels de script sont dispersés dans l'application, et avec les changements de code, il est assez difficile de suivre les appels des scripts nécessaires.
  2. Capacité à trouver des homologues de numérotation. Si quelque chose ne va pas dans un script particulier, la recherche de tous ses evaluateGroovy() numérotation sera un problème, sauf si vous appliquez une recherche par nom de fichier ou par des appels de méthode comme evaluateGroovy()
  3. La transparence Écrire un script n'est pas une tâche facile en soi, et c'est encore plus difficile pour ceux qui appellent ce script. Vous devez vous rappeler comment les paramètres d'entrée sont appelés, quel type de données ils contiennent et quel est le résultat de l'exécution. Ou regardez le code source du script à chaque fois.
  4. Test et mise à jour - il n'est pas toujours possible de tester le script dans l'environnement du code de l'application, et même après l'avoir téléchargé sur le serveur «battle», vous devez d'une manière ou d'une autre être capable de tout restaurer rapidement en cas de problème.

Il semble que l'encapsulation des appels de script dans les méthodes Java aidera à résoudre la plupart des problèmes ci-dessus. C'est très bien si de telles classes peuvent être publiées dans le conteneur IoC et appeler des méthodes avec des noms normaux et significatifs dans leurs services, au lieu d'appeler eval(“disc_10_cl.groovy”) partir d'une classe utilitaire. Un autre avantage est que le code devient auto-documenté, le développeur n'a pas à se demander quel type d'algorithme est caché derrière le nom du fichier.

En plus de cela, si chaque script sera associé à une seule méthode, vous pouvez trouver rapidement tous les homologues de numérotation dans l'application en utilisant le menu "Find Usages" de l'EDI et comprendre la place du script dans chaque algorithme de logique métier spécifique.

Les tests sont simplifiés - ils se transforment en tests de classe «normaux», en utilisant des cadres familiers, des simulateurs, etc.

Tout ce qui précède est très conforme à l'idée mentionnée au début de l'article - des classes «spéciales» pour les méthodes qui sont implémentées par des scripts. Mais que se passe-t-il si vous faites un pas de plus et que vous cachez tout le code de service du même type pour appeler des moteurs de script au développeur afin qu'il n'y pense même pas (enfin, presque)?

Référentiels de scripts - concept


L'idée est assez simple et devrait être familière à ceux qui ont au moins une fois travaillé avec Spring, en particulier avec Spring JPA. Ce dont vous avez besoin est de créer une interface Java et d'appeler le script lors de l'appel de ses méthodes. Dans JPA, en passant, une approche identique est utilisée - l'appel à CrudRepository est intercepté, en fonction du nom de la méthode et des paramètres, une demande est créée, qui est ensuite exécutée par le moteur de base de données.

De quoi a-t-on besoin pour mettre en œuvre le concept?

Tout d'abord, une annotation au niveau de la classe afin que vous puissiez trouver l'interface - le référentiel et créer un bac sur cette base.

De plus, les annotations sur les méthodes de cette interface seront probablement utiles pour stocker les métadonnées nécessaires pour appeler la méthode. Par exemple - où trouver le texte du script et quel moteur utiliser.

Un ajout utile sera la possibilité d'utiliser des méthodes avec implémentation dans l'interface (alias par défaut) - ce code fonctionnera jusqu'à ce que l'analyste commercial affiche une version plus complète de l'algorithme, et que le développeur crée un script basé sur
cette information. Ou laissez l'analyste écrire le script, et le développeur le copie simplement sur le serveur. Il existe de nombreuses options :-)

Supposons donc que pour une boutique en ligne, vous devez effectuer un service pour calculer les remises en fonction du profil de l'utilisateur. On ne sait pas encore comment procéder, mais l'analyste commercial jure que tous les utilisateurs enregistrés ont droit à une remise de 10%, il trouvera le reste auprès du client dans une semaine. Le service est nécessaire dès demain - la saison après tout. À quoi pourrait ressembler le code dans ce cas?

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

Et puis l'algorithme lui-même, écrit, par exemple, en groovy, arrivera à temps, là les remises seront légèrement différentes:

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

Le but de tout cela est de donner au développeur la possibilité d'écrire uniquement le code d'interface et le code de script, et de ne pas getEngine avec tous ces appels à getEngine , eval et autres. La bibliothèque pour travailler avec des scripts devrait faire toute la magie - intercepter l'appel de la méthode d'interface, obtenir le texte du script, remplacer les valeurs des paramètres, obtenir le moteur de script souhaité, exécuter le script (ou appeler la méthode par défaut s'il n'y a pas de texte de script) et renvoyer la valeur. Idéalement, en plus du code qui a déjà été écrit, le programme devrait avoir quelque chose comme ceci:

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

Le défi est lisible, compréhensible et pour le faire, il n'est pas nécessaire d'avoir des compétences particulières.

Ce sont les idées sur la base desquelles une petite bibliothèque pour travailler avec des scripts a été créée. Il est destiné aux applications Spring, ce framework a été utilisé pour créer la bibliothèque. Il fournit une API extensible pour charger des scripts à partir de diverses sources et les exécuter, ce qui masque le travail de routine avec les moteurs de script.

Comment ça marche


Pour toutes les interfaces marquées avec @ScriptRepository , les objets proxy sont créés lors de l'initialisation du contexte Spring à l'aide de la méthode newProxyInstance de la classe Proxy . Ces proxys sont publiés dans le contexte de Spring en tant que beans singleton, vous pouvez donc déclarer un champ de classe avec un type d'interface et y placer l'annotation @Autowired ou @Inject . Exactement comme prévu.

L'analyse et le traitement des interfaces de script sont activés à l'aide de l'annotation @EnableSriptRepositories , de la même manière que Spring active JPA ou les référentiels pour MongoDB ( @EnableJpaRepositories et @EnableMongoRepositories respectivement). En tant que paramètres d'annotation, vous devez spécifier un tableau avec les noms des packages à analyser.

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

Les méthodes doivent être annotées avec @ScriptMethod (il existe également @GroovyScript et @JavaScript , avec la spécialisation correspondante) pour ajouter des métadonnées pour appeler le script. Bien sûr, les méthodes par défaut dans les interfaces sont prises en charge.

La structure générale de la bibliothèque est illustrée dans le diagramme. Composants surlignés en bleu qui doivent être développés, blanc - qui sont déjà dans la bibliothèque. L'icône Spring marque les composants disponibles dans le contexte Spring.


Lorsque la méthode d'interface est appelée (en fait, l'objet proxy), le gestionnaire d'appel est lancé, qui dans le contexte de l'application recherche deux beans: le fournisseur, qui recherchera le texte du script, et l'exécuteur, qui, en fait, le texte trouvé s'exécutera. Le gestionnaire renvoie ensuite le résultat à la méthode appelante.

Les noms de @ScriptMethod fournisseur et exécuteur sont spécifiés dans l'annotation @ScriptMethod , où vous pouvez également définir une limite sur le temps d'exécution de la méthode. Voici un exemple de code d'utilisation de la bibliothèque:

 @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")); } } 

Vous pouvez remarquer les annotations @ScriptParam - elles sont nécessaires pour indiquer les noms des paramètres lors de leur transmission au script, car le compilateur Java efface les noms d'origine des sources (il existe des moyens de ne pas le faire, mais il vaut mieux ne pas s'y fier). Vous pouvez omettre les noms des paramètres, mais dans ce cas, vous devrez utiliser "arg0", "arg1" dans le script, ce qui n'améliore pas considérablement la lisibilité.

Par défaut, la bibliothèque a des fournisseurs pour lire les fichiers .groovy et .js à partir du disque et les exécuteurs correspondants, qui sont des wrappers sur l'API JSR-233 standard. Vous pouvez créer vos propres beans pour différentes sources de script et pour différents moteurs, pour cela, vous devez implémenter les interfaces correspondantes: ScriptProvider et SpringEvaluator . La première interface utilise org.springframework.scripting.ScriptSource et la seconde est org.springframework.scripting.ScriptEvaluator . L'API Spring a été utilisée pour que des classes prédéfinies puissent être utilisées si elles sont déjà dans l'application.
Le fournisseur et l'artiste sont recherchés par nom pour une plus grande flexibilité - vous pouvez remplacer les beans standard de la bibliothèque de votre application en nommant vos composants avec les mêmes noms.

Test et versioning


Parce que les scripts changent fréquemment et facilement, vous devez avoir un moyen de vous assurer que les changements ne cassent rien. La bibliothèque est compatible avec JUnit, le référentiel peut simplement être testé en tant que classe régulière dans le cadre d'un test unitaire ou d'intégration. Les bibliothèques fictives sont également prises en charge, dans les tests de la bibliothèque, vous pouvez trouver un exemple de la façon de faire de la simulation sur la méthode du référentiel de scripts.

Si le contrôle de version est nécessaire, vous pouvez créer un fournisseur qui lira différentes versions de scripts à partir du système de fichiers, de la base de données ou de Git, par exemple. Il sera donc facile d'organiser un retour à la version précédente du script en cas de problème sur le serveur principal.

Total


La bibliothèque présentée aidera à organiser les scripts dans l'application Spring:

  1. Le développeur aura toujours des informations sur les paramètres dont les scripts ont besoin et ce qui est retourné. Et si les méthodes d'interface sont nommées de manière significative, alors ce que fait le script.
  2. Les fournisseurs et exécuteurs aideront à conserver le code pour recevoir des scripts et interagir avec le moteur de script en un seul endroit et ces appels ne seront pas dispersés dans le code de l'application.
  3. Tous les appels de script peuvent être facilement trouvés en utilisant Find Usages.

La configuration automatique de Spring Boot, les tests unitaires et la simulation sont pris en charge. Vous pouvez obtenir des données sur les méthodes de «script» et leurs paramètres via l'API. Et vous pouvez également encapsuler le résultat de l'exécution avec un objet ScriptResult spécial, dans lequel il y aura un résultat ou une instance d'exception si vous ne voulez pas déranger avec try ... catch lors de l'appel de scripts. La configuration XML est prise en charge si elle est requise pour une raison ou une autre. Et enfin - vous pouvez spécifier un délai d'expiration pour la méthode de script, si le besoin s'en fait sentir.

Les sources de la bibliothèque sont ici.

Source: https://habr.com/ru/post/fr431084/


All Articles