Довольно давно я опубликовал на Хабре статью, где рассказал про свой проект, TeaVM. С тех пор много всего произошло с ним, в том числе одна важная вещь, про которую речь пойдёт ниже и ради которой я решил снова написать на Хабр. Но для начала кратко напомню, про что проект.


Итак, TeaVM — это компилятор байт-кода Java в JavaScript. Идея создания TeaVM пришла мне, пока я работал full-stack Java разработчиком и использовал для написания фронтэнда GWT. В те времена (а это где-то лет 5 назад) не были широко распространены инструменты вроде node.js, webpack, babel, TypeScript; Angular был в первой версии, а альтернатив вроде React и vue.js не было вообще. Тогда ещё на полном серьёзе люди тестировали сайты в IE7 (а некоторые, кому не повезло с заказчиками, даже IE6). В целом, экосистема JavaScript была гораздо менее зрелой, чем сейчас, и без боли писать на JavaScript было нельзя.


GWT мне нравился тем, что на фоне всего этого он казался адекватным решением, хотя и не лишённым своих недостатков. Основные проблемы перечислены под катом:


  • Скорость пересборки оставляла желать лучшего. GWT ну очень медленный. Запустили компиляцию и пошли пить чай с печеньками.
  • GWT на вход получает исходники на Java и поэтому, во-первых, медленно работает (парсить и резолвить Java-код — это задача непростая), во-вторых, не поддерживает ничего, кроме Java, в-третьих, он сильно отстаёт даже в поддержке новых версий самой Java.
  • В GWT просто ужасный фреймворк для создания UI. С одной стороны, он пытается абстрагироваться от DOM, с другой стороны, не всегда идёт в этом до конца, абстракции протекают, а поправить это прямым вмешательством в DOM непросто, т.к. приходится прорываться через толстые слои абстракции. Кроме того, пока весь цивилизованный мир шёл по пути декларативного описания разметки (WPF в .NET, JavaFX, Flex, упомянутые выше современные фреймворки для JavaScript), GWT по старинке заставлял собирать UI из виджетов.

Мне не казалось, что сама по себе идея написания веб-приложений на Java — плоха. Все недостатки GWT были от того, что Google, как мне кажется, просто не вложили достаточных ресурсов в его развитие. Я предпочитал мириться с недостатками GWT, лишь бы не переходить на JavaScript.


И тогда я подумал — а почему бы не использовать в качестве входного материала байт-код Java? Байт-код сохраняет много информации об исходной программе, настолько много, что декомпиляторы умудряются потом почти точь-в-точь восстановить исходники. Javaс делает всю самую сложную работу по генерации байт-кода, поэтому на генерацию JavaScript должно потратиться совсем немного времени. Плюсом получаем поддержку других языков для JVM и почти бесплатную поддержку новых версий Java (байт-код гораздо консервативнее, чем язык Java).


Что интересного произошло с проектом


Я уже говорил, что с проектом произошло много всего интересного. Вот самые важные моменты, о которых хотелось бы рассказать:


  • Появилась поддержка тредов (threads). В JavaScript их нет совсем, WebWorkers — это не про то, поскольку, в отличие от тредов, они не имеют разделяемого состояния, т.е. нельзя создать объект в одном воркере и передать его другому, не скопировав. Вместо этого TeaVM умеет преобразовывать код методов так, что их выполнение можно прервать в некоторых заданных точках (примерно аналогично тому, как это делает babel, когда видит await). Это позволило сделать некоторый вариант кооперативной многозадчности, когда один тред, добравшись до какой-нибудь долгой IO-операции (ну или просто какого-нибудь Thread.sleep()), приостанавливается и позволяет выполниться другому треду.
  • Появилась поддержка WebAssembly, пока экспериментальная. Поддерживается произвольный байт-код, но не поддерживается часть методов JDK, которые сильно завязаны на JVM (это прежде всего различный reflection). Так же совсем отсутствует интероп с чем-либо; чтобы вызвать JavaScript, надо очень долго поплясать с бубном. Вообще, ради поддержки WebAssembly мне пришлось написать свой GC и stack unwinding, настолько этот WebAssembly низкоуровневый! Со временем обещают добавить поддержку GC и исключений, но пока это — настоящий ассемблер.
  • Появился свой фреймворк для написания веб-фротнэнда на TeaVM, и это та самая новость, из-за которой я написал данную статью. Впрочем, подробности — ниже.

