Pull to refresh

Впечатления от работы с Play! Framework 2.1 + Java

Reading time13 min
Views62K
Шла четвёртая неделя тяжёлых боёв с Play! Framework 2.1 + Java. Победа неумолимо приближалась, но до полной капитуляции было далеко.
После обнадёживающих новостей про развитие Play! 2.1, например в LinkedIn, было решено попробовать его в одном новом проекте. Испытать его, так сказать, в деле. Что из этого получилось? Я бы сказал, что это была небольшая война между мной и Play! 2.1. Почему? Подробности под катом, а для нетерпеливых:

Краткий вывод

Для штурма надо было брать секретное оружие под кодовым названием Scala. Если встать лицом к лицу с Play! Framework 2.1 и крикнуть со всей силы: «Ты есть Scala-фреймворк!», то он испугается такой прямоты и скромно откроет свои двери в мир больших возможностей.
«Не знаете Scala?» — «Используйте Play 1.2».
«Хорошо разбираетесь в Scala?» — «Обязательно попробуйте Play 2.1. Но всё равно запаситесь терпением».


Предыстория


За 7 лет с мышью и клавиатурой мечом и щитом я побывал в разных государствах на планете Java. Это были и древние развалины Энтерпрайзности, где ещё теплится жизнь, и цветущая Спрингляндия, и его вассал Грувиляндия со столицей в Грэилз-Сити, и когда-то передовое и экзотическое Плейтерре Первое, и много-много других государств, городов и даже глухих деревень. Но поход на Плейтерре II оказался неожиданно тяжёлым.
Я около 9 месяцев работал над одним проектом, используя Play 1.2.x, и был в полном восторге. Даже после Spring Framework 3 это казалось верхом скорости и простоты разработки, не говоря про монструозный Java EE 5 + JSF.
Потом был небольшой перерыв, в который я успел 3 месяца интенсивно поработать с Grails 2.х и понять, насколько Java отстала от Groovy в плане удобства и возможностей синтаксиса. И что Grails в какой-то степени проще и быстрее в разработке, чем чистый Spring Framework 3 + Java.
Однако настало время выбирать фреймворк для нового проекта, и выбор пал на Play 2.1. Было ли это правильно решение? Интересное точно, а вот правильное — это ещё покажет время.
Пока же я хочу поделиться своими впечатлениями от работы с Play 2.1. Будет много сравнений с Play 1.2.x, стенаний по поводу Java и эмоций. Кода почти не будет, и общепринятые термины и понятия не будут переведены на русский язык. Например, stateless framework.

Впечатления


Недостаток документации

Карты местности, которые предоставили правители Плейтерре II, оказались далеко неполными. Главные дороги обозначены вполне точно, но вокруг этих дорог непролазная чаща. Чтобы не плутать, приходилось хватать местных жителей и задавать им немало вопросов. Отвечали они, кстати, весьма охотно.
Официальная документация достаточно скупа. Есть две книжки Play 2 with Java и Play 2 with Scala. Первая, про Java, во многом повторяет официальную документацию с комментариями для новичков. Вторая, про Scala, чуть более полезна и затрагивает нужные темы, например Chapter 8. Building a single-page JavaScript application with JSON.
Конечно, есть большое и активное сообщество, попросить о помощи и задать вопрос можно и в Google Groups, и на Stackoverflow. Но удовольствие от разработки несколько теряется, если постоянно приходится искать ответы на казалось бы простые вопросы.
Я даже поучаствовал в улучшении документации (вот), но есть ещё много мест, которые хочется осветить более подробно.
Для Play 1.2.x документация была полнее и полезнее. Искать на Stackoverflow и в Google Groups приходилось только действительно необычные вещи.

Скорость компиляции

