Spring Boot 2.0 学习笔记及由浅入深的代码示例

1. Spring Boot 介绍

Spring Boot 简化了基于 Spring 的应用开发,你只需要 "run" 就能创建一个独立的,产品级别的 Spring 应用。
你可以使用 Spring Boot 创建 Java 应用,并使用 java -jar 启动它或采用传统的 war 部署方式。我们也提供了一个运行 "Spring 脚本" 的命令行工具。

主要特性:

  • 为所有 Spring 开发提供一个从根本上更快,且随处可得的入门体验。
  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)。
  • 开箱即用,但通过不采用默认设置可以快速摆脱这种方式。
  • 提供一系列大型项目常用的非功能性特征,比如:内嵌服务器,安全,指标 metrics,健康检测 health checks,外部化配置 externalized configuration。
  • 绝对没有代码生成,也不需要XML配置。

关于 Spring Boot 升级到 2.0,请参见 http://blog.didispace.com/spring-boot-2-release/

1.1 系统要求

Java + 构建工具(Maven / Gradle)+ Servlet 容器(Tomcat / Jetty / Undertow)

1.2 Spring Boot CLI 安装

在 Mac 系统中使用 homebrew 来安装:

brew tap pivotal/tap
brew install springboot

homebrew 将把 Spring Boot 安装到 /usr/local/bin 下。

Spring Boot CLI安装

2. 一个 Spring Boot 示例 MyFirstSpringBoot

首先通过 https://start.spring.io/ 来生成基础代码,项目名:MyFirstSpringBoot,这里我们选择使用 Maven 来构建,并添加了 Web 依赖:

通过 https://start.spring.io/ 来生成基础代码

将代码下载到本地并解压缩,使用 IDE 打开,这里我们使用 IntelliJ IDEA,结构如下:


IntelliJ IDEA 打开 Spring Boot 示例项目

我们也可以通过 IntelliJ IDEA(社区版 Community) 的 Spring Assistant 插件来构造项目结构:


IntelliJ IDEA(社区版 Community) 的 Spring Assistant

打开 pom.xml 文件,可以看到如下的依赖配置:

  • 其中就包括了我们在项目创建时添加的 Web 依赖 spring-boot-starter-web
  • spring-boot-maven-plugin 是一个Maven插件,它可以将项目打包成一个可执行jar
  • starters 是一个依赖描述符的集合,你可以将它包含进项目中,这样添加依赖就非常方便。例如,如果你想使用 Spring 和 JPA 进行数据库访问,只需要在项目中包含 spring-boot-starter-data-jpa 依赖,然后你就可以开始了。完整的 starters 列表请点击此处查看。
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.3.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-web</artifactId>
        </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>

通过运行 mvn dependency:tree 命令,可以看到该项目具体的依赖树:

MyFirstSpringBoot 的依赖树

修改 MyFirstSpringBootApplication.java 类,内容如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableAutoConfiguration
public class MyFirstSpringBootApplication {

    @RequestMapping("/")
    String home() {
        return "Hello World!";
    }

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

对这段代码的几点分析:

  • 从 import 中可以看出,@RestController@RequestMapping 是Spring MVC中的注解(它们不是Spring Boot的特定部分)
  • @EnableAutoConfiguration,这个注解告诉 Spring Boot 根据添加的 jar 依赖猜测你想如何配置Spring。由于 spring-boot-starter-web 添加了 Tomcat 和 Spring MVC,所以 auto-configuration 将假定你正在开发一个 web 应用,并对 Spring 进行相应地设置。
  • SpringApplication.run(...) 启动Spring,相应地启动被自动配置的 Tomcat web 服务器。

修改 MyFirstSpringBootApplicationTests.java 类,内容如下:

import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class MyFirstSpringBootApplicationTests {

    @Test
    public void contextLoads() {
    }

}

最后通过 mvn spring-boot:run 来启动这个项目:

mvn spring-boot:run

可以看出,run 是定义在 spring-boot-maven-plugin 这个依赖中的。

mvn spring-boot:run

可以看出启动了 8080 端口 ,因此可以通过 http://localhost:8080/ 来访问:
通过 http://localhost:8080/ 来访问

我们也可以通过 mvn package 创建可执行 jar 包,并通过 jar 包来运行启动:

生成在 target 目录下

通过 java -jar 来运行启动

3. 扩展成一个 Web MVC 的应用

现在我们一步一步扩展上面的项目,并将其扩展为一个完整的 Web MVC 的应用。
注意,请将程序入口 MyFirstSpringBootApplication.java 放在 src 的根目录,如下所示,这样 Spring 就会扫描根目录及所有的子包,例如 /controller/model/service

程序入口 MyFirstSpringBootApplication.java 放在 src 的根目录

3.1 POM 中添加 JSP 依赖

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>

3.2 增加一个首页

现在我们添加第一个 JSP 页面 welcome.jsp,位置在 src/main/webapp/WEB-INF/jsp

JSP 页面的位置

<!DOCTYPE html>

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<html lang="en">

<body>
    ${welcomeMessage}
</body>

</html>

新建第一个 WelcomeController.java,新建一个 package 名为 controller

Controller 的位置

@Controller
public class WelcomeController {

    @Value("${welcomeMessage}")
    private String welcomeMessage;