Веб-фреймворк


Веб-фреймворк для TeaVM называется Flavour и он целиком написан на Java. Недавно я опубликовал первую версию (за номером 0.1.0) на Maven Central. Идеологически он напоминает современные Angular 2/4 и Vue.js, но построен целиком на идиомах, близких Java-разработчику. Например, все компоненты Flavour представлены обычными Java-классами, размеченными аннотациями, нет никаких отдельных props и state, или каких-то специальных объектов, которые должны инкапсулировать изменяющиеся свойства. Язык HTML-шаблонов полностью статически типизирован, любое обращение к свойству объекта или вызов обработчика события проверяются во время компиляции и, например, опечатки в названии свойств просто не дадут скомпилировать проект.


Для коммуникации с сервером Flavour предлагает использовать интерфейсы, размеченные аннотациями JAX-RS, а данные передаются с помощью DTO, которые в свою очередь размечаются аннотациями Jackson. Это должно быть удобно разработчикам на Java, которые, весьма вероятно, уже знают и используют эти API в своих проектах.


Возникает закономерный вопрос: а зачем создавать фреймворк, если существуют имеющиеся: React, Angular, Vue.js? Можно же просто воспользоваться JavaScript интеропом и ничего не изобретать. Конечно же, я думал об этом. Но нет, всё оказывается намного хуже, чем кажется на первый взгляд. Эти фреймворки построены вокруг идиом динамически типизированного JavaScript, а в нём и объект с нужным набором свойств можно создать из воздуха, и понадеяться, что у "класса" объекта есть магический метод с нужным названием. Вообще, в мире JavaScript, создатели фреймворков не привыкли думать про типизацию. Преодолеть это можно написанием всяческих адаптеров, обёрток, препроцессоров, генераторов. Но в итоге получится система посложнее исходных фреймворков, поэтому было решено написать свой.


Немного примеров


Создание проекта


Конечно, все интересующиеся могут почитать документацию на сайте. Но чтобы было проще и быстрее ощутить вкус Flavour, покажу небольшой пример здесь.


Итак, создать проект можно с помощью Maven:


  mvn archetype:generate     -DarchetypeGroupId=org.teavm.flavour     -DarchetypeArtifactId=teavm-flavour-application     -DarchetypeVersion=0.1.0

Собирается сгенерированный проект, как и ожидается, командой mvn package.


Шаблонизатор


Страничка описывается двумя файлами — кодом класса, который описывает её поведение (предоставляет данные для отображения, содержит обработчики событий) и HTML-шаблоном. В созданном проекте уже имеется пример странички, но я всё-таки приведу ещё один пример. Вы можете заменить сгенерированный из архетипа пример или просто добавить ещё два файла к имеющимся. Вот так выглядит код класса странички:


@BindTemplate("templates/fibonacci.html")
public class Fibonacci {
    private List<Integer> values = new ArrayList<>();

    public Fibonacci() {
        values.add(0);
        values.add(1);
    }

    public List<Integer> getValues() {
        return values;
    }

    public void next() {
        values.add(values.get(values.size() - 2) + values.get(values.size() - 1));
    }
}

А вот так — её шаблон:


<ul>
  <!-- values - это сокращение для this.getValues() -->
  <std:foreach var="fib" in="values">
    <li>
      <html:text value="fib"/>
    </li>
  </std:foreach>
  <li>
    <!-- на самом деле, event:click может просто выполнить какой-то кусочек кода,
         но для удобства рекомендуется обработчик события умещать в метод,
         а из шаблона просто вызывать его -->
    <button type="button" event:click="next()">Show next</button>
  </li>
</ul>

