ramostear.comramostear.com 谭朝红的技术分享博客

格言 编程是一门技术,也是一门艺术 !

5个Spring Event奇技淫巧,你GET到了吗?

5个Spring Event奇技淫巧,你GET到了吗?

5个Spring Event奇技淫巧,你GET到了吗?

前言:

谈到事件,接触过前端或GUI编程(JavaScript,Swing)的同学应该有较深刻印象。如事件源、事件的监听、回调等概念应该耳熟能详。而在Web应用程序中则很少用到事件。但在Web应用程序中,也可以轻松实现面向事件的编程。

1、为什么需要面向事件编程

​ 在本文中,将基于Spring框架来谈谈关于面向事件编程的5个奇技淫巧。在开始主要内容之前,先了解一下为什么需要面向事件编程。首先看一个生活中的一个场景来帮助我们快速的了解面向事件编程所带来的好处。以客户到银行柜台取现为例,在较早以前,银行柜台取现需要一个接一个的排长龙等待柜台业务员处理取现业务,在此期间,客户不能离开队伍去做其他的事情,否则需要重新排队。这好比常规的面向过程的编程,需要依次执行每条逻辑语句,直到所有的语句都执行完毕,方法才返回结果。现在,银行柜台取现多了一台叫号机,需要办理取现业务的客户先通过叫号机领取一张业务号,如果等待人数过多的时候,客户可以先处理自身的事情,直到柜台叫到自己的号时,才到柜台办理取现业务。这就是一个典型(不太严谨)面向事件的处理过程。首先,客户通过叫号机注册一个取现的事件,此时的叫号机,相当于事件发布器(事件注册中心),客户相当于事件源,当柜台收到有客户需要取现的消息时,就会广播提示客户办理取现业务,柜台就相当于一个事件监听器。

​ 如上所述,面向事件的编程(也称作事件驱动编程)是基于对消息信号的反应的编程。面向事件编程需要满三个条件:

  • 1、事件源:消息的源头,如点击一个按钮、访问某个页面、新增一条数据等。
  • 2、事件注册中心:事件注册中心用于存储和发布事件。
  • 3、事件监听器:用于接收特定的消息,并作出对应的反应。

下面通过一张图更为直观的了解面向事件编程的基本原理:

图 1-1 事件处理机制

2、Spring Events

​ Spring Events 是Spring Framework的一部分,但在应用程序中并不经常使用,甚至被忽略。然而,Spring的Application拥有强大的事件发布并注册事件监听器的能力,它拥有一套完整的事件发布与处理机制。Spring 4.1开始引入@EventListener注解来监听具体类型的事件,它的出现,消除了使用Spring定义一个事件监听器的复杂操作。仅仅通过一个简单的注解,就可以完成一个监听器的定义,你不需要额外的进行其他的配置;这简直太棒了。在Spring Framework中,事件的发布是由ApplicationContext提供的,如果想完成一个完整的面向事件(也称为事件驱动编程)编程,你需要遵循以下三个基本的原则:

  • 1、定义一个事件,且扩展ApplicationEvent
  • 2、事件发布者应该注入一个ApplicationEventPublisher对象
  • 3、监听器实现ApplicationListener接口或者使用@EventListener注解

在开始介绍面向事件编程的具体实施细节之前,先通过一张类图了解一下Spring的事件机制设计类图:

图 2-1 Spring Event 类图

​ 通过类图,先快速对Spring Event事件类图做一个简单的介绍:

  • 1、ApplicationEventPublisher是Spring的事件发布接口,所有的事件都是通过该接口提供的publishEvent方法发布事件的。
  • 2、ApplicationEventMulticaster是Spring事件广播器,它将提供一个SimpleApplicationEventMulticaster实现类,如果没有提供任何可用的自定义广播器,Spring将采用默认的广播规则。
  • 3、ApplicationListener是Spring事件监听接口,所有的事件监听器都需要实现此接口,并通过onApplicationEvent方法处理具体的逻辑。
  • 4、在Spring Framework中,ApplicationContext本身就具备监听器的能力,我们也可以通过ApplicationContext发布先关的事件。

​ 通过分析,可以看到:当一个事件通过ApplicationEventPublisher发布出去之后,ApplicationEventMulticaster广播器就会开始工作,它将去ApplicationContext中寻找对应的事件监听器(ApplicationListener),并执行对应监听器中的onApplicationEvent方法,从而完成事件监听的全部逻辑。

3、自定义Spring Event

​ 本次内容将使用Spring Boot 2.1.5快速搭建所需要的环境。首先将演示如何自定义一个事件,并监听该事件且对监听到的事件做出反应。

​ 我们将使用IntelliJ IDEA创建一个Web项目,并在pom.xml文件中添加如下的依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ramostear</groupId>
    <artifactId>spring-boot-event</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-event</name>
    <description>Demo project for Spring events</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
            <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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

说明:

这将是一个完整的演示Spring Event事件处理机制的项目,因为期间需要演示事务管理的特性,所以加入了MySql数据库依赖。

​ 本次的内容将依据这样一个场景展开:当成功添加一条用户信息后,将发送一条邮件到用户的邮箱账户。首先,需要定义两个实体,用户实体和邮件详情实体。具体代码如下:

接下来是邮件详情实体类:

持久化类通过扩展JPA的JpaRepository类快速获得CRUD能力。用户持久化类和邮件详情持久化类分别如下:

UserRepository.java

package com.ramostear.repository;

import com.ramostear.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface UserRepository extends JpaRepository<User,Long> {
}

EmailDetailRepository.java

package com.ramostear.repository;

import com.ramostear.domain.EmailDetail;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface EmailDetailRepository extends JpaRepository<EmailDetail,Long> {
}

接下来,将定义一个用户业务接口,提供创建和查找用户两个方法,代码如下:

package com.ramostear.service;

import com.ramostear.domain.User;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
public interface UserService {

    User created(User user);


    User findOne(Long id);
}

基本的环境已经准备就绪,现在开始定义定义第一个事件类:SendEmailEvent.java,其代码如下所示

如之前所述,自定义事件需要扩展ApplicationEvent类,并提供自己的实现细节。这里的事件需要为其提供一个用户ID,在接下来的监听器中,通过此ID来甄别具体的用户,并发送邮件。

接下来,我们将使用@EventListener注解实现一个事件监听器,其源码如下:

说明:

在此类中,我们注入了EmailDetailRepository和UserRepository两个依赖项,并使用@EventListener注解对sendEmail方法进行了标注。在方法中,使用了Thread.sleep(1000*3)让程序休眠3秒,以模拟发送邮件时所耗费的时间。@Slf4j注解是Lombok提供的快速使用Logger的一个快捷方式。@Component注解用于标注此类是一个普通的组件,将被Spring自动扫描并管理。

​ 准备好了事件源以及事件监听器,现在来看看如何发布一个事件。发布事件将在用户业务实现类中进行操作。UserServiceImpl.java类的实现细节如下:

​ 如前面章节所讲,如果使用Spring发布一个事件,需要在具体的类中注入一个ApplicationEventPublisher对象。在用户业务实现类中,通过构造函数的方式注入ApplicationEventPublisher对象和UserRepository持久化对象。在created(User user)方法中,当成功保存用户数据后,通过发布一个SendEmailEvent事件,将邮件发送给用户的邮箱。与此同时,我们记录了程序执行完所耗费的事件,在下一小节中我们将使用到这个数据。

​ 最后,我们定义一个控制器,提供一个处理添加用户请求的方法,验证发送邮件事件是否被监听器成功监听。控制器代码如下:

启动项目,并使用PostMan工具对POST /users API进行测试:

图 3-1 Postman测试结果

经测试,成功返回了用户信息,且耗时3530ms。我们在看看控制台的日志信息:

图 3-2 日志记录信息

​ 至此,第一个基于Spring Event的面向事件的编程案例完成。在这个案例中,虽然我们解决了事件的发送和监听的编码实现问题,但可能会有同学会想,这样子的处理方式和将发布事件的代码替换成直接调用邮件持久化类保存邮件信息没有任何区别,如同一开始说的案例,虽然有叫号机叫号,解决排队的问题,但等待的时间依旧没有改变,这和直接排队在时间耗费上没有根本的变化。那么针对这样一个问题,我们在接下来的内容中将讲解如何实现异步事件。异步事件就好比你现在从普通储蓄客户变成了VIP会员,你可以不去银行柜台叫号,而是通过电话或者其他手段预约一个时间去办理业务,银行客户经理会给你预留相应的时间,当预约时间到达之后,你才去银行柜台办理业务。

4、Spring 异步事件

​ 在该小节的基础上,将原有的事件改造成异步事件,以节省客户端等待服务端响应的时间。使用Spring实现异步事件有两种方式,第一种方式是使用@Async注解对监听器进行标注,这是最为简单的一种实现方式;第二种是通过配置类对Spring的事件广播器进行配置,设置Multicaster对异步操作的支持,此方法比第一种方法稍微复杂一些,但可以拥有对事件广播器更多的控制权。接下来,将分别介绍这两种实现方式。

4-1、使用@Async注解实现异步事件

​ 在现有的工程中,我们需要对两个类进行改造。第一处改造是在项目主类上加入@EnableAsync注解,开启对异步操作的支持。代码如下:

package com.ramostear;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class SpringBootEventApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootEventApplication.class, args);
    }
}

第二步是改造监听器,在原来的方法上加入@Async注解,其余部分保持不变,代码如下:

    @Async
    @EventListener
    public void sendEmail(SendEmailEvent event){
       ...
    }

通过上述两个步骤,我们就将Spring默认的同步事件改造为异步事件。重新启动项目,依旧使用Postman对接口POST /users进行测试,结果如下:

图 4-1 @Async异步事件

通过上图我们可以看到,服务端成功返回用户信息,注意观察,从客户端发起请求到服务端响应结束总共耗时480ms,而在第一个案例中,请求和响应的总耗时为3530ms。最后我们再来看看控制台的日志信息,确定一下数据库是否成功保存用户邮件:

图 4-2 @Async异步事件控制台日志信息

通过上述的测试结果,我们可以看到,异步事件已经生效,在相同的环境下,同样的业务请求,使用同步事件和异步事件,服务端响应时间存在巨大的差异。同步事件耗时3530ms,异步事件耗时480ms,这就是异步事件带来的巨大优势。

4-2 、使用配置文件开启对异步事件的支持

​ 在4-1中,我们学会了如何使用@Async定义异步事件,那本小节将展示另外一种方式获取对异步事件的支持。首先需要定义一个配置类,该类用于配置SimpleApplicationEventMulticaster类对于异步事件的支持。其源码如下:

package com.ramostear.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.core.task.SimpleAsyncTaskExecutor;

/**
 * Created by Ramostear on 2019/5/31 0031.
 */
@Configuration
public class AsyncSpringEventConfig {

    @Bean(name = "applicationEventMulticaster")
    public ApplicationEventMulticaster applicationEventMulticaster(){
        SimpleApplicationEventMulticaster multicaster = new SimpleApplicationEventMulticaster();
        multicaster.setTaskExecutor(new SimpleAsyncTaskExecutor());
        return multicaster;
    }
}

​ 在此配置文件中,applicationEventMulticaster方法将返回一个SimpleApplicationEventMulticaster对象。在返回此事件发布器之前,我们为此事件发布器设置了一个任务执行器:SimpleAsyncTaskExecutor,当应用程序中有新的异步事件时,将会调用此异步任务执行器的submitListenable(Callable task)方法,发布异步事件任务,并选择相应的监听器处理事件。定义好配置类后,还需要将主类中的@EnableAsync和监听器中的@Async两个注解注释掉,其余部分保持不变。最后重启项目,并测试API接口。

图 4-3 异步事件发布器配置测试

如图4-3所示,测试成功返回用户信息,其请求和响应总耗时为492ms。通过配置的方式开启Spring对异步事件的支持已生效。

5、事件过滤

​ 如果我们想对用户事件进行限制,某一部分满足条件的用户才发送邮件,可以使用condition属性来指定限制的条件。这就好比银行柜台提供的VIP预约取现业务,它分不同等级的VIP,只有达到某个条件时,才享有预约服务。在原来的基础上,我们做如下的改动,当用户ID大于5时,才发送邮件给用户。代码如下:

    @EventListener(condition = "#event.userId > 5")
    public void sendEmail(SendEmailEvent event){
       ...
    }

现在,数据库中用户ID最大为3,我们先添加两个用数据,看邮件详情表是否会新增数据。启动项目,测试API接口:

图 5-1 条件过滤测试一

服务端成功返回结果,用户ID=4,现在看看数据库是否有用户邮件数据被添加:

图 5-2 数据库查询结果一

从查询结果可以看到,并未给ID=4的用户发送邮件。控制台也只输出了插入用户数据的SQL语句:

Hibernate: 
    insert 
    into
        t_user
        (email, first_name, last_name) 
    values
        (?, ?, ?)
2019-06-03 06:14:19.334  INFO 4652 --- [p-nio-80-exec-2] c.r.service.impl.UserServiceImpl         : total times :58

接下来我们再新增两条用户数据,观察当用户ID=6时,是否会为用户发送邮件。图5-3是当用户ID=6时,数据库的查询结果以及控制台的输出:

图 5-3 数据库查询结果二

此时,控制台的输出信息如下所示:

图 5-4 控制台日志信息

由此可以看出,我们的事件过滤条件已生效。

6、事务处理

​ 现在,Spring Event的相关使用技巧以及介绍了大部分,最后让我们来考虑这样一个问题,如果在保存用户的时候发生错误,事件将会怎么样?在监听器处理事件时,保存用户的事务还未提交会怎么样?按照常规的逻辑,当用户持久化事务未提交,或者保存过程中发生异常时,不应该给用户发送邮件。试想一下,如果本次案例的场景是发生在用户注册的业务中,如果用户尚未注册成功,就给用户发送一封邮件,那是多么尴尬的事情。这就涉及到事件的事务控制。在Spring中,可以使用@TransactionalEventListener注解对监听器进行标注,一旦使用此注解,被标注的方法将被纳入到事务管理的范围。此注解是@EventListener的扩展,它允许你将事件的监听绑定到某个事务阶段。可以通过phase属性对事务阶段进行设置。下面是phase可设置的所有事务阶段:

  • 1、AFTER_COMMIT(默认值),用于在事务成功完成后出发事件
  • 2、AFTER_ROLLBACK,用于事务回滚是出发事件
  • 3、AFTER_COMPLETION,用于事务完成后出发事件(AFTER_COMMIT和AFTER_ROLLBACK的一种模糊表述)
  • 4、BEFORE_COMMIT,用于事务提交前出发事件