    @RequestMapping("/")
    public String welcome(Map<String, Object> model) {
        model.put("welcomeMessage", welcomeMessage);
        return "welcome";
    }

}

resources/application.properties 文件中添加如下内容:

spring.mvc.view.prefix: /WEB-INF/jsp/
spring.mvc.view.suffix: .jsp
welcomeMessage=Welcome, This is a Spring Boot Demo

修改程序入口 MyFirstSpringBootApplication.java 文件内容如下:

@SpringBootApplication
public class MyFirstSpringBootApplication {

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

启动引用,效果如下:


增加一个首页

3.3 使用模板

目前 Spring 官方已经不推荐使用 JSP 来开发 WEB 了,而是推荐使用模板引擎来开发。
下面我们使用 Freemaker 模板引擎来重构这个首页。
首先在 pom.xml 中引入 Freemaker:

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

随后在 /resources/templates 目录下新建一个模板文件 welcome.ftl

新建一个模板文件 welcome.ftl

<!DOCTYPE html>
<html>
<body>
    ${welcomeMessage}
</body>
</html>

最后删除文件 welcome.jsp,并且删除 resources/application.properties 文件中关于 Spring MVC 的两行配置。

3.4 错误处理

Spring Boot 默认提供一个 /error 映射用来以合适的方式处理所有的错误,并将它注册为 servlet 容器中全局的 错误页面。对于浏览器客户端,它会产生一个白色标签样式(whitelabel)的错误视图,该视图将以 HTML 格式显示同样的数据,默认的 404 错误处理如下:

默认的404错误处理

如果想为某个给定的状态码展示一个自定义的 HTML 错误页面,你需要将文件添加到 /error 文件夹下。错误页面既可以是静态HTML(比如,任何静态资源文件夹下添加的),也可以是使用模板构建的,文件名必须是明确的状态码或一系列标签。
404错误页面的位置

<html>
<head><title>404</title></head>

<body>
<div style="color: red">Sorry, we can't find it.</div>
</body>
</html>
自定义的404错误处理

3.5 静态内容

默认情况下,Spring Boot 从 classpath 下的 /static/public/resources/META-INF/resources)文件夹,或从ServletContext 根目录提供静态内容。这是通过 Spring MVC 的 ResourceHttpRequestHandler 实现的,你可以自定义 WebMvcConfigurerAdapter 并覆写 addResourceHandlers 方法来改变该行为(加载静态文件)。

例如,添加一个图片到 /resources/static/images 下:

静态内容

则可以通过路径 images/home.png 引用到该图片:

<!DOCTYPE html>
<html>
<body>
    <img src="images/home.png" width="48" />
    ${welcomeMessage}
</body>
</html>
引用静态内容

3.6 日志

Spring Boot默认的日志输出格式如下:

2018-06-20 17:29:46.426  INFO 85118 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2018-06-20 17:29:46.427  INFO 85118 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2152 ms
2018-06-20 17:29:46.666  INFO 85118 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]
2018-06-20 17:29:46.671  INFO 85118 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-06-20 17:29:46.673  INFO 85118 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-06-20 17:29:46.673  INFO 85118 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-06-20 17:29:46.673  INFO 85118 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2018-06-20 17:29:46.861  INFO 85118 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-06-20 17:29:47.158  INFO 85118 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@1bad5e6d: startup date [Wed Jun 20 17:29:44 CST 2018]; root of context hierarchy
2018-06-20 17:29:47.290  INFO 85118 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/]}" onto public java.lang.String com.example.MyFirstSpringBoot.controller.WelcomeController.welcome(java.util.Map<java.lang.String, java.lang.Object>)

输出的节点(items)如下:

  • 日期和时间 - 精确到毫秒,且易于排序。
  • 日志级别 - ERROR, WARN, INFO, DEBUG 或 TRACE。
  • Process ID。
  • ---分隔符,用于区分实际日志信息开头。
  • 线程名 - 包括在方括号中(控制台输出可能会被截断)。
  • 日志名 - 通常是源class的类名(缩写)。
  • 日志信息。

关于 Java 的日志,详细请参见 Java Log 日志

这里我们选择集成 Logback。Spring Boot包含很多有用的Logback扩展,你可以在 logback-spring.xml 配置文件中使用它们。
首先在 /resources 目录下新建一个文件 logback-spring.xml

Logback 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
    <property name="LOG_HOME" value="/Users/xianch/Desktop/SpringBoot/MyFirstSpringBoot/logs" />

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 按照每天生成日志文件 -->
    <appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/TestWeb.log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="FILE" />
    </root>
</configuration>

随后在 WelcomeController.java 中添加对日志的引用:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......
    // 使用日志框架SLF4J 中的 API,使用门面模式的日志框架
    private final static Logger logger = LoggerFactory.getLogger(WelcomeController.class);

    @Value("${welcomeMessage}")
    private String welcomeMessage;

    @RequestMapping("/")
    public String welcome(Map<String, Object> model) {

        logger.info("welcome page");

        model.put("welcomeMessage", welcomeMessage);
        return "welcome";
    }

日志打印如下:

2018-06-20 18:22:01.625 [http-nio-8080-exec-1] INFO  c.e.MyFirstSpringBoot.controller.WelcomeController - welcome page

3.7 增加完整的功能

现在我们给这个项目增加两个功能:

  • 提供一个页面查看所有的用户
  • 提供一个页面添加用户

首先添加模型类 User.java

package com.example.MyFirstSpringBoot.model;

public class User {