std:foreach, html:text и event:click — это компоненты Flavour. Пользователь может описывать свои компоненты (интересующиеся, как именно, могут почитать об этом в документации), при этом они могут либо вручную отрисовывать свой DOM, либо делать это через шаблон. В этих компонентах нет ничего особенного, они не реализуются компиляторной магией. При желании вы можете написать свои аналоги. Для иллюстрации можете ознакомиться с кодом html:text.


Наконец, вот как должен выглядеть код метода main:


public static void main(String[] args) {
    Templates.bind(new Fibonacci(), "application-content");
}

Вся основная магия стартует именно здесь. Фреймворк не создаёт экземпляр класса страницы, и никак не управляет им. Вместо этого вы создаёте его сами и сами же им управляете как хотите, а Flavour просто генерирует DOM, вставляет его в нужное место, отслеживает изменения состояния объекта и перерисовывает DOM согласно этим изменениям. Кстати, Flavour не перерисовывает весь DOM, а меняет только необходимую его часть.


Хочу ещё раз отметить, что шаблолны статически типизированы. Если ошибиться и написать event:click="nxt()", то компилятор напишет сообщение об ошибке. Такой подход позволяет ещё и генерировать более быстрый код — Flavour не тратит время после загрузки страницы, чтобы распарсить директивы и проинициализировать байндинги; он всё это делает во время компиляции.


REST-клиент


Теперь мне хотелось бы показать, как Flavour может быть полезен fullstack-разработчику. Допустим, вы используете какой-нибудь CXF в связке с JAX-RS. Вы написали примерно такой интерфейс:


@Path("math")
public interface MathService {
    @GET
    @Path("integers/sum")
    int sum(@QueryParam("a") int a, @QueryParam("b") int b);
}

и реализовали его (например, в классе MathServiceImpl), зарегистрировали реализацию в CXF. У вас готов небольшой REST-сервис. Теперь, чтобы сделать на него запрос, со стороны клиента можно написать такой код:


MathService math = RESTClient.factory(MathService.class).createResource("api");
System.out.println(math.sum(2, 3));

(можно увидеть в devtools, что этот код пошлёт GET-запрос на адрес /api/math/integers/sum?a=2&b=3).


В общем, не надо каким-то образом объяснять веб-клиенту, как правильно делать REST-запрос на нужный endpoint. Вы уже это сделали единым образом для сервера и для клиента. Можно дальше наращивать и рефакторить REST-сервис, при этом не надо синхронизировать это со стороны сервера и со стороны клиента — есть точка синхронизации в виде интерфейса MathService.


В GWT есть аналогичный механизм, GWT-RPC, но он заставляет генерировать дополнительный async-интерфейс и использовать колбэки, тогда как TeaVM умеет преобразовывать синхронный код в асинхронный. И GWT-RPC использует свой, ни с чем не совместимый протокол, так что создав endpoint для GWT-RPC, вы не сможете его переиспользовать, например, для iOS-клиента.


Что ещё интересного есть?


Разумеется, в небольшой обзорной статье я не могу рассказать обо всём вообще. Поэтому просто упомяну, что такого интересного есть в TeaVM и Flavour, что делает их вполне пригодными для создания качественных веб-приложений.


  • Интеграция с IntelliJ IDEA. Запускать каждый раз Maven — это несерьёзно, всё-таки Maven используют для сборки на production, а не во время разработки. TeaVM умеет запускаться прямо из IDEA, достаточно просто нажать кнопку Build.
  • Отладчик. Без него невозможно вообще говорить о чём-то, как о серьёзном инструменте. TeaVM умеет генерить стандартные source maps; так же есть свой формат отладочной информации, который используется в IDEA-плагине.
  • Очень быстрый компилятор. Я потратил усилия на оптимизацию, да и байт-код — это настолько примитивная вещь, что при его обработке просто нечему тормозить. Это не Java, где надо, например, типы выводить, с generics, вариантностью и captured-типами. Кроме того, при запуске компилятора из IDEA, применяется несколько техник для уменьшения времени компиляции, такие как запуск компилятора из демона и кэширование информации для последующих сборок. Всё это позволяет добиться вполне комфортной скорости пересборки JavaScript.
  • Хороший оптимизатор, способный отрезать от внушительного по размеру JDK очень небольшой кусочек, необходимый для нормальной работы приложения, да ещё и сделать его быстрым.
  • Роутинг. Flavour умеет парсить и генерировать ссылки, которые с точки зрения API — это вполне себе статически типизированные интерфейсы.
  • Валидация форм.
  • Модальные окошки с блокирующим API, как в Swing.
  • Это уже упоминалось выше — поддерживаются Kotlin и Scala, а не только Java. Между прочим, вариант TodoMVC для TeaVM написан на Kotlin.
  • Открытая лицензия Apache 2.0, привычная в мире Java-разработки.

