一 SpringBoot介绍

1.1 先从Spring谈起

我们知道Spring是重量级企业开发框架 Enterprise JavaBean(EJB) 的替代品,Spring为企业级Java开发提供了一种相对简单的方法,通过 依赖注入面向切面编程 ,用简单的 Java对象(Plain Old Java Object,POJO) 实现了EJB的功能

虽然Spring的组件代码是轻量级的,但它的配置却是重量级的(需要大量XML配置) 。Spring 2.5引入了基于注解的组件扫描,这消除了大量针对应用程序自身组件的显式XML配置。Spring 3.0引入了基于Java的配置,这是一种类型安全的可重构配置方式,可以代替XML。

尽管如此,我们依旧没能逃脱配置的魔爪。开启某些Spring特性时,比如事务管理和Spring MVC,还是需要用XML或Java进行显式配置。启用第三方库时也需要显式配置,比如基于Thymeleaf的Web视图。配置Servlet和过滤器(比如Spring的DispatcherServlet)同样需要在web.xml或Servlet初始化代码里进行显式配置。组件扫描减少了配置量,Java配置让它看上去简洁不少,但Spring还是需要不少配置。

光配置这些XML文件都够我们头疼的了,占用了我们大部分时间和精力。除此之外,相关库的依赖非常让人头疼,不同库之间的版本冲突也非常常见。

不过,好消息是:Spring Boot让这一切成为了过去。

1.2 再来谈谈 Spring Boot

最好直白的介绍莫过于官方的介绍:

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”...Most Spring Boot applications need very little Spring configuration.(Spring Boot可以轻松创建独立的生产级基于Spring的应用程序,只要通过 “just run”(可能是run ‘Application’或java -jar 或 tomcat 或 maven插件run 或 shell脚本)便可以运行项目。大部分Spring Boot项目只需要少量的配置即可)

简而言之,从本质上来说,Spring Boot就是Spring,它做了那些没有它你自己也会去做的Spring Bean配置。

1.2.1 为什么需要 Spring Boot?

Spring Framework旨在简化J2EE企业应用程序开发。Spring Boot Framework旨在简化Spring开发。

why-we-need-springboot

1.2.2 Spring Boot的主要优点

  1. 开发基于 Spring 的应用程序很容易。
  2. Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。
  3. Spring Boot不需要编写大量样板代码、XML配置和注释。
  4. Spring引导应用程序可以很容易地与Spring生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security等。
  5. Spring Boot遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。
  6. Spring Boot 应用程序提供嵌入式HTTP服务器,如Tomcat和Jetty,可以轻松地开发和测试web应用程序。(这点很赞!普通运行Java程序的方式就能运行基于Spring Boot web 项目,省事很多)
  7. Spring Boot提供命令行接口(CLI)工具,用于开发和测试Spring Boot应用程序,如Java或Groovy。
  8. Spring Boot提供了多种插件,可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。

JDK

截止到目前Spring Boot 的最新版本:2.1.8.RELEASE 要求 JDK 版本在 1.8 以上,所以确保你的电脑已经正确下载安装配置了 JDK(推荐 JDK 1.8 版本)。

构建工具

构建工具(本项目涉及的代码大部分会采用 Maven 作为包管理工具):

Build ToolVersion
Maven3.3+
Gradle4.4+

开发工具推荐

推荐使用 IDEA 进行开发。最好的 Java 后台开发编辑器,没有之一!

Web 服务器

Spring Boot支持以下嵌入式servlet容器:

NameServlet Version
Tomcat 9.04.0
Jetty 9.43.1
Undertow 2.04.0

您还可以将Spring引导应用程序部署到任何Servlet 3.1+兼容的 Web 容器中。

这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。


新建 Spring Boot 项目常用的两种方式

你可以通过 https://start.spring.io/ 这个网站来生成一个 Spring Boot 的项目。

start.spring.io

注意勾选上 Spring Web 这个模块,这是我们所必需的一个依赖。当所有选项都勾选完毕之后,点击下方的按钮 Generate 下载这个 Spring Boot 的项目。下载完成并解压之后,我们直接使用 IDEA 打开即可。

当然你也可以直接通过 IDEA 来生成一个 Spring Boot 的项目,具体方法和上面类似:File->New->Project->Spring Initializr

Spring Boot 项目结构分析

成功打开项目之后,项目长下面这个样子:

以 Application为后缀名的 Java 类一般就是 Spring Boot 的启动类,比如本项目的启动项目就是HelloWorldApplication 。我们直接像运行普通 Java 程序一样运行它,由于 Spring Boot 本身就嵌入servlet容器的缘故,我们的 web 项目就运行成功了, 非常方便。

需要注意的一点是 Spring Boot 的启动类是需要最外层的,不然可能导致一些类无法被正确扫描到,导致一些奇怪的问题。 一般情况下 Spring Boot 项目结构类似下面这样

com
  +- example
    +- myproject
      +- Application.java
      |
      +- domain
      |  +- Customer.java
      |  +- CustomerRepository.java
      |
      +- service
      |  +- CustomerService.java
      |
      +- controller
      |  +- CustomerController.java
      |  
      | +- config
      |  +- swagerConfig.java
      |
  1. Application.java是项目的启动类
  2. domain目录主要用于实体(Entity)与数据访问层(Repository)
  3. service 层主要是业务类代码
  4. controller 负责页面访问控制
  5. config 目录主要放一些配置类

@SpringBootApplication 注解分析

HelloWorldApplication

@SpringBootApplication
public class HelloWorldApplication {

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

}

说到 Spring Boot 启动类就不得不介绍一下 @SpringBootApplication 注解了,这个注解的相关代码如下:

package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
   ......
}
package org.springframework.boot;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}

可以看出大概可以把 @SpringBootApplication看作是 @Configuration@EnableAutoConfiguration@ComponentScan 注解的集合。根据 SpringBoot官网,这三个注解的作用分别是:

  • @EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
  • @ComponentScan: 扫描被@Component (@Service,@Controller)注解的bean,注解默认会扫描该类所在的包下所有的类。
  • @Configuration:允许在上下文中注册额外的bean或导入其他配置类。

所以说 @SpringBootApplication就是几个重要的注解的组合,为什么要有它?当然是为了省事,避免了我们每次开发 Spring Boot 项目都要写一些必备的注解。这一点在我们平时开发中也经常用到,比如我们通常会提一个测试基类,这个基类包含了我们写测试所需要的一些基本的注解和一些依赖。

新建一个 Controller

上面说了这么多,我们现在正式开始写 Spring Boot 版的 “Hello World” 吧。

新建一个 controller 文件夹,并在这个文件夹下新建一个名字叫做 HelloWorldController 的类。

@RestController是Spring 4 之后新加的注解,如果在Spring4之前开发 RESTful Web服务的话,你需要使用@Controller 并结合@ResponseBody注解,也就是说@Controller +@ResponseBody= @RestController。对于这两个注解,我在基础篇单独抽了一篇文章来介绍。

com.example.helloworld.controller

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

@RestController
@RequestMapping("test")
public class HelloWorldController {
    @GetMapping("hello")
    public String sayHello() {
        return "Hello World";
    }
}

默认情况下,Spring Boot 项目会使用 8080 作为项目的端口号。如果我们修改端口号的话,非常简单,直接修改

application.properties配置文件即可。

src/main/resources/application.properties

server.port=8333

大功告成,运行项目

运行 HelloWorldApplication ,运行成功控制台会打印出一些消息,不要忽略这些消息,它里面会有一些比较有用的信息。

2019-10-03 09:24:47.757  INFO 26326 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8333 (http) with context path ''

上面是我截取的一段控制台打印出的内容,通过这段内容我们就知道了 Spring Boot 默认使用的 Tomact 内置 web 服务器,项目被运行在我们指定的 8333 端口上,并且项目根上下文路径是 "/"。

浏览器 http://localhost:8333/test/hello 如果你可以在页面正确得到 "Hello World" 的话,说明你已经成功完成了这部分内容。

总结

通过本文我们学到了如何新建 Spring Boot 项目、SpringBoot 项目常见的项目结构分析、@SpringBootApplication 注解分析,最后实现了 Spring Boot 版的 "Hello World"。

代码地址: https://github.com/Snailclimb/springboot-guide/tree/master/source-code/start/hello-world
(建议自己手敲一遍!!!)

代办

  •  Spring Boot 启动流程分析

RESTful Web 服务介绍

本节我们将开发一个简单的 RESTful Web 服务。

RESTful Web 服务与传统的 MVC 开发一个关键区别是返回给客户端的内容的创建方式:传统的 MVC 模式开发会直接返回给客户端一个视图,但是 RESTful Web 服务一般会将返回的数据以 JSON 的形式返回,这也就是现在所推崇的前后端分离开发。

为了节省时间,本篇内容的代码是在 Spring Boot 版 Hello World & Spring Boot 项目结构分析 基础上进行开发的。

内容概览

通过下面的内容你将学习到下面这些东西:

  1. Lombok 优化代码利器
  2. @RestController
  3. @RequestParam以及@Pathvairable
  4. @RequestMapping@GetMapping......
  5. Responsity

下载 Lombok 优化代码利器

因为本次开发用到了 Lombok 这个简化 Java 代码的工具,所以我们需要在 pom.xml 中添加相关依赖。如果对 Lombok 不熟悉的话,我强烈建议你去了解一下,可以参考这篇文章:《十分钟搞懂Java效率工具Lombok使用与原理》

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.10</version>
		</dependency>

并且你需要下载 IDEA 中支持 lombok 的插件:

 IDEA 中下载支持 lombok 的插件

RESTful Web 服务开发

假如我们有一个书架,上面放了很多书。为此,我们需要新建一个 Book 实体类。

com.example.helloworld.entity

/**
 * @author shuang.kou
 */
@Data
public class Book {
    private String name;
    private String description;
}

我们还需要一个控制器对书架上进行添加、查找以及查看。为此,我们需要新建一个 BookController

import com.example.helloworld.entity.Book;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api")
public class BookController {

    private List<Book> books = new ArrayList<>();

    @PostMapping("/book")
    public ResponseEntity<List<Book>> addBook(@RequestBody Book book) {
        books.add(book);
        return ResponseEntity.ok(books);
    }

    @DeleteMapping("/book/{id}")
    public ResponseEntity deleteBookById(@PathVariable("id") int id) {
        books.remove(id);
        return ResponseEntity.ok(books);
    }

    @GetMapping("/book")
    public ResponseEntity getBookByName(@RequestParam("name") String name) {
        List<Book> results = books.stream().filter(book -> book.getName().equals(name)).collect(Collectors.toList());
        return ResponseEntity.ok(results);
    }
}
  1. @RestController **将返回的对象数据直接以 JSON 或 XML 形式写入 HTTP 响应(Response)中。**绝大部分情况下都是直接以 JSON 形式返回给客户端,很少的情况下才会以 XML 形式返回。转换成 XML 形式还需要额为的工作,上面代码中演示的直接就是将对象数据直接以 JSON 形式写入 HTTP 响应(Response)中。关于@Controller@RestController 的对比,我会在下一篇文章中单独介绍到(@Controller +@ResponseBody= @RestController)。
  2. @RequestMapping :上面的示例中没有指定 GET 与 PUT、POST 等,因为**@RequestMapping默认映射所有HTTP Action**,你可以使用@RequestMapping(method=ActionType)来缩小这个映射。
  3. @PostMapping实际上就等价于 @RequestMapping(method = RequestMethod.POST),同样的 @DeleteMapping ,@GetMapping也都一样,常用的 HTTP Action 都有一个这种形式的注解所对应。
  4. @PathVariable :取url地址中的参数。@RequestParam url的查询参数值。
  5. @RequestBody:可以HttpRequest body 中的 JSON 类型数据反序列化为合适的 Java 类型。
  6. ResponseEntity: 表示整个HTTP Response:状态码,标头和正文内容。我们可以使用它来自定义HTTP Response 的内容。

运行项目并测试效果

这里我们又用到了开发 Web 服务必备的 Postman 来帮助我们发请求测试。

1.使用 post 请求给书架增加书籍

这里我模拟添加了 3 本书籍。

使用 post 请求给书架增加书籍

2.使用 Delete 请求删除书籍

这个就不截图了,可以参考上面发Post请求的方式来进行,请求的 url: localhost:8333/api/book/1

3.使用 Get 请求根据书名获取特定的书籍

请求的 url:localhost:8333/api/book?name=book1

总结

通过本文我们需到了使用 Lombok 来优化 Java 代码,以及一些开发 RestFul Web 服务常用的注解:@RestController@RequestMapping@PostMapping@PathVariable@RequestParam@RequestBody以及和HTTP Response 有关的 Responsity类。关于这些知识点的用法,我在上面都有介绍到,更多用法还需要自己去查阅相关文档。

代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/start/hello-world
(建议自己手敲一遍!!!)


周末的时候分享了一个技术session,讲到了@RestController@Controller,当时没有太讲清楚,因为 team 里很多同事之前不是做 Java的,所以对这两个东西不太熟悉,于是写了篇文章整理了一下。

@RestController vs @Controller

Controller 返回一个页面

单独使用 @Controller 不加 @ResponseBody的话一般使用在要返回一个视图的情况,这种情况属于比较传统的Spring MVC 的应用,对应于前后端不分离的情况。

SpringMVC 传统工作流程

@RestController 返回JSON 或 XML 形式数据

@RestController只返回对象,对象数据直接以 JSON 或 XML 形式写入 HTTP 响应(Response)中,这种情况属于 RESTful Web服务,这也是目前日常开发所接触的最常用的情况(前后端分离)。

SpringMVC+RestController

@Controller +@ResponseBody 返回JSON 或 XML 形式数据

如果你需要在Spring4之前开发 RESTful Web服务的话,你需要使用@Controller 并结合@ResponseBody注解,也就是说@Controller +@ResponseBody= @RestController(Spring 4 之后新加的注解)。

@ResponseBody 注解的作用是将 Controller 的方法返回的对象通过适当的转换器转换为指定的格式之后,写入到HTTP 响应(Response)对象的 body 中,通常用来返回 JSON 或者 XML 数据,返回 JSON 数据的情况比较多。

Spring3.xMVC RESTfulWeb服务工作流程

Reference:

示例1: @Controller 返回一个页面

当我们需要直接在后端返回一个页面的时候,Spring 推荐使用 Thymeleaf 模板引擎。Spring MVC中@Controller中的方法可以直接返回模板名称,接下来 Thymeleaf 模板引擎会自动进行渲染,模板中的表达式支持Spring表达式语言(Spring EL)。如果需要用到 Thymeleaf 模板引擎,注意添加依赖!不然会报错。

Gradle:

 compile 'org.springframework.boot:spring-boot-starter-thymeleaf'

Maven:

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

src/main/java/com/example/demo/controller/HelloController.java

@Controller
public class HelloController {
    @GetMapping("/hello")
    public String greeting(@RequestParam(name = "name", required = false, defaultValue = "World") String name, Model model) {
        model.addAttribute("name", name);
        return "hello";
    }
}

src/main/resources/templates/hello.html

Spring 会去 resources 目录下 templates 目录下找,所以建议把页面放在 resources/templates 目录下

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Getting Started: Serving Web Content</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
<p th:text="'Hello, ' + ${name} + '!'"/>
</body>
</html>