    private Integer id;
    private String name;

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {

        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然后添加服务类 UserService.java

package com.example.MyFirstSpringBoot.service;

import com.example.MyFirstSpringBoot.model.User;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Component
public class UserService {

    private List<User> users = new ArrayList<User>();

    public UserService() {
        // Create some mockup data
        users.add(new User(1, "Test User 1"));
        users.add(new User(2, "Test User 2"));
    }

    public List<User> getUsers() {
        return users;
    }

    public void addUser(User user) {
        users.add(user);
    }
}

然后添加控制器 UserController.java

package com.example.MyFirstSpringBoot.controller;

import com.example.MyFirstSpringBoot.model.User;
import com.example.MyFirstSpringBoot.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.Map;

@Controller
@RequestMapping("/user")
public class UserController {

    private final static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    UserService userService;

    @RequestMapping("/list_user")
    public String listUser(Map<String, Object> model) {
        logger.info("list user page");

        model.put("users", userService.getUsers());

        return "user/list_user";
    }

    @RequestMapping("/add_user")
    public String addUser() {
        logger.info("add user page");

        return "user/add_user";
    }

    @RequestMapping("/create_user")
    public String createUser(@ModelAttribute User user, Map<String, Object> model) {
        logger.info("create user");

        userService.addUser(user);

        model.put("users", userService.getUsers());

        return "user/list_user";
    }
}

然后添加页面 list_user.ftladd_user.ftl

<!DOCTYPE html>
<html>
<body>
    <#list users as user>
        ID:${user.id} , Name:${user.name}
        <br />
   </#list>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
    <form action="/user/create_user" method="post">
        ID: <input type="text" name="id" /><br/>
        Name: <input type="text" name="name" /><br/>
        <input type="submit">
    </form>
</body>
</html>

然后修改首页,添加入口 welcome.ftl

<!DOCTYPE html>
<html>
<body>
    <img src="images/home.png" width="48" />
    ${welcomeMessage}

    <br />
    <a href="user/list_user">List User</a>
    <br />
    <a href="user/add_user">Add User</a>
</body>
</html>

效果如下:


首页
用户列表
添加用户
添加用户成功

3.8 连接数据库

在上面的例子中,我们使用一个全局变量 List<User> users = new ArrayList<User>(); 来存储用户信息,现在我们用一个数据库存储替代。
在这里,我们使用一个内存数据库 H2。我们不需要提供任何连接URLs,只需要添加想使用的内嵌数据库依赖。
首先在 pom.xml 中引入 JPA 和 H2:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

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

我们可以指定一些SQL文件来在启动时初始化数据库,例如新建文件 user_schema.sqluser_data.sql 来分别用来创建数据表和填充内容:

别用来创建数据表和填充内容

CREATE TABLE USERS(ID INT PRIMARY KEY, NAME VARCHAR(255));
INSERT INTO USERS(ID, NAME) VALUES(1, 'Test User 1');
INSERT INTO USERS(ID, NAME) VALUES(2, 'Test User 2');

然后在 application.properties 里进行数据库连接的配置:

spring.h2.console.settings.web-allow-others=true
spring.h2.console.path=/h2-console
spring.h2.console.enabled=true

spring.datasource.url=jdbc:h2:mem:test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.datasource.schema=classpath:db/user_schema.sql
spring.datasource.data=classpath:db/user_data.sql

spring.datasource.initialization-mode=always
spring.datasource.platform=h2
spring.jpa.hibernate.ddl-auto=none

当完成依赖和连接配置这两步之后,就可以在程序种使用 H2 了。Spring 会自动完成 DataSource 的注入。
同时我们可以通过 http://localhost:8080/h2-console/ 来访问 H2 的控制台:

H2 的控制台

H2 的控制台

接下来我们修改实体类 User.java

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "users")
public class User {

    @Id
    @Column
    private Integer id;
    @Column
    private String name;

    public User() {
    }

    public User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {

        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  • @Table 声明此对象映射到数据库的数据表
  • @Id 声明此属性为主键
  • @Column 声明该属性与数据库字段的映射关系。

Spring Data JPA 包含了一些内置的 Repository,实现了一些常用的方法:findonefindallsave等。我们新建一个 UserDAO.java 来进行数据库的交互:

新建一个 UserDAO

import com.example.MyFirstSpringBoot.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserDAO extends JpaRepository<User, Integer> {

}

最后修改 UserService.java 类:

@Component
public class UserService {

    @Autowired
    private UserDAO userDAO;

    public List<User> getUsers() {
        return userDAO.findAll();
    }

    public void addUser(User user) {
        userDAO.save(user);
    }
}

重新启动程序,使用效果跟上一小节 3.7 一样。

3.9 提供一个 Restful 接口

假设我们想要提供一个 Restful 接口 /user/list_user_json 返回 JSON 格式的用户数据。
新建一个 UserRestController.java

@RestController
@RequestMapping("/user")
public class UserRestController {
    private final static Logger logger = LoggerFactory.getLogger(UserRestController.class);

    @Autowired
    UserService userService;

    @RequestMapping("/list_user_json")
    public List<User> listUserJSON() {
        logger.info("list user in JSON format");

        return userService.getUsers();
    }
}
一个 Restful 接口

3.10 使用缓存 Redis

对于查询用户列表的这个服务,假设我们不希望每次都 Query 数据库,我们可以将用户列表放在缓存中,当有新用户添加时,才更新这个缓存。
这里我们使用 Redis 缓存。关于 Redis 的安装和启动,请参见 Spring 缓存开发实践 & Redis 集成
pom.xml 中添加 Cache 的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>1.4.7.RELEASE</version>
</dependency>

修改 application.properties,添加 Redis 相关配置:

## Redis 配置
spring.cache.type=redis
## Redis数据库索引(默认为0)
spring.redis.database=0
## Redis服务器地址
spring.redis.host=127.0.0.1
## Redis服务器连接端口
spring.redis.port=6379
## Redis服务器连接密码(默认为空)
spring.redis.password=
## 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
## 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
## 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
## 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
## 连接超时时间(毫秒)
spring.redis.timeout=0

添加一个 Redis 配置类 RedisConfig.java

package com.example.MyFirstSpringBoot.config;

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

import java.lang.reflect.Method;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean("keyGenerator")
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName());
                sb.append("-").append(method.getName());
                for (Object obj : params) {
                    sb.append(obj.toString());
                }
                return sb.toString();
            }
        };
    }

    // 缓存管理器
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory);
        return builder.build();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(cf);
        return redisTemplate;
    }
}

注意:

  • @Configuration:相当于传统的 XML 配置文件,如果有些第三方库需要用到 XML 文件,建议仍然通过 @Configuration 类作为项目的配置主类。
  • 通过 @EnableCaching 注解开启缓存支持,Spring Boot 就会根据实现自动配置一个合适的CacheManager。

同时也要修改模型类 User.java

  • 继承接口 implements Serializable
  • 添加一个 toString() 方法
  • 添加一个 serialVersionUID 字段

修改 UserService.java,通过注解的方式引入 Redis 缓存:

@Component
@CacheConfig(cacheNames = "UserService")
public class UserService {
    private final static Logger logger = LoggerFactory.getLogger(UserService.class);

    @Autowired
    private UserDAO userDAO;

    @Cacheable(value = "users")
    public List<User> getUsers() {
        logger.info("query DB to get users");

        return userDAO.findAll();
    }

    @CacheEvict(value = "users", allEntries = true)
    public void addUser(User user) {
        logger.info("update DB to save new user");

        userDAO.save(user);
    }
}

测试结果如下:
系统启动后,多次刷新页面,调用 http://localhost:8080/user/list_user,日志如下:可以看出,只进行了一次数据库的查询操作。

只进行了一次数据库的查询操作

通过 Redis CLI 可以看到结果已缓存到 Redis 中:其中 key 的生成方式是我们在 RedisConfig.java 中自定义的。

结果已缓存到 Redis 中

下面我们添加一个新用户,随后多次刷新页面,调用 http://localhost:8080/user/list_user,日志如下:可以看出,添加新用户后,重新查询数据库,更新缓存。

添加新用户后,重新查询数据库,更新缓存

3.11 数据验证

在添加用户页面时,如果我们在 ID 输入框输入一个字母,会发生什么?


在 ID 输入框输入一个字母
错误信息

现在我们希望实现如下的功能:

  • 增加校验
    • id 只能是数字,并且只能为正数
    • name 不能为空
  • 如果校验不通过,返回添加用户页面,并且现实相关错误信息

pom.xml 中添加 Validation 的依赖:

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

修改实体类 User.java,定义验证规则。更多的注解,请参见 http://www.baeldung.com/javax-validation

@Id
@Column
@Digits(integer = 3, message = "{validation.user.id}", fraction = 0)
@Positive(message = "{validation.user.id}")
private Integer id;

@Column
@NotEmpty(message = "{validation.user.name}")
private String name;

/resources 目录下新建一个文件 ValidationMessages.properties,定义错误文字信息:

# Validation
validation.user.id=ID can only be positive number
validation.user.name=Name shouldn't be empty

修改控制器 UserController.java,增加错误处理的逻辑:

    @RequestMapping("/add_user")
    public String addUser(Map<String, Object> model) {
        logger.info("add user page");

        model.put("user", new User());

        return "user/add_user";
    }

    @RequestMapping("/create_user")
    public String createUser(Map<String, Object> model, @ModelAttribute @Valid User user, BindingResult bindingResult) {
        logger.info("create user");

        if (bindingResult.hasErrors()) {
            logger.info("create user validation failed");

            model.put("user", user);

            return "user/add_user";
        }

        userService.addUser(user);

        model.put("users", userService.getUsers());

        return "user/list_user";
    }

修改前端页面 add_user.ftl,展示错误信息:

<#import "/spring.ftl" as spring />

<!DOCTYPE html>
<html>
<body>
    <form action="/user/create_user" method="post">
        <@spring.bind "user.id"/>
        ID: <input type="text" name="id" value="${user.id!}" />
        <@spring.showErrors ""/>
        <br/>

        <@spring.bind "user.name"/>
        Name: <input type="text" name="name" value="${user.name!}" />
        <@spring.showErrors ""/>
        <br/>

        <input type="submit">
    </form>
</body>
</html>

测试效果如下:


数据验证

3.12 使用消息队列

假设我们有了一个新的需求,新增一个用户后,需要为这个用户创建一个银行账号 Bank Account,而创建银行账号的服务不是同步的,它是一个异步服务,每天的早上9点到下午5点才会执行。
这里我们使用 ActiveMQ 消息队列,当新增用户 addUser(User user) 的时候,发送一条消息给队列。

关于 ActiveMQ 的安装和启动,请参见Spring 消息系统 JMS 开发实践

pom.xml 中添加 ActiveMQ 的依赖:

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

修改 application.properties,添加 ActiveMQ 相关配置:

spring.activemq.broker-url=tcp://localhost:61616  
spring.activemq.in-memory=true  
spring.activemq.pool.enabled=false
## 以下的配置使得可以发送 Java Object
spring.activemq.packages.trust-all=true

添加一个 ActiveMQ JMS 配置类 JMSConfig.java

package com.example.MyFirstSpringBoot.config;

import org.apache.activemq.command.ActiveMQQueue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.jms.Queue;

@Configuration
public class JMSConfig {
    @Bean(name = "usersQueue")
    public Queue counting() {
        return new ActiveMQQueue("testing.users");
    }
}

修改 UserSerive.java,在 addUser 操作中添加发送消息的逻辑:

    @Autowired
    private JmsMessagingTemplate jmsTemplate;

    @Autowired
    private Queue usersQueue;

    @CacheEvict(value = "users", allEntries = true)
    public void addUser(User user) {
        logger.info("update DB to save new user");

        userDAO.save(user);

        jmsTemplate.convertAndSend(usersQueue, user);
    }

如果需要从队列中获取消息,可以添加 JMSConsumer.java

import com.example.MyFirstSpringBoot.model.User;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

@Component
public class JMSConsumer {

    @JmsListener(destination = "testing.users")
    public void receiveQueue(User user) {
        System.out.println("Consumer a user from message queue. ID = " + user.getId() + ", Name = " + user.getName());
    }

}

测试效果,启动项目,添加两个用户,可以看到日志如下:


消息被读取

在浏览器中输入:http://127.0.0.1:8161/admin/ 默认用户名密码都为 admin。

ActiveMQ 控制台

3.13 单元测试

如果使用spring-boot-starter-test ‘Starter’(在 test scope 内),你将发现下列被提供的库:

  • JUnit - 事实上的(de-facto)标准,用于Java应用的单元测试。
  • Spring Test & Spring Boot Test  - 对Spring应用的集成测试支持。
  • AssertJ - 一个流式断言库。
  • Hamcrest - 一个匹配对象的库(也称为约束或前置条件)。
  • Mockito - 一个Java模拟框架。
  • JSONassert - 一个针对JSON的断言库。
  • JsonPath - 用于JSON的XPath。

你可以使用 @SpringBootTestwebEnvironment 属性定义怎么运行测试:

  • MOCK - 加载WebApplicationContext,并提供一个mock servlet环境,使用该注解时内嵌servlet容器将不会启动。如果classpath下不存在servlet APIs,该模式将创建一个常规的non-web ApplicationContext。
  • RANDOM_PORT - 加载EmbeddedWebApplicationContext,并提供一个真实的servlet环境。使用该模式内嵌容器将启动,并监听在一个随机端口。
  • DEFINED_PORT - 加载EmbeddedWebApplicationContext,并提供一个真实的servlet环境。使用该模式内嵌容器将启动,并监听一个定义好的端口(比如application.properties中定义的或默认的8080端口)。
  • NONE - 使用SpringApplication加载一个ApplicationContext,但不提供任何servlet环境(不管是mock还是其他)。
    注 不要忘记在测试用例上添加 @RunWith(SpringRunner.class),否则该注解将被忽略。

首先,我们修改 MyFirstSpringBootApplicationTests.java 来做 Smoke Test:

import com.example.MyFirstSpringBoot.controller.UserController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Java6Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyFirstSpringBootApplicationTests {

    @Autowired
    private UserController userController;

    @Test
    public void contextLoads() {
        assertThat(userController).isNotNull();
    }

}

可以使用 @WebMvcTest 检测 Spring MVC 控制器是否工作正常,该注解将自动配置 Spring MVC 设施,并且只扫描注解 @Controller@ControllerAdvice@JsonComponentFilterWebMvcConfigurerHandlerMethodArgumentResolver 的 beans,其他常规的 @Component beans 将不会被扫描。
通常 @WebMvcTest 只限于单个控制器(controller)使用,并结合 @MockBean 以提供需要的协作者(collaborators)的 mock 实现。@WebMvcTest 也会自动配置 MockMvc,Mock MVC 为快速测试MVC 控制器提供了一种强大的方式,并且不需要启动一个完整的 HTTP 服务器。
Spring Boot 提供一个 @MockBean 注解,可用于为 ApplicationContext 中的 bean 定义一个 Mockito mock,你可以使用该注解添加新 beans,或替换已存在的bean定义。

我们编写一个 UserControllerTest.java 类来测试 UserController

UserControllerTest.java

import com.example.MyFirstSpringBoot.model.User;
import com.example.MyFirstSpringBoot.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import java.util.Arrays;
import java.util.List;

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {
    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserService userService;

    @Test
    public void testExample() throws Exception {
        List<User> mockUpData = Arrays.asList(new User(1, "Test User 1"), new User(2, "Test User 2"));

        given(this.userService.getUsers())
                .willReturn(mockUpData);

        this.mvc.perform(get("/user/list_user").accept(MediaType.TEXT_PLAIN))
                .andExpect(status().isOk());
    }
}

最后通过 mvn test 来启动测试:

启动测试

3.14 安全

SpringSecurity是专门针对基于Spring项目的安全框架,充分利用了依赖注入和 AOP 来实现安全管控。

pom.xml 中添加 Spring Security 的依赖:

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

如果添加了 Spring Security 的依赖,那么 web 应用默认对所有的HTTP路径(也称为终点,端点,表示API的具体网址)使用 'basic' 认证。为了给 web 应用添加方法级别(method-level)的保护,你可以添加@EnableGlobalMethodSecurity 并使用想要的设置。

此时,重启服务,访问:http://localhost:8080/,Spring Security 会为我们提供一个默认的登录页面,界面如下:

默认的登录页面

默认用户名为 user,默认密码可以在启动日志中看到:

2018-06-25 10:22:14.546 [main] INFO  o.s.b.a.s.s.UserDetailsServiceAutoConfiguration -

Using generated security password: 2b1ce4e0-1603-4bee-8fd9-579917912158

用户名和密码也可以在 application.properties 中自定义配置:

spring.security.user.name=admin
spring.security.user.password=admin

默认的安全配置是通过 SecurityAutoConfigurationSpringBootWebSecurityConfiguration(用于web安全),AuthenticationManagerConfiguration(可用于非web应用的认证配置)进行管理的。

