В статье FreeMarker
Spring boot
Macros
REST API
Т.е. простое выражение на freemarker это например ${name}, в выражения поддерживаются вычисления, операции сравнения, условия, циклы, списки, встроенные функции, макрос и много др. Пример html с выражением ${name} (шаблон test.ftl):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>${name}!</title>
</head>
<body>
<h2>Hello ${name}!</h2>
</body>
</html>
Если теперь создать в java модель данных:
import freemarker.template.Configuration;
import freemarker.template.Template;
...
// Конфигурация
Configuration cfg = new Configuration(Configuration.VERSION_2_3_27);
// модель данных
Map<String, Object> root = new HashMap<>();
root.put("name", "Freemarker");
// шаблон
Template temp = cfg.getTemplate("test.ftl");
// обработка шаблона и модели данных
Writer out = new OutputStreamWriter(System.out);
// вывод в консоль
temp.process(root, out);
то получим html документ с заполненным name.
Если надо обработать список, то используется конструкция #list, например для html списка:
<ul>
<#list father as item>
<li>${item}</li>
</#list>
</ul>
В java, в модель данных подать список можно так
Map<String, Object> root = new HashMap<>();
....
root.put("father", Arrays.asList("Alexander", "Petrov", 47));
Перейдем к Spring
В Spring boot есть поддержка Freemarker. На сайте SPRING INITIALIZR можно получить pom файл проекта.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>demoFreeMarker</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demoFreeMarker</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@SpringBootApplication
public class DemoFreeMarkerApplication {
public static void main(String[] args) {
SpringApplication.run(DemoFreeMarkerApplication.class, args);
}
}
В Spring есть уже подготовленный компонент конфигурации Configuration для freemarker. Для примера консольного приложения возьму spring интерфейс для обработки командной строки(CommandLineRunner) и подготовлю модель данных для следующего шаблона ftl (hello_test.ftl):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello ${name}!</title>
</head>
<body>
<input type="text" placeholder="${name}">
<table>
<#list persons as row>
<tr>
<#list row as field>
<td>${field}</td>
</#list>
</tr>
</#list>
</table>
</body>
</html>
Java код для модели данных шаблона hello_test.ftl:
@Component
public class CommandLine implements CommandLineRunner {
@Autowired
private Configuration configuration;
public void run(String... args) {
Map<String, Object> root = new HashMap<>();
// для ${name}
root.put("name", "Fremarker");
// для <#list persons
List<List> persons = new ArrayList<>();
persons.add(Arrays.asList("Alexander", "Petrov", 47));
persons.add(Arrays.asList("Slava", "Petrov", 13));
root.put("persons", persons);
try {
Template template = configuration.getTemplate("hello_test.ftl");
Writer out = new OutputStreamWriter(System.out);
try {
template.process(root, out);
} catch (TemplateException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
После обработки получим html документ:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello Fremarker!</title>
</head>
<body>
<input type="text" placeholder="Fremarker">
<table>
<tr>
<td>Alexander</td>
<td>Petrov</td>
<td>47</td>
</tr>
<tr>
<td>Slava</td>
<td>Petrov</td>
<td>13</td>
</tr>
</table>
</body>
Макросы
В freemarker есть поддержка макросов, это очень удобная и сильная его сторона и использовать ее просто необходимо.
Простой пример:
<#macro textInput id value="">
<input type="text" id="${id}" value="${value}">
</#macro>
Это макрос с именем textInput и параметрами id (он обязательный) и value (он не обязательный, т.к. имеет значение по умолчанию). Далее идет его тело и использование входных параметров. В шаблоне файл с макросами подключается так:
<#import "ui.ftl" as ui/>
Из шаблона макрос вызывается так:
<@ui.textInput id="name" value="${name}"/>
Где ui это алиас который указали при подключении, ${name} переменная в модели, далее через алиас ссылаемся на имя макроса textInput и указываем его параметры, как минимум обязательные. Подготовлю простые макросы для html Input и Table:
<#-- textInput macro for html input -->
<#macro textInput id placeholder="" value="">
<input type="text" id="${id}" placeholder="${placeholder}" value="${value}">
</#macro>
<#-- table macro for html table -->
<#macro table id rows>
<table id="${id}">
<#list rows as row>
<tr>
<td>${row?index + 1}</td>
<#list row as field>
<td>${field}</td>
</#list>
</tr>
</#list>
</table>
</#macro>
${row?index + 1} это встроенная поддержка индекса элемента списка, подобных встроенных функций много. Если теперь изменить предыдущий основной шаблон и заменить в нем input и table на макросы, то получится такой документ:
<#import "ui.ftl" as ui/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello ${name}!</title>
</head>
<body>
<@ui.textInput id="name" placeholder="Enter name" value="${name}"/>
<@ui.table id="table1" rows=persons/>
</body>
</html>
REST
Конечно такую модель удобно использовать в web приложении. Подключаю зависимость в pom:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Добавляю REST Controller:
@Controller
public class DemoController {
@Autowired
private RepositoryService repositoryService;
@GetMapping("/")
public String index() {
return "persons";
}
@RequestMapping(value = "/search", method = RequestMethod.POST)
public String hello(Model model, @RequestParam(defaultValue = "") String searchName) {
List<List<String>> persons = repositoryService.getRepository();
List<List<String>> filterList = persons.stream()
.filter(p -> p.get(0).contains(searchName))
.collect(Collectors.toList());
model.addAttribute("persons", filterList);
model.addAttribute("lastSearch", searchName);
return "persons";
}
@RequestMapping(value = "/save", method = RequestMethod.POST)
public String save(Model model, @ModelAttribute("person") Person person) {
List<List<String>> persons = repositoryService.addPerson(person);
model.addAttribute("persons", persons);
return "persons";
}
}
Service репозиторий для лиц:
@Service
public class RepositoryService {
private static List<List<String>> repository = new ArrayList<>();
public List<List<String>> getRepository() {
return repository;
}
public List<List<String>> addPerson(Person person) {
repository.add(Arrays.asList(person.getFirstName(), person.getAge().toString()));
return repository;
}
}
Класс лицо:
public class Person {
public Person(String firstName, Integer age) {
this.firstName = firstName;
this.age = age;
}
private String firstName;
private Integer age;
public String getFirstName() {
return firstName;
}
public Integer getAge() {
return age;
}
}
Шаблон макросов:
<#macro formInput id name label type="text" value="">
<label for="${id}">${label}</label>
<input type="${type}" id="${id}" name="${name}" value="${value}">
</#macro>
<#macro table id rows>
<table id="${id}" border="1px" cellspacing="2" border="1" cellpadding="5">
<#list rows as row>
<tr>
<td>${row?index + 1}</td>
<#list row as field>
<td>${field}</td>
</#list>
</tr>
</#list>
</table>
</#macro>
Основной шаблон:
<#import "ui.ftl" as ui/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Person</title>
<link href="style/my.css" rel="stylesheet">
</head>
<body>
<div>
<fieldset>
<legend>Добавить лицо</legend>
<form name="person" action="save" method="POST">
<@ui.formInput id="t1" name="firstName" label="Имя"/> <br/>
<@ui.formInput id="t2" name="age" label="Возраст"/> <br/>
<input type="submit" value="Save" />
</form>
</fieldset>
</div>
<div>
<fieldset>
<legend>Поиск</legend>
<form name="searchForm" action="search" method="POST">
<@ui.formInput id="t3" name="searchName" label="Поиск"/> <br/>
<input type="submit" value="Search" />
</form>
</fieldset>
</div>
<p><#if lastSearch??>Поиск для: ${lastSearch}<#else></#if></p>
<@ui.table id="table1" rows=persons![]/>
</body>
</html>
Структура проекта:
Приложение будет обрабатывать две команды «save» и «search» лица (см. контроллер). Всю работу по обработке (мапингу) входных параметров, берет на себя Spring.
Некоторые пояснения к шаблону.
<#if lastSearch??>Поиск для: ${lastSearch}<#else></#if>
здесь проверяется, если параметр задан, то вывести фразу «Поиск для: ..», иначе ничего:
<@ui.table id="table1" rows=persons![]/>
здесь тоже сделана проверка, что список лиц присутствует, иначе пустой. Эти проверки важны при первом открытии страницы, иначе пришлось бы их инициализировать в index(), контроллера.
Работа приложения
Материалы:
> Apache FreeMarker Manual
Комментарии (7)
dplsoft
26.08.2018 14:54есть сравнение с Apache Velocity ?
собственно мы сейчас им (velocity) и пользуемся, и для проснения функциональности — пара вопросов, если можно:
* можно ли в free maker отправлять на рендеринг шаблоны, созданные динамически в рантайме? например в jsf такой фичи нет — все шаблоны компиляются во время сборки. а с velocity я так могу.
* если в значение строки подставить html — оно отрендерится как html или будет экранированно?
* есть возможность условного рендеринга? т.е. если значение переменной такое — то рендерим этот кусок, если нет то другой? мы часто этим пользуемся, перекладывая часть логики построения на плечи шаблонизатора.
* есть возможность реализовать внутри шаблона цикл? или list — это единственная «расширенная фича»?
* есть ли возможность обращаться к bean-свойствам объекта который погружен в контекст? т.е. я в хешмапе определяю одну переменную, а потом пользуюсь всеми его методами и свойствами, без необходимости в хешмапе определять 20+ значений.
Спасибо.evkin
27.08.2018 12:00По поводу динамических шаблонов — вообще не проблема. У нас шаблоны хранятся в базе (+кеш) и чудненько в процессе работы перегенерируются. Причем используются именно html, т.е. с экранированием тоже проблем нет. Соответственно вообще можно построить логику, что будет по нужным звездам разные шаблоны подаваться, делая ветвление в java логике или же if-ами.
С бинами проблем тоже нет. Через точку можно обращаться к свойства любой глубины вложенности. Там есть реализация бин обработчика (BeansWrapperBuilder), Map-у мы вообще не используем
arylkov Автор
27.08.2018 06:33— Есть возможность определить свой загрузчик шаблонов TemplateLoader, источник шаблоно может быть разный.
— Если в значение переменной поставить html, он так и выведется
— Условный рендеринг
<#if condition>
…
<#elseif condition2>
…
<#elseif condition3>
— В основном list, внутри уже можно if, break, continue
— Можно,
Map root = new HashMap();
Product latest = new Product();
latest.setUrl(«products/greenmouse.html»);
latest.setName(«green mouse»);
root.put(«latestProduct», latest);
Ftl
a href="${latestProduct.url}">${latestProduct.name}
token
Выглядит так будто эта чудесная технология была придумана ещё шумерами. А реакт к этому можно приделать для server side рендеринга?
j_wayne
> еще шумерами
Так и есть. Initial Release — в 2000 (https://en.wikipedia.org/wiki/Apache_FreeMarker).
Также, насколько я помню, разработчики старались не иметь сторонних библиотек в зависимостях (что большой плюс при энтерпрайзной некрокодоархеологии).
k12th
Можно, а как же!
Пишем шаблон (это весь шаблон):
${reactApp}
Запускаем рендеринг реакт-приложения в отдельном потоке, собираем его stdout
Кладем собранный stdout в HashMap как в примерах и рендерим шаблон.
Профит!
token
Класс! Спасибо, пойду попробую.