SpringBoot探索04 – Java Headless Mode

引入

在springboot的启动代码中看到了下面这一句:

先不谈这段代码的作用,只说这里的写法就有很多可以借鉴的地方:

  1. 定义了有意义的常量
  2. 在set之前先get以避免覆盖用户自定义配置
  3. 允许用户在程序中配置headless项,所以有了this.headless变量
  4. Boolean.toString这样的写法也是极好的,起到了规范统一的作用

从spring的代码中就可以看到设计者对我们这些普通Developer的友好。

忽略掉这些有价值(但有可能显得啰嗦)的内容,可以提炼出这行代码:

这行代码的作用是:声明当前应用使用headless模式。

说明

Headless模式是应用的一种配置模式。在服务器可能缺少显示设备、键盘、鼠标等外设的情况下可以使用这种模式。

java.awt.toolkitjava.awt.graphicsenvironment类中的很多方法,除了对字体、图形和打印的操作外还需要调用显示器、键盘和鼠标等设备。但是有一些类,比如CanvasPanel,是可以在headless模式下执行的。

在headless模式下,(awt)应用程序可以执行如下操作:

  1. 创建轻量级组件
  2. 收集关于可用的字体、字体指标和字体设置的信息
  3. 设置颜色来渲染准备图片
  4. 创造和获取图像,为渲染准备图片
  5. 使用java.awt.PrintJob,java.awt.print.*,和javax.print.*类里的打印操作

简单来说,headless模式是一种声明:现在不要指望硬件的支持了,只能使用系统的运算能力来做些非可视化的处理工作。

除此之外,应该是没有其他作用了。

重点

关于这个配置在性能上的影响:

  1. 如果应用不需要任何head,那么有无这个配置没有任何影响;
  2. 如果应用有弹出窗口之类的操作,那么在headless模式下这种操作会被阻止。

总之,这是一项提示类的配置。

参考

End!

springboot学习01 – 自定义自动配置

概述

SpringBoot提供了自动配置能力。通过自动配置我们可以非常方便地启动相关的服务。

SpringBoot自动配置有两个核心模块:

  1. 自动配置模块(autoconfigure):主要负责读取配置相关的内容,并尝试启动服务;
  2. 启动模块(starter):提供具体的服务能力以及所有相关的依赖。

通常这两个模块是分开的。比如使用Caffeine缓存,缓存自动配置在一个独立的包中,Caffine缓存支持又是一个独立的包。如果不想把配置和能力分开,这两个模块也可以放在一起。

创建

接下来尝试创建一个自启动配置组件:hello-spring-boot-starter。这个组件功能很简单,就是在服务启动后自动打印一行“Hello xxx!”。

命名

springboot官方的自动配置包和自启动包都是以“spring-boot-”开头的。但是springboot不建议第三方开发者这样命名,应该是担心和官方支持出现冲突——即使现在没有冲突,未必以后官方不会推出相同的服务。即使使用了不同的groupId也仍然不建议这么做。

官方的建议是将具体的名称放“spring-boot”在前面。比如,我们要创建一个名为hello的自动配置组件,那么自动配置模块包可以命名为“hello-spring-boot-autoconfigure”,自启动模块包可以命名为“hello-spring-boot-starter”。如果要把这两个模块合并起来,那么包名是“hello-spring-boot-starter”。

配置项

如果自定义的自动配置组件提供了配置项,那么需要为配置项提供一个独立的名称空间。注意,尽量不要和spring-boot默认提供的名称空间(servermanagementspring等等)产生冲突。建议使用组件的关键字作为名称空间,比如我的组件名称是hello,那么名称空间就是hello。组件只有一个配置项name,最终yaml格式的配置就是:

然后需要为这些配置项创建一个配置描述类来表示记录配置信息,如:

配置描述类中需要包含全部配置项,以确保其生效。

下面是一些SpringBoot内部的配置类定义的准则:

  • 不以“the”或“a”开头
  • boolean类型的配置项,以“weather”或“enable”开头
  • 对于简单集合类型,尽量使用逗号分隔的字符串形式
  • 对于毫秒级的时间,使用java.time.Duration替换long类型
  • 如果时间不是毫秒级的,需要在meta-data中提供必要的提示,如:”If a duration suffix is not specified, seconds will be used”
  • 提供默认值要谨慎,如果默认值不是在运行时必需的就不要设置

为了能让idea等开发工具识别我们提供的配置项,还需要提供一个meta-data文件META-INF/spring-configuration-metadata.json

SpringBoot提供了annotationProcessor来辅助生成meta-data文件。我们只需要添加如下依赖即可:

不过annotationProcessor对集合类型支持得不是很好,使用的时候要慎重。

此外,annotationProcessor还能生成一个配置项元数据文件META-INF/spring-autoconfigure-metadata.properties。当存在这个文件的时候,就可以了用来对配置项进行初步的过滤,有助于减少启动耗时。

配置类

自动配置组件的配置类就是一个标准的配置类,所以它也需要使用@Configuration注解。下面是一个配置类的示例:

示例代码中通过@EnableConfigurationProperties注解引入了配置描述类。还提供了相应的构造器以便注入配置信息。

此外这里还装模作样的使用了条件注解@ConditionalOnClassSystem.class是JRE的标配,因此这行注解实际上是没有任何作用的,在这里只是做个演示。条件注解通常多出现在自动配置中,以保证在满足设定条件后自动配置才能生效。关于条件注解前段时间写过一篇文:《SpringBoot条件注解》。这里就不重复啰嗦了。

因为自动配置组件要求放在独立的包中,而且包路径不能和应用包路径重合,所以需要提供一些帮助才能让SpringBoot识别我们提供的自动配置信息——这里是META-INF/spring.factories文件。SpringBoot会检查jar包中是否存在META-INF/spring.factories文件,并尝试读取解析文件中配置的类信息。关于读取解析spring.factories文件的过程在之前也有介绍过:《SpringBoot启动过程之getSpringFactoriesInstances》。

下面是一个spring.factories文件的示例:

应该可以看出spring.factories实际上就是一个典型的.properties文件。

注意:SpringBoot自动配置组件只能通过这种形式加载。在定义组件包路径的时候就需要注意包路径不能是Spring componentScan的目标。同时,在自定义组件类中也不能使用componentScan来获取其它的组件。如有必要,可以使用@Import注解代替(可以参考SpringBoot探索01-@Import注解)。

如果多个配置类之间存在先后顺序的话,可以使用@AutoConfigureAfter@AutoConfigureBefore注解来确定顺序。比如,如果定义的是web相关的配置类,那么这个配置类可能就需要在WebMvcAutoConfiguration之后生效。

如果想保证多个配置类的加载顺序,又不想让配置类之间存在显式的关联,那么可以使用@AutoConfigureOrder注解。这个注解和普通的@Order注解的作用是一样的,但是只能用于自动配置类。

启动类

关于启动类的作用,根据名称就可以猜出来:主要是负责组件服务的启动。前面配置类的示例代码中就有几行启动类相关的内容:

其中的HelloStarter就是一个启动类。在配置类中创建注入了HelloStarter的实例。具体的服务逻辑还是需要在启动类HelloStarter中完成。

很多时候,启动模块和配置模块是分别放在独立的包中的,不过这里实现的功能比较简单,且无其它的依赖,所以就干脆放在一个jar中了。

看下HelloStarter的实现:

只是在HelloStarter实例注入完成后执行了一行输出语句。可以说是极为简单了。

测试

自动配置可能会被多种因素影响:

  • 用户自定义配置(Bean定义和自定义环境参数)
  • 条件分析(是否存在某个类或某个依赖)
  • 其它约束

执行具体测试的时候就需要为每种情形定义一个ApplicationContext。这种情况下,使用ApplicationContextRunner事情会变得很简单。

ApplicationContextRunner主要被用来搜集基础的、通用的配置信息。通常是作为成员变量定义在测试类中,如下例:

如果定义了多个配置类,不用在测试中刻意控制声明的顺序,SpringBoot会保证它们的触发顺序和正常启动时一致。

每个测试都可以使用contextRunner执行一类测试案例。在下面的示例代码中定义了一个新的配置类HelloConfiguration ,但是在新的配置类中创建的HelloStarter Bean并不能覆盖自动配置中创建的同类的Bean:

