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配置。
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
下。
2. 一个 Spring Boot 示例 MyFirstSpringBoot
通过 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
命令,可以看到该项目具体的依赖树:
修改 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
来启动这个项目:
可以看出,
run
是定义在 spring-boot-maven-plugin
这个依赖中的。
mvn spring-boot:run
可以看出启动了 8080 端口 ,因此可以通过 来访问:
通过 http://localhost:8080/ 来访问
我们也可以通过 mvn package
创建可执行 jar 包,并通过 jar 包来运行启动:
3. 扩展成一个 Web MVC 的应用
现在我们一步一步扩展上面的项目,并将其扩展为一个完整的 Web MVC 的应用。
注意,请将程序入口 MyFirstSpringBootApplication.java
放在 src 的根目录,如下所示,这样 Spring 就会扫描根目录及所有的子包,例如 /controller
,/model
,/service
。
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
:
<!DOCTYPE html>
<%@ taglib prefix="spring"
<%@ taglib prefix="c"
<html lang="en">
<body>
${welcomeMessage}
</body>
</html>
新建第一个 WelcomeController.java
,新建一个 package 名为 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
:
<!DOCTYPE html>
<html>
<body>
${welcomeMessage}
</body>
</html>
最后删除文件 welcome.jsp
,并且删除 resources/application.properties
文件中关于 Spring MVC 的两行配置。
3.4 错误处理
Spring Boot 默认提供一个 /error
映射用来以合适的方式处理所有的错误,并将它注册为 servlet 容器中全局的 错误页面。对于浏览器客户端,它会产生一个白色标签样式(whitelabel)的错误视图,该视图将以 HTML 格式显示同样的数据,默认的 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的类名(缩写)。
- 日志信息。
这里我们选择集成 Logback。Spring Boot包含很多有用的Logback扩展,你可以在 logback-spring.xml
配置文件中使用它们。
首先在 /resources
目录下新建一个文件 logback-spring.xml
<?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
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.ftl
和 add_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 连接数据库
<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.sql
和 user_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 的控制台
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
,实现了一些常用的方法:findone
,findall
,save
等。我们新建一个 UserDAO.java
来进行数据库的交互:
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
<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);
}
}
只进行了一次数据库的查询操作
通过 Redis CLI 可以看到结果已缓存到 Redis 中:其中 key 的生成方式是我们在 RedisConfig.java
中自定义的。
3.11 数据验证
在添加用户页面时,如果我们在 ID 输入框输入一个字母,会发生什么?
在 ID 输入框输入一个字母 错误信息
现在我们希望实现如下的功能:
- 增加校验
-
id
只能是数字,并且只能为正数 -
name
不能为空
-
- 如果校验不通过,返回添加用户页面,并且现实相关错误信息
在 pom.xml
中添加 Validation 的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@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)
的时候,发送一条消息给队列。
在 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
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
@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());
}
}
测试效果,启动项目,添加两个用户,可以看到日志如下:
消息被读取 ActiveMQ 控制台
3.13 单元测试
如果使用spring-boot-starter-test
‘Starter’(在 test
scope 内),你将发现下列被提供的库:
- - 事实上的(de-facto)标准,用于Java应用的单元测试。
- & Spring Boot Test - 对Spring应用的集成测试支持。
- - 一个流式断言库。
- - 一个匹配对象的库(也称为约束或前置条件)。
- - 一个Java模拟框架。
- - 一个针对JSON的断言库。
- - 用于JSON的XPath。
你可以使用 @SpringBootTest
的 webEnvironment
属性定义怎么运行测试:
-
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
,@JsonComponent
,Filter
,WebMvcConfigurer
和 HandlerMethodArgumentResolver
的 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
:
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
并使用想要的设置。
默认用户名为
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
默认的安全配置是通过 SecurityAutoConfiguration
,SpringBootWebSecurityConfiguration
(用于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";
}
- 如果用户名密码错误,跳转到
- 如果用户名密码正确,跳转到响应的页面
- 也可以通过访问 来退出登陆
如果想要停止 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 异常,即 Exception
可 try{}
捕获的不会回滚。
在这个例子中,我们在方法前添加 @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 集成
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 监控
management.endpoints.web.exposure.include=*
{
"_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
}
}
}
健康信息
健康信息为了查看完整的健康信息,可以在
application.properties
中添加如下配置:
management.endpoint.health.show-details=ALWAYS
{
"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
@Component
public class ExampleHealthIndicator implements HealthIndicator {
@Override
public Health health() {
return Health.up().withDetail("counter", 123).build();
}
}
"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
import java.util.Collections;
@Component
public class ExampleInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example", Collections.singletonMap("someKey", "someValue"));
}
}
image.png
3.20 度量指标(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",
"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"
]
}
{
"name":"tomcat.global.request",
"measurements":[
{
"statistic":"COUNT",
"value":13.0
},
{
"statistic":"TOTAL_TIME",
"value":1.46
}
],
"availableTags":[
{
"tag":"name",
"values":[
"http-nio-8080"
]
}
]
}
记录自己的指标:
将 CounterService
或 GaugeService
注入到你的 bean 中可以记录自己的度量指标:CounterService
暴露 increment
,decrement
和 reset
方法;GaugeService
提供一个 submit
方法。
3.21 定时任务
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
@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..");
}
}
更多阅读
- Spring Boot 官方示例:
- Spring Boot 官方教程: