架构实战篇(四):Spring Boot整合 Thymeleaf

前言

Thymeleaf 是一种模板语言。那模板语言或模板引擎是什么?常见的模板语言都包含以下几个概念:数据(Data)、模板(Template)、模板引擎(Template Engine)和结果文档(Result Documents)。

Spring boot 支持多种模板语言(Thymeleaf 、Freemarker、Mustache、Groovy Templates)
Thymeleaf 跟大部分的模板语言类似,上手容易,使用简单

我们先看下已经完成的项目结构图

项目结构

最终运行结果

最终运行结果

下面开始一步一步的编写代码了

增加Spring boot的maven 依赖

在原有基础的pom结构中追加Swagger2的依赖

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>spring-boot-web-thymeleaf</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- Compile -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

增加一个消息类

package sample.web.ui;
import java.util.Calendar;
import org.hibernate.validator.constraints.NotEmpty;

public class Message {

    private Long id;

    // 编写不能为空的提示语
    @NotEmpty(message = "Message is required.")
    private String text;

    // 编写不能为空的提示语
    @NotEmpty(message = "Summary is required.")
    private String summary;

    private Calendar created = Calendar.getInstance();

        // get set
}

保存消息的接口

package sample.web.ui;

public interface MessageRepository {
    Iterable<Message> findAll();
    Message save(Message message);
    Message findMessage(Long id);
    void deleteMessage(Long id);
}

使用内存保存消息

package sample.web.ui;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

public class InMemoryMessageRepository implements MessageRepository {

    // 用来模拟主键自增
    private static AtomicLong counter = new AtomicLong();

    // 用来存储消息
    private final ConcurrentMap<Long, Message> messages = new ConcurrentHashMap<Long, Message>();

    @Override
    public Iterable<Message> findAll() {
        return this.messages.values();
    }

    @Override
    public Message save(Message message) {
        Long id = message.getId();
        if (id == null) {
            // 生成一个ID
            id = counter.incrementAndGet();
            message.setId(id);
        }
        // 保存消息
        this.messages.put(id, message);
        return message;
    }

    @Override
    public Message findMessage(Long id) {
        return this.messages.get(id);
    }

    @Override
    public void deleteMessage(Long id) {
        this.messages.remove(id);
    }
}

编写控制层代码

package sample.web.ui.mvc;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import sample.web.ui.Message;
import sample.web.ui.MessageRepository;
import javax.validation.Valid;

@Controller
@RequestMapping("/messages")
public class MessageController {

    private final MessageRepository messageRepository;

    public MessageController(MessageRepository messageRepository) {
        this.messageRepository = messageRepository;
    }

    // 进入消息列表页面
    @GetMapping
    public ModelAndView list() {
        Iterable<Message> messages = this.messageRepository.findAll();
        return new ModelAndView("messages/list", "messages", messages);
    }

    // 查看消息详情
    @GetMapping("{id}")
    public ModelAndView view(@PathVariable("id") Message message) {
        return new ModelAndView("messages/view", "message", message);
    }

    // 进入创建消息页面
    @GetMapping(params = "form")
    public String createForm(@ModelAttribute Message message) {
        return "messages/form";
    }

    // 创建消息
    @PostMapping
    public ModelAndView create(@Valid Message message, BindingResult result,
                               RedirectAttributes redirect) {
        // 内容验证
        if (result.hasErrors()) {
            return new ModelAndView("messages/form", "formErrors", result.getAllErrors());
        }
        // 保存消息
        message = this.messageRepository.save(message);
        // 重定向增加一个消息
        redirect.addFlashAttribute("globalMessage", "Successfully created a new message");
        return new ModelAndView("redirect:/messages/{message.id}", "message.id", message.getId());
    }

    // 删除消息
    @GetMapping(value = "delete/{id}")
    public ModelAndView delete(@PathVariable("id") Long id) {
        this.messageRepository.deleteMessage(id);
        Iterable<Message> messages = this.messageRepository.findAll();
        return new ModelAndView("messages/list", "messages", messages);
    }

    // 进入修改消息页面
    @GetMapping(value = "modify/{id}")
    public ModelAndView modifyForm(@PathVariable("id") Message message) {
        return new ModelAndView("messages/form", "message", message);
    }
}

程序入口main

package sample.web.ui;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@SpringBootApplication
public class SampleWebUiApplication {

    // 在Spring 容器中加入内存管理消息实例
    @Bean
    public MessageRepository messageRepository() {
        return new InMemoryMessageRepository();
    }

    // 自定义类型转换,Controller 入参字符串转换为 Message 类型
    @Bean
    public Converter<String, Message> messageConverter() {
        return new Converter<String, Message>() {
            @Override
            public Message convert(String id) {
                return messageRepository().findMessage(Long.valueOf(id));
            }
        };
    }

    // 跳转到消息列表
    @GetMapping("/")
    public ModelAndView index(){
        return new ModelAndView("redirect:/messages");
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(SampleWebUiApplication.class, args);
    }
}

编写布局页面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout">
<head>
<title>Layout</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"
    href="../../css/bootstrap.min.css" />
    <link rel="icon" th:href="@{/favicon.jpg}" href="favicon.jpg" />
</head>
<body>
    <div class="container">
        <div class="navbar">
            <div class="navbar-inner">
                <a class="brand" th:href="@{/messages}" href="#">Thymeleaf - Layout</a>
                <ul class="nav">
                    <li><a th:href="@{/messages}" href="messages.html"> Messages </a></li>
                </ul>
            </div>
        </div>
        <h1 layout:fragment="header">Layout</h1>
        <div layout:fragment="content">Fake content</div>
    </div>