最后给出一个简单的示例:

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT,condition = "#event.userId >=5")
    public void sendEmail(SendEmailEvent event){
        Optional<User> optional = userRepository.findById(event.getUserId());
        if(optional.isPresent()){
            User user = optional.get();
            log.info("User Detail:{ id:{},firstName:{},email:{}",user.getId(),user.getFirstName(),user.getEmail());
            try {
                Thread.sleep(1000*3);
            } catch (InterruptedException e) {
                log.error(e.getLocalizedMessage());
            }
            emailDetailRepository.save(EmailDetail.builder().
                    userId(user.getId()).
                    email(user.getEmail()).
                    sendTime(new Date()).
                    build());
            log.info("User email already send.");
        }
    }

其余代码保持不变。

总结

以上就是关于Spring Event的5个实用技巧,如果你想了解更多细节内容,我已将全部源码上传至Github仓库,你可以通过此链接获取全部源码。如有问题,请在文章最后给我留言。

(七讲)Spring Boot Annotations 全家桶快速通

(七讲)Spring Boot Annotations 全家桶快速通

本次课程将对Spring Boot 中所涉及到的注解进行全部的梳理,将开发过程中常用的Annotation进行介绍并给出相应的代码示例。帮助开发者统一的、规范的、全面的学习并掌握Spring Framework Annotations的用法。

1、Spring Web MVC 与Spring Bean 注解

1-1、Spring Web MVC 注解

1-1-1、@RequestMapping

@RequestMapping注解的主要用途是将Web请求与请求处理类中的方法进行映射。Spring MVC和Spring WebFlux都通过RquestMappingHandlerMappingRequestMappingHndlerAdapter两个类来提供对@RequestMapping注解的支持。

@RequestMapping注解对请求处理类中的请求处理方法进行标注;@RequestMapping注解拥有以下的六个配置属性:

  • value:映射的请求URL或者其别名
  • method:兼容HTTP的方法名
  • params:根据HTTP参数的存在、缺省或值对请求进行过滤
  • header:根据HTTP Header的存在、缺省或值对请求进行过滤
  • consume:设定在HTTP请求正文中允许使用的媒体类型
  • product:在HTTP响应体中允许使用的媒体类型

提示:在使用@RequestMapping之前,请求处理类还需要使用@Controller@RestController进行标记

下面是使用@RequestMapping的两个示例:

@RequestMapping还可以对类进行标记,这样类中的处理方法在映射请求路径时,会自动将类上@RequestMapping设置的value拼接到方法中映射路径之前,如下:

1-1-2、@RequestBody

@RequestBody在处理请求方法的参数列表中使用,它可以将请求主体中的参数绑定到一个对象中,请求主体参数是通过HttpMessageConverter传递的,根据请求主体中的参数名与对象的属性名进行匹配并绑定值。此外,还可以通过@Valid注解对请求主体中的参数进行校验。下面是一个使用@RequestBody的示例:

1-1-3、@GetMapping

@GetMapping注解用于处理HTTP GET请求,并将请求映射到具体的处理方法中。具体来说,@GetMapping是一个组合注解,它相当于是@RequestMapping(method=RequestMethod.GET)的快捷方式。下面是@GetMapping的一个使用示例:

1-1-4、@PostMapping

@PostMapping注解用于处理HTTP POST请求,并将请求映射到具体的处理方法中。@PostMapping@GetMapping一样,也是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.POST)的快捷方式。下面是使用@PostMapping的一个示例:

1-1-5、@PutMapping

@PutMapping注解用于处理HTTP PUT请求,并将请求映射到具体的处理方法中,@PutMapping是一个组合注解,相当于是@RequestMapping(method=HttpMethod.PUT)的快捷方式。下面是使用@PutMapping的一个示例:

1-1-6、@DeleteMapping

@DeleteMapping注解用于处理HTTP DELETE请求,并将请求映射到删除方法中。@DeleteMapping是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.DELETE)的快捷方式。下面是使用@DeleteMapping的一个示例:

1-1-7、@PatchMapping

@PatchMapping注解用于处理HTTP PATCH请求,并将请求映射到对应的处理方法中。@PatchMapping相当于是@RequestMapping(method=HttpMethod.PATCH)的快捷方式。下面是一个简单的示例:

1-1-8、@ControllerAdvice

@ControllerAdvice@Component注解的一个延伸注解,Spring会自动扫描并检测被@ControllerAdvice所标注的类。@ControllerAdvice需要和@ExceptionHandler@InitBinder以及@ModelAttribute注解搭配使用,主要是用来处理控制器所抛出的异常信息。首先,我们需要定义一个被@ControllerAdvice所标注的类,在该类中,定义一个用于处理具体异常的方法,并使用@ExceptionHandler注解进行标记。此外,在有必要的时候,可以使用@InitBinder在类中进行全局的配置,还可以使用@ModelAttribute配置与视图相关的参数。使用@ControllerAdvice注解,就可以快速的创建统一的,自定义的异常处理类。下面是一个使用@ControllerAdvice的示例代码:

1-1-9、@ResponseBody

@ResponseBody会自动将控制器中方法的返回值写入到HTTP响应中。特别的,@ResponseBody注解只能用在被@Controller注解标记的类中。如果在被@RestController标记的类中,则方法不需要使用@ResponseBody注解进行标注。@RestController相当于是@Controller@ResponseBody的组合注解。下面是使用该注解的一个示例:

1-1-10、@ExceptionHandler

@ExceptionHander注解用于标注处理特定类型异常类所抛出异常的方法。当控制器中的方法抛出异常时,Spring会自动捕获异常,并将捕获的异常信息传递给被@ExceptionHandler标注的方法。下面是使用该注解的一个示例:

1-1-11、@ResponseStatus

@ResponseStatus注解可以标注请求处理方法。使用此注解,可以指定响应所需要的HTTP STATUS。特别地,我们可以使用HttpStauts类对该注解的value属性进行赋值。下面是使用@ResponseStatus注解的一个示例:

1-1-12、@PathVariable

@PathVariable注解是将方法中的参数绑定到请求URI中的模板变量上。可以通过@RequestMapping注解来指定URI的模板变量,然后使用@PathVariable注解将方法中的参数绑定到模板变量上。特别地,@PathVariable注解允许我们使用value或name属性来给参数取一个别名。下面是使用此注解的一个示例:

​ 模板变量名需要使用“{ }”进行包裹,如果方法的参数名与URI模板变量名一致,则在@PathVariable中就可以省略别名的定义。下面是一个简写的示例:

提示:如果参数是一个非必须的,可选的项,则可以在@PathVariable中设置require = false

1-1-13、@RequestParam

@RequestParam注解用于将方法的参数与Web请求的传递的参数进行绑定。使用@RequestParam可以轻松的访问HTTP请求参数的值。下面是使用该注解的代码示例:

该注解的其他属性配置与@PathVariable的配置相同,特别的,如果传递的参数为空,还可以通过defaultValue设置一个默认值。示例代码如下:

1-1-14、@Controller

@Controller@Component注解的一个延伸,Spring会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。下面是使用此注解的示例代码:

1-1-15、@RestController

@RestController是在Spring 4.0开始引入的,这是一个特定的控制器注解。此注解相当于@Controller@ResponseBody的快捷方式。当使用此注解时,不需要再在方法上使用@ResponseBody注解。下面是使用此注解的示例代码:

1-1-16、@ModelAttribute

​ 通过此注解,可以通过模型索引名称来访问已经存在于控制器中的model。下面是使用此注解的一个简单示例:

@PathVariable@RequestParam注解一样,如果参数名与模型具有相同的名字,则不必指定索引名称,简写示例如下:

特别地,如果使用@ModelAttribute对方法进行标注,Spring会将方法的返回值绑定到具体的Model上。示例如下:

在Spring调用具体的处理方法之前,被@ModelAttribute注解标注的所有方法都将被执行。

1-1-17、@CrossOrigin

@CrossOrigin注解将为请求处理类或请求处理方法提供跨域调用支持。如果我们将此注解标注类,那么类中的所有方法都将获得支持跨域的能力。使用此注解的好处是可以微调跨域行为。使用此注解的示例如下:

1-1-18、@InitBinder

@InitBinder注解用于标注初始化WebDataBinider的方法,该方法用于对Http请求传递的表单数据进行处理,如时间格式化、字符串处理等。下面是使用此注解的示例:

1-2、Spring Bean 注解

​ 在本小节中,主要列举与Spring Bean相关的4个注解以及它们的使用方式。

1-2-1、@ComponentScan

@ComponentScan注解用于配置Spring需要扫描的被组件注解注释的类所在的包。可以通过配置其basePackages属性或者value属性来配置需要扫描的包路径。value属性是basePackages的别名。此注解的用法如下:

1-2-2、@Component

@Component注解用于标注一个普通的组件类,它没有明确的业务范围,只是通知Spring被此注解的类需要被纳入到Spring Bean容器中并进行管理。此注解的使用示例如下:

1-2-3、@Service

@Service注解是@Component的一个延伸(特例),它用于标注业务逻辑类。与@Component注解一样,被此注解标注的类,会自动被Spring所管理。下面是使用@Service注解的示例:

1-2-4、@Repository

@Repository注解也是@Component注解的延伸,与@Component注解一样,被此注解标注的类会被Spring自动管理起来,@Repository注解用于标注DAO层的数据持久化类。此注解的用法如下:

2、Spring Dependency Inject 与 Bean Scops注解

2-1、Spring DI注解

2-1-1、@DependsOn

@DependsOn注解可以配置Spring IoC容器在初始化一个Bean之前,先初始化其他的Bean对象。下面是此注解使用示例代码:

2-1-2、@Bean

@Bean注解主要的作用是告知Spring,被此注解所标注的类将需要纳入到Bean管理工厂中。@Bean注解的用法很简单,在这里,着重介绍@Bean注解中initMethod和destroyMethod的用法。示例如下:

2-2、Scops注解

2-2-1、@Scope

@Scope注解可以用来定义@Component标注的类的作用范围以及@Bean所标记的类的作用范围。@Scope所限定的作用范围有:singleton、prototype、request、session、globalSession或者其他的自定义范围。这里以prototype为例子进行讲解。当一个Spring Bean被声明为prototype(原型模式)时,在每次需要使用到该类的时候,Spring IoC容器都会初始化一个新的改类的实例。在定义一个Bean时,可以设置Bean的scope属性为prototype:scope=“prototype”,也可以使用@Scope注解设置,如下:

@Scope(value=ConfigurableBeanFactory.SCOPE_PROPTOTYPE)

下面将给出两种不同的方式来使用@Scope注解,示例代码如下:

2-2-2、@Scope 单例模式

​ 当@Scope的作用范围设置成Singleton时,被此注解所标注的类只会被Spring IoC容器初始化一次。在默认情况下,Spring IoC容器所初始化的类实例都为singleton。同样的原理,此情形也有两种配置方式,示例代码如下:

3、容器配置注解

3-1、@Autowired

@Autowired注解用于标记Spring将要解析和注入的依赖项。此注解可以作用在构造函数、字段和setter方法上。

3-1-1、作用于构造函数

​ 下面是@Autowired注解标注构造函数的使用示例:

3-1-2、作用于setter方法

​ 下面是@Autowired注解标注setter方法的示例代码:

3-1-3、作用于字段

@Autowired注解标注字段是最简单的,只需要在对应的字段上加入此注解即可,示例代码如下:

3-2、@Primary

​ 当系统中需要配置多个具有相同类型的bean时,@Primary可以定义这些Bean的优先级。下面将给出一个实例代码来说明这一特性:

输出结果:

this is send DingDing method message.

3-3、@PostConstruct@PreDestroy

​ 值得注意的是,这两个注解不属于Spring,它们是源于JSR-250中的两个注解,位于common-annotations.jar中。@PostConstruct注解用于标注在Bean被Spring初始化之前需要执行的方法。@PreDestroy注解用于标注Bean被销毁前需要执行的方法。下面是具体的示例代码:

3-4、@Qualifier

​ 当系统中存在同一类型的多个Bean时,@Autowired在进行依赖注入的时候就不知道该选择哪一个实现类进行注入。此时,我们可以使用@Qualifier注解来微调,帮助@Autowired选择正确的依赖项。下面是一个关于此注解的代码示例:

4、Spring Boot注解

4-1、@SpringBootApplication

@SpringBootApplication注解是一个快捷的配置注解,在被它标注的类中,可以定义一个或多个Bean,并自动触发自动配置Bean和自动扫描组件。此注解相当于@Configuration@EnableAutoConfiguration@ComponentScan的组合。在Spring Boot应用程序的主类中,就使用了此注解。示例代码如下:

@SpringBootApplication
public class Application{

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

}

4-2、@EnableAutoConfiguration

@EnableAutoConfiguration注解用于通知Spring,根据当前类路径下引入的依赖包,自动配置与这些依赖包相关的配置项。

4-3、@ConditionalOnClass@ConditionalOnMissingClass

​ 这两个注解属于类条件注解,它们根据是否存在某个类作为判断依据来决定是否要执行某些配置。下面是一个简单的示例代码:

@Configuration
@ConditionalOnClass(DataSource.class)
class MySQLAutoConfiguration {
    //...
}

4-4、@ConditionalOnBean@ConditionalOnMissingBean

​ 这两个注解属于对象条件注解,根据是否存在某个对象作为依据来决定是否要执行某些配置方法。示例代码如下:

@Bean
@ConditionalOnBean(name="dataSource")
LocalContainerEntityManagerFactoryBean entityManagerFactory(){
    //...
}

@Bean
@ConditionalOnMissingBean
public MyBean myBean(){
    //...
}

4-5、@ConditionalOnProperty

@ConditionalOnProperty注解会根据Spring配置文件中的配置项是否满足配置要求,从而决定是否要执行被其标注的方法。示例代码如下:

@Bean
@ConditionalOnProperty(name="alipay",havingValue="on")
Alipay alipay(){
    return new Alipay();
}

4-6、@ConditionalOnResource