访问:http://localhost:8999/hello?name=team-c ,你将看到下面的内容

Hello, team-c!

如果要对页面在templates目录下的hello文件夹中的话,返回页面的时候像下面这样写就可以了。

src/main/resources/templates/hello/hello.html

   return "hello/hello";

示例2: @Controller+@ResponseBody 返回 JSON 格式数据

SpringBoot 默认集成了 jackson ,对于此需求你不需要添加任何相关依赖。

src/main/java/com/example/demo/controller/Person.java

public class Person {
    private String name;
    private Integer age;
    ......
    省略getter/setter ,有参和无参的construtor方法
}

src/main/java/com/example/demo/controller/HelloController.java

@Controller
public class HelloController {
    @PostMapping("/hello")
    @ResponseBody
    public Person greeting(@RequestBody Person person) {
        return person;
    }

}

使用 post 请求访问 http://localhost:8080/hello ,body 中附带以下参数,后端会以json 格式将 person 对象返回。

{
    "name": "teamc",
    "age": 1
}

示例3: @RestController 返回 JSON 格式数据

只需要将HelloController改为如下形式:

@RestController
public class HelloController {
    @PostMapping("/hello")
    public Person greeting(@RequestBody Person person) {
        return person;
    }

}

@PostConstruct@PreDestroy 是两个作用于 Servlet 生命周期的注解,相信从 Servlet 开始学 Java 后台开发的同学对他应该不陌生。

被这两个注解修饰的方法可以保证在整个 Servlet 生命周期只被执行一次,即使 Web 容器在其内部中多次实例化该方法所在的 bean。

这两个注解分别有什么作用呢

  1. @PostConstruct : 用来修饰方法,标记在项目启动的时候执行这个方法,一般用来执行某些初始化操作比如全局配置。PostConstruct 注解的方法会在构造函数之后执行,Servlet 的init()方法之前执行。
  2. @PreDestroy : 当 bean 被 Web 容器的时候被调用,一般用来释放 bean 所持有的资源。。@PreDestroy 注解的方法会在Servlet 的destroy()方法之前执行。

被这个注解修饰的方法需要满足下面这些基本条件:

  • 非静态
  • 该方法必须没有任何参数,除非在拦截器的情况下,在这种情况下,它接受一个由拦截器规范定义的InvocationContext对象
  • void()也就是没有返回值
  • 该方法抛出未检查的异常
  • ......

我们新建一个 Spring 程序,其中有一段代码是这样的,输出结果会是什么呢?


import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Configuration
public class MyConfiguration {
    public MyConfiguration() {
        System.out.println("构造方法被调用");
    }

    @PostConstruct
    private void init() {
        System.out.println("PostConstruct注解方法被调用");
    }

    @PreDestroy
    private void shutdown() {
        System.out.println("PreDestroy注解方法被调用");
    }
}


输出结果如下:

但是 J2EE已在Java 9中弃用 @PostConstruct@PreDestroy这两个注解 ,并计划在Java 11中将其删除。我们有什么更好的替代方法吗?当然有!

package cn.javaguide.config;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfiguration2 implements InitializingBean, DisposableBean {
    public MyConfiguration2() {
        System.out.println("构造方法被调用");
    }

    @Override
    public void afterPropertiesSet() {
        System.out.println("afterPropertiesSet方法被调用");
    }

    @Override
    public void destroy() {
        System.out.println("destroy方法被调用");
    }

}

输出结果如下,可以看出实现Spring 提供的 InitializingBeanDisposableBean接口的效果和使用@PostConstruct@PreDestroy 注解的效果一样。

但是,Spring 官方不推荐使用上面这种方式,Spring 官方文档是这样说的:

We recommend that you do not use the InitializingBean interface, because it unnecessarily couples the code to Spring. Alternatively, we suggest using the @PostConstruct annotation or specifying a POJO initialization method. (我们建议您不要使用 InitializingBean回调接口,因为它不必要地将代码耦合到 Spring。另外,我们建议使用@PostConstruct注解或指定bean定义支持的通用方法。)

如果你还是非要使用 Java 9 及以后的版本使用 @PostConstruct@PreDestroy 这两个注解的话,你也可以手动添加相关依赖。

Maven:

<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.2</version>
</dependency>

Gradle:

compile group: 'javax.annotation', name: 'javax.annotation-api', version: '1.3.2'

源码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/life-cycle-annotation

推荐阅读:


很多时候我们需要将一些常用的配置信息比如阿里云oss配置、发送短信的相关信息配置等等放到配置文件中。

下面我们来看一下 Spring 为我们提供了哪些方式帮助我们从配置文件中读取这些配置信息。

application.yml 内容如下:

wuhan2020: 2020年初武汉爆发了新型冠状病毒,疫情严重,但是,我相信一切都会过去!武汉加油!中国加油!

my-profile:
  name: Guide哥
  email: koushuangbwcx@163.com

library:
  location: 湖北武汉加油中国加油
  books:
    - name: 天才基本法
      description: 二十二岁的林朝夕在父亲确诊阿尔茨海默病这天,得知自己暗恋多年的校园男神裴之即将出国深造的消息——对方考取的学校,恰是父亲当年为她放弃的那所。
    - name: 时间的秩序
      description: 为什么我们记得过去,而非未来?时间“流逝”意味着什么?是我们存在于时间之内,还是时间存在于我们之中?卡洛·罗韦利用诗意的文字,邀请我们思考这一亘古难题——时间的本质。
    - name: 了不起的我
      description: 如何养成一个新习惯?如何让心智变得更成熟?如何拥有高质量的关系? 如何走出人生的艰难时刻?

1.通过 @value 读取比较简单的配置信息

使用 @Value("${property}") 读取比较简单的配置信息:

@Value("${wuhan2020}")
String wuhan2020;

需要注意的是 @value这种方式是不被推荐的,Spring 比较建议的是下面几种读取配置信息的方式。

2.通过@ConfigurationProperties读取并与 bean 绑定

LibraryProperties 类上加了 @Component 注解,我们可以像使用普通 bean 一样将其注入到类中使用。


import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@ConfigurationProperties(prefix = "library")
@Setter
@Getter
@ToString
class LibraryProperties {
    private String location;
    private List<Book> books;

    @Setter
    @Getter
    @ToString
    static class Book {
        String name;
        String description;
    }
}

这个时候你就可以像使用普通 bean 一样,将其注入到类中使用:

package cn.javaguide.readconfigproperties;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author shuang.kou
 */
@SpringBootApplication
public class ReadConfigPropertiesApplication implements InitializingBean {

    private final LibraryProperties library;

    public ReadConfigPropertiesApplication(LibraryProperties library) {
        this.library = library;
    }

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

    @Override
    public void afterPropertiesSet() {
        System.out.println(library.getLocation());
        System.out.println(library.getBooks());    }
}

控制台输出:

湖北武汉加油中国加油
[LibraryProperties.Book(name=天才基本法, description........]

3.通过@ConfigurationProperties读取并校验

我们先将application.yml修改为如下内容,明显看出这不是一个正确的 email 格式:

my-profile:
  name: Guide哥
  email: koushuangbwcx@

ProfileProperties 类没有加 @Component 注解。我们在我们要使用ProfileProperties 的地方使用@EnableConfigurationProperties注册我们的配置bean:

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;

/**
* @author shuang.kou
*/
@Getter
@Setter
@ToString
@ConfigurationProperties("my-profile")
@Validated
public class ProfileProperties {
   @NotEmpty
   private String name;

   @Email
   @NotEmpty
   private String email;
 
   //配置文件中没有读取到的话就用默认值
   private Boolean handsome = Boolean.TRUE;

}

具体使用:

package cn.javaguide.readconfigproperties;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

/**
 * @author shuang.kou
 */
@SpringBootApplication
@EnableConfigurationProperties(ProfileProperties.class)
public class ReadConfigPropertiesApplication implements InitializingBean {
    private final ProfileProperties profileProperties;

    public ReadConfigPropertiesApplication(ProfileProperties profileProperties) {
        this.profileProperties = profileProperties;
    }

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

    @Override
    public void afterPropertiesSet() {
        System.out.println(profileProperties.toString());
    }
}

因为我们的邮箱格式不正确,所以程序运行的时候就报错,根本运行不起来,保证了数据类型的安全性:

Binding to target org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'my-profile' to cn.javaguide.readconfigproperties.ProfileProperties failed:

    Property: my-profile.email
    Value: koushuangbwcx@
    Origin: class path resource [application.yml]:5:10
    Reason: must be a well-formed email address

我们把邮箱测试改为正确的之后再运行,控制台就能成功打印出读取到的信息:

ProfileProperties(name=Guide哥, email=koushuangbwcx@163.com, handsome=true)

4.@PropertySource读取指定 properties 文件

import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;

@Component
@PropertySource("classpath:website.properties")
@Getter
@Setter
class WebSite {
    @Value("${url}")
    private String url;
}

使用:

@Autowired
private WebSite webSite;

System.out.println(webSite.getUrl());//https://javaguide.cn/

5.题外话:Spring加载配置文件的优先级

Spring 读取配置文件也是有优先级的,直接上图:

更对内容请查看官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config

本文源码:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/read-config-properties


1. 使用 @ControllerAdvice和@ExceptionHandler处理全局异常

这是目前很常用的一种方式,非常推荐。测试代码中用到了 Junit 5,如果你新建项目验证下面的代码的话,记得添加上相关依赖。

1. 新建异常信息实体类

非必要的类,主要用于包装异常信息。

src/main/java/com/twuc/webApp/exception/ErrorResponse.java

/**
 * @author shuang.kou
 */
public class ErrorResponse {

    private String message;
    private String errorTypeName;
  
    public ErrorResponse(Exception e) {
        this(e.getClass().getName(), e.getMessage());
    }

    public ErrorResponse(String errorTypeName, String message) {
        this.errorTypeName = errorTypeName;
        this.message = message;
    }
    ......省略getter/setter方法
}

2. 自定义异常类型

src/main/java/com/twuc/webApp/exception/ResourceNotFoundException.java

一般我们处理的都是 RuntimeException ,所以如果你需要自定义异常类型的话直接集成这个类就可以了。

/**
 * @author shuang.kou
 * 自定义异常类型
 */
public class ResourceNotFoundException extends RuntimeException {
    private String message;

    public ResourceNotFoundException() {
        super();
    }

    public ResourceNotFoundException(String message) {
        super(message);
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

3. 新建异常处理类

我们只需要在类上加上@ControllerAdvice注解这个类就成为了全局异常处理类,当然你也可以通过 assignableTypes指定特定的 Controller类,让异常处理类只处理特定类抛出的异常。

src/main/java/com/twuc/webApp/exception/GlobalExceptionHandler.java

/**
 * @author shuang.kou
 */
@ControllerAdvice(assignableTypes = {ExceptionController.class})
@ResponseBody
public class GlobalExceptionHandler {

    ErrorResponse illegalArgumentResponse = new ErrorResponse(new IllegalArgumentException("参数错误!"));
    ErrorResponse resourseNotFoundResponse = new ErrorResponse(new ResourceNotFoundException("Sorry, the resourse not found!"));

    @ExceptionHandler(value = Exception.class)// 拦截所有异常, 这里只是为了演示,一般情况下一个方法特定处理一种异常
    public ResponseEntity<ErrorResponse> exceptionHandler(Exception e) {

        if (e instanceof IllegalArgumentException) {
            return ResponseEntity.status(400).body(illegalArgumentResponse);
        } else if (e instanceof ResourceNotFoundException) {
            return ResponseEntity.status(404).body(resourseNotFoundResponse);
        }
        return null;
    }
}

4. controller模拟抛出异常

src/main/java/com/twuc/webApp/web/ExceptionController.java

/**
 * @author shuang.kou
 */
@RestController
@RequestMapping("/api")
public class ExceptionController {

    @GetMapping("/illegalArgumentException")
    public void throwException() {
        throw new IllegalArgumentException();
    }

    @GetMapping("/resourceNotFoundException")
    public void throwException2() {
        throw new ResourceNotFoundException();
    }
}

使用 Get 请求 localhost:8080/api/resourceNotFoundException (curl -i -s -X GET url),服务端返回的 JSON 数据如下:

{
    "message": "Sorry, the resourse not found!",
    "errorTypeName": "com.twuc.webApp.exception.ResourceNotFoundException"
}

5. 编写测试类

MockMvc 由org.springframework.boot.test包提供,实现了对Http请求的模拟,一般用于我们测试 controller 层。

/**
 * @author shuang.kou
 */
@AutoConfigureMockMvc
@SpringBootTest
public class ExceptionTest {
    @Autowired
    MockMvc mockMvc;

    @Test
    void should_return_400_if_param_not_valid() throws Exception {
        mockMvc.perform(get("/api/illegalArgumentException"))
                .andExpect(status().is(400))
                .andExpect(jsonPath("$.message").value("参数错误!"));
    }

    @Test
    void should_return_404_if_resourse_not_found() throws Exception {
        mockMvc.perform(get("/api/resourceNotFoundException"))
                .andExpect(status().is(404))
                .andExpect(jsonPath("$.message").value("Sorry, the resourse not found!"));
    }
}

2. @ExceptionHandler 处理 Controller 级别的异常

我们刚刚也说了使用@ControllerAdvice注解 可以通过 assignableTypes指定特定的类,让异常处理类只处理特定类抛出的异常。所以这种处理异常的方式,实际上现在使用的比较少了。

我们把下面这段代码移到 src/main/java/com/twuc/webApp/exception/GlobalExceptionHandler.java 中就可以了。

    @ExceptionHandler(value = Exception.class)// 拦截所有异常
    public ResponseEntity<ErrorResponse> exceptionHandler(Exception e) {

        if (e instanceof IllegalArgumentException) {
            return ResponseEntity.status(400).body(illegalArgumentResponse);
        } else if (e instanceof ResourceNotFoundException) {
            return ResponseEntity.status(404).body(resourseNotFoundResponse);
        }
        return null;
    }

3. ResponseStatusException

研究 ResponseStatusException 我们先来看看,通过 ResponseStatus注解简单处理异常的方法(将异常映射为状态码)。

src/main/java/com/twuc/webApp/exception/ResourceNotFoundException.java

@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class ResourseNotFoundException2 extends RuntimeException {

   public ResourseNotFoundException2() {
   }

   public ResourseNotFoundException2(String message) {
       super(message);
   }
}

src/main/java/com/twuc/webApp/web/ResponseStatusExceptionController.java

@RestController
@RequestMapping("/api")
public class ResponseStatusExceptionController {
    @GetMapping("/resourceNotFoundException2")
    public void throwException3() {
        throw new ResourseNotFoundException2("Sorry, the resourse not found!");
    }
}

使用 Get 请求 localhost:8080/api/resourceNotFoundException2 ,服务端返回的 JSON 数据如下:

{
    "timestamp": "2019-08-21T07:11:43.744+0000",
    "status": 404,
    "error": "Not Found",
    "message": "Sorry, the resourse not found!",
    "path": "/api/resourceNotFoundException2"
}

这种通过 ResponseStatus注解简单处理异常的方法是的好处是比较简单,但是一般我们不会这样做,通过ResponseStatusException会更加方便,可以避免我们额外的异常类。

    @GetMapping("/resourceNotFoundException2")
    public void throwException3() {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Sorry, the resourse not found!", new ResourceNotFoundException());
    }

使用 Get 请求 localhost:8080/api/resourceNotFoundException2 ,服务端返回的 JSON 数据如下,和使用 ResponseStatus 实现的效果一样:

{
   "timestamp": "2019-08-21T07:28:12.017+0000",
   "status": 404,
   "error": "Not Found",
   "message": "Sorry, the resourse not found!",
   "path": "/api/resourceNotFoundException3"
}

ResponseStatusException 提供了三个构造方法:

	public ResponseStatusException(HttpStatus status) {
		this(status, null, null);
	}

	public ResponseStatusException(HttpStatus status, @Nullable String reason) {
		this(status, reason, null);
	}

	public ResponseStatusException(HttpStatus status, @Nullable String reason, @Nullable Throwable cause) {
		super(null, cause);
		Assert.notNull(status, "HttpStatus is required");
		this.status = status;
		this.reason = reason;
	}

构造函数中的参数解释如下:

  • status : http status
  • reason :response 的消息内容
  • cause : 抛出的异常

总结

本文主要讲了 3 种捕获处理异常的方式:

  1. 使用 @ControllerAdvice@ExceptionHandler 处理全局异常
  2. @ExceptionHandler 处理 Controller 级别的异常
  3. ResponseStatusException

代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/springboot-handle-exception


这篇文章鸽了很久,我在这篇文章 《用好Java中的枚举,真的没有那么简单!》 中就提到要分享。还是昨天一个读者提醒我之后,我才发现自己没有将这篇文章发到公众号。说到这里,我发现自己一个很大的问题,就是有时候在文章里面说要更新什么,结果后面就忘记了,很多时候不是自己没写,就因为各种事情混杂导致忘记发了。以后要尽量改正这个问题!

在上一篇文章《SpringBoot 处理异常的几种常见姿势》中我介绍了:

  1. 使用 @ControllerAdvice@ExceptionHandler 处理全局异常
  2. @ExceptionHandler 处理 Controller 级别的异常
  3. ResponseStatusException

通过这篇文章,可以搞懂如何在 Spring Boot 中进行异常处理。但是,光是会用了还不行,我们还要思考如何把异常处理这部分的代码写的稍微优雅一点。下面我会以我在工作中学到的一点实际项目中异常处理的方式,来说说我觉得稍微优雅点的异常处理解决方案。

下面仅仅是我作为一个我个人的角度来看的,如果各位读者有更好的解决方案或者觉得本文提出的方案还有优化的余地的话,欢迎在评论区评论。

最终效果展示

下面先来展示一下完成后的效果,当我们定义的异常被系统捕捉后返回给客户端的信息是这样的:

效果展示

返回的信息包含了异常下面 5 部分内容:

  1. 唯一标示异常的 code
  2. HTTP状态码
  3. 错误路径
  4. 发生错误的时间戳
  5. 错误的具体信息

这样返回异常信息,更利于我们前端根据异常信息做出相应的表现。

异常处理核心代码

ErrorCode.java (此枚举类中包含了异常的唯一标识、HTTP状态码以及错误信息)

这个类的主要作用就是统一管理系统中可能出现的异常,比较清晰明了。但是,可能出现的问题是当系统过于复杂,出现的异常过多之后,这个类会比较庞大。有一种解决办法:将多种相似的异常统一为一个,比如将用户找不到异常和订单信息未找到的异常都统一为“未找到该资源”这一种异常,然后前端再对相应的情况做详细处理(我个人的一种处理方法,不敢保证是比较好的一种做法)。

import org.springframework.http.HttpStatus;


public enum ErrorCode {
  
    RESOURCE_NOT_FOUND(1001, HttpStatus.NOT_FOUND, "未找到该资源"),
    REQUEST_VALIDATION_FAILED(1002, HttpStatus.BAD_REQUEST, "请求数据格式验证失败");
    private final int code;

    private final HttpStatus status;

    private final String message;

    ErrorCode(int code, HttpStatus status, String message) {
        this.code = code;
        this.status = status;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public HttpStatus getStatus() {
        return status;
    }

    public String getMessage() {
        return message;
    }

    @Override
    public String toString() {
        return "ErrorCode{" +
                "code=" + code +
                ", status=" + status +
                ", message='" + message + '\'' +
                '}';
    }
}

ErrorReponse.java(返回给客户端具体的异常对象)

这个类作为异常信息返回给客户端,里面包括了当出现异常时我们想要返回给客户端的所有信息。

import org.springframework.util.ObjectUtils;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;

public class ErrorReponse {
    private int code;
    private int status;
    private String message;
    private String path;
    private Instant timestamp;
    private HashMap<String, Object> data = new HashMap<String, Object>();

    public ErrorReponse() {
    }

    public ErrorReponse(BaseException ex, String path) {
        this(ex.getError().getCode(), ex.getError().getStatus().value(), ex.getError().getMessage(), path, ex.getData());
    }

    public ErrorReponse(int code, int status, String message, String path, Map<String, Object> data) {
        this.code = code;
        this.status = status;
        this.message = message;
        this.path = path;
        this.timestamp = Instant.now();
        if (!ObjectUtils.isEmpty(data)) {
            this.data.putAll(data);
        }
    }

// 省略 getter/setter 方法

    @Override
    public String toString() {
        return "ErrorReponse{" +
                "code=" + code +
                ", status=" + status +
                ", message='" + message + '\'' +
                ", path='" + path + '\'' +
                ", timestamp=" + timestamp +
                ", data=" + data +
                '}';
    }
}

BaseException.java(继承自 RuntimeException 的抽象类,可以看做系统中其他异常类的父类)

系统中的异常类都要继承自这个类。


public abstract class BaseException extends RuntimeException {
    private final ErrorCode error;
    private final HashMap<String, Object> data = new HashMap<>();

    public BaseException(ErrorCode error, Map<String, Object> data) {
        super(error.getMessage());
        this.error = error;
        if (!ObjectUtils.isEmpty(data)) {
            this.data.putAll(data);
        }
    }

    protected BaseException(ErrorCode error, Map<String, Object> data, Throwable cause) {
        super(error.getMessage(), cause);
        this.error = error;
        if (!ObjectUtils.isEmpty(data)) {
            this.data.putAll(data);
        }
    }

    public ErrorCode getError() {
        return error;
    }

    public Map<String, Object> getData() {
        return data;
    }

}

ResourceNotFoundException.java (自定义异常)

可以看出通过继承 BaseException 类我们自定义异常会变的非常简单!

import java.util.Map;

public class ResourceNotFoundException extends BaseException {

    public ResourceNotFoundException(Map<String, Object> data) {
        super(ErrorCode.RESOURCE_NOT_FOUND, data);
    }
}

GlobalExceptionHandler.java(全局异常捕获)

我们定义了两个异常捕获方法。

这里再说明一下,实际上这个类只需要 handleAppException() 这一个方法就够了,因为它是本系统所有异常的父类。只要是抛出了继承 BaseException 类的异常后都会在这里被处理。

import com.twuc.webApp.web.ExceptionController;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;

@ControllerAdvice(assignableTypes = {ExceptionController.class})
@ResponseBody
public class GlobalExceptionHandler {
    
    // 也可以将 BaseException 换为 RuntimeException 
    // 因为 RuntimeException 是 BaseException 的父类
    @ExceptionHandler(BaseException.class)
    public ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {
        ErrorReponse representation = new ErrorReponse(ex, request.getRequestURI());
        return new ResponseEntity<>(representation, new HttpHeaders(), ex.getError().getStatus());
    }

    @ExceptionHandler(value = ResourceNotFoundException.class)
    public ResponseEntity<ErrorReponse> handleResourceNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
        ErrorReponse errorReponse = new ErrorReponse(ex, request.getRequestURI());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorReponse);
    }
}

(重要)一点扩展:

哈哈!实际上我多加了一个算是多余的异常捕获方法handleResourceNotFoundException() 主要是为了考考大家当我们抛出了 ResourceNotFoundException异常会被下面哪一个方法捕获呢?

答案:

会被handleResourceNotFoundException()方法捕获。因为 @ExceptionHandler 捕获异常的过程中,会优先找到最匹配的。

下面通过源码简单分析一下:

ExceptionHandlerMethodResolver.javagetMappedMethod决定了具体被哪个方法处理。


@Nullable
	private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
		List<Class<? extends Throwable>> matches = new ArrayList<>();
    //找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系
		for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
			if (mappedException.isAssignableFrom(exceptionType)) {
				matches.add(mappedException);
			}
		}
    // 不为空说明有方法处理异常
		if (!matches.isEmpty()) {
      // 按照匹配程度从小到大排序
			matches.sort(new ExceptionDepthComparator(exceptionType));
      // 返回处理异常的方法
			return this.mappedMethods.get(matches.get(0));
		}
		else {
			return null;
		}
	}

从源代码看出:getMappedMethod()会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。

写一个抛出异常的类测试

Person.java

public class Person {
    private Long id;
    private String name;

    // 省略 getter/setter 方法
}

ExceptionController.java(抛出异常的类)

@RestController
@RequestMapping("/api")
public class ExceptionController {

    @GetMapping("/resourceNotFound")
    public void throwException() {
        Person p=new Person(1L,"SnailClimb");
        throw new ResourceNotFoundException(ImmutableMap.of("person id:", p.getId()));
    }

}

源码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/springboot-handle-exception-improved


后端开发中热部署有很多方式,但是在开发 SpringBoot 项目有一种 Spring Boot 给我们提供好的很方便的一种方式,配置起来也很简单。

热部署可以简单的这样理解:我们修改程序代码后不需要重新启动程序,就可以获取到最新的代码,更新程序对外的行为。

热部署在我们日常开发可以为我们节省很多时间,通常我们在开发后端的过程中,当我们修改了后端代码之后都需要重启一下项目,这为我们浪费了时间,特别是在项目比较庞大,需要耗费大量时间的启动的时候。这种方式好像消耗性能挺大的,也需要慎重使用。

下面介绍一下如何通过 SpringBoot 提供的 spring-boot-devtools 实现简单的热部署。

依赖:

Maven:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
       <scope>runtime</scope>
			<optional>true</optional>
		</dependency>
  <plugin>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-maven-plugin</artifactId>
  </plugin>

Gradle:


configurations {
    developmentOnly
    runtimeClasspath {
        extendsFrom developmentOnly
    }
}
dependencies {
      developmentOnly("org.springframework.boot:spring-boot-devtools")
}

添加配置:

ctrl+,(Win) / cmd+(Mac)打开项目配置:

输入 Compiler , 并且勾选上 Build project automatically

dev-tools-idea1

输入快捷键 ctrl + shift + alt + / (Win)cmd+option+shift+/(Mac),并且选择 Registry

dev-tools-idea2

然后勾选上 Compiler autoMake allow when app running

dev-tools-idea3

很简单,这样你每次修改程序之后就不用重新启动了。


一文搞懂如何在 Spring Boot 中正确使用 JPA

JPA 这部分内容上手很容易,但是涉及到的东西还是挺多的,网上大部分关于 JPA 的资料都不是特别齐全,大部分用的版本也是比较落后的。另外,我下面讲到了的内容也不可能涵盖所有 JPA 相关内容,我只是把自己觉得比较重要的知识点总结在了下面。我自己也是参考着官方文档写的,官方文档非常详细了,非常推荐阅读一下。这篇文章可以帮助对 JPA 不了解或者不太熟悉的人来在实际项目中正确使用 JPA。

项目代码基于 Spring Boot 最新的 2.1.9.RELEASE 版本构建(截止到这篇文章写完),另外,新建项目就不多说了,前面的文章已经很详细介绍过。

1.相关依赖

我们需要下面这些依赖支持我们完成这部分内容的学习:

 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

2.配置数据库连接信息和JPA配置

下面的配置中需要单独说一下 spring.jpa.hibernate.ddl-auto=create这个配置选项。

这个属性常用的选项有四种:

  1. create:每次重新启动项目都会重新创新表结构,会导致数据丢失
  2. create-drop:每次启动项目创建表结构,关闭项目删除表结构
  3. update:每次启动项目会更新表结构
  4. validate:验证表结构,不对数据库进行任何更改

但是,一定要不要在生产环境使用 ddl 自动生成表结构,一般推荐手写 SQL 语句配合 Flyway 来做这些事情。

spring.datasource.url=jdbc:mysql://localhost:3306/springboot_jpa?useSSL=false&serverTimezone=CTT
spring.datasource.username=root
spring.datasource.password=123456
# 打印出 sql 语句
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.open-in-view=false
# 创建的表的 ENGINE 为 InnoDB
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL55Dialect

3.实体类

我们为这个类添加了 @Entity 注解代表它是数据库持久化类,还配置了主键 id。

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
@Data
@NoArgsConstructor
public class Person {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String name;
    private Integer age;

    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

}

如何检验你是否正确完成了上面 3 步?很简单,运行项目,查看数据如果发现控制台打印出创建表的 sql 语句,并且数据库中表真的被创建出来的话,说明你成功完成前面 3 步。

控制台打印出来的 sql 语句类似下面这样:

drop table if exists person
CREATE TABLE `person` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `age` int(11) DEFAULT NULL,
  `name` varchar(255) DEFAULT NULL,
   PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
alter table person add constraint UK_p0wr4vfyr2lyifm8avi67mqw5 unique (name)

4.创建操作数据库的 Repository 接口

@Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
}

首先这个接口加了 @Repository 注解,代表它和数据库操作有关。另外,它继承了 JpaRepository<Person, Long>接口,而JpaRepository<Person, Long>长这样:


@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
    List<T> findAll();

    List<T> findAll(Sort var1);

    List<T> findAllById(Iterable<ID> var1);

    <S extends T> List<S> saveAll(Iterable<S> var1);

    void flush();

    <S extends T> S saveAndFlush(S var1);

    void deleteInBatch(Iterable<T> var1);

    void deleteAllInBatch();

    T getOne(ID var1);

    <S extends T> List<S> findAll(Example<S> var1);

    <S extends T> List<S> findAll(Example<S> var1, Sort var2);
}

这表明我们只要继承了JpaRepository<T, ID> 就具有了 JPA 为我们提供好的增删改查、分页查询以及根据条件查询等方法。

4.1 JPA 自带方法实战

1) 增删改查

1.保存用户到数据库

    Person person = new Person("SnailClimb", 23);
    personRepository.save(person);

save()方法对应 sql 语句就是:insert into person (age, name) values (23,"snailclimb")

2.根据 id 查找用户

    Optional<Person> personOptional = personRepository.findById(id);

findById()方法对应 sql 语句就是:select * from person p where p.id = id

3.根据 id 删除用户

    personRepository.deleteById(id);

deleteById()方法对应 sql 语句就是:delete from person where id=id

4.更新用户

更新操作也要通过 save()方法来实现,比如:

    Person person = new Person("SnailClimb", 23);
    Person savedPerson = personRepository.save(person);
    // 更新 person 对象的姓名
    savedPerson.setName("UpdatedName");
    personRepository.save(savedPerson);

在这里 save()方法相当于 sql 语句:update person set name="UpdatedName" where id=id

2) 带条件的查询

下面这些方法是我们根据 JPA 提供的语法自定义的,你需要将下面这些方法写到 PersonRepository 中。

假如我们想要根据 Name 来查找 Person ,你可以这样:

    Optional<Person> findByName(String name);

