基于 Spring Boot 的 Blog 开发

为了进一步简化配置,考虑使用Spring Boot将之前的项目重写,以及对各个模块进行重构。由于是一步步探索的过程,因而在此一步一步记录下来,除了自我学习以外,更方便未来review文档,同时希望能为读者带来一定的便利。

基于 Spring Boot 的 Blog 开发

转载请注明出处:基于 Spring Boot 的 Blog 开发

一、引言

之前写过几篇关于利用 Spring MVC 来开发的博客,从博客下面的评论以及 GitHub 上的 Issues 看还是会出现许多的问题,且大部分的问题都出在配置上。虽然说 Spring MVC 的配置较 SSH 简化了不少,但是在使用过程中仍然会觉得配置的吃力。

为了进一步简化配置,考虑使用 Spring Boot 将之前的项目重写,以及对各个模块进行重构。由于是一步步探索的过程,因而在此一步一步记录下来,除了自我学习以外,更方便未来 review 文档,同时希望能为读者带来一定的便利。

二、Spring Boot

Spring 是一套非常大的框架,在这个框架下有许多的子项目,这些项目可以在 https://spring.io/projects 找到,我们需要用到的两个主要项目就是 Spring Framework 和 Spring Data。这些子项目可以被运用到许许多多的场景,随意组合。然而,可能是历史原因,开发者往往会受到框架组合和配置的困扰,一个个的 xml 文件让人看起来是非常头痛的事情,而且一旦某个部分配置出错,需要花费很长的时间来找出这个错误,这样做效率是极低的。

为了极大的简化配置,甚至达到零配置的可能,Spring 团队开发出了 Spring Boot 框架,与其说它是一个框架,倒不如它是一个极大降低 Spring 开发难度的一个应用程序。

下面是官方的介绍:

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run". We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need very little Spring configuration.

大致的意思:Spring Boot 使我们更加容易地创建一个独立的、生产级的,且可以直接运行的 Spring 应用。由于它整合了 Spring platform 和一些三方库的配置,使我们可以做到真正的开箱即用。大多数的 Spring Boot 应用只需要非常少的 Spring 配置。

那它的特征有哪些呢:

  • Create stand-alone Spring applications,创建独立的 Spring 应用程序
  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files),内嵌 Tomcat, Jetty或Undertow,不需要部署 WAR 包
  • Provide opinionated 'starter' POMs to simplify your Maven configuration,提供 startet POMs,以简化 Maven 配置
  • Automatically configure Spring whenever possible,任何时候都可以自动配置 Spring
  • Provide production-ready features such as metrics, health checks and externalized configuration,提供产品级特征,例如矩阵、健康检查以及外部化配置
  • Absolutely no code generation and no requirement for XML configuratio,绝对没有代码生成,以及不需要 XML 配置

有上面这几条就已经足够吸引人了。其他的不用做太多的介绍,直接从代码来看 Spring Boot 的配置是多么的 easy。

三、开发环境

在开发之前,首先交代一下开发环境:

IDE 选用 IntelliJ IDEA 2016,Spring Tool Suite 也是一个好的选择。

JDK 使用 JDK8。

就这些,不用 Tomcat (Spring Boot内嵌),不用 Maven(IDEA内嵌)。

四、开始开发 Spring Boot 应用

4.1 生成初始项目

创建项目的方法有两种,一种是使用官方的 SPRING INITIALIZR,另一种是利用 IDEA。

4.1.1 使用 Spring Initializr 生成初始项目

打开 http://start.spring.io/

填写好项目名,所需依赖后(目前只使用 Web 和 JPA,数据库暂时使用内存数据库 H2,方便演示),点击生成项目,会生成一个 zip 包,这就是你的原始项目,解压到指定目录,使用 IDEA 打开 pom.xml:

IDEA 会自动下载所有的依赖,这样原始的项目就配置完成了:

4.1.2 使用IDEA生成初始项目

这个过程其实和 1.1 类似,相当于 1.1 的本地版本。

打开 IDEA,新建项目,选择 Spring Initializr,点击 Next:

填写项目名,Next:

选择 Web、JPA 和 H2,Next,Finish。

这样就生成了一个和 1.1 一样的项目。

4.2 开发第一个Spring Restful服务

4.2.1 任务详情

打开 https://spring.io/guides,可以看到 Spring 官网提供的一些例程,一个例程大约 15~30 分钟的时间:

选择第一个例程,需要做的如下:

请求 :

http://localhost:8080/greeting

返回:

{"id":1,"content":"Hello, World!"}

请求:

http://localhost:8080/greeting?name=Gaussic

返回:

{"id":1,"content":"Hello, Gaussic!"}

我们需要做的是,发送一个请求,返回 JSON 格式的数据。Spring-Boot-Web 整合了 Spring MVC 的配置,我们将利用 Spring MVC 来开发一个简单的 Restful 服务。

4.2.2 开发

创建 model

首先新建一个 Greeting 类,这是一个 POJO,在 mvc 框架中称之为 model,用以保存数据:

package com.gaussic;

/**
 * Created by gaussic on 11/4/2016.
 * model
 */
public class Greeting {

    private final long id;
    private final String content;

    public Greeting(long id, String content) {
        this.id = id;
        this.content = content;
    }

    public long getId() {
        return id;
    }

    public String getContent() {
        return content;
    }
}

创建 controller

controller 在 mvc 框架中是控制层,它会解析浏览器的请求,并做特定地处理。由于我们需要开发一个简单的 Restful API,Spring MVC 提供了简单的 @RestController 注解,来标明某个类是一个 Restful Controller,简化 Controller 的配置,如下:

package com.gaussic;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicLong;

/**
 * Created by gaussic on 11/4/2016.
 * controller
 */

@RestController
public class GreetingController {

    private static final String template = "Hello, %s!";  // content 模板
    private final AtomicLong counter = new AtomicLong();  // 自动生成 id

    @RequestMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Greeting(
                counter.incrementAndGet(),    // 自增
                String.format(template, name) // content模板
        );
    }
}

在运行之前,先解释一下这段代码。

首先,@RestController 注解规定了 GreetingController 是一个 Restful API 的控制器,只返回 API 相关的数据,如果是普通的 @Controller 注解,如果在方法上不做特殊的配置,将默认返回一个视图(关于视图在后面会提及)。

其次,@RequestMapping("/greeting") 是一个请求映射,当浏览器访问 http://localhost:8080/greeting 时,将会转入这个方法进行处理。

@RequestParam 用于获取该请求的参数,当浏览器访问 http://localhost:8080/greeting?name=Gaussic 时,将会把参数的值写入 name 中,此处默认值是 World,如果不设置默认值且不传递参数,将会报错。

特别要注意的是,这个方法直接返回了一个 Greeting 对象,官方的解释是:

As you see in steps below, Spring uses the Jackson JSON library to automatically marshal instances of type Greeting into JSON.

也就是说,Spring 会使用 Jackson JSON 库自动地将这个 Greeting 对象转化为 JSON 并返回。这一点非常强大,而且在编写 API 时是非常有作用的。

4.3 运行Spring Boot

现在所有的开发已经完成,在 IDEA 中运行 Spring Boot 非常简单,点击右上角的箭头即可:

在浏览器中访问 http://localhost:8080/greeting,返回 json 格式数据,刷新之后 id 会自增:

传入参数 name,将返回新的数据:

这样,一个简单的 Restful Service 开发完成了。

如果需要在外部运行,需要将应用打成 jar 包。在此可以使用 IDEA 内部集成的 maven 进行打包,

点击右端的 Maven Projects,先选择 clean,再选择 package,将会在 target 文件夹下生成一个 .jar 文件。

现在,使用:

java -jar target/springblog-0.0.1-SNAPSHOT.jar

运行即可。

在具体的开发之前,对目录结构做一定的调整,如下图所示:

五、模板与 URL

5.1 Thymeleaf 模板

在上面的示例中展示了使用 @RestController 和 @RequestMapping 来处理请求并返回 JSON 格式数据的方法,在日常的需求中需要使用特定的模板页面来渲染,此时会使用常用的 @Controller 注解来标注这是一个普通的控制器类。

在 pom.xml 中引入 Thymeleaf 来作为模板引擎:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

我们在 src\main\java\com\gaussic\controller 中新建 MainController 类,用来处理网站的几个主要请求:

package com.gaussic.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Created by dzkan on 2016/11/21.
 * To deal with some main requests.
 */

@Controller
public class MainController {

    @RequestMapping("/")
    public String index() {
        return "index";
    }
}

