Легкая и динамическая бизнес логика на JavaScript при помощи Mozilla Rhino

в 9:44, , рубрики: java, javascript, mozilla rhino

image

Предыстория

Хотелось бы начать с предыстории. В данный момент я разрабатываю некое веб-приложение на Java, ничего необычного, но в документе от заказчика есть требование: будущие администраторы приложения должны иметь возможность налету подгружать код бизнес логики на сервер. Вроде бы ничего сверхъестественного, нужно будет сделать подгрузку java-классов, думал я, пока на днях мне в голову не пришла идея: “А что, если дать возможность программировать методы бизнес логики на JavaScript?”.
В тот момент идея показалась мне очень хорошей, и я видел целый ряд преимуществ этой идеи перед простой подгрузкой java-классов:

  • Во-первых, JavaScript — это очень простой язык описания логики, писать на нем может любой программист, знакомый с принципами ООП и C-подобным синтаксисом.
  • Во-вторых, т.к. внешнее API сервера спроектировано в стиле REST, js-код отлично ложится в рамки ресурса, без проблем сериализуется в JSON-строку и не требует компиляции и дополнительных манипуляций.
  • В-третьих, исполнение JavaScript-кода интерпретатором — это исполнение кода в рамках песочницы безопасности, что дает нам возможность четко настраивать правила поведения кода бизнес-логики.

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

Выбираем интерпретатор JavaScript

За движком, который будет интерпретировать для js далеко ходить не пришлось, первым же кандидатом является Mozilla Rhino — js движок, написанный полностью на Java.
Движок берет свое начало от 1997 года, родившись в стенах Netscape под именем “Javagator”, но позже, при загадочных обстоятельствах, был переименован в Rhino. “Носорог” лицензировался некоторыми крупными компаниями(включая Sun) для своих проектов. Изначально идея была в том, что бы компилировать Java-байт код на основе JavaScript, но этот подход имел некоторые проблемы:

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

В 1998 году в движок добавлен по умолчанию режим интерпретатора, т.е. JavaScript код исполняется на лету, минуя компиляцию в байт код. Это позволило избежать проблем с тяжелой компиляцией и утечками памяти. Позже в этом году движок открывает исходный код и отдается mozilla.org.
Скачать бинарники и исходный код библиотеки можно на оф.сайте: ссылка на сайт. В примерах будет использоваться rhino1.7 R4.
Rhino можно использовать двумя способами: из командной строки и напрямую, встраивая jar-файл в проект. Нас сейчас интересует именно второй способ — добавляем в проект файл js.jar.

Технология LiveConnect

Одно из основных идей Rhino — возможность программировать Java-сервер при помощи JavaScript-кода, используя технологию LiveConnect. Технология дает возможность обращаться к Java-классам напрямую из js, не прибегая к помощи какого-то стороннего кода.
Вот небольшой пример обращения к классам File и System:

var f = new java.io.File(“test.txt”);
java.lang.System.out.println(f.exists());

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

Использование Rhino

В качестве примера разработаем простенький модуль бизнес логики для гипотетического проекта “Сервис учета учеников и их оценок” для университета. Приложение должно хранить список всех учеников и их оценки, а так же уметь производить над ними какие-то действия, исходя из поставленных задач.
Начнем, пожалуй, с основ использования. Rhino имеет 2 основных понятия — это контекст(класс org.mozilla.javascript.Context) и сфера(класс org.mozilla.javascript.Scriptable). Контекст — это инстанс интерпретатора, который привязывается к одному потоку, следовательно, интерпретирует js в едином потоке. Сфера — это так называемый namespace, в котором мы определяем все интересующие нас переменные.
Пример создания контекста и сферы:

// Создаем контекст
Context context = Context.enter();

// Создаем сферу
Scriptable scope = context.initStandardObjects();

После того, как мы создали контекст и сферу, мы должны ограничить интерпретатору доступ к Java-классам. Это делается при помощи метода setClassShutter экземпляра контекста:

// Ограничиваем доступ LiveConnect к Java-классам
context.setClassShutter(new ClassShutter() {

@Override
public boolean visibleToScripts(String fullClassName) {
    // Определяет, виден ли класс с именем fullClassName скрипту
           return false;
    }
});