Дороги тут далеки от идеала. Иногда приходилось часами ехать, утопая в грязи и умоляя лошадь не сдаваться и скакать быстрее. Переброска большой армии занимает очень много времени. На дорогах везде КПП и проверки.
Для многих это не будет новостью, но компиляция тут работает медленно. Намного медленнее, чем в Play 1.2.x. Я понимаю, что тут компилируется почти всё, от простых Java-классов до Scala temlates и coffee scripts. Но это долго. Пусть выдаются вменяемые сообщения об ошибках, но это долго. Молниеносный в идеале Hit refresh workflow пробуксовывает и раздражает.

Работа с базой данных

Метные жители не умеют самостоятельно достраивать крепости. Им нельзя сказать: «Возведи ещё башню, да побыстрее!». Нет. Им нужен подробный план.
Есть встроенный механизм Database Evolutions. Но вот с генерацией этих скриптов проблема. Ebean ещё может создать полный скрипт, а с инкрементарными уже проблемы. Aналога jpa.ddl=update для Ebean я, к сожалению, не нашёл. Если кто-то подскажет, то буду безмерно рад. Аналог или адаптация LiquiBase's Diff была бы тоже очень кстати.

Нанятые фуражиры оказались странными мужиками. Вроде работают, но постоянно ждёшь перебоев с поставками.
Ebean как концепция весьма хорош. Главные преимущества:
  • отсутствие сессий (Session, EntityManager и т.п.), что хорошо вписывается в stateless framework. Однако как таковой Persitent Context есть.
  • поддержка Partial Objects, позволяющая писать более производительные запросы.

Спорные моменты:
  • Частичная поддержка JPA. Надо точно знать и понимать, где эта поддержка заканчивается.
  • Недостаток документации и примеров использования.
  • Не такая активная разработка и не такое большое сообщество, как у Hibernate.

Транзакции тут тоже есть. Можно даже метод контроллера пометить как @Transactional. Но только метод контроллера или сам контроллер. Для сторонних классов надо уже самому заботиться об этом.
В общем, Ebean — библиотека очень интересная, местами даже проще и понятнее, чем Hibernate или JPA, но я не могу избавиться от сомнений, что возможностей её хватит, чтобы удовлетворить все потребности растущего проекта.
Конечно, можно использовать стандартный JPA, но так не хватает Model.find("byName", name) из Play 1.2.x или интеграции со Spring Data JPA1. Возникает чувство, что возвращаешься на несколько лет в прошлое.

Кстати, у Ebean есть несколько существенных недостатков: подробнее о них можно прочитать в разделе Caveats на странице Using the Ebean ORM. И то, что Play 2.1 перестаёт генерировать getter/setter, если в классе есть хотя бы один такой рукописный метод, заставляет избегать названий методов, начинающихся c get или set. Для геттеров я теперь просто опускаю слово get и называю методы byName или byEmail.

А вот такая невероятная вещь, можно даже сказать killer feature в Ebean, как PagingList в stateless framework неприменима. Увы и ах.

Что касается работы с NoSql баз данных, то без труда можно найти плагины для Elasticsearch, MongoDB (даже несколько штук) и OrientDB. Сам не пользовался, поэтому ничего про качество этих плагинов сказать не могу.

Тестирование

Проверка состояния армии постоянно срывается вражескими шпионами. Организации учений мешает местная живность: обезьяны кидаются кокосовыми орехами, плющи оплетают ноги, а иногда встречаются скунсы-камикадзе.
Оно есть, возможностей много: от простых unit-тестов моделей до тестирования контроллеров и использования Selenium.
В своих тестах я пытался использовать in-memory database так, как это описано в документации:
@Before
public void setUp() throws Exception {
    start(fakeApplication(inMemoryDatabase("")));
}

Однако у меня сразу же возникли две проблемы, большая и маленькая.

Маленькая: как быть с транзакциями в тестах? Ничего подобного @Transactional из Spring или Grails я нашёл. Остаётся только самому открывать и откатывать транзакции в тестах. Неудобно и незрело.