Зачем Java в вебе?


В последнее время экосистема разработки JavaScript стала вполне зрелой. Разработчику проще не использовать всяких тяжеловесных монстров вроде GWT, а научиться настраивать инструменты, ставшие стандартами де-факто, и писать на современном языке с большим количеством продвинутых фич или даже на современном статически типизированном языке (TypeScript).


Проблема в том, что всё это надо изучать. Изучать не просто синтаксис языка (на это опытный разработчик потратит несколько дней), изучать много всего — библиотеки, идиомы, инструменты разработчика. Уверен, что вчерашний опытный Java-разработчик возьмёт и разберётся со всем этим за пару недель, но вопрос в том, насколько хорошо он успеет разобраться? Сможет ли он писать действительно хороший код? Даже если разберётся и сможет, есть ещё проблемы. Во-первых, разработчику придётся переключать контекст между Java и JavaScript. Во-вторых, так разработчику придётся потратить больше времени на настройку инструментов.


Ну и у меня есть вопрос к Java-коммьюнити. Почему-то же есть со стороны JavaScript такое настойчивое движение в сторону бэкэнда? Подозреваю, что ровно из тех соображений, которые я привёл выше. Почему бы не быть такому же движению в обратном направлении? Вот сообщество JavaScript frontend разработчиков скооперировалось и породило backend экосистему вокруг node.js. Чем мы, сообщество разработчиков на Java, хуже? Есть такой миф, что якобы JavaScript шустрый и легковесный, а Java — большая и тяжеловесная, и не подходит для создания фронтэнда. На самом деле, своим проектом я пытаюсь доказать, что это именно что миф, и что правильно приготовленная Java тоже может быть маленькой и шустрой. Тому пример — реализация TodoMVC, которая занимает 125kb (попробуйте написать TodoMVC на React или Angular 4 и посмотрите, какой здоровенный у вас получится бандл).


В заключение


Если вы заинтересовались Flavour, вот вам ещё немного материала для изучения:



Мне очень хотелось бы получить обратную связь. Интересен ли вам мой проект, хотели бы вы его попробовать для написания небольшого приложения? Нужно ли мне ещё публиковать статьи, и если да, о чём именно вы хотели бы в нём прочитать? Я могу публиковать туториалы по Flavour, могу рассказывать, как он устроен внутри, как работает TeaVM. Что из этого вам интереснее?