По умолчанию Rhino использует технологию LiveConnect, которая дает доступ к java-классам прямо из js. Она дает большие возможности доверенному коду, но у нас иной случай — наш сервер будет интерпретировать потенциально небезопасный код.
Будет весьма неприятно, если в интерпретатор попадет js-код такого вида:

java.lang.System.exit(0);

Поэтому, мы попросту “затыкаем” LiveConnect и оставляем доступ лишь к тем классам, которые нам нужны. После того, как мы получили контекст и сферу, нам не остается ничего другого, кроме как интерпретировать js-код:

String script = “var mathStuff = Math.cos(Math.PI)”;

c.evaluateString(scope, script, null, 1, null);

Вот и все, после работы с Rhino, завершаем работу с контекстом и освобождаем ресурсы:

Context.exit();

“Песочница” для бизнес логики

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

  • DatabaseModule, который будет отвечать за связь с базой данных,
  • NotificationModule, который будет отвечать за оповещение пользователей системы о каких-то событиях.

public class DatabaseModule {

    public DatabaseModule(){

    }

    /* Метод возвращает ФИО студента, принимая его идентификатор в качестве аргумента */
    public String getStudent(int id){

        return (id > 0) ? "Шевченко Константин Викторович" : null;
    }

    /* Метод возвращает оценку студента, принимая его ФИО в качестве аргумента */
    public int getRating(String student){

        return student.equals("Шевченко Константин Викторович") ? 5 : -1;
    }

    /* Метод указывает новую оценку студента, принимая его ФИО и новую оценку в качестве аргументов */
    public void setRating(String student, int newRating){
        System.out.println("setRating() student = "+student+", newRating = "+newRating);
        // Do something
    }
}

public class NotificationModule {

    public NotificationModule(){

    }

    /* Метод оповещает студента сообщением */
    public void notifyStudent(String student, String message){
        System.out.println("notifyStudent() student = "+student+", message = "+message);
    }

    /* Метод оповещает куратора */
    public void notifyCurator(String message){
        System.out.println("notifyCurator() message = "+message);
    }
}

Далее определяем константные ссылки на модули в ранее определенной сфере:

// Маппим DatabaseModule в js
DatabaseModule database = new DatabaseModule();
Object wrappedDatabaseModule = Context.javaToJS(database, scope);
ScriptableObject.putConstProperty(scope, "databaseModule", wrappedDatabaseModule);

// Маппим NotificationModule в js
NotificationModule notification = new NotificationModule();
Object wrappedNotificationModule = Context.javaToJS(notification, scope);
ScriptableObject.putConstProperty(scope, "notificationModule", wrappedNotificationModule);

Программирование бизнес логики на JavaScript

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

var student = databaseModule.getStudent(1);
var rating = databaseModule.getRating(student);

var pass = rating >= 40;

if(pass){
    notificationModule.notifyCurator("Student "+student+" is admitted to the exam.");
    notificationModule.notifyStudent(student, "You admitted to the exam.");
} else {
    var dif = 40 - rating;
    notificationModule.notifyCurator("Student "+student+" needs "+dif+" points to be admitted to the exam.");
    notificationModule.notifyStudent(student, "You need "+dif+" points to be admitted to the exam.");
}

Все, что остается, это настроить интерпретатор и передать ему js. Предупрежу сразу: движок болезненно воспринимает кириллицу.

Заключение

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

  • Безопасность скрипта: не имеет ли он рекурсий и бесконечных циклов, не сильно нагружает ли он память,
  • Валидация скрипта: правильно ли он написан, правильно ли он взаимодействует с серверным API,
  • Безопасность сервера: к каким модулям имеет доступ скрипт.

К счастью, все вопросы вполне решаемы, исходный код Rhino отркрыт для модификации.

Полезные ссылки

Оф. сайт проекта: https://developer.mozilla.org/ru/docs/Rhino
Статья на Википедии: http://ru.wikipedia.org/wiki/Rhino
Оф. документация: https://developer.mozilla.org/en-US/docs/Rhino_documentation
API Reference (не оф.): http://tool.oschina.net/uploads/apidocs/rhino/
Исходники примера на GitHub: https://github.com/andrew-medvedev/rhino-example

Автор: nh3000

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js