Spring boot
标签(空格分隔): springboot
HelloWorld
什么是spring boot
Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是spring boot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架(不知道这样比喻是否合适)。
使用spring boot有什么好处
其实就是简单、快速、方便!平时如果我们需要搭建一个spring web项目的时候需要怎么做呢?
- 配置web.xml,加载spring和spring mvc
- 配置数据库连接、配置spring事务
- 配置加载配置文件的读取,开启注解
- 配置日志文件
- …
- 配置完成之后部署tomcat 调试
- …
现在非常流行微服务,如果我这个项目仅仅只是需要发送一个邮件,如果我的项目仅仅是生产一个积分;我都需要这样折腾一遍!
但是如果使用spring boot呢?
很简单,我仅仅只需要非常少的几个配置就可以迅速方便的搭建起来一套web项目或者是构建一个微服务!
所以spring boot的优点为:
- 为所有Spring开发者更快的入门
- 开箱即用,提供各种默认配置来简化项目配置
- 内嵌式容器简化Web项目
- 没有冗余代码生成和XML配置的要求
快速入门
本节主要目标完成Spring Boot基础项目的构建,并且实现一个简单的Http请求处理,通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。
系统要求:
- Java 7 及以上
- Spring Framework 4.1.5及以上
本教材采用Java 1.8.0_131
、Spring Boot 1.5.10
实现。
虽然JDK目前已经发布1.9版本,但是目前在互联网公司中,使用JDK1.8版本的更多,而传统软件公司中很多还停留在JDK1.6,JDK1.7,甚至还有JDK1.5的。
spring boot也在2018年3月发布了2.0的正式版本,但是对现在来说还是太新。
使用Maven构建项目
通过SPRING INITIALIZR工具产生基础项目
选择构建工具Maven Project
、Spring Boot
版本1.5.10
以及一些工程基本信息,可参考下图所示:
点击Generate Project
下载项目压缩包,解压项目包,并用IDE以Maven项目导入,以IntelliJ IDEA
为例:
菜单中选择File
–>New
–>Project from Existing Sources...
选择解压后的项目文件夹,点击OK
点击Import project from external model
并选择Maven
,点击Next
到底为止。
若你的环境有多个版本的JDK,注意到选择Java SDK的时候请选择Java 7`以上的版本
项目结构解析
前面构建的spring boot项目的目录结构如下:
通过上面步骤完成了基础项目的创建,如上图所示,Spring Boot的基础结构共三个文件(具体路径根据用户生成项目时填写的Group所有差异):
src/main/java
下的程序入口:SbDemoApplication.java
src/main/resources
下的配置文件:application.properties
src/test/
下的测试入口:SbDemoApplicationTests.java
生成的SbDemoApplication.java
和SbDemoApplicationTests.java
类都可以直接运行来启动当前创建的项目,由于目前该项目未配合任何数据访问或Web模块,程序会在加载完Spring之后结束运行。
引入Web模块
当前的pom.xml
内容如下,仅引入了两个模块:
spring-boot-starter
:核心模块,包括自动配置支持、日志和YAML
spring-boot-starter-test
:测试模块,包括JUnit、Hamcrest、Mockito
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
引入Web模块,需添加spring-boot-starter-web
模块:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
编写HelloWorld
服务
创建package
命名为com.dengcl.sb_demo.web
(根据实际情况修改)
创建HelloController
类,内容如下
@RestController
public class HelloController {
@RequestMapping("/hello")
public String sayHello() {
return "Hello World";
}
}
启动主程序,执行src/main/java
下的程序入口:SbDemoApplication.java
,在控制台出现如下图内容:
打开浏览器访问http://localhost:8080/hello
,可以看到页面输出Hello World
编写单元测试用例·
打开的src/test/
下的测试入口SbDemoApplicationTests.java
类。下面编写一个简单的单元测试来模拟http请求,具体如下:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SbDemoApplicationTests {
@Autowired
private MockMvc mvc;
@Test
public void getHello() throws Exception {
mvc.perform(get("/hello").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().string(equalTo("Hello World!")));
}
}
注意引入下面内容,让status
、content
、equalTo
函数可用
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
至此已完成目标,通过Maven
构建了一个空白Spring Boot
项目,再通过引入web
模块实现了一个简单的请求处理。
使用IDEA中的Spring Initializr来快速构建Spring Boot/Cloud工程
前面提到使用SPRING INITIALIZR
页面工具来创建spring boot
项目,接下来将介绍嵌入的IDEA中的Spring Initializr
工具,它同Web提供的创建功能一样,可以帮助我们快速的构建出一个基础的Spring Boot
工程
菜单栏中选择File
=>New
=>Project
..,我们可以看到如下图所示的创建功能窗口。其中Initial Service Url
指向的地址就是Spring官方提供的Spring Initializr
工具地址,所以这里创建的工程实际上也是基于它的Web工具来实现的。
点击Next
,等待片刻后,我们可以看到如下图所示的工程信息窗口,在这里我们可以编辑我们想要创建的工程信息。其中,Type可以改变我们要构建的工程类型,比如:Maven
、Gradle
;Language可以选择:Java
、Groovy
、Kotlin
。
点击Next
,进入选择Spring Boot版本和依赖管理的窗口。在这里值的我们关注的是,它不仅包含了Spring Boot Starter POMs中的各个依赖,还包含了Spring Cloud的各种依赖。
点击Next
,进入最后关于工程物理存储的一些细节。最后,点击Finish
就能完成工程的构建了。
IDEA中的Spring Initializr虽然还是基于官方Web实现,但是通过工具来进行调用并直接将结果构建到我们的本地文件系统中,让整个构建流程变得更加顺畅,还没有体验过此功能的Spring Boot/Cloud爱好者们不妨可以尝试一下这种不同的构建方式。
Spring Boot构建RESTful API与单元测试
首先,回顾并详细说明一下在前面使用的@Controller
、@RestController
、@RequestMapping
注解。可以发现这些注解都和前面学习的Spring MVC
中一致,其实spring boot的web实现就是通过spring mvc来实现的。
@Controller
:修饰class,用来创建处理http请求的对象
@RestController
:Spring4之后加入的注解,原来在@Controller
中返回json
需要@ResponseBody
来配合,如果直接用@RestController
替代@Controller
就不需要再配置@ResponseBody
,默认返回json
格式。
@RequestMapping
:配置url
映射
下面我们尝试使用Spring MVC来实现一组对User对象操作的RESTful API,配合注释详细说明在Spring MVC中如何映射HTTP请求、如何传参、如何编写单元测试。
RESTful API具体设计如下:
User实体定义:
public class User {
private Long id;
private String name;
private Integer age;
// 省略setter和getter
}
实现对User对象的操作接口UserController
@RestController
@RequestMapping(value="/users") // 通过这里配置使下面的映射都在/users下
public class UserController {
// 创建线程安全的Map
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());
@RequestMapping(value="/", method= RequestMethod.GET)
public List<User> getUserList() {
// 处理"/users/"的GET请求,用来获取用户列表
// 还可以通过@RequestParam从页面中传递参数来进行查询条件或者翻页信息的传递
List<User> r = new ArrayList<User>(users.values());
return r;
}
@RequestMapping(value="/", method=RequestMethod.POST)
public String postUser(@ModelAttribute User user) {
// 处理"/users/"的POST请求,用来创建User
// 除了@ModelAttribute绑定参数之外,还可以通过@RequestParam从页面中传递参数
users.put(user.getId(), user);
return "success";
}
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public User getUser(@PathVariable Long id) {
// 处理"/users/{id}"的GET请求,用来获取url中id值的User信息
// url中的id可通过@PathVariable绑定到函数的参数中
return users.get(id);
}
@RequestMapping(value="/{id}", method=RequestMethod.PUT)
public String putUser(@PathVariable Long id, @ModelAttribute User user) {
// 处理"/users/{id}"的PUT请求,用来更新User信息
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}
@RequestMapping(value="/{id}", method=RequestMethod.DELETE)
public String deleteUser(@PathVariable Long id) {
// 处理"/users/{id}"的DELETE请求,用来删除User
users.remove(id);
return "success";
}
}
针对该Controller编写测试用例验证正确性,具体如下。当然也可以通过浏览器插件等进行请求提交验证。
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.equalTo;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {
@Autowired
private MockMvc mvc;
@Test
public void testUserController() throws Exception {
// 测试UserController
RequestBuilder request = null;
// 1、get查一下user列表,应该为空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
// 2、post提交一个user
request = post("/users/")
.param("id", "1")
.param("name", "测试大师")
.param("age", "20");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 3、get获取user列表,应该有刚才插入的数据
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[{"id":1,"name":"测试大师","age":20}]")));
// 4、put修改id为1的user
request = put("/users/1")
.param("name", "测试终极大师")
.param("age", "30");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 5、get一个id为1的user
request = get("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("{"id":1,"name":"测试终极大师","age":30}")));
// 6、del删除id为1的user
request = delete("/users/1");
mvc.perform(request)
.andExpect(content().string(equalTo("success")));
// 7、get查一下user列表,应该为空
request = get("/users/");
mvc.perform(request)
.andExpect(status().isOk())
.andExpect(content().string(equalTo("[]")));
}
至此,我们通过引入web模块(没有做其他的任何配置),就可以轻松利用Spring MVC的功能,以非常简洁的代码完成了对User
对象的RESTful API的创建以及单元测试的编写。其中同时介绍了Spring MVC中最为常用的几个核心注解:@Controller
,@RestController
,RequestMapping
以及一些参数绑定的注解:@PathVariable
,@ModelAttribute
,@RequestParam
等。
使用Thymeleaf模板引擎渲染web视图
在前面章节中我们完成了一个简单的RESTful Service,体验了快速开发的特性。但是如何把处理结果渲染到页面上呢?那么本篇就在上篇基础上介绍一下如何进行Web应用的开发。
静态资源访问
在我们开发Web应用的时候,需要引用大量的js、css、图片等静态资源。
默认配置
Spring Boot默认提供静态资源目录位置需置于classpath
下,目录名需符合如下规则:
/static
/public
/resources
/META-INF/resources
举例:我们可以在src/main/resources/
目录下创建static
,在该位置放置一个图片文件。启动程序后,尝试访问http://localhost:8080/D.jpg
。如能显示图片,配置成功。
渲染Web页面
在之前的示例中,我们都是通过@RestController
来处理请求,所以返回的内容为json
对象。那么如果需要渲染html页面的时候,要如何实现呢?
模板引擎
在动态HTML实现上Spring Boot依然可以完美胜任,并且提供了多种模板引擎的默认配置支持,所以在推荐的模板引擎下,我们可以很快的上手开发动态网站。
Spring Boot提供了默认配置的模板引擎主要有以下几种:
- Thymeleaf
- FreeMarker
- Velocity
- Groovy
- Mustache
Spring Boot建议使用这些模板引擎,避免使用JSP,若一定要使用JSP将无法实现Spring Boot的多种特性。
当使用上述模板引擎中的任何一个,它们默认的模板配置路径为:src/main/resources/templates
。当然也可以修改这个路径,具体如何修改,可在后续模板引擎的配置属性中查询并修改。
Thymeleaf
Thymeleaf是一个XML/XHTML/HTML5模板引擎,可用于Web与非Web环境中的应用开发。它是一个开源的Java库,基于Apache License 2.0许可,由Daniel Fernández创建,该作者还是Java加密库Jasypt的作者。
Thymeleaf提供了一个用于整合Spring MVC的可选模块,在应用开发中,你可以使用Thymeleaf来完全代替JSP或其他模板引擎,如Velocity、FreeMarker等。Thymeleaf的主要目标在于提供一种可被浏览器正确显示的、格式良好的模板创建方式,因此也可以用作静态建模。你可以使用它创建经过验证的XML与HTML模板。相对于编写逻辑或代码,开发者只需将标签属性添加到模板中即可。接下来,这些标签属性就会在DOM(文档对象模型)上执行预先制定好的逻辑。
示例模板:
<table>
<thead>
<tr>
<th th:text="#{msgs.headers.name}">Name</td>
<th th:text="#{msgs.headers.price}">Price</td>
</tr>
</thead>
<tbody>
<tr th:each="prod : ${allProducts}">
<td th:text="${prod.name}">Oranges</td>
<td th:text="${#numbers.formatDecimal(prod.price,1,2)}">0.99</td>
</tr>
</tbody>
</table>
可以看到Thymeleaf主要以属性的方式加入到html标签中,浏览器在解析html时,当检查到没有的属性时候会忽略,所以Thymeleaf的模板可以通过浏览器直接打开展现,这样非常有利于前后端的分离。
在Spring Boot中使用Thymeleaf,只需要引入下面依赖,并在默认的模板路径src/main/resources/templates
下编写模板文件即可完成。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在完成配置之后,举一个简单的例子,在前面工程的基础上,举一个简单的示例来通过Thymeleaf渲染一个页面。
定义Controller
@Controller
@RequestMapping("/view")
public class ViewController {
@RequestMapping(value = "/",method = RequestMethod.GET)
public String index(Model model)
{
// 加入一个属性,用来在模板中读取
model.addAttribute("host","http://www.dengcl.com");
// return模板文件的名称,对应src/main/resources/templates/index.html
return "index";
}
}
定义Thymeleaf模板页面index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" >
<head>
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
<h1 th:text="${host}">Hello World</h1>
</body>
</html>
注意:
index.html
文件在/src/main/resource/templates/
目录下。
如上页面,直接打开html页面展现Hello World
,但是启动程序后,访问http://localhost:8080/view
,则是展示Controller中host的值:·http://blog.didispace.com·,做到了不破坏HTML自身内容的数据逻辑分离。
Thymeleaf的默认参数配置
如有需要修改默认配置的时候,只需复制下面要修改的属性到application.properties中,并修改成需要的值,如修改模板文件的扩展名,修改默认的模板路径等。
# Enable template caching.
spring.thymeleaf.cache=true
# Check that the templates location exists.
spring.thymeleaf.check-template-location=true
# Content-Type value.
spring.thymeleaf.content-type=text/html
# Enable MVC Thymeleaf view resolution.
spring.thymeleaf.enabled=true
# Template encoding.
spring.thymeleaf.encoding=UTF-8
# Comma-separated list of view names that should be excluded from resolution.
spring.thymeleaf.excluded-view-names=
# Template mode to be applied to templates. See also StandardTemplateModeHandlers.
spring.thymeleaf.mode=HTML5
# Prefix that gets prepended to view names when building a URL.
spring.thymeleaf.prefix=classpath:/templates/
# Suffix that gets appended to view names when building a URL.
虽然spring boot支持这么多模板引擎实现视图渲染,但是所有的这些都是后端生成UI,在互联网的应用中还是建议设计成基于api的系统,后台只提供api,前端开发html app。基于vue,react都可以,不过vue更轻量级。
Spring Boot中使用Swagger2构建强大的RESTful API文档
由于Spring Boot能够快速开发、便捷部署等特性,相信有很大一部分Spring Boot的用户会用来构建RESTful API。而我们构建RESTful API的目的通常都是由于多终端的原因,这些终端会共用很多底层业务逻辑,因此我们会抽象出这样一层来同时服务于多个移动端或者Web前端。
这样一来,我们的RESTful API就有可能要面对多个开发人员或多个开发团队:IOS开发、Android开发或是Web开发等。为了减少与其他团队平时开发期间的频繁沟通成本,传统做法我们会创建一份RESTful API文档来记录所有接口细节,然而这样的做法有以下几个问题:
-
由于接口众多,并且细节复杂(需要考虑不同的HTTP请求类型、HTTP头部信息、HTTP请求内容等),高质量地创建这份文档本身就是件非常吃力的事,下游的抱怨声不绝于耳。
-
随着时间推移,不断修改接口实现的时候都必须同步修改接口文档,而文档与代码又处于两个不同的媒介,除非有严格的管理机制,不然很容易导致不一致现象。
为了解决上面这样的问题,本文将介绍RESTful API的重磅好伙伴Swagger2,它可以轻松的整合到Spring Boot中,并与Spring MVC程序配合组织出强大RESTful API文档。它既可以减少我们创建文档的工作量,同时说明内容又整合入实现代码中,让维护文档和修改代码整合为一体,可以让我们在修改代码逻辑的同时方便的修改文档说明。另外Swagger2也提供了强大的页面测试功能来调试每个RESTful API。具体效果如下图所示:
下面来具体介绍,如果在Spring Boot中使用Swagger2。首先,我们需要一个Spring Boot实现的RESTful API工程,这里使用前面的为用户构建RESTFul API的那个工程来持续开发。
添加依赖
在pom.xml
中加入Swagger2的依赖
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.2.2</version>
</dependency>
创建Swagger2配置类
在Springboot的入口函数类SbDemoApplication
同级创建Swagger2的配置类SwaggerConfig
。其代码如下:
@Configuration //定义这是一个spring的配置类
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())//API的说明信息
.select()
//选定要生成API的接口或者类的父包
.apis(RequestHandlerSelectors.basePackage("com.dengcl.sb_demo.controller"))
.paths(PathSelectors.any()) //路径规则,这里是包中所有
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("XXXX项目接口说明文档,使用Swagger2实现")
.description("XXXX项目接口说明文档的描述信息")
.termsOfServiceUrl("http://127.0.0.1:8080")
.contact("后台研发团队")
.version("0.0.1-SNAPSHOT")
.build();
}
}
如上代码所示,通过@Configuration
注解,让Spring来加载该类配置。再通过@EnableSwagger2
注解来启用Swagger2。
再通过createRestApi
函数创建Docket
的Bean
之后,apiInfo()
用来创建该Api的基本信息(这些基本信息会展现在文档页面中)。select()
函数返回一个ApiSelectorBuilder
实例用来控制哪些接口暴露给Swagger来展现,本例采用指定扫描的包路径来定义,Swagger会扫描该包下所有Controller
定义的API,并产生文档内容(除了被@ApiIgnore
指定的请求)
启动spring boot应用,在浏览器输入:localhost:8080/swagger-ui.html 就可以查看接口文档了,并且可以在该文档上执行API进行测试验证。如下图所示:
添加文档内容
虽然在完成了上述配置后,已经可以生产文档内容,但是这样的文档主要针对请求本身,而描述主要来源于函数等命名产生,对用户并不友好,我们通常需要自己增加一些说明来丰富文档内容。如下所示,我们通过@ApiOperation
注解来给API增加说明、通过@ApiImplicitParams
、@ApiImplicitParam
注解来给参数增加说明。
@RestController
@RequestMapping(value="/users") // 通过这里配置使下面的映射都在/users下
public class UserController {
// 创建线程安全的Map
static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());
@ApiOperation(value="获取用户列表", notes="")
@RequestMapping(value={""}, method=RequestMethod.GET)
public List<User> getUserList() {
List<User> r = new ArrayList<User>(users.values());
return r;
}
@ApiOperation(value="创建用户", notes="根据User对象创建用户")
@ApiImplicitParam(name = "user", value = "用户详细实体user", required = true, dataType = "User")
@RequestMapping(value="", method=RequestMethod.POST)
public String postUser(@RequestBody User user) {
users.put(user.getId(), user);
return "success";
}
@ApiOperation(value="获取用户详细信息", notes="根据url的id来获取用户详细信息")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Long")
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public User getUser(@PathVariable Long id) {
return users.get(id);
}
@ApiOperation(value="更新用户详细信息", notes="根据url的id来指定更新对象,并根据传过来的user信息来更新用户详细信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Long"),
@ApiImplicitParam(name = "user", value = "用户详细实体user", required = true, dataType = "User")
})
@RequestMapping(value="/{id}", method=RequestMethod.PUT)
public String putUser(@PathVariable Long id, @RequestBody User user) {
User u = users.get(id);
u.setName(user.getName());
u.setAge(user.getAge());
users.put(id, u);
return "success";
}
@ApiOperation(value="删除用户", notes="根据url的id来指定删除对象")
@ApiImplicitParam(name = "id", value = "用户ID", required = true, dataType = "Long")
@RequestMapping(value="/{id}", method=RequestMethod.DELETE)
public String deleteUser(@PathVariable Long id) {
users.remove(id);
return "success";
}
}
完成上述代码添加上,启动Spring Boot程序,访问:http://localhost:8080/swagger-ui.html
。就能看到前文所展示的RESTful API的页面。我们可以再点开具体的API请求,以POST类型的/users请求为例,可找到上述代码中我们配置的Notes信息以及参数user的描述信息,如下图所示。
API文档访问与调试
在上图请求的页面中,我们看到user的Value是个输入框?是的,Swagger除了查看接口功能外,还提供了调试测试功能,我们可以点击上图中右侧的Model Schema(黄色区域:它指明了User的数据结构),此时Value中就有了user对象的模板,我们只需要稍适修改,点击下方“Try it out!”按钮,即可完成了一次请求调用!
此时,你也可以通过几个GET请求来验证之前的POST请求是否正确。
相比为这些接口编写文档的工作,我们增加的配置内容是非常少而且精简的,对于原有代码的侵入也在忍受范围之内。因此,在构建RESTful API的同时,加入swagger来对API文档进行管理,是个不错的选择。
统一异常处理
我们在做Web应用的时候,请求处理过程中发生错误是非常常见的情况。Spring Boot提供了一个默认的映射:/error
,当处理中抛出异常之后,会转到该请求中处理,并且该请求有一个全局的错误页面用来展示异常内容。
选择上一章节实现过的Web应用为基础,启动该应用,访问一个不存在的URL,或是修改处理内容,直接抛出异常,如:
@RestController
public class HelloWorld {
@RequestMapping("/hello")
public String sayHello(){
return "Hello World!";
}
@RequestMapping("/happenError")
public String happenError() throws Exception {
throw new Exception("发生错误");
}
}
此时启动spring boot,访问/happenError
,可以看到类似下面的报错页面,该页面就是Spring Boot提供的默认error映射页面。
统一异常处理
虽然,Spring Boot中实现了默认的error映射,但是在实际应用中,上面你的错误页面对用户来说并不够友好,我们通常需要去实现我们自己的异常提示。
下面我们在上一章的应用中来进行统一异常处理的改造。
创建全局异常处理类:通过使用@ControllerAdvice
定义统一的异常处理类,而不是在每个Controller中逐个定义。@ExceptionHandler
用来定义函数针对的异常类型,最后将Exception
对象和请求URL映射到error.html
中
@ControllerAdvice
class GlobalExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
实现error.html
页面展示:在templates
目录下创建error.html
,将请求的URL
和Exception
对象的message
输出。
<!DOCTYPE html>
<html>
<head lang="en" >
<meta charset="UTF-8" />
<title>统一异常处理</title>
</head>
<body>
<h1>Error Handler</h1>
<div th:text="${url}"></div>
<div th:text="${exception.message}"></div>
</body>
</html>
启动该应用,访问:http://localhost:8080/happenError,可以看到如下错误提示页面。
通过实现上述内容之后,我们只需要在Controller
中抛出Exception
,当然我们可能会有多种不同的Exception
。然后在@ControllerAdvice
类中,根据抛出的具体Exception
类型匹配@ExceptionHandle
r中配置的异常类型来匹配错误映射和处理。
返回JSON格式
在上述例子中,通过@ControllerAdvice
统一定义不同Exception
映射到不同错误处理页面。而当我们要实现RESTful API时,返回的错误是JSON
格式的数据,而不是HTML
页面,这时候我们也能轻松支持。
本质上,只需在@ExceptionHandler
之后加入@ResponseBody
,就能让处理函数return的内容转换为JSON
格式。
下面以一个具体示例来实现返回JSON
格式的异常处理。
- 创建统一的JSON返回对象,code:消息类型,message:消息内容,url:请求的url,data:请求返回的数据
public class ErrorInfo<T> {
public static final Integer OK = 0;
public static final Integer ERROR = 100;
private Integer code;
private String message;
private String url;
private T data;
// 省略getter和setter
}
- 修改统一异常处理类
@ControllerAdvice
class GlobalExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
@ResponseBody //异常时响应JSON
public ErrorInfo defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
ErrorInfo errorInfo = new ErrorInfo();
errorInfo.setUrl(req.getRequestURL().toString());
//NoHandlerFoundException对应的是404异常
if(e instanceof NoHandlerFoundException){
errorInfo.setCode(404);
errorInfo.setMessage("页面找不到");
}else{
errorInfo.setCode(500);
errorInfo.setMessage(e.getMessage());
}
return errorInfo;
}
}
- spring boot的配置文件中增加以下配置
#出现错误时, 直接抛出异常
spring.mvc.throw-exception-if-no-handler-found=true
#不要为我们工程中的资源文件建立映射,
spring.resources.add-mappings=false
说明: spring boot中发生404,默认处理是servlet容器的404处理方案,而不是抛出异常,需要增加上面的配置后才能处理404.
设置spring.resources.add-mappings=false
后,客户端不能直接访问工程中的资源文件了。
如果我们访问一个不存在映射地址时,如下:
至此,已完成在Spring Boot中创建统一的异常处理,实际实现还是依靠Spring MVC的注解,更多更深入的使用可参考Spring MVC的文档。
Spring Boot日志
Spring Boot在所有内部日志中默认使用Commons Logging,但是默认配置也提供了对常用日志的支持,如:Java Util Logging
,Log4J
, Log4J2
和Logback
。每种Logger都可以通过配置使用控制台或者文件输出日志内容。
Spring Boot日志管理
格式化日志
默认的日志输出如下:
2018-03-16 11:01:19.728 INFO 10600 --- [ main] d.s.w.p.DocumentationPluginsBootstrapper : Context refreshed
输出内容元素具体如下:
- 时间日期 — 精确到毫秒
- 日志级别 — ERROR, WARN, INFO, DEBUG or TRACE
- 进程ID
- 分隔符 — --- 标识实际日志的开始
- 线程名 — 方括号括起来(可能会截断控制台输出)
- Logger名 — 通常使用源代码的类名
- 日志内容
控制台输出
在Spring Boot中默认配置了ERROR
、WARN
和INFO
级别的日志输出到控制台。
我们可以通过两种方式切换至DEBUG
级别:
- 在运行命令后加入
--debug
标志,如:java -jar myapp.jar --debug
- 在
application.properties
中配置debug=true
,该属性置为true
的时候,核心Logger(包含嵌入式容器、hibernate、spring)会输出更多内容,但是你自己应用的日志并不会输出为DEBUG级别。
多彩输出
如果你的终端支持ANSI
,设置彩色输出会让日志更具可读性。通过在application.properties
中设置spring.output.ansi.enabled
参数来支持。
- NEVER:禁用ANSI-colored输出
- DETECT:会检查终端是否支持ANSI,是的话就采用彩色输出(默认项)
- ALWAYS:总是使用ANSI-colored格式输出,若终端不支持的时候,会有很多干扰信息,不推荐使用
文件输出
Spring Boot默认配置只会输出到控制台,并不会记录到文件中,但是我们通常生产环境使用时都需要以文件方式记录。
若要增加文件输出,需要在application.properties
中配置logging.file
或logging.path
属性。
- logging.file,设置文件,可以是绝对路径,也可以是相对路径。如:
logging.file=my.log
- logging.path,设置目录,会在该目录下创建spring.log文件,并写入日志内容,如:
logging.path=/var/log
日志文件会在10Mb大小的时候被截断,产生新的日志文件,默认级别为:ERROR、WARN、INFO
级别控制
在Spring Boot中只需要在application.properties
中进行配置完成日志记录的级别控制。
配置格式:logging.level.*=LEVEL
- logging.level:日志级别控制前缀,
*
为包名或Logger名 - LEVEL:选项TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
举例:
logging.level.com.dengcl=DEBUG
:com.dengcl
包下所有class以DEBUG级别输出logging.level.root=WARN
:root日志以WARN级别输出
自定义日志配置
由于日志服务一般都在ApplicationContext
创建前就初始化了,它并不是必须通过Spring的配置文件控制。因此通过系统属性和传统的Spring Boot外部配置文件依然可以很好的支持日志控制和管理。
根据不同的日志系统,你可以按如下规则组织配置文件名,就能被正确加载:
日志系统 | 配置文件名 |
---|---|
Logback | logback-spring.xml, logback-spring.groovy, logback.xml, logback.groovy |
Log4j | log4j-spring.properties, log4j-spring.xml, log4j.properties, log4j.xml |
Log4j2 | log4j2-spring.xml, log4j2.xml |
JDK (Java Util Logging) | logging.properties |
Spring Boot官方推荐优先使用带有-spring
的文件名作为你的日志配置(如使用logback-spring.xml
,而不是logback.xml
)
自定义输出格式
在Spring Boot中可以通过在application.properties
配置如下参数控制输出格式:
logging.pattern.console
:定义输出到控制台的样式(不支持JDK Logger)logging.pattern.file
:定义输出到文件的样式(不支持JDK Logger)
Spring Boot中使用AOP统一处理Web请求日志
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是Spring框架中的一个重要内容,它通过对既有程序定义一个切入点,然后在其前后切入不同的执行内容,比如常见的有:打开数据库连接/关闭数据库连接、打开事务/关闭事务、记录日志等。基于AOP不会破坏原来程序逻辑,因此它可以很好的对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
下面主要讲两个内容,一个是如何在Spring Boot中引入Aop功能,二是如何使用Aop做切面去统一处理Web请求的日志。
准备工作
因为需要对web请求做切面来记录日志,所以先引入web模块,并创建一个简单的hello请求的处理。
pom.xml中引入web模块
引入AOP依赖
在Spring Boot中引入AOP就跟引入其他模块一样,非常简单,只需要在pom.xml中加入如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在完成了引入AOP依赖包后,一般来说并不需要去做其他配置。也许在Spring中使用过注解配置方式的人会问是否需要在程序主类中增加@EnableAspectJAutoProxy
来启用,实际并不需要。
可以看下面关于AOP的默认配置属性,其中spring.aop.auto
属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy
。
# AOP
spring.aop.auto=true # Add @EnableAspectJAutoProxy.
spring.aop.proxy-target-class=false # Whether subclass-based (CGLIB) proxies are to be created (true) as
opposed to standard Java interface-based proxies (false).
而当我们需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true
,不然默认使用的是标准Java的实现AOP。
实现Web层的日志切面
实现AOP的切面主要有以下几个要素:
- 使用
@Aspect
注解将一个java类定义为切面类 - 使用
@Pointcut
定义一个切入点,可以是一个规则表达式,比如下例中某个package下的所有函数,也可以是一个注解等。 - 根据需要在切入点不同位置的切入内容
- 使用
@Before
在切入点开始处切入内容 - 使用
@After
在切入点结尾处切入内容 - 使用
@AfterReturning
在切入点return内容之后切入内容(可以用来对处理返回值做一些加工处理) - 使用
@Around
在切入点前后切入内容,并自己控制何时执行切入点自身的内容 - 使用
@AfterThrowing
用来处理当切入内容部分抛出异常之后的处理逻辑
- 使用
切面类代码如下:
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(this.getClass());
@Pointcut("execution(public * com.dengcl.sb_demo.controller.*.*(..))")
public void pt(){}
@Before("pt()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 记录下请求内容
logger.info("URL : " + request.getRequestURL().toString());
logger.info("HTTP_METHOD : " + request.getMethod());
logger.info("IP : " + request.getRemoteAddr());
logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "ret", pointcut = "pt()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("RESPONSE : " + ret);
}
}
可以看上面的例子,通过·@Pointcut·定义的切入点为com.dengcl.sb_demo.controller
包下的所有函数(对web层所有请求处理做切入点),然后通过@Befor
e实现,对请求内容的日志记录(本文只是说明过程,可以根据需要调整内容),最后通过@AfterReturning
记录请求返回的对象。
通过运行程序并访问:http://localhost:8080/users/
,可以获得下面的日志输出
2018-03-16 11:01:32.662 INFO 10600 --- [nio-8080-exec-1] com.dengcl.sb_demo.advice.WebLogAspect : URL : http://localhost:8080/users
2018-03-16 11:01:32.662 INFO 10600 --- [nio-8080-exec-1] com.dengcl.sb_demo.advice.WebLogAspect : HTTP_METHOD : GET
2018-03-16 11:01:32.663 INFO 10600 --- [nio-8080-exec-1] com.dengcl.sb_demo.advice.WebLogAspect : IP : 0:0:0:0:0:0:0:1
2018-03-16 11:01:32.663 INFO 10600 --- [nio-8080-exec-1] com.dengcl.sb_demo.advice.WebLogAspect : CLASS_METHOD : com.dengcl.sb_demo.controller.UserController.getUserList
2018-03-16 11:01:32.665 INFO 10600 --- [nio-8080-exec-1] com.dengcl.sb_demo.advice.WebLogAspect : ARGS : []
2018-03-16 11:01:32.675 INFO 10600 --- [nio-8080-exec-1] com.dengcl.sb_demo.advice.WebLogAspect : RESPONSE : []
优化:AOP切面中的同步问题
在WebLogAspect切面中,分别通过doBefore
和doAfterReturning
两个独立函数实现了切点头部和切点返回后执行的内容,若我们想统计请求的处理时间,就需要在doBefore
处记录时间,并在doAfterReturning
处通过当前时间与开始处记录的时间计算得到请求处理的消耗时间。
那么我们是否可以在WebLogAspect
切面中定义一个成员变量来给doBefore
和doAfterReturning
一起访问呢?是否会有同步问题呢?
的确,直接在这里定义基本类型会有同步问题,所以我们可以引入ThreadLocal
对象,像下面这样进行记录:
@Aspect
@Component
public class WebLogAspect {
private Logger logger = Logger.getLogger(getClass());
ThreadLocal<Long> startTime = new ThreadLocal<>();
@Pointcut("execution(public * com.dengcl.sb_demo.controller.*.*(..))")
public void webLog(){}
@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
startTime.set(System.currentTimeMillis());
// 省略日志记录内容
}
@AfterReturning(returning = "ret", pointcut = "webLog()")
public void doAfterReturning(Object ret) throws Throwable {
// 处理完请求,返回内容
logger.info("RESPONSE : " + ret);
logger.info("SPEND TIME : " + (System.currentTimeMillis() - startTime.get()));
}
}
优化:AOP切面的优先级
由于通过AOP实现,程序得到了很好的解耦,但是也会带来一些问题,比如:我们可能会对Web层做多个切面,校验用户,校验头信息等等,这个时候经常会碰到切面的处理顺序问题。
所以,我们需要定义每个切面的优先级,我们需要@Order(i)
注解来标识切面的优先级。i的值越小,优先级越高。假设我们还有一个切面是CheckNameAspect
用来校验name
必须为didi
,我们为其设置@Order(10)
,而上文中WebLogAspect
设置为@Order(5)
,所以WebLogAspect
有更高的优先级,这个时候执行顺序是这样的:
- 在
@Before
中优先执行@Order(5)
的内容,再执行@Order(10)
的内容 - 在
@After
和@AfterReturning
中优先执行@Order(10)
的内容,再执行@Order(5)
的内容
所以我们可以这样子总结:
- 在切入点前的操作,按order的值由小到大执行
- 在切入点后的操作,按order的值由大到小执行
Spring boot整合mybatis操作数据库
添加依赖
pom.xml
中引入依赖
引入连接mysql
的必要依赖mysql-connector-java
引入整合MyBatis
的核心依赖mybatis-spring-boot-starter
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
配置数据源信息
在application.properties
中配置mysql
的连接配置
spring.datasource.url=jdbc:mysql://localhost:3306/users
spring.datasource.username=root
spring.datasource.password=12345678
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
简单且简洁的的完成了基本配置,下面看看如何在这个基础下轻松方便的使用MyBatis访问数据库。
使用MyBatis
在Mysql中创建User
表,包含id(BIGINT)、name(INT)、age(VARCHAR)字段。同时,创建映射对象User
,这里不再列出建表和映射对象的代码。
创建User映射的操作UserMapper.java
,为了后续单元测试验证,实现插入和查询操作。
@Mapper
public interface UserMapper {
@Select("SELECT * FROM USERS WHERE NAME = #{name}")
User findByName(@Param("name") String name);
@Insert("INSERT INTO USERS(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
}
@Mapper
定义本接口是一个mybatis的mapper接口类。
创建单元测试
- 测试逻辑:插入一条name=AAA,age=20的记录,然后根据name=AAA查询,并判断age是否为20
- 测试结束回滚数据,保证测试单元每次运行的数据环境独立
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper mapper;
@Test
@Transactional
@Rollback
public void findByName() throws Exception {
mapper.insert("AAA", 20);
User u = mapper.findByName("AAA");
Assert.assertEquals(20, u.getAge().intValue());
}
}
使用xml实现mapper
前面的示例是通过注解的方式来实现mybatis的mapper,但是在复杂的应用场景下,基于xml的mapper更加灵活,更方便维护。
修改UserMapper
,去掉注解
@Mapper
public interface UserMapper {
// @Select("SELECT * FROM USERS WHERE NAME = #{name}")
User findByName(@Param("name") String name);
// @Insert("INSERT INTO USERS(NAME, AGE) VALUES(#{name}, #{age})")
int insert(@Param("name") String name, @Param("age") Integer age);
}
建立UserMapper.xml
,实现mybatis映射
<?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.dengcl.sb_demo.mapper.UserMapper">
<select id="findByName" resultType="com.dengcl.sb_demo.pojo.User">
SELECT * FROM USERS WHERE NAME = #{name}
</select>
<insert id="insert" >
INSERT INTO USERS(NAME, AGE) VALUES(#{name}, #{age})
</insert>
</mapper>
特别重要:配置mybatis的mapper文件的所在位置
在application.properties
文件中增加如下配置:
mybatis.mapper-locations=classpath:/com/dengcl/sb_demo/mapper/*.xml
事务
在Spring Boot中,当我们使用了spring-boot-starter-jdbc
依赖的时候,框架会自动默认分别注入DataSourceTransactionManager
或JpaTransactionManager
。所以我们不需要任何额外配置就可以用@Transactional
注解进行事务的使用。
注意:在上面的示例中我们虽然未使用
spring-boot-starter-jdbc
依赖,但也可以直接使用事务,原因是mybatis-spring-boot-starter
中已经包含了此依赖
例如:
@Test
@Transactional
public void batchAdd(){
mapper.insert("aaa",12);
mapper.insert("aaaa",13);
mapper.insert("aaaaa",14);
mapper.insert("aaaaaa",15);
mapper.insert("aaaaaaa",16);
}
该方法中的多个数据操作就在同一个事务中。
这里是通过单元测试演示了如何使用@Transactional
注解来声明一个方法需要被事务管理,通常我们单元测试为了保证每个测试之间的数据独立,会使用@Rollback
注解让每个单元测试都能在结束时回滚。而真正在开发业务逻辑时,我们通常在service
层接口中使用@Transactional
来对各个业务逻辑进行事务管理的配置,例如:
public interface UserService {
@Transactional
User login(String name, String password);
}
事务详解
上面的例子中我们使用了默认的事务配置,可以满足一些基本的事务需求,但是当我们项目较大较复杂时(比如,有多个数据源等),这时候需要在声明事务时,指定不同的事务管理器。在声明事务时,只需要通过value属性指定配置的事务管理器名即可,例如:@Transactional(value="transactionManagerPrimary")
。
除了指定不同的事务管理器之后,还能对事务进行隔离级别和传播行为的控制,下面分别详细解释:
隔离级别
隔离级别是指若干个并发的事务之间的隔离程度,与我们开发时候主要相关的场景包括:脏读取、重复读、幻读。
我们可以看org.springframework.transaction.annotation.Isolation枚举类中定义了五个表示隔离级别的值:
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
}
DEFAULT
:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是READ_COMMITTED
。READ_UNCOMMITTED
:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读和不可重复读,因此很少使用该隔离级别。READ_COMMITTED
:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。REPEATABLE_READ
:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。即使在多次查询之间有新增的数据满足该查询,这些新增的记录也会被忽略。该级别可以防止脏读和不可重复读。SERIALIZABLE
:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
指定方法:通过使用isolation
属性设置事务的隔离级别,例如:
@Transactional(isolation = Isolation.DEFAULT)
传播行为
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。
我们可以看org.springframework.transaction.annotation.Propagation
枚举类中定义了6个表示传播行为的枚举值:
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
}
REQUIRED
:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。SUPPORTS
:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。MANDATORY
:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。REQUIRES_NEW
:创建一个新的事务,如果当前存在事务,则把当前事务挂起。NOT_SUPPORTED
:以非事务方式运行,如果当前存在事务,则把当前事务挂起。NEVER
:以非事务方式运行,如果当前存在事务,则抛出异常。NESTED
:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED
。
指定方法:通过使用propagation属性设置事务的传播行为,例如:
@Transactional(propagation = Propagation.REQUIRED)