Статьи

В любой непонятной ситуации выходят скрипты

Сценарии — это один из самых популярных способов настроить приложение под потребности клиента прямо во время выполнения. Как всегда, этот подход приносит не только пользу, например, существует хорошо известный компромисс между гибкостью и управляемостью. Эта статья не является одной из тех, которые теоретически обсуждают плюсы и минусы, она практически показывает различные способы применения сценариев и представляет библиотеку Spring, которая предоставляет удобную инфраструктуру сценариев и полезные функции.

Вступление

Сценарии (так называемая архитектура плагинов) — это самый простой способ сделать ваше приложение настраиваемым во время выполнения. Довольно часто сценарии входят в ваше приложение не случайно, а случайно. Скажем, у вас есть очень непонятная часть в функциональной спецификации, поэтому, чтобы не тратить один день на дополнительный бизнес-анализ, мы решили создать точку расширения и вызвать скрипт, который реализует заглушку — выясним, как она должна работать позже.

Существует много известных плюсов и минусов использования такого подхода: например, большая гибкость в определении бизнес-логики во время выполнения и экономия огромного количества времени на повторное развертывание по сравнению с невозможностью проведения комплексного тестирования, а следовательно, непредсказуемые проблемы с безопасностью, проблемы с производительностью и так далее.

Способы выполнения сценариев, обсуждаемые далее, могут быть полезны как для тех, кто уже решил придерживаться плагинов сценариев в своем Java-приложении, так и просто подумал о том, чтобы добавить их в свой код.

Ничего особенного, просто сценарии

С Java JSR-233 API оценка скриптов в Java — простая задача. Для этого API реализован ряд механизмов оценки производственного класса (Nashorn, JRuby, Jython и т. Д.), Поэтому нет проблем с добавлением некоторого волшебства сценариев в код Java, как показано здесь:

1
2
3
4
5
   Map parameters = createParametersMap();
   ScriptEngineManager manager = new ScriptEngineManager();
   ScriptEngine scriptEngine = manager.getEngineByName("groovy");
   Object result = scriptEngine.eval(script.getScriptAsString("discount.groovy"),
                                new SimpleBindings(parameters));

Очевидно, что разбрасывать такой код по всему вашему приложению не очень хорошая идея, когда у вас есть более одного файла сценария и один вызов в базе кода, поэтому вы можете извлечь этот фрагмент в отдельный метод, помещенный в служебный класс. Иногда вы можете пойти еще дальше: вы можете создать специальный класс (или набор классов), который группирует бизнес-логику, основанную на сценариях, на основе бизнес-домена, например, класса PricingScriptService . Это позволит нам обернуть вызовы для оценки Groovy () в хорошие строго типизированные методы, но все еще есть некоторый шаблонный код, все методы будут содержать отображение параметров, логику загрузки текста сценария и вызов механизма оценки сценария, подобный этому:

1
2
3
4
5
6
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);
}

Такой подход обеспечивает большую прозрачность в плане знания типов параметров и типа возвращаемого значения. И не забудьте добавить правило, запрещающее «развернутые» вызовы скриптового движка, в ваш документ по стандартам кодирования!

Сценарии на стероидах

Несмотря на то, что использовать скриптовые движки довольно просто, если у вас много скриптов в вашей кодовой базе, вы можете столкнуться с некоторыми проблемами производительности. В качестве примера — вы используете отличные шаблоны для отчетов и запускаете много отчетов одновременно. Рано или поздно вы увидите, что «простой» сценарий становится узким местом в производительности.

Вот почему некоторые фреймворки создают собственный движок сценариев поверх существующего API, добавляя некоторые приятные функции для повышения производительности, мониторинга выполнения, сценариев полиглота и т. Д.

Например, в среде CUBA есть довольно сложный механизм сценариев, который реализует функции для улучшения реализации и выполнения сценариев, такие как:

  1. Кэш классов, чтобы избежать повторной компиляции скриптов.
  2. Возможность написания скриптов с использованием языков Groovy и Java.
  3. Бин JMX для управления движком скриптов.

