En cualquier situación incomprensible - escriba guiones

imagen

Los scripts son una de las formas más comunes de hacer que una aplicación sea más flexible, con la capacidad de arreglar algo sobre la marcha. Por supuesto, este enfoque también tiene inconvenientes; siempre debe recordar el equilibrio entre flexibilidad y capacidad de administración. Pero en este artículo no discutiremos "en general" sobre los pros y los contras del uso de scripts, consideraremos formas prácticas de implementar este enfoque, y también presentaremos una biblioteca que proporcione una infraestructura conveniente para agregar scripts a las aplicaciones escritas en Spring Framework.

Algunas palabras introductorias


Cuando desee agregar la capacidad de cambiar la lógica de negocios en una aplicación sin recompilación y posterior implementación, las secuencias de comandos son una de las formas que se le ocurren en primer lugar. A menudo, los scripts aparecen no porque se pretendía, sino porque sucedieron. Por ejemplo, en la especificación hay una parte de la lógica que no está completamente clara en este momento, pero para no gastar un par de días adicionales (y a veces más) para el análisis, puede hacer un punto de extensión y llamar a un script: un trozo. Y luego, por supuesto, este script se reescribirá cuando los requisitos se aclaren.

El método no es nuevo, y sus ventajas y desventajas son bien conocidas: flexibilidad: puede cambiar la lógica en una aplicación en ejecución y ahorrar tiempo en una reinstalación, pero, por otro lado, los scripts son más difíciles de probar, de ahí los posibles problemas de seguridad, rendimiento, etc.

Esos trucos que se discutirán más adelante pueden ser útiles tanto para los desarrolladores que ya usan scripts en su aplicación como para aquellos que simplemente están pensando en ello.

Nada personal, solo secuencias de comandos


Con JSR-233, las secuencias de comandos Java se han vuelto muy simples. Hay suficientes motores de secuencias de comandos basados ​​en esta API (Nashorn, JRuby, Jython y algunos más), por lo que agregar un poco de magia de secuencias de comandos a su código no es un problema:

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

Obviamente, si dicho código está disperso por toda la aplicación, se convertirá en algo incomprensible. Y, por supuesto, si tiene más de una llamada de script en su aplicación, debe crear una clase separada para trabajar con ellos. A veces puede ir aún más lejos y hacer clases especiales que encapsularán las llamadas evaluateGroovy() en métodos Java escritos con mecanografía. Estos métodos tendrán un código de utilidad bastante uniforme, como en el ejemplo:

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

Este enfoque aumenta en gran medida la transparencia al llamar scripts desde el código de la aplicación: puede ver de inmediato qué parámetros acepta el script, de qué tipo son y qué se devuelve. ¡Lo principal es no olvidar agregar a los estándares de escritura de códigos una prohibición de llamar scripts que no sean de métodos escritos!

Bombeamos guiones


A pesar del hecho de que los scripts son simples, si tiene muchos y los usa intensivamente, existe una posibilidad real de encontrarse con problemas de rendimiento. Por ejemplo, si usa un montón de plantillas geniales para generar informes y las ejecuta al mismo tiempo, tarde o temprano se convertirá en uno de los cuellos de botella en el rendimiento de la aplicación.
Por lo tanto, muchos marcos crean varios complementos sobre la API estándar para mejorar la velocidad del trabajo, el almacenamiento en caché, el monitoreo de la ejecución, el uso de diferentes lenguajes de secuencias de comandos en una aplicación, etc.

Por ejemplo, se creó un motor de secuencias de comandos bastante ingenioso en CUBA que admite características adicionales, como:

  1. Capacidad para escribir scripts en Java y Groovy
  2. Caché de clase para no recompilar scripts
  3. JMX bin para controlar el motor

Todo esto, por supuesto, mejora el rendimiento y la usabilidad, pero aún así el motor de bajo nivel sigue siendo de bajo nivel, y aún necesita leer el texto del script, pasar parámetros y llamar a la API para ejecutar el script. Por lo tanto, aún debe hacer algún tipo de envoltura en cada proyecto para que el desarrollo sea aún más eficiente.

