![]() |
Навигация
|
Главная » Xml Используйте динамические языки динамично : Часть 2. Оперативный поиск, выполнение и изменение скриптов (исходники)Источник: developerWorks Россия Том МакКвини Java Scripting API, появившийся в Java SE 6, предлагает унифицированный подход для запуска (а также предоставления совместного доступа к коду и данным) внешних программ, написанных на различных динамических языках. Усиление Java-приложения благодаря мощности и гибкости скриптового языка особенно полезно в случае, когда скрипт может выполнить задачу более наглядно, просто или лаконично. Но Java Scripting API не просто позволяет добавлять множество скриптовых языков в Java-программу единообразным способом. Он также реализует нахождение скриптов, их чтение и оперативное выполнение. Такие динамические возможности позволяют модифицировать скрипты для изменения логики приложения во время исполнения программы. Эта статья демонстрирует как вы можете использовать Java Scripting API для вызова внешних скриптов и динамически влиять на программную логику. Здесь также рассматриваются проблемы с которыми вы можете столкнуться при интеграции одного или нескольких скриптовых языков в Java-приложение.Часть 1 знакомит с Java Scripting API используя приложение в стиле Hello World. Более реалистичный пример, который будет здесь приведен, использует Scripting API для создания механизма динамических правил, определяющего правила в виде внешних скриптов, написанных на Groovy, JavaScript и Ruby. Правила принимают решение, могут ли соискатели жилищного займа претендовать на отдельные ипотечные продукты. Определение бизнес-правил средствами скриптового языка упрощает написание этих правил и скорее всего облегчает процесс их чтения для непрограммистов, например, менеджеров по займам. Вынесение правил наружу посредством Java Scripting API также позволяет по ходу выполнения приложения изменять эти правила и добавлять новые ипотечные продукты. Привет, реальный мирЭтот пример приложения обслуживает типовые задачи по жилищному займу для вымышленной компании Shaky Ground Financial. Индустрия жилищного займа непрерывно выдумывает новые ипотечные продукты и изменяет правила предоставления доступа к ним. Shaky Ground Financial желает не только быстро добавлять и удалять имеющиеся предложения, но и оперативно вносить изменения в бизнес-правила, определяющие доступность каждого продукта для конкретного потребителя.На выручку приходит Java Scripting API. Приложение состоит из класса ScriptMortgageQualifier ответственного за ответ на вопрос доступен ли данный ипотечный продукт для заемщика, предпринимающего попытку купить конкретную собственность. Класс приведен в листинге 1.Листинг 1. Класс ScriptMortgageQualifier
Этот класс сравнительно небольшой поскольку он делегирует все бизнес-решения внешним скриптам. Каждый скрипт представляет один ипотечный продукт. Код в каждом скриптовом файле содержит бизнес-правила для определения типа заемщика, собственности или ссуды доступных на соискание для этого ипотечного продукта. Если это так, то новые ипотечные продукты могут быть добавлены копированием нового скриптового файла в директорию скриптовых продуктов. Если же меняются бизнес-правила оценки претендента, это учитывается обновлением соответствующего скрипта. Написание бизнес-правил под ипотечные предложения на скриптовых языках удачно подчеркивает возможности Java Scripting API. Это также показывает, что скриптовый язык иногда может быть легче для прочтения, изменения и понимания, даже для непрограммистов. Как работает класс ScriptMortgageQualifierНаиболее важным методом в классеScriptMortgageQualifier является qualifyMortgage() . Этот метод принимает в качестве параметров:
Borrower , Property и Loan здесь не показан. Это просто сущностные классы, доступные в исходных кодах, прилагаемых к этой статье (см. Файлы для загрузки).При поиске ScriptEngine для запуска скриптового файла метод qualifyMortgage() использует внутренний впомогательный метод getEngineForFile() . Метод getEngineForFile() задействует переменную экземпляра scriptEngineManager : она инициализирована на этапе конструирования класса через вызов ScriptEngineManager() для поиска скриптового движка, способного отработать скрипт с заданным файловым расширением. Метод getEngineForFile() обращается к методу ScriptEngineManager.getEngineByExtension() , выделенному полужирным шрифтом в листинге 1, для поиска и получения ScriptEngine .Получив скриптовый движок, qualifyMortgage() связывает входные сущностные параметры с контекстом движка, обеспечивая доступность этих данных в скрипте. Первых три вызова scriptEngine.put() , также выделенных полужирным шрифтом, выполняют такое связывание. Четвертый вызов scriptEngine.put() создает новый Java-объект MortgageQualificationResult и разделяет доступ к нему со скриптовым движком. Такой разделяемый объект, возвращаемый qualifyMortgage() , позволяет скрипту передавать результаты своей работы обратно в Java-приложение через установку свойств объекта. Скрипты взаимодействуют с этим Java-объектом используя глобальную переменную result . Каждый скрипт несет ответственность за сообщение результата Java-программе посредством такого разделяемого скриптового объекта.Заключительный вызов scriptEngine.put() обеспечивает доступность экземпляра внутреннего вспомогательного класса ScriptEarlyExit , также приведенного в листинге 1, в скриптах через переменную с именем scriptExit . ScriptEarlyExit определяет два простых метода: withMessage() и noMessage() , их единственная задача выбросить исключение. Если скрипт вызывает scriptExit.withMessage() или scriptExit.noMessage() , метод выбрасывает ScriptEarlyExitException . Скриптовый движок перехватывает это исключение, прекращает отработку скрипта и выбрасывает ScriptException в метод eval() , вызвавший скрипт.Такой обходной маневр для преждевременного завершения скрипта обеспечивает устойчивый способ выхода из скриптового процесса и возврат в функцию или метод. Не все скриптовые языки поддерживают конструкции для такой цели. В JavaScript, например, вы не можете использовать оператор return при исполнении кода верхнего уровня, наподобие того, как оформлены скрипты ипотечной обработки в текущем примере приложения. Разделяемый объект scriptExit обеспечивает решение проблемы, позволяя скрипту, реализованному на любом языке, завершать работу сразу же, как только скрипт выясняет, что заемщику данный ипотечный продукт не доступен.Обращение из qualifyMortgage к методу eval скриптового движка, выделенное полужирным шрифтом, использует блок try /catch для перехвата ScriptException . Исследовав сообщение об ошибке для ScriptException , код в блоке catch определяет, является ли причиной скриптового исключения ScriptEarlyExitException или же другая, специфичная для скриптинга ошибка. Если сообщение об ошибке содержит название ScriptEarlyExitException , код предполагает, что все происходит в рамках допустимого и игнорирует такое исключение.Такая техника определения скриптовых исключений уровня Java Scripting API по строковым литералам несколько аляповата, но она работает для языковых интерпретаторов Groovy, JavaScript и Ruby, используемых в рассматриваемом примере. Было бы лучше, если бы все реализации скриптовых языков добавляли Java-исключения, выбрасываемые из вызываемого Java-кода, в стек исключений с последующим доступом через метод Throwable.getCause() . Такие интерпретаторы как JRuby и Groovy именно так и поступают, а вот встроенный интерпретатор Rhino JavaScript - нет.Запускаем код: ScriptMortgageQualifierRunnerДля тестирования классаScriptMortgageQualifier вы будете использовать тестовые данные, представляющие четырех простых заемщиков, экземпляр имущества, которое заемщик попытается приобрести, и вариант ипотечной ссуды. Вы запустите заемщика в связке с имуществом и ссудой в комбинации с каждым из трех скриптов, чтобы увидеть бизнес-правила для соответствующего ипотечного продукта, представленного скриптом.В листинге 2 частично приведена программа ScriptMortgageQualifierRunner , которую вы будете использовать для создания тестовых объектов, поиска скриптовых файлов в директории и их последующего запуска посредством класса ScriptMortgageQualifier из листинга 1. Вспомогательные методы программы createGoodBorrower() , createAverageBorrower() , createInvestorBorrower() , createRiskyBorrower() , createProperty() и createLoan() не приводятся для экономии места. Они всего лишь создают сущностные объекты и устанавливают соответствующие значения для тестирования. Доступен полный исходный код со всеми методами (см. Файлы для загрузки).Листинг 2. Программа ScriptMortgageQualifierRunner
Метод main() в ScriptMortgageQualifierRunner ищет по параметру из командной строки директорию, из которой необходимо читать скриптовые файлы, инициализирует статическую переменную объектом File , если директория существует и вызывает метод run() для выполнения дальнейшей обработки.Метод run() создает экземпляр класса ScriptMortgageQualifier из листинга 1, затем в бесконечном цикле вызывает внутренний метод runQualifications() с четырьмя сценариями заемщик/заем. Бесконечный цикл имитирует продолжительное функционирование ипотечного приложения. Цикл позволяет добавлять или изменять скрипты (продукты ипотечного заема) в рабочей директории и отслеживать эти изменения динамически, без остановки приложения. Динамическая возможность изменения бизнес-логики в процессе выполнения обеспечивается тем фактом, что бизнес-логика приложения живет во внешних скриптах.Вспомогательный метод runQualifications() на самом деле вызывает ScriptMortgageQualifer.qualifyMortgage , по одному вызову для каждого скриптового файла, обнаруженного в скриптовой директории. Каждый вызов предваряется операторами вывода описания скриптового файла и текущего заемщика, затем следуют остальные операторы вывода, показывающие, может ли заемщик претендовать на очередной ипотечный продукт. Квалификационные итоги определяются атрибутами разделяемого Java-объекта MortgageQualificationResult , используемого скриптовым кодом для возврата результатов.ZIP-файл с исходными кодами для этой статьи содержит простые скриптовые файлы, написанные на Groovy, JavaScript и Ruby. Каждый представляет различный тип стандартного 30-летнего ипотечного займа с фиксированным процентом по ссуде. Код в скриптах определяет, квалифицируется ли заемщик на этот тип ипотеки и возвращает результат вызовом методов на разделяемой глобальной переменной result , внесенной в скриптовый движок методом put() , описанным мною ранее. Глобальная переменная result является экземпляром класса MortgageQualificationResult , частично приведенного в листинге 3.Листинг 3. Результирующий класс MortgageQualificationResult
Скрипты устанавливают свойства result , приведенные в листинге 3, для возврата данных о праве заемщика на ипотеку и о размере процентной ставки. Свойства message и productName позволяют скрипту указать причину, по которой заемщику отказано в ипотеке и вернуть имя соответствующего продукта.Скриптовые файлыПрежде чем я покажу вывод от запускаScriptMortgageQualifierRunner , давайте взглянем на скриптовые файлы Groovy, JavaScript и Ruby, вызываемые программой. Бизнес-логика в Groovy-скрипте определяет ипотечный продукт, на который относительно просто квалифицироваться, но для него заявлена повышенная процентная ставка, отражающая более высокий финансовый риск. JavaScript-скрипт представляет спонсируемый государством ипотечный заем, ориентированный на максимальную доходность и прочие ограничения, которым должен соответствовать заемщик. Ипотечный продукт в Ruby-скрипте содержит бизнес-правила, ограничивающие квалификационные возможности заемщика хорошей кредитной историей и гарантированной способностью оплатить задаток. В качестве поощрения предлагается пониженная процентная ставка.Листинг 4 демонстрирует Groovy-скрипт, с которым вы сможете разобраться даже не зная Groovy. Листинг 4. Ипотечный Groovy-скрипт
Обратите внимание на глобальные переменные result , borrower , loan и property , используемые скриптом для доступа и установки значений в разделяемых Java-объектах. Это имена тех переменных, которые были назначены вызовом метода ScriptEngine.put() .Также отметьте такие конструкции Groovy как result.productName = 'Groovy Mortgage' . Здесь напрямую устанавливается строковое свойство productName объекта MortgageQualificationResult , даже невзирая на то, что в листинге 3 четко показано, что это приватная переменная экземпляра класса. Это не коварство Java Scripting API, допускающего нарушение принципов инкапсуляции. Наоборот, Groovy и большинство других интерпретаторов скриптовых языков, доступных вам через Java Scripting API, достаточно хорошо взаимодействуют с разделяемыми Java-объектами. Если оператор Groovy намеревается установить или прочитать значение приватного свойства некоторого Java-объекта, Groovy подыскивает и использует публичные методы типа setter /getter в JavaBean-стилистике, если таковые определены. Например, выражение result.productName = 'Groovy Mortgage' автоматически транслируется в легко предсказуемый Java-оператор: result.setProductName("Groovy Mortgage") . Такой эквивалентный Java-setter также допустим в Groovy и также смог бы замечательно работать в скрипте, но прямое использование оператора присвоения для свойства считается "Groov-астее".Теперь перейдем к JavaScript-продукту ипотеки в листинге 5. JavaScript-ипотека призвана презентовать спонсируемый государством заем для поддержки домовладельцев. Бизнес-правила в данном случае требуют, чтобы у заемщика это было первое приобретение недвижимости и чтобы заемщик планировал использовать ее именно для проживания, а не, скажем, для сдачи в аренду с целью получения прибыли. Листинг 5. Ипотечный JavaScript-скрипт
Обратите внимание: JavaScript-код не может использовать Java-метод scriptExit.withMessage() , задействованный Groovy-скриптом для установки сообщения о дисквалификации и выхода из скрипта в одном выражении. Причина в том, что интерпретатор Rhino JavaScript не обеспечивает всплывания ( bubble-up ) выброшенного Java-исключения как вложенной "причины" в результирующей трассировке стека ScriptException . Таким образом, весьма затруднительно отыскать в стеке сообщение об исключении на уровне скрипта, выброшенное из Java-кода. Поэтому JavaScript-код в листинге 5 должен установить причину в результирующем сообщении отдельно, перед вызовом scriptExit.noMessage() , чтобы затем возбудить исключение, приводящее к завершению работы скрипта.Третий и заключительный ипотечный продукт, показанный в листинге 6, написан на Ruby. Продукт предназначен для заемщиков с хорошей кредитной историей, способных оплатить 20%-ный задаток. Листинг 6. Ипотечный Ruby-скрипт
$ в начале имени. Таков синтаксис Ruby для глобальных переменных. Скриптовые движки со скриптовыми языками разделяют Java-переменные как глобальные, поэтому в Ruby должен использоваться соответствующий синтаксис.Также обратите внимание в листинге и на то, как JRuby автоматически конвертирует "Ruby-змы" в "Java-измы" при вызове разделяемых Java-объектов. Например, если JRuby видит, что вызываемый на Java-объекте метод следует принятому в Ruby соглашению по разделению слов подчеркиваниями типа $result.product_name = 'Ruby Mortgage' , JRuby ищет в качестве альтернативы имя метода со смешанным регистром, если не удалось отыскать исходное имя с подчеркиваниями. Таким образом, оформленное в стилистике Ruby имя метода product_name= корректно оттранслируется в Java-вызов result.setProductName("Ruby Mortgage") .Программный выводТеперь взглянем на вывод программыScriptMortgageQualifierRunner при ее запуске с тремя скриптовыми файлами ипотечных продуктов. Вы можете запустить эту программу, используя Ant-скрипт из состава предлагаемых к загрузке исходников. Если же вы предпочитаете Maven, файл README.txt из загрузочного ZIP-архива содержит инструкции по построению и запуску проекта под Maven. Команда для Ant такая: ant run . Задание run гарантирует, что скриптовые движки и языковые JAR-файлы доступны через classpath. Листинг 7 показывает результат вывода для Ant.Листинг 7. Программный вывод для Ant
Вывод содержит 12 секций, так как программа направляет каждый из четырех тестовых профилей заемщиков ссуды в каждый из трех скриптов для выяснения возможности заемщика квалифицироваться на любой (или на все) из трех ипотечных продуктов. Далее, для демонстрационных целей в рамках данной статьи, программа приостанавливается на одну минуту, затем повторяет отработку ипотечных скриптов. Во время паузы вы можете редактировать любой из скриптовых файлов для изменения бизнес-правил или же добавлять новые файлы в скриптовую директорию, чтобы презентовать ипотечные продукты собственного изготовления. Программа сканирует скриптовую директорию на каждом проходе и отрабатывает любой из обнаруженных скриптовых файлов. Например, предположим, что вы хотите повысить минимальный уровень кредитных баллов, необходимых для прохождения квалификации на заем. В течение минутной паузы вы могли бы отредактировать скрипт JavaScriptFirstTimeMortgage.js из директории src/main/scripts/mortgage-products (см. листинг 5) для изменения бизнес-правила в строке 23 с if (borrower.creditScore < 500) { на if (borrower.creditScore < 550) { . При следующем запуске правил вы заметите, что Risk E. Borrower более не квалифицируется на "JavaScript FirstTime Mortgage". Для этого заемщика стало бы недостаточно 520 кредитных баллов. Сообщение об ошибке по-прежнему гласило бы: "Количество ваших кредитных баллов в 520 единиц не соответствует требованию в 500", но вы смогли бы сразу же подправить и это устаревшее на данный момент сообщение.Как избежать опасностей динамического скриптингаВозможность изменять ваши программы во время их исполнения неоценима. И потенциально опасна. Новая функциональность и новые бизнес-правила могут быть добавлены в работающее приложение без его остановки и перезапуска. Таким же образом новые баги, особенно потенциально фатальные, могут быть добавлены с той же легкостью.Однако динамическое изменение работающего приложения не более опасно, чем изменение остановленного. Статическая техника подразумевает лишь то, что вы должны перезапустить приложение перед поиском этих новых ошибок. Хорошая практика разработки программного обеспечения диктует, что любое изменение - динамическое или статическое - в результирующем приложении должно быть предварительно протестировано, прежде чем оно будет внедрено. Java Scripting API не отменяет этого правила. Внешние скриптовые файлы могуть быть протестированы в процессе разработки в рамках регулярного модульного тестирования. Вы можете задействовать JUnit или же другую тестовую оснастку с подставными ( mock ) Java-объектами, необходимыми скрипту в процессе работы, затем выполнить скрипт с такими подстановками, чтобы убедиться, что скрипт отрабатывает корректно и продуцирует ожидаемые результаты. Воплощение логики приложения на не-Java скриптовых файлах не дает нам права отказаться от тестирования этих скриптов. Если в прошлом или же на данный момент вы являетесь программистом скриптов Web CGI, для вас не будет сюрпризом необходимость соблюдать осторожность в том, что вы передаете в метод eval() объекта ScriptEngine . Скриптовый движок немедленно выполнит переданный в метод eval код. Поэтому вам не следует передавать строку или объект Reader , содержащие текст из ненадежного источника, в скриптовый движок на выполнение.Например, вполне возможно использовать скриптовый API для удаленного мониторинга Web-приложения. Вы можете предоставить скриптовому движку доступ к ключевым Java-объектам, содержащим статусную информацию о Web-приложении и создать простую Web-страницу, принимающую произвольное скриптовое выражение для отработки скриптовым движком с последующими вычислениями и выводом результата обратно на Web-страницу. Таким способом вы могли бы запросить и выполнить методы на запущенных Java-объектах для простого определения статуса и работоспособности приложения. В приведенном сценарии кто угодно, имея доступ к такой Web-странице, смог бы выполнить любые выражения, доступные в скриптовом языке и добраться до любого из разделяемых Java-объектов. Небрежное программирование, конфигурационные просчеты или бреши в системе безопасности могут привести к утечке конфиденциальной информации к неуполномоченным пользователям или же подставить ваше приложение под атаку "отказа в обслуживании" ( denial-of-service attack ), если хакер выполнит скриптовую конструкцию вроде System.exit или /bin/rm -fr / . Как и любой другой мощный инструмент, Java Scripting API требует от вас соблюдения определенной осторожности.Дальнейшее изучениеЭта статья сфокусирована на способности Java-приложений динамически читать и оперативно выполнять внешние скрипты, а также возможностях доступа из таких скриптов к Java-объектам, явным образом опубликованным для этой цели. Java Scripting API предоставляет дополнительные возможности. Например, вы можете:
Файлы для загрузки:Исходный код и все JAR-файлы
![]() ![]() ![]() ![]() ![]() Главная » Xml |
© 2023 Team.Furia.Ru.
Частичное копирование материалов разрешено. |