</body>
</html>

编写列表页面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"
    layout:decorator="layout">
<head>
<title>Messages : View all</title>
</head>
<body>
    <h1 layout:fragment="header">Messages : View all</h1>
    <div layout:fragment="content" class="container">
        <div class="pull-right">
            <a href="form.html" th:href="@{/messages/(form)}">Create Message</a>
        </div>
        <table class="table table-bordered table-striped">
            <!-- 使用下面注解类解决IntelliJ提示问题 -->
            <!--/*@thymesVar id="messages" type="java.util.List"*/-->
            <!--/*@thymesVar id="message" type="sample.web.ui.Message"*/-->
            <thead>
                <tr>
                    <td>ID</td>
                    <td>Created</td>
                    <td>Summary</td>
                </tr>
            </thead>
            <tbody>
                <tr th:if="${messages.isEmpty()}">
                    <td colspan="3">No messages</td>
                </tr>
                <tr th:each="message : ${messages}">
                    <td th:text="${message.id}">1</td>
                    <td th:text="${#calendars.format(message.created)}">July 11,
                        2012 2:17:16 PM CDT</td>
                    <td><a href="view.html" th:href="@{'/messages/' + ${message.id}}"
                        th:text="${message.summary}"> The summary </a></td>
                </tr>
            </tbody>
        </table>
    </div>
</body>
</html>

编写增加页面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"
    layout:decorator="layout">
<head>
<title>Messages : Create</title>
</head>
<body>
    <h1 layout:fragment="header">Messages : Create</h1>
    <div layout:fragment="content" class="container">
        <!-- 使用下面注解类解决IntelliJ提示问题 -->
        <!--/*@thymesVar id="message" type="sample.web.ui.Message"*/-->
        <form id="messageForm" th:action="@{/messages/(form)}" th:object="${message}"
            action="#" method="post">
            <div th:if="${#fields.hasErrors('*')}" class="alert alert-error">
                <p th:each="error : ${#fields.errors('*')}" th:text="${error}">
                    Validation error</p>
            </div>
            <div class="pull-right">
                <a th:href="@{/messages}" href="messages.html"> Messages </a>
            </div>
            <input type="hidden" th:field="*{id}"
                th:class="${#fields.hasErrors('id')} ? 'field-error'" /> <label
                for="summary">Summary</label> <input type="text"
                th:field="*{summary}"
                th:class="${#fields.hasErrors('summary')} ? 'field-error'" /> <label
                for="text">Message</label>
            <textarea th:field="*{text}"
                th:class="${#fields.hasErrors('text')} ? 'field-error'"></textarea>
            <div class="form-actions">
                <input type="submit" value="Save" />
            </div>
        </form>
    </div>
</body>
</html>

编写详情页面

<html xmlns:th="http://www.thymeleaf.org"
    xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"
    layout:decorator="layout">
<head>
<title>Messages : View</title>
</head>
<body>
<!-- 使用下面注解类解决IntelliJ提示问题 -->
<!--/*@thymesVar id="message" type="sample.web.ui.Message"*/-->
    <h1 layout:fragment="header">Messages : Create</h1>
    <div layout:fragment="content" class="container">
        <!--/*@thymesVar id="globalMessage" type=""*/-->
        <div class="alert alert-success" th:if="${globalMessage}"
            th:text="${globalMessage}">Some Success message</div>
        <div class="pull-right">
            <a th:href="@{/messages}" href="list.html"> Messages </a>
        </div>
        <dl>
            <dt>ID</dt>
            <dd id="id" th:text="${message.id}">123</dd>
            <dt>Date</dt>
            <dd id="created" th:text="${#calendars.format(message.created)}">
                July 11, 2012 2:17:16 PM CDT</dd>
            <dt>Summary</dt>
            <dd id="summary" th:text="${message.summary}">A short summary...
            </dd>
            <dt>Message</dt>
            <dd id="text" th:text="${message.text}">A detailed message that
                is longer than the summary.</dd>
        </dl>
        <div class="pull-left">
            <a href="messages" th:href="@{'/messages/delete/' + ${message.id}}">
                delete </a> | <a href="form.html"
                th:href="@{'/messages/modify/' + ${message.id}}"> modify </a>
        </div>
    </div>
</body>
</html>

Spring 设置 application.properties

spring.thymeleaf.cache=false
server.tomcat.basedir=target/tomcat
server.tomcat.accesslog.enabled=true

编写日志文件 logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
</configuration>

bootstrap v2.0 请到官网下载

到这里所有的类都编写完了,让我们来用用看吧

让我们打开浏览器地址栏访问
http://localhost:8080/

演示

你的运行结果对了吗?

更多精彩内容

架构实战篇(一):Spring Boot 整合MyBatis
架构实战篇(二):Spring Boot 整合Swagger2
架构实战篇(三):Spring Boot 整合MyBatis(二)
架构实战篇(四):Spring Boot 整合 Thymeleaf
架构实战篇(五):Spring Boot 表单验证和异常处理
架构实战篇(六):Spring Boot RestTemplate的使用

关注我们

如果需要源码可以关注“IT实战联盟”公众号并留言(源码名称+邮箱),小萌看到后会联系作者发送到邮箱,也可以加入交流群和作者互撩哦~~~!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容