如果你想要找到年龄大于某个值的人,你可以这样:

    List<Person> findByAgeGreaterThan(int age);

4.2 自定义 SQL 语句实战

很多时候我们自定义 sql 语句会非常有用。

根据 name 来查找 Person:

    @Query("select p from Person p where p.name = :name")
    Optional<Person> findByNameCustomeQuery(@Param("name") String name);

Person 部分属性查询,避免 select *操作:

    @Query("select p.name from Person p where p.id = :id")
    String findPersonNameById(@Param("id") Long id);

根据 id 更新Person name:


    @Modifying
    @Query("update Person p set p.name = ?1 where p.id = ?2")
    void updatePersonNameById(String name, Long id);

4.3 创建异步方法

如果我们需要创建异步方法的话,也比较方便。

异步方法在调用时立即返回,然后会被提交给TaskExecutor执行。当然你也可以选择得出结果后才返回给客户端。如果对 Spring Boot 异步编程感兴趣的话可以看这篇文章:《新手也能看懂的 SpringBoot 异步编程指南》

@Async
Future<User> findByName(String name);               

@Async
CompletableFuture<User> findByName(String name); 

5.测试类和源代码地址

测试类:


@SpringBootTest
@RunWith(SpringRunner.class)
public class PersonRepositoryTest {
    @Autowired
    private PersonRepository personRepository;
    private Long id;

    /**
     * 保存person到数据库
     */
    @Before
    public void setUp() {
        assertNotNull(personRepository);
        Person person = new Person("SnailClimb", 23);
        Person savedPerson = personRepository.saveAndFlush(person);// 更新 person 对象的姓名
        savedPerson.setName("UpdatedName");
        personRepository.save(savedPerson);

        id = savedPerson.getId();
    }

    /**
     * 使用 JPA 自带的方法查找 person
     */
    @Test
    public void should_get_person() {
        Optional<Person> personOptional = personRepository.findById(id);
        assertTrue(personOptional.isPresent());
        assertEquals("SnailClimb", personOptional.get().getName());
        assertEquals(Integer.valueOf(23), personOptional.get().getAge());

        List<Person> personList = personRepository.findByAgeGreaterThan(18);
        assertEquals(1, personList.size());
        // 清空数据库
        personRepository.deleteAll();
    }

    /**
     * 自定义 query sql 查询语句查找 person
     */

    @Test
    public void should_get_person_use_custom_query() {
        // 查找所有字段
        Optional<Person> personOptional = personRepository.findByNameCustomeQuery("SnailClimb");
        assertTrue(personOptional.isPresent());
        assertEquals(Integer.valueOf(23), personOptional.get().getAge());
        // 查找部分字段
        String personName = personRepository.findPersonNameById(id);
        assertEquals("SnailClimb", personName);
        System.out.println(id);
        // 更新
        personRepository.updatePersonNameById("UpdatedName", id);
        Optional<Person> updatedName = personRepository.findByNameCustomeQuery("UpdatedName");
        assertTrue(updatedName.isPresent());
        // 清空数据库
        personRepository.deleteAll();
    }

}

源代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/jpa-demo

6. 总结

本文主要介绍了 JPA 的基本用法:

  1. 使用 JPA 自带的方法进行增删改查以及条件查询。

  2. 自定义 SQL 语句进行查询或者更新数据库。

  3. 创建异步的方法。

在下一篇关于 JPA 的文章中我会介绍到非常重要的两个知识点:

  1. 基本分页功能实现
  2. 多表联合查询以及多表联合查询下的分页功能实现。

JPA 连表查询和分页

对于连表查询,在 JPA 中还是非常常见的,由于 JPA 可以在 respository 层自定义 SQL 语句,所以通过自定义 SQL 语句的方式实现连表还是挺简单。这篇文章是在上一篇入门 JPA的文章的基础上写的,不了解 JPA 的可以先看上一篇文章。

上一节的基础上我们新建了两个实体类,如下:

相关实体类创建

Company.java

@Entity
@Data
@NoArgsConstructor
public class Company {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String companyName;
    private String description;

    public Company(String name, String description) {
        this.companyName = name;
        this.description = description;
    }
}

School.java

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class School {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String name;
    private String description;
}

自定义 SQL语句实现连表查询

假如我们当前要通过 person 表的 id 来查询 Person 的话,我们知道 Person 的信息一共分布在CompanySchoolPerson这三张表中,所以,我们如果要把 Person 的信息都查询出来的话是需要进行连表查询的。

首先我们需要创建一个包含我们需要的 Person 信息的 DTO 对象,我们简单第将其命名为 UserDTO,用于保存和传输我们想要的信息。

@Data
@NoArgsConstructor
@Builder(toBuilder = true)
@AllArgsConstructor
public class UserDTO {
    private String name;
    private int age;
    private String companyName;
    private String schoolName;
}

下面我们就来写一个方法查询出 Person 的基本信息。

    /**
     * 连表查询
     */
    @Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " +
            "from Person p left join Company c on  p.companyId=c.id " +
            "left join School s on p.schoolId=s.id " +
            "where p.id=:personId")
    Optional<UserDTO> getUserInformation(@Param("personId") Long personId);

可以看出上面的 sql 语句和我们平时写的没啥区别,差别比较大的就是里面有一个 new 对象的操作。

自定义 SQL 语句连表查询并实现分页操作

假如我们要查询当前所有的人员信息并实现分页的话,你可以按照下面这种方式来做。可以看到,为了实现分页,我们在@Query注解中还添加了 countQuery 属性。

@Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " +
        "from Person p left join Company c on  p.companyId=c.id " +
        "left join School s on p.schoolId=s.id ",
        countQuery = "select count(p.id) " +
                "from Person p left join Company c on  p.companyId=c.id " +
                "left join School s on p.schoolId=s.id ")
Page<UserDTO> getUserInformationList(Pageable pageable);

实际使用:

//分页选项
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "age");
Page<UserDTO> userInformationList = personRepository.getUserInformationList(pageRequest);
//查询结果总数
System.out.println(userInformationList.getTotalElements());// 6
//按照当前分页大小,总页数
System.out.println(userInformationList.getTotalPages());// 2
System.out.println(userInformationList.getContent());

加餐:自定以SQL语句的其他用法

下面我只介绍两种比较常用的:

  1. IN 查询
  2. BETWEEN 查询

当然,还有很多用法需要大家自己去实践了。

IN 查询

在 sql 语句中加入我们需要筛选出符合几个条件中的一个的情况下,可以使用 IN 查询,对应到 JPA 中也非常简单。比如下面的方法就实现了,根据名字过滤需要的人员信息。

@Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " +
        "from Person p left join Company c on  p.companyId=c.id " +
        "left join School s on p.schoolId=s.id " +
        "where p.name IN :peopleList")
List<UserDTO> filterUserInfo(List peopleList);

实际使用:

List<String> personList=new ArrayList<>(Arrays.asList("person1","person2"));
List<UserDTO> userDTOS = personRepository.filterUserInfo(personList);

BETWEEN 查询

查询满足某个范围的值。比如下面的方法就实现查询满足某个年龄范围的人员的信息。

    @Query(value = "select new github.snailclimb.jpademo.model.dto.UserDTO(p.name,p.age,c.companyName,s.name) " +
            "from Person p left join Company c on  p.companyId=c.id " +
            "left join School s on p.schoolId=s.id " +
            "where p.age between :small and :big")
    List<UserDTO> filterUserInfoByAge(int small,int big);

实际使用:

List<UserDTO> userDTOS = personRepository.filterUserInfoByAge(19,20);

总结

本节我们主要学习了下面几个知识点:

  1. 自定义 SQL 语句实现连表查询;
  2. 自定义 SQL 语句连表查询并实现分页操作;
  3. 条件查询:IN 查询,BETWEEN查询。

1. Filter 介绍

Filter 过滤器这个概念应该大家不会陌生,特别是对与从 Servlet 开始入门学 Java 后台的同学来说。那么这个东西我们能做什么呢?Filter 过滤器主要是用来过滤用户请求的,它允许我们对用户请求进行前置处理和后置处理,比如实现 URL 级别的权限控制、过滤非法请求等等。Filter 过滤器是面向切面编程——AOP 的具体实现(AOP切面编程只是一种编程思想而已)。

另外,Filter 是依赖于 Servlet 容器,Filter接口就在 Servlet 包下面,属于 Servlet 规范的一部分。所以,很多时候我们也称其为“增强版 Servlet”。

如果我们需要自定义 Filter 的话非常简单,只需要实现 javax.Servlet.Filter 接口,然后重写里面的 3 个方法即可!

Filter.java

public interface Filter {
  
   //初始化过滤器后执行的操作
    default void init(FilterConfig filterConfig) throws ServletException {
    }
   // 对请求进行过滤
    void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
   // 销毁过滤器后执行的操作,主要用户对某些资源的回收
    default void destroy() {
    }
}

2. Filter是如何实现拦截的?

Filter接口中有一个叫做 doFilter 的方法,这个方法实现了对用户请求的过滤。具体流程大体是这样的:

  1. 用户发送请求到 web 服务器,请求会先到过滤器;
  2. 过滤器会对请求进行一些处理比如过滤请求的参数、修改返回给客户端的 response 的内容、判断是否让用户访问该接口等等。
  3. 用户请求响应完毕。
  4. 进行一些自己想要的其他操作。

3. 如何自定义Filter

下面提供两种方法。

3.1自己手动注册配置实现

自定义的 Filter 需要实现javax.Servlet.Filter接口,并重写接口中定义的3个方法。

MyFilter.java

/**
 * @author shuang.kou
 */
@Component
public class MyFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(MyFilter.class);

    @Override
    public void init(FilterConfig filterConfig) {
        logger.info("初始化过滤器:", filterConfig.getFilterName());
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //对请求进行预处理
        logger.info("过滤器开始对请求进行预处理:");
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String requestUri = request.getRequestURI();
        System.out.println("请求的接口为:" + requestUri);
        long startTime = System.currentTimeMillis();
        //通过 doFilter 方法实现过滤功能
        filterChain.doFilter(servletRequest, servletResponse);
        // 上面的 doFilter 方法执行结束后用户的请求已经返回
        long endTime = System.currentTimeMillis();
        System.out.println("该用户的请求已经处理完毕,请求花费的时间为:" + (endTime - startTime));
    }

    @Override
    public void destroy() {
        logger.info("销毁过滤器");
    }
}

MyFilterConfig.java

在配置中注册自定义的过滤器。

@Configuration
public class MyFilterConfig {
    @Autowired
    MyFilter myFilter;
    @Bean
    public FilterRegistrationBean<MyFilter> thirdFilter() {
        FilterRegistrationBean<MyFilter> filterRegistrationBean = new FilterRegistrationBean<>();

        filterRegistrationBean.setFilter(myFilter);

        filterRegistrationBean.setUrlPatterns(new ArrayList<>(Arrays.asList("/api/*")));

        return filterRegistrationBean;
    }
}

3.2 通过提供好的一些注解实现

在自己的过滤器的类上加上@WebFilter 然后在这个注解中通过它提供好的一些参数进行配置。

@WebFilter(filterName = "MyFilterWithAnnotation", urlPatterns = "/api/*")
public class MyFilterWithAnnotation implements Filter {

   ......
}

另外,为了能让 Spring 找到它,你需要在启动类上加上 @ServletComponentScan 注解。

4.定义多个拦截器,并决定它们的执行顺序

假如我们现在又加入了一个过滤器怎么办?

MyFilter2.java

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

    @Override
    public void init(FilterConfig filterConfig) {
        logger.info("初始化过滤器2");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //对请求进行预处理
        logger.info("过滤器开始对请求进行预处理2:");
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String requestUri = request.getRequestURI();
        System.out.println("请求的接口为2:" + requestUri);
        long startTime = System.currentTimeMillis();
        //通过 doFilter 方法实现过滤功能
        filterChain.doFilter(servletRequest, servletResponse);
        // 上面的 doFilter 方法执行结束后用户的请求已经返回
        long endTime = System.currentTimeMillis();
        System.out.println("该用户的请求已经处理完毕,请求花费的时间为2:" + (endTime - startTime));
    }

    @Override
    public void destroy() {
        logger.info("销毁过滤器2");
    }
}

在配置中注册自定义的过滤器,通过FilterRegistrationBeansetOrder 方法可以决定 Filter 的执行顺序。

@Configuration
public class MyFilterConfig {
    @Autowired
    MyFilter myFilter;

    @Autowired
    MyFilter2 myFilter2;

    @Bean
    public FilterRegistrationBean<MyFilter> setUpMyFilter() {
        FilterRegistrationBean<MyFilter> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setOrder(2);
        filterRegistrationBean.setFilter(myFilter);
        filterRegistrationBean.setUrlPatterns(new ArrayList<>(Arrays.asList("/api/*")));

        return filterRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean<MyFilter2> setUpMyFilter2() {
        FilterRegistrationBean<MyFilter2> filterRegistrationBean = new FilterRegistrationBean<>();
        filterRegistrationBean.setOrder(1);
        filterRegistrationBean.setFilter(myFilter2);
        filterRegistrationBean.setUrlPatterns(new ArrayList<>(Arrays.asList("/api/*")));
        return filterRegistrationBean;
    }
}

自定义 Controller 验证过滤器

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

@RestController
@RequestMapping("/api")
public class MyController {

    @GetMapping("/hello")
    public String getHello() throws InterruptedException {
        Thread.sleep(1000);
        return "Hello";
    }
}

实际测试效果如下:

2019-10-22 22:32:15.569  INFO 1771 --- [           main] g.j.springbootfilter.filter.MyFilter2    : 初始化过滤器2
2019-10-22 22:32:15.569  INFO 1771 --- [           main] g.j.springbootfilter.filter.MyFilter     : 初始化过滤器
2019-10-22 22:32:55.199  INFO 1771 --- [nio-8080-exec-1] g.j.springbootfilter.filter.MyFilter2    : 过滤器开始对请求进行预处理2:
请求的接口为2:/api/hello
2019-10-22 22:32:55.199  INFO 1771 --- [nio-8080-exec-1] g.j.springbootfilter.filter.MyFilter     : 过滤器开始对请求进行预处理:
请求的接口为:/api/hello
该用户的请求已经处理完毕,请求花费的时间为:1037
该用户的请求已经处理完毕,请求花费的时间为2:1037

代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/springboot-filter-interceptor


本文大部分内容是对国外一个不错的文章 :https://o7planning.org/en/11689/spring-boot-interceptors-tutorial 的翻译,做了适当的修改。

1.Interceptor介绍

拦截器(Interceptor)同 Filter 过滤器一样,它俩都是面向切面编程——AOP 的具体实现(AOP切面编程只是一种编程思想而已)。

你可以使用 Interceptor 来执行某些任务,例如在 Controller 处理请求之前编写日志,添加或更新配置......

Spring中,当请求发送到 Controller 时,在被Controller处理之前,它必须经过 Interceptors(0或更多)。

Spring Interceptor是一个非常类似于Servlet Filter 的概念 。

2.过滤器和拦截器的区别

对于过滤器和拦截器的区别, 知乎@Kangol LI 的回答很不错。