因为没有提供配置信息,所以自动配置中创建的HelloStarter Bean的name值是null。

在测试中使用了Assert4J来进行值的比较。

还可以自定义配置参数,如下:

contextRunne还可以展示ConditionEvaluationReport,即条件注解检查过程日志。日志的级别可以设置为INFODEBUG,下面的测试代码使用了ConditionEvaluationReportLoggingListener来打印条件注解检查过程日志:

借助于SpringBoot提供的FilteredClassLoader,我们还能够验证在某个类或某个jar不存在的情况下自动配置如何处理。在下面的代码中,我们在类加载器中排除掉了HelloStarter.class,这样自动配置就不会生效:

另外,如果我们需要的是Servlet或Reactive web应用Context,可以使用WebApplicationContextRunner或者ReactiveWebApplicationContextRunner

其它

这里的测试代码已经上传到了GitHub,见: GitHub/zhyea

不过这个自启动组件实现的功能太过简单,如果想深入了解下,可以参考SpringBoot官方提供的自启动配置。我自己还写过一个简易的kafka自启动组件,如果有兴趣也可以参考下。

还有一个自动配置演示项目也不错,在git:spring-boot-master-auto-configuration

参考

End!

SpringBoot探索03 – 条件注解

SpringBoot中提供了一系列的条件注解(@Conditional)来实现对@Bean@Configuration等实例的创建进行约束。这些注解包括:

  • Class Conditions,类条件约束
  • Bean Conditions,Bean条件约束
  • Property Conditions,配置项条件约束
  • Resource Conditions,文件条件约束
  • Web Application Conditions,Web应用条件约束
  • SpEL Expression Conditions,SpEL表达式条件约束

类条件约束

类条件注解有两个:@ConditionalOnClass@ConditionalOnMissingClass

类条件注解通常用于自动化配置管理中,作用是根据某个(或某些)类的存在与否来决定一个@Configuration注解的类是否需要被解析并注入。

因为自动化配置类是在一个独立的jar包中,而且SpringBoot是使用ASM执行类条件注解的解析,也就是说相关的@Configuration类不一定会被加载,所以@ConditionalOnClass@ConditionalOnMissingClassvalue属性可以是一个类。

当然,如果存在顾虑的话,也可以使用全限定类名字符串设置name属性的值。尤其是在应用内部使用类条件约束注解的时候,就只能使用name属性了。

当类条件约束注解用于@Bean注解的方法时存在一种特殊的情形:即约束注解的value值就是方法的返回值的类型。因为在注解生效前,JVM就已经加载了这个类并开始尝试处理方法的引用,此时如果相关的类不存在就会执行失败。

为了解决这种问题,可以将一个@Configuration类拆分开来,如下例所示:

类条件约束注解会影响@Configuration类的创建。

Bean条件约束

Bean条件注解也有两个:@ConditionalOnBean@ConditionalOnMissingBean,作用是根据当前容器中是否存在指定的Bean来决定是否进行注入。

可以使用注解的value属性来按类指定Bean,也可以使用name属性按名称指定Bean。search属性可以用来限制在容器的哪些层级上搜索Bean。

当把Bean条件约束注解用于@Bean方法上时,type属性的值默认就是方法返回值的类型,看个例子:

如上面的示例代码:如果如果容器中目前没有MyService类的实例,这里就会创建一个myService 实例并注入。

因为是根据当前已经加载过的类进行判断的,所以在使用Bean条件约束注解时,需要注意相关Bean的加载次序。

在自动化配置类中使用时,则无需考虑这些事情,因为自动化配置类中的Bean一定会在用户自定义的Bean都注入完成后才会被加载。

@ConditionalOnBean@ConditionalOnMissingBean并不会阻止@Configuration类的创建,只会对@Configuration类实例的注入产生影响。

配置项条件约束

配置项条件注解@ConditionalOnProperty会根据SpringEnvironment的属性(或者说是配置信息)进行约束。

使用注解的prefixname属性可以指定要检查的配置项。

可以根据havingValuematchIfMissing属性来设置配置项的预期值。

如果不做任何设置的话,只要配置项的值存在且不为false就一定能通过校验。