Большая: конфликт между тем, что основная база у меня MySql, а для тестов in-memory H2. Так вот, H2 отказывается понимать evoluation scripts для MySql, а использование не default конфигурации БД, например так:
start(fakeApplication(inMemoryDatabase("login-test")));

влечёт за собой неопределённость того, куда всё же сохранятся данные, если использовать метод save() без указания конфигурации.
Я видел несколько неясных описаний решения этой проблемы, но простотой там и не пахнет.
Официальная документация поводу того, как организовать работу БД в тестах, ничего не говорит.

Из хорошего. Есть полезные команды ~test или ~test-only, которые будут срабатывать каждый раз, кода вы меняет исходный код. Позволяет сэкономить время, необходимое для запуска тестов.

Security

В государственных кузницах есть только самые простые доспехи. Народные умельцы куют намного более качественные вещи.
Возможности по авторизации и аутентификации остались на уровне Play 1.2.x. Есть Action Composition с аннотацией @With, есть Security.Authenticator и хороший пример как им пользоваться: страница Adding authentication, раздел Implementing authenticators.
Сторонние плагины предлагают более широкие возможности. Например, Deadbolt или SecureSocial, но мне они показались через чур накрученными для небольшого проекта, и я прекрасно обошёлся своей рукописной авторизацией.
Кстати, это удивительно, но Play 2.0 лишился такой полезной возможности как Authenticity Token, но есть сторонний плагин, который восполняет утраченные способности. Однако, в Play 2.1 добавили некие filters, которые упомянуты в пресс-релизе версии 2.1, но про которые очень мало документации. Среди фильтров упомянут как раз CSRFFilter. Единственное, что удалось найти про него — это вот этот пост, который описывает как работать с фильтром для защиты от CSRF в Scala. Как применить и применимо ли это вообще к Play 2.1 + Java надо ещё разобраться.

Нестабильность

Иногда на пути попадались миражи. При приближении они исчезали. На картах миражей отмечено не было.
Несколько раз вполне работающий код переставал работать! Видимо, не всегда корректно работает компиляция и генерация getters/setters. Помогали play clean и перезапуск проекта.
Самый странный момент, на который я наткнулся, был в том, что валидный для JDK 7 код типа:

catch (IndexOutOfBoundsException | ArrayStoreException e)

приводил Play 2.1 в полный ступор: ни ошибок, ни сообщений, просто сервер зависает на этих строчках и дальше ничего не происходит. Задал вопрос здесь, жду ответа.

Шаблоны

Тут попадаются такие болота, что могут засосать с головой. А могут и не засосать.
Надо сказать прямо: для эффективного использования шаблонов надо хорошо знать Scala или хотя бы что-то в ней понимать. Иначе всё это превращается в непонятное хитросплетение кода. И надо быть очень осторожным, потому что так работает:
@main(title = "Home") {    
  <h1>Home page</h1>    
}

