Хоть Node.js и обзавелся с момента своего появления множеством модулей, он все еще существенно уступает по возможностям мощному набору библиотек Java. Так отчего бы не воспользоваться потенциалом Java для разработки web-приложений на JavaScript? Давайте посмотрим, как можно построить удобный JavaScript MVC framework на Java.
Mozilla Rhino
Прежде всего, начнем с носорогов. Для компиляции/интерпретации JavaScript будем использовать движок Mozilla Rhino, обеспечивающий отличную интеграцию кода ECMAScript в Java-приложения. Начиная с J2SE 6 Rhino включается в JRE в составе Java Scripting API, однако версия в JRE значительно устаревшая и, кроме того, с некоторыми неприятными особенностями реализации от Sun, поэтому лучше воспользоваться свежим build-ом.
Прежде всего, helloworld.js
:
print('Hey you!');
Предполагая, что библиотеки Rhino распакованы в ./lib
, запускаем пример следующим образом:
java -Djava.ext.dirs=./lib org.mozilla.javascript.tools.shell.Main helloworld.js
Кстати, в комплекте идет и отладчик с неплохим UI, запускается он так:
java -Djava.ext.dirs=./lib org.mozilla.javascript.tools.debugger.Main helloworld.js
Rhino, помимо стандартных объектов ECMAScript, включает в глобальный контекст целый ряд функций, облегчающих связь JavaScript с Java. Да, если вы еще не поняли, из кода на JavaScript можно будет прозрачно работать с кодом на Java. Для работы с пакетами существует глобальный объект Packages
. Например, так можно создать экземпляр java.io.File
:
var file = new Packages.java.io.File('filename');
Впрочем, также существуют глобальные объекты java
, com
, org
, edu
и net
, поэтому код можно сократить до следующего:
var file = new java.io.File('filename');
Для импорта можно пользоваться таким pattern-ом:
var File = java.io.File;
//...
var file = new File('filename');
Но все же удобнее так:
importClass(java.io.File);
//...
var file = new File('filename');
Или так:
importPackage(java.io);
//...
var file = new File('filename');
Rhino позволяет реализовывать интерфейсы Java удобным для JS-программиста способом:
var runnable = new java.lang.Runnable({run: function() { print("I'm running!"); }});
new java.lang.Thread(runnable).start();
Кстати, обратите внимание на то, что java.lang
не импортируется в глобальный контекст во избежание конфликтов со встроенными типами ECMAScript.
А еще последние версии Rhino включают полноценную реализацию CommonJS, которую можно включить в Rhino Shell с помощью switch-а -require
.
Если у нас есть модуль ./modules/math.js
:
exports.sum = function(a, b) {
return a + b;
}
То воспользоваться им можно так:
var math = require('math');
print(math.sum(2, 4));
Запускается этот код так:
java -Djava.ext.dirs=./lib org.mozilla.javascript.tools.shell.Main -require -modules ./modules test.js
Jetty
В качестве основы для HTTP-сервера возьмем Jetty. Jetty — контейнер servlet-ов, а заодно и гибкий в настройке web-server с поддержкой SPDY, WebSocket, OSGi, JMX, JNDI и JAAS. Скачать дистрибутив можно тут.
Простейший код, запускающий Jetty:
importPackage(org.eclipse.jetty.server);
var server = new Server(8888); // Порт 8888
server.start();
server.join(); // Передадим управление Jetty
JAR-ы из дистрибутива Jetty следует также поместить в ./lib
, в дальнейшем все будем запускать так:
java -Djava.ext.dirs=./lib org.mozilla.javascript.tools.shell.Main -require -modules ./modules server.js
Да, это все.
Этот сервер выдает HTTP 404
при любом запросе. Превратим его в простой файловый сервер.
importPackage(org.eclipse.jetty.server);
importPackage(org.eclipse.jetty.server.handler);
var resourceHandler = new ResourceHandler();
resourceHandler.setDirectoriesListed(true); // Разрешим просмотр списка файлов в папках
resourceHandler.setResourceBase('web'); // Установим базовой директорию ./web
resourceHandler.setWelcomeFiles(['index.html']); // В качестве главной страницы будет использоваться index.html
var server = new Server(8888);
server.setHandler(resourceHandler);
server.start();
server.join();
Теперь попробуем создать servlet.
importPackage(org.eclipse.jetty.server);
importPackage(org.eclipse.jetty.server.handler);
importPackage(org.eclipse.jetty.servlet);
importPackage(javax.servlet.http);
var contextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
contextHandler.setContextPath('/');
contextHandler.addServlet(
new ServletHolder(new HttpServlet({ // Обратим еще раз внимание на то, как изящно реализуются интерфейсы
doGet: function(request, response) {
response.setContentType('text/plain');
response.getWriter().println('Yes, it works!');
}
})),
'/test'
);
var server = new Server(8888);
server.setHandler(contextHandler);
server.start();
server.join();
Наш servlet доступен на http://localhost:8888/test
. В качестве еще одного примера оформим в виде модуля servlet, генерирующий на лету картинку с текстом.
importPackage(java.awt.image);
importClass(java.awt.Color);
importClass(javax.imageio.ImageIO); // Входит в состав J2SE 6
var width = 400, height = 400;
exports.doGet = function(request, response) {
var image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
var graphics = image.createGraphics();
var color = new Color(Math.random(), Math.random(), Math.random());
graphics.setColor(color);
graphics.fillRect(0, 0, width, height);
graphics.setColor(color.brighter());
graphics.drawString('On the fly!', 10, 20);
response.setContentType('image/png');
var outputStream = response.getOutputStream();
ImageIO.write(image, 'png', outputStream);
outputStream.close();
};
Поместим его в папку ./modules
как imageServlet.js
и включим в код сервера:
contextHandler.addServlet(
new ServletHolder(new HttpServlet(require('imageServlet'))),
'/image.png'
);
Что там с СУБД? Посмотрим, как получить список баз данных из MySQL.
importPackage(java.sql);
exports.doGet = function(request, response) {
var resultSet = DriverManager.getConnection('jdbc:mysql://localhost/?', 'root', '')
.createStatement()
.executeQuery('show databases;');
response.setContentType('text/html;charset=UTF-8');
var writer = response.getWriter();
writer.println('<h1>Databases</h1>');
while (resultSet.next()) {
writer.println(resultSet.getString('Database') + '<br />');
}
};
Для этого кода понадобится MySQL Connector/J для JDBC.
Теперь остался последний компонент, шаблонизатор.
FreeMarker
FreeMarker — определенно лучший шаблонизатор для Java, причем не только для HTML и HTTP. Про его богатые возможности можно написать отдельную статью, так что сразу перейдем к конкретике.
Положим в ./templates/template.ftl
такой вот шаблон:
<html>
<head>
<title>${title}</title>
</head>
<body>
<h1>${title}</h1>
<#if message??>
<pre>${message?html}</pre>
<#else>
<form method="post">
<textarea name="message"></textarea>
<p><input type="submit" value="Post!"/></p>
</form>
</#if>
</body>
</html>
Суффикс ?html
заменяет в подставляемой переменной те самые спецсимволы на escape-последовательности. Этот шаблон будет использовать следующий servlet:
importPackage(Packages.freemarker.template);
importPackage(Packages.freemarker.ext.rhino);
var configuration = new Configuration();
configuration.setObjectWrapper(new RhinoWrapper()); // Поистине приятный сюрприз.
var template = configuration.getTemplate('templates/template.ftl');
exports.doGet = function(request, response) {
response.setContentType('text/html;charset=UTF-8');
template.process(
{'title': 'Compose a message'},
response.getWriter()
);
};
exports.doPost = function(request, response) {
response.setContentType('text/html;charset=UTF-8');
template.process(
{
'title': 'Message',
'message': request.getParameter('message')
},
response.getWriter()
);
};
Сравнение с Node.js я благородно сваливаю на читателя. Полный код примера доступен на GitHub.
Автор: dannote