  • 过滤器(Filter):当你有一堆东西的时候,你只希望选择符合你要求的某一些东西。定义这些要求的工具,就是过滤器。
  • 拦截器(Interceptor):在一个流程正在进行的时候,你希望干预它的进展,甚至终止它进行,这是拦截器做的事情。

3.自定义 Interceptor

如果你需要自定义 Interceptor 的话必须实现 org.springframework.web.servlet.HandlerInterceptor接口或继承 org.springframework.web.servlet.handler.HandlerInterceptorAdapter类,并且需要重写下面下面3个方法:

public boolean preHandle(HttpServletRequest request,
                         HttpServletResponse response,
                         Object handler)
 
 
public void postHandle(HttpServletRequest request,
                       HttpServletResponse response,
                       Object handler,
                       ModelAndView modelAndView)
 
 
public void afterCompletion(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler,
                            Exception ex)

注意: ***preHandle***方法返回 truefalse。如果返回 true,则意味着请求将继续到达 Controller 被处理。

Interceptor示意图

每个请求可能会通过许多拦截器。下图说明了这一点。

每个请求可能会通过许多拦截器

LogInterceptor用于过滤所有请求

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class LogInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        long startTime = System.currentTimeMillis();
        System.out.println("\n-------- LogInterception.preHandle --- ");
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("Start Time: " + System.currentTimeMillis());

        request.setAttribute("startTime", startTime);

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, //
                           Object handler, ModelAndView modelAndView) throws Exception {

        System.out.println("\n-------- LogInterception.postHandle --- ");
        System.out.println("Request URL: " + request.getRequestURL());

        // You can add attributes in the modelAndView
        // and use that in the view page
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, //
                                Object handler, Exception ex) throws Exception {
        System.out.println("\n-------- LogInterception.afterCompletion --- ");

        long startTime = (Long) request.getAttribute("startTime");
        long endTime = System.currentTimeMillis();
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("End Time: " + endTime);

        System.out.println("Time Taken: " + (endTime - startTime));
    }

}

**OldLoginInterceptor**是一个拦截器,如果用户输入已经被废弃的链接 “ / admin / oldLogin”,它将重定向到新的 “ / admin / login”。

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class OldLoginInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        System.out.println("\n-------- OldLoginInterceptor.preHandle --- ");
        System.out.println("Request URL: " + request.getRequestURL());
        System.out.println("Sorry! This URL is no longer used, Redirect to /admin/login");

        response.sendRedirect(request.getContextPath() + "/admin/login");
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, //
                           Object handler, ModelAndView modelAndView) throws Exception {

        // This code will never be run.
        System.out.println("\n-------- OldLoginInterceptor.postHandle --- ");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, //
                                Object handler, Exception ex) throws Exception {

        // This code will never be run.
        System.out.println("\n-------- QueryStringInterceptor.afterCompletion --- ");
    }

}

AdminInterceptor

package org.o7planning.sbinterceptor.interceptor;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
 
public class AdminInterceptor extends HandlerInterceptorAdapter {
 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
 
        System.out.println("\n-------- AdminInterceptor.preHandle --- ");
        return true;
    }
 
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, //
            Object handler, ModelAndView modelAndView) throws Exception {
         
        System.out.println("\n-------- AdminInterceptor.postHandle --- ");
    }
 
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, //
            Object handler, Exception ex) throws Exception {
 
        System.out.println("\n-------- AdminInterceptor.afterCompletion --- ");
    }
 
}

配置拦截器

import github.javaguide.springbootfilter.interceptor.AdminInterceptor;
import github.javaguide.springbootfilter.interceptor.LogInterceptor;
import github.javaguide.springbootfilter.interceptor.OldLoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // LogInterceptor apply to all URLs.
        registry.addInterceptor(new LogInterceptor());

        // Old Login url, no longer use.
        // Use OldURLInterceptor to redirect to a new URL.
        registry.addInterceptor(new OldLoginInterceptor())//
                .addPathPatterns("/admin/oldLogin");

        // This interceptor apply to URL like /admin/*
        // Exclude /admin/oldLogin
        registry.addInterceptor(new AdminInterceptor())//
                .addPathPatterns("/admin/*")//
                .excludePathPatterns("/admin/oldLogin");
    }

}

自定义 Controller 验证拦截器

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

@Controller
public class InterceptorTestController {

    @RequestMapping(value = { "/", "/test" })
    public String test(Model model) {

        System.out.println("\n-------- MainController.test --- ");

        System.out.println(" ** You are in Controller ** ");

        return "test";
    }

    // This path is no longer used.
    // It will be redirected by OldLoginInterceptor
    @Deprecated
    @RequestMapping(value = { "/admin/oldLogin" })
    public String oldLogin(Model model) {

        // Code here never run.
        return "oldLogin";
    }

    @RequestMapping(value = { "/admin/login" })
    public String login(Model model) {

        System.out.println("\n-------- MainController.login --- ");

        System.out.println(" ** You are in Controller ** ");

        return "login";
    }

}

thymeleaf 模板引擎

test.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
 
   <head>
      <meta charset="UTF-8" />
      <title>Spring Boot Mvc Interceptor example</title>
   </head>
 
   <body>
      <div style="border: 1px solid #ccc;padding: 5px;margin-bottom:10px;">
         <a th:href="@{/}">Home</a>
         &nbsp;&nbsp; | &nbsp;&nbsp;
         <a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a>  
      </div>
 
      <h3>Spring Boot Mvc Interceptor</h3>
       
      <span style="color:blue;">Testing LogInterceptor</span>
      <br/><br/>
 
      See Log in Console..
 
   </body>
</html>

login.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
   <head>
      <meta charset="UTF-8" />
      <title>Spring Boot Mvc Interceptor example</title>
   </head>
   <body>
    
      <div style="border: 1px solid #ccc;padding: 5px;margin-bottom:10px;">
         <a th:href="@{/}">Home</a>
         &nbsp;&nbsp; | &nbsp;&nbsp;
         <a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a>  
      </div>
       
      <h3>This is Login Page</h3>
       
      <span style="color:blue">Testing OldLoginInterceptor &amp; AdminInterceptor</span>
      <br/><br/>
      See more info in the Console.
       
   </body>
    
</html>

4.运行程序并测试效果

测试用户访问 http://localhost:8080/ 的时候, LogInterceptor记录相关信息(页面地址,访问时间),并计算 Web服务器处理请求的时间。另外,页面会被渲染成 test.html

当用户访问 http://localhost:8080/admin/oldLogin 也就是旧的登录页面(不再使用)时, OldLoginInterceptor将请求重定向 http://localhost:8080/admin/login 页面会被渲染成正常的登录页面 login.html

注意看控制台打印出的信息。

5.总结

首先介绍了 Interceptor 的一些概念,然后通过一个简单的小案例走了一遍自定义实现 Interceptor 的过程。

代办:

  1. Filter 和 Interceptor 执行顺序分析;
  2. Spring Boot 实现监听器;
  3. Filter、Interceptor、Listener对比分析;

代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/basis/springboot-filter-interceptor


在上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》中,带着大家整合了 SpringBoot 和 Mybatis ,我们在当时使用的时单数据源的情况,这种情况下 Spring Boot的配置非常简单,只需要在 application.properties 文件中配置数据库的相关连接参数即可。但是往往随着业务量发展,我们通常会进行数据库拆分或是引入其他数据库,从而我们需要配置多个数据源。下面基于 SpringBoot+Mybatis ,带着大家看一下 SpringBoot 中如何配置多数据源。

这篇文章所涉及的代码其实是基于上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》 的项目写的,但是为了考虑部分读者没有读过上一篇文章,所以我还是会一步一步带大家走完每一步,力争新手也能在看完之后独立实践。

目录:

一 开发前的准备

1.1 环境参数

  • 开发工具:IDEA
  • 基础工具:Maven+JDK8
  • 所用技术:SpringBoot+Mybatis
  • 数据库:MySQL
  • SpringBoot版本:2.1.0. SpringBoot2.0之后会有一些小坑,这篇文章会给你介绍到。注意版本不一致导致的一些小问题。

1.2 创建工程

创建一个基本的 SpringBoot 项目,我这里就不多说这方面问题了,具体可以参考下面这篇文章:

https://blog.csdn.net/qq_34337272/article/details/79563606

本项目结构:

基于SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置项目结构

1.3 创建两个数据库和 user 用户表、money工资详情表

我们一共创建的两个数据库,然后分别在这两个数据库中创建了 user 用户表、money工资详情表。

我们的用户表很简单,只有 4 个字段:用户 id、姓名、年龄、余额。如下图所示:

用户表信息

添加了“余额money”字段是为了给大家简单的演示一下事务管理的方式。

我们的工资详情表也很简单,也只有 4 个字段: id、基本工资、奖金和惩罚金。如下图所示:

工资详情表信息

建表语句:

用户表:

CREATE TABLE `user` (
  `id` int(13) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(33) DEFAULT NULL COMMENT '姓名',
  `age` int(3) DEFAULT NULL COMMENT '年龄',
  `money` double DEFAULT NULL COMMENT '账户余额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8

工资详情表:

CREATE TABLE `money` (
  `id` int(33) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `basic` int(33) DEFAULT NULL COMMENT '基本工资',
  `reward` int(33) DEFAULT NULL COMMENT '奖金',
  `punishment` int(33) DEFAULT NULL COMMENT '惩罚金',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8

1.4 配置 pom 文件中的相关依赖

由于要整合 springboot 和 mybatis 所以加入了artifactId 为 mybatis-spring-boot-starter 的依赖,由于使用了Mysql 数据库 所以加入了artifactId 为 mysql-connector-java 的依赖。

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

1.5 配置 application.properties

配置两个数据源:数据库1和数据库2!

注意事项:

在1.0 配置数据源的过程中主要是写成:spring.datasource.url 和spring.datasource.driverClassName。
而在2.0升级之后需要变更成:spring.datasource.jdbc-url和spring.datasource.driver-class-name!不然在连接数据库时可能会报下面的错误:

### Error querying database.  Cause: java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.

另外在在2.0.2+版本后需要在datasource后面加上hikari,如果你没有加的话,同样可能会报错。

server.port=8335
# 配置第一个数据源
spring.datasource.hikari.db1.jdbc-url=jdbc:mysql://127.0.0.1:3306/erp?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8
spring.datasource.hikari.db1.username=root
spring.datasource.hikari.db1.password=153963
spring.datasource.hikari.db1.driver-class-name=com.mysql.cj.jdbc.Driver
# 配置第二个数据源
spring.datasource.hikari.db2.jdbc-url=jdbc:mysql://127.0.0.1:3306/erp2?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT%2B8
spring.datasource.hikari.db2.username=root
spring.datasource.hikari.db2.password=153963
spring.datasource.hikari.db2.driver-class-name=com.mysql.cj.jdbc.Driver

1.6 创建用户类 Bean和工资详情类 Bean

User.java

public class User {
    private int id;
    private String name;
    private int age;
    private double money;
    ...
    此处省略getter、setter以及 toString方法
}

Money.java

public class Money {
    private int basic;
    private int reward;
    private int punishment;
    ...
    此处省略getter、setter以及 toString方法
}

二 数据源配置

通过 Java 类来实现对两个数据源的配置,这一部分是最关键的部分了,这里主要提一下下面这几点:

  • @MapperScan 注解中我们声明了使用数据库1的dao类所在的位置,还声明了 SqlSessionTemplate 。SqlSessionTemplate是MyBatis-Spring的核心。这个类负责管理MyBatis的SqlSession,调用MyBatis的SQL方法,翻译异常。SqlSessionTemplate是线程安全的,可以被多个DAO所共享使用。
  • 由于我使用的是全注解的方式开发,所以下面这条找并且解析 mapper.xml 配置语句被我注释掉了
    bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/db2/*.xml"));
  • 比如我们要声明使用数据1,直接在 dao 层的类上加上这样一个注释即可:@Qualifier("db1SqlSessionTemplate")
  • 我们在数据库1配置类的每个方法前加上了 @Primary 注解来声明这个数据库时默认数据库,不然可能会报错。

DataSource1Config.java

@Configuration
@MapperScan(basePackages = "top.snailclimb.db1.dao", sqlSessionTemplateRef = "db1SqlSessionTemplate")
public class DataSource1Config {

    /**
     * 生成数据源.  @Primary 注解声明为默认数据源
     */
    @Bean(name = "db1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.hikari.db1")
    @Primary
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }

    /**
     * 创建 SqlSessionFactory
     */
    @Bean(name = "db1SqlSessionFactory")
    @Primary
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("db1DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        //  bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/db1/*.xml"));
        return bean.getObject();
    }

    /**
     * 配置事务管理
     */
    @Bean(name = "db1TransactionManager")
    @Primary
    public DataSourceTransactionManager testTransactionManager(@Qualifier("db1DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "db1SqlSessionTemplate")
    @Primary
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

DataSource2Config.java

@Configuration
@MapperScan(basePackages = "top.snailclimb.db2.dao", sqlSessionTemplateRef = "db2SqlSessionTemplate")
public class DataSource2Config {

    @Bean(name = "db2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.hikari.db2")
    public DataSource testDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "db2SqlSessionFactory")
    public SqlSessionFactory testSqlSessionFactory(@Qualifier("db2DataSource") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(dataSource);
        //bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/mapper/db2/*.xml"));
        return bean.getObject();
    }

    @Bean(name = "db2TransactionManager")
    public DataSourceTransactionManager testTransactionManager(@Qualifier("db2DataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "db2SqlSessionTemplate")
    public SqlSessionTemplate testSqlSessionTemplate(@Qualifier("db2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

}

三 Dao 层开发和 Service 层开发

新建两个不同的包存放两个不同数据库的 dao 和 service。

3.1 Dao 层

对于两个数据库,我们只是简单的测试一个查询这个操作。在上一篇文章《优雅整合 SpringBoot+Mybatis ,可能是你见过最详细的一篇》中,我带着大家使用注解实现了数据库基本的增删改查操作。

UserDao.java

@Qualifier("db1SqlSessionTemplate")
public interface UserDao {
    /**
     * 通过名字查询用户信息
     */
    @Select("SELECT * FROM user WHERE name = #{name}")
    User findUserByName(String name);

}

MoneyDao.java


@Qualifier("db2SqlSessionTemplate")
public interface MoneyDao {

    /**
     * 通过id 查看工资详情
     */
    @Select("SELECT * FROM money WHERE id = #{id}")
    Money findMoneyById(@Param("id") int id);
}

3.2 Service 层

Service 层很简单,没有复杂的业务逻辑。

UserService.java

@Service
public class UserService {
    @Autowired
    private UserDao userDao;

    /**
     * 根据名字查找用户
     */
    public User selectUserByName(String name) {
        return userDao.findUserByName(name);
    }

}

MoneyService.java

@Service
public class MoneyService {
    @Autowired
    private MoneyDao moneyDao;

    /**
     * 根据名字查找用户
     */
    public Money selectMoneyById(int id) {
        return moneyDao.findMoneyById(id);
    }

}

四 Controller层

Controller 层也非常简单。

UserController.java

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/query")
    public User testQuery() {
        return userService.selectUserByName("Daisy");
    }
}

MoneyController.java

@RestController
@RequestMapping("/money")
public class MoneyController {
    @Autowired
    private MoneyService moneyService;

    @RequestMapping("/query")
    public Money testQuery() {
        return moneyService.selectMoneyById(1);
    }
}

五 启动类

//此注解表示SpringBoot启动类
@SpringBootApplication
public class MainApplication {

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

}

这样基于SpirngBoot2.0+ 的 SpringBoot+Mybatis 多数据源配置就已经完成了, 两个数据库都可以被访问了。


本文转载自:https://pjmike.github.io/2018/11/03/Bean映射工具之Apache-BeanUtils-VS-Spring-BeanUtils/,作者 pjmike 。

前言

在我们实际项目开发过程中,我们经常需要将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息,比如DTO数据传输对象和数据对象DO,我们需要将DO对象进行属性复制到DTO,但是对象格式又不一样,所以我们需要编写映射代码将对象中的属性值从一种类型转换成另一种类型。

对象拷贝

在具体介绍两种 BeanUtils 之前,先来补充一些基础知识。它们两种工具本质上就是对象拷贝工具,而对象拷贝又分为深拷贝和浅拷贝,下面进行详细解释。

什么是浅拷贝和深拷贝

在Java中,除了 基本数据类型之外,还存在 类的实例对象这个引用数据类型,而一般使用 “=”号做赋值操作的时候,对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际还是指向的同一个对象。

而浅拷贝和深拷贝就是在这个基础上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝

简单来说:

  • 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝
  • 深拷贝: 对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

deep and shallow copy

BeanUtils

前面简单讲了一下对象拷贝的一些知识,下面就来具体看下两种 BeanUtils 工具

Apache 的 BeanUtils

首先来看一个非常简单的BeanUtils的例子

public class PersonSource  {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    // getters/setters omiited
}
public class PersonDest {
    private Integer id;
    private String username;
    private Integer age;
    // getters/setters omiited
}
public class TestApacheBeanUtils {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
       //下面只是用于单独测试
        PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
        PersonDest personDest = new PersonDest();
        BeanUtils.copyProperties(personDest,personSource);
        System.out.println("persondest: "+personDest);
    }
}
persondest: PersonDest{id=1, username='pjmike', age=21}

从上面的例子可以看出,对象拷贝非常简单,BeanUtils最常用的方法就是:

//将源对象中的值拷贝到目标对象
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

默认情况下,使用org.apache.commons.beanutils.BeanUtils对复杂对象的复制是引用,这是一种浅拷贝

但是由于 Apache下的BeanUtils对象拷贝性能太差,不建议使用,而且在阿里巴巴Java开发规约插件上也明确指出:

Ali-Check | 避免用Apache Beanutils进行属性的copy。

commons-beantutils 对于对象拷贝加了很多的检验,包括类型的转换,甚至还会检验对象所属的类的可访问性,可谓相当复杂,这也造就了它的差劲的性能,具体实现代码如下:

public void copyProperties(final Object dest, final Object orig)
        throws IllegalAccessException, InvocationTargetException {

        // Validate existence of the specified beans
        if (dest == null) {
            throw new IllegalArgumentException
                    ("No destination bean specified");
        }
        if (orig == null) {
            throw new IllegalArgumentException("No origin bean specified");
        }
        if (log.isDebugEnabled()) {
            log.debug("BeanUtils.copyProperties(" + dest + ", " +
                      orig + ")");
        }

        // Copy the properties, converting as necessary
        if (orig instanceof DynaBean) {
            final DynaProperty[] origDescriptors =
                ((DynaBean) orig).getDynaClass().getDynaProperties();
            for (DynaProperty origDescriptor : origDescriptors) {
                final String name = origDescriptor.getName();
                // Need to check isReadable() for WrapDynaBean
                // (see Jira issue# BEANUTILS-61)
                if (getPropertyUtils().isReadable(orig, name) &&
                    getPropertyUtils().isWriteable(dest, name)) {
                    final Object value = ((DynaBean) orig).get(name);
                    copyProperty(dest, name, value);
                }
            }
        } else if (orig instanceof Map) {
            @SuppressWarnings("unchecked")
            final
            // Map properties are always of type <String, Object>
            Map<String, Object> propMap = (Map<String, Object>) orig;
            for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
                final String name = entry.getKey();
                if (getPropertyUtils().isWriteable(dest, name)) {
                    copyProperty(dest, name, entry.getValue());
                }
            }
        } else /* if (orig is a standard JavaBean) */ {
            final PropertyDescriptor[] origDescriptors =
                getPropertyUtils().getPropertyDescriptors(orig);
            for (PropertyDescriptor origDescriptor : origDescriptors) {
                final String name = origDescriptor.getName();
                if ("class".equals(name)) {
                    continue; // No point in trying to set an object's class
                }
                if (getPropertyUtils().isReadable(orig, name) &&
                    getPropertyUtils().isWriteable(dest, name)) {
                    try {
                        final Object value =
                            getPropertyUtils().getSimpleProperty(orig, name);
                        copyProperty(dest, name, value);
                    } catch (final NoSuchMethodException e) {
                        // Should not happen
                    }
                }
            }
        }

    }

Spring 的 BeanUtils

使用spring的BeanUtils进行对象拷贝:

public class TestSpringBeanUtils {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {

       //下面只是用于单独测试
        PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
        PersonDest personDest = new PersonDest();
        BeanUtils.copyProperties(personSource,personDest);
        System.out.println("persondest: "+personDest);
    }
}

spring下的BeanUtils也是使用 copyProperties方法进行拷贝,只不过它的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性。具体实现如下:

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
			@Nullable String... ignoreProperties) throws BeansException {

		Assert.notNull(source, "Source must not be null");
		Assert.notNull(target, "Target must not be null");

		Class<?> actualEditable = target.getClass();
		if (editable != null) {
			if (!editable.isInstance(target)) {
				throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
						"] not assignable to Editable class [" + editable.getName() + "]");
			}
			actualEditable = editable;
		}
		PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
		List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

		for (PropertyDescriptor targetPd : targetPds) {
			Method writeMethod = targetPd.getWriteMethod();
			if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
				PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
				if (sourcePd != null) {
					Method readMethod = sourcePd.getReadMethod();
					if (readMethod != null &&
							ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
						try {
							if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
								readMethod.setAccessible(true);
							}
							Object value = readMethod.invoke(source);
							if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
								writeMethod.setAccessible(true);
							}
							writeMethod.invoke(target, value);
						}
						catch (Throwable ex) {
							throw new FatalBeanException(
									"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
						}
					}
				}
			}
		}
	}

