
Суть вопроса
Каждый кто хоть раз сталкивался с долго разрабатывающимися Java-приложениями, знает, что многие из них очень медленно собираются и стартуют. Не будем обсуждать почему так получается. Это тема отдельной статьи.
По долгу службы пришлось поддерживаю очень древнее приложение с громадной кодовой базой. Хуже всего то, что оно собирается оно от минуты до семи и ещё минуты три стартует. Опять же каждому программисту не сложно представить себе какой ад написать энное количество кода, а затем ловить NullPointerException'ы от внешних сервисов с таким длинным циклом Implement->Compile->Start Deploy->Wait->Smoke->Wait->Test.
Возможен также другой вариант. Есть энное количество кода в классе, который нужно адаптировать под выполнение задачи, близкой уже им выполняемой. А теперь представьте, что этот класс реализован в рамках Java 1.4. Он не работает с Generic'ами, потому что они были добавлены только в Java 1.5. Кроме того программисты, ранее занимавшиеся поддержкой системы, этим активно пользуются и суют в коллекции возвращаемые методами других классов, что не попадя вплоть до анонимных реализаций java.lang.Object.
Поев кактус пару дней я почувствовал, что начинаю сходить с ума от того, что пишу пять-двадцать строк работающего кода в день.
Способом ускорения разработки в таких варварских условиях я усмотрел только вкрутить в приложение интерпретатор какого-либо динамического языка для быстрого прототипирования уязвимого к таким условиям кода сначала на нём, а потом реализации алгоритма на Java. Конечно же в рамках решение нужна была ещё и интерактивная консоль этого языка. Какая же без этого динамика? Консоль хотелось в не в апплете из-за того, что апплеты очень плохо работают(скажем так, они просто не работают) во FreeBSD, к которой я очень уж привык в последнее время.
Поиск существующих решений
Как приверженец мнения, что изобретать «вило»-сипеды(потому как часто от них остаются только вилы) не тру, решил на выходных копать в сторону уже существующих решений этого вопроса. Очень хотелось Python. Но перед тем как вернутся к первому выбору, успел посмотреть в сторону:
- Beanshell — отброшен по причине убогости телнет-консоли, поставляемой из коробки вместе с интерпретатором. Апплет не подходит.
- JRuby — отброшен по причине не того, что я обнаружил, что успел благополучно забыть этот язык с тех пор, как игрался с Rails во времена первой вспышки его популярности. Быстрое гугление не дало результатов кроме каких-то Corba-монстров. Испугался и закрыл...
- Groovy
Что-то похожее на то, что хотел нашёл у Sakai. Реализовано в качестве Spring Bean'а, а у нас свой Dependecy Injection-фрэймворк с блэкджеком и шлюхами. Не попробовал.
Jython
Python-программистам хорошо известна одна из его концепций «batteries included», что означает много разных вкусных библиотек даже в стандартной поставке. Только у Java свои батарейки. Реализация Python для JVM просто эталонная, а вот библиотеки пока портированы не полностью(например setuptools начал устанавливаться в Jython-окружении совсем недавно), так что у меня не получилось завести родные Python-решения.
Попробовал RPyC с разными версиями Jython(ночь прошла незаметно). Почитал про Twisted Manhole. Решил не сливать исходники тестовой ветки интеграции с Jython, потому как была опасность начать фиксить Twisted для Jython и стать после этого холостяком.
Собрал github.com/EnigmaCurry/jython-shell-server и понял: нет readline, нет счастья. Telnet не поддерживает readline, если разработчик не реализовал эту функциональность на серверной стороне. Представьте, что написали строку длиной в символов в 80 и вспомнили, что первый объект называется по другому. Конечно можно торкать мышкой в консоль, но хотелось родной похожей на bash-среды.
Моя реализация
Сервер
За пять минут выбрал XMLRPC в качестве интерфейса сервера. За следующих пять минут не передумал. За следующих 20 минут реализовал.
Код сервера на Jython:
from SimpleXMLRPCServer import *<br/>
from os import path<br/>
from code import InteractiveConsole as BaseInteractiveConsole<br/>
<br/>
class Stdout(object):<br/>
"""Замена stdout для буферизации вывода в строку"""<br/>
def __init__(self):<br/>
self.buffer = ''<br/>
<br/>
def get_buffer(self):<br/>
"""Получаем накопленный буфер и сбрасываем его"""<br/>
bc = self.buffer<br/>
self.buffer = ''<br/>
return bc<br/>
<br/>
def write(self, bs):<br/>
"""Пишем в буфер вместо стандартного вывода"""<br/>
self.buffer += bs<br/>
return len(bs)<br/>
<br/>
class InteractiveConsole(BaseInteractiveConsole):<br/>
"""Интерактивная консоль, возращает вывод выполнения команды"""<br/>
<br/>
def __init__(self, locals):<br/>
"""Принимаем контекст выполнения консоли"""<br/>
BaseInteractiveConsole.__init__(self, locals)<br/>
#Заменяем стандартные потоки собственной реализацией<br/>
self.stdout = sys.stdout = sys.stderr = Stdout() <br/>
<br/>
def push(self, line):<br/>
result = BaseInteractiveConsole.push(self, line)<br/>
return (result, self.stdout.get_buffer()) #Возвращаем вывод вместе с результатом<br/>
<br/>
<br/>
<br/>
class Server(SimpleXMLRPCServer):<br/>
"""XMLRPC-сервер, поставляющий в сеть методы интерактивной консоли"""<br/>
<br/>
def __init__(self, ls, *args, **kwargs):<br/>
SimpleXMLRPCServer.__init__(self, *args, **kwargs)<br/>
self.register_introspection_functions()<br/>
#Регистрируем экземпляр консоли как обработчик с передачей контекста<br/>
self.register_instance(InteractiveConsole(ls))
Клиент
В качестве базового интерфейса был выбран Cmd, который из коробки поддерживает readline, что нам и нужно.
Код сервера на этот раз на Python(похоже Cmd в Jython не поддерживает readline):
from cmd import Cmd as BaseCmd<br/>
from code import InteractiveConsole as BaseInteractiveConsole<br/>
import re, sys<br/>
from xmlrpclib import ServerProxy<br/>
<br/>
class Cmd(BaseCmd):<br/>
"""Реализация прокси-консоли"""<br/>
reg = re.compile('^\s*')<br/>
def __init__(self, host, port): <br/>
BaseCmd.__init__(self)<br/>
self.s = ServerProxy('http://%s:%d' % (host, int(port))) #Клиент нашего сервиса<br/>
self.prompt = '>>> ' #Приглашение к вводу<br/>
self.leading_ws = '' #Переменная для ведущих пробелов<br/>
self.is_empty = False #Переменная определяющая пустую команду<br/>
<br/>
def precmd(self, line):<br/>
"""Тестируем различные условия с сырой строкой,<br/>
которая затем фильтруется"""<br/>
#Сохраняем ведущие пробелы, т.к. они фильтруется при передаче в default<br/>
self.leading_ws = self.reg.match(line).group(0) <br/>
#Пустая ли команда, т.к. пустая команда далее преобразуется в повторение предыдущей<br/>
self.is_empty = (line == '') <br/>
return line #Выполняем контракт, описанный в документации<br/>
<br/>
def default(self, line): <br/>
if(self.is_empty): #Восстанавливаем пустую строкy<br/>
line = ''<br/>
line = self.leading_ws + line #Восстанавливаем ведущие пробелы<br/>
(result, output) = self.s.push(line) #Выполняем строку в удалённой консоли<br/>
#В случае если требуется новый ввод устанавливаем соответствующее приглашение<br/>
self.prompt = ('... ' if result else '>>> ') <br/>
sys.stdout.write(output) #Пишем аутпут в аутпут :)<br/>
<br/>
if __name__ == '__main__':<br/>
HOST, PORT = sys.argv[1:]<br/>
Cmd(HOST, PORT).cmdloop()
Java-обёртка для сервера
Простой Bean для запуска нашего сервера. Проще просто некуда.
package net.rjyc;<br/>
<br/>
import org.python.util.PythonInterpreter;<br/>
import java.util.*;<br/>
<br/>
public class Server {<br/>
private PythonInterpreter i;<br/>
public PythonInterpreter getInterpreter() {<br/>
return i;<br/>
}<br/>
public Server(String host, int port) {<br/>
this(host, port, new HashMap<String, Object>());<br/>
}<br/>
public Server(String host, int port, Map<String, Object> locals) {<br/>
i = new PythonInterpreter();<br/>
//устанавливаем аргументы в экземпляр интерпретатора<br/>
i.set("host", host);<br/>
i.set("port", port);<br/>
i.set("ls", locals);<br/>
}<br/>
<br/>
public void start() {<br/>
//запускаем сервер интерактивной консоли<br/>
i.exec("from rjyc import Server; Server(dict(ls), (host, port), logRequests = False).serve_forever()");<br/>
}<br/>
}
Использование
Предствьте себе гипотетический сервлет, который выводит список ссылок из своего поля.
import javax.servlet.http.*;<br/>
import java.util.*;<br/>
import java.io.*;<br/>
<br/>
public class Hello extends HttpServlet {<br/>
public final Map<String, String> links = new HashMap<String, String>();<br/>
{<br/>
links.put("Python", "http://python.org"); <br/>
links.put("Java", "http://java.net");<br/>
}<br/>
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response)<br/>
throws IOException {<br/>
PrintWriter writer = response.getWriter();<br/>
for(Map.Entry<String, String> e: links.entrySet())<br/>
writer.println("<a href=\""+e.getValue()+"\">"+e.getKey()+"</a>");<br/>
writer.close();<br/>
}<br/>
}
Вот, что нам отвечает вебсервер:
siasia@siasia ~ % wget http://localhost:8080 -O - 2>/dev/null
<a href="http://python.org">Python</a>
<a href="http://java.net">Java</a>
Теперь внедрим в него нашу консоль.
import javax.servlet.http.*;<br/>
import java.util.*;<br/>
import java.io.*;<br/>
import net.rjyc.Server;<br/>
<br/>
public class Hello extends HttpServlet {<br/>
public final Map<String, String> links = new HashMap<String, String>();<br/>
{<br/>
links.put("Python", "http://python.org"); <br/>
links.put("Java", "http://java.net");<br/>
Thread t = new Thread() {<br/>
@Override public void run() {<br/>
Map<String, Object> locals = new HashMap<String, Object>();<br/>
locals.put("this", Hello.this);<br/>
new Server("localhost", 8081, locals).start();<br/>
}<br/>
};<br/>
t.start();<br/>
}<br/>
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response)<br/>
throws IOException {<br/>
PrintWriter writer = response.getWriter();<br/>
for(Map.Entry<String, String> e: links.entrySet())<br/>
writer.println("<a href=\""+e.getValue()+"\">"+e.getKey()+"</a>");<br/>
writer.close();<br/>
}<br/>
}
И подключимся к ней:
siasia@siasia ~ % python client.py localhost 8081
>>> this
examples.Hello@13ebc5c
>>> this.links
{Python=http://python.org, Java=http://java.net}
>>> this.links['Scala'] = 'http://scala-lang.org'
>>> this.links
{Scala=http://scala-lang.org, Python=http://python.org, Java=http://java.net}
Проверим результат:
siasia@siasia ~ % wget http://localhost:8080 -O - 2>/dev/null
<a href="http://scala-lang.org">Scala</a>
<a href="http://python.org">Python</a>
<a href="http://java.net">Java</a>
Maven
Подготовил Maven-артефакт, на случай если вы пользуетесь Maven.
- Добавьте репозиторий в свой pom.xml:
<repository> <id>Rjyc Repository</id> <url>http://siasia.github.com/maven2</url> </repository>
- Добавьте зависимость от rjyc:
<dependency> <groupId>org.python</groupId> <artifactId>rjyc</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
- Импортируйте сервер в своём коде:
import net.rjyc.Server;
- Запустите его как описано выше.
- Скачайте клиент http://github.com/siasia/rjyc/raw/master/client.py
python client.py [host] [port]
- PROFIT!!!
Надеюсь эта статья сделает ещё кого-то немного счастливее.
Форкайте меня на github http://github.com/siasia.