Комментарии (24)


  1. slavap
    13.10.2017 06:02

    > В GWT просто ужасный фреймворк для создания UI.
    Это уже давно не так. Например Errai или VueGWT позволяют нормально использовать Html для UI со статической проверкой во время компиляции. Да и стандартный UiBinder вполне себе декларативный, и ему можно подсунуть любые нестандартные widgets, например Bootstrap или jQueryMobile. И городить свой UI фреймворк имеет смысл только с прицелом на android разработчиков по-моему. Взял xml из android проекта, чуток поправил, получил рабочую web страницу.


  1. justboris
    13.10.2017 11:28

    Очень интересный проект, желаю ему успешного продолжения!


    В Kotlin же есть нативная поддержка компиляции в Javascript. Получается, что я могу скомпилировать Kotlin-фронтенд двумя разными способами. Уже пробовали их сравнивать?


    1. konsoletyper Автор
      13.10.2017 11:46

      В Kotlin же есть нативная поддержка компиляции в Javascript. Получается, что я могу скомпилировать Kotlin-фронтенд двумя разными способами.

      Да, всё так


      Уже пробовали их сравнивать?

      В каком смысле сравнивать? По производительности? По размеру генерируемого кода? Полагаю, можно написать синтетические бенчмарки, показывающие преимущества того или другого в специфическом сценарии. Что касается концептуального: в Kotlin/JS нет возможности использовать библиотеки, написанные на Java, в TeaVM немного сложнее интеропиться с JS (потому что нет специальной поддержки со стороны языка в Java и Kotlin/JVM, приходится придумывать костыли и обкладываться аннотациями). А так, если интересно, можно потратить некоторое время на исследование и составить подробный список различий, преимуществ и недостатков каждого подхода.


      1. justboris
        13.10.2017 11:47

        Да, хотелось узнать подробнее об отличиях двух разных подходов, понять плюсы и минусы каждого из них.


        Из вашего ответа это становится понятно, спасибо!


  1. vintage
    13.10.2017 16:07

    1. konsoletyper Автор
      13.10.2017 16:15

      Спасибо. Вообще, я знаю в чём дело. Надо просто немного улучшить реализацию std:foreach, потому что сейчас она полностью перестраивает DOM, если изменяется начало списка. Улучшение тривиальное, но руки не доходили сделать. Попробую сегодня поправить, собрать TodoMVC на снапшоте и повторить замер.


    1. konsoletyper Автор
      13.10.2017 16:31

      Кстати, мне пришло в голову, что этот бенчмарк измеряет не просто фреймворк, на котором писали TodoMVC, но ещё и конкретную реализацию. Реализацию, прямо скажем, я даже не пытался оптимизировать, написав максимально простой код, ведь это же пример. Уверен, можно сделать и лучше.


      1. vintage
        13.10.2017 18:01
        -1

        Ну, по хорошему реализация должна быть максимально нативная для фреймворка, без заточенных специально под бенчмарк оптимизаций, типа "сохраняем в localStorage лишь через секунду".


        Присылайте пул-реквест сюда: https://github.com/eigenmethod/todomvc


        1. konsoletyper Автор
          13.10.2017 18:25

          Ну, по хорошему реализация должна быть максимально нативная для фреймворка, без заточенных специально под бенчмарк оптимизаций, типа "сохраняем в localStorage лишь через секунду".

          Я не об этом. Например, в моей наивной реализации TodoMVC фильтр (это тот, который All|Active|Completed) применяется при каждом чтении свойства, причём не в ленивый sequence, а в новый ArrayList. Можно сделать sequence, можно немного умнее перестраивать список и т.п. Это никак не относится к фреймворку.


          1. vintage
            13.10.2017 18:52

            Ну, некоторые фреймворки умеют в реактивное программирование, где пресчёт происходит лишь при изменении данных, от которых зависит результат, а не при каждом обращении.


            1. konsoletyper Автор
              13.10.2017 18:55

              Flavour — это не про данные, а про их отображение. По идее, для пересчёта данных можно придумать/переиспользовать что-то другое, это никак не отразится на том, как шаблоны выглядят.


              1. vintage
                13.10.2017 19:17

                Обновление массива от обновления дома ни чем принципиально не отличается. В обоих случаях при изменении одного состояния нужно изменить другое за минимальное число шагов, не делая при этом лишней работы.


                1. konsoletyper Автор
                  13.10.2017 19:25
                  +1

                  В статье я ясно написал:


                  Фреймворк не создаёт экземпляр класса страницы, и никак не управляет им. Вместо этого вы создаёте его сами и сами же им управляете как хотите

                  Делается это, чтобы не навязывать пользователю определённую модель данных. А ещё для того, чтобы соблюсти SRP. Но я поиграюсь на выходных, попробую зацепить какую-нибудь RxJava или поищу другие фреймворки для реактивных данных. Суть в том, что пользователь может обновлять данные так, как ему хочется. Хочется руками — пусть обновляет руками, хочется реактива — будет реактив. А если перфоманс не принципиален в конкретном случае, так пусть вообще оставит наивную но простую реализацию.


                  1. vintage
                    13.10.2017 19:35

                    Скорее всего пользователь сделает как проще и всё буде тормозить. Насколько я понял Flavour никакой не фреймворк, а просто библиотека для рендеринга.


                    1. konsoletyper Автор
                      13.10.2017 19:48

                      Скорее всего пользователь сделает как проще и всё буде тормозить

                      Premature optimization is the root of all evil © Donald Knuth


                      Обычно, тормозит 10% программного кода, остальные 90% в принципе работают с такими объёмами данных, что тормозить нечему. Зачем в этих 90% кода городить лишние абстракции?


                      Насколько я понял Flavour никакой не фреймворк, а просто библиотека для рендеринга.

                      В какой-то степени так. А ещё для роутинга, сериализации, валидации и REST. Не люблю фреймворки, потому что они принуждают пользователей к определённой структуре организации приложения, при этом шаг влево, шаг вправо — расстрел. Но если назвать свой проект "библиотека" или "toolkit", люди не поймут.


                      1. vintage
                        13.10.2017 21:03

                        https://habrahabr.ru/post/126818/


                        Не нужно городить лишних абстракций. Фреймворк может брать на себя оптимизации, позволяя программисту писать простой и понятный код.


                        1. konsoletyper Автор
                          13.10.2017 21:34
                          +1

                          Фреймворк может брать на себя оптимизации

                          Заставляя при этом подчиняться ему. И, как я уже писал выше, шаг влево, шаг вправо — расстрел. Вместо этого на себя оптимизации могут брать библиотеки. Одна библиотека (RxJava) оптимизирует пересчёт данных, другая (Flavour) оптимизирует перерисовку DOM.


                          1. vintage
                            14.10.2017 03:40

                            В этом собственно и вся суть отличия фреймворка от библиотеки, что качество результата не зависит от того, воспользовался ли разработчик правильной библиотекой — выбор библиотеки уже был сделан за него и она была грамотно сопряжена с остальными библиотеками.


                            1. babylon
                              14.10.2017 14:32

                              Суть фреймоворка в рамках — от слова frame. Из рамок вылезти значительно труднее и ещё труднее в них влезть. Для технологических решений с осознанными ограничениями рамки удобнее.


        1. konsoletyper Автор
          15.10.2017 01:45

          Заоптимизировал, прислал пулл-реквест. Сам код TodoMVC не менял, просто собрал последний Flavour локально и заменил в pom.xml версию на 0.1.0-SNAPSHOT. А ещё выставил уровень оптимизации FULL. Стало значительно лучше, причём и при добавлении


  1. sshikov
    14.10.2017 11:00

    Как раз на днях видел описание проекта по запуску Graphhopper в браузере.


    Если вы его еще не упомянули — то стоит это сделать.


    1. konsoletyper Автор
      15.10.2017 13:22

      Это очень древняя история, я давно уже этим не занимаюсь (как и автор Graphhopper). Не стал от греха подальше упоминать о подобных вещах. Но вот в комментах напишу: ещё вполне актуально использование TeaVM в CodenameOne: https://www.codenameone.com/demos.html Почти на каждой демке есть опция JS port. Это они с помощью TeaVM скомпилили.


      1. sshikov
        15.10.2017 15:55

        Ну да, это TeaVM, там насколько я понял, UI кажется вообще нет (да и не нужен). Не знаю, на мой взгляд такая попытка затащить в браузер полноценный роутер заслуживает упоминания. Хотя бы чтобы помнить, почему он оказался неудачен.


        Удачные попытки пока редки, про одну совсем недавно пост был, но там была пусть и отлично решенная, но сильно упрощенная в сравнении с GH задача.


        1. konsoletyper Автор
          15.10.2017 16:42

          Ну да, это TeaVM, там насколько я понял, UI кажется вообще нет

          Есть там UI: http://teavm.org/live-examples/graphhopper/


          Хотя бы чтобы помнить, почему он оказался неудачен.

          Технических проблем не было вообще. Были организационные и коммуникационные проблемы.