Y sería injusto no mencionar GraalVM, un motor experimental que puede ejecutar programas en diferentes idiomas (JVM y no JVM) y le permite insertar módulos en estos idiomas en aplicaciones Java . Espero que Nashorn pase a la historia tarde o temprano, y tengamos la oportunidad de escribir partes del código en diferentes idiomas en una sola fuente. Pero esto es solo un sueño.

Spring Framework: ¿una oferta difícil de rechazar?


Spring tiene soporte de ejecución de script incorporado integrado sobre la API JDK. En el paquete org.springframework.scripting.* , Puede encontrar muchas clases útiles, todo para que pueda usar convenientemente la API de bajo nivel para las secuencias de comandos en su aplicación.

Además, hay un mayor nivel de soporte, se describe en detalle en la documentación . En resumen: debe crear una clase en un lenguaje de secuencias de comandos (por ejemplo, Groovy) y publicarla como un bean mediante una descripción XML:

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

Una vez que se publica un bean, se puede agregar a sus clases usando IoC. Spring proporciona una actualización automática de la secuencia de comandos al cambiar el texto en el archivo, puede colgar aspectos de métodos, etc.

Se ve bien, pero necesita hacer clases "reales" para publicarlas, no puede escribir una función regular en un script. Además, los scripts solo se pueden almacenar en el sistema de archivos, para usar la base de datos que tiene que escalar dentro de Spring. Sí, y muchos consideran que la configuración XML es obsoleta, especialmente si la aplicación ya tiene todo en las anotaciones. Esto, por supuesto, es aromatizante, pero a menudo hay que tener en cuenta.

Guiones: dificultades e ideas


Entonces, cada solución tiene su propio precio, y si hablamos de scripts en aplicaciones Java, al introducir esta tecnología, uno puede encontrar algunas dificultades:

  1. Manejabilidad A menudo, las llamadas de script están dispersas por toda la aplicación, y con los cambios en el código es bastante difícil rastrear las llamadas de los scripts necesarios.
  2. Posibilidad de encontrar pares de marcado. Si algo sale mal en un script en particular, entonces encontrar todos sus evaluateGroovy() marcado será un problema, a menos que aplique una búsqueda por nombre de archivo o llamadas a métodos como evaluateGroovy()
  3. Transparencia Escribir un guión no es una tarea fácil en sí mismo, y aún más difícil es para aquellos que lo llaman. Debe recordar cómo se llaman los parámetros de entrada, qué tipo de datos tienen y cuál es el resultado de la ejecución. O mire el código fuente del script cada vez.
  4. Prueba y actualización: no siempre es posible probar el script en el entorno del código de la aplicación, e incluso después de cargarlo en el servidor de "batalla", de alguna manera debe poder deshacer rápidamente todo si algo sale mal.

Parece que las llamadas de script de ajuste en métodos Java ayudarán a resolver la mayoría de los problemas anteriores. Es muy bueno si tales clases pueden publicarse en el contenedor de IoC y llamar a métodos con nombres normales y significativos en sus servicios, en lugar de llamar a eval(“disc_10_cl.groovy”) desde alguna clase de utilidad. Otra ventaja es que el código se documenta automáticamente, el desarrollador no tiene que preguntarse qué tipo de algoritmo está oculto detrás del nombre del archivo.

Además de eso, si cada secuencia de comandos se asociará con un solo método, puede encontrar rápidamente todos los pares de marcado en la aplicación utilizando el menú "Buscar usos" del IDE y comprender el lugar de la secuencia de comandos en cada algoritmo de lógica de negocios específico.

Las pruebas se simplifican: se convierten en pruebas de clase "normales", utilizando marcos familiares, simulacros y más.