а так уже будет ошибка из-за { на новой строке:
@main(title = "Home") 
{    
  <h1>Home page</h1>    
}

Или другой пример:
//так работает
@if(user == null)
//а так нет, потому что стоит пробел:
@if (user == null)

Когда я первый раз прочитал про Scala templates в Play 2.0, то был очень рад, потому что это напоминало очень удобный Razor из .Net. Действительно похоже, и, на мой взгляд, удобнее многих template engine в мире Java. Однако не забывайте, что первой строчкой вы должны объявить все входные параметры. И объявить переменную fullName в коде можно только так:
@defining(user.getFirstName() + " " + user.getLastName()) { fullName =>
  <div>Hello @fullName</div>
}

И что надо делать импорт для некоторых вещей: @import helper._. И ещё множество «если» и «но», в рамках которых чувствуешь себя не очень свободно.
Главный плюс — Scala templates компилируются и проверяются на этапе компиляции, что уменьшает количество неотловленных ошибок и обидных опечаток.
В общем, хороши или плохи Scala template в Play 2.1 — вопрос спорный, но он отступает на второй план, потому что Javascript MVC фреймворки набирают популярность, и актуальным становится следующее:

Работа с JSON

Тут отважный воин вообще растроился и ушёл пить в кабак.
На мой взгляд, Play 2.1 немного проигрывает Play 1.2.x. Главная причина — вместо Gson используется Jackson как основная библиотека для работы с JSON в Java. Мне понравилось работать с GSON в Play 1.2.x, это было удобно и гибко. С Jackson надо работать совсем по-другому. Видимо, я ещё не достиг того уровня понимания Jackson, чтобы получать от него наслаждение. А то, что имена полей в строке c JSON должны быть эскапированы двойными кавычками, вызвали у меня тяжкий вздох:
return ok(Json.parse("{\"status\": \"success\"}"));

Читаемость почти никакая. Особенно, если сравнивать с лёгкостью работы с JSON в Grails:
render([result: 'fail'] as JSON)

Конечно, не всё так плохо, но самое лучшее, что можно сделать на Java, это что-то типа2:
ObjectNode result = Json.newObject();
ArrayNode arrayNode = result.putArray("photos");
for (Photo photo : photos) {
    ObjectNode photoAsJson = arrayNode.addObject();
    photoAsJson.put("id", photo.id);
    photoAsJson.put("guid", photo.guid);
}
return ok(arrayNode);

Вполне себе сносно.
Однако для Scala поддержку JSON в версии Play 2.1 значительно доработали:
Json.obj(
  "users" -> Json.arr(
    Json.obj(
      "name" -> "bob",
      "age" -> 31,
      "email" -> "bob@gmail.com"  	  
    ),
    Json.obj(
      "name" -> "kiki",
      "age" -> 25,
      "email" -> JsNull  	  
    )
  )
)

Но в Groovy это всё равно выглядит короче и проще. Остаётся надеяться, что в Scala всё-таки добавят нативную поддержку JSON, как это есть сейчас для XML.
Я же решил упростить себе жизнь, дописал два дополнительных класса со статическими методами: один — это Pair, аналог Tuple2 из Scala, другой — класс, который кладёт Pairs в Map. В итоге мне стало проще:
return ok(toJson(map(
                    pair("status", "fb-logged-in"),
                    pair("user", map(
                            pair("id", fbUser.id),
                            pair("facebookUserId", fbUser.facebookUser.userId),
                            pair("fullName", fbUser.fullName())
                    ))
            )));

Раз уж речь зашла о кортежах (Tuples), то в Play 2.1 поддержка функционального для Java скопирована из Play 1.2.x, библиотека play.libs.F. Однако упоминаний в официальной документации об это нет. Надо смотреть доки для Play 1.2.x.

И ещё один момент: поддержка XML тоже перекочевала из Play 1.2.x, и пользоваться play.libs.XPath для работы с XML очень удобно.

Работа с почтой

Почтовые голуби и вороны здесь все завозные. Достать их легко. И ходят слухи, что правители Плейтерре II сами всех птиц отстреляли.
Хочу сказать про почту несколько слов потому, что в Play 1.2.x была встроенная поддержка отправки почты, а в Play 2.1 нет. Есть официальный плагин для почты. Вот вот тут говорят, что сами создатели приняли решение не добавлять поддержку почты. Якобы это так легко и просто, что каждый может сделать сам. Множество вопросов и возмущений в Google Groups опровергает эту точку зрения: люди ждут от Play 2.1 не меньшей функциональности, чем была в Play 1.2.x.

Конфигурация

Если раньше договоры писали на свитках, то теперь на отдельных листочках.
Главное, что стоит отметить, — это то, что убрали framework ID. Теперь вместо одного большого конфигурационного файла, надо иметь несколько отдельных. Спорный момент, у каждого подхода есть свои плюсы и минусы. Если раньше для запуска production было:
play run --%production
то теперь это выглядит так:
play -Dconfig.resource=prod.conf start
И prod.conf содержит в себе все переопределения настроек без всяких префиксов.

Интеграция с IDE

Моя лучшая лошадь подвернула ногу на здешних дорогах!
Я вам не скажу за всю Одессу за все IDE, но моя любимая Intellij Idea имеет пока далёкую от идеала поддержку Play 2.x. Навигация по коду очень часто приводит в сгенерированные классы, которые смотреть и видеть не нужно, иногда бывают проблемы с редактированием scala templates, полная поддержка SBT будет только в 13 версии. Вот только то, что нашёл и сообщил я сам: youtrack.jetbrains.com/issues/SCL?q=reported+by%3A+Andrey.Volkov+
Удовольствия от написания кода мало. Я понимаю, что Play 2.x вышел не так уж давно и может быть не так ещё популярен, чтобы тратить большие ресурсы его поддержку в IDE. Просто знайте, что большой помощи от IDE ждать не стоит.
С Play 1.2.x в этом плане намного лучше.

Исчезнувшая простота

Если страна Плейтерре Первое была полна чудес и многое делалось по мановению волшебной палочки, то в Плейтерре II никакого волшебства, только лопата и мотыга, щит и меч.
Play 1.2.x был очень прост и удобен. Многое делалось как-то само собой, и это работало. Да, я слышал мнения, что в Play 1.2.x слишком многое происходило само, слишком многое генерировалось и было совершенно непохоже на привычный Java-мир, но это работало просто, легко и быстро. Вся сложность была скрыта от обычных пользователей, и только самым дотошным программистом приходилось капаться в замысловатом исходном коде.
В Play 2.1 же почти вся «магия» отдана на откуп Scala. И это уже не магия, это чётко определённая работа Scala. На которую программисту приходится натыкаться постоянно. И без знания хотя бы основ Scala разобраться очень тяжело.
Сравните ради интереса:
public static void index() {
    render()
}

и
public static Result login() {
    return ok(
        login.render()
    );
}

Первый кусочек кода проще, но надо знать и следовать некоторым договорённостям, а второй кусочек кода более многословен, но также надо кое-что знать и кое-чему следовать. Второй может быть более гибким, но не более простым. Первый лично мне нравится больше.
Кстати, как вы думаете, что такое login во втором примере? Это класс, сгенерированный из scala template. Которые появляется только после компиляции этого самого scala template. Т.е. IDE будет жаловаться, что такого класса нет или сигнатуры методов не совпадают, если вы что-то поменяли. После компиляции жалобы пропадут, но пользоваться подсказками IDE становится проблематично.
Плейтерре Первое было полно легкокрылых фей и добрых духов. В Плейтерре II есть только механические големы, в которых можно разглядеть каждую шестерёнку.

Неоспоримые преимущества


Несмотря на множество странностей, если не сказать недоработок, Play 2.1 обладает рядом неоспоримых преимуществ.

Работа с формами и валидация

Очень удобна. Полная поддержка JSR-303 (Bean Validation) плюс возможность дописать свой собственный validate-метод для специфичных проверок. Не буду переписывать примеры из официальной документации. Их можно прекрасно посмотреть здесь.
Для шаблонов есть встроенная генерация input полей, причём можно сразу генерировать разметку для Twitter Bootstrap. Жаль только, что для версии 1.4. Приходится писать кастомный генератор (раздел Writing your own field constructor).
Если бы не убрали Authenticity Token и генерировали поля для Twitter Bootstrap 2.3, то был бы практически идеал.

Akka и прочие асинхронные штучки

В здешних кабаках и тавернах очень быстрое обслуживание. И никогда не бывает очередей.
В плане асинхронности и поддержки многопоточности Play 2.1 на коне. Тут и нативная поддержка асинхронных HTTP-запросов и интеграция с Akka. Если бы только не упомянутая даже в официальной документации многословность Java при работе с функциями…
Просто сравните. Java:
public static Result index() {
  Promise<Integer> promiseOfInt = play.libs.Akka.future(
    new Callable<Integer>() {
      public Integer call() {
        return intensiveComputation();
      }
    }
  );
  return async(
    promiseOfInt.map(
      new Function<Integer,Result>() {
        public Result apply(Integer i) {
          return ok("Got result: " + i);
        } 
      }
    )
  );
}

Scala:
def index = Action {
  val futureInt = scala.concurrent.Future { intensiveComputation() }
  Async {
    futureInt.map(i => Ok("Got result: " + i))
  }
}

Ещё один повод перейти на Scala. Или на что-то другое, но более «функциональное», чем Java.
Однако конкуренты не дремлят: в Grails 2.3 будет весьма неплохая поддержка асинхронных вычислений (пост в официальном блоге). Тем более заниматься асинхронными вычислениями на Groovy удобнее, чем на Java: поддержка замыканий играет свою роль.

Масштабируемость и облака

Земля здешняя плодородна и может прокормить почти любое население. А по небу плывут и плывут облака самых разных форм и размеров.
Говорят, что благодаря асинхронной и stateless природе, Play 2.1 прекрасно масштабируется. Пока мой проект не дорос до таких потребностей, поэтому не могу подтвердить это на собственном опыте.
Да, и Play 2.х поддерживается множеством облачных хостингов. Причём поддержка нативная, т.е. не надо собирать war-файл и деплоить под Tomcat/Jetty.
Если всё же необходимо собрать war-файл, то сделать это можно только с помощью стороннего плагина.

Поддержка CoffeeScript и Less

Мелких бесов тут приструнили, и теперь они несут верную службу людям.
Нативная поддержка CoffeeScript и Less. Даже не знаю, что тут можно добавить или объяснять. Удобно. Так и надо. Даже минифицируется всё проще некуда: просто загружаешь .min.js вместо .js.
Проблем с этим пока не было.

Консоль

Таможня на въезде в Плейтерре II наняла новых работников. Хотя кому какая разница, их работы всё равно никто не видит. Пусть хорошо работают, на них тогда никто и внимания не обратит.
Консоль стала намного удобнее и функциональнее, чем была для Play 1.2.x. Но это больше заслуга SBT (Scala Build Tool), чем самого Play 2.1.
Из того, что мне пригодилось, хочу упомянуть команды, начинающиеся с ~, которые выполняются каждый раз, когда происходит изменение исходного кода. Пример с ~test я приводил выше.

Что в итоге?


Съездить в отпуск на Плейтерре II интересно. Вопрос же с миграцией туда на совсем пока открытый.
Для себя я сделал следующие выводы:
  • Чтобы использовать Play 2.1 по полной, надо хорошо знать Scala.
  • Надо быстренько написать веб-приложение на Java? Бери Play 1.2.x. Или Grails. В Groovy не так сложно разобраться, как в Scala.
  • Надо изучать Scala. Именно изучать, потому что отличия от Java и даже Groovy очень существенные.
  • Java почти безнадёжно отстала от других языков программирования. Возвращаться к написанию кода только на Java после того, как начал получать удовольствие от функционального программирования, очень тяжело. Может быть Java 8 спасёт ситуацию.




[1] — честно говоря, интегрировать Spring в Play 2.1 можно, это даже выглядит не очень сложно. Т.е. Spring Data JPA и другие проекты из Spring Data теоретически можно добавить, но конкретных примеров и статей по этому поводу я не нашёл. У самого руки тоже не дошли.
[2] — при подготовке статьи обнаружил, что для Java EE 7 должен быть реализован Java API for JSON Processing. Судя по этому посту в блоге Oracle, это будет удобнее и лучше, чем приведённый пример. Достаточно близко к поддержке JSON в Groovy. Только это в будущем.
Tags:
Hubs:
+33
Comments40

Articles