​ 此注解用于检测当某个配置文件存在使,则触发被其标注的方法,下面是使用此注解的代码示例:

@ConditionalOnResource(resources = "classpath:website.properties")
Properties addWebsiteProperties(){
    //...
}

4-7、@ConditionalOnWebApplication@ConditionalOnNotWebApplication

​ 这两个注解用于判断当前的应用程序是否是Web应用程序。如果当前应用是Web应用程序,则使用Spring WebApplicationContext,并定义其会话的生命周期。下面是一个简单的示例:

@ConditionalOnWebApplication
HealthCheckController healthCheckController(){
    //...
}

4-8、@ConditionalExpression

​ 此注解可以让我们控制更细粒度的基于表达式的配置条件限制。当表达式满足某个条件或者表达式为真的时候,将会执行被此注解标注的方法。

@Bean
@ConditionalException("${localstore} && ${local == 'true'}")
LocalFileStore store(){
    //...
}

4-9、@Conditional

@Conditional注解可以控制更为复杂的配置条件。在Spring内置的条件控制注解不满足应用需求的时候,可以使用此注解定义自定义的控制条件,以达到自定义的要求。下面是使用该注解的简单示例:

@Conditioanl(CustomConditioanl.class)
CustomProperties addCustomProperties(){
    //...
}

总结

本次课程总结了Spring Boot中常见的各类型注解的使用方式,让大家能够统一的对Spring Boot常用注解有一个全面的了解。由于篇幅的原因,关于Spring Boot不常用的一些注解,将在下一次分享中进行补充和说明。

架构师必备技能指南:SaaS(软件即服务)架构设计

架构师必备技能指南:SaaS(软件即服务)架构设计

架构师必备技能指南:SaaS(软件即服务)架构设计

SaaS(软件即服务)平台架构设计指南

1、介绍

从计算机诞生开始,就伴随着计算机应用程序的演变。简短的回顾历史,我们可以清楚的看到应用程序发生的巨大变化。上世纪70年代中期,随着个人PC机的爆炸式增长以及程序员的崛起,让计算机的计算能力得到了大跨越的提升,个人PC机上可以运行非常复杂的应用程序。

进入上世纪80年代,随着Bulletin Board System(简称:BBS)电子公告板系统的兴起,它可以为广大PC机用户提供基本的在线服务,如在线聊天、电子邮件、消息发送和文件下载。由于受到那个时代计算机网络传输速度的限制,在线服务的响应速度慢,交互体验差是最大的通病。

进入90年代中后期,随着万维网的出现,计算机的计算能开始进入快速提升阶段,加之网络基础设施的持续完善,计算机网络技术也随之发展起来,这让Web网站可以提供功能多元化和更为复杂的在线服务,直到今天,我们所看到的互联网(或云)开发的在线服务应用程序。

在这段计算机技术快速成长的时间里,计算机软件到底发生了哪些变化?从历史的发展中,我们可以看到,应用程序本身没有发生本质的变化(程序=数据结构+算法),变化的是软件的供需方式发生了改变。现在,应用程序消费者不需要再在他们的PC机上下载和安装特定的应用程序,即可获得软件所提供的计算服务。在云计算技术的支持下,消费者(企业或个人)只需要使用Web工具(浏览器)访问并登录软件提供商的Web系统,通过简单的配置,就可以获得自己所需应用程序服务。这种通过网络即可使用软件的服务,即使SaaS(软件即服务)。

图 1-1 2015中国SaaS生态

在本篇文章中,我们将着重介绍SaaS架构设计,并围绕WHAT(是什么?)、WHY(为什么?)、WHERE(在哪里?)和HOW(怎么样?)这四个问题,对以下的几点进行阐述:

图 1-2 文章结构
  • 1、什么是SaaS平台?
  • 2、为什么需要使用SaaS平台架构?
  • 3、SaaS平台主要的特性和优势有哪些?
  • 4、SaaS平台适合在什么领域进行实施?
  • 5、SaaS平台有哪些先天性的缺陷?
  • 6、SaaS平台有哪些核心的组件?
  • 7、实施SaaS架构设计时的注意事项有哪些?

2、什么是SaaS平台?

图 2-1 SaaS组成结构

在你决定实施SaaS品台架构设计前,你有必要先了解SaaS平台是什么。从宏观的角度来看,SaaS是一种软件应用程序交付方式,软件提供商集中化托管一个或多个软件应用程序,并通过互联网向租户体用这些软件应用程序。从分类上看,SaaS(软件即服务)也是云计算重要的一部分。目前国内主流的云服务提供商如阿里云、百度云、腾讯云等,为广大用户提供了不同业务需求的云服务,它们大致可以分为以下几类:

  • 1、基础设施即服务:如CPU、Network、Disk和Memory等
  • 2、平台即服务:如阿里云服务器和云数据库等
  • 3、软件即服务:阿里短信、阿里邮箱等
  • 4、数据即服务:如阿里云对象存储,七牛云存储等
  • 5、其他软件服务:机器学习、人工智能等

SaaS应用程序的任何更新或者修复漏洞操作都是由软件提供商负责实施和处理的,由于租户是通过互联网获取软件服务,所以租户端无需下载任何的升级包或者修复补丁,是一种开箱即获取最新软件产品的服务方式。

通过对什么是SaaS的介绍,接下来,我们了解一下选择SaaS作为软件架构来设计产品的一些理由。

3、为什么选择SaaS?

我们将从不同的角度来阐述几个为什么选择SaaS的理由。透过对这些因素的分析,为你是否需要将自己的软件SaaS化提供一定的参考依据。

3.1、消费者角度

获取软件服务的方式足够简单,SaaS也许是迄今为止使用软件最简单的方式之一,租户只需要动动鼠标和键盘,即可在几小时甚至几分钟内获得一个大型的软件服务。相比于传统使用软件的方式,租户省去了研发、部署、运维等一系列繁复的过程,且获得软件的时间和费用成本都大幅度降低。

3.2、商业角度

SaaS可以体用跨地域、跨平台的软件服务。与此同时,软件服务商可以统一对软件进行版本管理,这将带来以下几点好处(包括但不限于):

  • 1、缩短产品上线时间:多端适配,统一版本,统一更新
  • 2、降低维护成本:不需要同时维护多个版本的软件实例,运维压力减小
  • 3、容易升级:由于版本得到有效控制,一次升级,即可覆盖所有租户端

4、SaaS的特性和优势

我们将SaaS应用程序与传统的桌面应用程序做一个水平的对比,部署一个SaaS产品将可以获得以下的几点优势。

4.1、简单

SaaS化的产品通过互联网向租户提供软件服务,随着Web技术(如jQuery、Node.js)的进步,Web页面的交互体验度大幅度提升,交互更流畅、更人性化。与传统的桌面应用程序的人机交互效果相差无几。

4.2、经济实惠

SaaS化产品可以为租户提供弹性的付费方案,如按日、按月、按年、按使用人数或者按使用量进行计费,它将给租户提供更经济的使用软件的财务预算表。

4.3、安全

使用SaaS产品无需担心数据安全问题,这好比将钱存入银行一样安全。相较于企业内部部署的软件系统而言,SaaS产品具备更高的安全保障能力,因为软件提供商具有更多软件安全防护的技术资源、人力资源和财政资源。

4.4、兼容性

与传统软件相比、SaaS软件的兼容性更好,它没有传统软件的多本版维护问题和操作系统兼容问题。在SaaS软件中,租户用户在使用软件的过程中,几乎上感觉不到软件发生了改变。当租户用户登录到系统上时,就已经获得了最新版本的软件。

5、SaaS软件的适用范围

SaaS产品具有广泛的适应范围,特别是与其他云产品(如IaaS(基础设施即服务)和PaaS(平台即服务))配合使用时这种能力表现尤为突出,例如阿里云之类的云计算技术允许你配置可托管的Web站点、数据库服务器等。你只需要打开浏览器并登录到阿里云控制台,通过操作对应的控制面板,即可获得相关的软件服务。

从理论上讲,SaaS可以将任何的软件SaaS,下面列举一些通用的分类供大家参考:

  • 1、Office在线办公类SaaS产品
  • 2、电子邮件和即时消息类SaaS产品
  • 3、社交媒体类SaaS产品
  • 4、第三方API类SaaS产品
  • 5、安全和访问控制类SaaS产品
  • 6、机器学习类SaaS产品
  • 7、人工智能类SaaS产品
  • 8、地理位置服务类SaaS产品
  • 9、数据流和数据检索类SaaS产品

6、SaaS产品的天生缺陷

图 6-1 SaaS产品的缺点

从上图我们可以直观的看到,SaaS产品与生俱来的几个缺陷,接下来我们将逐一进行描述。

6.1、软件控制权

与企业内部部署的软件不同,由于SaaS软件被击中托管在服务提供商的Web服务器中,所以租户无法控制所有的软件应用程序,SaaS化的软件比企业自行部署的软件获得的控制权更少,租户可操作的自定义控制权极度有限。

6.2、消费者基数小

由于SaaS软件是将一套应用程序共享给一个或者多个租户共同使用,这种共享的消费方式还未被大多数的消费者所接受。同时,受制于市场环境的影响,目前还有大多数的软件还未SaaS化。

6.3、性能瓶颈

共享应用程序必然会带来服务器性能的下降、如计算速度、网络资源、I/O读写等都将面临严峻的考验。在性能方面,企业内部部署的“独享模式”的应用程序比SaaS软件的“共享模式”略胜一筹。

6.4、安全问题

当租户在选择一款SaaS产品时,产品的安全性将会被放置在第一位进行考虑。如数据的隔离、敏感数据的加密、数据访问权限控制、个人隐私等问题。在2018年5月25日,GDPR(General Data Protection Regulation)《通用数据保护条例》出现之后,越来越多的人开始重视数据安全问题。如何最大程度的打消租户的这一顾虑,需要服务提供商加强对自身信誉度的提升,以赢得租户的信赖。

7、SaaS产品的核心组件

不同类型的SaaS产品,由于要面对不同的用户愿景,可能在功能和业务上会有所不同,但任何一个SaaS产品,都具备以下几个共同的核心组件。

图 7-1 SaaS 核心组件

7.1、安全组件

在SaaS产品中,系统安全永远是第一位需要考虑的事情,如何保障租户数据的安全,是你首要的事情。这如同银行首选需要保障储户资金安全一样。安全组件就是统一的对SaaS产品进行安全防护,保障系统数据安全。

7.2、数据隔离组件

安全组件解决了用户数据安全可靠的问题,但数据往往还需要解决隐私问题,各企业之间的数据必须相互不可见,即相互隔离。在SaaS产品中,如何识别、区分、隔离个租户的数据时你在实施SaaS平台架构设计时需要考虑的第二个问题。

7.3、可配置组件

尽管SaaS产品在设计之初就考虑了大多数通用的功能,让租户开箱即用,但任然有为数不少的租户需要定制服务自身业务需求的配置项,如UI布局、主题、标识(Logo)等信息。正因为无法抽象出一个完全通用的应用程序,所以在SaaS产品中,你需要提供一个可用于自定义配置的组件。

7.4、可扩展组件

随着SaaS产品业务和租户数量的增长,原有的服务器配置将无法继续满足新的需求,系统性能将会与业务量和用户量成反比。此时,SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术,在多个服务器上部署多个软件运行示例并提供相同的软件服务,以此实现水平扩展SaaS产品的整体服务性能。为了实现可扩展能力,就需要SaaS展示层的代码与业务逻辑部分的代码进行分离,两者独立部署。例如使用VUE+微服务构建前后端分离且可水平进行扩展的分布式SaaS应用产品。对于可扩展,还有另外一种方式,即垂直扩展,其做法比较简单,也比较粗暴:通过增加单台服务器的配置,如购买性能更好的CUP、存储更大的内存条、增大带宽等措施,让服务器能够处理更多的用户请求。但此做法对于提升产品性能没有质的改变,且成本很高。

7.5、0停机时间升级产品

以往的软件在升级或者修复Bug是,都需要将运行的程序脱机一段时间,等待升级或修复工作完成后,再重新启动应用程序。而SaaS产品则需要全天候保障服务的可用性。这就需要你考虑如何实现在不重启原有应用程序的情况下,完成应用程序的升级修复工作。

7.6、多租户组件

要将原有产品SaaS化,就必须提供多租户组件,多租户组件是衡量一个应用程序是否具备SaaS服务能力的重要指标之一。SaaS产品需要同时容纳多个租户的数据,同时还需要保证各租户之间的数据不会相互干扰,保证租户中的用户能够按期望索引到正确的数据,多租户组件是你必须要解决的一个问题。其余的组件都将围绕此组件展开各自的业务。

总结

本文将软件应用程序的发展历程作为切入点,并围绕WHAT(是什么?)、WHY(为什么?)、WHERE(在哪些领域实施?)和HOW(怎么样?)这四个问题对SaaS展开了介绍。文中详细的阐述了基于SaaS架构的软件设计需要注意的问题,并分析了SaaS产品的特性、有点、缺点。最后还介绍了基于SaaS架构的软件产品应该具备的几个核心组件以及他们各自的作用。希望本次能够让你对SaaS平台架构有一个全面的了解,并且在你准备实施SaaS平台架构设计前能够提供一些价值的参考信息。

Spring Boot 构建多租户SaaS平台核心技术指南

Spring Boot 构建多租户SaaS平台核心技术指南

本次教程所涉及到的源码已上传至Github,如果你不需要继续阅读下面的内容,你可以直接点击此链接获取源码内容。https://github.com/ramostear/una-saas-toturial

1. 概述

笔者从2014年开始接触SaaS(Software as a Service),即多租户(或多承租)软件应用平台;并一直从事相关领域的架构设计及研发工作。机缘巧合,在笔者本科毕业设计时完成了一个基于SaaS的高效财务管理平台的课题研究,从中收获颇多。最早接触SaaS时,国内相关资源匮乏,唯一有的参照资料是《互联网时代的软件革命:SaaS架构设计》(叶伟等著)一书。最后课题的实现是基于OSGI(Open Service Gateway Initiative)Java动态模块化系统规范来实现的。

时至今日,五年的时间过去了,软件开发的技术发生了巨大的改变,笔者所实现SaaS平台的技术栈也更新了好几波,真是印证了那就话:“山重水尽疑无路,柳暗花明又一村”。基于之前走过的许多弯路和踩过的坑,以及近段时间有许多网友问我如何使用Spring Boot实现多租户系统,决定写一篇文章聊一聊关于SaaS的硬核技术。