在 resources\templates 中新建 index.html,表示首页:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>Document</title>
</head>
<body>
    <h1>这是首页</h1>

</body>
</html>

(在 IDEA 中有一个小技巧,输入 ! 然后再按 Tab 键,上面的代码就会自动完成,不过需要将 meta 的标签闭合(如上所示),其他的还有很多可以发掘,一些基本的标签组合 Tab 键都能自动补全,用过 Sublime text 的同学应该会很容易发现。)

重新启动 Spring Boot,访问 http://localhost:8080,将跳转到我们的首页:

从上面我们可以发现,使用普通的 @Controller 注解,返回的时候会查找字符串定义的模板页面,如 返回 index,spring boot 会自动的在 resources\templates 下面的 index.html 文件,如果存在嵌套目录,如 blog/list.html,那么只需返回 "blog/list" 即可。

此外,有一点需要极其注意,Thymeleaf 遇到标签不闭合的状态会报错,但是部分的 html5 标签是可以不闭合的,例如

等等,为了让 Thymeleaf 支持 HTML5,需要在 application.properties 中配置:

spring.thymeleaf.mode=LEGACYHTML5

还需要添加 nekohtml 依赖:

<dependency>
    <groupId>net.sourceforge.nekohtml</groupId>
    <artifactId>nekohtml</artifactId>
    <version>1.9.22</version>
</dependency>

这样 Thymeleaf 才能正常的识别 HTML5 了,否则的话,请保证每一个标签都是闭合的。

5.2 URL映射

我们已经了解,URL 的映射主要通过在 Controller 中定义 @RequestMapping 来完成。

如 5.1 中的 index() 方法:

@RequestMapping("/")
    public String index() {
        return "index";
    }

以及 4.2 中的 greeting() 方法:

@RequestMapping("/greeting")
    public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
        return new Greeting(
                counter.incrementAndGet(),    // 自增
                String.format(template, name) // content模板
        );
    }

@RequestMapping 默认处理的是 GET 请求,以上两种情况相当于:

@RequestMapping(value = "/", method = RequestMethod.GET)
    @RequestMapping(value = "/greeting", method = RequestMethod.GET)

如果是 POST 请求,可以将 method 改为 RequestMethod.POST。此外,新版本的 Spring MVC 提供了更加简化的请求方式:

  • @GetMapping:Get 请求,常用于页面访问操作
  • @PostMapping:Post 请求,常用于添加等表单操作
  • @PutMapping:Put 请求,常用于修改或编辑等表单操作
  • @DeleteMapping:Delete 操作,常用于删除操作

这一些将在后面涉及到,我们把这两个方法改为:

@GetMapping("/")
    @GetMapping("/greeting")

此外,在 4.2 中使用了 @RequestParam 来获取参数,其 url 形式如下:

http://localhost:8080/greeting?name=Gaussic

@RequestParam 可以非常轻松地获取以上 URL 的参数,然而目前大多数流行的网站都使用了 Rest 风格的参数,以更直观的表达 url 的意图,例如本文地址:

https://my.oschina.net/gaussik/blog/781773

其结构为 http://xxx.net/{username}/blog/{blogId},这样的结构相比问号的形式更加简洁明了,如果改为旧的方法,如下所示:

https://my.oschina.net?username=gaussic&type=blog&id=781773

当参数越来越多时,阅读起来就不是那么的容易。本文的所有 URL 都将使用后者。

那么如何获取获取形如:

https://my.oschina.net/gaussik/blog/781773

的参数呢?Spring MVC 提供了 @PathVariable 注解,来获取 URL 中的路径参数。

以 index() 方法为例,我们将 index() 改为如下形式:

@GetMapping("/{username}/blog/{blogId}")
    public String index(@PathVariable("username") String username,
                        @PathVariable("blogId") String blogId, ModelMap model) {
        model.addAttribute(username);
        model.addAttribute(blogId);
        return "index";
    }

注意如下几点:

  • 所有的参数用 {} 包含,再使用 @PathVariable 来获取
  • 使用 ModelMap 来向模板页面传递数据

修改 index.html,来打印传递过来的数据:

<!doctype html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>这里是首页!</h1>
    <ul>
        <li th:text="${username}">用户名:${username}</li>
        <li th:text="${blogId}">博客id:${blogId}</li>
    </ul>
</body>
</html>

重新启动,访问 http://localhost:8080/gaussic/blog/23333