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

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

Ehcache:Another CacheManager with same name exception

Ehcache:Another CacheManager with same name exception

Ehcache:Another CacheManager with same name异常

​ 在Spring Boot中开启热部署功能后,整合Shiro和Ehcache缓存时发生如下的异常信息:

Caused by: net.sf.ehcache.CacheException: Another CacheManager with same name 'shiro-cache' already exists in the same VM. Please provide unique names for each CacheManager in the config or do one of following:
1. Use one of the CacheManager.create() static factory methods to reuse same CacheManager with same name or create one if necessary
2. Shutdown the earlier cacheManager before creating new one with same name.

导致此错误的原因是针对新版本(2.5以后)的Ehcache,CacheManager使用了单例模式,当使用Spring Boot devtools开启热加载功能时,在加载SecurityManager Bean 对象实例时,会创建多个CacheManager实例,从而导致上述的异常。

​ 在此之前,网上已给出很多的解决方案,如降低Ehcache版本,设置缓存shared属性等。那么,如何在不改变当前框架版本的情况下修复上述异常呢?下面是笔者的完整解决方案。

1.改造前的代码

​ 下面的代码片段是改造前的示例:

在此示例中,只是简单的创建了ehcacheManager对象实例,并设置了缓存文件的存放地址。而新版本中,CacheManager要求是单例模式,因此需要使用EhCachManagerFactoryBean对象来限定cacheManager为共享模式。EhCachManagerFactoryBean是Spring提供的ehcache实现类。接下来我们看看如何改造。

2. 改造后的代码

​ 修改的地方一共有三处,它们是:配置EhcacheManagerFactoryBean并设置其为共享模式,配置DefaultAdvisorAutoProxyCreator,让Spring 来管理Shiro的Bean生命周期,配置lifecycleBeanPostProcessor,并让DefaultAdvisorAutoProxyCreator依赖于此对象。

2.1 添加EhcacheManagerFactoryBean配置项

​ 在原有的配置文件中添加EhcacheManagerFactoryBean配置项,并将shared属性设置为true,示例代码如下:

2.2 添加LifecycleBeanPostProcessor配置项

​ 添加LifecycleBeanPostProcessor配置项,LifecycleBeanPostProcessor将统一管理Initializable和Destroyable的实现类,从而达到统一管理Shiro Bean的生命周期的目的。示例代码如下:

2.3 添加DefaultAdvisorAutoProxyCreator配置项

DefaultAdvisorAutoProxyCreator的作用是动态代理Shiro的事务,最终将事务交由Spring进行统一管理。此配置项需要依赖于LifecycleBeanPostProcessor。示例代码如下:

通过添加上述的三个配置项,在Spring Boot中开启热加载功能就可以避免Another CacheManager with same name的异常出现。

鱼与熊掌得兼:Hibernate与Mybatis共存

鱼与熊掌得兼:Hibernate与Mybatis共存

鱼与熊掌得兼:Hibernate与Mybatis共存

很长一段时间,网上有很多关于Hibernate与Mybatis孰优孰劣的争论,两个阵营的人谁也不能说服谁,每个人的理由都很有道理。今天,我分享的主题是:在一个项目中同时使用Hibernate和Mybatis两个ORM框架。

​ 作为一个开发者,没有必要花费过多的时间去证明技术无用论,当你开始指责某个框架垃圾,另外一个框架最好时,隐性的暴露出你对某个框架没有深入的研究,无知的指责对于技术的提升没有任何的帮助。框架本身没有对错一说,只有适合和更适合项目的选择。任何框架都有自身的能力范围,就拿Hibernate和Mybatis这两个ORM框架来说,Hibernate封装了很多有用的API给开发者,降低了操作数据库的难度和复杂度,同时也减少了模板代码的数量,但Hibernate留给开发者可操作的空间相对Mybatis少了很多;Mybatis框架使用起来很灵活,开发者可以自定义查询语句,但增加了模板代码的数量,看起来没有Hibernate那么便捷。两种框架在便捷与灵活两个指标上做出了取舍与妥协,这不能说是框架的错。对于一个框架而言,需要有自身专注的领域和设计愿景,不可能面面俱到。

​ 使用任何一种技术框架,都需要贴合现实的业务需求以及自身的技术能力。当你还没有深入的去了解一门技术或者当前业务需求无法与框架契合时,不要盲目的批判框架的好坏。今天,我不再去对比Hibernate与Mybatis两者之间的优劣,而是给出一个比较中庸的放方案,将两个ORM框架同时整合在一个项目中。

一、准备开发环境

​ 如果你想成功运行本文中的源代码,需要满足一下的几个条件:

  • 1、JDK : JDK 1.8.x及以上版本
  • 2、Maven : Maven 3.x或更高版本
  • 3、Git:版本控制工具,选择一个你喜欢的
  • 4、IDE : 选择你比较喜欢的一个代码编辑器,如STS、IntelliJ IDEA。笔者使用的是IntelliJ IDEA
  • 5、Databases : 选择一个你熟练使用的数据库系统。笔者在本文中使用的是MySQL 5.1.x版本的数据库系统

如需获取本次分享内容的源代码进调试,可以到文章末尾找到源代码仓库连接

二、搭建项目

2-1、引入依赖

​ 为了快速构建项目,笔者采用Spring Boot来构建项目,同时使用加入Spring Data JPA和Mybatis两个ORM框架的依赖包。在此需要特别说明,Hibernate是一个JPA标准的实现,尔Spring Data JPA是一个JPA数据访问抽象,通过Spring Data JPA,可以轻松使用Hibernate框架。