说起SaaS,它只是一种软件架构,并没有多少神秘的东西,也不是什么很难的系统,我个人的感觉,SaaS平台的难度在于商业上的运营,而非技术上的实现。就技术上来说,SaaS是这样一种架构模式:它让多个不同环境的用户使用同一套应用程序,且保证用户之间的数据相互隔离。现在想想看,这也有点共享经济的味道在里面。

笔者在这里就不再深入聊SaaS软件成熟度模型和数据隔离方案对比的事情了。今天要聊的是使用Spring Boot快速构建独立数据库/共享数据库独立Schema的多租户系统。我将提供一个SaaS系统最核心的技术实现,而其他的部分有兴趣的朋友可以在此基础上自行扩展。

2. 尝试了解多租户的应用场景

假设我们需要开发一个应用程序,并且希望将同一个应用程序销售给N家客户使用。在常规情况下,我们需要为此创建N个Web服务器(Tomcat),N个数据库(DB),并为N个客户部署相同的应用程序N次。现在,如果我们的应用程序进行了升级或者做了其他任何的改动,那么我们就需要更新N个应用程序同时还需要维护N台服务器。接下来,如果业务开始增长,客户由原来的N个变成了现在的N+M个,我们将面临N个应用程序和M个应用程序版本维护,设备维护以及成本控制的问题。运维几乎要哭死在机房了…

为了解决上述的问题,我们可以开发多租户应用程序,我们可以根据当前用户是谁,从而选择对应的数据库。例如,当请求来自A公司的用户时,应用程序就连接A公司的数据库,当请求来自B公司的用户时,自动将数据库切换到B公司数据库,以此类推。从理论上将没有什么问题,但我们如果考虑将现有的应用程序改造成SaaS模式,我们将遇到第一个问题:如果识别请求来自哪一个租户?如何自动切换数据源?

3. 维护、识别和路由租户数据源

我们可以提供一个独立的库来存放租户信息,如数据库名称、链接地址、用户名、密码等,这可以统一的解决租户信息维护的问题。租户的识别和路由有很多种方法可以解决,下面列举几个常用的方式:

  • 1.可以通过域名的方式来识别租户:我们可以为每一个租户提供一个唯一的二级域名,通过二级域名就可以达到识别租户的能力,如tenantone.example.com,tenant.example.com;tenantone和tenant就是我们识别租户的关键信息。
  • 2.可以将租户信息作为请求参数传递给服务端,为服务端识别租户提供支持,如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的参数tenantId就是应用程序识别租户的关键信息。
  • 3.可以在请求头(Header)中设置租户信息,例如JWT等技术,服务端通过解析Header中相关参数以获得租户信息。
  • 4.在用户成功登录系统后,将租户信息保存在Session中,在需要的时候从Session取出租户信息。

解决了上述问题后,我们再来看看如何获取客户端传入的租户信息,以及在我们的业务代码中如何使用租户信息(最关键的是DataSources的问题)。

我们都知道,在启动Spring Boot应用程序之前,就需要为其提供有关数据源的配置信息(有使用到数据库的情况下),按照一开始的需求,有N个客户需要使用我们的应用程序,我们就需要提前配置好N个数据源(多数据源),如果N<50,我认为我还能忍受,如果更多,这样显然是无法接受的。为了解决这一问题,我们需要借助Hibernate 5提供的动态数据源特性,让我们的应用程序具备动态配置客户端数据源的能力。简单来说,当用户请求系统资源时,我们将用户提供的租户信息(tenantId)存放在ThreadLoacal中,紧接着获取TheadLocal中的租户信息,并根据此信息查询单独的租户库,获取当前租户的数据配置信息,然后借助Hibernate动态配置数据源的能力,为当前请求设置数据源,最后之前用户的请求。这样我们就只需要在应用程序中维护一份数据源配置信息(租户数据库配置库),其余的数据源动态查询配置。接下来,我们将快速的演示这一功能。

4. 项目构建

我们将使用Spring Boot 2.1.5版本来实现这一演示项目,首先你需要在Maven配置文件中加入如下的一些配置:

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>

然后提供一个可用的配置文件,并加入如下的内容:

spring:
  freemarker:
    cache: false
    template-loader-path:
    - classpath:/templates/
    prefix:
    suffix: .html
  resources:
    static-locations:
    - classpath:/static/
  devtools:
    restart:
      enabled: true
  jpa:
    database: mysql
    show-sql: true
    generate-ddl: false
    hibernate:
      ddl-auto: none
una:
  master:
    datasource:
      url:  jdbc:mysql://localhost:3306/master_tenant?useSSL=false
      username: root
      password: root
      driverClassName:  com.mysql.jdbc.Driver
      maxPoolSize:  10
      idleTimeout:  300000
      minIdle:  10
      poolName: master-database-connection-pool
logging:
  level:
    root: warn
    org:
      springframework:
        web:  debug
      hibernate: debug

由于采用Freemarker作为视图渲染引擎,所以需要提供Freemarker的相关技术

una:master:datasource配置项就是上面说的统一存放租户信息的数据源配置信息,你可以理解为主库。

接下来,我们需要关闭Spring Boot自动配置数据源的功能,在项目主类上添加如下的设置:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class UnaSaasApplication {

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

}

最后,让我们看看整个项目的结构:

5. 实现租户数据源查询模块

我们将定义一个实体类存放租户数据源信息,它包含了租户名,数据库连接地址,用户名和密码等信息,其代码如下:

@Data
@Entity
@Table(name = "MASTER_TENANT")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MasterTenant implements Serializable{

    @Id
    @Column(name="ID")
    private String id;

    @Column(name = "TENANT")
    @NotEmpty(message = "Tenant identifier must be provided")
    private String tenant;

    @Column(name = "URL")
    @Size(max = 256)
    @NotEmpty(message = "Tenant jdbc url must be provided")
    private String url;

    @Column(name = "USERNAME")
    @Size(min = 4,max = 30,message = "db username length must between 4 and 30")
    @NotEmpty(message = "Tenant db username must be provided")
    private String username;

    @Column(name = "PASSWORD")
    @Size(min = 4,max = 30)
    @NotEmpty(message = "Tenant db password must be provided")
    private String password;

    @Version
    private int version = 0;
}

持久层我们将继承JpaRepository接口,快速实现对数据源的CURD操作,同时提供了一个通过租户名查找租户数据源的接口,其代码如下:

package com.ramostear.una.saas.master.repository;

import com.ramostear.una.saas.master.model.MasterTenant;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:22
 * @modify by :
 * @since:
 */
@Repository
public interface MasterTenantRepository extends JpaRepository<MasterTenant,String>{

    @Query("select p from MasterTenant p where p.tenant = :tenant")
    MasterTenant findByTenant(@Param("tenant") String tenant);
}

业务层提供通过租户名获取租户数据源信息的服务(其余的服务各位可自行添加):

package com.ramostear.una.saas.master.service;

import com.ramostear.una.saas.master.model.MasterTenant;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:26
 * @modify by :
 * @since:
 */

public interface MasterTenantService {
    /**
     * Using custom tenant name query
     * @param tenant    tenant name
     * @return          masterTenant
     */
    MasterTenant findByTenant(String tenant);
}

最后,我们需要关注的重点是配置主数据源(Spring Boot需要为其提供一个默认的数据源)。在配置之前,我们需要获取配置项,可以通过@ConfigurationProperties(“una.master.datasource”)获取配置文件中的相关配置信息:

@Getter
@Setter
@Configuration
@ConfigurationProperties("una.master.datasource")
public class MasterDatabaseProperties {

    private String url;

    private String password;

    private String username;

    private String driverClassName;

    private long connectionTimeout;

    private int maxPoolSize;

    private long idleTimeout;

    private int minIdle;

    private String poolName;

    @Override
    public String toString(){
        StringBuilder builder = new StringBuilder();
        builder.append("MasterDatabaseProperties [ url=")
                .append(url)
                .append(", username=")
                .append(username)
                .append(", password=")
                .append(password)
                .append(", driverClassName=")
                .append(driverClassName)
                .append(", connectionTimeout=")
                .append(connectionTimeout)
                .append(", maxPoolSize=")
                .append(maxPoolSize)
                .append(", idleTimeout=")
                .append(idleTimeout)
                .append(", minIdle=")
                .append(minIdle)
                .append(", poolName=")
                .append(poolName)
                .append("]");
        return builder.toString();
    }
}

接下来是配置自定义的数据源,其源码如下:

package com.ramostear.una.saas.master.config;

import com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties;
import com.ramostear.una.saas.master.model.MasterTenant;
import com.ramostear.una.saas.master.repository.MasterTenantRepository;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.cfg.Environment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:31
 * @modify by :
 * @since:
 */
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.ramostear.una.saas.master.model","com.ramostear.una.saas.master.repository"},
                       entityManagerFactoryRef = "masterEntityManagerFactory",
                       transactionManagerRef = "masterTransactionManager")
@Slf4j
public class MasterDatabaseConfig {

    @Autowired
    private MasterDatabaseProperties masterDatabaseProperties;

    @Bean(name = "masterDatasource")
    public DataSource masterDatasource(){
        log.info("Setting up masterDatasource with :{}",masterDatabaseProperties.toString());
        HikariDataSource datasource = new HikariDataSource();
        datasource.setUsername(masterDatabaseProperties.getUsername());
        datasource.setPassword(masterDatabaseProperties.getPassword());
        datasource.setJdbcUrl(masterDatabaseProperties.getUrl());
        datasource.setDriverClassName(masterDatabaseProperties.getDriverClassName());
        datasource.setPoolName(masterDatabaseProperties.getPoolName());
        datasource.setMaximumPoolSize(masterDatabaseProperties.getMaxPoolSize());
        datasource.setMinimumIdle(masterDatabaseProperties.getMinIdle());
        datasource.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout());
        datasource.setIdleTimeout(masterDatabaseProperties.getIdleTimeout());
        log.info("Setup of masterDatasource successfully.");
        return datasource;
    }

    @Primary
    @Bean(name = "masterEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){
        LocalContainerEntityManagerFactoryBean lb = new LocalContainerEntityManagerFactoryBean();
        lb.setDataSource(masterDatasource());
        lb.setPackagesToScan(
           new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()}
        );

        //Setting a name for the persistence unit as Spring sets it as 'default' if not defined.
        lb.setPersistenceUnitName("master-database-persistence-unit");

        //Setting Hibernate as the JPA provider.
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        lb.setJpaVendorAdapter(vendorAdapter);

        //Setting the hibernate properties
        lb.setJpaProperties(hibernateProperties());

        log.info("Setup of masterEntityManagerFactory successfully.");
        return lb;
    }

    @Bean(name = "masterTransactionManager")
    public JpaTransactionManager masterTransactionManager(@Qualifier("masterEntityManagerFactory")EntityManagerFactory emf){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);
        log.info("Setup of masterTransactionManager successfully.");
        return transactionManager;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){
        return new PersistenceExceptionTranslationPostProcessor();
    }

    private Properties hibernateProperties(){
        Properties properties = new Properties();
        properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
        properties.put(Environment.SHOW_SQL,true);
        properties.put(Environment.FORMAT_SQL,true);
        properties.put(Environment.HBM2DDL_AUTO,"update");
        return properties;
    }
}

在改配置类中,我们主要提供包扫描路径,实体管理工程,事务管理器和数据源配置参数的配置。

6. 实现租户业务模块

在此小节中,租户业务模块我们仅提供一个用户登录的场景来演示SaaS的功能。其实体层、业务层和持久化层根普通的Spring Boot Web项目没有什么区别,你甚至感觉不到它是一个SaaS应用程序的代码。

首先,创建一个用户实体User,其源码如下:

@Entity
@Table(name = "USER")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User implements Serializable {
    private static final long serialVersionUID = -156890917814957041L;

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "PASSWORD")
    @Size(min = 6,max = 22,message = "User password must be provided and length between 6 and 22.")
    private String password;

    @Column(name = "TENANT")
    private String tenant;
}

业务层提供了一个根据用户名检索用户信息的服务,它将调用持久层的方法根据用户名对租户的用户表进行检索,如果找到满足条件的用户记录,则返回用户信息,如果没有找到,则返回null;持久层和业务层的源码分别如下:

@Repository
public interface UserRepository extends JpaRepository<User,String>,JpaSpecificationExecutor<User>{

    User findByUsername(String username);
}
@Service("userService")
public class UserServiceImpl implements UserService{

    @Autowired
    private UserRepository userRepository;

    private static TwitterIdentifier identifier = new TwitterIdentifier();



    @Override
    public void save(User user) {
        user.setId(identifier.generalIdentifier());
        user.setTenant(TenantContextHolder.getTenant());
        userRepository.save(user);
    }

    @Override
    public User findById(String userId) {
        Optional<User> optional = userRepository.findById(userId);
        if(optional.isPresent()){
            return optional.get();
        }else{
            return null;
        }
    }

    @Override
    public User findByUsername(String username) {
        System.out.println(TenantContextHolder.getTenant());
        return userRepository.findByUsername(username);
    }

在这里,我们采用了Twitter的雪花算法来实现了一个ID生成器。

7. 配置拦截器

我们需要提供一个租户信息的拦截器,用以获取租户标识符,其源代码和配置拦截器的源代码如下:

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/26 0026-23:17
 * @modify by :
 * @since:
 */
@Slf4j
public class TenantInterceptor implements HandlerInterceptor{

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tenant = request.getParameter("tenant");
        if(StringUtils.isBlank(tenant)){
            response.sendRedirect("/login.html");
            return false;
        }else{
            TenantContextHolder.setTenant(tenant);
            return true;
        }
    }
}
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TenantInterceptor()).addPathPatterns("/**").excludePathPatterns("/login.html");
        super.addInterceptors(registry);
    }
}

/login.html是系统的登录路径,我们需要将其排除在拦截器拦截的范围之外,否则我们永远无法进行登录

8. 维护租户标识信息

在这里,我们使用ThreadLocal来存放租户标识信息,为动态设置数据源提供数据支持,该类提供了设置租户标识、获取租户标识以及清除租户标识三个静态方法。其源码如下:

public class TenantContextHolder {

    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void setTenant(String tenant){
        CONTEXT.set(tenant);
    }

    public static String getTenant(){
        return CONTEXT.get();
    }

    public static void clear(){
        CONTEXT.remove();
    }
}

此类时实现动态数据源设置的关键

9. 动态数据源切换

要实现动态数据源切换,我们需要借助两个类来完成,CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。从它们的命名上就可以看出,一个负责解析租户标识,一个负责提供租户标识对应的租户数据源信息。

首先,我们需要实现CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法,完成租户标识的解析功能。实现类的源码如下:

package com.ramostear.una.saas.tenant.config;

import com.ramostear.una.saas.context.TenantContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/26 0026-22:38
 * @modify by :
 * @since:
 */
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    /**
     * 默认的租户ID
     */
    private static final String DEFAULT_TENANT = "tenant_1";

    /**
     * 解析当前租户的ID
     * @return
     */
    @Override
    public String resolveCurrentTenantIdentifier() {
        //通过租户上下文获取租户ID,此ID是用户登录时在header中进行设置的
        String tenant = TenantContextHolder.getTenant();
        //如果上下文中没有找到该租户ID,则使用默认的租户ID,或者直接报异常信息
        return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

此类的逻辑非常简单,就是从ThreadLocal中获取当前设置的租户标识符

有了租户标识符解析类之后,我们需要扩展租户数据源提供类,实现从数据库动态查询租户数据源信息,其源码如下:

@Slf4j
@Configuration
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{

    private static final long serialVersionUID = -7522287771874314380L;

    @Autowired
    private MasterTenantRepository masterTenantRepository;

    private Map<String,DataSource> dataSources = new TreeMap<>();

    @Override
    protected DataSource selectAnyDataSource() {
        if(dataSources.isEmpty()){
            List<MasterTenant> tenants = masterTenantRepository.findAll();
            tenants.forEach(masterTenant->{
                dataSources.put(masterTenant.getTenant(), DataSourceUtils.wrapperDataSource(masterTenant));
            });
        }
        return dataSources.values().iterator().next();
    }

    @Override
    protected DataSource selectDataSource(String tenant) {
        if(!dataSources.containsKey(tenant)){
            List<MasterTenant> tenants = masterTenantRepository.findAll();
            tenants.forEach(masterTenant->{
                dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant));
            });
        }
        return dataSources.get(tenant);
    }
}

在该类中,通过查询租户数据源库,动态获得租户数据源信息,为租户业务模块的数据源配置提供数据数据支持。

最后,我们还需要提供租户业务模块数据源配置,这是整个项目核心的地方,其代码如下:

@Slf4j
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {
        "com.ramostear.una.saas.tenant.model",
        "com.ramostear.una.saas.tenant.repository"
})
@EnableJpaRepositories(basePackages = {
        "com.ramostear.una.saas.tenant.repository",
        "com.ramostear.una.saas.tenant.service"
},entityManagerFactoryRef = "tenantEntityManagerFactory"
,transactionManagerRef = "tenantTransactionManager")
public class TenantDataSourceConfig {

    @Bean("jpaVendorAdapter")
    public JpaVendorAdapter jpaVendorAdapter(){
        return new HibernateJpaVendorAdapter();
    }

    @Bean(name = "tenantTransactionManager")
    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean(name = "datasourceBasedMultiTenantConnectionProvider")
    @ConditionalOnBean(name = "masterEntityManagerFactory")
    public MultiTenantConnectionProvider multiTenantConnectionProvider(){
        return new DataSourceBasedMultiTenantConnectionProviderImpl();
    }

    @Bean(name = "currentTenantIdentifierResolver")
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){
        return new CurrentTenantIdentifierResolverImpl();
    }

    @Bean(name = "tenantEntityManagerFactory")
    @ConditionalOnBean(name = "datasourceBasedMultiTenantConnectionProvider")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            @Qualifier("datasourceBasedMultiTenantConnectionProvider")MultiTenantConnectionProvider connectionProvider,
            @Qualifier("currentTenantIdentifierResolver")CurrentTenantIdentifierResolver tenantIdentifierResolver
    ){
        LocalContainerEntityManagerFactoryBean localBean = new LocalContainerEntityManagerFactoryBean();
        localBean.setPackagesToScan(
                new String[]{
                        User.class.getPackage().getName(),
                        UserRepository.class.getPackage().getName(),
                        UserService.class.getPackage().getName()

                }
        );
        localBean.setJpaVendorAdapter(jpaVendorAdapter());
        localBean.setPersistenceUnitName("tenant-database-persistence-unit");
        Map<String,Object> properties = new HashMap<>();
        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider);
        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver);
        properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
        properties.put(Environment.SHOW_SQL,true);
        properties.put(Environment.FORMAT_SQL,true);
        properties.put(Environment.HBM2DDL_AUTO,"update");
        localBean.setJpaPropertyMap(properties);
        return localBean;
    }
}

在改配置文件中,大部分内容与主数据源的配置相同,唯一的区别是租户标识解析器与租户数据源补给源的设置,它将告诉Hibernate在执行数据库操作命令前,应该设置什么样的数据库连接信息,以及用户名和密码等信息。

10. 应用测试

最后,我们通过一个简单的登录案例来测试本次课程中的SaaS应用程序,为此,需要提供一个Controller用于处理用户登录逻辑。在本案例中,没有严格的对用户密码进行加密,而是使用明文进行比对,也没有提供任何的权限认证框架,知识单纯的验证SaaS的基本特性是否具备。登录控制器代码如下:

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/27 0027-0:18
 * @modify by :
 * @since:
 */
@Controller
public class LoginController {

    @Autowired
    private UserService userService;

    @GetMapping("/login.html")
    public String login(){
        return "/login";
    }

    @PostMapping("/login")
    public String login(@RequestParam(name = "username") String username, @RequestParam(name = "password")String password, ModelMap model){
        System.out.println("tenant:"+TenantContextHolder.getTenant());
        User user = userService.findByUsername(username);
        if(user != null){
            if(user.getPassword().equals(password)){
                model.put("user",user);
                return "/index";
            }else{
                return "/login";
            }
        }else{
            return "/login";
        }
    }
}

在启动项目之前,我们需要为主数据源创建对应的数据库和数据表,用于存放租户数据源信息,同时还需要提供一个租户业务模块数据库和数据表,用来存放租户业务数据。一切准备就绪后,启动项目,在浏览器中输入:http://localhost:8080/login.html

在登录窗口中输入对应的租户名,用户名和密码,测试是否能够正常到达主页。可以多增加几个租户和用户,测试用户是否正常切换到对应的租户下。

总结

在这里,我分享了使用Spring Boot+JPA快速实现多租户应用程序的方法,此方法只涉及了实现SaaS应用平台的最核心技术手段,并不是一个完整可用的项目代码,如用户的认证、授权等并未出现在本文中。额外的业务模块感兴趣的朋友可以在此设计基础上自行扩展,如对其中的代码有任何的疑问,欢迎大家在下方给我留言。

(六讲)Spring Boot REST API异常处理指南

(六讲)Spring Boot REST API异常处理指南

能够正确的处理REST API程序抛出的异常以及返回友好的异常信息是一件非常重要的事情,因为它可以帮助API客户端正确的对服务端的问题作出正确的响应。这有助于提高REST API的服务质量。Spring Boot默认返回的异常信息对于API客户端来说是晦涩难懂的,只有开发者才会关注那些堆栈异常报告。在本讲中,将对如何处理好Spring REST API异常信息做一个梳理。

最近一段时间,Spring Boot成为了Java开发圈子的网红,越来越多的开发者选择Spring Boot来构建REST API。使用Spring Boot,能够帮助开发者减少模板代码和配置文件的编写工作量。Spring Boot开箱即用的特性,受到广大开发者的热宠。在本讲中,通过一个精简的Demo项目,着重介绍一些Spring Boot REST API的异常处理技巧。

如果你不想阅读本次内容,只是想快速获得相关的源码,那你可以直接跳转到文章的结尾,找到Gihub仓库链接,通过该链接,你可以轻松的获得本次内容的全部源码。

1. 定义明确的异常信息

当程序发送错误时,不应该将晦涩的堆栈报告信息返回给API客户端,从某种意义将,这是一种不礼貌的和不负责任的行为。现在,我们将模拟这样一个需求,API客户端可以向服务端发送请求以获取一个或者多个用户信息,同时还可以发送请求创建一个新的用户信息。下面是大致的一个API信息:

API 名称 说明
GET /users/{userId} 根据用户ID检索用户信息,如果没有找到,则返回用户未找到异常信息
GET /users 根据传入的ID集合,检索用户信息,若未找到,返回未找到用户异常信息
POST /users 创建一个新的用户

Spring MVC为我们提供了一些很有用的功能,以帮助我们解决系统的异常信息,并将有用的提示信息返回给API客户端。

以 POST /users 创建一个新用户为例,当我们提供正常的用户数据并请求此接口时,REST API将返回如下的提示信息:

{
  "id": 2,
  "username": "wukong",
  "age": 52,
  "height": 170
}

现在,将用户年龄修改为200岁,身高修改为500厘米,用户名为rulai ,在此请求此REST API,观察API的返回信息:

{
  "restapierror": {
    "status": "BAD_REQUEST",
    "timestamp": "2019-05-19 06:04:47",
    "message": "Validation error",
    "subErrors": [
      {
        "object": "user",
        "field": "height",
        "rejectedValue": 500,
        "message": "用户身高不能超过250厘米"
      },
      {
        "object": "user",
        "field": "age",
        "rejectedValue": 200,
        "message": "用户年龄不能超过120岁"
      }
    ]
  }
}

如上所示,当API客户端提供不正确的数据时,REST API返回了格式良好的异常提示信息,timestamp由原来的整形时间戳格式化为一个可读的日期+时间,同时还详细列举了详细的错误报告。

2. 包装异常信息

为了能够提供一个可读的JSON格式异常信息给API客户端,我们需要在项目中引入Jackson JSR 310的依赖包,使用其提供的@JsonFormat注解将Java中的日期和时间按照我们给定的日期和时间模板进行格式化。现在,将下面的依赖包加入的Maven pom.xml文件中:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.9.8</version>
</dependency>

依赖就绪后,我们需要提供一个异常信息的包装类:RestApiError。它将负责对REST API抛出的异常信息进行封装:

public class RestApiError {

    private HttpStatus status;

    @JsonFormat(shape = JsonFormat.Shape.STRING,pattern = "yyyy-MM-dd hh:mm:ss")
    private LocalDateTime timestamp;

    private String message;

    private String debugMessage;

    private List<RestApiSubError> subErrors;

    private RestApiError(){
        timestamp = LocalDateTime.now();
    }

    RestApiError(HttpStatus status){
        this();
        this.status = status;
    }

    RestApiError(HttpStatus status,Throwable ex){
        this();
        this.status = status;
        this.message = "Unexpected error";
        this.debugMessage = ex.getLocalizedMessage();
    }

    RestApiError(HttpStatus status,String message,Throwable ex){
        this();
        this.status = status;
        this.message = message;
        this.debugMessage = ex.getLocalizedMessage();
    }
}
  • status 属性用于记录响应状态。它沿用了HttpStatus的所有状态吗,如4xx和5xx。
  • timestamp属性用于记录发送错误的时间
  • message属性用于记录自定义的异常消息,通常是对API客户端友好的提示信息
  • debugMessage属性用于记录更为详细的错误报告
  • subErrors属性用于记录异常附带的子异常信息,如用户实体中字段校验信息等

RestApiSubError类用于记录更为细致的异常信息,通常为实体类中字段校验失败的异常报告:

abstract class RestApiSubError{}
@Data
@EqualsAndHashCode(callSuper = false)
@AllArgsConstructor
class RestApiValidationError extends RestApiSubError{
    private String object;
    private String field;
    private Object rejectedValue;
    private String message;

    RestApiValidationError(String object,String message){
        this.object = object;
        this.message = message;
    }
}

RestApiSubError类是一个抽象的空类,具体的扩展将在RestApiValidationError中进行实现。RestApiValidationError类将记录实体类中(如本讲中的User对象)属性校验失败报告。

现在,我们来校验GET /users/1 API,检索用户ID为1的用户信息:

{
  "id": 1,
  "username": "ramostear",
  "age": 28,
  "height": 170
}

REST API成功的返回了用户信息,接下来我们传入一个系统不存在的用户ID,看看REST API返回什么信息:

GET /users/100

{
  "restapierror": {
    "status": "NOT_FOUND",
    "timestamp": "2019-05-19 06:31:17",
    "message": "User was not found for parameters {id=100}"
  }
}

通过上述的JSON信息我们可以看到,检索不存在的用户信息,REST API返回了友好的提示信息。在一开始的时候我们测试提供不合符规范的用户年龄和身高信息,接下来我们在来测试一下提供一个空的用户名,观察REST API返回的信息:

{
  "restapierror": {
    "status": "BAD_REQUEST",
    "timestamp": "2019-05-19 06:37:46",
    "message": "Validation error",
    "subErrors": [
      {
        "object": "user",
        "field": "username",
        "rejectedValue": "",
        "message": "不能为空"
      }
    ]
  }
}

3. Spring Boot 处理异常信息的流程

Spring Boot 处理REST API异常信息将会涉及到三个注解:

@ControllerAdivice注解是在Spring 3.2版本中新增的一个注解,它能够将单个由@ExceptionHandler注解标注的方法应用到多个控制器中。使用它的好处是我们可以在一个统一的地方同时处理多个控制器抛出的异常,当控制器有异常抛出时,ControllerAdvice会根据当前抛出的异常类型,自动匹配对应的ExceptionHandler;当没有特定的Exception可用时,将调用默认的异常信息处理类来处理控制器抛出的异常(默认的异常信息处理类)。

下面,我们通过一张流程示例图,更为直观的了解Spring Application处理控制器异常信息的全部过程:

在图中,蓝色箭头表示正常的请求和响应过程,红色箭头表示发生异常的请求和响应过程。

4. 自定义异常信息处理类

Spring Framework自带的异常信息处理类往往不能满足我们实际的业务需求,这就需要我们定义符合具体情况的异常信息处理类,在自定义异常信息处理类中,我们可以封装更为详细的异常报告。