你可以添加一个 @EnableWebSecurity bean来彻底关掉 Spring Boot 的默认配置。

想要覆盖访问规则而不改变其他自动配置的特性,你可以添加一个注解@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)WebSecurityConfigurerAdapter 类型的 @Bean

这里我们希望添加如下的权限控制功能:

  • 访问首页 /,不需要验证
  • 访问静态资源,例如图片,CSS等,不需要验证
  • 访问用户列表 /list_user 和添加用户页面 /add_user需要验证

新建一个类 ApplicationSecurity.java

package com.example.MyFirstSpringBoot.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http.authorizeRequests()
                .antMatchers("/", "/images/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login").loginPage("/login").permitAll()
                .and()
                .logout().permitAll();

                http.csrf().disable();
        // @formatter:on
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .inMemoryAuthentication()
                .withUser("user").password("{noop}user").roles("USER")
                .and()
                .withUser("admin").password("{noop}admin").roles("USER", "ADMIN");
    }
}
  • 通过 @EnableWebSecurity 注解开启 Spring Security 的功能
  • 继承 WebSecurityConfigurerAdapter,并重写它的方法来设置一些 web 安全的细节
  • configure(HttpSecurity http)方法
    • 通过 authorizeRequests() 定义哪些URL需要被保护、哪些不需要被保护。
    • 通过 formLogin() 定义当需要用户登录时候,转到的登录页面。
  • configureGlobal(AuthenticationManagerBuilder auth) 方法,在内存中创建了用户。

然后创建一个我们自己的登陆页面 login.ftl

<!DOCTYPE html>
<html>
<body>
    <#if RequestParameters['error']??>
        <div style="color: red">用户名或者密码错误。</div>
    <#elseif RequestParameters['logout']??>
        <div style="color: red">您已成功退出登陆。</div>
    </#if>

    <form name="f" action="/login" method="post">
        User Name: <input type="text" name="username" /><br/>
        Password: <input type="password" name="password" /><br/>
        <input type="submit">
    </form>
</body>
</html>

同时,修改 WelcomeController.java,增加 /login 的 mapping:

    @RequestMapping(value = "/login", method = RequestMethod.GET)
    public String login(Map<String, Object> model) {
        return "login";
    }

到这里,我们启用应用,并访问 http://localhost:8080/,可以正常访问。
但是访问 http://localhost:8080/list_user 或者 http://localhost:8080/add_user 的时候被重定向到了 http://localhost:8080/login 页面:

如果想要停止 Security,在 ApplicationSecurityConfig.java 中将 .antMatchers("/", "/images/**").permitAll() 修改为 .antMatchers("/**").permitAll()

3.15 事务控制

从如下的代码中,我们可以看出,在添加用户的逻辑中,首先往数据库插入一条记录,然后往消息队列发送一条消息。

@CacheEvict(value = "users", allEntries = true)
public void addUser(User user) {
    logger.info("update DB to save new user");

    userDAO.save(user);

    jmsTemplate.convertAndSend(usersQueue, user);
}

现在,我们通过 /activemq stop 命令停止掉 ActiveMQ 服务。然后尝试在页面上添加一个用户,在点击提交后,跳转到错误页面,并且后台日志也会显示相关错误信息:

错误页面

同时查询数据库,我们发现该新用户被成功的插入到数据库中。
现在我们希望,如果没有成功发送消息到队列中 jmsTemplate.convertAndSend(usersQueue, user);,该新用户也不能被插入到数据库中 userDAO.save(user);

Spring 的 AOP 即声明式事务管理默认是针对 unchecked exception 回滚,也就是默认对RuntimeException 异常或是其子类进行事务回滚;checked 异常,即 Exceptiontry{} 捕获的不会回滚。

在这个例子中,我们在方法前添加 @Transactiona 注解,默认的传播机制是 Propagation.REQUIRED,即创建一个新的事务。
在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,这个代理对象决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor 来使用拦截,在 TransactionInterceptor 拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑,最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务。

@CacheEvict(value = "users", allEntries = true)
@Transactional
public void addUser(User user) {

3.16 集成

Spring Boot 为 Spring 集成提供了一些便利,包括 spring-boot-starter-integration ‘Starter’。
Spring 集成提供基于消息和其他传输协议的抽象,比如HTTP,TCP等。
一个示例请参见 https://spring.io/guides/gs/integration/

3.17 Session 会话

Spring Boot 为 Spring Session 自动配置了各种存储:

  • JDBC
  • MongoDB
  • Redis
  • Hazelcast
  • HashMap

在这个例子中,我们利用 Redis 透明的存储并共享 Web 应用的 HttpSession
pom.xml 中添加 Spring Session 的依赖:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

创建一个 HttpSessionConfig.java

package com.example.MyFirstSpringBoot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@EnableRedisHttpSession
@Configuration
public class HttpSessionConfig {
}

添加 @EnableRedisHttpSession 注解即可,该注解会创建一个名字叫 springSessionRepositoryFilter 的 Spring Bean,其实就是一个 Filter,这个 Filter 负责用 Spring Session 来替换原先的默认 HttpSession实现,在这个例子中,Spring Session 是用 Redis 来实现的 RedisHttpSession

如何操作 Spring Session,可以通过如下方式:

@RequestMapping("/")
public String welcome(Map<String, Object> model, HttpServletRequest req) {

    req.getSession().setAttribute("testKey", "testValue");

    logger.info(req.getSession().getAttribute("testKey").toString());

    logger.info("welcome page");

    model.put("welcomeMessage", welcomeMessage);
    return "welcome";
}

下面我们通过 redis-cli 进入 Redis 命令行。
通过 keys * 可以看到相关的 Sessions:

127.0.0.1:6379> keys *
1) "spring:session:expirations:1530001440000"
2) "users::com.example.MyFirstSpringBoot.service.UserService-getUsers"
3) "spring:session:expirations:1530002760000"
4) "spring:session:sessions:3c23beb2-ea94-409e-a78c-02afa41b9e1f"
5) "spring:session:sessions:expires:3c23beb2-ea94-409e-a78c-02afa41b9e1f"
6) "spring:session:sessions:79e1809f-b0a0-4d72-a9a6-17dfdde94afe"
7) "spring:session:sessions:expires:79e1809f-b0a0-4d72-a9a6-17dfdde94afe"