Todo lo anterior está muy en consonancia con la idea mencionada al principio del artículo: clases "especiales" para los métodos implementados por scripts. Pero, ¿qué sucede si da un paso más y oculta todo el código de servicio del mismo tipo para llamar a los motores de script del desarrollador para que ni siquiera lo piense (bueno, casi)?

Repositorios de guiones - concepto


La idea es bastante simple y debería ser familiar para aquellos que al menos una vez trabajaron con Spring, especialmente con Spring JPA. Lo que necesita es crear una interfaz Java y llamar al script cuando llame a sus métodos. En JPA, por cierto, se utiliza un enfoque idéntico: la llamada a CrudRepository se intercepta, según el nombre del método y los parámetros, se crea una solicitud, que luego es ejecutada por el motor de la base de datos.

¿Qué se necesita para implementar el concepto?

Primero, una anotación de nivel de clase para que pueda encontrar la interfaz: el repositorio y hacer un bin basado en él.

Además, las anotaciones sobre los métodos de esta interfaz probablemente serán útiles para almacenar los metadatos necesarios para llamar al método. Por ejemplo: dónde obtener el texto del script y qué motor utilizar.

Una adición útil será la capacidad de usar métodos con implementación en la interfaz (también conocido como predeterminado): este código funcionará hasta que el analista de negocios muestre una versión más completa del algoritmo y el desarrollador cree un script basado en
Esta información. O deje que el analista escriba el guión y el desarrollador simplemente lo copie en el servidor. Hay muchas opciones :-)

Entonces, supongamos que para una tienda en línea necesita hacer un servicio para calcular descuentos basados ​​en el perfil del usuario. No está claro en este momento cómo hacer esto, pero el analista de negocios jura que todos los usuarios registrados tienen derecho a un descuento del 10%, él descubrirá el resto del cliente dentro de una semana. El servicio es necesario mañana, temporada después de todo. ¿Cómo se vería el código para este caso?

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

Y luego el algoritmo en sí, escrito, por ejemplo, en groovy, llegará a tiempo, allí los descuentos serán ligeramente diferentes:

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

El propósito de todo esto es dar al desarrollador la capacidad de escribir solo el código de la interfaz y el código del script, y no perder el tiempo con todas estas llamadas a getEngine , eval y otras. La biblioteca para trabajar con scripts debe hacer toda la magia: interceptar la invocación del método de interfaz, obtener el texto del script, sustituir los valores de los parámetros, obtener el motor de script deseado, ejecutar el script (o llamar al método predeterminado si no hay texto del script) y devolver el valor. Idealmente, además del código que ya se ha escrito, el programa debería tener algo como esto:

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

El desafío es legible, comprensible, y para hacerlo, uno no necesita tener ninguna habilidad especial.

Estas fueron las ideas sobre las cuales se hizo una pequeña biblioteca para trabajar con scripts. Está destinado a aplicaciones Spring, este marco se utilizó para crear la biblioteca. Proporciona una API extensible para cargar scripts de varias fuentes y ejecutarlos, lo que oculta el trabajo de rutina con los motores de scripts.

Como funciona


Para todas las interfaces marcadas con @ScriptRepository , los objetos proxy se crean durante la inicialización del contexto Spring utilizando el método newProxyInstance de la clase Proxy . Estos proxies se publican en el contexto de Spring como beans singleton, por lo que puede declarar un campo de clase con un tipo de interfaz y poner la anotación @Autowired o @Inject en él. Exactamente según lo planeado.

El escaneo y el procesamiento de las interfaces de script se activan mediante la anotación @EnableSriptRepositories , de la misma manera que Spring activa JPA o repositorios para MongoDB ( @EnableJpaRepositories y @EnableMongoRepositories respectivamente). Como parámetros de anotación, debe especificar una matriz con los nombres de los paquetes a escanear.

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

Los métodos se deben anotar con @ScriptMethod (también hay @GroovyScript y @JavaScript , con la especialización correspondiente) para agregar metadatos para llamar al script. Por supuesto, los métodos predeterminados en las interfaces son compatibles.