自定义异常信息处理类,我们可以站在“巨人”的肩膀上,快速封装自己的异常信息处理类,而不必要从头开始造“轮子”。现在,为了快熟实现自定义异常信息处理类,并让其正常工作,我们可以直接扩展Spring 提供的ResponseEntityExceptionHandler类来定义用户异常信息处理类。ResponseEntityExceptionHandler已经提供了很多可用的功能,我们只需要扩展该类或者覆盖其提供的方法即可。

打开ResponseEntityExceptionHandler类,我们可以看到如下的源码:

public abstract class ResponseEntityExceptionHandler {

 //不支持的HTTP请求方法异常信息处理方法
 protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(...){...}
//不支持的HTTP媒体类型异常处理方法
 protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(...){...}   
//不接受的HTTP媒体类型异常处方法
 protected ResponseEntity<Object> handleHttpMediaTypeNotAcceptable(...){...}
 //请求路径参数缺失异常处方法
 protected ResponseEntity<Object> handleMissingPathVariable(...){...}   
 //缺少servlet请求参数异常处理方法   
 protected ResponseEntity<Object> handleMissingServletRequestParameter(...){...}
 //servlet请求绑定异常
 protected ResponseEntity<Object> handleServletRequestBindingException(...){...}   
//不支持转换
 protected ResponseEntity<Object> handleConversionNotSupported(...){...}
 //类型不匹配
 protected ResponseEntity<Object> handleTypeMismatch(...){...}
 //消息无法检索
 protected ResponseEntity<Object> handleHttpMessageNotReadable(...){...}
 //HTTP消息不可写
 protected ResponseEntity<Object> handleHttpMessageNotWritable(...){...}
 //方法参数无效
 protected ResponseEntity<Object> handleMethodArgumentNotValid(...){...}
 //缺少servlet请求部分
 protected ResponseEntity<Object> handleMissingServletRequestPart(...){...}
 //绑定异常
 protected ResponseEntity<Object> handleBindException(...){...}
 //没有发现处理程序异常
 protected ResponseEntity<Object> handleNoHandlerFoundException(...){...}   
 //异步请求超时异常   
 @Nullable
 protected ResponseEntity<Object> handleAsyncRequestTimeoutException(...){...}
 //内部异常
 protected ResponseEntity<Object> handleExceptionInternal(...){...}       
}

我们选择性的覆盖几个常用的异常处理方法,并添加我们自定义异常处理方法:

public class RestExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    protected ResponseEntity<Object> handleUserNotFound(UserNotFoundException ex){
        RestApiError apiError = new RestApiError(HttpStatus.NOT_FOUND);
        apiError.setMessage(ex.getMessage());
        return buildResponseEntity(apiError);
    }


    @Override
    protected ResponseEntity<Object> handleMissingServletRequestParameter(
            MissingServletRequestParameterException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        String error = ex.getParameterName() + " parameter is missing";
        return buildResponseEntity(new RestApiError(BAD_REQUEST, error, ex));
    }



    @Override
    protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
            HttpMediaTypeNotSupportedException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {
        StringBuilder builder = new StringBuilder();
        builder.append(ex.getContentType());
        builder.append(" media type is not supported. Supported media types are ");
        ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(", "));
        return buildResponseEntity(new RestApiError(HttpStatus.UNSUPPORTED_MEDIA_TYPE, builder.substring(0, builder.length() - 2), ex));
    }

    ...

}

UserNotFoundException类为我们自定义异常信息类,在执行GET /users/{userIds}或 GET /users请求时,如果数据库中不存在该ID的记录信息,将抛出UserNotFoundException异常信息,且将响应状态码设置为NOT_FOUND。UserNotFoundException源码如下:

public class UserNotFoundException extends Exception {

    public UserNotFoundException(Class clz,String...searchParams){
        super(UserNotFoundException.generateMessage(clz.getSimpleName(),toMap(String.class,String.class,searchParams)));
    }

    private static String generateMessage(String entity, Map<String,String> searchParams){
        return StringUtils.capitalize(entity)+
                " was not found for parameters "+
                searchParams;
    }

    private static <K,V> Map<K,V> toMap(Class<K> key,Class<V> value,Object...entries){
        if(entries.length % 2 == 1){
            throw new IllegalArgumentException("Invalid entries");
        }
        return IntStream.range(0,entries.length/2).map(i->i*2)
                .collect(HashMap::new,
                        (m,i)->m.put(key.cast(entries[i]),value.cast(entries[i+1])),Map::putAll);
    }
}

下图将更为直观的说明自定义异常处理的整个流程:

当UserService发生异常时,异常信息将向上传递到UserController,此时的异常信息被Spring所捕获,并将其跳转到UserNotFoundException处理方法中。UserNotFoundException将异常报告封装到RestApiError对象中,并回传给API Client。通过此方法,API客户端将获得一份逻辑清晰的响应报告。

本次课程的全部源码已经上传到 Github 仓库,你可以点击此链接获取源码:https://github.com/ramostear/Spring_Boot_2.X_Tutorial/tree/master/spring-boot-exception-handing

(第五讲)自定义Spring Boot Starter

(第五讲)自定义Spring Boot Starter

(第五讲)自定义Spring Boot Starter

第四讲中我们已经了解了Spring Boot自动配置的基本原理,了解其原理之后,你会觉得Spring Boot的自动配置能力其实也没有那么的神秘。在本讲中,我们将尝试实现一个自定义的Spring Boot Starter,以巩固上一件中的内容。

1. 主要内容

  • 1.实现自定义的spring boot starter
  • 2.验证自定义的spring boot starter是否生效
  • 3.验证自动配置是否生效

本讲所涉及的源码已经上传到Github仓库,你可以通过下面的链接获取全部的源码:

https://github.com/ramostear/Spring_Boot_2.X_Tutorial/tree/master/spring-boot-starter-site

2. 实现自定义的spring boot starter

现在,我们来实现这一一个功能,当SiteInformation类在classpath下的时候,系统自动配置SiteInformation类的Bean,并且SiteInformation类中的属性可以在application.properties文件中进行配置。

首先,使用IntelliJ IDEA创建一个Maven工程,在当前Project上选择新建Module,在弹出面板中填写相关信息,如下图所示:

点击下一步,选择module存放路径,如下图所示:

点击完成按钮,生成新的Module,然后修改module中的pom.xml,添加spring-boot-autoconfigure依赖,完整的配置代码如下所示:

添加spring-boot-autoconfigure依赖的目的是让自定义的starter具有自动配置的能力

然后,创建一个名为:com.ramostear.spring.boot.starter.site的包,并在包下创建SiteInformation.java文件,这个类提供了站点的一些基本属性,源码如下:

@ConfigurationProperties(prefix = “site”)注解的作用是当我们需要覆盖站点信息时,在application.properties文件中需要使用“site”作为属性的前缀,如site.name = ramostear。如果不做任何配置,站点信息将使用默认值:unknown

接下来,创建一个SiteInformationProvider.java文件,用于返回站点信息,其源码如下所示:

SiteInformationProvider.java是自动配置的依据类,当引用此自定义Starter时,会依据SiteInformationProvider是否存在类创建这个类的Bean。

准备好上述两个类之后,需要创建一个自动配置类,如SiteInformationAutoConfiguration.java,其源码如下所示:

@ConditionalOnWebApplication注解表名此自动配置可以在Web应用中进行使用,@EnableConfigurationProperties(SiteInformation.class)注解指明提供自动配置的属性对象@ConditionalOnMissingBean(SiteInformationProvider.class)指明当容器中没有SiteInformationProvider这个类的Bean时,自动配置这个类的Bean

最后,需要将此配置类进行注册,Spring Boot才能完成自动配置工作。在resources资源文件夹下创建名为META-INF的文件夹,并添加名为spring.factories的文件,修改其内容为:

若有多个自动配置项,请用“,”进行分割,此处的“\”是为了换行后任然能够读取到属性。

完成上述的所有工作后,点击IDEA右侧Maven工具栏,选择当前项目,找到Plugins中的install选项并点击执行安装,如下图所示:

上述操作运行成功后,你可以在控制台看到如下所示的提示信息:

提示,请使用maven自带的插件对项目进行打包和安装,切勿使用spring-boot-maven-plugin进行打包安装,否则打包后的jar无法使用,请将下面的代码从你的pom文件中剔除:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

至此,自定义的spring boot starter制作完成。接下来创建一个web工程,引入spring-boot-starter-site,测试此starter是否能够正常使用。

3. 创建Web工程

新建一个测试用的Module,并使用IDEA自带的Spring Initializr创建一个Web工程,如下图所示:

选择Module SDK的版本,选择下一步,填写Module相关信息,如下图所示:

然后点击下一步,将Web依赖添加到项目中,然后点击保存按钮,生成新的Module。如下如所示:

接下来,将spring-boot-starter-site依赖添加到测试用的Web工程中,pom.xml源码如下:

4. 验证自定义Starter

修改CustomStarterTestApplication.java中的源码,添加@RestController注解,并创建一个获取站点信息的方法,源码如下:

接下来在application.properties文件中添加如下的类容:

site.name = 谭朝红的技术分享博客
site.domain = www.ramostear.com
site.keywords = blog
site.description = 谭朝红的技术分享博客
site.copyright = Copyright 2019-Ramostear

logging.level.org.springframework = debug

配置logging.level.org.springframework= debug属性的作用是能够在控制台日志中查看自定义的starter是否生效。

5. 运行并测试

点击运行CustomStarterTestApplication主类,并观察控制台输出,当自定义的starter生效后,你可以在控制台看到如下的信息:

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------
...
   SiteInformationAutoConfiguration matched:
      - @ConditionalOnWebApplication (required) found 'session' scope (OnWebApplicationCondition)

   SiteInformationAutoConfiguration#provider matched:
      - @ConditionalOnMissingBean (types: com.ramostear.spring.boot.starter.site.SiteInformationProvider; SearchStrategy: all) did not find any beans (OnBeanCondition)
...

最后,使用Postman工具请求http://localhost:8080/site/info 并观察返回的信息:

请求成功、并返回了站点的配置信息,详细信息如下所示:

{
  "name": "谭朝红的技术分享博客",
  "domain": "www.ramostear.com",
  "copyright": "Copyright 2019-Ramostear",
  "keywords": "blog",
  "description": "谭朝红的技术分享博客"
}

6. 总结

本小节演示了如何创建一个自定义的Spring Boot Starter,并进行测试。进一步加深和强化了对Spring Boot自动配置原理的理解。本次分享类容到此结束,谢谢~~

(第四讲)Spring Boot 自动化配置原理解析

(第四讲)Spring Boot 自动化配置原理解析

本讲着重介绍Spring Boot的核心技术原理—自动化配置(AutoConfiguration)。通过对Spring Boot自动化配置相关源码的梳理,一步步揭开Spring Boot自动化配置的神秘面纱。

1. 主要内容

通过本讲的内容,你可以找到下面几个问题的具体答案:

  1. 什么是自动化配置?
  2. 为什么需要自动化配置?
  3. 自动化配置是如何实现的?
  4. 如何调试自动化配置?

2. 为什么需要自动化配置

在常规的Spring应用程序中,充斥着大量的配置文件,我们需要手动去配置这些文件,如配置组件扫描、配置servlet、配置视图解析器,配置http编码等。这里以配置http编码为例,我们需要在web.xml文件中配置类似的一个filter:

下面的这段代码向我们展示了配置一个Servlet的经典写法:

如果我们的项目中使用Hibernate/JPA时 ,我们还需要配置诸如数据源、Bean工厂和事务管理器等。

上述的配置可以称得上是Spring应用程序的经典案例,但现在看起来,你是否觉得心累?常规的配置让开发人员将更多的经历耗费在了配置文件上。而这些配置都是一些固定模式的配置方式,甚至很多都是模板代码,那既然是这样一种情况,有没有一种可能性,让Spring自动完成这些模板配置工作呢?答案是肯定的,这就是Spring Boot AutoConfiguration产生的初衷。将开发人员从繁重的配置工作中解放出来,而这些繁琐的配置细节交由Spring Boot去完成,如果我们需要提供自己的配置参数,只需要覆盖自动配置的参数即可。现在是否觉得有点意思了?

3. Spring Boot自动化配置的核心原理

Spring Boot有关自动化配置的源码可以在spring-boot-autoconfigure-2.x.x.x.jar/spring-boot-autoconfigure-1.x.x.x.jar包中找到。在org.springframework.boot.autoconfigure包下,提供了如下的一些自动化配置源码:

如何你想了解更多关于Spring Boot自动配置的知识,可以在这里查看具体的源码。

在spring-boot-autoconfigure.jar包中还有一份重要的文件,它是Spring Boot实施自动配置的关键所在。该文件是/META-INFO/spring.factories。此文件列出了所有可以被Spring Boot实施自动配置的模块清单,下面列举其中的一部分以供参考:

4. 调试自动配置

有三种方式可以调试并查看有关自动配置的相关信息,如当前项目中有哪些组件被自动配置,有哪些组件未被自动配置以及产生的原因等。

  1. 通过java -jar [jarfile] —debug的方式运行项目,可以观察自动化配置的相关信息。

  2. 打开项目的调试日志记录。在application.properties文件中为org.springframework包添加日志记录级别为DEBUG :

    logging.level.org.springframework:DEBUG
    
  3. 为项目添加spring-boot-starter-actuator和spring-data-rest-hal-browser两个依赖:

使用方式一和方式二时,当我们重新启动应用程序时,你可以在控制台找到一份自动配置的报告,这份报告包含了被自动配置的组件信息和未被自动配置的组件信息以及相关的说明。

如果采用第三种方式,重新启动项目后,在浏览器地址栏输入http://localhost:8080/actuator/#http://localhost:8080/autoconfig ,你将收到如下的信息:

5 Spring Boot自动配置的核心原理

在Spring Boot应用程序中,所有的运作都起源于@SpringBootApplication注解,@SpringBootApplication打开了运行程序的大门。该注解是一个组合注解,其核心功能是由@EnableAutoConfiguration注解提供的。现在先让我们看一下@SpringBootApplication@EnableAutoConfiguration注解的源码。

在SpringBootApplication注解类中,最核心的地方是上图1处的@EnableAutoConfiguration注解,它为@SpringBootApplication注解类贡献了大部分的功能。我们再看看@Enableautoconfiguration注解的源码:

