Pull to refresh

Jython-консоль вашего приложения

Reading time 12 min
Views 17K
Расскажу вам как я использую интерактивную консоль Jython для ускорения разработки Bean'ов в поддерживаемом мной приложении.

Суть вопроса


Каждый кто хоть раз сталкивался с долго разрабатывающимися 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__(selflocals):<br/>
        """Принимаем контекст выполнения консоли"""<br/>
        BaseInteractiveConsole.__init__(selflocals)<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 resys<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.
  1. Добавьте репозиторий в свой pom.xml:
    <repository>
      <id>Rjyc Repository</id>
      <url>http://siasia.github.com/maven2</url>
    </repository>
  2. Добавьте зависимость от rjyc:
    <dependency>
      <groupId>org.python</groupId>
      <artifactId>rjyc</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>
  3. Импортируйте сервер в своём коде:
    import net.rjyc.Server;
  4. Запустите его как описано выше.
  5. Скачайте клиент http://github.com/siasia/rjyc/raw/master/client.py
  6. python client.py [host] [port]
  7. PROFIT!!!

Надеюсь эта статья сделает ещё кого-то немного счастливее.
Форкайте меня на github http://github.com/siasia.
Tags:
Hubs:
+24
Comments 16
Comments Comments 16

Articles