可以看到,成员变量赋值是基于目标对象的成员列表,并且会跳过ignore的以及在源对象中不存在,所以这个方法是安全的,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同

小结

以上简要的分析两种BeanUtils,因为Apache下的BeanUtils性能较差,不建议使用,可以使用 Spring的BeanUtils ,或者使用其他拷贝框架,比如:Dozer、**ModelMapper**等等,在后面的文章中我会对这些拷贝框架进行介绍。

参考资料 & 鸣谢


本文由 JavaGuide 翻译自 https://www.baeldung.com/java-performance-mapping-frameworks 。转载请注明原文地址以及翻译作者。

1. 介绍

创建由多个层组成的大型 Java 应用程序需要使用多种领域模型,如持久化模型、领域模型或者所谓的 DTO。为不同的应用程序层使用多个模型将要求我们提供 bean 之间的映射方法。手动执行此操作可以快速创建大量样板代码并消耗大量时间。幸运的是,Java 有多个对象映射框架。在本教程中,我们将比较最流行的 Java 映射框架的性能。

综合日常使用情况和相关测试数据,个人感觉 MapStruct、ModelMapper 这两个 Bean 映射框架是最佳选择。

2. 常见 Bean 映射框架概览

2.1. Dozer

Dozer 是一个映射框架,它使用递归将数据从一个对象复制到另一个对象。框架不仅能够在 bean 之间复制属性,还能够在不同类型之间自动转换。

要使用 Dozer 框架,我们需要添加这样的依赖到我们的项目:

<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer</artifactId>
    <version>5.5.1</version>
</dependency>

更多关于 Dozer 的内容可以在官方文档中找到: http://dozer.sourceforge.net/documentation/gettingstarted.html ,或者你也可以阅读这篇文章:https://www.baeldung.com/dozer

2.2. Orika

Orika 是一个 bean 到 bean 的映射框架,它递归地将数据从一个对象复制到另一个对象。

Orika 的工作原理与 Dozer 相似。两者之间的主要区别是 Orika 使用字节码生成。这允许以最小的开销生成更快的映射器。

要使用 Orika 框架,我们需要添加这样的依赖到我们的项目:

<dependency>
    <groupId>ma.glasnost.orika</groupId>
    <artifactId>orika-core</artifactId>
    <version>1.5.2</version>
</dependency>

更多关于 Orika 的内容可以在官方文档中找到:https://orika-mapper.github.io/orika-docs/,或者你也可以阅读这篇文章:https://www.baeldung.com/orika-mapping。

2.3. MapStruct

MapStruct 是一个自动生成 bean mapper 类的代码生成器。MapStruct 还能够在不同的数据类型之间进行转换。Github 地址:https://github.com/mapstruct/mapstruct。

要使用 MapStruct 框架,我们需要添加这样的依赖到我们的项目:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
</dependency>

更多关于 MapStruct 的内容可以在官方文档中找到:https://mapstruct.org/,或者你也可以阅读这篇文章:https://www.baeldung.com/mapstruct。

要使用 MapStruct 框架,我们需要添加这样的依赖到我们的项目:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.2.0.Final</version>
</dependency>

2.4. ModelMapper

ModelMapper 是一个旨在简化对象映射的框架,它根据约定确定对象之间的映射方式。它提供了类型安全的和重构安全的 API。

更多关于 ModelMapper 的内容可以在官方文档中找到:http://modelmapper.org/

要使用 ModelMapper 框架,我们需要添加这样的依赖到我们的项目:

<dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>1.1.0</version>
</dependency>

2.5. JMapper

JMapper 是一个映射框架,旨在提供易于使用的、高性能的 Java bean 之间的映射。该框架旨在使用注释和关系映射应用 DRY 原则。该框架允许不同的配置方式:基于注释、XML 或基于 api。

更多关于 JMapper 的内容可以在官方文档中找到:https://github.com/jmapper-framework/jmapper-core/wiki。

要使用 JMapper 框架,我们需要添加这样的依赖到我们的项目:

<dependency>
    <groupId>com.googlecode.jmapper-framework</groupId>
    <artifactId>jmapper-core</artifactId>
    <version>1.6.0.1</version>
</dependency>

3.测试模型

为了能够正确地测试映射,我们需要有一个源和目标模型。我们已经创建了两个测试模型。

第一个是一个只有一个字符串字段的简单 POJO,它允许我们在更简单的情况下比较框架,并检查如果我们使用更复杂的 bean 是否会发生任何变化。

简单的源模型如下:

public class SourceCode {
    String code;
    // getter and setter
}

它的目标也很相似:

public class DestinationCode {
    String code;
    // getter and setter
}

源 bean 的实际示例如下:

public class SourceOrder {
    private String orderFinishDate;
    private PaymentType paymentType;
    private Discount discount;
    private DeliveryData deliveryData;
    private User orderingUser;
    private List<Product> orderedProducts;
    private Shop offeringShop;
    private int orderId;
    private OrderStatus status;
    private LocalDate orderDate;
    // standard getters and setters
}

目标类如下图所示:

public class Order {
    private User orderingUser;
    private List<Product> orderedProducts;
    private OrderStatus orderStatus;
    private LocalDate orderDate;
    private LocalDate orderFinishDate;
    private PaymentType paymentType;
    private Discount discount;
    private int shopId;
    private DeliveryData deliveryData;
    private Shop offeringShop;
    // standard getters and setters
}

整个模型结构可以在这里找到:https://github.com/eugenp/tutorials/tree/master/performance-tests/src/main/java/com/baeldung/performancetests/model/source。

4. 转换器

为了简化测试设置的设计,我们创建了如下所示的转换器接口:

public interface Converter {
    Order convert(SourceOrder sourceOrder);
    DestinationCode convert(SourceCode sourceCode);
}

我们所有的自定义映射器都将实现这个接口。

4.1. OrikaConverter

Orika 支持完整的 API 实现,这大大简化了 mapper 的创建:

public class OrikaConverter implements Converter{
    private MapperFacade mapperFacade;

    public OrikaConverter() {
        MapperFactory mapperFactory = new DefaultMapperFactory
          .Builder().build();

        mapperFactory.classMap(Order.class, SourceOrder.class)
          .field("orderStatus", "status").byDefault().register();
        mapperFacade = mapperFactory.getMapperFacade();
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return mapperFacade.map(sourceOrder, Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return mapperFacade.map(sourceCode, DestinationCode.class);
    }
}

4.2. DozerConverter

Dozer 需要 XML 映射文件,有以下几个部分:

<mappings xmlns="http://dozer.sourceforge.net"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://dozer.sourceforge.net
  http://dozer.sourceforge.net/schema/beanmapping.xsd">

    <mapping>
        <class-a>com.baeldung.performancetests.model.source.SourceOrder</class-a>
        <class-b>com.baeldung.performancetests.model.destination.Order</class-b>
        <field>
            <a>status</a>
            <b>orderStatus</b>
        </field>
    </mapping>
    <mapping>
        <class-a>com.baeldung.performancetests.model.source.SourceCode</class-a>
        <class-b>com.baeldung.performancetests.model.destination.DestinationCode</class-b>
    </mapping>
</mappings>

定义了 XML 映射后,我们可以从代码中使用它:

public class DozerConverter implements Converter {
    private final Mapper mapper;

    public DozerConverter() {
        DozerBeanMapper mapper = new DozerBeanMapper();
        mapper.addMapping(
          DozerConverter.class.getResourceAsStream("/dozer-mapping.xml"));
        this.mapper = mapper;
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return mapper.map(sourceOrder,Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return mapper.map(sourceCode, DestinationCode.class);
    }
}

4.3. MapStructConverter

Map 结构的定义非常简单,因为它完全基于代码生成:

@Mapper
public interface MapStructConverter extends Converter {
    MapStructConverter MAPPER = Mappers.getMapper(MapStructConverter.class);

    @Mapping(source = "status", target = "orderStatus")
    @Override
    Order convert(SourceOrder sourceOrder);

    @Override
    DestinationCode convert(SourceCode sourceCode);
}

4.4. JMapperConverter

JMapperConverter 需要做更多的工作。接口实现后:

public class JMapperConverter implements Converter {
    JMapper realLifeMapper;
    JMapper simpleMapper;

    public JMapperConverter() {
        JMapperAPI api = new JMapperAPI()
          .add(JMapperAPI.mappedClass(Order.class));
        realLifeMapper = new JMapper(Order.class, SourceOrder.class, api);
        JMapperAPI simpleApi = new JMapperAPI()
          .add(JMapperAPI.mappedClass(DestinationCode.class));
        simpleMapper = new JMapper(
          DestinationCode.class, SourceCode.class, simpleApi);
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
        return (Order) realLifeMapper.getDestination(sourceOrder);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return (DestinationCode) simpleMapper.getDestination(sourceCode);
    }
}

我们还需要向目标类的每个字段添加@JMap注释。此外,JMapper 不能在 enum 类型之间转换,它需要我们创建自定义映射函数:

@JMapConversion(from = "paymentType", to = "paymentType")
public PaymentType conversion(com.baeldung.performancetests.model.source.PaymentType type) {
    PaymentType paymentType = null;
    switch(type) {
        case CARD:
            paymentType = PaymentType.CARD;
            break;

        case CASH:
            paymentType = PaymentType.CASH;
            break;

        case TRANSFER:
            paymentType = PaymentType.TRANSFER;
            break;
    }
    return paymentType;
}

4.5. ModelMapperConverter

ModelMapperConverter 只需要提供我们想要映射的类:

public class ModelMapperConverter implements Converter {
    private ModelMapper modelMapper;