La estructura general de la biblioteca se muestra en el diagrama. Componentes resaltados en azul que deben desarrollarse, blanco, que ya están en la biblioteca. El icono de Spring marca los componentes que están disponibles en el contexto de Spring.


Cuando se llama al método de interfaz (de hecho, el objeto proxy), se inicia el controlador de llamadas, que en el contexto de la aplicación busca dos beans: el proveedor, que buscará el texto del script, y el ejecutor, que, de hecho, se ejecutará el texto encontrado. Luego, el controlador devuelve el resultado al método de llamada.

Los nombres de @ScriptMethod proveedor y ejecutor se especifican en la anotación @ScriptMethod , donde también puede establecer un límite de tiempo para la ejecución del método. A continuación se muestra un código de uso de la biblioteca de muestra:

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

Puede observar @ScriptParam anotaciones de @ScriptParam : son necesarias para indicar los nombres de los parámetros al pasarlos al script, ya que el compilador de Java borra los nombres originales de las fuentes (hay formas de hacer que no lo haga, pero es mejor no confiar en él). Puede omitir los nombres de los parámetros, pero en este caso, deberá usar "arg0", "arg1" en el script, lo que no mejora en gran medida la legibilidad.

Por defecto, la biblioteca tiene proveedores para leer archivos .groovy y .js del disco y los ejecutores correspondientes, que son envoltorios sobre la API estándar JSR-233. Puede crear sus propios beans para diferentes fuentes de script y para diferentes motores, para esto necesita implementar las interfaces correspondientes: ScriptProvider y SpringEvaluator . La primera interfaz usa org.springframework.scripting.ScriptSource y la segunda es org.springframework.scripting.ScriptEvaluator . Se usó Spring API para que las clases preparadas se pudieran usar si ya están en la aplicación.
Se busca al proveedor y al artista por nombre para una mayor flexibilidad: puede reemplazar los beans estándar de la biblioteca en su aplicación nombrando sus componentes con los mismos nombres.

Pruebas y versiones


Debido a que los scripts cambian con frecuencia y facilidad, debe tener una manera de asegurarse de que los cambios no rompan nada. La biblioteca es compatible con JUnit, el repositorio simplemente se puede probar como una clase regular como parte de una unidad o prueba de integración. Las bibliotecas simuladas también son compatibles, en las pruebas para la biblioteca puede encontrar un ejemplo de cómo hacer simulacros en el método del repositorio de scripts.

Si se necesitan versiones, puede crear un proveedor que lea diferentes versiones de scripts del sistema de archivos, de la base de datos o de Git, por ejemplo. Por lo tanto, será fácil organizar una reversión a la versión anterior del script en caso de problemas en el servidor principal.

Total


La biblioteca presentada ayudará a organizar los scripts en la aplicación Spring:

  1. El desarrollador siempre tendrá información sobre qué parámetros necesitan los scripts y qué se devuelve. Y si los métodos de la interfaz tienen un nombre significativo, entonces lo que hace el script.
  2. Los proveedores y ejecutores ayudarán a mantener el código para recibir scripts e interactuar con el motor de scripts en un solo lugar y estas llamadas no se distribuirán por todo el código de la aplicación.
  3. Todas las llamadas de guiones se pueden encontrar fácilmente utilizando Buscar usos.

Se admite la configuración automática de Spring Boot, pruebas de unidad, simulacro. Puede obtener datos sobre los métodos de "script" y sus parámetros a través de la API. Y también puede ajustar el resultado de la ejecución con un objeto ScriptResult especial, en el que habrá un resultado o una instancia de excepción si no desea molestarse con try ... catch al invocar scripts. La configuración XML es compatible si se requiere por una razón u otra. Y finalmente, puede especificar un tiempo de espera para el método de secuencia de comandos, si surge la necesidad.

Las fuentes de la biblioteca están aquí.

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


All Articles