Spring Boot 不是一门新技术,所以不用紧张。从本质上来说,Spring Boot 就是 Spring,它做了那些没有它你也会去做的 Spring 配置。它使用“约定优于配置”(内置了一个习惯性的配置,让你无需手动进行配置)的理念让你的项目快速运行起来。使用 Spring Boot 很容易创建一个独立运行(运行 jar,内嵌 Servlet 容器)、准生产级别的基于 Spring 框架的项目,使用 Spring Boot 你可以不用或者只需要很少的 Spring 配置。
SpringBoot 浅析
SpringBoot 之所以会诞生,是因为 Spring 框架的配置越来越复杂了,不适合“快速开发”,就拿 spring mvc 项目来说,至少需要两个 xml 配置文件:
web.xml
:servlet 容器的配置文件(拦截所有请求到 spring mvc servlet,以及配置一个 UTF-8 字符编码过滤器)mvc.xml
:springmvc 的配置文件(负责将请求交给 Controller 处理,处理之后再交给 ViewResolver 去渲染页面)
当然,从 spring 3 开始,也提供了完全基于 javaconfig 的配置方式,即:
web.xml
->WebConfig.java
:从 xml 形式变为了 javaconfig 形式mvc.xml
->MvcConfig.java
:从 xml 形式变为了 javaconfig 形式
但是配置的复杂度依旧没有降低,只是 javaconfig 可以避免 xmlconfig 的“非类型安全”缺点而已啦(IDEA 无所畏惧,xmlconfig 也能很好的提示)。
而现在又流行什么“微服务、云计算”,虽然我对这些也不是太懂,但是也大概了解了一下,我们先来说说“微服务”,微服务其实就是将一个复杂的大项目拆分为多个小的项目(每个小项目称为一个“服务”),不同组件之间使用 restful-api 或 rpc 进行交互,这样每个组件都可以使用不同的技术栈来开发,比如组件 A 可以使用 java 来开发,组件 B 可以使用 python 来开发,组件 C 可以使用 golang 来开发,因为这些都没有任何影响,各个模块之间的交互都是通过 restful-api 来进行的,它屏蔽了底层的开发语言以及开发框架的差异。
微服务的概念
微服务是一种软件架构风格,一个大型复杂的软件系统应该由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务,不同微服务之间通常使用语言无关、平台无关的 API 进行通信,如 HTTP API(RESTful API)。在所有情况下,每个任务代表着一个小的业务能力。微服务的概念源于 2014 年 3 月 Martin Fowler 所写的一篇文章 Microservices。尽管“微服务”这种架构风格没有精确的定义,但其具有一些共同的特性,如围绕业务能力组织服务、自动化部署、智能端点、对语言及数据的“去集中化”控制等等。
微服务的由来
微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
为什么需要微服务
在传统的 IT 行业,软件大多都是各种独立系统的堆砌,这些系统的问题总结来说就是扩展性差,可靠性不高,维护成本高。到后面引入了 SOA 服务化,但是,由于 SOA 早期均使用了总线模式,这种总线模式是与某种技术栈强绑定的,比如:J2EE。这导致很多企业的遗留系统很难对接,切换时间太长,成本太高,新系统稳定性的收敛也需要一些时间。最终 SOA 看起来很美,但却成为了企业级奢侈品,中小公司都望而生畏。
而微服务又有点“快速开发”的意思,说到快速开发,传统的 spring mvc 程序的痛点就出来了,一大堆繁琐的 xmlconfig/javaconfig 配置,光是搭建一个基本的开发环境就得半天,因为有很多模板式的配置文件,总是复制粘贴,然后修改,效率太低了,不能让我们开发人员专注于写业务代码(必须先专注于写配置文件这些东西)。
于是,spring boot 诞生了,spring boot 不同于 spring framework 的其它模块,实际上 spring boot 并没有引入什么新的功能,springboot 的使命是为了实现“spring应用程序的快速开发”,所以不用害怕,spring boot 只是一颗大的语法糖,帮助你快速搭建一个 spring 应用程序开发环境,而业务代码什么的还是你自己写,没有什么变化,变化的仅仅是 spring 框架的一系列配置(简化了许多)。
那么 springboot 是如何简化 spring 框架的繁琐复杂的配置的呢?核心就是“自动配置”,自动配置本质上还是 spring 4 引入的 条件化创建 bean 功能,所以 spring boot 其实也没有提供什么“自动配置魔法”,只不过将 spring 4 的条件化创建 bean 运用的炉火纯青罢了,所以其实也没什么神奇的。
当然,spring boot 除了简化 spring 框架的初始化配置之外,还有另一个贡献,那就是 starter,所谓 starter 就是一组依赖,springboot 将日常开发的各种框架进行了分类汇总,比如 web starter 就表示 web 开发需要的依赖项目,我们只需要引入这个 web starter 依赖,就可以一键获取到与 web 开发相关的那些依赖项目,如 spring-webmvc、validator、slf4j、logback 等,这样我们就不需要一个一个的配置需要的依赖了,非常方便。使用 starter 依赖组还有另一个好处,那就是我们不需要在关心不同依赖之间的版本冲突问题了,因为一组依赖里面的依赖项的版本都是经过 spring boot 实际测试出来的,是最佳组合,因此也不建议手动更改依赖组里面的依赖项版本号,否则可能引发传递依赖的版本冲突问题,这纯属是自找麻烦。
另外,springboot 还提供了一个令人欣喜的功能:直接将传统的 webapp 项目打包为一个可执行的 jar 包,这样使用 maven 打包之后,就可以直接使用 java -jar my-web-project.jar
来运行我们的应用,而不需要单独的 servlet 容器,简直不要太爽了。当然 springboot 也允许将我们的项目打包为 war 包,然后将这个 war 包放到传统的 servlet 容器中使用,一般在开发环境中,打包为 jar 包比较合适,因为方便嘛;而在生产环境中,打包为 war 可能比较合适,当然这些都不重要,看个人的喜好。默认情况下,springboot 使用 tomcat 作为嵌入式 servlet 容器,你也可以将其替换为 jetty。其实仔细想想,将 webapp 打包为一个可执行 jar 也没什么稀奇的,因为 tomcat 实际上也就是一个普通的 java 项目,而 tomcat 也有对应的嵌入式版本,所谓嵌入式版本就是直接在 java 程序中启动的 tomcat,而不是作为一个单独的服务运行的 tomcat 进程。
自动配置、条件化创建 bean
springboot 的核心是“自动配置”,而自动配置的本质是“条件化创建bean”。在 spring 4 之前,xmlconfig/javaconfig 中的 <bean>
、@Bean
都是无条件创建的,只要你配置了 bean 元素、@Bean 方法,那么对应的 bean 就会被创建。这显然有点原始了,很多时候,为了完成稍微复杂一些的配置任务,都需要使用条件化创建 bean 功能,好在 spring 4 之后,提供了相关的注解以及接口:
@Conditional
:条件化创建注解,可以标注 @Configuration 配置类以及 @Configuration 配置类中的 @Bean 方法,该注解有一个 value 属性,用于指定进行条件判断的实现类(Condition 接口的实现类,可以指定多个),只有所有的 Condition 对象的 matches() 方法都返回 true 才表示条件成立,才会去执行当前 @Configuration 或 @Configuration 中的 @Bean 方法。Condition
:条件判断的接口,只有一个方法,即 matches(),它是一个函数式接口,如果 matches() 方法返回 true,则表示当前条件成立,如果 matches() 方法返回 false,则表示当前条件不成立。
Spring 4 提供了很多 Condition 接口实现,方便我们开发“条件化创建bean”的配置类(条件化配置),如:
- @ConditionalOnBean:如果 context 中存在指定 bean,则条件成立
- @ConditionalOnMissingBean:如果 context 中不存在指定 bean,则条件成立
- @ConditionalOnClass:如果 classpath 中存在指定 Class,则条件成立
- @ConditionalOnMissingClass:如果 classpath 中不存在指定 Class,则条件成立
- @ConditionalOnExpression:如果给出的 SpEL 表达式结果值为 true,则条件成立
借助这些内置 Condition 条件判断类 + 自定义实现的条件判断类,就可以非常方便的实现:Spring 框架的条件化、自动化配置了。这其实就是 Spring Boot 自动配置的原理所在。
传统的 SSM 项目结构
- pom.xml(每个依赖都要自己导入)
- web.xml/WebConfig.java
- mvc.xml/MvcConfig.java
- Employee.java
- EmployeeMapper.java
- EmployeeMapper.xml
- EmployeeService.java
- EmployeeServiceImpl.java
- EmployeeController.java
- employee-list.jsp
- employee-edit.jsp
SpringBoot 版本的 SSM
- pom.xml(直接导入 starter 依赖组)
- Application.java(配置类&启动类)
- Employee.java
- EmployeeMapper.java
- EmployeeMapper.xml
- EmployeeService.java
- EmployeeServiceImpl.java
- EmployeeController.java
- employee-list.ftl
- employee-edit.ftl
可以发现,变化的就是“配置部分”,换为 springboot 后,pom.xml 简化了,WebConfig.java 和 MvcConfig.java 不需要了,取而代之的是 Application.java 配置类&启动类。另外,SpringBoot 不建议使用 JSP 视图解析器,因为打包为 jar 包时,运行是有问题的,即使打包为 war 包也可能会出现不可预知的问题,而且 JSP 不支持 SpringBoot 的很多特性,所以建议使用 FreeMarker、Thymeleaf 这些专业的 Java 模板引擎。这里我就选用 FreeMarker,FreeMarker 的模板文件后缀名通常为 .ftl
,但是这个变化其实是无关紧要的,因为使用什么模板引擎其实没什么关系,很多公司的项目也是使用的 FreeMarker 这些模板引擎,而不是 JSP。
pom.xml 的变化这里就先不说了,后面会详细讲解;我们来看看变化最明显的 WebConfig&MvcConfig => Application。
WebConfig(web.xml 的 javaconfig 版本)
MvcConfig(mvc.xml 的 javaconfig 版本)
Application(MvcConfig 的简化版本)
因为 WebConfig 的任务非常简单,简单的说,就是两个东西:
- 注册 springmvc dispatcher servlet,拦截所有请求到 springmvc 处理;
- 注册 charset filter,将 request 和 response 的字符编码规范为 UTF-8。
基本上每个 SpringMVC 程序都是这样配置的,不怎么变化,也不需要怎么自定义配置,所以 SpringBoot 干脆将这些配置“内置化“,即默认就配置了这些东西,我们无需关心。所以,Application 和 MvcConfig 是相同的作用,都是 Spring 的配置文件。
可以发现,Application 中我们什么都没有配置,只是在上面加了一个 @SpringBootApplication
注解,就完成了 MvcConfig 里面的全部配置(甚至是更多配置,因为 SpringBoot 的核心功能 - “自动配置”)。那么这里面究竟是什么黑魔法呢?为什么只需要一个 SpringBootApplication 注解,然后一个普通的 main 启动方法就可以直接运行传统的 spring mvc 应用程序呢?
先冷静一下,我们知道,注解和 XML 都是提供元数据的一种方式,如果你没有相应的代码去处理这些 注解/XML 元数据,那么这些 注解/XML 实际上不会有任何作用,就相当于没有使用这个注解一样。OK,理解到这一点之后,我们再来分析一下 SpringBoot 程序启动的原理和流程。
首先映入眼帘的就是 Application 启动类中的 main() 方法:
当我们运行这个 main() 方法的时候,执行的就是 SpringApplication
的 run()
静态方法,run() 方法的第一个参数是当前启动类的 Class 对象,第二个参数是 main() 方法接收到的命令行参数(字符串数组,args)。
那么我们先来看看这个 run() 方法内部做了什么操作(仅列举核心操作):
- 如果我们调用的是静态 run() 方法,则该方法内部会首先创建 SpringApplication 实例,再调用该实例的 run() 方法。
- 根据当前 classpath 是否存在 servlet 相关的类,决定创建 Web 类型的 ApplicationContext 还是普通的 ApplicationContext。
- 最核心的一步,将之前通过 @EnableAutoConfiguration 获取的所有 @Configuration 配置加载到已经准备完毕的 ApplicationContext。
这其中最重要的还是第三步,不过这个 @EnableAutoConfiguration
是什么注解?哪来的?别急,我们来看看另一个重要的东西:@SpringBootApplication
,这个注解其实是一个组合注解,声明如下:
前面的 4 个 jdk 内置的元注解就不说明了,以下省略;所以核心的注解是这 3 个:
先来看第一个注解,@SpringBootConfiguration
,其实就是 @Configuration
而已:
所以又可以简化为:
OK,现在我们来解析一下这些注解,@Configuration
注解大家都比较熟悉,这是 Spring 3 引入的注解,等价于一个 spring.xml 配置文件,也就是说,被 @Configuration 标注的类都是一个 Spring 配置类(等价于一个 spring xml 配置文件),而 @ComponentScan
注解也是 Spring 3 引入的注解,等价于 spring 配置文件中的 <context:component-scan/>
元素,作用就是扫描指定 package(含子包)中的 @Component
、@Controller
、@Service
、@Repository
注解的 Bean 类,然后将它们注册到 bean 容器中,并处理这些 Bean 类上面的 Spring 注解,如 @Autowired
。注意,对于 @ComponentScan
注解,如果不指定 basePackage,那么默认就是当前所在的 package(含子包),所以一个很好的实践是,将 Application 启动类&配置类放到我们的顶级 package 下面,比如 com.zfl9 包下面,这样就可以不需要指定 basePackage 属性,“约定优于配置”。
那么最核心的注解其实就是 @EnableAutoConfiguration
,前面两个注解都是 Spring 3 中的注解,没什么可讲的。不知道大家有没有发现,Spring 的 @EnableXxx
注解都是 @Import(PartConfigurationClass.class)
注解来实现的,所以看到 @Enable*
就要将其看作为 @Import(Xxx.class)
注解,那么 @Import
注解的作用又是什么呢?还记得我们的 xml-based 配置文件么?如果 spring 配置文件很大,配置的内容很多,通常我们都会采取“分模块”的方法来拆分一个大的 spring 配置文件,然后在主配置文件上使用 <import resource="classpath:part-config.xml"/>
来引入子配置文件到当前配置文件中(合并为一个配置文件),而 @Import
的作用和 <import/>
标签的作用是一样的,都是用来导入其它的子配置文件上的配置内容,只不过 <import/>
导入的是 xml-based configuration,而 @Import
导入的是 java-based configuration。
那么我们来看看 @EnableAutoConfiguration
注解的声明是什么:
看吧,就是 @Import(AutoConfigurationImportSelector.class)
的封装注解而已,不过它上面还有一个 @AutoConfigurationPackage
注解,我们来看看这个 @AutoConfigurationPackage
注解是什么东西:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}
所以这还是一个 @Import
封装注解,那么组合起来,@EnableAutoConfiguration
其实就是这两个 @Import
的组合注解:
@Import(AutoConfigurationPackages.Registrar.class)
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}
所以,下面两个用法是完全一样的,没有区别(只是 AutoConfigurationPackages.Registrar 不是 public 访问权限的,所以会报错):
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@Configuration
@ComponentScan
@Import(AutoConfigurationImportSelector.class)
@Import(AutoConfigurationPackages.Registrar.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
AutoConfigurationPackages.Registrar
的 javadoc 说明是(作用暂时不清楚,尝试去除它启动会报错):
// 告诉 SpringBoot,被 `@AutoConfigurationPackage` 标注的类所在的包应该使用 AutoConfigurationPackages 注册。
Indicates that the package containing the annotated class should be registered with AutoConfigurationPackages.
先不管这个,我们来看看进一步精简版的 @SpringBootApplication
注解:
@Configuration
@ComponentScan
@Import(AutoConfigurationImportSelector.class)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
OK,代码已经很清楚了,Spring Boot 自动配置的关键所在就是 AutoConfigurationImportSelector
这个 sub-configuration 配置类。
而这个类内部就会自动查找 classpath:/META-INF/spring.factories
资源文件(会查找所有的同名资源文件,然后将它们合并为一个),spring.factories 有点类似于 JDK1.6 的 SPI 机制,spring.factories 文件其实就是普通的 java properties 属性文件,这个文件中,有一个 key 为 org.springframework.boot.autoconfigure.EnableutoConfiguration
的配置项,它指定的其实就是所有的 @Configuration
Spring 配置类(也就是所谓的自动配置类,因为它们上面有很多 @Conditional
条件配置注解),Spring Boot 内部会使用 SpringFactoriesLoader 工具类读取所有的 spring.factories 文件中的 org.springframework.boot.autoconfigure.EnableutoConfiguration
指定的所有 @Configuration 配置类,然后通过反射 API,实例化它们,将它们合并到当前 @SpringBootApplication
标注的 @Configuration 配置类中(汇总为一个,就相当于一个根 xmlconfig 配置文件中使用 <import>
标签引入所有的外部的 sub xmlconfig 配置文件一样),收集完毕之后,就像往常一样,初始化 Spring Context 容器,运行即可。所以这样看来,Spring Boot 真的没什么神秘的。
SpringBoot Hello
SpringBoot 建议我们使用 maven 或 gradle 项目构建和管理工具,因为我自己对 maven 比较熟悉,所以这里就以 maven 作为例子讲解。为了方便开发人员迅速搭建 SpringBoot 开发环境,SpringBoot 提供了一个非常实用的工具网站,Spring Initializr,在这上面我们可以选择使用 maven 还是 gradle、java 版本、springboot 版本、以及需要使用的 springboot starter 模块,选好之后,我们就可以直接点击 Generate Project 按钮,下载 maven/gradle 项目压缩包,然后使用喜欢的 IDE 打开,编写我们的业务类即可,简直不要太爽。
流行的 Java IDE:IntelliJ IDEA 也提供了对 SpringBoot 应用程序的创建支持,所以我们更多的是直接在 IDE 中创建 SpringBoot 项目,本质上,IDEA 还是调用的 https://start.spring.io 的 API 来完成的,所以它们本质上没有区别,只是直接在 IDEA 中创建显然更方便一些,不需要打开浏览器一个一个选,然后再下载到本地,最后还要使用 IDE 导入 maven/gradle 项目。
因为本人使用的也是 IDEA,所以直接就在 IDEA 中创建 springboot 项目(创建方法很简单,和创建普通的 maven 项目差不多,自己一看就会,这里就不详细说明了)。
项目结构
$ tree
.
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── zfl9
│ │ └── HelloWorld.java
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
src/main/java
目录是我们的源码目录,然后 src/main/resources
目录是我们的资源目录,这个目录下默认有 3 个文件:
application.properties
:springboot 的配置文件,当然也可以在里面存放我们的自定义属性,除了 properties 格式外,springboot 还支持使用 yaml(*.yml
)格式,但是这里依旧使用大家比较熟悉的 properties 配置文件格式,如果要使用 yaml 文件格式,可以自己去了解一下,貌似可读性确实好一些,但是我个人不太喜欢这种“缩进”风格的语言、格式,比如 Python,感觉有点恶心。static
:静态文件的存放目录,如 index.html、css 和 js 等静态文件也都是放到这个目录下。templates
:模板文件的存放目录,如 employee.ftl(freemarker 模板文件)。
因为我们编写的是一个简单的 RESTful 应用,所以就没有编写静态网页文件或模板文件,直接返回一个 "Hello, World!"
字符串。application.properties
也是空的,保持默认。所以我们只需要关心两个文件:pom.xml 和 HelloWorld.java。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9.springboot</groupId>
<artifactId>springboot-helloworld</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
HelloWorld.java
package com.zfl9;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class HelloWorld {
public static void main(String[] args) {
SpringApplication.run(HelloWorld.class, args);
}
@GetMapping("/")
public String helloWorld() {
return "Hello, World!";
}
}
先来分析 pom.xml,第一个地方就是 <parent>
标签,这里我们引入了一个父级 pom:spring-boot-starter-parent
,每个 springboot 项目通常都会引入这个父 pom,这个父 pom 中定义了与 springboot 相关的各种 starter 依赖组,以及相关的 maven 插件,以及各种 properties(依赖版本),基本上,这些 dependency 和 plugin 都是放在 dependencyManagement、pluginManagement 标签中的,这样就不会带来各种不必要的依赖,我们想要使用哪些依赖、插件就自己在项目的 pom.xml 中引入即可(只需提供 groupId、artifactId,不需要给出 version 等信息),而对于依赖项目的版本,也可以在我们的 pom.xml 中通过定义同名的 properties 属性来覆盖父级 pom 中的版本属性,但是不推荐这么做,可能会带来版本冲突风险。
当然,如果当前项目已经有了一个父级 pom,那就不能直接定义 springboot 的 parent pom 了,因为 maven 和 java 一样,都是单继承模型的,不允许多继承。这时候也是有相应的解决办法的,不去动当前项目的已有的父级 pom,而是在我们的项目中,定义 dependencyManagement 标签,在里面定义 type=pom、scope=import 类型的 dependency,这个 dependency 就是 springboot 提供的 spring-boot-dependencies
,这个项目里面定义的全是 springboot 的 starter 依赖组,然后我们仍然像上面那样在 dependencies 标签中定义 dependency 依赖项(不需要提供 version 等信息),具体如下(不需要改动我们自己的父级 pom.xml):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
第二个就是:<java.version>
,注意这个属性会被 springboot 使用,而不是被 maven 使用,指定 maven 使用的 jdk 版本不是用的这个属性,而是
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
第三个就是:spring-boot-starter-web
starter 依赖组(web),这里面就包含了 spring-webmvc 等基础模块,不需要自己去手动引入众多依赖项。
spring boot starter 命名规范
spring-boot-starter
:核心 starter,包含自动配置支持、日志框架支持、yaml 配置文件解析库等。spring-boot-starter-web
:Web 相关的 starter(使用 Tomcat 作为默认嵌入式 servlet 容器)。spring-boot-starter-aop
:AOP 相关的 starter(面向切面编程,Spring AOP and AspectJ)。spring-boot-starter-test
:测试相关的库,比如:JUnit、Hamcrest、Mockitospring-boot-starter-jdbc
:spring-jdbc 支持、HikariCP 连接池。spring-boot-starter-mail
:java mail 支持、spring framework email 支持。spring-boot-starter-validation
:Validation with Hibernate Validator。spring-boot-starter-thymeleaf
:Thymeleaf 模板引擎支持。spring-boot-starter-freemarker
:FreeMarker 模板引擎支持。
官方 starter 的命名为:
spring-boot-starter-{name}
,第三方 starter 的命名为:{name}-spring-boot-starter
。
第四个就是:spring-boot-maven-plugin
plugin 插件,该插件的作用是让 springboot 支持将 webapp 打包为可执行的 jar 包,所以通常建议加上。
接下来就是我们的 SpringBoot 启动类&配置类,相关的注解就不一一解释了,上面说的很明白,而 @RestController
则是之前学习 SpringMVC 时候使用的 RESTful 相关的注解,也不详细解释,整个 HelloWorld 项目结构很简单,清晰易懂;OK,现在我们直接右键 main() 方法,执行即可,然后你就会看到 SpringBoot 的启动信息(并且还会打印一个 ASCII 徽标,以及相关的彩色日志,非常炫酷):
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.1.2.RELEASE)
2019-02-15 20:30:16.039 INFO 4872 --- [ main] com.zfl9.HelloWorld : Starting HelloWorld on Otokaze-Win10 with PID 4872 (C:\Users\Otokaze\IdeaProjects\springboot\01-springboot-helloworld\target\classes started by Otokaze in C:\Users\Otokaze\IdeaProjects\springboot)
2019-02-15 20:30:16.039 INFO 4872 --- [ main] com.zfl9.HelloWorld : No active profile set, falling back to default profiles: default
2019-02-15 20:30:16.930 INFO 4872 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-02-15 20:30:16.945 INFO 4872 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-02-15 20:30:16.945 INFO 4872 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.14]
2019-02-15 20:30:16.945 INFO 4872 --- [ main] o.a.catalina.core.AprLifecycleListener : An older version [1.2.18] of the APR based Apache Tomcat Native library is installed, while Tomcat recommends a minimum version of [1.2.19]
2019-02-15 20:30:16.945 INFO 4872 --- [ main] o.a.catalina.core.AprLifecycleListener : Loaded APR based Apache Tomcat Native library [1.2.18] using APR version [1.6.5].
2019-02-15 20:30:16.961 INFO 4872 --- [ main] o.a.catalina.core.AprLifecycleListener : APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true].
2019-02-15 20:30:16.961 INFO 4872 --- [ main] o.a.catalina.core.AprLifecycleListener : APR/OpenSSL configuration: useAprConnector [false], useOpenSSL [true]
2019-02-15 20:30:16.961 INFO 4872 --- [ main] o.a.catalina.core.AprLifecycleListener : OpenSSL successfully initialized [OpenSSL 1.1.1 11 Sep 2018]
2019-02-15 20:30:17.023 INFO 4872 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-02-15 20:30:17.023 INFO 4872 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 922 ms
2019-02-15 20:30:17.164 INFO 4872 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-02-15 20:30:17.305 INFO 4872 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-02-15 20:30:17.305 INFO 4872 --- [ main] com.zfl9.HelloWorld : Started HelloWorld in 1.516 seconds (JVM running for 1.963)
2019-02-15 20:30:32.069 INFO 4872 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2019-02-15 20:30:32.070 INFO 4872 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2019-02-15 20:30:32.078 INFO 4872 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 8 ms
其中几个比较重要的信息:
- SpringBoot 的版本
- 当前的 JVM 进程 ID
- 当前激活的 profile
- Tomcat 监听的端口号
- SpringBoot 启动时间
现在,打开浏览器或其它客户端,访问 http://localhost:8080 就可以看到 “Hello, World!” 信息了。
启动类&配置类
这个类是我们的启动类,也是我们的配置类,不要把它看得有多么神秘,其实就是一个普通的 @Configuration
配置类而已,我们可以在里面配置自己的 @Bean
方法,注册相应的 bean 到 spring 容器中。来看一个简单的例子:
package com.zfl9;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class HelloWorld {
public static void main(String[] args) {
SpringApplication.run(HelloWorld.class, args);
}
@Bean
public Object object() {
System.out.println("HelloWorld#object() -- return new Object()");
return new Object();
}
@GetMapping("/")
public String helloWorld() {
return "Hello, World!";
}
}
运行结果:
2019-02-16 08:28:46.236 INFO 5260 --- [ main] com.zfl9.HelloWorld : Starting HelloWorld on Otokaze-Win10 with PID 5260 (C:\Users\Otokaze\IdeaProjects\springboot\01-springboot-helloworld\target\classes started by Otokaze in C:\Users\Otokaze\IdeaProjects\springboot)
2019-02-16 08:28:46.236 INFO 5260 --- [ main] com.zfl9.HelloWorld : No active profile set, falling back to default profiles: default
2019-02-16 08:28:47.122 INFO 5260 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-02-16 08:28:47.137 INFO 5260 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-02-16 08:28:47.137 INFO 5260 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.14]
2019-02-16 08:28:47.137 INFO 5260 --- [ main] o.a.catalina.core.AprLifecycleListener : An older version [1.2.18] of the APR based Apache Tomcat Native library is installed, while Tomcat recommends a minimum version of [1.2.19]
2019-02-16 08:28:47.137 INFO 5260 --- [ main] o.a.catalina.core.AprLifecycleListener : Loaded APR based Apache Tomcat Native library [1.2.18] using APR version [1.6.5].
2019-02-16 08:28:47.137 INFO 5260 --- [ main] o.a.catalina.core.AprLifecycleListener : APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true].
2019-02-16 08:28:47.137 INFO 5260 --- [ main] o.a.catalina.core.AprLifecycleListener : APR/OpenSSL configuration: useAprConnector [false], useOpenSSL [true]
2019-02-16 08:28:47.153 INFO 5260 --- [ main] o.a.catalina.core.AprLifecycleListener : OpenSSL successfully initialized [OpenSSL 1.1.1 11 Sep 2018]
2019-02-16 08:28:47.200 INFO 5260 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-02-16 08:28:47.200 INFO 5260 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 901 ms
HelloWorld#object() -- return new Object()
2019-02-16 08:28:47.356 INFO 5260 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-02-16 08:28:47.481 INFO 5260 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-02-16 08:28:47.481 INFO 5260 --- [ main] com.zfl9.HelloWorld : Started HelloWorld in 1.542 seconds (JVM running for 2.258)
SpringApplication.run() 是非阻塞的
注意哦,对于 web 应用,我们运行 main() 方法之后,是不会立即退出的,而是一直占用着前台(一直运行),这时候你可能就会理所当然的认为,肯定是 SpringApplication.run()
方法阻塞了,但其实不是这样的,该方法内部实际上会创建一个新的线程去执行(嵌入式 tomcat 服务器),所以它并不是一个阻塞调用,在 Spring 容器初始化完毕之后,它就会创建一个新的线程在后台运行 Tomcat 嵌入式服务器,然后就返回了,不信的话可以看下面这个例子,在运行 main() 方法之后,你可以看到 SpringApplication.run() 方法后的代码很快就被执行了:
package com.zfl9;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class HelloWorld {
public static void main(String[] args) {
ApplicationContext context = SpringApplication.run(HelloWorld.class, args);
Object bean = context.getBean("object", Object.class);
System.out.println(bean);
}
@Bean
public Object object() {
return new Object();
}
@GetMapping("/")
public String helloWorld() {
return "Hello, World!";
}
}
如果是普通应用(非 web),则没有后台进程
首先,我们将 pom.xml 中的 spring-boot-starter-web
(web 模块)改为 spring-boot-starter
(核心模块),然后去除启动类中的 Web 相关注解和方法,运行 main() 方法,就可以发现,执行完之后就会很快退出,不会一直运行,因为没有 Tomcat 嵌入式服务器在运行。
SpringBoot Runner
这里指的 Runner 是 SpringBoot 的 CommandLineRunner
、ApplicationRunner
,这两个都是“函数式接口”,声明如下:
@FunctionalInterface
public interface CommandLineRunner {
void run(String... args) throws Exception;
}
@FunctionalInterface
public interface ApplicationRunner {
void run(ApplicationArguments args) throws Exception;
}
Spring 容器启动后,会搜寻 context 中,所有实现了 CommandLineRunner、ApplicationRunner 的 bean 对象,然后将它们排序(@Order
注解,该注解有一个 value 属性,类型为 int,表示“优先级”,值越小优先级越高,所以 Integer.MIN_VALUE 的优先级最高,Integer.MAX_VALUE 的优先级最低),最后依次调用这些对象的 run() 方法,其中传递的参数就是 main() 启动方法接收到的命令行参数,对于 CommandLineRunner,就是直接传递 main() 方法的 String[] args 字符串数组过去,对于 ApplicationRunner,会先将这个字符串数组包装一下,再传递给它们的 run() 方法。
ApplicationArguments 是 String[] args
原始命令行参数的包装对象,提供了对命令行参数的解析方法,如果你的 Runner 需要解析命令行参数,建议实现 ApplicationRunner。这些 Runner 会在容器启动后立即执行,所以我们可以借助这些 Runner 接口来做一些应用初始化操作,具体就不演示了。
SpringBoot 日志配置
SpringBoot 默认使用 slf4j + logback 日志记录框架,而且通过上面的 helloworld 程序大家也有了一个基本的印象,springboot 的日志格式非常整齐和漂亮,也非常易读。默认情况下,springboot 启用的日志级别为 INFO,所以低于 INFO 级别的日志消息不会显示在控制台,即 TRACE、DEBUG 级别的日志消息不会显示出来,而 INFO、WARN、ERROR 级别则会打印到控制台;我们来通过一个简单的例子验证一下:
package com.zfl9;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class HelloWorld {
private static final Logger logger = LoggerFactory.getLogger(HelloWorld.class);
public static void main(String[] args) {
SpringApplication.run(HelloWorld.class, args);
logger.trace("trace message");
logger.debug("debug message");
logger.info("info message");
logger.warn("warn message");
logger.error("error message");
}
@GetMapping("/")
public String helloWorld() {
return "Hello, World!";
}
}
运行结果:
2019-02-16 13:57:32.042 INFO 5656 --- [ main] com.zfl9.HelloWorld : Starting HelloWorld on Otokaze-Win10 with PID 5656 (C:\Users\Otokaze\IdeaProjects\springboot\01-springboot-helloworld\target\classes started by Otokaze in C:\Users\Otokaze\IdeaProjects\springboot)
2019-02-16 13:57:32.042 INFO 5656 --- [ main] com.zfl9.HelloWorld : No active profile set, falling back to default profiles: default
2019-02-16 13:57:32.924 INFO 5656 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-02-16 13:57:32.939 INFO 5656 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-02-16 13:57:32.939 INFO 5656 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.14]
2019-02-16 13:57:32.939 INFO 5656 --- [ main] o.a.catalina.core.AprLifecycleListener : An older version [1.2.18] of the APR based Apache Tomcat Native library is installed, while Tomcat recommends a minimum version of [1.2.19]
2019-02-16 13:57:32.939 INFO 5656 --- [ main] o.a.catalina.core.AprLifecycleListener : Loaded APR based Apache Tomcat Native library [1.2.18] using APR version [1.6.5].
2019-02-16 13:57:32.939 INFO 5656 --- [ main] o.a.catalina.core.AprLifecycleListener : APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true].
2019-02-16 13:57:32.939 INFO 5656 --- [ main] o.a.catalina.core.AprLifecycleListener : APR/OpenSSL configuration: useAprConnector [false], useOpenSSL [true]
2019-02-16 13:57:32.939 INFO 5656 --- [ main] o.a.catalina.core.AprLifecycleListener : OpenSSL successfully initialized [OpenSSL 1.1.1 11 Sep 2018]
2019-02-16 13:57:33.002 INFO 5656 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-02-16 13:57:33.002 INFO 5656 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 898 ms
2019-02-16 13:57:33.158 INFO 5656 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-02-16 13:57:33.283 INFO 5656 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-02-16 13:57:33.283 INFO 5656 --- [ main] com.zfl9.HelloWorld : Started HelloWorld in 1.475 seconds (JVM running for 1.908)
2019-02-16 13:57:33.283 INFO 5656 --- [ main] com.zfl9.HelloWorld : info message
2019-02-16 13:57:33.283 WARN 5656 --- [ main] com.zfl9.HelloWorld : warn message
2019-02-16 13:57:33.283 ERROR 5656 --- [ main] com.zfl9.HelloWorld : error message
那么如何更改这个默认的日志级别呢?或者是如何更改指定 package 的 logging level?很简单,在 application.properties 配置文件中配置一行:
logging.level.com.zfl9 = trace
语法 logging.level.[logger-name] = <level>
,如果是为根 logger 设置日志级别,则直接写 logging.level.root = debug
。运行结果:
2019-02-16 14:04:56.201 INFO 4244 --- [ main] com.zfl9.HelloWorld : Starting HelloWorld on Otokaze-Win10 with PID 4244 (C:\Users\Otokaze\IdeaProjects\springboot\01-springboot-helloworld\target\classes started by Otokaze in C:\Users\Otokaze\IdeaProjects\springboot)
2019-02-16 14:04:56.201 DEBUG 4244 --- [ main] com.zfl9.HelloWorld : Running with Spring Boot v2.1.2.RELEASE, Spring v5.1.4.RELEASE
2019-02-16 14:04:56.201 INFO 4244 --- [ main] com.zfl9.HelloWorld : No active profile set, falling back to default profiles: default
2019-02-16 14:04:57.033 INFO 4244 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-02-16 14:04:57.049 INFO 4244 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-02-16 14:04:57.049 INFO 4244 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.14]
2019-02-16 14:04:57.049 INFO 4244 --- [ main] o.a.catalina.core.AprLifecycleListener : An older version [1.2.18] of the APR based Apache Tomcat Native library is installed, while Tomcat recommends a minimum version of [1.2.19]
2019-02-16 14:04:57.049 INFO 4244 --- [ main] o.a.catalina.core.AprLifecycleListener : Loaded APR based Apache Tomcat Native library [1.2.18] using APR version [1.6.5].
2019-02-16 14:04:57.049 INFO 4244 --- [ main] o.a.catalina.core.AprLifecycleListener : APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true].
2019-02-16 14:04:57.049 INFO 4244 --- [ main] o.a.catalina.core.AprLifecycleListener : APR/OpenSSL configuration: useAprConnector [false], useOpenSSL [true]
2019-02-16 14:04:57.065 INFO 4244 --- [ main] o.a.catalina.core.AprLifecycleListener : OpenSSL successfully initialized [OpenSSL 1.1.1 11 Sep 2018]
2019-02-16 14:04:57.111 INFO 4244 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-02-16 14:04:57.111 INFO 4244 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 847 ms
2019-02-16 14:04:57.268 INFO 4244 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-02-16 14:04:57.377 INFO 4244 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-02-16 14:04:57.377 INFO 4244 --- [ main] com.zfl9.HelloWorld : Started HelloWorld in 1.442 seconds (JVM running for 1.9)
2019-02-16 14:04:57.377 TRACE 4244 --- [ main] com.zfl9.HelloWorld : trace message
2019-02-16 14:04:57.377 DEBUG 4244 --- [ main] com.zfl9.HelloWorld : debug message
2019-02-16 14:04:57.377 INFO 4244 --- [ main] com.zfl9.HelloWorld : info message
2019-02-16 14:04:57.377 WARN 4244 --- [ main] com.zfl9.HelloWorld : warn message
2019-02-16 14:04:57.377 ERROR 4244 --- [ main] com.zfl9.HelloWorld : error message
自定义外部的 logback.xml 配置文件
如果是一些比较简单的 logging 配置,如更改日志级别,建议直接在 application.properties 文件中设置,简单方便;如果 application.properties 提供的配置项无法满足你的要求,springboot 也允许你提供一个外部的 logback 配置文件。注意,你经常会看到两种名称的 logback 配置文件,logback.xml
和 logback-spring.xml
,了解 logback 的同学应该知道,只有前者才是 logback 框架的标准配置文件名,后者不是;logback-spring.xml 其实是 springboot 规定的一个文件名,springboot 建议使用 logback-spring.xml 而不是 logback.xml,因为日志服务通常在 ApplicationContext 创建之前就已经初始化完毕了,所以 springboot 不能很好的控制 logback.xml,但是 logback-spring.xml 能够比较好的控制,如 springboot 会在我们的 logback-spring.xml 配置的基础上添加一些 springboot 特有的配置项;所以建议使用带 -spring
后缀的日志配置文件。
SpringBoot 单元测试
Spring 单元测试 - 简单例子
package com.zfl9.spring;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
public class IocByXmlConfigTestBySpring {
@Resource(name = "钟时兰")
private Teacher teacher;
@Resource(name = "钟学华")
private Student student01;
@Resource(name = "尹帧瑶")
private Student student02;
@Resource(name = "廖小兵")
private Student student03;
@Test
public void test01() {
System.out.println(teacher);
System.out.println(student01);
System.out.println(student02);
System.out.println(student03);
}
@Test
public void test02() {
for (Student student : teacher.getStudents()) {
System.out.println(student);
}
}
}
SpringBoot 单元测试 - 简单例子
package com.zfl9;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTest {
@Autowired
private Application application;
@Test
public void test() {
Logger logger = LoggerFactory.getLogger(this.getClass());
logger.info("application: {}", application);
}
}
其实没多大区别,最关键的地方在于测试类的注解声明:
// Spring 单元测试
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring.xml")
// SpringBoot 单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
@RunWith(SpringRunner.class)
是 JUnit 4 的注解,用于指定使用哪个 Runner 运行当前测试类,无论是 Spring 还是 SpringBoot 都需要使用这个注解,具体含义和作用大家应该都比较清楚,这里不详细解释。在 Spring 单元测试类中,我们用的是 @ContextConfiguration
注解来指定 spring 的配置文件(xml 或 class);但是在 SpringBoot 单元测试类中,我们只需要使用 @SpringBootTest
注解标注就可以了,@SpringBootTest
注解会自动搜索当前包(含子包)中被 @SpringBootApplication
标注的启动类&配置类(本质就是 java-based 形式的配置),不用指定使用哪个“启动类”。
除了这个区别外,其它的行为都没有什么区别;比如:如果测试类、测试方法上使用了 @Transactional
声明式事务注解,那么 spring-test 会自动回滚当前方法的事务,无论是 Spring 还是 SpringBoot 的单元测试,都是一样的,默认都是会自动回滚事务;来回顾一下 Spring 单元测试中的事务管理:
- @BeforeTransaction:在事务开启之前执行,从输出结果也看得出来,此时没有开启事务。
- @Before:在事务开启之后且在测试方法之前执行,即在 @Test 前执行,此时已经有一个事务。
- @Test:在 @Before 方法之后执行,此时也有一个事务,@Before、@Test、@After 都是同一个事务。
- @After:在 @Test 方法之后执行,此时也有一个事务,@Before、@Test、@After 也都是同一个事务。
- @AfterTransaction:在事务完成后执行,此时事务已经提交或回滚,可以在方法中测试事务提交是否成功。
SpringBoot FreeMarker
项目结构
$ tree src
src
├── main
│ ├── java
│ │ └── com
│ │ └── zfl9
│ │ └── springboot
│ │ ├── Application.java
│ │ └── HelloController.java
│ └── resources
│ ├── application.properties
│ ├── static
│ │ └── index.html
│ └── templates
│ └── hello.ftl
└── test
└── java
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9.springboot</groupId>
<artifactId>springboot-simple-webapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
集成 FreeMarker 非常简单,只需要引入 spring-boot-starter-freemarker
即可;然后你就能立即使用 FreeMarker 模板了,不需要任何必须的 application.properties 配置(开箱即用)。不过为了更愉快的使用 FreeMarker,我们通常也会在 application.properties 中配置一些 FreeMarker 的属性,如开启 cache,设置日期、时间、数字的显示格式等。
另外解释一下 spring-boot-devtools
模块的作用,看名字大概也猜得出来,这是 springboot 提供的一个开发工具,在开发 web 程序的时候,一个共同的需求就是:热重载,即修改了 java 文件或 resource 文件(总之就是 classpath 下的文件变动了)后,能够通过某种手段,更新这些 class、resource 文件,而不需要重启 servlet 服务器,这就叫做热重载,也有人叫做热更新。而 devtools 模块的作用就是提供 springboot 应用的热更新功能,只要我们引入了 devtools 模块,当我们运行 Application.main() 方法之后,如果更改了源代码或者是其它资源文件(如 index.html),不需要重新运行 Application.main() 方法了,而是只需要重新编译当前 project/module 即可,在 IDEA 中,编译当前项目的快捷键为 Ctrl + F9,编译之后,devtools 就会自动感知到 classpath 下的文件内容变动了,于是就会丢弃原来的旧文件内容,重新加载新的文件内容,以此完成热更新。
devtools 热重载原理浅析
spring-boot-devtools 使用了两个类加载器来实现热重载:base 类加载器、restart 类加载器。
- base ClassLoader:用于加载不会改变的 jar(eg:第三方依赖的 jar 包)。
- restart ClassLoader:用于加载我们正在开发的 jar(eg:我们当前的项目文件)。应用重载后,devtools 会将原先的 restart ClassLoader 丢掉、然后重新 new 一个 restart ClassLoader 来加载这些修改过的东西,而 base ClassLoader 却不需要动一下。这就是 devtools 重载速度快的原因。
devtools 会自动禁用 FreeMarker、Thymeleaf 等模板引擎的缓存,即使手动指定设置 freemarker/thymeleaf 的 cache 选项为 true 也是如此。
application.properties
spring.freemarker.cache = true
spring.freemarker.charset = UTF-8
spring.freemarker.content-type = text/html; charset=UTF-8
spring.freemarker.expose-request-attributes = true
spring.freemarker.expose-session-attributes = true
spring.freemarker.expose-spring-macro-helpers = true
spring.freemarker.settings.locale = zh_CN
spring.freemarker.settings.time_zone = Asia/Shanghai
spring.freemarker.settings.output_encoding = UTF-8
spring.freemarker.settings.url_escaping_charset = UTF-8
spring.freemarker.settings.number_format = 0.###
spring.freemarker.settings.boolean_format = true,false
spring.freemarker.settings.date_format = yyyy-MM-dd
spring.freemarker.settings.time_format = HH:mm:ss
spring.freemarker.settings.datetime_format = yyyy-MM-dd HH:mm:ss
Application.java
package com.zfl9.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
HelloController.java
package com.zfl9.springboot;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloController {
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("addr", "www.zfl9.com");
model.addAttribute("port", 443);
model.addAttribute("method", "aes-128-gcm");
model.addAttribute("passwd", "123456@zfl9.com");
return "hello";
}
}
index.html
<h1>hello, world! [freemarker]</h1>
hello.ftl
<h2>addr = ${addr}</h2>
<h2>port = ${port}</h2>
<h2>method = ${method}</h2>
<h2>passwd = ${passwd}</h2>
SpringBoot HikariCP
SpringBoot 1.x 的默认数据库连接池为 Tomcat Jdbc Pool,SpringBoot 2.x 的默认数据库连接池为 HikariCP。HikariCP 虽然是后起之秀,但是它凭借着“速度快”,“轻量级”,“高性能”这些优势,赢得了许多用户的好口碑;SpringBoot 当然也不例外,在 2.x 的时候,直接就将 HikariCP 作为默认的数据库连接池实现,而不是 Tomcat Jdbc Pool。由于本文始终使用 SpringBoot 2.x,所以这里直接介绍 HikariCP 数据库连接池。
项目结构
$ tree src
src
├── main
│ ├── java
│ │ └── com
│ │ └── zfl9
│ │ └── springboot
│ │ ├── Application.java
│ │ ├── Employee.java
│ │ ├── EmployeeRunner.java
│ │ └── HelloController.java
│ └── resources
│ ├── application.properties
│ ├── data.sql
│ ├── schema.sql
│ ├── static
│ │ └── index.html
│ └── templates
│ └── hello.ftl
└── test
└── java
HelloController、index.html、hello.ftl 这些不用管,这是上一节 FreeMarker 的测试类以及文件。Application 还是照常,一个简单的 main() 方法。
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9.springboot</groupId>
<artifactId>springboot-simple-webapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
新增的依赖为 spring-boot-starter-jdbc
(spring-jdbc 模块、HikariCP 连接池)、mysql-connector-java
(MySQL JDBC 驱动)。
application.properties
# FreeMarker
spring.freemarker.cache = true
spring.freemarker.charset = UTF-8
spring.freemarker.content-type = text/html; charset=UTF-8
spring.freemarker.expose-request-attributes = true
spring.freemarker.expose-session-attributes = true
spring.freemarker.expose-spring-macro-helpers = true
spring.freemarker.settings.locale = zh_CN
spring.freemarker.settings.time_zone = Asia/Shanghai
spring.freemarker.settings.output_encoding = UTF-8
spring.freemarker.settings.url_escaping_charset = UTF-8
spring.freemarker.settings.number_format = 0.###
spring.freemarker.settings.boolean_format = true,false
spring.freemarker.settings.date_format = yyyy-MM-dd
spring.freemarker.settings.time_format = HH:mm:ss
spring.freemarker.settings.datetime_format = yyyy-MM-dd HH:mm:ss
# HikariCP
spring.datasource.initialization-mode = always
spring.datasource.type = com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost/test
spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.hikari.connection-timeout = 10000
spring.datasource.hikari.max-lifetime = 7200000
spring.datasource.hikari.maximum-pool-size = 30
这里简单解释一下 spring.datasource.initialization-mode
选项,它有 3 个取值:
always
:不管什么数据源都会初始化;never
:不管什么数据源都不进行初始化;embedded
:只初始化嵌入式数据源(默认)。
这里的 DataSource 初始化的意思是(仅讲解 JDBC 数据源的初始化):在应用启动时,会自动执行 classpath 下的 schema.sql
和 data.sql
文件:
schema.sql
:DDL 语句,如 create、alter、drop,通常在该脚本中写 table 的 create、drop 命令;data.sql
:DML 语句,如 select、insert、update、delete,通常在该脚本中写 table 的 insert 命令。
schema.sql
drop table if exists `springboot`;
create table `springboot` (
`id` int(11) not null auto_increment,
`name` varchar(30) not null,
`email` varchar(50) not null,
primary key (`id`)
) charset=utf8mb4
data.sql
insert into springboot(name, email) values("zfl9", "root@zfl9.com");
insert into springboot(name, email) values("baidu", "root@baidu.com");
insert into springboot(name, email) values("google", "root@google.com");
insert into springboot(name, email) values("youtube", "root@youtube.com");
insert into springboot(name, email) values("facebook", "root@facebook.com");
springboot 应用启动后,就会自动执行 schema.sql(创建 springboot 测试表)、data.sql(插入 5 条测试数据)。
Employee.java
package com.zfl9.springboot;
public class Employee implements java.io.Serializable {
private static final long serialVersionUID = 9189662856423316374L;
private Integer id;
private String name;
private String email;
public Employee() {
}
public Employee(String name, String email) {
this.name = name;
this.email = email;
}
public Employee(Integer id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
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;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return String.format("Employee{id=%d, name='%s', email='%s'}", id, name, email);
}
}
EmployeeRunner.java
package com.zfl9.springboot;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Repository
public class EmployeeRunner implements CommandLineRunner {
private static final Logger LOGGER = LoggerFactory.getLogger(EmployeeRunner.class);
private JdbcTemplate jdbcTemplate;
private RowMapper<Employee> rowMapper = (resultSet, rowNum) -> {
Employee employee = new Employee();
employee.setId(resultSet.getInt("id"));
employee.setName(resultSet.getString("name"));
employee.setEmail(resultSet.getString("email"));
return employee;
};
@Autowired
public EmployeeRunner(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
private List<Employee> getEmployees() {
List<Employee> employees = new ArrayList<>();
employees.add(new Employee("小明", "xiaoming@gmail.com"));
employees.add(new Employee("小刚", "xiaogang@gmail.com"));
employees.add(new Employee("小红", "xiaohong@gmail.com"));
return employees;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void run(String... args) {
String sql = "insert into springboot(name, email) values(?, ?)";
for (Employee employee : getEmployees()) {
LOGGER.info("insert employee: {}", employee);
jdbcTemplate.update(sql, employee.getName(), employee.getEmail());
}
throw new RuntimeException("runtime exception");
}
}
运行结果
2019-02-16 21:43:32.146 INFO 8988 --- [ restartedMain] com.zfl9.springboot.Application : Starting Application on Otokaze-Win10 with PID 8988 (C:\Users\Otokaze\IdeaProjects\springboot\02-springboot-simple-webapp\target\classes started by Otokaze in C:\Users\Otokaze\IdeaProjects\springboot)
2019-02-16 21:43:32.158 INFO 8988 --- [ restartedMain] com.zfl9.springboot.Application : No active profile set, falling back to default profiles: default
2019-02-16 21:43:32.224 INFO 8988 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2019-02-16 21:43:32.224 INFO 8988 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : For additional web related logging consider setting the 'logging.level.web' property to 'DEBUG'
2019-02-16 21:43:33.409 INFO 8988 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2019-02-16 21:43:33.426 INFO 8988 --- [ restartedMain] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2019-02-16 21:43:33.426 INFO 8988 --- [ restartedMain] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.14]
2019-02-16 21:43:33.429 INFO 8988 --- [ restartedMain] o.a.catalina.core.AprLifecycleListener : An older version [1.2.18] of the APR based Apache Tomcat Native library is installed, while Tomcat recommends a minimum version of [1.2.19]
2019-02-16 21:43:33.429 INFO 8988 --- [ restartedMain] o.a.catalina.core.AprLifecycleListener : Loaded APR based Apache Tomcat Native library [1.2.18] using APR version [1.6.5].
2019-02-16 21:43:33.431 INFO 8988 --- [ restartedMain] o.a.catalina.core.AprLifecycleListener : APR capabilities: IPv6 [true], sendfile [true], accept filters [false], random [true].
2019-02-16 21:43:33.431 INFO 8988 --- [ restartedMain] o.a.catalina.core.AprLifecycleListener : APR/OpenSSL configuration: useAprConnector [false], useOpenSSL [true]
2019-02-16 21:43:33.433 INFO 8988 --- [ restartedMain] o.a.catalina.core.AprLifecycleListener : OpenSSL successfully initialized [OpenSSL 1.1.1 11 Sep 2018]
2019-02-16 21:43:33.518 INFO 8988 --- [ restartedMain] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2019-02-16 21:43:33.518 INFO 8988 --- [ restartedMain] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1293 ms
2019-02-16 21:43:33.587 INFO 8988 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2019-02-16 21:43:33.666 INFO 8988 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2019-02-16 21:43:33.726 INFO 8988 --- [ restartedMain] o.s.b.d.a.OptionalLiveReloadServer : LiveReload server is running on port 35729
2019-02-16 21:43:33.982 INFO 8988 --- [ restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-02-16 21:43:34.061 INFO 8988 --- [ restartedMain] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2019-02-16 21:43:34.182 INFO 8988 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2019-02-16 21:43:34.185 INFO 8988 --- [ restartedMain] com.zfl9.springboot.Application : Started Application in 2.295 seconds (JVM running for 2.812)
2019-02-16 21:43:34.193 INFO 8988 --- [ restartedMain] com.zfl9.springboot.EmployeeRunner : insert employee: Employee{id=null, name='小明', email='xiaoming@gmail.com'}
2019-02-16 21:43:34.204 INFO 8988 --- [ restartedMain] com.zfl9.springboot.EmployeeRunner : insert employee: Employee{id=null, name='小刚', email='xiaogang@gmail.com'}
2019-02-16 21:43:34.205 INFO 8988 --- [ restartedMain] com.zfl9.springboot.EmployeeRunner : insert employee: Employee{id=null, name='小红', email='xiaohong@gmail.com'}
2019-02-16 21:43:34.230 INFO 8988 --- [ restartedMain] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2019-02-16 21:43:34.238 ERROR 8988 --- [ restartedMain] o.s.boot.SpringApplication : Application run failed
java.lang.IllegalStateException: Failed to execute CommandLineRunner
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:816) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:797) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:324) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
at com.zfl9.springboot.Application.main(Application.java:9) [classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_172]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_172]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_172]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_172]
at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) [spring-boot-devtools-2.1.2.RELEASE.jar:2.1.2.RELEASE]
Caused by: java.lang.RuntimeException: runtime exception
at com.zfl9.springboot.EmployeeRunner.run(EmployeeRunner.java:50) ~[classes/:na]
at com.zfl9.springboot.EmployeeRunner$$FastClassBySpringCGLIB$$359c64c5.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:139) ~[spring-tx-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294) ~[spring-tx-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) ~[spring-tx-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at com.zfl9.springboot.EmployeeRunner$$EnhancerBySpringCGLIB$$8cae41f3.run(<generated>) ~[classes/:na]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:813) [spring-boot-2.1.2.RELEASE.jar:2.1.2.RELEASE]
... 10 common frames omitted
2019-02-16 21:43:34.241 INFO 8988 --- [ restartedMain] o.s.s.concurrent.ThreadPoolTaskExecutor : Shutting down ExecutorService 'applicationTaskExecutor'
2019-02-16 21:43:34.241 INFO 8988 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2019-02-16 21:43:34.246 INFO 8988 --- [ restartedMain] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
得益于 springboot 的自动配置功能,我们在配置好 DataSource 之后,springboot 就帮我们开启了声明式事务支持,所以我们可以直接在 run() 方法上使用 @Transactional
注解,需要注意的是,我故意在方法最后跑出一个 RuntimeException 运行时异常,以此测试声明式事务是否正常生效:
- 如果未生效:那么查看 springboot 测试表可以看到多了 3 条记录(小明、小刚、小红);
- 如果已生效:那么查看 springboot 测试表就看不到这 3 条新纪录(还是原来的 5 条记录);
我测试的结果是第二个,所以证明声明式事务已经正常生效了。
SpringBoot MyBatis
SpringBoot 集成 MyBatis 也很简单,MyBatis 提供了一个 mybatis-spring-boot-starter
,添加到 pom.xml 即可。
项目结构
$ tree src
src
├── main
│ ├── java
│ │ └── com
│ │ └── zfl9
│ │ ├── Application.java
│ │ ├── mapper
│ │ │ └── EmployeeMapper.java
│ │ └── model
│ │ └── Employee.java
│ └── resources
│ ├── application.properties
│ └── mapper
│ └── EmployeeMapper.xml
└── test
└── java
└── com
└── zfl9
└── ApplicationTest.java
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9.springboot</groupId>
<artifactId>springboot-mybatis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
# HikariCP
spring.datasource.initialization-mode = always
spring.datasource.type = com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost/test
spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.hikari.connection-timeout = 10000
spring.datasource.hikari.max-lifetime = 7200000
spring.datasource.hikari.maximum-pool-size = 30
# MyBatis
mybatis.mapper-locations = classpath:mapper/*.xml
mybatis.type-aliases-package = com.zfl9.model
mybatis.configuration.map-underscore-to-camel-case = true
# Logging
logging.level.com.zfl9 = trace
logging.level.org.mybatis.spring = trace
Application.java
package com.zfl9;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.zfl9.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@MapperScan("com.zfl9.mapper")
作用和之间的 SSM 项目是一样的,扫描指定 package 下面的 mapper 接口,自动注入到 spring 容器中。
Employee.java
package com.zfl9.model;
public class Employee implements java.io.Serializable {
private static final long serialVersionUID = -8898382136809677158L;
private Integer id;
private String name;
private String email;
private String address;
private String telephone;
public Employee() {
}
public Employee(String name, String email, String address, String telephone) {
this.name = name;
this.email = email;
this.address = address;
this.telephone = telephone;
}
public Employee(Integer id, String name, String email, String address, String telephone) {
this.id = id;
this.name = name;
this.email = email;
this.address = address;
this.telephone = telephone;
}
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;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
@Override
public String toString() {
return String.format("Employee{id=%d, name='%s', email='%s', address='%s', telephone='%s'}", id, name, email, address, telephone);
}
}
EmployeeMapper.java
package com.zfl9.mapper;
import com.zfl9.model.Employee;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface EmployeeMapper {
Employee getEmployeeById(int id);
List<Employee> getAllEmployees();
int addEmployee(Employee employee);
int updateEmployee(Employee employee);
int deleteEmployeeById(int id);
int deleteAllEmployees();
void truncateEmployeeTable();
}
EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zfl9.mapper.EmployeeMapper">
<select id="getEmployeeById" resultType="Employee">
select id,name,email,address,telephone from employee where id=#{id}
</select>
<select id="getAllEmployees" resultType="Employee">
select id,name,email,address,telephone from employee
</select>
<insert id="addEmployee" useGeneratedKeys="true" keyProperty="id">
insert into employee(name, email, address, telephone) values(#{name}, #{email}, #{address}, #{telephone})
</insert>
<update id="updateEmployee">
update employee set name=#{name}, email=#{email}, address=#{address}, telephone=#{telephone} where id=#{id}
</update>
<delete id="deleteEmployeeById">
delete from employee where id=#{id}
</delete>
<delete id="deleteAllEmployees">
delete from employee
</delete>
<delete id="truncateEmployeeTable">
truncate table employee
</delete>
</mapper>
ApplicationTest.java
package com.zfl9;
import com.zfl9.mapper.EmployeeMapper;
import com.zfl9.model.Employee;
import org.junit.After;
import org.junit.Before;
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 org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationTest {
@Autowired
private EmployeeMapper mapper;
private void queryAllEmployees() {
List<Employee> employees = mapper.getAllEmployees();
for (Employee employee : employees) {
System.out.println(employee);
}
}
@Before
public void before() {
System.out.println("===> before insert <===");
queryAllEmployees();
}
@Test
@Transactional(rollbackFor = Exception.class)
public void test() {
mapper.addEmployee(new Employee("小金", "xiaojing@gmail.com", "广州市天河区", "999999"));
mapper.addEmployee(new Employee("小青", "xiaoqing@gmail.com", "南昌市红谷滩", "111111"));
}
@After
public void after() {
System.out.println("===> after insert <===");
queryAllEmployees();
}
@AfterTransaction
public void verify() {
System.out.println("===> after transaction <===");
queryAllEmployees();
}
}
运行结果
2019-02-17 14:52:45.625 INFO 2520 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2019-02-17 14:52:45.770 INFO 2520 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2019-02-17 14:52:45.775 INFO 2520 --- [ main] o.s.t.c.transaction.TransactionContext : Began transaction (1) for test context [DefaultTestContext@27ce24aa testClass = ApplicationTest, testInstance = com.zfl9.ApplicationTest@5119fb47, testMethod = test@ApplicationTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@481a996b testClass = ApplicationTest, locations = '{}', classes = '{class com.zfl9.Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@7791a895, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1d16f93d, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@49fc609f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4f063c0a], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]; transaction manager [org.springframework.jdbc.datasource.DataSourceTransactionManager@3ae66c85]; rollback [true]
===> before insert <===
2019-02-17 14:52:45.983 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Creating a new SqlSession
2019-02-17 14:52:45.990 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22]
2019-02-17 14:52:46.006 DEBUG 2520 --- [ main] o.m.s.t.SpringManagedTransaction : JDBC Connection [HikariProxyConnection@1145391264 wrapping com.mysql.cj.jdbc.ConnectionImpl@3766c667] will be managed by Spring
2019-02-17 14:52:46.011 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : ==> Preparing: select id,name,email,address,telephone from employee
2019-02-17 14:52:46.043 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : ==> Parameters:
2019-02-17 14:52:46.068 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Columns: id, name, email, address, telephone
2019-02-17 14:52:46.068 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 1, 小明, xiaoming@gmail.com, 北京市海淀区, 123456
2019-02-17 14:52:46.077 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 2, 小红, xiaohong@gmail.com, 上海市浦东区, 654321
2019-02-17 14:52:46.077 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 3, 小刚, xiaogang@gmail.com, 深圳市龙岗区, 987654
2019-02-17 14:52:46.077 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Total: 3
2019-02-17 14:52:46.078 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22]
Employee{id=1, name='小明', email='xiaoming@gmail.com', address='北京市海淀区', telephone='123456'}
Employee{id=2, name='小红', email='xiaohong@gmail.com', address='上海市浦东区', telephone='654321'}
Employee{id=3, name='小刚', email='xiaogang@gmail.com', address='深圳市龙岗区', telephone='987654'}
2019-02-17 14:52:46.080 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22] from current transaction
2019-02-17 14:52:46.081 DEBUG 2520 --- [ main] c.z.mapper.EmployeeMapper.addEmployee : ==> Preparing: insert into employee(name, email, address, telephone) values(?, ?, ?, ?)
2019-02-17 14:52:46.082 DEBUG 2520 --- [ main] c.z.mapper.EmployeeMapper.addEmployee : ==> Parameters: 小金(String), xiaojing@gmail.com(String), 广州市天河区(String), 999999(String)
2019-02-17 14:52:46.083 DEBUG 2520 --- [ main] c.z.mapper.EmployeeMapper.addEmployee : <== Updates: 1
2019-02-17 14:52:46.084 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22]
2019-02-17 14:52:46.085 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22] from current transaction
2019-02-17 14:52:46.085 DEBUG 2520 --- [ main] c.z.mapper.EmployeeMapper.addEmployee : ==> Preparing: insert into employee(name, email, address, telephone) values(?, ?, ?, ?)
2019-02-17 14:52:46.085 DEBUG 2520 --- [ main] c.z.mapper.EmployeeMapper.addEmployee : ==> Parameters: 小青(String), xiaoqing@gmail.com(String), 南昌市红谷滩(String), 111111(String)
2019-02-17 14:52:46.086 DEBUG 2520 --- [ main] c.z.mapper.EmployeeMapper.addEmployee : <== Updates: 1
2019-02-17 14:52:46.086 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22]
===> after insert <===
2019-02-17 14:52:46.087 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22] from current transaction
2019-02-17 14:52:46.087 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : ==> Preparing: select id,name,email,address,telephone from employee
2019-02-17 14:52:46.088 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : ==> Parameters:
2019-02-17 14:52:46.088 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Columns: id, name, email, address, telephone
2019-02-17 14:52:46.088 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 1, 小明, xiaoming@gmail.com, 北京市海淀区, 123456
2019-02-17 14:52:46.089 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 2, 小红, xiaohong@gmail.com, 上海市浦东区, 654321
2019-02-17 14:52:46.089 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 3, 小刚, xiaogang@gmail.com, 深圳市龙岗区, 987654
2019-02-17 14:52:46.090 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 8, 小金, xiaojing@gmail.com, 广州市天河区, 999999
2019-02-17 14:52:46.090 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 9, 小青, xiaoqing@gmail.com, 南昌市红谷滩, 111111
2019-02-17 14:52:46.090 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Total: 5
2019-02-17 14:52:46.090 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22]
Employee{id=1, name='小明', email='xiaoming@gmail.com', address='北京市海淀区', telephone='123456'}
Employee{id=2, name='小红', email='xiaohong@gmail.com', address='上海市浦东区', telephone='654321'}
Employee{id=3, name='小刚', email='xiaogang@gmail.com', address='深圳市龙岗区', telephone='987654'}
Employee{id=8, name='小金', email='xiaojing@gmail.com', address='广州市天河区', telephone='999999'}
Employee{id=9, name='小青', email='xiaoqing@gmail.com', address='南昌市红谷滩', telephone='111111'}
2019-02-17 14:52:46.093 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22]
2019-02-17 14:52:46.094 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@40ee0a22]
2019-02-17 14:52:46.134 INFO 2520 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test: [DefaultTestContext@27ce24aa testClass = ApplicationTest, testInstance = com.zfl9.ApplicationTest@5119fb47, testMethod = test@ApplicationTest, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@481a996b testClass = ApplicationTest, locations = '{}', classes = '{class com.zfl9.Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@7791a895, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@1d16f93d, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@49fc609f, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4f063c0a], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]
===> after transaction <===
2019-02-17 14:52:46.134 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Creating a new SqlSession
2019-02-17 14:52:46.134 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25a73de1] was not registered for synchronization because synchronization is not active
2019-02-17 14:52:46.135 DEBUG 2520 --- [ main] o.m.s.t.SpringManagedTransaction : JDBC Connection [HikariProxyConnection@696591495 wrapping com.mysql.cj.jdbc.ConnectionImpl@3766c667] will not be managed by Spring
2019-02-17 14:52:46.135 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : ==> Preparing: select id,name,email,address,telephone from employee
2019-02-17 14:52:46.135 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : ==> Parameters:
2019-02-17 14:52:46.135 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Columns: id, name, email, address, telephone
2019-02-17 14:52:46.135 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 1, 小明, xiaoming@gmail.com, 北京市海淀区, 123456
2019-02-17 14:52:46.136 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 2, 小红, xiaohong@gmail.com, 上海市浦东区, 654321
2019-02-17 14:52:46.136 TRACE 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Row: 3, 小刚, xiaogang@gmail.com, 深圳市龙岗区, 987654
2019-02-17 14:52:46.136 DEBUG 2520 --- [ main] c.z.m.EmployeeMapper.getAllEmployees : <== Total: 3
2019-02-17 14:52:46.136 DEBUG 2520 --- [ main] org.mybatis.spring.SqlSessionUtils : Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@25a73de1]
Employee{id=1, name='小明', email='xiaoming@gmail.com', address='北京市海淀区', telephone='123456'}
Employee{id=2, name='小红', email='xiaohong@gmail.com', address='上海市浦东区', telephone='654321'}
Employee{id=3, name='小刚', email='xiaogang@gmail.com', address='深圳市龙岗区', telephone='987654'}
SpringBoot SSM 示例
项目结构
$ tree src
src
├── main
│ ├── java
│ │ └── com
│ │ └── zfl9
│ │ ├── Application.java
│ │ ├── controller
│ │ │ └── EmployeeController.java
│ │ ├── mapper
│ │ │ └── EmployeeMapper.java
│ │ ├── model
│ │ │ └── Employee.java
│ │ └── service
│ │ ├── EmployeeService.java
│ │ └── EmployeeServiceImpl.java
│ └── resources
│ ├── application.properties
│ ├── mapper
│ │ └── EmployeeMapper.xml
│ ├── static
│ │ └── index.html
│ └── templates
│ ├── employee-edit.ftl
│ └── employee-list.ftl
└── test
└── java
14 directories, 11 files
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9.springboot</groupId>
<artifactId>springboot-ssm</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.properties
# FreeMarker
spring.freemarker.cache = true
spring.freemarker.charset = UTF-8
spring.freemarker.content-type = text/html; charset=UTF-8
spring.freemarker.expose-request-attributes = true
spring.freemarker.expose-session-attributes = true
spring.freemarker.expose-spring-macro-helpers = true
spring.freemarker.settings.locale = zh_CN
spring.freemarker.settings.time_zone = Asia/Shanghai
spring.freemarker.settings.output_encoding = UTF-8
spring.freemarker.settings.url_escaping_charset = UTF-8
spring.freemarker.settings.number_format = 0.###
spring.freemarker.settings.boolean_format = true,false
spring.freemarker.settings.date_format = yyyy-MM-dd
spring.freemarker.settings.time_format = HH:mm:ss
spring.freemarker.settings.datetime_format = yyyy-MM-dd HH:mm:ss
# HikariCP
spring.datasource.initialization-mode = never
spring.datasource.type = com.zaxxer.hikari.HikariDataSource
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost/test
spring.datasource.username = root
spring.datasource.password = 123456
spring.datasource.hikari.connection-timeout = 10000
spring.datasource.hikari.max-lifetime = 7200000
spring.datasource.hikari.maximum-pool-size = 30
# MyBatis
mybatis.mapper-locations = classpath:mapper/*.xml
mybatis.configuration.map-underscore-to-camel-case = true
# Logging
logging.level.com.zfl9 = trace
logging.level.org.mybatis.spring = trace
Application.java
package com.zfl9;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.zfl9.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Employee.java
package com.zfl9.model;
import java.io.Serializable;
public class Employee implements Serializable {
private static final long serialVersionUID = -8369197914775006439L;
private Integer id;
private String name;
private String email;
private String address;
private String telephone;
public Employee() {
}
public Employee(String name, String email, String address, String telephone) {
this.name = name;
this.email = email;
this.address = address;
this.telephone = telephone;
}
public Employee(Integer id, String name, String email, String address, String telephone) {
this.id = id;
this.name = name;
this.email = email;
this.address = address;
this.telephone = telephone;
}
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;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
@Override
public String toString() {
return String.format("Employee{id=%d, name='%s', email='%s', address='%s', telephone='%s'}", id, name, email, address, telephone);
}
}
EmployeeMapper.java
package com.zfl9.mapper;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.zfl9.model.Employee;
@Repository
public interface EmployeeMapper {
Employee getEmployeeById(int id);
List<Employee> getAllEmployees();
int addEmployee(Employee employee);
int updateEmployee(Employee employee);
int deleteEmployeeById(int id);
int deleteAllEmployees();
void truncateEmployeeTable();
}
EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zfl9.mapper.EmployeeMapper">
<select id="getEmployeeById" resultType="com.zfl9.model.Employee">
select id,name,email,address,telephone from employee where id=#{id}
</select>
<select id="getAllEmployees" resultType="com.zfl9.model.Employee">
select id,name,email,address,telephone from employee
</select>
<insert id="addEmployee" useGeneratedKeys="true" keyProperty="id">
insert into employee(name, email, address, telephone) values(#{name}, #{email}, #{address}, #{telephone})
</insert>
<update id="updateEmployee">
update employee set name=#{name}, email=#{email}, address=#{address}, telephone=#{telephone} where id=#{id}
</update>
<delete id="deleteEmployeeById">
delete from employee where id=#{id}
</delete>
<delete id="deleteAllEmployees">
delete from employee
</delete>
<delete id="truncateEmployeeTable">
truncate table employee
</delete>
</mapper>
EmployeeService.java
package com.zfl9.service;
import java.util.List;
import com.zfl9.model.Employee;
public interface EmployeeService {
Employee getEmployeeById(int id);
List<Employee> getAllEmployees();
int addEmployee(Employee employee);
int updateEmployee(Employee employee);
int deleteEmployeeById(int id);
int deleteAllEmployees();
void truncateEmployeeTable();
}
EmployeeServiceImpl.java
package com.zfl9.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.zfl9.mapper.EmployeeMapper;
import com.zfl9.model.Employee;
@Service
public class EmployeeServiceImpl implements EmployeeService {
private final EmployeeMapper employeeMapper;
@Autowired
public EmployeeServiceImpl(EmployeeMapper employeeMapper) {
this.employeeMapper = employeeMapper;
}
@Override
public Employee getEmployeeById(int id) {
return employeeMapper.getEmployeeById(id);
}
@Override
public List<Employee> getAllEmployees() {
return employeeMapper.getAllEmployees();
}
@Override
@Transactional(rollbackFor = Exception.class)
public int addEmployee(Employee employee) {
return employeeMapper.addEmployee(employee);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int updateEmployee(Employee employee) {
return employeeMapper.updateEmployee(employee);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteEmployeeById(int id) {
return employeeMapper.deleteEmployeeById(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteAllEmployees() {
return employeeMapper.deleteAllEmployees();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void truncateEmployeeTable() {
employeeMapper.truncateEmployeeTable();
}
}
EmployeeController.java
package com.zfl9.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import com.zfl9.model.Employee;
import com.zfl9.service.EmployeeService;
@Controller
@RequestMapping("/employees")
public class EmployeeController {
private final EmployeeService employeeService;
@Autowired
public EmployeeController(EmployeeService employeeService) {
this.employeeService = employeeService;
}
@GetMapping
public String listEmployee(Model model) {
model.addAttribute("employees", employeeService.getAllEmployees());
return "employee-list";
}
@GetMapping("/new")
public String newEmployee(Model model) {
model.addAttribute("title", "Create Employee");
model.addAttribute("employee", new Employee());
return "employee-edit";
}
@GetMapping("/edit/{id}")
public String editEmployee(Model model, @PathVariable int id) {
model.addAttribute("title", "Update Employee");
model.addAttribute("employee", employeeService.getEmployeeById(id));
return "employee-edit";
}
@PostMapping("/save")
public String saveEmployee(@ModelAttribute Employee employee) {
if (employee.getId() == null) {
employeeService.addEmployee(employee);
} else {
employeeService.updateEmployee(employee);
}
return "redirect:/employees";
}
@GetMapping("/delete/{id}")
public String deleteEmployee(@PathVariable String id) {
if ("all".equals(id)) {
employeeService.deleteAllEmployees();
} else {
employeeService.deleteEmployeeById(Integer.valueOf(id));
}
return "redirect:/employees";
}
}
index.html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<div align="center">
<h2><a href="employees">Employee Management System</a></h2>
</div>
</body>
</html>
employee-list.ftl
<#assign ctx = springMacroRequestContext.contextPath>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>List Employee</title>
</head>
<body>
<div align="center">
<h1>List Employee</h1>
<table border="1">
<tr>
<th>Id</th>
<th>Name</th>
<th>Email</th>
<th>Address</th>
<th>Telephone</th>
<th>Action</th>
</tr>
<#list employees as employee>
<tr>
<td>${employee.id}</td>
<td>${employee.name!}</td>
<td>${employee.email!}</td>
<td>${employee.address!}</td>
<td>${employee.telephone!}</td>
<td>
<a href="${ctx}/employees/edit/${employee.id}">Edit</a>
<a href="${ctx}/employees/delete/${employee.id}">Delete</a>
</td>
</tr>
</#list>
</table>
<h3><a href="${ctx}/employees/new">Create Employee</a></h3>
<h3><a href="${ctx}/employees/delete/all">Delete All Employee</a></h3>
</div>
</body>
</html>
employee-edit.ftl
<#assign ctx = springMacroRequestContext.contextPath>
<#import "spring.ftl" as spring>
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Edit Employee</title>
</head>
<body>
<div align="center">
<h1>${title}</h1>
<form action="${ctx}/employees/save" method="post">
<@spring.formHiddenInput "employee.id"/>
<table>
<tr>
<td>Name:</td>
<td>
<@spring.formInput "employee.name"/>
</td>
</tr>
<tr>
<td>Email:</td>
<td>
<@spring.formInput "employee.email"/>
</td>
</tr>
<tr>
<td>Address:</td>
<td>
<@spring.formInput "employee.address"/>
</td>
</tr>
<tr>
<td>Telephone:</td>
<td>
<@spring.formInput "employee.telephone"/>
</td>
</tr>
</table>
<input type="submit" value="Save">
</form>
</div>
</body>
</html>
命令行参数与 Profile
命令行参数传递 properties 属性
springboot 的配置文件为 application.properties
,properties 就是所谓的“属性”文件,即 java.util.Properties
;我们通过 JVM 的启动参数 -Dname=value
可以传递 property 进去,JVM 会将这些 name/value 对保存到 System.getProperties()
这个 properties 属性类中,假设运行方式为 java -Dname=value com.zfl9.MainApp
,那么我们就可以在 MainApp 中通过 System.getProperty("name")
来读取该属性对应的值("value"
),可以传递多个 -D
参数;java 的 properties 类似于操作系统的环境变量,key 和 value 都是字符串类型。
SpringBoot 允许我们通过命令行参数提供 application.properties 中的属性值(jar 运行方式),如改变 tomcat 的监听端口:java -jar springboot-helloworld-webapp.jar --server.port=80
语法非常简单,就是在 property 的 key 前面加上 --
就行了,命令行提供的属性值优先于 application.properties 中的属性值。
当然,也可以直接通过 jvm 的 -D
选项传递 properties 属性,如:java -Dserver.port=80 -jar springboot-helloworld-webapp.jar
如果同时指定了 -Dserver.port=80
jvm 启动参数、--server.port=80
命令行参数,则后者的优先级高。
springboot 的多环境支持(profile)
我们在开发 Spring Boot 应用时,通常同一套程序会被应用和安装到几个不同的环境,比如:开发环境(dev)、测试环境*(test)、生产环境(prod)。其中每个环境的数据源配置、服务器端口等配置都会不同,如果在为不同环境打包时都要频繁修改配置文件的话,那必将是个非常繁琐且容易发生错误的事。
对于多环境的配置,各种项目构建工具或是框架的基本思路是一致的,即通过配置多份不同环境的配置文件,再通过打包命令指定需要打包的内容之后进行区分打包,Spring Boot 也不例外,或者说更加简单。在 Spring Boot 中,多环境配置文件名需要满足 application-{profile}.properties
的格式,其中 {profile}
对应你的环境标识,比如:
application-dev.properties
:开发环境application-test.properties
:测试环境application-prod.properties
:生产环境
至于使用哪个运行环境,需要在 application.properties 文件中通过 spring.profiles.active
属性来设置,其值对应 {profile}
值。如:spring.profiles.active=test
就会加载 application-test.properties
配置文件内容(无论什么运行环境,application.properties
配置文件总是会被加载,然后才是加载对应环境下的配置文件,如果存在同名属性,则对应环境下的配置文件的属性值优先级更高);前面提到了,我们可以通过命令行参数的形式提供 springboot 的属性值,且命令行方式指定的属性值的优先级是最高的。
通常,application.properties
中配置通用的属性以及默认的属性,而在 application-{profile}.properties
中配置与环境相关的属性(可覆盖 application.properties 中的同名属性),然后在 application.properties
中设置 spring.profiles.active=dev
来指定默认的运行环境为开发环境,然后通过命令行参数形式,提供 --spring.profiles.active=prod
参数来切换为生产环境。
例子:在 src/main/resources 目录下,创建 3 个不同环境下的配置文件,且设置一个不同的 server.port 监听端口:
application-dev.properties
:监听端口为 111application-test.properties
:监听端口为 222application-prod.properties
:监听端口为 333
在 application.properties 文件中设置默认的运行环境为 dev,也就是说,如果更改 spring.profiles.active
属性,那么默认就是监听 111 端口。
然后分别以下面三种形式运行我们的 springboot 应用:
java -jar test.jar
:监听 111 端口java -jar test.jar --spring.profiles.active=dev
:监听 111 端口java -jar test.jar --spring.profiles.active=test
:监听 222 端口java -jar test.jar --spring.profiles.active=prod
:监听 333 端口
SpringBoot 打包为 War
第一步:修改 pom.xml,将 packaging 打包方式改为 war(默认为 jar):
<packaging>war</packaging>
第二步:修改 pom.xml,添加 spring-boot-starter-tomcat
依赖,将 scope 改为 provided:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
完整 pom.xml 参考如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zfl9.springboot</groupId>
<artifactId>springboot-ssm</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
第三步:修改 Application 启动类,继承 SpringBootServletInitializer
初始化类,并重写 configure() 方法:
package com.zfl9;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
@MapperScan("com.zfl9.mapper")
public class Application extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}
}
最后,执行 mvn clean package
,将 target 目录下的 war 包拷贝到 Tomcat 的 webapps 目录下,打开浏览器测试吧。