这里最关键的地方是由@Import注解导入的自动配置功能,EnableConfigurationImportSelector通过SpringFactoriesLoader.loadFactoryNames()方法来扫描spring-boot-autoconfigure.jar文件下/META-INF/spring.factories文件中配置的jar包信息,这就是一开始提到的那一份spring.factories文件的重要性。我们可以看一下EnableConfigurationImportSelector类中对应的代码:

6. 一个自动配置的示例分析

org.springframework.boot.autoconfigure包下已经提供了很多的自动配置类,接下来我们以DataSourceAutoConfiguration为例子,看看其中配置的细节。通常,所有自动配置类都会查看类路径中可用的其他类。如果类路径中有特定的类,则通过自动配置启用该功能的配置。判断类路径上是否存在特定的东西,这是通过org.springframework.boot.autoconfigure.condition包下的条件注解来完成的,这里列举几个常用的条件注解解:

  1. @ConditionalOnBean:当容器中有指定的Bean的条件下
  2. @ConditionalOnClass:当类路径下有指定的类的条件下
  3. @ConditionalOnMissingBean: 当容器中没有指定Bean的情况下
  4. @ConditionalOnMissingClass: 当类路径下没有指定的类的条件下

@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }):仅当类路径中有这些类时,才会启用此配置。

@ConditionalOnMissingBean :仅当没有其他bean配置相同名称时才配置此Bean。

7.结束语

通过以上内容,简单的梳理了Spring Boot 自动配置的大致原理,同时回答了一开始提出的4个问题。Spring Boot的出现,是为了提高我们的开发效率,我们需要去了解其核心的基本原理,但任何时候都不应该被一个框架或者技术所绑架,更为重要的是学习其基本的原理,而不是框架本身。基于这个出发点,在下一讲中,将实现一个自定义的Spring Boot Starter,并提供自动化配置的能力。那本次内容到这里就结束了,谢谢~~

(第三讲)使用JUnit对Spring Boot中的Rest Controller进行单元测试

(第三讲)使用JUnit对Spring Boot中的Rest Controller进行单元测试

(第三讲)使用JUnit对Spring Boot中的Rest Controller进行单元测试

本次教程主要讲解如何对Spring Boot中的Rest Service进行单元测试。以往我们主要是使用JUnit对业务层进行单元测试,本次课程将使用一个简单的案例来说明如何使用JUnit对Spring Boot的Rest Service进行单元测试。

1. 主要类容

  • 快速搭建Restfull Service 环境
  • 创建GET请求以检索用户信息
  • 创建GET请求检索用户角色信息
  • 创建POST请求新增用户角色信息
  • 如何使用PostMan请求Restfull Service
  • 使用JUnit对GET请求进行单元测试
  • 使用JUnit对POST请求进行单元测试

2. 你将需要准备的工具

  • JDK 1.8及以上版本
  • Maven 3.0及以上版本的项目构建工具
  • IDEA代码编辑器

3. 你可以通过以下的地址获取本次课程的所有示例代码

项目代码已经上传到GitHub仓库中,你可以通过以下的地址获取示例源码:

https://github.com/ramostear/Spring_Boot_2.X_Tutorial/tree/master/spring-boot-junit-rest-service

4. 项目结构

下面通过一张截图来了解以下本次课程中我们使用到的项目结构。

首先我们需要位单元测试提供一个可用的Rest Controller。UserController文件为我们提供了一个可用于测试的Rest Controller。在UserController类中,我们提供两种请求类型的方法,一种是GET请求,另一种是POST请求。然后我们为这两种请求方式的方法编写单元测试用例。

在接下来的测试过程中,我们将使用Mockito来模拟请求UserService的过程,使用MockMvc来模拟请求UserController。单元测试的目的是将测试范围尽可能的缩小。在本次案例中,我们仅对UserController中的方法进行测试。

5. 初始化项目

我们依然使用Spring Initializr来初始化本次课程的项目,你需要配置如下图中的参数:

现在我们需要提供两个实体类:User和Role:

User.java

Role.java

6. 提供可用的业务服务

所有的应用都需要有数据的存储,本次课程主要的重点是为了Rest Controller的单元测试,因此使用ArrayList来充当数据库的角色。在案例中,一个用户可以有多个角色,一个角色也可以被赋予给多个用户。用户有ID,名字,别名和角色列表,角色具有ID,名称和描述。在UserService类中,将提供如图所示的公共方法。

7. 提供GET请求方法

在UserController类中,我们将提供如下几个公开的GET请求方法:

  • @GetMapping(value=”/users”) : 获取所有的用户信息
  • @GetMapping(value=”/users/{id}/roles”) : 根据用户ID获取该用户的所有角色信息

UserController.java类中的详细代码如下:

package com.ramostear.spring.boot.test.restservice.controller;

import com.ramostear.spring.boot.test.restservice.model.Role;
import com.ramostear.spring.boot.test.restservice.model.User;
import com.ramostear.spring.boot.test.restservice.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class UserController {
    private  final UserService userService;

    @Autowired
    public UserController(UserService userService){
        this.userService = userService;
    }

    @GetMapping(value = "/users")
    public List<User> findAllStudents(){
        return userService.findAllUsers();
    }

    @GetMapping(value = "/users/{id}/roles")
    public List<Role> findUserRoles(@PathVariable(value = "id")String id){
        return userService.findUserAllRoles(id);
    }

}

8. 使用Postman对RestController进行测试

我们将使用Postman工具对上述两个Rest API进行请求,首先向Postman地址栏输入http://localhost:8080/users 进行测试,获得的响应信息如下:

[
    {
        "id": "1001",
        "name": "ramostear",
        "alias": "谭朝红",
        "roles": [
            {
                "id": "1001",
                "name": "admin",
                "description": "all permissions for this role."
            }
        ]
    }
]

下图显示了Postman对此API进行测试的实际结果:

9. 为RestController编写单元测试

当我们需要对一个Rest Controller进行单元测试时,我们只想启动和SpringMVC相关的组件,而不必要启动所有的Web组件。我们可以使用WebMvcTest注解来解决这样的测试需求。此注解将禁用Spring Boot的自动化配置,仅仅启动与MVC相关的配置。下面将对测试用例中的几个核心注解做一下介绍:

  • @RunWith(SpringRunner.class) : SpringRunner是SpringJUnit4ClassRunner的简写,它扩展了BlockJUnit4ClassRunner类,用于提供测试时的Spring应用上下文信息。
  • @WebMvcTest(value=UserController.class,secure = false) : 该注解用于测试Spring MVC应用程序,使用此注解的好处是我们只需要加载UserController类并对其中的方法进行单元测试,而不需要加载其他的控制器。
  • MockMvc : MockMvc是测试Spring MVC应用程序的主要入口,它将为我们的测试提供一个模拟的应用上下文环境。
  • @MockBean : MockBean主要是模拟向Spring应用上下文注入一个Bean对象,并使该Bean对象可以在控制器中被访问到。

下面是测试用例的源代码:

package com.ramostear.spring.boot.test.restservice;

import com.ramostear.spring.boot.test.restservice.controller.UserController;
import com.ramostear.spring.boot.test.restservice.model.Role;
import com.ramostear.spring.boot.test.restservice.model.User;
import com.ramostear.spring.boot.test.restservice.service.UserService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.skyscreamer.jsonassert.JSONAssert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

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

@RunWith(SpringRunner.class)
@WebMvcTest(value = UserController.class,secure = false)
public class UserControllerTests {
    private static Logger logger = LoggerFactory.getLogger(UserControllerTests.class);
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    public void findAllUsers() throws Exception{
        User user = new User();
        user.setId("1001");
        user.setName("ramostear");
        user.setAlias("谭朝红");

        Role role = new Role();
        role.setId("1001");
        role.setName("admin");
        role.setDescription("all permissions for this role.");
        List<Role> roles = new ArrayList<>();
        roles.add(role);
        user.setRoles(roles);
        List<User> users = new ArrayList<>();
        users.add(user);

        Mockito.when(userService.findAllUsers()).thenReturn(users);
        RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/users");
        MvcResult result = mockMvc.perform(requestBuilder).andReturn();
        String expected = "[{\"id\":\"1001\",\"name\":\"ramostear\",\"alias\":\"谭朝红\",\"roles\":[{\"id\":\"1001\",\"name\":\"admin\",\"description\":\"all permissions for this role.\"}]}]";
        logger.info(result.getResponse().getContentAsString());
        JSONAssert.assertEquals(expected,result.getResponse().getContentAsString(),false);
    }

    @Test
    public void findAllUserRoles() throws Exception{
        Role role = new Role();
        role.setId("1001");
        role.setName("admin");
        role.setDescription("all permissions for this role.");
        List<Role> roles = new ArrayList<>();
        roles.add(role);
        Mockito.when(userService.findUserAllRoles("1001")).thenReturn(roles);
        RequestBuilder requestBuilder = MockMvcRequestBuilders.get("/users/1001/roles");
        MvcResult result = mockMvc.perform(requestBuilder).andReturn();
        String expected = "[{\"id\":\"1001\",\"name\":\"admin\",\"description\":\"all permissions for this role.\"}]";
       logger.info(result.getResponse().getContentAsString());
        JSONAssert.assertEquals(expected,result.getResponse().getContentAsString(),false);
    }
}

Mockito.when().thenReturn():用于测试UserService在被调用时是否返回和预期一致的结果

mockMvc.perform().andReturn():mockMvc主要用于执行请求并返回响应数据

下面我们执行上述两个方法,看看测试结果:

两个方法均测试通过,且控制台也输出了如下的日志信息:

2019-05-10 05:36:40.567  INFO 18268 --- [           main] c.r.s.b.t.r.UserControllerTests          : [{"id":"1001","name":"admin","description":"all permissions for this role."}]
2019-05-10 05:36:40.585  INFO 18268 --- [           main] c.r.s.b.t.r.UserControllerTests          : [{"id":"1001","name":"ramostear","alias":"谭朝红","roles":[{"id":"1001","name":"admin","description":"all permissions for this role."}]}]

10 . 添加POST请求方法

现在我们在Rest Controller中新增一个为用户添加新角色的方法,当角色被成功设置后将返回状态码为201的一个创建资源状态。代码如下:

 @PostMapping(value = "/users/{id}")
    public ResponseEntity<Object> setUserRole(@PathVariable(value = "id")String id, @RequestBody Role role){
        userService.addUserRole(id,role);
        return new ResponseEntity<>(role, HttpStatus.CREATED);
    }

使用Postman对此接口进行请求,获得如下的响应信息:

{
    "id": "1002",
    "name": "editor",
    "description": "content editor"
}

下图时使用Postman请求的实际结果:

11. 为Post方法提供单元测试用例

在接下来的测试中,我们将使用MockMvcRequestBuilders.post()方法来模拟请求添加用户角色的方法,并使用accept()方法来设置数据格式,另外还需断言请求响应的状态值是否为CREATED且返回的角色信息是否与预期的一致。Post方法的测试源码如下:

@Test
    public void addUserRole() throws Exception{
        String JSON = "{\"id\":\"1002\",\"name\":\"editor\",\"description\":\"content editor\"}";
        RequestBuilder requestBuilder = MockMvcRequestBuilders.post("/users/1001")
                .accept(MediaType.APPLICATION_JSON).content(JSON)
                .contentType(MediaType.APPLICATION_JSON);
        MvcResult result = mockMvc.perform(requestBuilder).andReturn();
        MockHttpServletResponse response = result.getResponse();
        Assert.assertEquals(HttpStatus.CREATED.value(),response.getStatus());
        String expected = "{\"id\":\"1002\",\"name\":\"editor\",\"description\":\"content editor\"}";
        logger.info(result.getResponse().getContentAsString());
        JSONAssert.assertEquals(expected,result.getResponse().getContentAsString(),false);
    }

最后,我们执行此测试用例方法,观察测试结构:

通过图我们可以看到,Post方法已成功通过测试。

今天的课程分享到这里就结束了,在本次课程中,给出了如何测试Rest Controller的方法,同时还使用Postman来进行辅助测试。所有的测试都达到了预期的测试效果。

(第二讲)Spring&Spring MVC&Spring Boot三者之间的区别与联系

(第二讲)Spring&Spring MVC&Spring Boot三者之间的区别与联系

Spring Framework的诞生让开发人员的工作从石器时代跨域到了工业时代,你是否还能记起手撸Servlet和JDBC的岁月?,你是否还对Struts1以及Struts2莫名其妙的404错误记忆犹新?从2004年3月Spring 1.0发布到至今,Spring的发展已经走过了15个年头,其创造的价值让人瞩目。今天,带着这样一个背景来梳理一下Spring Framework,Spring MVC和Spring Boot三者之间的区别。

我们使用Spring家族的系列产品这么长时间,不禁会问这样几个问题:Spring Framework是什么?Spring MVC是什么?Spring Boot又是什么?它们被设计出来的目的是什么?

你需要了解的知识

在接下来的内容中,将梳理这样几个知识点:

  • Spring Framework基本概述
  • Spring Framework主要解决的问题是什么?
  • Spring MVC基本概述
  • Spring MVC主要解决的问题是什么?
  • Spring Boot主要解决的问题是什么?
  • Spring,Spring MVC和Spring Boot三者之间的区别是什么?

Spring Framework 解决了哪些核心问题?

当你仔细思考这个问题的时候你会发现,很多地方它都有渗透到,貌似一个Spring就可以撑起开发的半边天,以至于很难一下子回答这个问题。那Spring Framework到底解决了哪些核心问题?

Spring Framework最重要也是最核心的特性是依赖注入。所有的Spring模块的核心就是DI(依赖注入)或者IoC(控制反转)。

依赖注入或控制反转是Spring Framework最大的特性,当我们正确使用DI(依赖注入)或IoC时,可以开发出一个高内聚低耦合的应用程序,而这一一个低耦合的应用程序可以轻松的对其实施单元测试。这就是Spring Framework解决的最核心的问题。

无依赖注入

请考虑这一一个案例:UserAction依赖于UserService来获取用户信息,在没有依赖注入的情况下,我们需要手动在UserAction中实例化一个UserService对象,这样的手工作业意味着UserAction和UserService必须精密的联系在一起,才能正常工作。如果一个Action需要多个Service提供服务,那实例化这些Service将是一个繁重的工作。下面我们给出一个不使用依赖注入的代码片段加以说明:

UserService.java

public interface UserService{
    User profile();
}

UserServiceImpl.java

public class UserServiceImpl implements UserService{
    @Override
    User profile(){
        // TODO
    }
}

UserAction.java

@RestController
public class UserAction{

    private UserService userService = new UserServiceImpl();
    // other services...

    @GetMapping("/profile")
    public User profile(){
        return userService.profile();
    }
}

引入依赖注入

引入依赖注入将会使整个代码看起来很清爽。为了能够开发出高内聚低耦合的应用程序,Spring Framework为我们做了大量的准备工作。下面我们使用两个简单的注解@Component@Autowired来实现依赖注入。

  • @Component : 该注解将会告诉Spring Framework,被此注解标注的类需要纳入到Bean管理器中。
  • @Autowired : 告诉Spring Framework需要找到一与其类型匹配的对象,并将其自动引入到所需要的类中。

在接下来的示例代码中,我们会看到Spring Framework将为UserService创建一个Bean对象,并将其自动引入到UserAction中。

UserService.java

public interface UserService{
    User profile();
}

UserServiceImpl.java

@Component
public class UserServiceImpl implements UserService{
    @Override
    User profile(){
        // TODO
    }
}

UserAction.java

@RestController
public class UserAction{
    @Autowired
    private UserService userService;
    // other services...

    @GetMapping("/profile")
    public User profile(){
        return userService.profile();
    }
}

对比上下两部分的代码,你是否发现了他们之间的区别?Action所依赖的Service的初始化工作全部交由Spring Framework来管理,我们只需要在适当的地方向Spring Framework索取想要服务即可。这就好比当我想要吃薯片的时候,我不需要自己亲自种土豆,施肥,收获…清洗,切片…一直到最后炸土豆片,想想都觉得累,而更简单的方法是直接去超市购买自己想要的薯片即可。

Spring Framework还有其他的核心特性吗?

#1:衍生的特性

Spring Framework的依赖注入是核心中的核心,在依赖注入核心特性的基础上,Spring Framework还衍生出了很多的高级模块:

  • Spring JDBC
  • Spring MVC
  • Spring AOP
  • Spring ORM
  • Spring JMS
  • Spring Test

对于这些新的高级模块,可能会产生这一一个问题:它们是否是一个全新的功能?答案是否定的,在不使用Spring Framework的情况下,我们依然能够使用JDBC连接数据库,依然能够对视图和数据模型进行控制,依然能够使用第三方的ORM框架。那Spring Framework干了什么?Spring Framework站在巨人的肩膀上,对这些原生的模块进行了抽象,而抽象可以带来这样一些好处:

  • 减少了应用中模板代码的数量
  • 降低了原生框架的技术门槛
  • 基于依赖注入特性,实现了代码的解耦,真正的高内聚、低耦合
  • 更细粒度的单元测试

这样的好处是显而易见的,比如与传统的JDBC相比,使用JDBCTemplate操作数据库,首先是代码量小了,其次是我们不需要再面对恐怖的try-catch。

#2:优秀的集成能力

Spring Framework还具备另外一个重要特性,那就是能够快速的与其他三方框架进行整合。与其自己造轮子,还不如想办法将好的轮子整合在一起,我想这句话应该可以用来概况Spring Framework这一特性。Spring Framework对于整合其他的框架,给出了不错的解决方案,下面将列举一些常见的方案:

  • 与Hibernate ORM框架整合
  • 与MyBatis 对象映射框架整合
  • 与Junit单元测试框架整合
  • 与Log4J日志记录框架整合

Spring MVC是什么?

Spring MVC提供了构建Web应用程序的全功能MVC模块,实现了Web MVC设计模式以及请求驱动类型的轻量级Web框架,即采用了MVC架构模式的思想,将Web层进行职责解耦。基于请求驱动指的是使用请求-响应模型,视图与数据模型分离,以简化Web应用的开发。

使用Spring MVC提供的Dispatcher Servlet,ModelAndView和ViewResolver等功能,可以轻松的开发出一个Web应用程序。

Spring Boot出现的原因是什么?

我们都知道,使用Spring Framework来开发应用程序,需要进行大量的配置工作以及依赖包的管理,工作繁重而且极易出现配置错误,尤为明显的是依赖包之间的版本冲突问题。

举一个简单的案例,当我们使用Spring MVC来开发Web应用程序时,我们大致需要经历这样几个步骤:

  • 1:配置组件扫描的包路径

  • 2:配置对应的Servlet程序

  • 3:配置视图解析器

  • 4:配置页面模板引擎(JSP、Freemarker等)

下面给出了一个小范围的举例:

...
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix">
        <value>/WEB-INF/pages</value>
    </property>
    <property name="suffix">
        <value>.jsp</value>
    </property>
</bean>
<mvc:resources mapping="/static/**" location="/static/"/>
...

此外,我们还需要配置先关的Servlet处理程序,它们大致是这样的:

<servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>
        org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/todo-servlet.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

如果我们的应用程序还需要链接数据,则还需要配置数据源,实体对象管理器,事务管理器等众多配置:

<bean id="datasource" class="">
    ...
</bean>
<bean id="entityManagerFactory" class="">
    ...
</bean>
<bean id="transactionManager" class="">
    ...
</bean>
...

面对众多的配置文件,我们需要花费大量的时间去处理,这时你可能会问,我为什么要花费那么多的时间去管理Spring的配置工作?不是应该专注于应用本身的业务逻辑吗?现在,有了Spring Boot,这些烦心事就不需要你去操心了。

#1:Spring Boot的自动化配置能力

我为什么会把Spring Boot的自动化配置能力放在第一位,因为它极大的降低了我们使用Spring Framework所付出的成本。这是Spring Boot的自动化配置是一个最具价值的解决方案。

这难道不值得我们拍案叫好吗?如果你想要开发一个Web应用程序,你需要做的事情就是将Spring Boot Web包引入到项目的类路径下,Spring Boot就可以帮你解决后续的大多数配置工作。

  • 如果Hibernate的依赖被放到了类路径上,Spring Boot会自动配置数据源
  • 如果Spring MVC的依赖被放到了类路径上,Spring Boot又会自动配置Dispatcher Servlet

当Spring Boot检测到有新的依赖包添加到类路径上,Spring Boot会采用默认的配置对新的依赖包进行设置,如果我们想自己配置依赖包时,只需要手动覆盖默认的配置项即可。

  1. Spring Boot扫描类路径上可用的框架信息
  2. 获取应用程序现有的配置信息
  3. 如果应用程序没有提供框架的配置信息,Spring Boot将采用默认的配置来配置框架,这就是Spring Boot的自动配置特性(Auto Configuration)

#2:Spring Boot Starter项目

在传统模式的开发过程中,我们需要反复的确认应用程序所需要的第三方JAR包,以及这些JAR的版本和依赖关系。例如,现在我们打算开发一款Web应用程序,应用程序大概需要如下的一些依赖包:Spring MVC,Jackson Databind(用于数据绑定),Hibernate-Validator(用于服务端的数据校验)和Log4j(用于日志记录)。现在,我们需要去下载对应的jar包到应用程序中,并且还需要处理依赖包之间版本冲突的问题。

<dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-webmvc</artifactId>
   <version>4.2.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.5.3</version>
</dependency>

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.0.2.Final</version>
</dependency>

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

这不是一件简单的事情,特别是发生版本冲突的时候,让应用程序能够正常运行起来就需要花费一定的时间。

Spring Boot Starter是一组用于管理依赖关系的描述符,通过这些描述符,我们可以在应用程序中轻松的管理依赖包,你可以以开箱即用的方式获取想要的依赖包,而无需去Maven仓库总检索对应的依赖,并将依赖配置复制粘贴到应用程序的pom文件中。例如,如果你想要使用Spring和JPA进行数据库访问,只需要在pom中添加spring-boot-starter-data-jpa依赖项就可以。

现在,如果我们想要开发一个Web应用程序,使用Spring Boot Starter Web依赖会是一个不错的选择。我们可以通过使用Spring INitializr快速构建一个Web应用程序,并将Spring Boot Starter Web添加到项目中。此时我们的pom文件只需要很少的配置:

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

Spring Boot Starter Web会为我们预装如下的一些依赖:

  • Spring : core,beans,context,aop
  • Web MVC : Spring MVC
  • Jackson : JSON Binding
  • Validation : Hibernate Validator,Validation API
  • Embedded Servlet Container: Tomcat
  • Logging : logback,slf4j

对于开发人员而言,我们不需要去担心这些依赖项的管理工作以及解决他们之间的兼容性问题,Spring Boot已经帮我们解决了。

Spring Boot的核心目标

Spring Boot的核心目标在于快速实现生产就绪的应用程序,这将包含这样几个部分:

  • 执行器 : 启用高级监控和跟踪应用程序功能
  • 嵌入式服务器:Spring Boot已经内置了多个Web服务器,如Undertow,jetty,tomcat,因此我们不需要再额外的配置服务器,就可以完成应用程序的调试工作。
  • 默认的异常处理机制
  • 开箱即用的依赖项管理机制
  • 自动化配置

总结

通过上述的梳理,我们可以看到,Spring Framework是一个提供了DI(依赖注入)和IoC(控制反转)的开发框架,使用Spring Framework可以帮助我们开发出高内聚,低耦合的应用程序,Spring MVC是在Spring Framework基础上发展出来的基于MVC模式的全功能Web开发框架,实现了Model,View和Controller之间的职责解耦;Spring Boot为我们提供了一个能够快速使用Spring Framework的优秀解决方案,通过最小化的配置,我们就可以使用Spring Framework,严格意义上讲,Spring Boot并不是某种框架,它只是为开发人员提供了一个更好的更方便的使用Spring Framework的解决方案。

(第一讲)Spring Initializr-快速入门Spring Boot的最好选择

(第一讲)Spring Initializr-快速入门Spring Boot的最好选择

Spring Initializr [http://start.spring.io/]是引导你快速构建Spring Boot项目的不二选择。

它允许你通过简单的操作步骤,就可以构建出一个完整的Spring Boot应用程序。你可以通过Spring Initializr引导界面构建如下类型的Spring Boot应用:

  • Web应用程序

  • Restful 应用程序

  • Batch应用程序

Spring Boot对很多第三方框架提供了良好的支持,可以通过对应的artifactId获得他们,这里列举其中的一部分供参考:

  • spring-boot-starter-web-services:用于构建可与外界交互的SOAP Web服务应用程序
  • spring-boot-starter-web:可用于构建Web应用程序或者基于Restful风格的应用程序
  • spring-boot-starter-test:可用于构建并编写单元测试和集成测试
  • spring-boot-starter-jdbc:构建基于JDBC的应用程序
  • spring-boot-starter-hateoas:通过引入HATEOAS功能,让你轻松实现RESTful服务
  • spring-boot-starter-security:使用Spring Security对系统用户进行身份验证和鉴权
  • spring-boot-starter-data-jpa:基于Hibernate实现的Spring Data JPA
  • spring-boot-starter-cache:开启基于Spring Framework的缓存支持
  • spring-boot-starter-data-rest:使用Spring Data REST提供REST服务

在本讲中,我将通过使用Spring Initializr来演示如何快速创建一个简单的Web应用程序。

使用Spring Initializr构建Web应用程序

使用Spring Initializr构建Web应用程序是一件非常简单快速的事情。

如上图所示,我们需要执行如下的几个操作:

  • 通过浏览器访问Spring Initializr官网 ,然后再执行下面的几个选择项
    • 设置groupId : com.ramostear.spring.boot
    • 设置artifactId: spring-boot-quick-start
    • 项目名称:默认为spring-boot-quick-start
    • 基础包名:默认即可(你也可以选择修改)(通过点击More options展开)
    • 在搜索框中分别检索并选择如下几个组件:Web,Actuator,DevTools
  • 最后,点击“Generate Project”生成并下载项目
  • 将项目导入到IntelleJ IDEA中

Spring Boot项目目录结构

下图显示了在IDEA中导入刚才下载的项目目录结构:

  • SpringBootQuickStartApplication.java:Spring Boot运行的主文件,它负责初始化Spring Boot自动配置和Spring应用程序上下文
  • application.properties : 应用程序配置文件
  • SpringBootQuickStartApplicationTests : 用于单元测试的简单启动器
  • pom.xml : Maven构建项目的配置文件,包括了Spring Boot Starter Web等相关依赖项。特别指出,它会自动将Spring Boot Starter Parent作为整个工程的父依赖。

核心的代码

src/main/java 包下放置我们主要的逻辑代码,src/test/java包下放置项目的测试代码,src/main/resources包下放置项目的配置文件以及一些静态资源文件,如页html文件,css文件和js文件等。我们从上到下依次进行介绍。

SpringBootQuickStartApplication.java

package com.ramostear.spring.boot.springbootquickstart;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootQuickStartApplication {

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

}
  • @SpringBootApplication : 负责初始化Spring Boot 自动化配置项和Spring应用程序上下文
  • SpringApplication.run() : 负责启动Spring Boot应用程序的静态方法

application.properties

Spring Boot应用程序的配置文件,这里我们简单的设置一下项目启动的端口为8080(默认端口8080)和应用名称为Spring Boot Quick Start

server.port= 8080
spring.application.name= Spring Boot Quick Start

SpringBootQuickStartApplicationTests

package com.ramostear.spring.boot.springbootquickstart;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

   @Test
   public void contextLoads() {
   }

}
  • 集成了对Spring Boot应用程序进行单元测试所需要的全部环境

运行应用程序

如上如所示,点击运行按钮一键运行Spring Boot应用程序,成功启动后你将看到控制台中输出的如下日志信息:

"C:\Program Files\Java\jdk1.8.0_144\bin\java" ...

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.4.RELEASE)

 ...
 2019-04-21 16:44:16.784  INFO 2320 --- [  restartedMain] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 2 endpoint(s) beneath base path '/actuator'
2019-04-21 16:44:16.849  INFO 2320 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-04-21 16:44:16.852  INFO 2320 --- [  restartedMain] .r.s.b.s.SpringBootQuickStartApplication : Started SpringBootQuickStartApplication in 3.526 seconds (JVM running for 5.448)

总结

现在,你已经掌握了如何通过Spring Initializr来快速构建Spring Boot Weby9应用程序,你可以在Github上获取本次内容的全部源码。