然后可以通过 hkeys 来查看具体 Session 里面包含的属性:

27.0.0.1:6379> hkeys "spring:session:sessions:3c23beb2-ea94-409e-a78c-02afa41b9e1f"
1) "maxInactiveInterval"
2) "creationTime"
3) "sessionAttr:testKey"
4) "lastAccessedTime"

然后可以通过 hget 来查看具体属性的值:

27.0.0.1:6379> hget "spring:session:sessions:3c23beb2-ea94-409e-a78c-02afa41b9e1f" "sessionAttr:testKey"
"\xac\xed\x00\x05t\x00\ttestValue"

3.18 执行器 Actuator:Production-ready特性

Spring Boot 包含很多其他特性,可用来帮你监控和管理发布到生产环境的应用。你可以选择使用HTTP端点,JMX,甚至通过远程 shell 来管理和监控应用。审计(Auditing),健康(health)和数据采集(metrics gathering)会自动应用到你的应用。
spring-boot-actuator 模块提供Spring Boot所有的production-ready特性,启用该特性的最简单方式是添加spring-boot-starter-actuator ‘Starter’依赖。

pom.xml 中添加 Actuator 的依赖:

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

3.19 监控

执行器端点(endpoints)可用于监控应用及与应用进行交互,Spring Boot 包含很多内置的端点,你也可以添加自己的。参见 https://qbgbook.gitbooks.io/spring-boot-reference-guide-zh/V.%20Spring%20Boot%20Actuator/46.%20Endpoints.html
application.properties 中添加如下配置:

management.endpoints.web.exposure.include=*

重启应用,访问 http://localhost:8080/actuator/,可以看到如下 response:

{
   "_links":{
      "self":{
         "href":"http://localhost:8080/actuator",
         "templated":false
      },
      "auditevents":{
         "href":"http://localhost:8080/actuator/auditevents",
         "templated":false
      },
      "beans":{
         "href":"http://localhost:8080/actuator/beans",
         "templated":false
      },
      "health":{
         "href":"http://localhost:8080/actuator/health",
         "templated":false
      },
      "conditions":{
         "href":"http://localhost:8080/actuator/conditions",
         "templated":false
      },
      "configprops":{
         "href":"http://localhost:8080/actuator/configprops",
         "templated":false
      },
      "env":{
         "href":"http://localhost:8080/actuator/env",
         "templated":false
      },
      "env-toMatch":{
         "href":"http://localhost:8080/actuator/env/{toMatch}",
         "templated":true
      },
      "info":{
         "href":"http://localhost:8080/actuator/info",
         "templated":false
      },
      "loggers":{
         "href":"http://localhost:8080/actuator/loggers",
         "templated":false
      },
      "loggers-name":{
         "href":"http://localhost:8080/actuator/loggers/{name}",
         "templated":true
      },
      "heapdump":{
         "href":"http://localhost:8080/actuator/heapdump",
         "templated":false
      },
      "threaddump":{
         "href":"http://localhost:8080/actuator/threaddump",
         "templated":false
      },
      "metrics":{
         "href":"http://localhost:8080/actuator/metrics",
         "templated":false
      },
      "metrics-requiredMetricName":{
         "href":"http://localhost:8080/actuator/metrics/{requiredMetricName}",
         "templated":true
      },
      "scheduledtasks":{
         "href":"http://localhost:8080/actuator/scheduledtasks",
         "templated":false
      },
      "sessions-sessionId":{
         "href":"http://localhost:8080/actuator/sessions/{sessionId}",
         "templated":true
      },
      "sessions":{
         "href":"http://localhost:8080/actuator/sessions",
         "templated":false
      },
      "httptrace":{
         "href":"http://localhost:8080/actuator/httptrace",
         "templated":false
      },
      "mappings":{
         "href":"http://localhost:8080/actuator/mappings",
         "templated":false
      }
   }
}

健康信息

健康信息可以检查应用的运行状态,它经常被监控软件用来提醒人们生产环境是否存在问题。/health 端点暴露的默认信息取决于端点是如何被访问的。健康信息是从你的 ApplicationContext 中定义的所有 HealthIndicator beans 收集过来的。Spring Boot包含很多自动配置的 HealthIndicators,参见 https://qbgbook.gitbooks.io/spring-boot-reference-guide-zh/V.%20Spring%20Boot%20Actuator/46.6.1%20Auto-configured-HealthIndicators.html
通过 http://localhost:8080/actuator/health 来查看健康信息:

健康信息

为了查看完整的健康信息,可以在 application.properties 中添加如下配置:

management.endpoint.health.show-details=ALWAYS

再次访问 http://localhost:8080/actuator/health,返回的内容如下:

{
   "status":"UP",
   "details":{
      "diskSpace":{
         "status":"UP",
         "details":{
            "total":120124866560,
            "free":64647090176,
            "threshold":10485760
         }
      },
      "redis":{
         "status":"UP",
         "details":{
            "version":"4.0.10"
         }
      },
      "jms":{
         "status":"UP",
         "details":{
            "provider":"ActiveMQ"
         }
      },
      "db":{
         "status":"UP",
         "details":{
            "database":"H2",
            "hello":1
         }
      }
   }
}

也可以写自己的,例如我们创建一个 ExampleHealthIndicator.java:。你需要实现 health() 方法,并返回一个 Health 响应,该响应需要包含一个 status 和其他用于展示的详情。

package com.example.MyFirstSpringBoot.monitor;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class ExampleHealthIndicator implements HealthIndicator {

    @Override
    public Health health() {
        return Health.up().withDetail("counter", 123).build();
    }

}

再次访问 http://localhost:8080/actuator/health,返回的内容包含如下:

      "example":{
         "status":"UP",
         "details":{
            "counter":123
         }
      }

应用信息

应用信息会暴露所有 InfoContributor beans收集的各种信息,Spring Boot 包含很多自动配置的InfoContributors,你也可以编写自己的实现。
例如我们创建一个 ExampleInfoContributor.java

package com.example.MyFirstSpringBoot.monitor;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;

import java.util.Collections;

@Component
public class ExampleInfoContributor implements InfoContributor {

    @Override
    public void contribute(Info.Builder builder) {
        builder.withDetail("example", Collections.singletonMap("someKey", "someValue"));
    }

}

访问 http://localhost:8080/actuator/info,返回的内容包含如下:

image.png

3.20 度量指标(Metrics)

访问 http://localhost:8080/actuator/metrics,会得到如下结果:

{
   "names":[
      "jvm.memory.max",
      "http.server.requests",
      "jdbc.connections.active",
      "process.files.max",
      "jvm.gc.memory.promoted",
      "tomcat.cache.hit",
      "system.load.average.1m",
      "tomcat.cache.access",
      "jvm.memory.used",
      "jvm.gc.max.data.size",
      "jdbc.connections.max",
      "jdbc.connections.min",
      "jvm.gc.pause",
      "jvm.memory.committed",
      "system.cpu.count",
      "logback.events",
      "tomcat.global.sent",
      "jvm.buffer.memory.used",
      "tomcat.sessions.created",
      "jvm.threads.daemon",
      "system.cpu.usage",
      "jvm.gc.memory.allocated",
      "tomcat.global.request.max",
      "hikaricp.connections.idle",
      "hikaricp.connections.pending",
      "tomcat.global.request",
      "tomcat.servlet.request.max",
      "tomcat.sessions.expired",
      "hikaricp.connections",
      "tomcat.servlet.request",
      "jvm.threads.live",
      "jvm.threads.peak",
      "tomcat.global.received",
      "hikaricp.connections.active",
      "hikaricp.connections.creation",
      "process.uptime",
      "tomcat.sessions.rejected",
      "process.cpu.usage",
      "tomcat.threads.config.max",
      "jvm.classes.loaded",
      "tomcat.servlet.error",
      "hikaricp.connections.max",
      "hikaricp.connections.min",
      "jvm.classes.unloaded",
      "tomcat.global.error",
      "tomcat.sessions.active.current",
      "tomcat.sessions.alive.max",
      "jvm.gc.live.data.size",
      "hikaricp.connections.usage",
      "tomcat.threads.current",
      "hikaricp.connections.timeout",
      "process.files.open",
      "jvm.buffer.count",
      "jvm.buffer.total.capacity",
      "tomcat.sessions.active.max",
      "hikaricp.connections.acquire",
      "tomcat.threads.busy",
      "process.start.time"
   ]
}

选择其中一个指标,例如 tomcat.global.request,访问 http://localhost:8080/actuator/metrics/tomcat.global.request,可以得到该指标对应的值:

{
   "name":"tomcat.global.request",
   "measurements":[
      {
         "statistic":"COUNT",
         "value":13.0
      },
      {
         "statistic":"TOTAL_TIME",
         "value":1.46
      }
   ],
   "availableTags":[
      {
         "tag":"name",
         "values":[
            "http-nio-8080"
         ]
      }
   ]
}

记录自己的指标:
CounterServiceGaugeService 注入到你的 bean 中可以记录自己的度量指标:CounterService暴露 incrementdecrementreset 方法;GaugeService 提供一个 submit 方法。

注意:In Spring Boot 2.0, the in-house metrics were replaced with Micrometer support. Thus, we can expect breaking changes. If our application was using metric services such as GaugeService or CounterService they will no longer be available.
Instead, we’re expected to interact with Micrometer directly. In Spring Boot 2.0, we’ll get a bean of type *MeterRegistry *autoconfigured for us.

3.21 定时任务

关于定时任务,详细请参见 Java 定时任务 & 任务调度

SpringBoot 内置了定时任务 @Scheduled,操作可谓特别简单。

假设我们有有一个新的需求,每天晚上24点,给用户发一个促销邮件。
首先在启动类 MyFirstSpringBootApplication 上增加一个注解 @EnableScheduling
随后创建一个任务类 PromotionMailTask.java

package com.example.MyFirstSpringBoot.task;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class PromotionMailTask {
    private final static Logger logger = LoggerFactory.getLogger(PromotionMailTask.class);

    @Scheduled(cron = "0 0 24 * * ?")
    public void scheduledPromotionMailTask() {
        logger.info("sending promotion mail..");
    }

}

关于 con 的参数设置,请参考 https://www.cnblogs.com/softidea/p/5833248.html

更多阅读


引用:
Spring Boot参考指南
Spring Boot

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

推荐阅读更多精彩内容