另外,如果将配置视为是树结构的话,那么指定的配置项一定是叶子结点,如下例:

如果指定的配置项是kafka.commonkafka.consumers,校验的返回结果一定是false,只有指定kafka.common.bootstrap_serverskafka.consumers[0].topics才有意义。

资源条件约束

资源条件注解@ConditionalOnResource会根据指定的资源文件是否存在来进行校验。资源文件名如:file:/home/user/test.dat

Web应用条件约束

Web应用条件注解同样有两个:@ConditionalOnWebApplication@ConditionalOnNotWebApplication

这类注解会校验当前应用是否是web应用。

如当前应用是一个servlet类型的web应用,那么判断依据就会是是否存在WebApplicationContext容器,有没有定义session scope,或者是否存在ConfigurableWebEnvironment实例。

而react类型的的web应用判断依据则是是否有使用ReactiveWebApplicationContext容器,或者是否存在ConfigurableReactiveWebEnvironment实例。

SpEL表达式条件约束

SpEL表达式条件注解@ConditionalOnExpression会根据SpEL表达式进行校验。

关于SpEL表达式可以参考前文《使用SpEL表达式》。

End!

SpringBoot探索02 – 启动过程之getSpringFactoriesInstances

最近计划整理下SpringBoot的启动过程以及API请求返回过程,以便支持后续的部分内容,也方便实现一周一文的计划。

先做SpringBoot启动流程分析。

初见

在SpringBoot启动代码的开头部分可以看到如下的内容:

比较显眼的是getSpringFactoriesInstances方法——这个方法出现了两次,而且以后还会继续出现。

从代码上看来,这个方法的作用应该就是获取指定类型的实例集合。

具体如何,还得继续走走看。从这里一步步走下去,可以看到一个重载的方法,这个方法的内容比较实在:

关键是方法体中的三四两行。第三行应该是获取了名称集合,第四行则应是获取了名称对应的实例集合。

问名

首先看看SpringFactoriesLoader.loadFactoryNames方法做了哪些事情。这个方法的核心内容在于它调用的loadSpringFactories方法。loadSpringFactories的方法体稍稍长了一些,沾出来显得太啰嗦,所以仅在下面介绍下这个方法具体做了那些事情。

这个类的作用是根据类加载器获取类名称集合(map结构,接口->实现类集合),具体流程如下:

  1. 首先尝试从内存缓存中获取,如获取到就立即返回,没有则继续下面内容;
  2. 从类加载路径(的jar文件)中获取全部的spring.factories文件路径;
  3. 循环遍历读取这些文件中的键值对(K->List[V]);
  4. 将读取内容放入内存缓存,下次再调用这个方法时会优先从缓存中获取;
  5. 返回读取到的全部键值对集合(Map[K,List[V]]结构)。

简单做些解释:

spring.factories文件本质上是一个.properties文件,也就是说,它的内容是键值对集合,下面是一段示例:

这个片段中有三个键值对。根据后面的代码可知spring.factories文件中每个键值对的key是一个接口的名称,value则是实现类名称的集合(以“,”分隔)。

spring.factories文件中的内容会被读取到一个(K->List[V])结构的Map中。最终全部这些内容又会以类加载器实例为key保存在内存缓存中。

然后,SpringFactoriesLoader.loadFactoryNames方法就可以根据接口的名称获取到接口的实现类的名称集合了。

得意

在得到类名称集合后就是根据类名构建类对应的实例了。这是createSpringFactoriesInstances方法做的事情。这个方法不长,可以粘出来看看:

方法的内容很清晰也很简单:

  1. 根据方法名获取Class实例;
  2. 通过Class实例获取构造器;
  3. 使用构造器构建类实例。

很普通的一个反射过程。期间有一个校验是判断Class是不是指定接口的子类;还有就是在BeanUtils.instantiateClass中创建实例的时候修改了构造器的可见性范围,并提供了对Kotlin的支持。

这块儿内容大体上就这样了。至于这里获取到了哪些ApplicationContextInitializerApplicationListener的实例,以及它们的作用,在后续的部分会陆续提到。

End!

使用maven构建scala项目

构建及编译scala工程我使用过三种工具:sbt、gradle和maven。