Все это повышает производительность и удобство использования, но, тем не менее, это низкоуровневые API для создания карт параметров, выборки текста сценария и т. Д., Поэтому нам все еще нужно сгруппировать их в модули высокого порядка, чтобы эффективно использовать сценарии в приложении.

И было бы несправедливо не упомянуть новый экспериментальный движок GraalVM и его API-интерфейс polyglot, который позволяет нам расширять Java-приложения другими языками. Так что, возможно, мы увидим, как Нашорн рано или поздно уйдет на пенсию и сможет писать на разных языках программирования в одном и том же исходном файле, но это все еще в будущем.

Spring Framework: предложение, от которого трудно отказаться?

В Spring Framework у нас есть встроенная поддержка сценариев через API JDK, вы можете найти много полезных классов в пакетах org.springframework.scripting. *. Есть оценщики, фабрики и т. Д. Все инструменты, необходимые для создания собственной поддержки сценариев.

Помимо низкоуровневых API, в Spring Framework есть реализация, которая должна упростить работу со сценариями в вашем приложении — вы можете определять bean-компоненты, реализованные на динамических языках, как описано в документации .

Все, что вам нужно сделать, это реализовать класс с использованием динамического языка, такого как Groovy, и описать bean-компонент в XML-файле конфигурации следующим образом:

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

После этого вы можете внедрить bean-компонент Messenger в классы вашего приложения, используя XML-конфигурацию. Этот bean-компонент может быть автоматически «обновлен» в случае изменений базового сценария, рекомендуется использовать AOP и т. Д.

Этот подход выглядит хорошо, но вы, как разработчик, должны реализовать полноценные классы для ваших компонентов, если вы хотите использовать всю мощь поддержки динамического языка. В реальной жизни сценарии могут быть чисто функциями, поэтому вам нужно добавить дополнительный код в сценарий, чтобы он был совместим с Spring. Также в настоящее время некоторые разработчики считают конфигурацию XML «устаревшей» по сравнению с аннотациями и стараются избегать ее использования, поскольку определения и внедрения bean-компонентов разделены между кодом Java и кодом XML. Хотя это скорее вопрос вкуса, а не производительности / совместимости / читабельности и т. Д., Мы могли бы принять это во внимание.

Написание сценариев: проблемы и идеи

Итак, у всего есть своя цена, и когда вы добавляете скрипты в свое приложение, вы можете столкнуться с некоторыми проблемами:

  1. Управляемость. Обычно сценарии разбросаны по всему приложению, поэтому довольно сложно управлять многочисленными вызовами figureGroovy (или аналогичными).
  2. Обнаруживаемость — если что-то идет не так в вызывающем скрипте, довольно сложно найти реальную точку в исходном коде. Мы должны быть в состоянии легко найти все точки вызова скрипта в нашей IDE.
  3. Прозрачность — написание скриптового расширения не является тривиальной задачей, так как нет информации о переменных, отправляемых в скрипт, а также нет информации о результате, который он должен вернуть. В конце концов, сценарии могут быть сделаны только разработчиком и только изучая источники.
  4. Тестирование и обновления — развертывание (обновление) нового скрипта всегда опасно, нет возможности отката и инструментов для его тестирования перед производством.

Похоже, что скрытие вызовов методов со сценариями под обычными методами Java может решить большинство из этих проблем. Предпочтительный способ — вводить «скриптовые» бины и вызывать их методы со значимыми именами, а не вызывать просто другой «eval» метод из служебного класса. Поэтому наш код становится самодокументированным, разработчику не нужно искать файл «disc_10_cl.groovy», чтобы выяснить имена параметров, типы и т. Д.

Еще одно преимущество — если все сценарии имеют уникальные java-методы, связанные с ними, будет легко найти все точки расширения в приложении с помощью функции «Найти использование» в IDE, а также понять, каковы параметры этого сценария и для чего он нужен. возвращается.