​ 你可以通过Spring Initializer来初始化项目,也可以通过IDEA自带的Spring Initializer功能构建项目,项目构建完成之后,pom.xml文件中的配置如下(包含但不限于文中给出的依赖项):

2-2、定义实体类-User.java

​ 为了演示同时使用Hibernate和Mybatis操作数据库,需要提供一个实体类User.java,代码如下所示:

说明:

在本次演示的项目中,使用到了Lombok插件,它可以让开发者减少模板代码的书写,提高开发速度。@Data注解可以自动生成类属性的getter、setter和toString方法。@NoArgsConstructor会自动为类生成无参构造函数,@AllArgsConstructor则会生成带全部属性的构造函数。

2-3、定义数据持久化接口

​ 在本次课程中,将使用Spring Data JPA来完成写操作,如新增、修改、删除;使用Mybatis来完成读操作,如根据用户ID查询、查询所有的用户等。Spring Data JPA和MyBatis的持久化接口都位于com.ramostear.hm.orm包下,Spring Data JPA的持久化接口相对比较简单,之间继承JpaRepository类即可,代码如下:

说明:因为JPA只负责写操作,所以直接继承并使用JpaRepository提供的API即可,不需要额外的定义其他的接口方法。

​ 下面是Mybatis的映射接口,定义了两个方法:根据ID查询用户信息和查询所有的用户信息。代码如下所示:

说明:

此接口需要注意的地方是@Component@Mapper注解,@Component注解标注此接口后,Spring会自动扫描并配置此类;@Mapper注解是把这个mapper的DAO交由Spring进行管理。

定义完Mybatis 映射接口后,需要提供一个进行数据库查询的xml配置文件。该文件位于resources/mapper文件夹中,UserMapper.xml完整代码如下:

2-4、定义UserService

​ 在UserService接口中,提供三个方法:保存用户信息、根据ID查询用户、查询所有的用户。UserService接口代码如下:

在UserService接口的实现类中,需要同时注入UserRepository和UserMapper两个依赖。我们使用构造函数的方式来注入这两个依赖。代码如下:

说明:

@Transactional注解用于设置每个方法的事务控制方式。@Service注解声明该类是一个服务提供类,且设置了该类被Spring初始化时Bean对象的名称为“userService”。

2-5、定义控制器

​ 最后,提供一个控制器,用于处理客户端的相关请求。在控制器中,提供了三个请求处理方法,分别处理客户端新增用户、根据ID查询用户和查询所有用户的请求。控制器代码如下:

说明:

在本次教程中,为了编码IDEA报警告,所有的依赖注入都采用构造函数的方式注入相关的依赖。

三、配置Hibernate和Mybatis

​ 网络上有很多关于在Spring Boot项目中配置Hibernate和Mybatis的教程,但同时配置Hibernate和Mybatis的文章很少,有一些是通过Java代码的方式对这两个ORM框架进行配置,采用的是多数据源的方法来整合两个框架。其实整合这两个框架没有想象中的那么难,只需要在application.yml或者application.properties配置文件中加入几行代码,就可以完成两个框架的整合。以application.yml配置文件为例,配置代码如下:

是不是很简单,并为没有太多复杂的配置,这是一种较为简单的整合方式。Hibernate和Mybatis共用一个数据源,如果是JPA的忠实粉丝,现在想要使用Mybatis,只需要额外加入mybatis的配置即可。

四、测试

​ 通过以上的几个步骤,整个项目已经搭建完毕,接下来将使用Postman测试工具对Controller的三个方法进行测试,验证两个ORM框架在同一个项目中是否能共存。

​ 首先测试 POST http://localhost/users ,验证Hibernate是否能够成功将用户信息持久化。打开Postman工具,在地址栏输入http://localhost/users请求地址,请求方式选择POST,在Body栏输入如下的信息:

{
    "username":"谭朝红",
    "alias":"ramostear",
    "age":28
}

点击“Send”按钮发送请求,观察服务端响应信息,测试结果如下图所示:

可以看到,服务端成功返回用户信息,且用户ID=3。接下来,我们请求 GET http://localhost/users/3 ,验证Mybatis是否能够成功查询出用户信息,测试结果如下:

通过测试,服务端成功返回了用户ID=3的用户信息:

{
  "id": 3,
  "username": "谭朝红",
  "alias": "ramostear",
  "age": 28
}

由此证明,在同一个项目中,Hibernate和Mybatis均能正常工作,整合方案有效,解决了在同一项目中Hibernate与Mybatis共存的问题。

五、总结

​ 本次课程验证了同时使用Hibernate和Mybatis两个ORM框架的方案可行,且采用了一种比较简单的方式来整合两个框架,摒弃了多数据源的复杂配置,快速实现两个框架并用的需求。

​ 在一个项目中同时使用两个ORM框架有没有实际的意义呢?我的答案是肯定的。同时使用两个ORM框架,两者之间可以相互弥补自身的不足,以达到灵活性和便捷性同时兼顾,另外一方面,在单独使用Mybatis时,开发者需要手动或者借助其他的工具生成数据库表信息,而采用本文的整合方案,Mybatis可以借助JPA自动生成数据库表的能力,从而简化使用Mybatis的步骤。最后,对于一些读多于写的系统,完全可以将这两个框架同时使用,写操作少的模块,可以使用Spring Data JPA快速完成相关功能的实现,对于读操作部分,则可以利用Mybatis来优化查询语句。两者之间的优势互补,能进一步的提升开发效率和系统性能。

本次分享内容的源代码仓库地址:

https://github.com/ramostear/orm-hm.git

(七讲)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不常用的一些注解,将在下一次分享中进行补充和说明。

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上获取本次内容的全部源码。