    public ModelMapperConverter() {
        modelMapper = new ModelMapper();
    }

    @Override
    public Order convert(SourceOrder sourceOrder) {
       return modelMapper.map(sourceOrder, Order.class);
    }

    @Override
    public DestinationCode convert(SourceCode sourceCode) {
        return modelMapper.map(sourceCode, DestinationCode.class);
    }
}

5. 简单的模型测试

对于性能测试,我们可以使用 Java Microbenchmark Harness,关于如何使用它的更多信息可以在 这篇文章:https://www.baeldung.com/java-microbenchmark-harness 中找到。

我们为每个转换器创建了一个单独的基准测试,并将基准测试模式指定为 Mode.All。

5.1. 平均时间

对于平均运行时间,JMH 返回以下结果(越少越好):

AverageTime

这个基准测试清楚地表明,MapStruct 和 JMapper 都有最佳的平均工作时间。

5.2. 吞吐量

在这种模式下,基准测试返回每秒的操作数。我们收到以下结果(越多越好):

Throughput

在吞吐量模式中,MapStruct 是测试框架中最快的,JMapper 紧随其后。

5.3. SingleShotTime

这种模式允许测量单个操作从开始到结束的时间。基准给出了以下结果(越少越好):

SingleShotTime

这里,我们看到 JMapper 返回的结果比 MapStruct 好得多。

5.4. 采样时间

这种模式允许对每个操作的时间进行采样。三个不同百分位数的结果如下:

SampleTime

所有的基准测试都表明,根据场景的不同,MapStruct 和 JMapper 都是不错的选择,尽管 MapStruct 对 SingleShotTime 给出的结果要差得多。

6. 真实模型测试

对于性能测试,我们可以使用 Java Microbenchmark Harness,关于如何使用它的更多信息可以在 这篇文章:https://www.baeldung.com/java-microbenchmark-harness 中找到。

我们为每个转换器创建了一个单独的基准测试,并将基准测试模式指定为 Mode.All。

6.1. 平均时间

JMH 返回以下平均运行时间结果(越少越好):

平均时间

该基准清楚地表明,MapStruct 和 JMapper 均具有最佳的平均工作时间。

6.2. 吞吐量

在这种模式下,基准测试返回每秒的操作数。我们收到以下结果(越多越好):

在吞吐量模式中,MapStruct 是测试框架中最快的,JMapper 紧随其后。

6.3. SingleShotTime

这种模式允许测量单个操作从开始到结束的时间。基准给出了以下结果(越少越好):

6.4. 采样时间

这种模式允许对每个操作的时间进行采样。三个不同百分位数的结果如下:

SampleTime

尽管简单示例和实际示例的确切结果明显不同,但是它们的趋势相同。在哪种算法最快和哪种算法最慢方面,两个示例都给出了相似的结果。

6.5. 结论

根据我们在本节中执行的真实模型测试,我们可以看出,最佳性能显然属于 MapStruct。在相同的测试中,我们看到 Dozer 始终位于结果表的底部。

7. 总结

在这篇文章中,我们已经进行了五个流行的 Java Bean 映射框架性能测试:ModelMapper MapStruct Orika ,Dozer, JMapper。

示例代码地址:https://github.com/eugenp/tutorials/tree/master/performance-tests。


数据的校验的重要性就不用说了,即使在前端对数据进行校验的情况下,我们还是要对传入后端的数据再进行一遍校验,避免用户绕过浏览器直接通过一些 HTTP 工具直接向后端请求一些违法数据。

本文结合自己在项目中的实际使用经验,可以说文章介绍的内容很实用,不了解的朋友可以学习一下,后面可以立马实践到项目上去。

下面我会通过实例程序演示如何在 Java 程序中尤其是 Spring 程序中优雅地的进行参数验证。

基础设施搭建

相关依赖

如果开发普通 Java 程序的的话,你需要可能需要像下面这样依赖:

   <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.9.Final</version>
   </dependency>
   <dependency>
             <groupId>javax.el</groupId>
             <artifactId>javax.el-api</artifactId>
             <version>3.0.0</version>
     </dependency>
     <dependency>
            <groupId>org.glassfish.web</groupId>
            <artifactId>javax.el</artifactId>
            <version>2.2.6</version>
     </dependency>

使用 Spring Boot 程序的话只需要spring-boot-starter-web 就够了,它的子依赖包含了我们所需要的东西。除了这个依赖,下面的演示还用到了 lombok ,所以不要忘记添加上相关依赖。

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

实体类

下面这个是示例用到的实体类。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {

    @NotNull(message = "classId 不能为空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能为空")
    private String name;

    @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可选范围")
    @NotNull(message = "sex 不能为空")
    private String sex;

    @Email(message = "email 格式不正确")
    @NotNull(message = "email 不能为空")
    private String email;

}

正则表达式说明:

- ^string : 匹配以 string 开头的字符串
- string$ :匹配以 string 结尾的字符串
- ^string$ :精确匹配 string 字符串
- ((^Man$|^Woman$|^UGM$)) : 值只能在 Man,Woman,UGM 这三个值中选择

下面这部分校验注解说明内容参考自:https://www.cnkirito.moe/spring-validation/ ,感谢@徐靖峰

JSR提供的校验注解:

  • @Null 被注释的元素必须为 null
  • @NotNull 被注释的元素必须不为 null
  • @AssertTrue 被注释的元素必须为 true
  • @AssertFalse 被注释的元素必须为 false
  • @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
  • @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
  • @Size(max=, min=) 被注释的元素的大小必须在指定的范围内
  • @Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
  • @Past 被注释的元素必须是一个过去的日期
  • @Future 被注释的元素必须是一个将来的日期
  • @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式

Hibernate Validator提供的校验注解

  • @NotBlank(message =) 验证字符串非null,且长度必须大于0
  • @Email 被注释的元素必须是电子邮箱地址
  • @Length(min=,max=) 被注释的字符串的大小必须在指定的范围内
  • @NotEmpty 被注释的字符串的必须非空
  • @Range(min=,max=,message=) 被注释的元素必须在合适的范围内

验证Controller的输入

验证请求体(RequestBody)

Controller:

我们在需要验证的参数上加上了@Valid注解,如果验证失败,它将抛出MethodArgumentNotValidException。默认情况下,Spring会将此异常转换为HTTP Status 400(错误请求)。


@RestController
@RequestMapping("/api")
public class PersonController {

    @PostMapping("/person")
    public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
        return ResponseEntity.ok().body(person);
    }
}

ExceptionHandler:

自定义异常处理器可以帮助我们捕获异常,并进行一些简单的处理。如果对于下面的处理异常的代码不太理解的话,可以查看这篇文章 《SpringBoot 处理异常的几种常见姿势》

@ControllerAdvice(assignableTypes = {PersonController.class})
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, String>> handleValidationExceptions(
            MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getAllErrors().forEach((error) -> {
            String fieldName = ((FieldError) error).getField();
            String errorMessage = error.getDefaultMessage();
            errors.put(fieldName, errorMessage);
        });
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
    }
}

通过测试验证:

下面我通过 MockMvc 模拟请求 Controller 的方式来验证是否生效,当然你也可以通过 Postman 这种工具来验证。

我们试一下所有参数输入正确的情况。

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class PersonControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void should_get_person_correctly() throws Exception {
        Person person = new Person();
        person.setName("SnailClimb");
        person.setSex("Man");
        person.setClassId("82938390");
        person.setEmail("Snailclimb@qq.com");

        mockMvc.perform(post("/api/person")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(objectMapper.writeValueAsString(person)))
                .andExpect(MockMvcResultMatchers.jsonPath("name").value("SnailClimb"))
                .andExpect(MockMvcResultMatchers.jsonPath("classId").value("82938390"))
                .andExpect(MockMvcResultMatchers.jsonPath("sex").value("Man"))
                .andExpect(MockMvcResultMatchers.jsonPath("email").value("Snailclimb@qq.com"));
    }
}

验证出现参数不合法的情况抛出异常并且可以正确被捕获。

 @Test
    public void should_check_person_value() throws Exception {
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");

        mockMvc.perform(post("/api/person")
                .contentType(MediaType.APPLICATION_JSON_UTF8)
                .content(objectMapper.writeValueAsString(person)))
                .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可选范围"))
                .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能为空"))
                .andExpect(MockMvcResultMatchers.jsonPath("email").value("email 格式不正确"));
    }

使用 Postman 验证结果如下:

Postman 验证结果

验证请求参数(Path Variables 和 Request Parameters)

Controller:

一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

@RestController
@RequestMapping("/api")
@Validated
public class PersonController {

    @GetMapping("/person/{id}")
    public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超过 id 的范围了") Integer id) {
        return ResponseEntity.ok().body(id);
    }

    @PutMapping("/person")
    public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "超过 name 的范围了") String name) {
        return ResponseEntity.ok().body(name);
    }
}

ExceptionHandler:

    @ExceptionHandler(ConstraintViolationException.class)
    ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }

通过测试验证:

  @Test
    public void should_check_param_value() throws Exception {

        mockMvc.perform(get("/api/person/6")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isBadRequest())
                .andExpect(content().string("getPersonByID.id: 超过 id 的范围了"));
    }

    @Test
    public void should_check_param_value2() throws Exception {

        mockMvc.perform(put("/api/person")
                .param("name","snailclimbsnailclimb")
                .contentType(MediaType.APPLICATION_JSON_UTF8))
                .andExpect(status().isBadRequest())
                .andExpect(content().string("getPersonByName.name: 超过 name 的范围了"));
    }

验证 Service 中的方法

我们还可以验证任何Spring组件的输入,而不是验证控制器级别的输入,我们可以使用@Validated@Valid注释的组合来实现这一需求。

一定一定不要忘记在类上加上 Validated 注解了,这个参数可以告诉 Spring 去校验方法参数。

@Service
@Validated
public class PersonService {

    public void validatePerson(@Valid Person person){
        // do something
    }
}

通过测试验证:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class PersonServiceTest {
    @Autowired
    private PersonService service;

    @Test(expected = ConstraintViolationException.class)
    public void should_throw_exception_when_person_is_not_valid() {
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        service.validatePerson(person);
    }

}

Validator 编程方式手动进行参数验证

某些场景下可能会需要我们手动校验并获得校验结果。

    @Test
    public void check_person_manually() {

        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        Set<ConstraintViolation<Person>> violations = validator.validate(person);
        //output:
        //email 格式不正确
        //name 不能为空
        //sex 值不在可选范围
        for (ConstraintViolation<Person> constraintViolation : violations) {
            System.out.println(constraintViolation.getMessage());
        }
    }

上面我们是通过 Validator 工厂类获得的 Validator 示例,当然你也可以通过 @Autowired 直接注入的方式。但是在非 Spring Component 类中使用这种方式的话,只能通过工厂类来获得 Validator

@Autowired
Validator validate

自定以 Validator(实用)

如果自带的校验注解无法满足你的需求的话,你还可以自定义实现注解。

案例一:校验特定字段的值是否在可选范围

比如我们现在多了这样一个需求:Person类多了一个 region 字段,region 字段只能是ChinaChina-TaiwanChina-HongKong这三个中的一个。

第一步你需要创建一个注解:

@Target({FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = RegionValidator.class)
@Documented
public @interface Region {

    String message() default "Region 值不在可选范围内";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

第二步你需要实现 ConstraintValidator接口,并重写isValid 方法:

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;

public class RegionValidator implements ConstraintValidator<Region, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        HashSet<Object> regions = new HashSet<>();
        regions.add("China");
        regions.add("China-Taiwan");
        regions.add("China-HongKong");
        return regions.contains(value);
    }
}

现在你就可以使用这个注解:

    @Region
    private String region;

案例二:校验电话号码

校验我们的电话号码是否合法,这个可以通过正则表达式来做,相关的正则表达式都可以在网上搜到,你甚至可以搜索到针对特定运营商电话号码段的正则表达式。

PhoneNumber.java

import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public @interface PhoneNumber {
    String message() default "Invalid phone number";
    Class[] groups() default {};
    Class[] payload() default {};
}

PhoneNumberValidator.java

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class PhoneNumberValidator implements ConstraintValidator<PhoneNumber,String> {

    @Override
    public boolean isValid(String phoneField, ConstraintValidatorContext context) {
        if (phoneField == null) {
            // can be null
            return true;
        }
        return phoneField.matches("^1(3[0-9]|4[57]|5[0-35-9]|8[0-9]|70)\\d{8}$") && phoneField.length() > 8 && phoneField.length() < 14;
    }
}

搞定,我们现在就可以使用这个注解了。

@PhoneNumber(message = "phoneNumber 格式不正确")
@NotNull(message = "phoneNumber 不能为空")
private String phoneNumber;

使用验证组

某些场景下我们需要使用到验证组,这样说可能不太清楚,说简单点就是对对象操作的不同方法有不同的验证规则,示例如下(这个就我目前经历的项目来说使用的比较少,因为本身这个在代码层面理解起来是比较麻烦的,然后写起来也比较麻烦)。

先创建两个接口:

public interface AddPersonGroup {
}
public interface DeletePersonGroup {
}

我们可以这样去使用验证组

@NotNull(groups = DeletePersonGroup.class)
@Null(groups = AddPersonGroup.class)
private String group;
@Service
@Validated
public class PersonService {

    public void validatePerson(@Valid Person person) {
        // do something
    }

    @Validated(AddPersonGroup.class)
    public void validatePersonGroupForAdd(@Valid Person person) {
        // do something
    }

    @Validated(DeletePersonGroup.class)
    public void validatePersonGroupForDelete(@Valid Person person) {
        // do something
    }

}

通过测试验证:

  @Test(expected = ConstraintViolationException.class)
    public void should_check_person_with_groups() {
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        person.setGroup("group1");
        service.validatePersonGroupForAdd(person);
    }

    @Test(expected = ConstraintViolationException.class)
    public void should_check_person_with_groups2() {
        Person person = new Person();
        person.setSex("Man22");
        person.setClassId("82938390");
        person.setEmail("SnailClimb");
        service.validatePersonGroupForDelete(person);
    }

使用验证组这种方式的时候一定要小心,这是一种反模式,还会造成代码逻辑性变差。

代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/advanced/bean-validation-demo

@NotNull vs @Column(nullable = false)(重要)

在使用 JPA 操作数据的时候会经常碰到 @Column(nullable = false) 这种类型的约束,那么它和 @NotNull 有何区别呢?搞清楚这个还是很重要的!

  • @NotNull是 JSR 303 Bean验证批注,它与数据库约束本身无关。
  • @Column(nullable = false) : 是JPA声明列为非空的方法。

总结来说就是即前者用于验证,而后者则用于指示数据库创建表的时候对表的约束。

TODO

  •  原理分析

参考


很多时候我们都需要为系统建立一个定时任务来帮我们做一些事情,SpringBoot 已经帮我们实现好了一个,我们只需要直接使用即可,当然你也可以不用 SpringBoot 自带的定时任务,整合 Quartz 很多时候也是一个不错的选择。