Такой способ создания сценариев также упрощает тестирование — мы сможем не только тестировать эти классы «как обычно», но и, при необходимости, использовать макетные среды.

Все это напоминает подход, упомянутый в начале этой статьи, — «специальные» классы для скриптовых методов. А что, если мы пойдем еще дальше и скроем все вызовы механизма сценариев, создания параметров и т. Д. От разработчика?

Концепция хранилища сценариев

Идея довольно проста и должна быть знакома всем разработчикам, которые работали с Spring Framework. Мы просто создаем интерфейс Java и как-то связываем его методы со скриптами. Например, Spring Data JPA использует аналогичный подход, где методы интерфейса преобразуются в запросы SQL на основе имени метода, а затем выполняются механизмом ORM.

Что нам может понадобиться для реализации концепции?

Вероятно, аннотация уровня класса, которая поможет нам обнаружить интерфейсы репозитория сценариев и создать для них специальный компонент Spring.

Аннотация уровня метода поможет нам связать метод с его реализацией по сценарию.

И было бы неплохо иметь реализацию по умолчанию для метода, который является не простой заглушкой, а действительной частью бизнес-логики. Это будет работать, пока мы не реализуем алгоритм, разработанный бизнес-аналитиком. Или мы можем позволить ему / ей написать этот сценарий 🙂

Предположим, вам нужно создать сервис для расчета скидки на основе профиля пользователя. А бизнес-аналитик говорит, что мы можем смело предположить, что по умолчанию для всех зарегистрированных клиентов может быть предоставлена ​​скидка 10%. Мы можем подумать о следующей концепции кода для этого случая:

1
2
3
4
5
6
7
8
9
@ScriptRepository
public interface PricingRepository {
 
       @ScriptMethod
           default BigDecimal applyCustomerDiscount(Customer customer,
                         BigDecimal orderAmount) {
             return orderAmount.multiply(new BigDecimal("0.9"));
       }
}

И когда дело доходит до правильной реализации алгоритма дисконтирования, Groovy скрипт будет выглядеть так:

1
2
3
4
5
6
-------- file discount.groovy --------
def age = 50
if ((Calendar.YEAR - cust.birthday.year) >= age) {
   return amount.multiply(0.75)
}
--------

Конечная цель всего этого — позволить разработчику реализовать только интерфейс и только скрипт алгоритма дисконтирования, и не возиться со всеми этими вызовами «getEngine» и «eval». Решение для сценариев должно делать всю магию: когда метод вызывается, перехватывает вызов, находит и загружает текст сценария, оценивает его и возвращает результат (или выполняет метод по умолчанию, если текст сценария не найден). Идеальное использование должно выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@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
 
 }

Вызов скрипта читабелен, и способ его вызова знаком любому разработчику Java, я полагаю.

Это были идеи, и они были использованы для создания библиотеки для реализации репозиториев скриптов с использованием Spring Framework. В библиотеке есть средства для загрузки текста сценария из разных источников и оценки, а также некоторые API, которые позволяют разработчику при необходимости реализовывать расширения для библиотеки.

Как это работает

Библиотека вводит некоторые аннотации (а также XML-конфигурацию для тех, кто предпочитает ее), которые инициируют создание динамических прокси для всех интерфейсов репозитория, помеченных аннотацией @ScriptRepository во время инициализации контекста. Эти прокси публикуются в виде одноэлементных компонентов, которые реализуют интерфейсы репозитория. Это означает, что вы можете внедрить эти прокси-компоненты в свои компоненты с помощью @Autowired или @Inject в точности так, как показано в фрагменте кода в предыдущем разделе.

Аннотация @EnableSpringRepositories, используемая в одном из классов конфигурации приложения, активирует репозитории сценариев. Этот подход аналогичен другим знакомым аннотациям Spring, таким как @EnableJpaRepositories или @EnableMongoRepositories. И для этой аннотации вам нужно указать массив имен пакетов, которые следует сканировать аналогично репозиториям JPA.

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

Как было показано ранее, нам нужно пометить каждый метод в репозитории сценариев @ScriptMethod (библиотека также предоставляет @GroovyScript и @JavaScript ), чтобы добавить метаданные к этим вызовам и указать, что эти методы являются сценариями. И реализация по умолчанию для скриптовых методов, конечно, поддерживается. Все компоненты решения отображаются на диаграмме ниже. Синие фигуры относятся к коду приложения, белые — к библиотеке. Весенние бобы отмечены логотипом Spring.

Scripting

Когда вызывается скриптовый метод интерфейса, он перехватывается прокси-классом, который выполняет поиск двух компонентов: провайдера для получения текста сценария реализации и оценщика для получения результата. После оценки скрипта результат возвращается вызывающей службе. И поставщик, и оценщик могут быть указаны в свойствах аннотации @ScriptMethod, а также в тайм-ауте выполнения (хотя библиотека предоставляет значения по умолчанию для этих свойств):

01
02
03
04
05
06
07
08
09
10
11
12
@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 и javascript из файловой системы, а также оценщики на основе JSR-233 для обоих языков сценариев. Вы можете создавать собственные провайдеры и оценщики для разных хранилищ скриптов и механизмов исполнения. Все эти средства основаны на интерфейсах среды Spring ( org.springframework.scripting.ScriptSource и org.springframework.scripting.ScriptEvaluator ), поэтому вы можете повторно использовать все ваши классы на основе Spring, например StandardScriptEvaluator, вместо класса по умолчанию.

Поставщики (а также оценщики) публикуются как компоненты Spring, поскольку прокси-сервер хранилища сценариев разрешает их по имени для гибкости — вы можете заменить исполнителя по умолчанию новым, не изменяя код приложения, но заменяя один компонент в контексте приложения.

Тестирование и управление версиями

Поскольку сценарии могут быть легко изменены, мы должны гарантировать, что мы не сломаем производственный сервер при изменении сценария. Библиотека совместима с тестовой средой JUnit, в этом нет ничего особенного. Поскольку вы используете его в приложении на основе Spring, вы можете протестировать свои сценарии, используя как модульные, так и интеграционные тесты как часть приложения, прежде чем загружать их в производство, также поддерживается mocking.

Кроме того, вы можете создать провайдер скриптов, который будет читать разные текстовые версии скриптов из базы данных или даже из Git или другой системы контроля версий. В этом случае будет легко переключиться на более новую версию скрипта или откатиться к предыдущей версии скрипта, если что-то пойдет не так в работе.

Вывод

Библиотека поможет вам организовать скрипты в вашем коде, обеспечивая следующее:

  1. Внедряя интерфейсы Java, разработчик всегда получает информацию о параметрах скрипта и их типах.
  2. Поставщики и оценщики помогают вам избавиться от вызовов механизма сценариев, разбросанных по коду вашего приложения.
  3. Мы можем легко определить местонахождение всех сценариев в коде приложения, используя команду IDE «Найти примеры (ссылки)» или просто текстовый поиск по имени метода.

Вдобавок к этому поддерживается автоконфигурация Spring Boot, и вы также можете протестировать свои сценарии перед их развертыванием в производство, используя знакомые модульные тесты и технику насмешки.

В библиотеке есть API для получения метаданных сценариев (имен методов, параметров и т. Д.) Во время выполнения, вы можете получить упакованные результаты выполнения, если хотите избежать записи блоков try..catch для обработки исключений, генерируемых сценариями, а также поддерживает XML Конфигурация, если вы предпочитаете хранить свою конфигурацию в этом формате.

Кроме того, время выполнения скрипта может быть ограничено параметром времени ожидания в аннотации.

Источники библиотеки можно найти по адресу https://github.com/cuba-rnd/spring-script-repositories .

Опубликовано на Java Code Geeks с разрешения Андрея Беляева, партнера нашей программы JCG . Смотрите оригинальную статью здесь: в любой непонятной ситуации идите скрипты

Мнения, высказанные участниками Java Code Geeks, являются их собственными.