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

    Расскажу вам как я использую интерактивную консоль 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.

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 16

      +1
      Любопытный велосипед, хотя описано все довольно сумбурно. Сам писал похожее несколько раз, последний раз пользовал Janino (интерпретация) + JMX (транспорт) + jconsole (клиент). Но опять же Janino это типизированная Java, и даже не очень 1.6.

      Кстати, а насчет rhino (javascrip) вы не задумывались. По умолчанию в дефолтной java даже есть какой то не очень свежий интерпретатор. Да и javacript немного похож на Java Ж)
        +1
        Не очень люблю джаваскрипт за разгильдяский синтаксис. Да и как бы в Питоне из коробки много вкусного кода.
          0
          «Suum cuique»@кто-то-умный, а вообще javascript довольно удобен для скриптования, мы вот например в одном приложении всю клиентскую бизнес логику в него вынесли (возможно и серверная тоже частично там будет).
        +1
        Не очень понятно, чем предложенное решение лучше стандартного интерактивного дебаггера + HotSwap.
          +1
          Наверное тем, что дебаггер не позволит вызвать код так, как он ещё не используется в коде или написать скрипт, который дёргает разные классы в нужном порядке до того, как их хореография будет налажена или поиграться с объектом, по которому абсолютно нет документации наугад без редеплоя.
            +1
            Вообще, такое есть в дебагере Eclipse, правда интерфейс к таким возможностям довольно убог (или может я чего не понял).
          +4
          Проблема понятна — проекты разные бывают, но в большинстве случаев правильнее взять на вооружение TDD: написать несколько тестов для нового куска функционала — таких чтобы быстро бегали, замокать всё тяжёлое, а затем написать/отладить новый код гоняя только эти локальные тесты, а не пересобирая весь проект каждый раз.

          Ведь код, который вы скормите интерпретатору, пропадёт безвозвратно, почему бы не оформить его в виде тестов? — тогда он ещё сослужит службу в будущем.
            +1
            Если я правильно уловил суть (в питоне и джаве не силён), то вместо изобретения велосипеда могли бы взять готовые ABCL и SLIME. Язык для быстрого прототипирования подходит, нахаляву получаете отличный отладчик и, кроме консоли, IDE (emacs).

            Правда связку SLIME с ABCL сам не смотрел, но по отдельности они, очевидно, работают отлично.
              0
              Ну можно было попробовать clojure. Опыт есть. Не очень люблю Лисп опять же.
              0
              я, честно говоря, так и не понял: почему за основу своего велосипеда (или не очень велосипеда) нельзя было groovy-то взять? Или просто хотелось именно питон и никто кроме вас не будет этот велосипед использовать/поддерживать?
                0
                Если прочитать комментарии выше, то заметно, что каждый предлагает свой язык :) И каждый считает, что именно он единственно верный :)
                Были Javascript, Lisp, теперь вот Groovy. В Python огромная встроенная библиотека сверх того, что предлагает Java API. Mожет ли этим похвастаться Javascript, Lisp или Groovy?
                  –1
                  >В Python огромная встроенная библиотека сверх того, что предлагает Java API. Mожет ли этим похвастаться Javascript, Lisp или Groovy?

                  учитывая что ТС использует maven, то сторонних библиотек у него невероятно большое количество.
                  а groovy лучше в первую очередь тем, что он синтаксически значительно ближе к java, чем любой другой язык, а значит он значительно проще и понятнее для остальной java-команды.
                    0
                    С удовольствием почитаю про удалённую Groovy-консоль с автодополнением, которую можно встроить в не Spring.
                    –1
                    Да, все три могут. Но не в этом суть.

                    Вам нужна интерактивная разработка? Как вообще могут в голову придти какие-то Python'ы когда есть куча языков разработанных с оглядкой на это с самого начала? Интерактивная разработка Jython выглядит как неудачная шутка после ABCL.

                    Ах, да. Вы «не любите» лиспы. Ну, не сильно уступающие скобкам альтернативы вам уже предложили.
                  0
                  Очень интересный способ!
                    +1
                    Довольно интересное предложение конечно, но как было подмечено выше — довольно-таки сумбурно описано. Условия у Вас все-таки не варварские, для примера наше приложение собирается около 80 минут + 30 минут install/deploy + 10-30 минут конфигурирование. Для некоторых тестов уcтановку/настройку приходится делать на нескольких разных серверах. И я это не к тому, чтобы пожаловаться или померятся письками, а к тому, что работа в таких условиях жутко дисциплинирует, особенно к тому, чтобы хотя бы пробежаться по написанному коду глазами хотя бы разок. Хотя конечно NullPointer'ы реально бесят, но тут как вариант нас спасает инкрементальная компиляция в эклипсе)

                    Only users with full accounts can post comments. Log in, please.