感谢GFW,sbt的速度慢得让人心碎。即使使用诸如广谈公服等加速方案,也还是会遇到各种各样的问题(比如时不时的lock住)。所以pass。

gradle是最让人惊喜的一个方案,简洁便利上超过sbt,更远胜maven,执行速度也没短板。只有下载源码这问题让我头疼,但也是小问题。然而,关键是公司发布环境不支持gradle,所以一切休谈。

最后是maven。有人说比起gradle来,maven是老人脸上的皱纹。意思是maven的配置非常繁琐,显得过时了。比喻非常形象。但是老人的好处就是经验丰富,很多问题都能很快找到解决方案,虽然略嫌啰嗦但却让人放心。所以最终还是使用maven管理和构建生产环境中的scala项目。

介绍下如何使用maven构建scala工程。

编译

下面是一个示例pom文件:

scala环境最重要的三个依赖:scala-compiler、scala-reflect和scala-library都可以通过scala-compiler及其间接依赖获取到。

一些依赖可能会间接引用不同版本的scala,所以在<dependencyManagement>中做了下scala版本的统一。

<build>中使用了maven-scala-plugin插件来执行scala的编译,不然在测试执行时会提示“错误: 找不到或无法加载主类 chobit.MyDriver”这样的错误。

打包

如果有打包需求的话建议使用assembly插件执行打包。assembly支持的打包方案非常丰富,算是同类产品里最好的。

看个打包的示例:

首先添加assembly插件

这里的配置设置了两个重要信息:

  • 打包配置文件的具体位置:src/main/assembly.xml;
  • 在package阶段自动执行assembly插件;如果没有指明,就得执行mvn assembly:assembly命令来进行打包。

打包配置文件的详情:

配置文件中的注释应该已经足够说明用法了。

这里的代码是从一个spark工程上copy来的,所以会有将工程打包成zip文件,又包含shell脚本这样的内容。

在windows主机上执行mvn clean package命令时,可能会遇到这样的错误:

解决方案有两个:

  • 如果要把文件放在zip包下的根目录,可以直接将<outputDirectory>标签闭合,如: <outputDirectory />
  • 如果要把文件放在zip包下的其他目录,可以使用相对路径,如:<outputDirectory>./shell</outputDirectory>

示例代码见: GitHub / Zhyea

参考文档

springboot入门08 – 创建非web项目

概述

从开始使用SpringBoot到现在,一直都是在用SpringBoot开发web服务(API服务)。直到前段时间,需要帮其他组的同事写一个非web的简单服务时,才想到Springboot是不是也支持非web项目。

答案是肯定的:spring诞生之初就不是为web项目定制的,springBoot无非是在spring核心项目的基础上添加了一些方便开发者使用的组件,所以使用springboot开发非web项目也是可行的。

首先我们要弄明白常用的web项目和非web项目的区别在哪儿?私以为是服务启动和执行逻辑触发的方式:

  • web项目需要依赖web容器来启动,通过http请求来触发相关的服务;
  • 非web项目则不需要依赖web容器来启动,它可以是自启动的;
  • 非web项的服务通常是主动触发的或者通过非http的方式被动触发的。

接下来详细介绍下如何使用springboot构建非web项目。

依赖

创建web项目通常需要使用的依赖是spring-boot-starter-web

这个依赖间接引用了tomcat,spring-webmvc,spring-context,json-starter等依赖。这些在非web项目里基本上都用不到,非web项目可以直接使用spring-boot-starter

spring-boot-starter间接引用了spring-core、yaml、auto-configure等依赖,已经足够用来创建一个普通的spring项目了。

如果有特殊情况(说实话,我强烈怀疑)非要使用spring-boot-starter-web依赖来构建非web项目也不是不行,只需要加一些配置来避免启动web容器就行:

或者在启动类中添加web配置并设置为NONE

启动

springboot非web项目的启动类定义和web项目并无不同。如下:

但是执行启动类中的main方法以后呢?如果是web项目,在启动后等待HTTP请求调用就行了。不过这里是非web项目,我们得想办法执行我们定义好的业务逻辑。接下来按常见的业务逻辑特征分别介绍下。

1. 任务需要定时执行