本文不涉及 SpringBoot 整合 Quartz 的内容,只演示了如何使用 SpringBoot 自带的实现定时任务的方式。相关代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/springboot-schedule-tast

Spring Schedule 实现定时任务

我们只需要 SpringBoot 项目最基本的依赖即可,所以这里就不贴配置文件了。

1. 创建一个 scheduled task

我们使用 @Scheduled 注解就能很方便地创建一个定时任务,下面的代码中涵盖了 @Scheduled的常见用法,包括:固定速率执行、固定延迟执行、初始延迟执行、使用 Cron 表达式执行定时任务。

Cron 表达式: 主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。

推荐一个在线Cron表达式生成器:http://cron.qqe2.com/

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

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * @author shuang.kou
 */
@Component
public class ScheduledTasks {
    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    /**
     * fixedRate:固定速率执行。每5秒执行一次。
     */
    @Scheduled(fixedRate = 5000)
    public void reportCurrentTimeWithFixedRate() {
        log.info("Current Thread : {}", Thread.currentThread().getName());
        log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date()));
    }

    /**
     * fixedDelay:固定延迟执行。距离上一次调用成功后2秒才执。
     */
    @Scheduled(fixedDelay = 2000)
    public void reportCurrentTimeWithFixedDelay() {
        try {
            TimeUnit.SECONDS.sleep(3);
            log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * initialDelay:初始延迟。任务的第一次执行将延迟5秒,然后将以5秒的固定间隔执行。
     */
    @Scheduled(initialDelay = 5000, fixedRate = 5000)
    public void reportCurrentTimeWithInitialDelay() {
        log.info("Fixed Rate Task with Initial Delay : The time is now {}", dateFormat.format(new Date()));
    }

    /**
     * cron:使用Cron表达式。 每分钟的1,2秒运行
     */
    @Scheduled(cron = "1-2 * * * * ? ")
    public void reportCurrentTimeWithCronExpression() {
        log.info("Cron Expression: The time is now {}", dateFormat.format(new Date()));
    }
}

关于 fixedRate 这里其实有个坑,假如我们有这样一种情况:我们某个方法的定时器设定的固定速率是每5秒执行一次。这个方法现在要执行下面四个任务,四个任务的耗时是:6 s、6s、 2s、 3s,请问这些任务默认情况下(单线程)将如何被执行?

我们写一段简单的程序验证:

    private static final Logger log = LoggerFactory.getLogger(AsyncScheduledTasks.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    private List<Integer> index = Arrays.asList(6, 6, 2, 3);
    int i = 0;
    @Scheduled(fixedRate = 5000)
    public void reportCurrentTimeWithFixedRate() {
        if (i == 0) {
            log.info("Start time is {}", dateFormat.format(new Date()));
        }
        if (i < 5) {
            try {
                TimeUnit.SECONDS.sleep(index.get(i));
                log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date()));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            i++;
        }
    }

运行程序输出如下:

Start time is 20:58:33
Fixed Rate Task : The time is now 20:58:39
Fixed Rate Task : The time is now 20:58:45
Fixed Rate Task : The time is now 20:58:47
Fixed Rate Task : The time is now 20:58:51

看下面的运行任务示意图应该很好理解了。

如果我们将这个方法改为并行运行,运行结果就截然不同了。

 2. 启动类上加上@EnableScheduling注解

在 SpringBoot 中我们只需要在启动类上加上@EnableScheduling便可以启动定时任务了。

@SpringBootApplication
@EnableScheduling
public class DemoApplication {

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

3. 自定义线程池执行 scheduled task

默认情况下,@Scheduled任务都在Spring创建的大小为1的默认线程池中执行,你可以通过在加了@Scheduled注解的方法里加上下面这段代码来验证。

logger.info("Current Thread : {}", Thread.currentThread().getName());

你会发现加上上面这段代码的定时任务,每次运行都会输出:

Current Thread : scheduling-1

如果我们需要自定义线程池执行话只需要新加一个实现SchedulingConfigurer接口的 configureTasks 的类即可,这个类需要加上 @Configuration 注解。

@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
    private final int POOL_SIZE = 10;

    @Override
    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();

        threadPoolTaskScheduler.setPoolSize(POOL_SIZE);
        threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
        threadPoolTaskScheduler.initialize();

        scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
    }
}

通过上面的验证的方式输出当前线程的名字会改变。

4. @EnableAsync 和 @Async 使定时任务并行执行

如果你想要你的代码并行执行的话,还可以通过@EnableAsync 和 @Async这两个注解实现

@Component
@EnableAsync
public class AsyncScheduledTasks {
    private static final Logger log = LoggerFactory.getLogger(AsyncScheduledTasks.class);
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    /**
     * fixedDelay:固定延迟执行。距离上一次调用成功后2秒才执。
     */
    //@Async
    @Scheduled(fixedDelay = 2000)
    public void reportCurrentTimeWithFixedDelay() {
        try {
            TimeUnit.SECONDS.sleep(3);
            log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行程序输出如下,reportCurrentTimeWithFixedDelay() 方法会每5秒执行一次,因为我们说过了@Scheduled任务都在Spring创建的大小为1的默认线程池中执行。

Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:23
Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:28
Current Thread : scheduling-1
Fixed Delay Task : The time is now 14:24:33

reportCurrentTimeWithFixedDelay() 方法上加上 @Async 注解后输出如下,reportCurrentTimeWithFixedDelay() 方法会每 2 秒执行一次。

Current Thread : task-1
Fixed Delay Task : The time is now 14:27:32
Current Thread : task-2
Fixed Delay Task : The time is now 14:27:34
Current Thread : task-3
Fixed Delay Task : The time is now 14:27:36

代码地址:https://github.com/Snailclimb/springboot-guide/tree/master/source-code/advanced/springboot-schedule-tast


本文已经收录自 springboot-guide : https://github.com/Snailclimb/springboot-guide (Spring Boot 核心知识点整理。 基于 Spring Boot 2.19+。)

新手也能看懂的 SpringBoot 异步编程指南

通过本文你可以了解到下面这些知识点:

  1. Future 模式介绍以及核心思想
  2. 核心线程数、最大线程数的区别,队列容量代表什么;
  3. ThreadPoolTaskExecutor 饱和策略;
  4. SpringBoot 异步编程实战,搞懂代码的执行逻辑。

Future 模式

异步编程在处理耗时操作以及多任务处理的场景下非常有用,我们可以更好的让我们的系统利用好机器的 CPU 和 内存,提高它们的利用率。多线程设计模式有很多种,Future模式是多线程开发中非常常见的一种设计模式,本文也是基于这种模式来说明 SpringBoot 对于异步编程的知识。

实战之前我先简单介绍一下 Future 模式的核心思想 吧!。

Future 模式的核心思想是 异步调用 。当我们执行一个方法时,假如这个方法中有多个耗时的任务需要同时去做,而且又不着急等待这个结果时可以让客户端立即返回然后,后台慢慢去计算任务。当然你也可以选择等这些任务都执行完了,再返回给客户端。这个在 Java 中都有很好的支持,我在后面的示例程序中会详细对比这两种方式的区别。

SpringBoot 异步编程实战

如果我们需要在 SpringBoot 实现异步编程的话,通过 Spring 提供的两个注解会让这件事情变的非常简单。

  1. @EnableAsync:通过在配置类或者Main类上加@EnableAsync开启对异步方法的支持。
  2. @Async 可以作用在类上或者方法上,作用在类上代表这个类的所有方法都是异步方法。

1. 自定义 TaskExecutor

很多人对于 TaskExecutor 不是太了解,所以我们花一点篇幅先介绍一下这个东西。从名字就能看出它是任务的执行者,它领导执行着线程来处理任务,就像司令官一样,而我们的线程就好比一只只军队一样,这些军队可以异步对敌人进行打击?。

Spring 提供了TaskExecutor接口作为任务执行者的抽象,它和java.util.concurrent包下的Executor接口很像。稍微不同的 TaskExecutor接口用到了 Java 8 的语法@FunctionalInterface声明这个接口是一个函数式接口。

org.springframework.core.task.TaskExecutor

@FunctionalInterface
public interface TaskExecutor extends Executor {
    void execute(Runnable var1);
}

如果没有自定义Executor, Spring 将创建一个 SimpleAsyncTaskExecutor 并使用它。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

/** @author shuang.kou */
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

  private static final int CORE_POOL_SIZE = 6;
  private static final int MAX_POOL_SIZE = 10;
  private static final int QUEUE_CAPACITY = 100;

  @Bean
  public Executor taskExecutor() {
    // Spring 默认配置是核心线程数大小为1,最大线程容量大小不受限制,队列容量也不受限制。
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // 核心线程数
    executor.setCorePoolSize(CORE_POOL_SIZE);
    // 最大线程数
    executor.setMaxPoolSize(MAX_POOL_SIZE);
    // 队列大小
    executor.setQueueCapacity(QUEUE_CAPACITY);
    // 当最大池已满时,此策略保证不会丢失任务请求,但是可能会影响应用程序整体性能。
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.setThreadNamePrefix("My ThreadPoolTaskExecutor-");
    executor.initialize();
    return executor;
  }
}

ThreadPoolTaskExecutor 常见概念:

  • Core Pool Size : 核心线程数线程数定义了最小可以同时运行的线程数量。
  • Queue Capacity : 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
  • Maximum Pool Size : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。

一般情况下不会将队列大小设为:Integer.MAX_VALUE,也不会将核心线程数和最大线程数设为同样的大小,这样的话最大线程数的设置都没什么意义了,你也无法确定当前 CPU 和内存利用率具体情况如何。

如果队列已满并且当前同时运行的线程数达到最大线程数的时候,如果再有新任务过来会发生什么呢?

Spring 默认使用的是 ThreadPoolExecutor.AbortPolicy。在Spring的默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy。当最大池被填满时,此策略为我们提供可伸缩队列。

ThreadPoolTaskExecutor 饱和策略定义:

如果当前同时运行的线程数量达到最大线程数量时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
  • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

2. 编写一个异步的方法

下面模拟一个查找对应字符开头电影的方法,我们给这个方法加上了@Async注解来告诉 Spring 它是一个异步的方法。另外,这个方法的返回值 CompletableFuture.completedFuture(results)这代表我们需要返回结果,也就是说程序必须把任务执行完成之后再返回给用户。

请留意completableFutureTask方法中的第一行打印日志这句代码,后面分析程序中会用到,很重要!

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

/** @author shuang.kou */
@Service
public class AsyncService {

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

  private List<String> movies =
      new ArrayList<>(
          Arrays.asList(
              "Forrest Gump",
              "Titanic",
              "Spirited Away",
              "The Shawshank Redemption",
              "Zootopia",
              "Farewell ",
              "Joker",
              "Crawl"));

  /** 示范使用:找到特定字符/字符串开头的电影 */
  @Async
  public CompletableFuture<List<String>> completableFutureTask(String start) {
    // 打印日志
    logger.warn(Thread.currentThread().getName() + "start this task!");
    // 找到特定字符/字符串开头的电影
    List<String> results =
        movies.stream().filter(movie -> movie.startsWith(start)).collect(Collectors.toList());
    // 模拟这是一个耗时的任务
    try {
      Thread.sleep(1000L);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    //返回一个已经用给定值完成的新的CompletableFuture。
    return CompletableFuture.completedFuture(results);
  }
}

3. 测试编写的异步方法

/** @author shuang.kou */
@RestController
@RequestMapping("/async")
public class AsyncController {
  @Autowired 
  AsyncService asyncService;

  @GetMapping("/movies")
  public String completableFutureTask() throws ExecutionException, InterruptedException {
    //开始时间
    long start = System.currentTimeMillis();
    // 开始执行大量的异步任务
    List<String> words = Arrays.asList("F", "T", "S", "Z", "J", "C");
    List<CompletableFuture<List<String>>> completableFutureList =
        words.stream()
            .map(word -> asyncService.completableFutureTask(word))
            .collect(Collectors.toList());
    // CompletableFuture.join()方法可以获取他们的结果并将结果连接起来
    List<List<String>> results = completableFutureList.stream().map(CompletableFuture::join).collect(Collectors.toList());
    // 打印结果以及运行程序运行花费时间
    System.out.println("Elapsed time: " + (System.currentTimeMillis() - start));
    return results.toString();
  }
}

请求这个接口,控制台打印出下面的内容:

2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-1] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-1start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-6] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-6start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-5] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-5start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-4] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-4start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-3] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-3start this task!
2019-10-01 13:50:17.007  WARN 18793 --- [lTaskExecutor-2] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-2start this task!
Elapsed time: 1010

首先我们可以看到处理所有任务花费的时间大概是 1 s。这与我们自定义的 ThreadPoolTaskExecutor 有关,我们配置的核心线程数是 6 ,然后通过通过下面的代码模拟分配了 6 个任务给系统执行。这样每个线程都会被分配到一个任务,每个任务执行花费时间是 1 s ,所以处理 6 个任务的总花费时间是 1 s。

List<String> words = Arrays.asList("F", "T", "S", "Z", "J", "C");  
List<CompletableFuture<List<String>>> completableFutureList =
        words.stream()
            .map(word -> asyncService.completableFutureTask(word))
            .collect(Collectors.toList());

你可以自己验证一下,试着去把核心线程数的数量改为 3 ,再次请求这个接口你会发现处理所有任务花费的时间大概是 2 s。

另外,从上面的运行结果可以看出,当所有任务执行完成之后才返回结果。这种情况对应于我们需要返回结果给客户端请求的情况下,假如我们不需要返回任务执行结果给客户端的话呢? 就比如我们上传一个大文件到系统,上传之后只要大文件格式符合要求我们就上传成功。普通情况下我们需要等待文件上传完毕再返回给用户消息,但是这样会很慢。采用异步的话,当用户上传之后就立马返回给用户消息,然后系统再默默去处理上传任务。这样也会增加一点麻烦,因为文件可能会上传失败,所以系统也需要一点机制来补偿这个问题,比如当上传遇到问题的时候,发消息通知用户。

下面会演示一下客户端不需要返回结果的情况:

completableFutureTask方法变为 void 类型

@Async
public void completableFutureTask(String start) {
  ......
  //这里可能是系统对任务执行结果的处理,比如存入到数据库等等......
  //doSomeThingWithResults(results);
}

Controller 代码修改如下:

  @GetMapping("/movies")
  public String completableFutureTask() throws ExecutionException, InterruptedException {
    // Start the clock
    long start = System.currentTimeMillis();
    // Kick of multiple, asynchronous lookups
    List<String> words = Arrays.asList("F", "T", "S", "Z", "J", "C");
        words.stream()
            .forEach(word -> asyncService.completableFutureTask(word));
    // Wait until they are all done
    // Print results, including elapsed time
    System.out.println("Elapsed time: " + (System.currentTimeMillis() - start));
    return "Done";
  }

请求这个接口,控制台打印出下面的内容:

Elapsed time: 0
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-4] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-4start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-3] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-3start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-2] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-2start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-1] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-1start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-6] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-6start this task!
2019-10-01 14:02:44.052  WARN 19051 --- [lTaskExecutor-5] g.j.a.service.AsyncService               : My ThreadPoolTaskExecutor-5start this task!

可以看到系统会直接返回给用户结果,然后系统才真正开始执行任务。

待办

Reference