Шаблоны на java с помощью Velocity. Часть 1
Разработка веб-приложений за последние несколько лет сделала качественный скачок. Если ранее веб-сайт считался набором html-страничек, пусть и генерируемых динамически, то сейчас сайт становится похожим по функциональности на традиционные desktop-приложения. Все чаще встречаются страницы, использующие сложные элементы управления (таблицы, деревья), в которые динамически подгружаются данные с сервера (ajax). А появление огромнейшего количества framework-ов, просто библиотек и утилит делают процесс создания такого сайта все более простым и понижают “пороговый уровень” вхождения в технологию. С другой стороны, существуют достаточное количество сайтов для которых все эти новомодные, интерактивные “фишки” просто не нужны или вредны, а все что нужно это “просто хороший” движок шаблонов. У многих на слуху такие термины как MVC или Model-View-Controller (а кое-кто даже его использует). Для современного процесса разработки приложений (и сайты не исключение) характерно совмещение конвейерного подхода с параллельной работой нескольких человек/подразделений. Т.е. мы стараемся избегать специалистов “все-в-одном-флаконе”, предпочитаем пусть несколько человек, но каждый из них настоящий специалист в какой-то области. Параллельная работа дизайнеров и программистов возможна только в случае, если согласованы правила по которым дизайн (верстка) и программная часть (логика отбора данных) будут совмещены. Одним из наилучших способов является генерация информационного наполнения сайта в виде xml-документов (то, что делают программисты). Верстальщики же параллельно создают xsl-правила, по которым выполняется трансформация xml-документа в html-документ. С широким распространением ajax-сайтов, flash¬-сайтов, сайтов для мобильных устройств такой подход становится все более привлекательным. Действительно, для какого устройства мы бы не генерировали сайт, в каком бы формате не должны были возвращены данные клиенту, но логика отбора данных и упаковка их в xml-документ всегда останется неизменной – меняются только правила визуализации. Основным недостатком такого подхода считаются повышенные требования к уровню верстальщиков (ну-ну, за пару недель xsl можно гарантированно выучить). Остальные же недостатки (небольшая скорость работы, ограниченный набор встроенных функций xsl) уже давно решен. Сегодняшний материал, однако, не будет посвящен xml|xsl (ну разве что в самом конце и немножко), а расскажет о более привычном подходе к разделению логики сайта и его визуализации – шаблонах построенных по правилу “вставь вот в это место переменную”. Этот подход предполагает, что существует некоторый файл шаблона страницы (его сделали наши верстальщики), например:
А на стадии генерации итогового html-кода страницы, вместо слова “XXX_FIO” будет помещено реальное значение ФИО взятое из базы данных (а это сделали наши программисты). Шаблонных движков много, очень много (и для разных языков программирования). Хотя разработчики подобных движков начинают с лозунга “самый простой язык шаблонов, который поймет даже самый … верстальщик”, но спустя какое-то время в состав языка начинают вводить все больше и больше конструкций (сначала условия, потом циклы, затем процедуры, директивы, средства для работы с базой данных …). Заканчивается это тем, что через какое-то время синтаксис такого языка (изначально планировавшегося только для вставки в определенные места шаблона данных) превращается в некоторое подобие полноценного языка программирования. А если сдублированных средств не хватает, то разрешим до “полной кучи” вставлять в шаблон кусочки кода на основном языке программирования. А что в этом плохого? Да нет ничего плохого: я и сам понимаю, что бывают случаи когда “нужно, действительно нужно” в шаблоне и цикл, и сделать проверку сложного условия, такого что на “изначальном, очень понятном для верстальщиков” языке сделать не возможно. Беда в человеческом факторе. Хотя мы все знаем, что нужно делать сайт, четко разделяя логику и представление. Но когда появляется новое требование, а “над душой” стоит босс и кричит “это должно было быть сделано еще вчера”. То, скажите честно, кто позволит себе медленно и аккуратно переписать часть логики, создать новые классы, выделить интерфейсы и “подчистить” старый код, если можно “по быстренькому” вставить пару строчек в шаблон и все заработало? Нет, конечно, мы знаем, что через несколько таких итераций код шаблона превратится в настоящее “болото” с кучей вставок, дублирующихся, написанных на скорую руку. Но ведь это будет не сейчас, а позже. А самое плохое, что через какое-то время мы станем заложниками выбранного подхода “хаков” и по-другому добавить функциональность уже не сможем. Как тут не вспомнить бессмертное “так спешил работать, что даже топор наточить не успевал”. С другой стороны нельзя заниматься созданием массы вещей на будущее, чтобы, когда требования к сайту бац … и поменялись, то мы были во всеоружии. В современной высокотехнологичном мире (читай, когда заказчик и сам толком не знает чего хочет), закладываться с запасом на срок более пары недель опасно: есть большой риск, что сделанная вами на запас функциональность вообще не понадобится. Одним словом, нет “серебряных пуль”, а качество используемой библиотеки или framework-а больше зависит от наличия “царя в голове”. Заканчивая затянувшееся вступление, я перехожу к описанию velocity (сайт проекта
http://velocity.apache.org/).
Velocity – это шаблонный движок для java. Я часто сталкиваюсь с парадоксальным мнением, что java это сложная технология для сложных сайтов и использовать ее для разработки небольших “в десять страниц” проектов не целесообразно – гораздо быстрее и проще сделать это на php. С одной стороны это утверждение может быть оправдано тем, что для java много (иногда слишком много) различных framework-ов, которые можно применить при разработке веб-приложений (различаются не только в деталях, но в концепции). Это приводит к “рассеиванию” точки приложения силы (в отличие от подхода microsoft), а с другой стороны, больше вероятность того есть framework наилучшим образом подходящим для вашей специфики (если, конечно разработчики его доведут до ума, а вы о нем когда-нибудь услышите). Сегодня я рассказываю о Velocity – небольшом движке шаблонов, для его работы не нужно закачивать на хостинг 100 мегабайт не нужных библиотек. А ваш хостер не пришлет гневное письмо, что ваш проект потребляет столько ресурсов, что нужен выделенный физический сервер (вы ведь в курсе, что уже давно, даже самые дешевые хостинги предлагают поддержку java). Да и изучить Velocity можно буквально за пару дней. Сразу скажу что Veloctity – может быть использован для генерации не только веб-страниц, но и всего что угодно: отчеты, тексты писем, исходники программ и т.д. (фактически, первые примеры будут запускаться в обычном консольном приложении, читать файл шаблона с диска и печатать результирующий документ на экран).
Вам нужно на сайте
http://velocity.apache.org/ загрузить: velocity-1.5, anakia-1.0, texen-1.0, velocity-dvsl-1.0, velocity-tools-1.4. На первых порах вам потребуется только velocity-1.5, остальные же библиотеки либо расширяют возможности velocity, либо используют velocity для решения собственных целей. Распаковав архив с velocity-1.5 вы найдете два файла velocity-1.5.jar и velocity-dep-1.5.jar. Первый из них – собственно, сама библиотека шаблонификации, второй же файл содержит зависимости (другие библиотеки нужные для работы veloctity). В папке docs находится документация (неплохое подспорье, т.к. каких-то специализированных русскоязычных источников по velocity я не нашел). В папке examples находятся примеры программ использующих veloctity, anakia.
Подключив к вашей IDE указанные выше две библиотеки пора набрать первые строки:
Velocity.init();
VelocityContext vc = new VelocityContext();
vc.put("fio", "Вася Тапкин");
Template t = Velocity.getTemplate("velo.vm", "utf-8");
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
t.merge(vc, bw);
bw.flush();
bw.close();
Шаблон приложения прост: первый шаг – инициализация движка Velocity (за это отвечает метод init). Затем нужно создать VelocityContext, считайте, что это место внутри которого хранятся все данные нужные для работы velocity. В примере я положил в “корзинку” переменную “fio” и ее значение “Вася Тапкин” (в последующем я покажу, как работать с более сложными структурами данных: массивами, списками). Шаг три – загрузка шаблона, за это отвечает метод getTemplate, в качестве параметров которому передается имя файла шаблона (принято, что расширение такого файла “vm”), а также имя кодировки, в которой сохранен файл. Шаг четыре – создание “выходного потока” (переменная bw), т.е. куда будет помещен сформированный html-документ. Шаг пять – самое главное – метод merge принимает в качестве параметров набор данных (VelocityContext), также объект BufferedWriter, куда будет направлен сформированный документ.
Теперь нужно создать файл шаблона “velo.vm”. Для вставки помещенной в VelocityContext переменной fio, я записал ее имя с предшествующим символом “$”:
Результат работы приложения будет таким:
<h1>Привет, Вася Тапкин</h1>
Перед тем как я продолжу рассказ о командах, которые можно использовать внутри файла шаблона, нужно разобраться с очень важным вопросом. Откуда этот файл вообще взялся? То есть, откуда он взялся ясно: его написали мы. Но вот как Velocity нашло файл шаблона? Velocity по-умолчанию действует так: когда мы указываем имя файла шаблона (использовать полные пути нельзя), то файл ищется в текущем каталоге. Т.е. вы имеете в виду, что при создании веб-приложения мы должны разместить шаблон в служебных папках сервера, до которых доступа по определению нет (в случае если дело идет не у вас на компьютере, а на настоящем хостинге). Как вывод: нам нужен способ указать откуда будут загружаться шаблоны. Также нам нужно настроить правила загрузки шаблонов: будет ли использовано кэширование шаблонов, как часто мы будем проверять наличие обновленных шаблонов, как настроить журналирование ошибок возникающих в ходе работы Velocity и многое другое. Внутри архива velocity-1.5.jar находится конфигурационный файл с правилами работы Velocity по-умолчанию. Этот файл (velocity.properties) мы возьмем как основу для того, чтобы создать собственные настройки. Сохраните приведенный далее пример под именем “veloconf.properties”:
resource.loader = file
file.resource.loader.description = Velocity File Resource Loader
file.resource.loader.class = org.apache.velocity.runtime.resource.loader.FileResourceLoader
file.resource.loader.path = c:/templates
file.resource.loader.cache = true
file.resource.loader.modificationCheckInterval = 2
Загрузка ресурсов выполняется специализированным объектом “Resource Loader”. Есть несколько разновидностей Loader-ов оптимизированных для работы с различными источниками данных. В самом верху файла настроек я указал директиву resource.loader, значением которой являются “public” имена загрузчиков (названия я придумываю сам и слово file, в общем случае, ничего не значит). Загрузчиков может быть несколько, например, из одного каталога, затем второго, затем из архива… Сколько загрузчиков я хочу использовать, столько их “public” имен я должен указать через запятую как значение переменной resource.loader. Затем нужно для каждого из имен загрузчиков указать настройки (имя переменной должно предваряться “public” именем загрузчика). Первая опция – description – просто текст с комментарием к загрузчику. Вторая опция – class – содержит имя класса загрузчика (чуть позже я приведу перечень доступных загрузчиков). Опция path содержит путь к каталогу, в котором будет выполняться поиск файлов, возможно через запятую указать несколько путей. Опция cache управляет кэшированием шаблонов: если кэширование включено, то после первого обращения к шаблону в памяти сохраняется ссылка на “прочитанный и подготовленный к выполнению” шаблон, это повышает скорость обработки последующих запросов. Опция modificationCheckInterval хранит значение интервала времени (в секундах) через которые файл шаблона проверяется на модификацию и если нужно перечитывается с диска. Значение modificationCheckInterval равное нулю фактически отключает кэширование шаблонов. Рекомендации работы с КЭШем стандартные: на стадии разработки и тестирования сайта кэширование отключить, на стадии развертывания включить. А теперь я перечислю “Resource Loader”-ы:
1. FileResourceLoader - Загрузка файлов шаблонов выполняется из некоторого каталога (значение переменной path).
2. JarResourceLoader – В этом случае загрузка шаблонов идет из файла архива jar. Значением переменной path будет адрес архива, например, такой:
jar.resource.loader.path = jar:file:c:/docs.jar
3. ClasspathResourceLoader – один из удобнейших и вреднейших способов загрузить файлы шаблонов. В этом случае поиск файла *.vm выполняется внутри classpath. Так если вы поместить файл jar с шаблонами в папку lib веб-приложения или скопируете шаблоны в папку classes, то ресурсы будут найдены автоматически (правила поиска ресурсов vm совпадают с правилами поиска классов ClassLoader-ом).
4. URLResourceLoader – значением свойства path будет URL сайта, где размещены шаблоны.
5. DataSourceResourceLoader – загрузка шаблонов будет выполняться из базы данных (крайне редко возникает подобная необходимость).
Для того чтобы приведенный выше пример взял настройки из файла конфигурации нужно указать в первой строке при вызове метода init путь к файлу с настройками:
Velocity.init("conf/veloconf.properties");
Есть и другие способы настроить поведение Velocity, но в них я думаю, вы разберетесь самостоятельно. А пока мы переходим к описанию языка шаблонов Velocity. Сразу же, как вставить в файл шаблона комментарий:
## Однострочный комментарий
#* А вот многострочный
комментарий *#
А теперь разберемся с тем, какие типы данных могут быть поданы на вход Velocity (положены в VelocityContext). В Context можно положить все: строки, числа, объекты, массивы, списки. Если вы положили в контекст скалярную переменную, то для того чтобы из шаблона обратиться к переменной используйте синтаксис $имя-переменной. Возможно, что сразу за именем переменной идет другое слово (без разделяющего слова пробела или перехода на новую строку). В этом случае нужно поместить имя переменной внутрь фигурных скобок:
<h1>Привет, ${fio}Ура</h1>
Возможно, что мы забыли переменную fio положить в Context или ее значение равно null. В этом случае Velocity выведет имя переменной (т.е. текст “$fio”), а мы хотим, чтобы в подобных случаях выводилась “пустота” (ссылка на ошибочную переменную игнорировалась). В этом случае в шаблоне, перед именем переменной, нужно поставить знак восклицания:
Еще ситуация: нам нужно вывести в тексте шаблона некоторый текст начинающийся со знака “$”, но мы не хотим чтобы Velocity подставила вместо него значение какой-то переменной. Здесь пригодится экранирование спец. символов с помощью “\”, например, “\$fio”. В случаях, когда вы хотите вставить внутрь шаблона большой кусок текста, который не должен обрабатываться Velocity, то имеет смысл поместить этот текст внутрь специальной директивы “#literal() что-то-там #end”, например, так:
#literal()
#foreach ($fruit in ["apple", "orange"])
## комментарий
I like $fruit
#end
#end
При работе со сложными структурами данных есть особенности. В следующем примере я создам класс User содержащий поле fio:
public class User {
private String fio;
User(String fio) {
this.fio = fio;
}
}
Теперь поместим экземпляр данного класса в контекст:
vc.put("user", new User("Jim"));
И используем в шаблоне:
<h1>Привет, $user.fio</h1>
Результат будет таким:
<h1>Привет, $user.fio</h1>
Что-то не получилось: я хотел вывести значение поля fio для объекта user, а velocity не вывел ничего. Дело в том, что есть определенные правила поиска внутри объекта его свойств. Когда я пишу “user.fio”, то Velocity ищет внутри объекта user метод getter для поля fio. В моем примере такого метода нет, поэтому Velocity перешел ко второму этапу: ищется метод с названием get и получающий в качестве единственного параметра строку (имя переменной которую нужно найти внутри объекта).
public Object get (String name){
if (name.equals("fio")) return fio;
return "get::"+name;
}
Если такой метод найден, то он вызывается, а если нет, то Velocity ищет в составе объекта поле с именем fio. В приведенном мною примере нет методов get и getFio, а для поля fio установлен модификатор видимости private (если его поменять на public, то все заработает). Еще одно правило: если вы передаете внутрь velocity объекты, то удостоверьтесь, что их классы объявлены с модификатором public, в противном случае Velocity не сможет обращаться к их содержимому. Из шаблонов Velocity можно обращаться и к методам класса, например, если я добавлю в класс User такой метод:
public Object makeBar (String var){
return "variable is " + var;
}
То смогу вызывать метод makeBar, вот так:
Очевидно, что шаблон не может свестись только к вставке в заранее подготовленные места какой-то информации: нам нужны средства для простейших условных операций. Например, проверить сумму денег на счету человека и вывести ее на экран различными цветами (если зеленое, то денег много, красное – кончились):
// кладем сумму денег в контекст
vc.put ("money", 100000L);
А теперь проверяем количество денег:
#if ($money < 10)
<p style="red">$money</p>
#elseif ($money < 20)
<p style="blue">$money</p>
#else
<p style="green">$money</p>
#end
Внутри условия можно использовать логические операции (синтаксис привычен по java: “&&” означает “И”, “||” означает “ИЛИ”, а для отрицания условия используется “!”).
Что касается циклов и работы с коллекциями. В Velocity есть только один универсальный код цикла, который умеет перебирать и содержимое некоторого массива полученного из контекста и выполнять некоторые действия фиксированное количество раз. Например, следующий код выведет на экран десять абзацев:
#foreach ($i in [1..10])
<p>Paragraph # $i</p>
#end
Цикл может быть организован не только по диапазону значений “от и до”, но и по произвольному их перечислению (жаль только что совмещать диапазон и отдельные значения не возможно):
#foreach ($i in [1,2,"apple"])
<p>Paragraph # $i</p>
#end
Более интересный пример, когда границы изменения цикла поступят в шаблон извне:
// помещаем в контекст границы цикла
vc.put ("min", 10);
vc.put ("max", 20);
И используем их
#foreach ($i in [$min .. $max])
<p>Paragraph # $i</p>
#end
Разработчики Velocity приняли очень красивое решение при работе с вложенными циклами, когда используемые как счетчик цикла переменные никак не пересекаются (в следующем примере, в обоих циклах используется одна переменная $i):
#foreach ($i in [$min .. $max])
<h1>Paragraph # $i</h1>
#foreach ($i in [$min .. $max])
<p>SubParagraph # $i</p>
#end
#end
Теперь попробуем организовать цикл по массиву значений переданных внутрь шаблона из контекста:
// кладем в контекст массив
vc.put("users", new User[]{
new User ("Вася"),new User ("Петя"),new User ("Ленка"),
});
И используем его
#foreach ($user in $users)
<h1>fio # $velocityCount = $user.fio</h1>
#end
В этом примере еще перед выводом ФИО записи, вывожу еще и порядковый номер элемента в массиве (за это отвечает, служебная, автоматически создаваемая Velocity переменная $velocityCount). Velocity достаточно умен, чтобы суметь организовать цикл не только по обычному массиву, но и по любому объекту поддерживающему интерфейс Collection, например, ArrayList или Vector. Можно делать цикл и по HashMap-у, например, так:
// так наполняем контекст информацией
HashMap map = new HashMap();
map.put("fio", "Вася");
map.put("age", 100);
map.put("sex", "male");
vc.put ("map", map);
А так используем
#foreach ($key in $map.keySet())
<h1>$key = ${map.get($key)}</h1>
#end
Если вас интересуют не помещенные в HashMap пары “ключ=значение”, а только значения, то цикл можно сделать и так:
#foreach ($i in $map)
<h1>value $i</h1>
#end
Более менее сложные сайты не возможно создать только одним шаблоном: мы можем определить один главный шаблон (в котором определяется структура страницы). А затем в каждую из частей того шаблона (левая, правая колонка, шапка страницы) неплохо бы вставить содержимое сгенерированное другим шаблоном. Для подобных целей в состав Velocity входят две директивы: #include и #parse. Первая из них просто вставляет в шаблон содержимое некоторого текстового файла, а вот вторая директива (#parse) хитрее: она рассматривает вставляемый в некоторое место шаблона файл как написанный на Velocity и вычисляет все встреченные в нем директивы и переменные. Фактически, с помощью #parse мы можем создать цепочку из подключаемых файлов.
First File:
#include ("velo2.vm")
Second File:
#parse ("velo2.vm")
Сборка результирующего файла из кусочков может быть очень гибкой. Это достигается за счет того, что в качестве входного параметра директивам #parse и #include можно подать не только строку с именем файла, но и переменную с его именем.
// помещаем в контекст имя файла
vc.put ("file", "velo2.vm");
// а так мы подключаем сам файл в шаблон
First File:
#include ($file)
Second File:
#parse ($file)
Для отладки шаблонов пригодится механизм аварийного прерывания интерпретации скрипта (директива #end). Так следующий код не выведет значение второй переменной:
$firstValue
#stop ## аварийное прерывание
$secondValue
Последняя возможность, о которой я сегодня расскажу, – создание макросов (в терминологии Velocity они называются Velocimacro). Макросы в Velocity точь-в-точь похожи на макросы в c|c++ (если вы не знакомы с этим языком и понятием макросов в них, то считайте что Velocimacro – что-то очень похожее на процедуру). Если некоторые куски шаблона страницы будут дублироваться, то имеет смысл оформить их в виде Velocimacro, дать имя, определить параметры, поступающие на вход Velocimacro и, наконец, вызвать Velocimacro в нужном месте. Кода я директивой #macro объявляют Velocimacro, то первым параметром идет название этого макро, остальные же параметры (начинающиеся с символа “$” это входные переменные для макро – их количество не ограничено и может быть равным нулю).
#macro( alert $msg )
Error: <p style="color: red">$msg</p>
#end
Вызываем Velocimacro:
#alert ("Привет")
И еще раз вызываем Velocimacro:
#alert ("Пока")
Когда я вызываю Velocimacro, то на это место будет подставлен тот код, который находится внутри макроса. Момент в том, что macro – это не процедура, несмотря на очевидно сходство. И следующий пример это демонстрирует. Сначала я немного изменю код класса User, добавив еще одно поле history. Всякий раз, когда будет вызываться метод makeBar в объект “историю” будет помещаться значение, переданное makeBar как параметр.
ArrayList history = new ArrayList();
public Object makeBar (String var){
history.add(var);
return "variable is " + history;
}
Теперь я изменю код шаблона:
#macro( alert $msg )
Error: <p style="color: red">$msg</p>
Info: <p style="color: green">$msg</p>
Warning: <p style="color: yellow">$msg</p>
#end
Вызываем Velocimacro:
#alert (
$user.makeBar("apple")
)
Результат вывода будет несколько обескураживающим:
Error: <p style="color: red">variable is [apple]</p>
Info: <p style="color: green">variable is [apple, apple]</p>
Warning: <p style="color: yellow">variable is [apple, apple, apple]</p>
Как видите, вызов метода makeBar был выполнен трижды, по числу использования переменной $msg внутри макроса. А, следовательно, внутрь макроса передается не вычисленное значение переменной, а текст команды, которая будет вычисляться каждый при обращении к “как-бы-уже-не-совсем-переменной”. Если вы задумались об оптимизации и хотите, чтобы внутри макроса параметр не вычислялся бы каждый раз заново, то можно использовать фокус с промежуточной переменной, например, так:
#macro( alert $object )
#set ($msg = $object)
Error: <p style="color: red">$msg</p>
Info: <p style="color: green">$msg</p>
Warning: <p style="color: yellow">$msg</p>
#end
Директива set служит для создания переменной и присвоения ей некоторого значения.
Созданные вами Velocimacro можно использовать многократно, однако прием с созданием файла *.vm с перечнем макросов (затем подключаете такой файл-библиотеку с помощью #parse) не рекомендуется. Вместо этого лучше всего в конфигурационном файле velocity создать переменную “velocimacro.library”, значением которой и будет путь к файлу с макросами.
Вот и все: никаких других синтаксических конструкций в Velocity шаблонах нет. Разительный контраст по сравнению с рядом других шаблонных движков, в состав которых входит еще множество конструкций и для работы с файлами и с кэшированием и с базой данных (хотя свое негативное отношение к таким движкам “переросткам” я высказывал ранее). С другой стороны разработчики Velocity должны были предусмотреть способ расширения возможностей Velocity. И действительно т.к. внутрь VelocityContext можно получить любой объект java, обратиться к его свойствам и вызывать методы. А, следовательно, возможности Velocity безграничны.
Может быть Вам еще понравится статья
Velocity Фильтр