这种情况需要配置定时任务。SpringBoot对定时任务的支持还算可以。前段时间我写过关于《SpringBoot定时任务配置》这样的文章,有需要可以参考下,这里就不重复介绍了。

2. 在某个类的实例注入后就立即执行

这种场景说实话不多见。通常是需要该类实现InitializingBean接口,并在afterPropertiesSet方法中实现相关的逻辑。如下:

还有一种方式是使用java提供的@PostConstruct注解:

建议最好使用前者。

3. 使用SpringBoot的提供的主动执行接口

springboot提供了两个接口ApplicationRunnerCommandLineRunner来支持主动执行业务逻辑。

比如我们可以直接实现ApplicationRunner接口,并在run方法中添加业务逻辑:

CommandLineRunner接口和ApplicationRunner接口的差别不大。从执行时机还是调用过程上来着,这两者几乎都是一样的。这唯一的区别在于他们提供的run方法的args参数类型上:

  • ApplicationRunner的args参数是ApplicationArguments类型,对原始参数做了一层封装;
  • CommandLineRunner的args参数是字符串类型,取的是启动类收到的原始参数。

这种差别源于SpringBoot对二者的定位上:

ApplicationRunner适用于启动即执行的场景,只需要读取一次参数信息即可。它的参数通常是“option=value”这种结构的,如:“–foo=bar –foo=baz” 。ApplicationArguments中封装了一些对这种参数进行处理的方法,以便开发使用。

CommandLineRunner从名字上看就是用来做命令行交互用的,所以它这里直接取了原始参数,看一个使用示例:

如果在执行命令行交互之前也需要读取解析传入的参数,那么这里的MyRunner3类完全也可以实现ApplicationRunner接口。二者的差别几乎可以忽略。

在一个应用里面可以有多个ApplicationRunnerCommandLineRunner的实现。要调整两者的实现类之间的执行顺序可以使用@Order注解。

就这样。这一节虽然介绍的是如何创建启动非web项目,但是在web项目中也会有主动触发一些执行逻辑的需要,上面介绍的这些方案也是完全可用的。

示例代码已上传到了 GitHub/Zhyea ,有需要可自行查阅。

End!

spring feign https配置

前两天需要通过springboot-feign来调用一个https的外部服务接口,因此要实现feign-client的SSL设置。

feign执行http请求通常会调用feign.Client接口的实现。这个接口的默认实现类Default提供了添加SSL配置的构造器:

简单介绍下构造器参数中用到的SSL相关的接口:

  • SSLSocketFactory:SSLSocket工厂;SSLSocket扩展了Socket并提供使用SSL或TLS协议的安全套接字。这种套接字是正常的流套接字,但是它们在基础网络传输协议(如TCP)上添加了安全保护层。
  • HostnameVerifier:实现主机名验证功能;在握手期间,如果URL的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口实现程序来确定是否应该允许此连接,如果回调内实现不恰当,默认接受所有域名,则有安全风险。

默认使用的Client实例的两个构造参数的值都是null。我们需要在配置类中创建使用SSL的Client实例,这里参考了默认的配置类FeignClientsConfiguration

代码中使用了SSLContext的getSocketFactory方法来创建 SSLSocketFactory对象。在创建SSLContext对象时,通过(chain, authType) -> true指定了SSL信任策略,即:不做任何处理,信任全部请求。

另一个HostnameVerifier类型的参数使用了一个实现类NoopHostnameVerifier的实例,作用是关闭主机名验证功能,并且永远不会抛出SSLException。

最后将配置添加到FeignClient注解上即可:

如代码中所示可以为configuration项一次指定多个配置类。这里的第一个配置类是spring默认提供的配置类。

另外,如需要创建Client的Bean实例,在使用Eureka的情况下,应选择LoadBalancerFeignClient的对象,不然会产生诸如无法解析host之类的问题。

就这样了。

springboot入门07 – 配置文件详解

概述

这一篇主要介绍下SpringBoot配置相关的内容。

通过配置文件,我们可以做到如下事情:

  • 修改springboot的默认配置项
  • 添加自定义配置项

SpringBoot使用全局的配置文件,主配置文件名是固定的:application.xxx

