Easy and dynamic JavaScript business logic with Mozilla Rhino
- From the sandbox
- Tutorial
Background
I would like to start with the background. At the moment I am developing some kind of Java web application, nothing unusual, but there is a requirement in the document from the customer: future application administrators should be able to load business logic code on the server on the fly. It seems to be nothing supernatural, it will be necessary to load java classes, I thought, until recently the idea came to my mind: “But what if you give the opportunity to program business logic methods in JavaScript?”.
At that moment, the idea seemed very good to me, and I saw a number of advantages of this idea over simple loading of java classes:
- Firstly, JavaScript is a very simple language for describing logic; any programmer familiar with the principles of OOP and C-like syntax can write on it.
- Secondly, because the external server API is designed in the REST style, the js-code fits perfectly into the resource framework, it can be serialized into a JSON-string without any problems and does not require compilation and additional manipulations.
- Thirdly, the execution of JavaScript code by the interpreter is the execution of code within the security sandbox, which allows us to clearly configure the rules of behavior of the business logic code.
But do not forget that great power is a big responsibility, therefore, after new opportunities, new questions arise that require a detailed answer.
In this article, I would like to briefly talk about the idea of describing business logic in pure js, touch on the theoretical and practical parts, as well as describe some of the nuances that may arise after this decision.
Choosing a JavaScript interpreter
There was no need to go far behind the engine that will interpret for js, the first candidate is Mozilla Rhino - the js engine, written entirely in Java.
The engine originates from 1997, born in the walls of Netscape under the name “Javagator”, but later, under mysterious circumstances, it was renamed Rhino. Rhino has been licensed by some large companies (including Sun) for its projects. Initially, the idea was to compile Java-byte code based on JavaScript, but this approach had some problems:
- Firstly, the process of compiling and loading classes was a bit heavy,
- Secondly, memory leaks sometimes occurred.
In 1998, the interpreter mode was added to the engine by default, i.e. JavaScript code is executed on the fly, bypassing compilation into byte code. This avoided problems with heavy compilation and memory leaks. Later this year, the engine opens the source code and is given to mozilla.org.
You can download the binaries and the source code of the library on the official website: link to the site . The examples will use rhino1.7 R4.
Rhino can be used in two ways: from the command line and directly by embedding the jar file in the project. We are now interested in exactly the second method - we add the js.jar file to the project.
LiveConnect Technology
One of the main ideas of Rhino is the ability to program a Java server using JavaScript code using LiveConnect technology. The technology makes it possible to access Java classes directly from js without resorting to any third-party code.
Here is a small example of calling classes
File
and System
:var f = new java.io.File(“test.txt”);
java.lang.System.out.println(f.exists());
LiveConnect gives us a lot of space for action, a lot has been written about it in the office. engine documentation, but I will not dwell on it now, because we are interested in interpreting code from an untrusted external source that should not have access to Java classes.
Using Rhino
As an example, we will develop a simple business logic module for the hypothetical project “Student Accounting and Grade Accounting Service” for the university. The application should keep a list of all students and their grades, as well as be able to perform some actions on them, based on the tasks.
Let's start with the basics of use. Rhino has 2 basic concepts - a context (class
org.mozilla.javascript.Context
) and a sphere (class org.mozilla.javascript.Scriptable
). Context is an instance of an interpreter that binds to a single thread, therefore, interprets js in a single thread. A sphere is a so-called namespace in which we define all the variables of interest to us. An example of creating a context and scope:
// Создаем контекст
Context context = Context.enter();
// Создаем сферу
Scriptable scope = context.initStandardObjects();
After we have created the context and scope, we must restrict the interpreter access to Java classes. This is done using the
setClassShutter
context instance method :// Ограничиваем доступ LiveConnect к Java-классам
context.setClassShutter(new ClassShutter() {
@Override
public boolean visibleToScripts(String fullClassName) {
// Определяет, виден ли класс с именем fullClassName скрипту
return false;
}
});
By default, Rhino uses LiveConnect technology, which gives access to java classes directly from js. It gives great opportunities to trusted code, but we have a different case - our server will interpret potentially unsafe code.
It will be very unpleasant if a js-code of the following form gets into the interpreter:
java.lang.System.exit(0);
Therefore, we simply “plug” LiveConnect and leave access only to the classes that we need. After we get the context and scope, we have no choice but to interpret the js code:
String script = “var mathStuff = Math.cos(Math.PI)”;
c.evaluateString(scope, script, null, 1, null);
That's all, after working with Rhino, we finish working with the context and free up resources:
Context.exit();
“Sandbox” for business logic
Now that we know how to get started with Rhino, we can move on to defining an external business logic API in the form of several constant links to top-level modules:
- DatabaseModule , which will be responsible for communicating with the database,
- NotificationModule , which will be responsible for notifying users of the system about some events.
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);
}
}
Next, we define constant links to modules in a previously defined area:
// Маппим 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 business logic programming
Suppose that we have a task: to select information about a student from the database by his identifier, get his grade, calculate whether he has enough points to get admission to the exam and notify the curator and the student himself. In this case, the entire task will fall on such a js-code:
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.");
}
All that remains is to configure the interpreter and pass js to it. I’ll warn you right away: the engine painfully perceives the Cyrillic alphabet.
Conclusion
In conclusion, I will say that the idea of programming business logic on js is quite interesting, although not new. This approach provides some flexibility and ease of implementation.
A programmer who is faced with the task of adding a new method may not have to think about which technology stack is used in the server side, but simply prescribes what needs to be done and easily expand and supplement the functionality.
Following the opportunities offered by this approach, there are a number of questions that must be taken care of before embedding it in a combat server:
- Script security: does it have recursions and infinite loops, does it load memory too much,
- Validation of the script: is it written correctly, does it interact correctly with the server API,
- Server security: which modules the script has access to.
Fortunately, all issues are completely solvable, the Rhino source code is open for modification.
useful links
Of. project website: https://developer.mozilla.org/en/docs/Rhino
Wikipedia article: http://en.wikipedia.org/wiki/Rhino
Of. documentation: https://developer.mozilla.org/en-US/docs/Rhino_documentation
API Reference (not of.): http://tool.oschina.net/uploads/apidocs/rhino/
GitHub example sources: https: //github.com/andrew-medvedev/rhino-example