FreeMarker шаблоны

    Apache FreeMarker — это механизм шаблонов: библиотека Java для генерации текстового вывода (HTML-страницы, xml, файлы конфигурации, исходный код и.т.д. На вход подается шаблон, например html в котором есть специальные выражения, подготавливаются данные соответствующие этим выражением, а Freemarker динамически вставляет эти данные и получается динамически заполненный документ.

    image

    В статье 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 файл проекта.

    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>
    


    Класс DemoFreeMarkerApplication
    @SpringBootApplication
    public class DemoFreeMarkerApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(DemoFreeMarkerApplication.class, args);
    	}
    }
    


    В Spring есть уже подготовленный компонент конфигурации Configuration для freemarker. Для примера консольного приложения возьму spring интерфейс для обработки командной строки(CommandLineRunner) и подготовлю модель данных для следующего шаблона ftl (hello_test.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:

    Класс CommandLine и модель данных
    @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 документ:

    Output 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:

    файл макросов ui.ftl
    <#-- 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 на макросы, то получится такой документ:

    Шаблон hello.ftl
    <#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:

    DemoController.java
    @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 репозиторий для лиц:

    RepositoryService.java
    @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;
        }
    }
    


    Класс лицо:

    Person.java
    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;
        }
    }
    


    Шаблон макросов:

    ui.ftl
    <#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>
    


    Основной шаблон:

    persons.ftl
    <#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>
    


    Структура проекта:

    image

    Приложение будет обрабатывать две команды «save» и «search» лица (см. контроллер). Всю работу по обработке (мапингу) входных параметров, берет на себя Spring.

    Некоторые пояснения к шаблону.

    <#if lastSearch??>Поиск для: ${lastSearch}<#else></#if>

    здесь проверяется, если параметр задан, то вывести фразу «Поиск для: ..», иначе ничего:

    <@ui.table id="table1" rows=persons![]/>

    здесь тоже сделана проверка, что список лиц присутствует, иначе пустой. Эти проверки важны при первом открытии страницы, иначе пришлось бы их инициализировать в index(), контроллера.

    Работа приложения

    image

    Материалы:

    Apache FreeMarker Manual
    Поделиться публикацией

    Комментарии 16

      0
      Выглядит так будто эта чудесная технология была придумана ещё шумерами. А реакт к этому можно приделать для server side рендеринга?
        0
        > еще шумерами

        Так и есть. Initial Release — в 2000 (https://en.wikipedia.org/wiki/Apache_FreeMarker).
        Также, насколько я помню, разработчики старались не иметь сторонних библиотек в зависимостях (что большой плюс при энтерпрайзной некрокодоархеологии).
          +2

          Можно, а как же!


          Пишем шаблон (это весь шаблон): ${reactApp}
          Запускаем рендеринг реакт-приложения в отдельном потоке, собираем его stdout
          Кладем собранный stdout в HashMap как в примерах и рендерим шаблон.
          Профит!

            0
            Класс! Спасибо, пойду попробую.
          +2
          есть сравнение с Apache Velocity ?

          собственно мы сейчас им (velocity) и пользуемся, и для проснения функциональности — пара вопросов, если можно:

          * можно ли в free maker отправлять на рендеринг шаблоны, созданные динамически в рантайме? например в jsf такой фичи нет — все шаблоны компиляются во время сборки. а с velocity я так могу.

          * если в значение строки подставить html — оно отрендерится как html или будет экранированно?

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

          * есть возможность реализовать внутри шаблона цикл? или list — это единственная «расширенная фича»?

          * есть ли возможность обращаться к bean-свойствам объекта который погружен в контекст? т.е. я в хешмапе определяю одну переменную, а потом пользуюсь всеми его методами и свойствами, без необходимости в хешмапе определять 20+ значений.

          Спасибо.
            +1

            По поводу динамических шаблонов — вообще не проблема. У нас шаблоны хранятся в базе (+кеш) и чудненько в процессе работы перегенерируются. Причем используются именно html, т.е. с экранированием тоже проблем нет. Соответственно вообще можно построить логику, что будет по нужным звездам разные шаблоны подаваться, делая ветвление в java логике или же if-ами.
            С бинами проблем тоже нет. Через точку можно обращаться к свойства любой глубины вложенности. Там есть реализация бин обработчика (BeansWrapperBuilder), Map-у мы вообще не используем

              +1
              Переводил View спринга и не нашёл ничего в Velocity, чего бы Freemarker не смог бы. А вот в обратную сторону не работает: у freemarker удобнее работа с мапами, лучше type-хинты в идее, понятнее синтаксис (вкусовщина, но всё же)
              Плюс в спринге поддержка velocity теперь deprecated и рекомендуют переезд на freemarker
                0
                Спасибо!
                  0
                  про «вкусовщину»… имхо вставки велосиси не смешиваются с html кодом, а во фримаркеке есть элементы похожие на теги, это мешает, имхо ) но да, может и вкусовщина.

                  тайпхинты? рад что вам нравится, но для нас это не не аргумент — идеей не пользуемся и в здравом уме и твердой памяти ее закупать в масштабах предприятия никто не будет; есть еще к ней личные претензии по поводу невозможности отобразить структуру анонимных классов в навигаторе по структуре кода, но… отложим. это вообще не по теме сейчас)

                  работа с мапами удобнее? давайте чуть позже обсудим.
                  сначала конкретику: как в freemarker сделать вот такие вещи?
                  пример 1
                  ##пример шаблона apache velocity
                  #if ( $var == "zz") 
                    <div>zzzz</div>
                  #else
                   <b>xxxx</b>
                  #end

                  пример 2
                  ##пример шаблона apache velocity
                  #foreach ($var in $obj.mycollect)
                     ##getDescription - это функция в бине obj
                     #set ($dVar = $obj.getDescription($var)) 
                     ##value - это bean - свойство с геттером
                     <div>${var.value} ( $dVar )</div>
                  #end


                    0
                    1. пример для freemarker
                    <#assign x = «plain»> — create variable x

                    <#if ${x} == 1>
                    zzzz
                    <#else>
                    xxxx
                    </#if>

                    2. второй пример делаем через лист, вызовы методов бина, делаем на момент формирования модели, и сохраняем в полях объекта, так думаю даже лучше в плане поиска ошибок и отладки, А так есть локальные, глобальные переменные шаблонов, условия и пр.
                    Вообще не сторонник доказывать что именно этот Фреймворк лучше, хуже, все имеют право быть под Солнцем, есть критерии выбора для проекта, предприятия и пр. Главное чтобы решал поставленные задачи, был эффективным, ну и приятным в использовании и разработке.

                    <#list mycollect as item>

                    </#list>
                      +1
                      спасибо.
                      исходя из описания по пункту 2:
                      1) верно ли я понимаю, что во freemarker нет переменных, которые вы можете создавать в самом шаблоне?
                      2) есть ли вызов паблик-функций класса? (вызов bean-свойств есть, но это несколько другое)
                        0
                        1. переменные можно определять:
                        локальные в рамках шаблона <#local name=value>
                        глобальные, доступные во всех шаблонах <#global name=value>
                        2. Как вариант можно так:
                        делаем класс
                           public class JavaCall implements TemplateMethodModel {
                            @Override
                            public Object exec(List args) throws TemplateModelException {
                                return "Hello " + (String) args.get(0);
                            }
                        }
                        

                        //
                        подготавливаем в модели вызов метода
                        root.put("javaHello", new JavaCall());
                        //
                        в шаблоне ftl вызываем
                        ${javaHello("FreeMatker")}

                0
                — Есть возможность определить свой загрузчик шаблонов 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}

                  0
                  Есть ли у этого шаблонизатора преимущества над thymeleaf'ом?
                  0
                  Всегда можно найти какое-то превосходство в чем-то )

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

                  Самое читаемое