springboot的配置文件有多种格式可选,如:.properties,.xml,.yml。

我个人比较喜欢yaml的简洁,所以推荐使用yaml。

yaml的基本语法

下面介绍些yaml的语法

  1. 使用空格缩进来表示结构,相同层级的数据其缩进的长度必定相同:

  1. 支持使用注解,但只支持使用单行注解,注解以“#”号开头:

  1. 支持直接类型或字符串的列表或数组结构,有两种表达形式:
  • 每个列表成员占一行,以横杠和空格(- )开头
  • 所有列表成员在同一行,被方括号([])包围,列表成员间以逗号和空格分隔

  1. 也支持Map结构,同样也有两种表达形式:
  • 每个KV对占一行,键值用冒号分隔
  • 所有KV对在同一行,被大括号({})包围, KV对之间用逗号和空格分隔,键值用冒号分隔

  1. 通常字符串不需要使用引号,但必要时可以使用单引号(’)或双引号(”),比如需要使用转义字符时

  1. 可以使用两个叹号(!!)来强制指定格式(这个感觉用处不大,但是万一用到了呢)

  1. 内容可以重复使用,原始位置的内容用(&标记名)来标记,在引用位置用(*标记名)来引用

  1. 如果想在一个文件里面配置不同的profile,可以使用三个横杠“—”将yaml文件分割成多个独立区域,每个区域被视为一个独立的文件。三个点号“…”用来标识文件结尾。

如示例代码,在一个yaml文件中存在多个独立区域时,使用spring.profiles属性来标识配置区域的profile信息。

多环境配置

在主配置文件application.yml之外,还可以有多个外部配置文件,其名称格式固定为application-{profile}.yml

SpringBoot会根据主配置文件中的spring.profiles.active参数来决定加载哪个配置文件。

通过这种方式SpringBoot实现了支持多环境配置,从而能够在不同环境下(开发、测试、生产)进行配置参数的切换。

主配置文件中的spring.profiles.active属性还可以有多个值,每个值以逗号进行分割:

这种方式允许我们为不同需求创建不同的配置文件。SpringBoot会依序加载每个值指向的配置文件。

配置文件的加载顺序是主配置文件application.yml,spring.profiles.active指定的外部配置文件。如有冲突项,后面加载的配置会覆盖前面的。

如果多个外部配置文件有相同的配置项,建议在主配置文件中进行配置。

spring.profiles.active指定的值无法与任何外部文件匹配,SpringBoot会尝试加载application-default.yml文件。

结合@Profile实现内部实例的动态选择

spring.profiles.active参数不仅可以指明要使用哪个外部配置文件,也可以控制内部实例的选择。这需要通过配合@Profile注解实现。

如下例:

如果没有@Profile注解,在启动的时候SpringBoot就会报错,因为为一个Worker类提供了两个实例。但是现在,SpringBoot可以根据spring.profiles.active指明的profile和@Profile注解来选择注入哪个实例。

@Profile注解可以用于含有@Component@Configuration@Bean等注解的类及其方法。

@Profile注解也可以有多个值,如下:

这样,在profile是dev或test时,这个Bean都可以被成功注入。

配置项获取

使用SpringBoot的配置项有两种方式:通过@Value注解或通过@ConfigurationProperties注解。这两种方式各有长处,可根据进行选择。

通过@Value注解获取

@Value注解适用于单个配置项的获取,比如我们可以获取当前的外部配置文件的profile:

@Value注解的一个优势是可以使用SPEL表达式(关于SPEL表达式,我有整理过:《SpringBoot SpEL表达式》),如下是一个非常粗略的SPEL表达式:

@Value注解的不足之处在于只能获取直接类型的值或字符串,不能映射为复杂类型。

通过@PropertiesConfiguration注解获取

@PropertiesConfiguration注解的主要特点是可以将配置映射为对象。因此平时还是通过这种方式获取SpringBoot配置为主。

@PropertiesConfiguration注解可应用于类或成员方法。

如下是将应用于类的示例:

从示例代码中可以看到配置类需要有@Component注解或@Configuration注解,以便完成注入。

此外,在使用@ConfigurationProperties注解的时候需要传入注入配置属性的前缀,以便获得配置项的范围。

在成员方法上使用@PropertiesConfiguration注解则是一种更为灵活的方式,能够充分弥补@Value注解的不足,示例如下:

注意这里还需要用到@Bean注解,以便将读取到的配置信息注入到实例上。不过不能通过getWorker()方法调用,否则获取的还是一个只有默认值的对象。要使用该实例还是需要通过@Autowired注解来注入。

最后,可以看下我在GITHUB上的示例应用,在里面我尝试了SpringBoot配置的各种应用实践。

就这样。

springboot入门06 – 接口单元测试方案

以前写过关于springboot Controller层单元测试的系列文章(Spring Controller层测试)。但是那几篇文章还是更偏方法论一些,不能直接拿来使用。所以有了这偏内容,目的主要是记录下平时使用的Controller层单元测试方案。

在这里先定义一个普通的api接口类WorkerApi

这个接口里的4个方法覆盖了平时常用的四种Http请求方案,并将请求结果用一个统一的ResultWrapper类进行了封装(关于如何封装请求结果请参考上一篇文章SpringBoot Controller返回值封装)。

然后是单元测试方案。这里有两个超类:TestBaseApiTestBase。前者用来对普通的注入实例进行测试,后者主要用来对Api接口进行测试。

TestCase类内容如下:

通过类内容可以看到,在测试中会启动一个内嵌的WebServer来加载Spring Context。这样的测试比较重一些,不过也和实际使用场景更一致一些。

ApiTestBase类继承了TestBase类。在这个类里引用了TestRestTemplate的实例执行具体的接口调用,此外还定义了一些公用方法来减少使用时的代码量:

这里为每种请求都定义了两个方法,以根据需要返回不同形式的返回值。

看下是怎样使用的:

有时候还会有在执行测试前添加虚拟机环境参数的需要,此时可以视情况在TaseBaseApiTestBase或测试类中添加静态代码块并设置虚拟机参数:

就这样。代码已经传到了GitHub上,有需要请自行查阅: GitHub / zhyea

springboot入门05 – 包装SpringBoot Controller返回值

一个项目使用了SpringBoot,需要对Controller的返回值进行二次包装。包装类结构大致如下:

通过查找资料,找到了两种封装方式。

方法一

第一种方式是替换掉RequestResponseBodyMethodProcessor,这需要使用一个MethodReturnValueHandler的装饰类:

在装饰类中使用一个Result类的实例替换了returnValue。而后在InitializingBean中基于原来的RequestResponseBodyMethodProcessor的实例创建一个ResponseBodyWrapHandler的实例来完成替换:

使用ResponseBodyWrapFactoryBean,完成afterProperties方法的调用,只需要创建一个ResponseBodyWrapFactoryBean的实例即可:

这行代码可以放在启动类中。

方法二

第二种方式基于ControllerAdvice和HttpMessageConverter实现。

首先用一个ResponseBodyAdvice类的实现包装Controller的返回值:

如果Controller类的返回值没有String类型的,仅有上面这个类就够了。如果有String类型的返回值,就有可能遇到类型不匹配的问题。HttpMessageConverter是根据Controller的原始返回值类型进行处理的,而我们在ResponseAdvisor中改变了返回值的类型。如果HttpMessageConverter处理的目标类型是Object还好说,如果是其它类型就会出现问题,其中最容易出现问题的就是String类型,因为在所有的HttpMessageConverter实例集合中,StringHttpMessageConverter要比其它的Converter排得靠前一些。我们需要尝试将处理Object类型的HttpMessageConverter放得靠前一些,这可以在一个Configuration类中完成:

在这个方案中,如需要对异常做些特别处理,还可以创建一个ExceptionAdvisor类来完成:

这样还可以根据异常类型来设置返回时的HttpStatus。

就这样。

有朋友在评论区指出问题了,做了些调整,也写了一份示例程序上传到CSDN。有兴趣的可以下载来看看。下载地址如下:点击此处下载

这是许久之前刚用springboot时写的,现在适应springboot的最新版本存在一些问题。所以稍稍重新调整了下,并将之加入到了最近正在尝试进行的一个系列里面。

新版本的代码可以在GitHub / zhyea上看到。

##########