疯狂springboot终极讲义笔记(二)
@springBootAppliction注解相当于以下3个注解的组合版
@EnableAutoConfiguration 启动自动配置
@SpringBootConfiguration 其底层就是@Configuration 该注解修饰的类作为java配置类
@ComponentScan 指定“零配置”时扫描哪些包及其以下包的bean
@Import 该注解显示指定springboot要加载的配置类 如 @Import(cn.fkjava.app.MyConfig.class)
@ComponentScan 该注解指定springboot扫描指定包及其子包下的所有配置类或者bean组件
@ImportResource("classpath:beans.xml") 加载类加载路径下beans.xml文件作为配置文件
@SpringBootApplication(scanBasePackages = {})其scanBasePackages属性本质就是@ComponentScan注解
如果没有合适的失败分析器来处理启动过程中出现的错误,但依然希望SpringBoot显示完整的错误报告,则可通过开启debug 属性,或者将ConditionEvaluationReportL oggingListener的日志级别设为DEBUG来实现。
在application.properties文件中添加如下配置,可以将ConditionEvaluationReportL oggingListener的日志级别设为DEBUG。
logging.level.org.springframework.boot.autoconfigure.logging=debug
开发者自定义失败分析器,需要继承 AbstractFailureAnalyzer
该泛型T代表失败分析器要处理的异常 继承这个类就必须实现analyze()方法该方法,返回FailureAnalysis对象代表该异常的分析结果。如果不想让该失败分析器分析该异常,而是希望将该异常留给下一个分析器进行分析,则可让该方法返回null
public FailureAnalysis(String description, String action, Throwable cause) {
this.description = description;
this.action = action;
this.cause = cause;
}
description 失败的描述信息
action 对该失败的修复建议
cause 导致失败的异常
public class MyAnalyzer extends AbstractFailureAnalyzer<BindException>
{
@Override
public FailureAnalysis analyze(Throwable rootFailure, BindException cause)
{
cause.printStackTrace();
return new FailureAnalysis("程序启动出错,程序绑定的端口被占用:"
+ cause.getMessage(),
"请先停止占用8080端口的程序后再运行本应用或使用" +
"server.port改变本应用的端口", cause);
}
}
自定义失败分析器需要在META-INF/spring.factories文件中注册,首先需要在resources目录下创建META-INF文件夹,然后在META-INF文件夹创建spring.factories文件里面内容为
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.crazyit.app.MyAnalyzer
在某些特殊需求下,Spring Boo也允许将取消Application Context的预初始化行为改为延迟初始化。要将Spring容器设为延迟初始化,有如下三种方式。
➢调用SpringApplication对象的setlazylnitialization(true)方法。
需要在启动类中配置如
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
var application=new SpringApplication(App.class);
// 设置启用延迟初始化
application.setLazyInitialization(true);
application.run(args);
}
}
➢调用SpringApplicationBuilder对象的lazyInitialization(true)方法。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
new SpringApplicationBuilder().
//加载父容器对应的配置类
sources(Parent.class).
//App类对应的配置作为子容器
child(App.class).
//启用延迟初始化
lazyInitialization(true).
//关闭Banner
bannerMode(Banner.Mode.OFF).run(args);
}
}
➢在application.properties文件中配置如下代码:
spring.main.lazy-initialization=true
启用延迟初始化之后,Spring容器将不会预初始化Bean,而是等到程序需要调用Bean的方法时才执行初始化,因此可降低SpringBoot应用的启动时间。
启用延迟初始化主要有如下缺点:
➢在Web应用中,很多与Web相关的Bean要等到HTTP请求第一次到来时才会初始化,因此会降低第次处理HTTP 请求的响应效率。
Bean错误被延迟发现。由于延迟初始化的缘故,有些Bean的配置可能存在错误,但应用启动时并不会报错,只有等到应用要用到这个配置错误的Bean时,程序才会报错,这样就延迟了发现错误的时机。
➢运行过程中的内存紧张。由于应用启动时并未初始化程序所需的全部Bean, 随着应用程序的运行,当应用中所有的Bean初始化完成后,可能就会造成运行时内存紧张。为了避免出现这个问题,可以在延迟初始化之前对JVM的堆内存进行微调。
一般来说,如果不是有非常特殊的原因,则不建议启用Spring Boot的延迟初始化。
设置springApplication
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
var application=new SpringApplication(App.class);
// 设置启用延迟初始化
application.setLazyInitialization(true);
application.run(args);
}
}
除了直接调用构造器来创建SpringApplication对象,Spring Boot还提供了SpringplicationBuilder工具类,通过该工具类能以流式API创建SpringApplication,并启动Spring Boot应用。
此外,SpringApplicationBuilder 还提供了如下方法来加载配置文件。
➢sources(Class>… sources):为应用添加配置类。
➢child(Class>… sources): 为当前容器添加子容器配置类。
➢parent(Class>… sources):为当前容器添加父容器配置类。
从这些方法可以看出,使用SpringApplicationBuilder可以构建ApplicationContext的层次结构(让Spring容器具有父子关系)。例如,如下代码片段使用SpringApplicationBuilder来启动SpringBoot应用。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
new SpringApplicationBuilder().
//加载父容器对应的配置类
sources(Parent.class).
//App类对应的配置作为子容器
child(App.class).
//启用延迟初始化
lazyInitialization(true).
//关闭Banner
bannerMode(Banner.Mode.OFF).run(args);
}
}
除了Spring 框架原有的容器事件( 如ContextRefreshedEvent),SpringApplication 还会额外触发一些新的事件,而且有些事件是在ApplicationContext被创建之前被触发的。因此,如果采用传统方式注册事件监听器(如通过
为了监听SpringApplication触发的事件,SpringAplication提供了如下方式来注册事件监听器。
➢调用 SpringApplication的addLiseners()方法或SpringApplicationBuider的listners()方法添加事件监听器。
➢使用META-INF/spring. factories 文件来配置事件监听器。在该文件中添加如下代码即可始册事件监听器:
org. springframework.context. ApplicationListener=org.crazyit.app.MyListener
Spring Boot应用启动时,SpringAplication 会依次触发如下事件:
➊在应用刚刚开始、还未进行任何处理之时,触发ApplicationStartingEvent事件,除非监和初始化器注册失败。
❷当ApplicationContext要使用的Environment 已经确定,但ApplicationContext 还未被创建之时,触发ApplicationEnvironmentPreparedEvent事件
③当 ApplicationContext准备完成且初始化器( ApplicationContextInitializer)已被调用,但未加载任何Bean定义之前,触发ApplicationContextInitializedEvent事件。
❹在所有Bean定义被加载之后、Spring容器刷新之前,触发ApplicationPreparedEvent事件。
⑤在Spring容器刷新之后,ApplicationRunner、 CommandLineRunner的接口方法被调用之前,触发ApplicationStartedEvent事件。
6 当应用程序的LivenessState 状态变成CORRECT, 表明应用程序进入live状态时,触发AvailabilityChangeEvent事件。
❼当所有ApplicationRunner、CommandLineRunner 的接口方法被调用完成后,触发ApplicationReadyEvent事件。
❽当应用程序的ReadinessState状态变成ACCEPTING TRAFFIC, 表明应用程序准备接受服
务请求时,触发AvailabilityChangeEvent事件。
❾如果启动遇到异常,则触发ApplicationFailedEvent事件。
上面列出的都是绑定到SpringApplication 的SpringApplicationEvent 事件。此外,还会在
ApplicationPreparedEvent事件与ApplicationStartedEvent事件之间触发如下两个容器事件:
➢Web 服务器初始化完成后触发WebServerInitializeEvent事件;如果使用Servlet Web服务器,则触发ServletWebServerInitializedEvent 事件;如果使用反应式Web服务器,则触发ReactiveWebServerInitializedEvent事件。
➢在刷新ApplicationContext时触发ContextRefreshedEvent 事件。
SpringBoot使用事件机制来处理应用初始化的各种任务,掌握SpringBoot事件机制有助于更好地理解SpringBoot应用的初始化过程,通过添加自定义的事件监听器,可以让程序在SpringBoot应用启动过程中对其进行某些特殊的定制。
需要说明的是,由于事件监听器采用同一个线程来执行,因此不应该在事件监听器中执行某些耗时的操作;如果确实需要完成某些耗时的操作,则建议使用ApplicationRunner 或CommandLineRunner接口。
由于上面这些事件都是通过Spring 框架的事件机制来发布的,这种机制会保证子容器中触发的事件也会自动触发父容器中的监听器。因此,如果应用程序采用了层次结构的容器(通过SpringAplicationBuilder启动Spring Boot应用时,可使用层次结构的容器), 那么事件监听器就有可能接收到同一类型事件的多个实例一它 们来自不同的容器。
为了让事件监听器能区分事件到底来自哪个容器,可以用事件监听器依赖注入的容器与事件来自的容器进行比较;为了将容器依赖注入事件监听器,可通过如下两种方式。
接口注入:让事件监听器实现ApplicationContextAware 接口,Spring容器将会被注入监听器。
➢普通注入:如果事件监听器是容器中的Bean, 则可直接使用@Autowired注解来完成依赖注入。
除了监听器,Spring Boot还提供了ApplicationContextInitializer 对Spring容器执行初始化。
ApplicationContextInitializer接口的实现类被称为“初始化器”,该接口的实现类必然实现其中的
initialize(C ctx)方法,通过该方法的参数也可对Spring容器进行设置。
有了ApplicationContextInitializer实现类之后,接下来可通过如下方式来注册初始化器。
➢调用SpringApplication 的addInitializers(方法或SpringApplicationBuilder的initializers()方法添加初始化器。
➢使用META-INF/spring. factories 文件来配置初始化器。在该文件中添加如下代码即可注册初始化器:
# 定义初始化器
org.springframework.context.ApplicationContextInitializer=\
org.crazyit.app.MyInitializer
如果想在Eavironment对象创建之后、Spring 容器刷新之前对Enironment对象(配置环境)进行定制,Spring Boot提供了配置环境后处理器一实现 EnironmentPostProcessor接口的类被称为“配置环境后处理器”。EnvironmentPostProcessor 实现类必然实现其中的postProcessEnvironmem(enviroment,application)方法,通过该方法的参数可对创建Spring容器的Environment进行额外的定制,比如将自定义的配置文件的属性加载到配置环境中。
注意:
可能有人会产生疑问:直接在Spring Boot配置类(用@SpringBootApplication修饰的类)中用@ValueSource注解加载自定义资源,然后将它添加到Environment中,这样不是更方便吗?请记住一点: @ValueSource 注解必须等到Spring容器刷新之后才会得到处理,此时去读取@ValueSource注解所加载的资源,再添加到Environment中已经太迟了。
有了ApplicationContextInitializer 实现类之后,可通过META-INF/spring. fctories文件来注册配置环境后处理器。例如如下代码:
# 定义配置环境后处理器
org.springframework.boot.env.EnvironmentPostProcessor=\
org.crazyit.app.FkEnvironmentPostProcessor
下面通过例子示范如何为SpringBoot应用添加监听器、初始化器和配置环境后处理器。按照惯例,首先创建一个Maven项目,然后用前面SpringBoot项目中的pom.xml文件替换本项目的pom.xml文件,本项目只保留spring-boot-starter web依赖即可。
本应用的主类很简单,依然是在main0方法中使用SpringApplication的rum0方法运行带@SpringBootApplication注解修饰的类即可。
监听器:
本例开发一个事件监听 器,监听Srinpplication 触发的ApictaonstaredEvenrt事件。该监听器类的代码如下。
// 定义事件监听器,负责监听ApplicationStartedEvent事件
// 实现ApplicationContextAware接口,可用于获取所在的Spring容器
public class MyListener implements ApplicationContextAware,
ApplicationListener<ApplicationStartedEvent>
{
private ApplicationContext ctx;
@Override
public void onApplicationEvent(ApplicationStartedEvent event)
{
// 获取激发事件的容器
ConfigurableApplicationContext c = event.getApplicationContext();
if (c == ctx)
{
System.out.println("-----激发事件的容器与监听器所在容器相同-----");
}
// 后面的代码可插入任意自定义的处理
System.out.println("========执行自定义处理=======");
}
// 接口注入方法,通过该方法可访问Spring容器
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException
{
this.ctx = ctx;
}
}
上面的监听器类实现了ApplicationListener接口, 表明该监听器会监
听ApplicationStartedEvent事件;而且该类实现了ApplicationContextAware接口,通过该接口可让m该监听器访问它所在的容器。
该监听器类中的onApplicationEvent(0方法就是事件处理方法,SpringApplication的事件将会触发监听器的onApplicationEvent()方法,该方法中的粗体字代码对发布事件的容器与监听器所在的容器进行判断-------当SpringApplication加载了层次结构的容器时,子容器发布的事件会触发父容器中的事件监听器,此时发布事件的容器和监听器所在的容器可能不是同个容器。
容器初始化
接下来定义一个初始化器,初始化器可对Spring容器执行初始化设置。下面是该初始化器类的代码。
public class MyInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext>
{
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext)
{
// 接下来的代码可对Spring容器执行任意的初始化
System.out.println("====模拟对Spring容器执行初始化====");
}
}
初始化器在实现ApplicationContextInitializer 接口时必须实现initialize()方法, 该方法可对Spring容器执行任意初始化。上面代码仅仅输出一行来模拟对Spring容器进行初始化设置一大部分时候,Spring Boot对容器已经进行了完备的初始化,不需要开发者执行额外的初始化;如果要对Spring容器进行某些与业务相关的特定设置,或者让SpringBoot整合某些前沿的、SpringBoot官方暂未支持的框架,则可在此处对Spring容器执行自定义的初始化设置。
ApplicationContextInitializer 使用和理解
https://www.cnblogs.com/hello-shf/p/10987360.html
环境后处理器
再定义一个配置环境后处理器,配置环境后处理器可对Environment 进行设置,Spring Boot会使用设置、修改后的Environment来创建ApplicationContext (Spring 容器)。配置环境后处理器最常见的用法就是:通过它来加载自定义的配置文件,该配置文件中的配置信息最终将被应用于Spring容器。
下面是本例中配置环境后处理器类的代码。
public class FkEnvironmentPostProcessor implements EnvironmentPostProcessor
{
private final PropertiesPropertySourceLoader loader = new PropertiesPropertySourceLoader();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application)
{
// 指定自定义的配置文件
Resource path = new ClassPathResource("fk/fk.properties");
// 加载自定义配置文件
PropertySource<?> ps = loadProperty(path);
System.out.println("fkjava.name: " + ps.getProperty("fkjava.name"));
System.out.println("fkjava.age: " +ps.getProperty("fkjava.age"));
// 将PropertySource中的属性添加到Environment配置环境中
environment.getPropertySources().addLast(ps);
}
private PropertySource<?> loadProperty(Resource path)
{
if (!path.exists())
{
throw new IllegalArgumentException("资源: " + path + " 不存在");
}
try
{
// 加载path对应的配置文件
return this.loader.load("custom-resource", path).get(0);
}
catch (IOException ex)
{
throw new IllegalStateException("加载配置文件出现错误: " + path, ex);
}
}
}
配置环境后处理器要实现FiEinmomPortProeer 接口,实现该接口必须实现postProcessEnvironment()方法,该方法用于对Environment进行后处理。
在postProcessEnvironment方法中,第一-行粗体字代码使用Resource定义资源,用理加载路径下的自定义资源文件:第二行粗体字代码调用loadProperty()方法加载自定义的资源文件;第三行粗体字代码则负责将自定义资源文件中的属性添加到Environment中,这就实现了对配置环境的改置、修改,Spring Boot会自动使用修改后的Environment来创建ApplicationContext,因此此处所做的修改最终将被应用于Sprng容器。
上面的环境配置后处理器指定要加载类加载路径下的fk/fkproperties文件。在resources目录下创建一个fk子目录,并在该子目录下新建一个k.roperies文件,该文件的内容如下。
fkjava.name=疯狂软件
fkjava.age=20
按下来使用METAINF/ spring.factories文件来注册上面的事件监听器、容器初始化器和环境配更后处理器。下面是 该配置文件的代码。
# 定义事件监听器
org.springframework.context.ApplicationListener=\
org.crazyit.app.MyListener
# 定义初始化器
org.springframework.context.ApplicationContextInitializer=\
org.crazyit.app.MyInitializer
# 定义配置环境后处理器
org.springframework.boot.env.EnvironmentPostProcessor=\
org.crazyit.app.FkEnvironmentPostProcessor
总结
从上面的执行过程可以看到,由配置环境后处理器负贵对Environment进行处理,因此在
Environment被创建之后,该后处理器立即得到执行的机会,所以它可以在Spring Banner被打印之前执行。
当Spring容器被创建之后,容器初始化器会获得执行的机会,它可以对Spring容器进行初始化设置。当Spring容器被初始化之后,程序开始初始化容器中的Bean。
当容器被刷新之后,容器发布ApplicationStartedEvent事件,触发MyListener监听器,这就是上面的事件监听器、初始化器和配置环境后处理器的执行过程。
所以执行过程为 环境后处理器 FkEnvironmentPostProcessor-> 容器初始化器 MyInitializer->监听器MyListener
通过上面的执行过程可以看到,虽然Spring Boot进行了高度封装,但它依然提供了大量的初始化API:事件监听器、容器初始化器和配置环境后处理器。通过这些初始化API,开发者完全可以在恰当的时机,对Spring容器执行任何自己想要的初始化。
ApplicationRunner和CommandLineRunner 也属于SpringApplication 的初始化API, 在SpringApplication的run()方法(应用启动)完成之前,ApplicationRunner 和CommandLineRunner实现类会自动调用接口中的run()方法。
由此可见,ApplicationRunner 和CommandLineRunner非常适合在应用程序启动之后、对外提供服务之前执行一些特定的初始化处理。
ApplicationRunner和CommandLineRunner这两个接口基本是一样的, 它们都只定义了一个run()方法,该方法将会在应用程序启动之后、对外提供服务之前自动执行。其区别是两个run()方法的参数类型不同:
➢ApplicationRunner 中run()方法的参数类型是ApplicationArguments, 用于访问运行SpringApplication的参数。
➢CommandLinetRunner中run()方法的参数类型是Srig…,它可直接代表运行SpringApplication的参数。
不难看出,ApplicationRunner接口与CommandLinetRunnerr接口的功力能基本是相同的。总之,随便用哪个区别并不大,只不过它们获取SpringApplication运行参数的方式不同而已,ApplicationRunner接口的run()方法使用AplcatinArguments获取参数,而AplicatinArguments提供了如下方法来获取运行参数。
➢Set getOptionNames():获取参数中所有的选项参数名,以双横线(–) 开头的参数才是选项参数。
➢List getOptionValues(String name):根据选项名获取选项值。
➢List getNonOptionArgs():获取非选项参数。
➢String[] getSourceArgs():获取原始的参数。
下面通过例子示范如何通过ApplicationRunner 对SpringApplication执行初始化。首先创建一个Maven项目,然后用前面Spring Boot项目中的pom.xml文件替换本项目的pom.xml文件,本项目只保留spring-boot-starter web依赖即可。
本应用的主类很简单,依然是在main(方 法中使用Sringppication 的run()方法运行带@SpringBootApplication注解修饰的类即可。
本例提供如下ApplicationRunner 实现类。
@Component
public class FkRunner implements ApplicationRunner
{
// 该run()方法将应用启动完成之前执行
@Override
public void run(ApplicationArguments args)
{
System.out.println("模拟对SpringApplication执行初始化,下面获取运行参数");
System.out.println("getSourceArgs:" + Arrays.toString(args.getSourceArgs()));
System.out.println("getOptionValues:" + args.getOptionValues("book.name"));
System.out.println("getNonOptionArgs:" + args.getNonOptionArgs());
}
}
上面的FkRunner实现类使用了@Component注解修饰,Spring 容器会自动载该Bean,该类实现了ApplicationRunner接口,该接口中的run()方法将会在Sping picio的run()方法完成之前自动执行,该run(方法通过ApplicationArguments获取SpringApplication的运行参数。
本例主类的main()方法代码如下。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
**SpringApplication.run(App.class, args);**
}
}
从上面的粗体字代码可以看到,SpringApplication 的运行参数就是运行该程序的命令行参数:arg。为了在运行该程序时配置命令行参数,打开IDEA的运行配置,按图所示输入命令行参数。
如图所示,为该程序配置了3个,命令行参数,其中第三个参数以双短横线开头,这意味着它是spring boot 应用的选项参数
运行结果
Spring Application会按如下规则来创建ApplicationContext 容器:
➢如果为项目添加了Spring MVC依赖,则使用AnnotationConfigServletWebServer Application-Context作为Spring容器。
➢如果为项目添加了Spring WebFlux 依赖,没有添加SpringMVC依赖,则使用AnnotationConfigReactiveWebServerApplicationContext作为Spring容器。
➢否则,使用AnnotationConfigApplicationContext 作为Spring容器
当为项目同时添加了Spring MVC依赖和Spring WebFlux依赖时,依然使用AnnotationConfig-ServletWebServerApplicationContext作为Spring容器。
SpringApplication还提供了一个 setApplicationContextClass()方法,该方法用于设置Spring容器的实现类, 这样可获得对Spring容器类型的全部控制权。但通常不建议调用该方法,因为有太多细节需要处理,很容易出现错误。
更现实的做法是,调用SpringApplication的setWebApplicationType()方法来设置Spring容器的类型,该方法接受如下WebApplicationType类型的枚举值。
NONE:运行非Web应用,不启动内嵌的Web服务器。
REACTIVE:运行反应式Web应用,启动内嵌的反应式WebWeb服务器
➢SERVLET:运行基于Servlet的Web应用,启用内嵌的Servlet Web服务器
因此,如果要创建非Web应用,则可通过如下主类来运行SpringBoot应用。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
//创建 springApplication对象
var application = new SpringApplication(App.class);
/**
* 注意:如果你同时添加了spring-boot-starter-web和spring-boot-starter-webflux依赖,
* 那么Spring Boot会自动配置Spring MVC,而不是WebFlux。你当然可以强制指定应用类型,
* 通过SpringApplication.setWebApplicationType(WebApplicationType.REACTIVE)
*
* 使用Spring WebFlux 使用WebApplicationType.REACTIVE属性
* 使用springMvc 使用WebApplicationType.SERVLET属性
* 使用非web应用 WebApplicationType.NONE
*/
//创建非web应用
.setWebApplicationType(WebApplicationType.NONE);
//调用run()方法运行springboot
application.run(args);
}
}
前面在介绍ApcionRimer时已经讲了Aleianorgentes实际上Sprig Boot会自动在容器中配置个该类 型的Bean,它可被依赖注入任何其他Bean组件,这样其他Bean组件即可通过ApplicationArguments来访问应用的运行参数。
下面通过例子示范如何通过AplicationArguments访间应用的运行参数。首先创建一个 Maven项目,然后用前面SpringBoot项目中的pom.xml文件替换本项目的pom.xml文件,本项目只保留spring-boot-starter-web依赖即可。
本应用的主类很简单,依然是在main()方法中使用SpringApplication 的run()方 法运行带@SpringBootApplication注解修饰的类即可。
接下来定义-个控制器类,该控制器类的代码如下。
@RestController
public class HelloController
{
// 依赖注入容器中的ApplicationArguments Bean
@Autowired
private ApplicationArguments args;
@GetMapping("/")
public void test()
{
System.out.println("访问应用的运行参数");
System.out.println("getSourceArgs:" + Arrays.toString(args.getSourceArgs()));
System.out.println("getOptionValues:" + args.getOptionValues("book.name"));
System.out.println("getNonOptionArgs:" + args.getNonOptionArgs());
}
}
上面控制器类中的粗体字代码使用@Autowired修饰了ApplicationArguments 变量,因此Spring将会把容器中的ApplicationArguments Bean依赖注入该实例变量。
在获得了ApplicationArguments对象的引用之后,其他方法都可通过该对象来获取应用的运行参数。
运行结果如下:
getSourceArgs: Ifkjava, crazyit, -- book. name=Spring Boot]
getOptionValues: [Spring Boot]
getNonOptionArgs: [fkjava, crazyit]
正如从前面示例所看到的,Spring Boot允许使用配置文件对应用程序进行配置,Spring支持如下不同形式的配置源。
➢属性文件(前面介绍的application.properties就是属性文件)。
➢YAML文件(后缀可以是.yml或.yaml) 。
➢环境变量。
➢命令行参数。
获取这些外部化的属性主要有如下几种方式:
➢使用@Value 注解将属性值注入任何Bean组件。
➢使用Spring的Environment抽象层进行访问。
➢使用CofigurationProperties 注解将- -批特定属性绑定到指定Java对象。
各种外部配置源的加载顺序如下,所有先加载的配置源都可能被后加载的配置源覆盖,因此可认为后加载的配置源的优先级更高。
➊默认属性(通过SpringApplication.setDefaultPropertiesQ方法指定)。
❷配置类( @Configuration修饰的类)上用@PropertySource注解加载的属性文件中的属性值。
*注意:
采用这种方式配置的属性只有等到Spring容器刷新时才会被添加到Environment中,因此通过这种方式来配置某些属性(如logging.、spring.main.*等)就太迟了,因为这些属性需要在Spring容器刷新之前起作用。
注意:
@PropertySource和第13项中的@TestPropertySource都是Spring 框架的注解,它们都可用于加载一份或多份属性文件,从而读取该属性文件中的所有属性值。
❸配置数据(如application.properties 文件等)。
4 RandomValuePropertySource, 只包含random.*中的属性。
❺操作系统环境变量。
⑥Java 系统属性( System的getProperties(方法返回的属性)。
❼来自java:comp/env的JNDI属性。
❽ServletContext 的初始化参数(在web.xml文件中通过
元素设参数)。
❾ServletConfig 的初始化参数(在web.xml文件中通过init-param…>元素设置的初始化参数,
或者通过@Servlet注解设置的初始化参数)。
10 来自SPRING_ APPLICATION_JSON的属性(嵌套在环境变量或系统属性中的JSON文本
11 命令行参数。
12 测试用例类上通过@SpringBooTest注解的properties所指定的属性。
13 测试用例类上用@TestPropertySource注解加载的属性文件中的属性值。
14 当Spring Boot的devools工具处于激活状态时,用户Home目录中.configspring-boot子目
录下spring-boot-devtools.properties或spring-boot-devtools.yml文件中设置的属性
上面的第7项和第8项只有在Web服务器或应用服务器中才有用,因此,只有把Spring Boot
应用部署到Web服务器或应用服务器中才能使用这两项。
上面的第12项和第13项仅对单元测试有效
第14项需要在用户Home目录(在Windows平台下对应于“C:\Users\用户名”目录)下添加配置文件,因此上面这些配置项中优先级最高的可认为是命令行参数。
Spring Boot会自动提取命令行参数中的选项参数(以双横线开头的参数,如–server.port=9090),
如果这些选项参数对Spring Boot 是有意义的,如server.port等选项参数,Spring Boot就会用该选项参数的值覆盖配置文件中所指定的属性值。
例如,在启动Spring Boot应用时指定了命令行参–server.port 9090,不管在application.properties
文件中将server.port指定了什么端口,该应用的服务端口肯定都是9090。
如果不希望将命令行的选项属性添加到配置环境中,则可调用SpringApplication.setAddCommandLineProperties(false)方法来禁用该功能。
从上面的加载顺序可以看出,通过SpringAplication (包括SpringApplicationBuiler)设置的属性值的优先级是最低的。
假如应用的主类通过如下代码运行spring容器:
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
new SpringApplicationBuilder ()
. bannerMode (Banner.Mode.OFF )
. sources (org.crazyit.app.App.class)
.run (args) ;
}
}
而且该应用还提供了如下application.properties文件:
spring.main.sources=org.fkit.app.Config, org.fkjava.app.ExtraConfig
spring.main.banner-mode=console
由于aplication poprties配置文件后加载(相当于优先级更高),文件中配置的属性药会覆盛Sping Apineair对象所设置的属性,因此该应用启动时依然会显示Spring Boot的Banners
但应用的主配置类会按如下顺序加载:
og.crazyit.app.App → org.fkit.app.Contig → org.tkjava.app.ExtraConfig
可能有人感到奇怪,为问不是用Contig、ExtmaContig 这两个主配置类覆盖App类呢?这是由
于Spring AplicationBuilder的sources()方法和spring.main.sources属性的作用都是添加更多的主配
置类,因此不会覆盖。
此外,application.properties (包括application.yml) 也有几个不同的来源,它们按如下顺序加
载:
1 JAR包内的application properties (或application.yml)、
2 JAR包内的application- (profile}.properties或appication- {profile}. yml),这是特定的Profile对应的配置文件
③JAR包外临时指定的applieation,properties (或application.yml)。
④JAR包外临时指定的application- (profile}.properties(或application- (profile}.yml)
从上面的加载顺序可以看出如下规律:
JAR包外临时指定的配置文件的优先级高于JAR包内配置义件的优先级:特定的Profile对应
的配置文件的优先级高于通用配置文件的优先级。
掌握这些配置源的加载顺序和优先级是很有必要的,尤其是在实际项目的开发过程中,项目的
配置源可能有书种不同的形式,不同配置源的加载顺序和优先级将会影响配置属性最終的值。
Spring Boot 还支持一个 特殊的系统属性(环境变量):
spring.application.json ( SPRING_APPLICATION_ JSON),通过这个特殊的系统属性或环境变量可以传入一段JSON文本,而这段JSON 文本将会被解析成配置属性一该配 置属性在加载顺序中排在第10位(见2.14 节)。
例如,对于如下示例的控制器。
@RestController
public class HelloController
{
// 使用@Value注解访问配置属性
@Value("${fkjava.name}")
private String name;
@Value("${fkjava.age}")
private String age;
@Value("${fkjava.servers[0]}")
private String server1;
@Value("${fkjava.servers[1]}")
private String server2;
@GetMapping
public Map<String, String> hello()
{
return Map.of("名称", name, "年龄", age,
"服务器1", server1, "服务器2", server2);
}
}
上面的控制器要访问fkjava.name、 fkjava.age 和fkjava.servers 这些配置属性,本例先使用
application.properties来配置如下属性。
fkjava.name=疯狂软件服务器
fkjava.age=35
上面的配置文件配置了两个属性,但并未配置fkjava.servers 属性。因此,如果直接运行该示
例的主类,则肯定会引起异常。
下面将该示例的主类改为如下形式。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
// 设置spring.application.json系统属性
**System.setProperty("spring.application.json",
"{\"fkjava\":{\"name\":\"疯狂软件\", \"age\":20, " +
"\"servers\":[\"fkjava.org\", \"crazyit.org\"]}}");**
SpringApplication.run(App.class, args);
}
}
上面的粗体字代码设置了-个spring ppiatinjion系统属性,该属性的值为:
{“fkjava”: {“name”: “疯狂软件”,“age”:20,“servers”:[ ("fkjava.org,“crazyit.org”] } }
上面的文本就是一段典型的JSON文本,Spring Boot会自动将这段JSON属性:
➢fkjava.name, 其值为“疯狂软件”。
➢fkjava.age, 其值为20。
➢fkjava.servers, 其值为List
运行上面的主程序启动Spring Boot应用,访问“http://localhoast:8080/’测试控制器的hello()
方法,将会看到如下输出:
{
"名称": "疯狂软件",
"年龄": "20",
"服务器2":"crazyit,org",
"服务器1";"fkjava,org"
}
从上面的输出可以看到,通过spring.application.json系统属性传入的配置属性覆盖了
application. properies中配置的属性,这也正好符合2.14所介绍的内容。
上面的App类使用租体字代码设置了spring. pplication.json系统属性,也可删除这行粗体字代码,改为设置SPRING_ APPLICATION_JSON环境变量,同样设置为如下值:
{“fkjava”: {“name”: “疯狂软件”,“age”:20, “servers”:[ ("fkjava.org,“crazyit.org”] } }
再次运行该程序,将会看到同样的运行效果。
也可通过VM选项的-D选项来设置系统属性,例如添加如下VM选项:
-Dspring.application.json="{\"fkjava\":{\"name\":\"疯狂软件\", \"age\":20, \"servers\":[\"fkjava.org\", \"crazyit.org\"] } }"
还可通过命令行参数来设置spring application.json系统属性,例如添加如下命令行参数:
--spring. application. json="{\"fkjava\":{\"name\":\"疯狂软件\",\"age\":20,
\"servers\":[\"fkjava. org\", \"crazyit.org\"]}}"
添加上面任何一张配置之后,再次运行都可以看到相同的运行效果
前面多次提到,application. properties文件的另-种形式是application.ym,这两种文件只是载
体形式不同,其本质是一-样的。
YAML是JSON格式的超集,因此它能以层次格式来存储配置属性。Spring Boot使用
SnakeYAML来解析YAML文件,但由于spring-boot-starter自动依赖SnakeYAML,
因此只要为项目添加了spring -boot-starter依赖(几乎所有Spring Boot项目都会添加),SpringAplication 即可使用SnakeYAML解析YAML文件。
比如有如下application.properties 配置片段:
spring. application. name=cruncher
spring. datasource. driver-class-name=com. mysql.cj. jdbc. Driver
spring. datasource. url=jdbc :mysql:/ / localhost/springboot
spring. datasource. username= root
spring. datasource . password=32147
server . port=9000
如果写成YAML配置片段,则对应于如下形式:
spring:
application:
name: "cruncher"
datasource :
driver-class-name: "com. mysql.cj. jdbc. Driver"
url: "jdbc:mysql: // localhost/test"
username;” root"
password; "32147"
server :
port: 9000
发现其中的对应关系了吧!如果属性文件的key中包含了点号(.),那么转换为YAML文件时就变成了缩进。因此,对于“spring.application.name"形式的key,就变成了YAML文件中的如下形式:
spring:
application:
name:
在掌握了属性文件与YAML文件之间的对应关系之后,其实项目是用属性文件作为配置文件,
还是使用YAML文件作为配置文件,只是个人的习惯问题
YAML配置属性值包含多个列表项,那么它将被自动转换为[index]的形式。例如如下配置:
fkjava:
servers :
- www.crazyit.org
- WWW. fkit.org
fkjava.servers[0]= www.crazyit.org
fkjava.servers[1]= WWW. fkit.org
如果程序要加载YAML文件配置的属性,则Spring Boot提供了如下工具类。
➢YamIPropertiesFactoryBean: 它将YAML文件加载为Properties对象。
➢YamIMapFactoryBean: 它将YAML文件加载为Map对象。
➢YamlPropertySourceLoader: 它将Y AML文件加载为PropertySource。
实际上,YamlPropertiesFactoryBean和YamlMapFactoryBean是由Spring框架提
供的。
需要说明的是,@PropertySource 和@TestPropertySource这两个注解都只能加载属性文件,不
能加载YAML文件,这是YAML文件的局限之-。
注意:
@PropertySource注解和@TestPropertySource注解只能读取属性文件。
下面通过例子来示范使用YamlPopertySourerLoader加载YAML文件,并将所加教的文件内容
添加到配置环境(Environment) 中。这里依然创建个Maven 项目,然后用前面Spring Boot项目
中的pom.xml文件替换本项目的pom.xml 文件,本项目只保留spring-boot-starter-web
本应用的主类很简单,依然是在main0方法中使用SpringApplication 的run()方法运行带
@SpringBootApplication注解修饰的类即可。
为本例定义两份YAML文件,其中第-份 是系统会自动加载的aplication.yml 文件,该文件
的内容如下。
fkjava:
server:
name: "疯狂软件服务器"
port: 9000
第二份是系统不会自动加载fk/fk.yaml文件,该内容如下:
fkjava:
name: "疯狂软件"
age: 20
servers:
- www.fkjava.org
- www.crazyit.org
本例定义一个配置环境后处理器加载这份fk.yaml 代码如下:
public class FkEnvironmentPostProcessor implements EnvironmentPostProcessor
{
// 创建YamlPropertySourceLoader,用于加载YAML文件
private final YamlPropertySourceLoader loader = new YamlPropertySourceLoader();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application)
{
// 指定自定义的配置文件
Resource path = new ClassPathResource("fk/fk.yml");
// 加载自定义配置文件
PropertySource<?> ps = null;
try
{
ps = this.loader.load("custom-resource", path).get(0);
}
catch (IOException e)
{
e.printStackTrace();
}
System.out.println("fkjava.name: " + ps.getProperty("fkjava.name"));
System.out.println("fkjava.age: " +ps.getProperty("fkjava.age"));
System.out.println("fkjava.servers[0]: " +ps.getProperty("fkjava.servers[0]"));
System.out.println("fkjava.servers[1]: " +ps.getProperty("fkjava.servers[1]"));
// 将PropertySource中的属性添加到Environment配置环境中
environment.getPropertySources().addLast(ps);
}
}
上面处理器创建了YamlPropertySourceLoade对象,使用load()方法加载yaml配置文件
注意:
与YamlPropertySourceLoader对应的是PropertiesPropertySourceLoader, 只不过它用于将属性文件加载为PropertySource, 前面介绍配置环境初始化器时已示范了PropertiesPropertySourceLoader的用法。
public class FkEnvironmentPostProcessor implements EnvironmentPostProcessor
{
private final PropertiesPropertySourceLoader loader = new PropertiesPropertySourceLoader();
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application)
{
// 指定自定义的配置文件
Resource path = new ClassPathResource("fk/fk.properties");
// 加载自定义配置文件
PropertySource<?> ps = loadProperty(path);
System.out.println("fkjava.name: " + ps.getProperty("fkjava.name"));
System.out.println("fkjava.age: " +ps.getProperty("fkjava.age"));
// 将PropertySource中的属性添加到Environment配置环境中
environment.getPropertySources().addLast(ps);
}
private PropertySource<?> loadProperty(Resource path)
{
if (!path.exists())
{
throw new IllegalArgumentException("资源: " + path + " 不存在");
}
try
{
// 加载path对应的配置文件
return this.loader.load("custom-resource", path).get(0);
}
catch (IOException ex)
{
throw new IllegalStateException("加载配置文件出现错误: " + path, ex);
}
}
}
还需要使用META-INF/spring factories文件来定义该配置环境后处理器,该文件的内容如下。
# 定义配置环境后处理器
org.springframework.boot.env.EnvironmentPostProcessor=\
org.crazyit.app.FkEnvironmentPostProcessor
这样一来,默认的application.yml和fk/fk.yml文件都被加载进来,接下来即可在其他任何Bean组件(如控制器)中通过@Value注解来访问它们、例如如下代码。
@RestController
public class HelloController
{
// 使用@Value注解访问配置属性
@Value("${fkjava.server.name}")
private String serverName;
@Value("${fkjava.server.port}")
private String serverPort;
@Value("${fkjava.age}")
private String age;
@GetMapping
public String hello()
{
return "名称: " + serverName + ", 端口: " + serverPort
+ ", 年龄: " + age;
}
}
从上面的粗体字代码可以看到,当使用@Value注解来访问YAML配置属性时,依然要通过“扁平化”之后的key(就是转换为属性文件对应的key)进行访问。
运行主类启动SpringBoot应用,可以在控制台看到如下输出:
fkjava. name:疯狂软件
fkjava. age: 20
fkjava. servers[0]: www. fkjava.org
fkjava. servers [1] : wWW .crazyit.org
通过浏览器访问“htp://ocalhost:8080/"测试上面的hello()方法,将看到如下输出
名称:疯狂软件服务器,端口:9000,年龄: 20
如果仅仅需要加载自定义的YAML文件,在普通组件中使用这些配置属性,并不需要将YAML文件中的属性添加到配置环境中,那么只要在容器中配置一个 YamlPropertiesFactoryBean 工厂Bear或YamlMapFactoryBean工厂Bean, 它们就会自动读取YAML文件,并将其中的配置内容加载头Properties对象或Map对象。
例如,下面的示例对上面的示例进行一些修改,删除其中的配置环境后处理器,然后添加如下配置类。
@Configuration
public class MyConfig
{
// 在容器中配置一个YamlPropertiesFactoryBean
@Bean
public YamlPropertiesFactoryBean fkProps()
{
var factory = new YamlPropertiesFactoryBean();
factory.setResources(new ClassPathResource("fk/fk.yml"));
return factory;
}
}
上面的配置类在容器中配置了一个YamlPropertiesFactoryBean工厂Bean,并指定该工厂Bean
要加载fk/fk.yml文件。Spring容器中的工厂Bean (实现FacoryBea接口的Bean)有一个特征
当程序通过Spring容器获取工厂Bean时,Spring容器实际返回的是该工厂Bean 的产品( getObje
方法的返回值)。因此,当程序获取上面配置的fkProps时,实际返回的只是一个 Properties对象。
/**
从 YAML 源读取的Properties工厂,公开字符串属性值的平面结构。
YAML 是一种很好的人类可读的配置格式,它有一些有用的层次属性。 它或多或少是 JSON 的超集,因此它具有许多相似的功能。
注意:所有公开的值都是String类型,可通过常见的Properties.getProperty方法访问(例如,通过PropertyResourceConfigurer.setProperties(Properties)进行配置属性解析)。 如果这是不可取的,请改用YamlMapFactoryBean 。
该工厂创建的属性具有分层对象的嵌套路径,例如这个 YAML
environments:
dev:
url: https://dev.bar.com
name: Developer Setup
prod:
url: https://foo.bar.com
name: My Cool App
转化为这些属性:
environments.dev.url=https://dev.bar.com
environments.dev.name=Developer Setup
environments.prod.url=https://foo.bar.com
environments.prod.name=My Cool App
列表被拆分为带有[]解引用器的属性键,例如这个 YAML:
servers:
- dev.bar.com
- foo.bar.com
变成这样的属性:
servers[0]=dev.bar.com
servers[1]=foo.bar.com
从 Spring Framework 5.0.6 开始,需要 SnakeYAML 1.18 或更高版本。
因为:
4.1
作者:
戴夫·赛尔、斯蒂芬妮·尼科尔、尤尔根·霍勒
*/
public class YamlPropertiesFactoryBean extends YamlProcessor implements FactoryBean<Properties>, InitializingBean {
private boolean singleton = true;
@Nullable
private Properties properties;
/**
设置是否应创建单例,否则为每个请求创建一个新对象。 默认为true (单例)
*/
public void setSingleton(boolean singleton) {
this.singleton = singleton;
}
@Override
public boolean isSingleton() {
return this.singleton;
}
@Override
public void afterPropertiesSet() {
if (isSingleton()) {
this.properties = createProperties();
}
}
@Override
@Nullable
public Properties getObject() {
return (this.properties != null ? this.properties : createProperties());
}
@Override
public Class<?> getObjectType() {
return Properties.class;
}
/**
子类可以重写以构造此工厂返回的对象的模板方法。 默认实现返回一个包含所有资源内容的属性。
在共享单例的情况getObject()一次调用getObject()时延迟调用; 否则,在每个getObject()调用上。
返回值:这个工厂返回的对象
请参见:process(YamlProcessor.MatchCallback)
*/
protected Properties createProperties() {
Properties result = CollectionFactory.createStringAdaptingProperties();
process((properties, map) -> result.putAll(properties));
return result;
}
}
接下来,其他Bean组件(如控制器)则可通过如下方式来访问fk/kyml文件中的属性。
@RestController
public class HelloController
{
// 使用@Value注解访问配置属性
@Value("${fkjava.server.name}")
private String serverName;
@Value("${fkjava.server.port}")
private String serverPort;
// 指定将容器中fkProps Bean注入fkProps实例变量
@Resource(name = "fkProps")
private Properties fkProps;
@GetMapping
public String hello()
{
return "名称: " + serverName + ", 端口: " + serverPort
+ ", 年龄: " + fkProps.getProperty("fkjava.age");
}
}
上面程序中的粗体字代码指定将容器中的fkPropsBean注入fkProps实例变量,
而fkProps就是上面配置类中配置的YamlPropertiesFactoryBean 的产品,也就是它所加载的
YAML文件转换德到的Properties对象。
在默认情况下,Spring Boot会自动按如下顺序加载默认的配置文件,后加载的属性文件可以
覆盖先加载的属性文件,因此可认为后加载的属性文件具有更高的优先级。
①类加载路径的根路径。
2 类加载路径下的/confg子目录。
3 当前项目根路径
❹当前项目根路径下的/config子目录。
❺当前项目根路径下/config子目录的任意直接子目录,如/config/abc/、 /config/xyz/等。
如果在不同的目录中存在多个配置文件,它的读取顺序是:
1、config/application.properties(项目根目录中config目录下)
2、config/application.yml
3、application.properties(项目根目录下)
4、application.yml
5、resources/config/application.properties(项目resources目录中config目录下)
6、resources/config/application.yml
7、resources/application.properties(项目的resources目录下)
8、resources/application.yml
注:
1、如果同一个目录下,有application.yml也有application.properties,默认先读取application.properties。
2、如果同一个配置属性,在多个配置文件都配置了,默认使用第1个读取到的,后面读取的不覆盖前面读取到的。
3、创建SpringBoot项目时,一般的配置文件放置在“项目的resources目录下”
打包成jar包后的读取顺序
如果在不同的目录中存在多个配置文件,它的读取顺序是:
1、与jar同级目录中config目录下, config/application.properties
2、与jar同级目录中config目录下,config/application.yml
3、与jar同级目录中,application.properties
4、与jar同级目录中,application.yml
如果想改变这种默认行为,Spring Boot可通过如下系统属性(或环境变量,或命令行参数)
来改变配置文件的位置。
➢spring.config.name:改变配置文件的文件名,默认是application.如果用OS环境变量来设
置,则该属性对应于SPRING_CONFIG_NAME环境变量名。
➢spring.config.location: 改变配置文件的加载路径。如果用OS环境变量来设置,则该属性
对应于SPRING_CONFIG_LOCATION 环境变量名。
spring.config.additional-location:添加配置文件的加载路径,不会覆盖原有的配置文件的加
载路径。
注意:
不要尝试使用Spring Boot配置属性来设置spring.config.name、spring. config.location、
spring.config.additional-location属性,因为这几个属性的加载时机非常早,所以只能
通过系统环境变量、系统属性或命令行参数来设置。
通过spring.config.location或spring.config.additional-location属性指定的加载路径是有先后顺序的,后面路径中的配置文件将会后加载,因此它具有更高的优先级。
比如将spring.config.location设置为“optional:classath:/fkjava/ ,optional:fle:./fkit/”, Spring Boot将依次从如下路径加载配置文件:
1 optional:classpath:fkjava/
2optional:fle:./fkit/
因此上面的配置会后加载optional:fl:/fkit路径中的配置文件,所以该路径中的配置文件具有更高的优先级。
如果使用spring.config.additional-location属性来添加配置文件的加载路径,那么新增的路径总是排在默认的加载路径之后.换句话说,通过spring config. additional-location属性指定的加载路径,比系统默认的加载路径具有更好的优先级。比如将
spring.config.additional-location 属性设置为“optional:classpath:/fkjava/ ,optional:ile.:/fkit/”
此时Spring Boot将依次从如下路径加载配置文件:
1 optional:classpath:/ (类加载路径的根路径)
2 optional:classpath:/config/ (类加载路径下的/config子目录)
3 optional:file./ (当前项目根路径)
4 optional:file:./config/ (当前路径下的/config子目录)
5 optional:fle:./config/*/ (当前路径下/config子目录的任意直接子目录)
6 optional:classpath:fkjava/
7 optional:file…/fkit/
上面列出的前5项是SpringBoot配置文件的默认加载路径。
上面的第5顶“ optioalfle:./config /* / ”路径中包含-一个通配符(*), 这意味着它可以匹配当前路径下/config子目录的任意直接子目录,如/config/abc/、 /configxyz/等。 在配置加载路径时,使用通配符有如下限制:
➢通配符只能在外部路径中使用,例如file:前缀就表示使用文件系统的当前路径;
classpath:前缀的路径不能使用通配符。
➢每个加载路径 只能在最后面使用一一个通配符, 不能写成file:/* / * /,这太荒唐了,只能写成
如fle:fkjava/*/的形式,这表示当前路径下/fkjava子目录的任意直接子目录。
对于2.2.3节示例中位于resources 目录下的fk/fk.ym文件,可将主类修改为如下形式加载该
配置文件。
@SpringBootApplication
public class App
{
static {
// 设置配置文件的文件名
**System.setProperty("spring.config.name", "application, fk");
// 设置配置文件的加载路径
System.setProperty("spring.config.location",
"classpath:/, optional:classpath:/fk/");**
// 设置额外的加载路径
// System.setProperty("spring.config.additional-location",
// "optional:classpath:/fk/");
}
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
SpringApplication.run(App.class, args);
}
}
上面的粗体字代码通过系统属性设置了SpringBoot配置文件的主文件名可以是aplicatioin和fk;
:配置文件的加载路径是classpath:/ (代表类加载路径的根路径)和optional:classpath:/fk/: (代表类加载路径下的/fk子目录)。
其中optional:classpath:/fk/使用了optional:前缀,用于告诉Spring Boot不要检查该路径下是否
存在配置文件,如果该路径下存在配置文件就加载,否则就忽略该路径。如果不添加optional:前缀,那么Spring Boot会强制检查该路径下是否存在配里文件:若该路径下不存在任何配置文件,则
Spring Boot将会抛出ConfigDataLocationNotFoundException异常。
使用系统属性设置了Spring Boot 配置文件的文件名之后,Spring Boot就不会自动加载
application.poperties (及application.yml) 文件了,因此上面的粗体字代码还显式地设置了要加载
类加载路径下的application.poperties ( 及application,yml)文件。
如果使用spring. config aditional-location系统属性,则只是添加新的配置文件的加载路径,因此
上面被注释掉的代码设rsingonfiaddtiona-location"属性时,只要设置“optionalclasspath:/fk/”
即可。
置文件,这样其他Bean组件(如控制器)依然可通过@Value注解来访问这些配置属性。例如如下代码:
@RestController
public class HelloController
{
// 使用@Value注解访问配置属性
@Value("${fkjava.server.name}")
private String serverName;
@Value("${fkjava.server.port}")
private String serverPort;
@Value("${fkjava.age}")
private String age;
@GetMapping
public String hello()
{
return "名称: " + serverName + ", 端口: " + serverPort
+ ", 年龄: " + age;
}
}
在Spring容器刷新之后,Spring Boot还可使用如下方式导入额外的配置文件:
➢使用@PropertySource 注解导入额外的属性文件。
➢使用spring.config import属性导入额外的配置文件( 包括属性文件和YAML文件)。
因此,如果只是需要为应用中的Bean组件(如控制器)导入一些配置属性, 则完全可通过上
面两种方式来导入。
下面通过例子示范使用@PropertySource注解和spring config.import属性导入额外的配置文件。
首先创建-个Maven项目,然后用前面SpringBoot项目中的pom.xml文件替换本项目的pom.xml
文件,本项目只保留spring-boot-starter-web依赖即可。
先为本应用定义一份自定义的YAML文件,该文件的内容如下。
fk.yaml
fkjava:
name: "疯狂软件"
age: 25
servers:
- www.fkjava.org
- www.crazyit.org
在定义一份属性文件,内容如下:
crayit.properties
crazyit.book.name=Spring Boot
crazyit.book.price=128
其中,fk.yml 文件只能通过spring.config.import属性导入,而crazyit.properteie文件既可通过
spring. config. import属性导入,也可通过@PropertySource注解导入。
下面是本例中默认的配置文件。
application.yml
fkjava:
server:
name: "疯狂软件服务器"
port: 9000
spring:
config:
# 指定导入类加载路径下fk/fk.yml文件
import: optional:classpath:/fk/fk.yml
上面配置文件中的粗体字代码导入了类加载路径下的fk/fk.yml文件。
下面通过在应用主类中使用@PropertySource注解导入crazyit.prpertis文件。该主类的代码如下
@SpringBootApplication
// 导入类加载路径下fk/crazyit.properties文件
@PropertySource("classpath:/fk/crazyit.properties")
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
SpringApplication.run(App.class, args);
}
}
通过上面的方式导入额外的配置文件之后。接下来在其他Ben组件(如控制器)中同样可通过@Value注解来读取这些配置属性。
在配置文件中可通过占位符( ${ } )的方式来引用已定义的属性,或者引用其他配置源(如系统属性、环境变量、命令参数等)配置的属性。
下面示例的配置文件使用了占位符。
app:
name: "占位符"
# 引用配置文件中的配置属性
**description: "${app.name}应用演示了如何在配置文件中使用占位符"**
book:
# 引用外部的配置属性
**description: ${book.name}是一本非常优秀的图书**
server:
# 配置服务端口
**port: ${port}**
上面配置文件中的三行粗体字代码使用了占位符,其中第一个占位符引用的是app.name属性。
由于在该配置文件前面配置了该属性,因此它引用的是当前文件中已有的属性。
后面两个占位符所引用的属性在配置文件中不存在,因此它们引用的是来自其他配置源的属性。
本例的控制器类同样可通过@Value属性访问这些属性,就像它们没有使用占位符一一样。下面
是本例中控制器类的代码。
@RestController
public class HelloController
{
// 使用@Value注解访问配置属性
@Value("${app.description}")
private String appDescription;
@Value("${book.description}")
private String bookDescription;
@GetMapping
public Map<String, String> hello()
{
return Map.of("应用描述:", appDescription,
"图书描述: ", bookDescription);
}
}
为本例的运行添加如下命令行参数:
–book.name= “疯狂Spring Boot” --port=9090
上面的命令行参数配置了两个选项参数: --book.name 和 --port,这两个选项参数将用于填充配
置文件中的占位符。启动该应用,使用浏览器访问“http://localhost:9090/” (命令行传入的–port选
项参数改变了配置文件中的server.port选项,因此端口变成了9090) 来测试hello0方法,将会看到
如下输出:
{
"应用描述:":"占位符应用演示了如何在配置文件中使用占位符",
"图书描述:":"疯狂Spring Boot是一本非常优秀的图书"
}
Spring Boot还允许配置文件读取构建文件(pom.xml或build.gradle)中的属性。
对于Maven项目,只要在项目的pom.xml文件中定义如下
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/>
</parent>
接下来即可在配置文件中通过“@属性名@”来引用pom.xml文件中的属性。如下是本例的配
置文件。
app:
java:
# 引用pom.xml文件中的属性
version: @java.version@
sourceEncoding: @project.build.sourceEncoding@
name: @name@
version: @version@
在上面的配置文件中,通过“@属性名@”的形式引用了pom.xml文件中的属性。
本例的控制器类同样可通过@Value属性访问这些属性,就像访问普通属性一样。 下面是本例中控制器类的代码。
@RestController
public class HelloController
{
// 使用@Value注解访问配置属性
@Value("${app.java.version}")
private String javaVersion;
@Value("${app.sourceEncoding}")
private String sourceEncoding;
@Value("${app.name}")
private String appName;
@Value("${app.version}")
private String appVersion;
@GetMapping
public Map<String, String> hello()
{
return Map.of("javaVersion", javaVersion,
"sourceEncoding", sourceEncoding,
"appName", appName,
"appVersion", appVersion);
}
}
启动应用,测试hello()方法输出如下:
{ "appName":"expansion",
"javaVersion":"11.0.11",
"sourceEncoding":"UTF-8",
"appVersion":"0.0.1-SNAPSHOT"
}
上面输出的这些属性就来自构建文件:pom.xml.
对于Gradle项目,首先需要在build.gadle文件中对Java插件的processResoures进行如下隆置,然后将这段配置添加到原有的build.gradle文件的最后。
plugins {
id 'org.springframework.boot' version '2.4.2'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
// 定义项目的基本信息
group = 'org.crazyit'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
// 定义Gradle资源库
repositories {
// 优先使用Maven本地资源库
mavenLocal()
mavenCentral()
}
dependencies {
// Spring Web依赖
implementation 'org.springframework.boot:spring-boot-starter-web'
}
test {
useJUnitPlatform()
}
// 配置Java插件的processResources Task
**processResources {
expand(project.properties)
}**
接下来即可在配置文件中通过“${属性名}”的形式引用build.gradle文件中的属性。
可能有人已经发现,引用build.gade文件中的属性的语法和通配符语法是相同的,这就会引起冲突。为了避免冲突,请按这种方式转义配置文件中的占位符:$ (…}.
下面是本例的配置文件:
app:
java:
version: "${sourceCompatibility}"
name: "${rootProject.name}"
version: "${version}"
上面配置文件中的${…}形式不再是通配符语法,而是引用build.gradle文件中的属性。该应用的控制器及其测试项目与前一个示例大致相同,此处不再介绍。
@RestController
public class HelloController
{
// 使用@Value注解访问配置属性
@Value("${app.java.version}")
private String javaVersion;
@Value("${app.name}")
private String appName;
@Value("${app.version}")
private String appVersion;
@GetMapping
public Map<String, String> hello()
{
return Map.of("javaVersion", javaVersion,
"appName", appName,
"appVersion", appVersion);
}
}
有些特殊的时候(比如出于测试的需要),需要为应用配置各种随机值,包括随机整数、UUID等,它们都可通过Spring Boot的RandomValuePropertySource来配置。
实际上,在配置文件中配置随机值甚至无须知晓RandomValuePropertySource的存在,只要在配置文件中使用${random.xxx}的形式即可生成各种随机值,不过这个random就是RandomValuePropertySource。
下面是本例的配置文件。
fkjava:
secret: "${random.value}"
number: "${random.int}"
bignumber: "${random.long}"
uuid: "${random.uuid}"
number-less-than-ten: "${random.int(10)}"
number-in-range: "${random.int(20,100)}"
有了该配置文件之后,接下来其他Bean组件(如控制器等)同样可通过@Value属性访问这些属性,就像访问普通属性一 样。下面是本例中控制器类的代码。
@RestController
public class HelloController
{
// 使用@Value注解访问配置属性
@Value("${fkjava.secret}")
private String secret;
@Value("${fkjava.number}")
private String number;
@Value("${fkjava.bignumber}")
private String bignumber;
@Value("${fkjava.uuid}")
private String uuid;
@Value("${fkjava.number-less-than-ten}")
private String numberLessThanTen;
@Value("${fkjava.number-in-range}")
private String numberInRange;
@GetMapping
public Map<String, String> hello()
{
return Map.of("secret", secret, "number", number,
"bignumber", bignumber,
"uuid", uuid,
"numberLessThanTen", numberLessThanTen,
"numberInRange", numberInRange);
}
}
启动应用,测试hello()方法输出如下:
{ "numberLessThanTen":"1",
"uuid":"a42bd991-8a41-4330-88c6-caf16e87e1ca",
"numberInRange":"53",
"number":"1867134009",
"bignumber":"-1423761240052625503",
"secret":"a0e536832d4d8268464464827d460fce"
}
前面介绍的都是使用@Value注解来读取配置文件中的属性,但使用@Value注解每次只能读取
一个配置属性,若需要整体读取多个属性,或者读取具有某种结构关系的一组属性,Spring Boot
则提供了@ConfigurationProperties注解来进行处理。
@ConfigurationProperties注解有两种主要用法。
➢修饰属性处理类:当@ConfigurationProperties 注解修饰的类被部署为容器中的Bean时,该注解指定的属性将会被注入该Bean的属性。因此,将@ConfigurationProperties在解珍饰的
类称为“属性处理类”。
➢修饰@Bean 注解修饰的方法:使用@Bean修饰的方法将会配置一个容器中的Bean, 而@ConfigurationProperties注解指定的属性将会被注入该Bean的属性。
在使用@ConfigurationProperties注解时可指定如下属性。
➢prefix(或value):指定要加载的属性的前缀。
➢ignoreInvalidFields(): 指定是否忽略无效属性值。比如处理类定义了某个字段的类型是
Integer,但在配置文件中为该字段配置的值是abc,这就是无效的值。
➢ignoreUnknownFields():指定是否忽略未知的字段值。如果在配置文件中配置的属性比处
理类需要的属性更多,那么多出来的属性就属于未知属性。
请先看使用@ConfigurationProperties注解修饰类的例子。下面是本例的配置文件。
org.crazyit.enabled=true
org.crazyit.name=Crazy Java
org.crazyit.remoteAddress=192.168.1.188
org.crazyit.item.brand=Tesla
org.crazyit.item.comments=Good, Excellent
接下来定义如@ConfigurationProperties注解的属性处理类来处理上面的配置信息
// 指定读取以org.crazyit.*开头的属性
**@ConfigurationProperties(prefix = "org.crazyit", ignoreUnknownFields=false)
@Component**
public class CrazyitProperties
{
private boolean enabled;
private String name;
private InetAddress remoteAddress;
private final Item item = new Item();
public boolean isEnabled()
{
return this.enabled;
}
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public InetAddress getRemoteAddress()
{
return remoteAddress;
}
public void setRemoteAddress(InetAddress remoteAddress)
{
this.remoteAddress = remoteAddress;
}
public Item getItem()
{
return item;
}
public static class Item
{
private String brand;
private List<String> comments = new ArrayList<>(Collections.singleton("GREAT"));
public String getBrand()
{
return brand;
}
public void setBrand(String brand)
{
this.brand = brand;
}
public List<String> getComments()
{
return comments;
}
public void setComments(List<String> comments)
{
this.comments = comments;
}
}
}
上面第 一行粗体字代码使用了@ConfigurationProperties 注解修饰CrazyitProperties 类,因此
该类将会被配置成容器中的Bean,且配置文件中以“org.crnzyit"开头的属性将会被注入该Bean
实例。
如果使用IDEA开发上面的属性处理类,则IDEA 会提示添加spring-boot-configuration-processor依赖,添加该依赖后 IDEA可提供“自动补全”功能。比如上面的属性处理类定义了enabled、 name 等属性,当切换到配置属性的编辑器中编写以“org.crazyit”开头的属性时,IDEA可自动提示要配置enabled、 nam等属性。如果在配置属性的编辑器中按住住Ctrl键,再单击任何以“org.crazyit"开头的配置属性,则IntelliJ IDEA将会自动打开该CrazytProperties类,并滚动到该配置属性对应的setter方法处。
注意:
尽量为@ConfigurationProperties修饰的类添加spring-boot-configuration-processor
依赖,这样IntelliJ IDEA能提供更友好的编辑帮助。
值得注意的是,Spring Boot并不会自动启用@ConfigurationProperties注解。让Spring Boot启
用该注解有如下方式:
2 ➢将@ConfigurationProperties 注解修饰的类显式配置成容器中的Bean。
3 ➢使用@EnableConfigurationProperties注解,该注解可显式指定一个或多个属性处理类,
Spring Boot将会启用这些属性处理类上的@ConfigurationProperties注解。
4 ➢使用@ConfigurationPropertiesScan 注解,该注解可指定启用一个或多个包及其子包下所有
带@ConfigurationProperties注解的类。
其实上面前两种方式的本质是一样的,无论是使用@Component注解修饰属性处理类,还是将属性处理类配置成容器中的Bean,它们最终的本质都是将该属性处理类配置成容器中的Bean
。
因此上面的第二行粗体字代码使用了@Component注解修饰,这也是让SpringBoot启月
@ConfigurationProperties注解的方式之一。
当该属性处理类被配置成容器中的Bean之后,接下来该Bean可被注入任何其他Bean组件(如控制器),这个其他Bean组件即可通过该属性处理类的实例来读取所有以“org.crazyit"开头的属性:
下面是本例中控制器类的代码。
@RestController
public class HelloController
{
private final CrazyitProperties crazyitProperties;
// 依赖注入CrazyitProperties属性处理Bean
@Autowired
public HelloController(CrazyitProperties crazyitProperties)
{
this.crazyitProperties = crazyitProperties;
}
@GetMapping
public CrazyitProperties hello()
{
return crazyitProperties;
}
}
上面控制器类的粗体字代码将容器中的CrazyitProperties依赖注入控制器,这样该控制器即可
通过CrazyitProperties来访问所有以“org.crazyit"开头的属性。
启动该应用,使用浏览器访问“httpo:localhtot:8080/’来测试hlleo()方法,将会看到如下输出:
{
"enabled":true,
"name":"Crazy Java",
"remoteAddress":"192.168.1.188",
"item":
{"brand":"Tesla",
"comments":["Good","Excellent"]
}
}
从上面的输出来看,使用@ConfigurationProperties 注解修饰的CrazyitProperties
可“整体”读取所有以"org.crazyit" 开头的属性,这确实非常方便。
刚刚定义的CrazjiProperties 为每个配置属性都提供了同名的实例变量和setter方法,这样
Spring Boot会通过反射调用这些setter方法来完成属性值注入。
实际上,属性处理类同样也支持用构造器来完成属性值注入,只要额外使用@ConstructorBinding
注解修饰用于执行属性值注入的构造器即可。如果该类仅包含一个构造器,则可直接用该注解修饰
属性处理类。
值得注意的是,如果使用构造器来完成属性值注入,则要求使用@EnableConfigurationProperties
注解或@ConfigurationPopriesScan注解来启用@ConfiguratioProperties注解。
下面的示例对前面的属性处理类略做修改,将其改为如下所示的属性处理类。
@ConfigurationProperties(prefix = "org.crazyit", ignoreUnknownFields=false)
public class CrazyitProperties
{
private boolean enabled;
private String name;
private InetAddress remoteAddress;
private final Item item;
**@ConstructorBinding**
public CrazyitProperties(boolean enabled, String name, InetAddress remoteAddress, Item item)
{
this.enabled = enabled;
this.name = name;
this.remoteAddress = remoteAddress;
this.item = item;
}
public boolean isEnabled()
{
return this.enabled;
}
public String getName()
{
return name;
}
public InetAddress getRemoteAddress()
{
return remoteAddress;
}
public Item getItem()
{
return item;
}
public static class Item
{
private String brand;
private List<String> comments = new ArrayList<>(Collections.singleton("GREAT"));
public String getBrand()
{
return brand;
}
public void setBrand(String brand)
{
this.brand = brand;
}
public List<String> getComments()
{
return comments;
}
public void setComments(List<String> comments)
{
this.comments = comments;
}
}
}
上面的属性处理类并未为实例变量定义setter方法,而是定义了一个带参数的构造器,且该构造器使用了@ConstructorBinding修饰,这样Spring Boot将会使用构造器来完成属性值注入。下面是本例的配置文件,该配置使用了YAML格式(本质是一样的)。
org:
crazyit:
enabled: true
name: 疯狂Java
remote-address: 192.168.1.188
item:
brand: Apple
comments:
- Good
- Excellent
留意上面配置的属性为org.crazyit.remote-address,这与CrazyitProperties 类中定义的
remoteAddress属性并不完全相同,Spring Boot能成功注入吗?
答案是肯定的,原因就在于Spring Boot支持所谓的宽松绑定( Relaxed Binding)。宽松绑定并不要求配置属性的属性名与属性处理类中的属性名完全相同。
例如,对于上面CrazyitProperties类中的remoteAddress属性,表2.2中列出的各种属性都可被成功注入remoteAddress属性。
对比上面*.properties和*.yml两个文件在配置org.crazyititem.comments属性时的区别,该属性
的类型是List,因此必须在配置文件中配置List。
➢在*.properties文件中配置List有两种方式:简单地使用英文逗号隔开的多个值(如前面setter
例子所示),或者用标准的方括号语法来配置Listo
➢在*.yml 文件中配置List也有两种方式:以短横线开头(如上面constructor例子所示),
或者简单地使用英文逗号隔开的多个值来配置List.
对于用构造器执行属性值注入的属性处理类,要求使用@ConfigurationPropertiesScan或
@EnableConfigurationProperties注解来启用@ConfigurationProperties,因此本例在应用主类上增加
了@ConfigurationPropertiesScan注解。下面是本例的应用主类。
@SpringBootApplication
// 指定扫描org.crazyit.app.config包及其子包下的@ConfigurationProperties注解修饰的类
**@ConfigurationPropertiesScan("org.crazyit.app.config")**
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
SpringApplication.run(App.class, args);
}
}
上面的粗体字注解指定了要启用org.crzyt.app.config 包及其子包下所有属性处理类上
@ConfigurationProperties注解。
运行、测试该应用,其效果与前一个示例的效果基本相同。
@ConfigurationProperties注解除了可修饰属性处理类,还可修饰@Bean注解修饰的方法这样Spring Boot将会ConfrgtioPopertie注解加载的配置属性,并将属性值注入该@Bean方法所配置的Bean组件。
例如,下面的示例定义 一个Book类,该类的代码如下。
public class Book
{
private String title;
private double price;
private String author;
private List<String> keywords;
public String getTitle()
{
return title;
}
public void setTitle(String title)
{
this.title = title;
}
public double getPrice()
{
return price;
}
public void setPrice(double price)
{
this.price = price;
}
public String getAuthor()
{
return author;
}
public void setAuthor(String author)
{
this.author = author;
}
public List<String> getKeywords()
{
return keywords;
}
public void setKeywords(List<String> keywords)
{
this.keywords = keywords;
}
}
接下来使用Java配置来配置该Bean类,并使用@ConfigurationProperties注解修饰该@Bean方法。下面是该配置类的代码。
@Configuration
public class MyConfig
{
@Bean
// @ConfigurationProperties注解会驱动Spring自动调用该Bean的setter方法
@ConfigurationProperties("fkjava.book")
public Book book()
{
return new Book();
}
}
上面带@Bean注解的book()方法配置了Book类的实例。上面的配置只是创建了一个默认的 Book
对象,并没有为之设置任何属性。但由于该book()方法使用了@ConfiguratonProperties(“fkjavabook”)
注解修饰,Spring Boot将会自动读取所有以“fkjava.book"开头的属性,并将这些属性值对应地注
入该Book对象。
在配置文件中提供如下配置属性。
fkjava:
book:
title: "疯狂Spring Boot终极讲义"
price: 128
author: "李刚"
keywords:
- Java
- Spring
- 疯狂
上面配置文件中配置的属性与Book类的属性对应(不需要严格对应,按Spring Boot推荐,
配置文件中配置的属性名应使用“烤串”写法),这样Spring Boot就会读取这些配置属性,并将它
们注入Book Bean。
注意:
从本质上看,@ConfigurationProperties注解的作用就是驱动被修饰的@Bean方法
所配置的对象调用相应的setter方法,比如@ConfigurationProperties 注解读取到title属:
性。它就会驱动Spring Boot以反射方式执行@Bean方法所配置的对象的setTitle()方法,
并将title属性值作为setTitle()方法的参数。
通过使用@ConfigurationProperties修饰@Bean方法,让Spring Boot将属性值注入@Bean方法
所配置的Bean组件。
当容器中有了属性完备的Book对象之后,接下来可将它注入任何Bean组件(如控制器组件)。
下面是本例的控制器类。
@RestController
public class HelloController
{
private final Book book;
@Autowired
public HelloController(Book book)
{
this.book = book;
}
@GetMapping
public Book hello()
{
return book;
}
}
上面的控制器类接受容器注入的Book Bean。
运行主类,启动Spring Boot 应用,使用浏览器访问“http://localhost:8080/”测试hello()方法,
将会看到如下输出:
{
"title":"疯狂Spring Boot终极讲义",
"price":128.0,"author":"李刚",
"keywords":["Java","Spring","疯狂"]
}
通过上面示例示范了@ConfigurationProperties的基本用法,不难发现它与@Value各有特色一
使用@Value注解读取配置属性简单、方便(只要用该注解修饰实例变量即可),但每个@Value注
解只能注入一个配置属性;而@ConfigurationProperties 可整体注入一批配置属性,但它需要额外定
义一个属性处理类(即使修饰@Bean方法,也需要有一个Bean类)。
由于@Value是Spring容器的核心特征,因此该注解可以支持SpEL (Spring 表达式语言),而
@ConfigurationProperties则不支持该功能。
@Value和@ConfigurationProperties的对比如表2.3所示。
从表2.3可以看出,@Value对宽松绑定并不完全支持,因此建议在@Value注解中引用要注入
的属性名时,总使用Spring Boot 推荐的“烤串”写法。例如,对于@Value(" fkjava.book-price"),
Spring Boot 可从application.properties (或application.yml) 文件中读取fkjava.book-price 或
fkjava.bookPrice属性来完成注入,也可从OS环境变量中读取FKJAVA _BOOKPRICE环境变量的
值来完成注入;但如果写成@Value(“fkjava.bookPrice”),,则只会读取application.properties (或
application.yml)文件中的fkjava.bookPrice 属性来执行注入,fkjava.book-price 属性和
FKJAVA_BOOKPRICE环境变量都不会起作用。
Spring Boot 内置了常用的类型转换机制,例如,从前面的示例看到,Spring Boot可将配置属
性值自动转换为int、double类型。在默认情况下,如果转换失败,Spring Boot应用启动将会失败,
并抛出异常。如果希望Spring Boot忽略转换失败的配置属性值,则可将@ConfigurationProperies
注解的ignoreInvalidFields属性设置为true (它的默认值为false)。
此外,Spring Boot还可自动转换如下类型。
➢Duration Sping Boot可自动将配置属性值转换为Duration 类型,支持为属性值指定单位。
➢Period: Spring Boot可自动将配置属性值转换为Period类型,支持为属性值指定单位。
➢DataSize: Spring Boot可自动将配置属性值转换为DataSize类型中,支持为属性值单位。
在定义Duration类型的配置属性时,如果直接定义整数值,则该整数值将被当成多少毫秒处
理,除非在该属性上使用@DurationUnit注解指定了默认的时间单位。在配置属性值时可指定如下
时间单位。
➢ ns: 纳秒。
➢ μs: 微秒。
➢ ms: 毫秒。
➢ s: 秒。
➢ m: 分钟。
➢ h: 小时。
➢ d: 天。
➢ y: 年。
➢ m: 月。
➢ w: 星期。
➢ d: 天。
例如配置属性值1y3d.它代表了1年3天。
➢ B: 字节。
➢ KB: 千字节。
➢ MB: 兆字节。
➢ GB: 吉字节:
➢ TB: 太字节:
例如,本例提供如下属性处理类
// 指定读取以org.crazyit.*开头的属性
@ConfigurationProperties(prefix = "org.crazyit")
@Component
public class CrazyitProperties
{
private Duration timeout;
@DurationUnit(ChronoUnit.SECONDS)
private Duration lastTime;
private Period runPeriod;
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize maxSize;
public Duration getTimeout()
{
return timeout;
}
public void setTimeout(Duration timeout)
{
this.timeout = timeout;
}
public Duration getLastTime()
{
return lastTime;
}
public void setLastTime(Duration lastTime)
{
this.lastTime = lastTime;
}
public Period getRunPeriod()
{
return runPeriod;
}
public void setRunPeriod(Period runPeriod)
{
this.runPeriod = runPeriod;
}
public DataSize getMaxSize()
{
return maxSize;
}
public void setMaxSize(DataSize maxSize)
{
this.maxSize = maxSize;
}
}
上面的lastTime 使用了@DurationUnit(ChronoUnit.SECONDS)修饰,这意味着它的默认时间单
位是秒: maxSize 使用了@DataSizeUnit(DataUnit.MEGABYTES)修饰,这意味着它的默认数据单位
是MB.
下面是本例的配置文件。
# 默认时间单位是毫秒
org.crazyit.timeout=30000
# 默认时间单位是秒
org.crazyit.last-time=45
org.crazyit.run-period=2m5d
# 默认时间单位是MB
org.crazyit.max-size=2
将上面的CrazyitProperties 属性处理类的实例注入控制器,该控制器的代码很简单,此处不再
给出。
@RestController
public class HelloController
{
private final CrazyitProperties crazyitProperties;
// 依赖注入CrazyitProperties属性处理Bean
@Autowired
public HelloController(CrazyitProperties crazyitProperties)
{
this.crazyitProperties = crazyitProperties;
}
@GetMapping
public Map<String, Object> hello()
{
System.out.println(crazyitProperties.getMaxSize().toKilobytes());
return Map.of("props", crazyitProperties,
"maxSize", crazyitProperties.getMaxSize().toKilobytes());
}
}
运行主类启动Spring Boot应用,使用浏览器访问“http://localhost:8080/”测试控制器的处理
方法,将会看到如下输出:
{
"maxSize":2048,
"props": {"timeout":"PT30S",
"lastTime":"PT45S",
"runPeriod":"P2M5D",
"maxSize":{"negative":false}
}
}
SpringBoot还可对属性处理类进行校验,只要为属性处理类添加@Validated注解,并使用JSR303的校验注解修饰需要校验的实例变量,SpringBoot就会自动校验配置文件中的属性值。如果某个属性值不能通过校验,Spring Boot应用启动将会失败,并用FailureAnalyzer 显示校验错误信息。
如果属性处理类包含复合类型的属性,且需要SpringBoot对该复合类型的子属性进行校验,则应为复合类型的属性添加@Vaid注解。
@ConfigurationProperties的数据校验是基于JSR 303的,因此在执行数据校验之前,必须先添
JSR 303 规范的依赖以及JSR303实现的依赖。Spring Boot为数据校验提供了
spring-boot-starter-validation依赖库,它已经包含了JSR 303规范和实现的依赖,因此只要在pom.xml
文件中添加如下依赖库即可。
<!-- 添加Spring Boot Validation依赖库 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
接下来为属性处理类添加校验注解。
// 指定读取以org.crazyit.*开头的属性
@ConfigurationProperties(prefix = "org.crazyit", ignoreUnknownFields=false)
@Component
**@Validated**
public class CrazyitProperties
{
@NotEmpty
private String name;
@Range(max = 150, min=90, message = "价格必须位于90~150之间")
private double price;
@Pattern(regexp = "[1][3-8][0-9]{9}", message = "必须输入有效的手机号")
private String mobile;
@Valid
private final Item item = new Item();
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public double getPrice()
{
return price;
}
public void setPrice(double price)
{
this.price = price;
}
public String getMobile()
{
return mobile;
}
public void setMobile(String mobile)
{
this.mobile = mobile;
}
public Item getItem()
{
return item;
}
public static class Item
{
@Length(min=5, max=10, message = "品牌名长度必须在5到10个字符")
private String brand;
@Size(min = 1, message = "comments至少包含一个元素")
private List<String> comments = new ArrayList<>(Collections.singleton("GREAT"));
public String getBrand()
{
return brand;
}
public void setBrand(String brand)
{
this.brand = brand;
}
public List<String> getComments()
{
return comments;
}
public void setComments(List<String> comments)
{
this.comments = comments;
}
}
}
上面的属性处理类使用了@Validated注解修饰,且其中的name、price、 mobile 等属性使用了
@NotEmpty、@Range、@Pattern注解修饰,因此Spring Boot将会对它们对应的配置属性进行校验。
上面属性处理类中的item属性是Item类型的,程序使用了@Valid修饰它,这样可保证Spring Boot对Item类包含的属性也执行数据校验。
下面是本例的配置文件。
org.crazyit.name=Crazy Java
org.crazyit.price=89
org.crazyit.mobile=13334444
org.crazyit.item.brand=Apple
org.crazyit.item.comments=Great, Excellent
该配置文件故意将price和mobile两个属性配置得无法通过校验,这样SpringBoot应用启动
时就会看到如图2.6所示的错误提示。
对于使用@ConfigurationProperties修饰@Bean方法的情况,Spring Boot同样可对配置属性值
进行数据校验,只要为@Bean方法添加@Validated注解修饰即可。
所谓Profile,就是一组配置环境、 各种程序组件的合集。
在实际开发环境中,经常需要在不同的环境间切换,比如开发项目时用的是开发场的部署环境
(如数据库、索引库、部署节点等各种环境);测试项目时用的是测试场的部署环境;上线项 目时用
的是产品场的部署环境,不同的部署环境使用的配置信息肯定是不同的,甚至个别程序组件还需要
随着部署环境的改变而改变。
传统的做法是,当要变更应用程序的部署环境时,采用手动方式来更改配置信息,但实际项目
的配置信息往往有很多,如端口、数据库连接、索引库连接、消息配置、缓存配置等信息,如果每
次更换部署环境都需要手动修改配置信息的话,则相当麻烦,而Profile就可以解决这个问题。
每个Profile 都包括一组配置 文件和程序组件等,对应于一个部署环境。只要为每个部署环境
都提供了对应的Profile,接下来当应用被部署到不同的环境中时,只要切换到相应的Profile 即可。
Profile可包括程序组件和配置文件,声明程序组件和配置文件的Profile 很简单:
➢使用@Profle 注解修饰@Component、@Configuration、 @ConfigurationProperties 等注解修
饰的类,这限制了这些类仅对特定的Profile有效。
➢通过配置文件的文件名限制Profile。比如application-dev.properties(或application-dev.yml)
文件限制了仅对dev Profile 有效; application-test properties (或application-test.yml)文件
, 限制了仅对test Profile有效。文件名是application-{profile}.properties/yml
➢在配置文件中使用特定语法限制某些属性仅对特定的Profile 有效。这种特殊的配置文件被
称为“多Profile配置文件”。
对于@ConfigurationProperties类有一点需 要说明:如果@ConfigurationProperties类是通过
@EnableConfigurationProperties注解来启用的,而不是通过扫描方式(用@ConfigurationPropertieScam
注解)来启用的,则需要在@EnableConfigurationProperties注解所在的@Configuration类上使用
@Profile注解。对于以扫描方式启用的@ConfigurationProperties注解,则可@ConfigurationProperties
类本身上指定@Profile注解。
下面通过例子来示范Profile的功能和用法。首先创建一个Maven项目,然后用前面SpringBoot
项目中的pom.xml文件替换本项目的pom.xml文件,并为本项目添加springboot-starterweb依赖。
此外,由于本例要示范连接不同部署环境的数据库,因此还需要添加spring-boot-starter-jdbc 和
mysql-connector-java两个依赖。
接下来定义如下4份配置文件。
application-default.yml
spring:
datasource:
# 指定连接deft数据库
url: jdbc:mysql://localhost:3306/deft?serverTimezone=UTC
username: root
password: 32147
从文件名可以看出,这份配置文件仅对default Profile有效。
application-dev.yml
spring:
datasource:
# 指定连接dev数据库
url: jdbc:mysql://localhost:3306/dev?serverTimezone=UTC
username: root
password: 32147
从文件名可以看出,这份配置文件仅对dev Profile有效。
application-prod.yml
spring:
datasource:
# 指定连接prod数据库
url: jdbc:mysql://localhost:3306/prod?serverTimezone=UTC
username: root
password: 32147
从文件名可以看出,这份配置文件仅对prod Profile有效。
application-test.yml
spring:
datasource:
# 指定连接test数据库
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: 32147
从文件名可以看出,这份配置文件仅对test Profile有效。
上面4份配置文件指定连接不同的数据库。由于本例是在本机测试的,因此连接4个数据库的
用户名、密码是相同的,但在实际开发中,4个不同Profile的配置文件所连接的数据库、用户名、
密码可以完全不同。
@RestController
@Profile("default")
public class DefaultController
{
private final DataSource dataSource;
@Autowired
public DefaultController(DataSource dataSource)
{
this.dataSource = dataSource;
}
@GetMapping
public Map<String, String> hello() throws SQLException
{
return Map.of("class", "默认场的控制器","数据库",
dataSource.getConnection().getCatalog());
}
}
除配置文件外,还可限制程序组件仅对特定的Profile 有效,只要为该程序组件添加@Profle
注解即可。下面是本例用到的控制器类的代码。
@RestController
@Profile("default")
public class DefaultController
{
private final DataSource dataSource;
@Autowired
public DefaultController(DataSource dataSource)
{
this.dataSource = dataSource;
}
@GetMapping
public Map<String, String> hello() throws SQLException
{
return Map.of("class", "默认场的控制器","数据库",
dataSource.getConnection().getCatalog());
}
}
上面的控制器类使用了@Profile(“default”)修饰,这意味着该控制器类仅对default Profile有效。
类似地,还定义了DevController、 ProdController、 TestController 控制器类,只不过它们分别使用
了@Profile(“dev”)、@Profile(“prod”)、 @Profile(“test”)修饰, 这意味着它们都只对特定的Profile 有
效。
上面的控制器类使用了@Profile(“default”)修饰,这意味着该控制器类仅对default Profile有效。
类似地,还定义了DevController、 ProdController、 TestController 控制器类,只不过它们分别使用
了@Profile(“dev”)、@Profile(“prod”)、 @Profile(“est”)修饰, 这意味着它们都只对特定的Profile 有
效。
上面程序直接将容器中的DataSource 注入控制器,但在实际开发中绝不会这么干,此例仅仅
是示范不同的Profile能连接不同的数据库。
根据上面的介绍不难看出,在Spring Boot应用中使用Profile其实很简单:配置文件通过文件
名限制它所属的Profile;程序组件通过@Profile注解来限制它所属的Profile。
在运行应用时,可通过spring,profiles.active属性指定激活哪个Profile。 该属性可通过前面介绍
的各种方式来指定,但主要还是使用如下方式来指定:
➢通过application.properties (或application.yml)文件指定。
➢使用操作系统的SPRING_PROFILES_ACTIVE环境变量指定。
➢使用系统属性指定。
➢使用命令行参数指定。
例如:
application-test.properties 代表测试环境配置
application-dev.properties 代表开发环境配置
但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;
我们需要通过一个配置来选择需要激活的环境:
#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
#我们启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev
yaml的多文档块
和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了 !
server:
port: 8081
#选择要激活那个环境块
spring:
profiles:
active: prod
---
server:
port: 8083
spring:
profiles: dev #配置环境的名称
---
server:
port: 8084
spring:
profiles: prod #配置环境的名称
与前面介绍的知识类似,上面4种方式也是按加载顺序排列的,最先加载的属性具有最低优先
级,因此通过命令行参数指定的spring:profiles.active属性会覆盖前面几种方式指定的该属性。
如果在运行程序时没有指定spring.profiles.active属性,则会默认使用default Profile,这意味着
Spring Boot将会自动加载application-default.properties (或application-default.yml)文件。
如果项目中还定义了通用的配置文件: application.properties (或application.yml)文件,则Spring
Boot也会加载它,但对特定的Profile 才有效的配置文件,其优先级更高,它总会覆盖通用的配置
文件中的同名属性。
如果直接运行该应用,不指定spring:profiles.active 属性,Spring Boot 将会自动使用default
Profile。使用浏览器访问"http://localhost:8080/”, 可以看到如下输出:
{
"数据库": "deft",
"class":"默认场的控制器"
}
如果为应用程序配置如下命令行参数:
-- spring. profiles. active=dev
如果直接运行该应用Spring Boot 将会自动使用dev Profile。使用浏览器访问"http://localhost:8080/”, 可以看到如下输出:
{
"class":"开发场的控制器",
"数据库": "dev"
}
如果为应用程序配置如下命令行参数:
-- spring. profiles. active=prod
如果直接运行该应用Spring Boot 将会自动使用prod Profile。使用浏览器访问"http://localhost:8080/”, 可以看到如下输出:
{
"class":"产品场的控制器",
"数据库": "prod"
}
从上面的运行过程可以看出,通过使用Profile,可以非常方便地让应用在不同的部署环境之间
自由切换。虽然本例只是切换不同部署环境的数据库,但随着后面介绍的不断深入、配置文件的逐
渐增多,使用Profle完全可以切换不同部署环境的各种基础资源。
正如前面所介绍的,spring.profiles.active 属性也和其他属性一样, 同样遵循越早加载,优先级
越低的规则,后面加载的属性值可以覆盖前面加载的属性值。
除了可以改变激活的Profile, Spring Boot还允许添加额外的活动Profle.新增的活动Profle不会彻底替换原有的Profile,而是对原有的Profile进行追加:当追加的Profile中的程序组件、配置属性与原有的Profile冲突时,追加的Profile中的程序组件、配置属性会覆盖原有的Profile的设定;否则依然使用原有的Profile的设定。
添加新Profile可通过如下方式进行:
➢使用spring.profiles.include属性,与spring.profiles.active 不同,include 是添加Profile,而
active是指定激活新的活动Profile。
➢调用SpringApplication的setAdditionalProfiles()方法来添加新的活动Profile。
下面对上一个示例略做修改,本例再次添加一份与新的Profile相关的配置文件。
application-addition.yml
spring:
datasource:
# 指定连接addition数据库
url: jdbc:mysql://localhost:3306/addition?serverTimezone=UTC
从该配置可以看到,这份配置文件仅配置了连接URL地址,并未指定用户名、密码信息,单独使用它无法连接数据库。
提示:
控制层没有任何修改
本例无须对上一个示例的其他部分进行修改,只是将主程序改为如下形式。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
var app = new SpringApplication(App.class);
// 添加Profile
app.setAdditionalProfiles("addition");
app.run(args);
}
}
上面程序中的粗体字代码添加了addition Profile, 它不会替换程序原来设置的Profile,而是以
添加的方式进行。
如果为应用程序配置如下命令行参数:
--spring. profiles. active=prod
再次运行该应用,此时Spring Boot将使用prod Profile。使用浏览器访问“http:/localhost.8080/’”,
可以看到如下输出:
{
"class": "产品场的控制器",
"数据库": "addition"
}
此时命令行参数指定当前活动Profile是prod,但主程序通过代码添加的是addition Profile,这
意味着当prod和addition两个Profile冲突时,addition Profile 的配置会覆盖prod Profile 的配置,
其余的将依然使用prod Profile 的配置,因此我们就看到了上面的输出:程序依然使用prod Profile
的控制器类,连接数据库时依然使用prod Profile 配置的用户名、密码,只是它所连接的数据库变
成了addition.
先运行的Profile配置会被后面运行Profile配置的yaml覆盖,但只覆盖相同的属性,其余不变
在有些情况下,我们可能按不同的功能组定义了相应的配置文件(比如为数据库配置定义了一
个文件,为消息机制又定义了一个配置文件,等等),其实这组配置文件依然应该属于一个特定的
Profle。在这种需求下,则可以考虑将它们定义成Profile组。
例如,在属性文件中通过如下配置片段来配置一个组:
spring.profiles.group.prod[0] =banner
spring.profiles.group.prod[1]=server
上面配置指定banner和server都属于prodProfile组,因此,如果程序设置prod作为活动Profile,
那么banner和server两个Profile 的配置也会被加载。
下面的示例定义了如下几份配置文件。
application-prod.yml
spring:
datasource:
# 指定连接prod数据库
url: jdbc:mysql://localhost:3306/prod?serverTimezone=UTC
username: root
password: 32147
application-banner.yml
# 定义图片Banner的大小
spring:
banner:
image:
height: 20
width: 60
# 设置字符串的色深
bitdepth: 4
application-server.yml
server:
port: 9090
接下来程序定义了一份与Profile 无关的配置文件,在这份配置文件中指定本应用所激活的Profile,并配置对应的Profile组。下面是该配置文件的内容。
application.yml
spring:
profiles:
# 定义Profile组,该组还包括banner和server两个Profile
group:
prod:
- banner
- server
# 将prod设为活动Profile
active: prod
上面的配置文件配置了Profile 组: prod, 该组包括banner和server 两个Prfile该配置文件
还设置了当前活动Prfle是prod.
直接运行主类启动Spring Boot应用,将可以看到该应用同时加载了上面prod、banner、 server
这三个Profile的配置文件。
提示
运行顺序:首先运行通用的application.yml-----指定激活的配置文件----按照顺序运行Profile组
当SpringBoot可以从多个配置文件中加载List类型的属性时,后加载的List集合总是完全替
换先加载的List集合。打个比方,假如Spring Boot先从第一个配置 文件中加载的List 集合包含2
个元素,接下来从第二个配置文件中加载的List 集合包含一个元素,那么这个List 属性最终就只
有一个元素。
当SpringBoot可以从多个配置文件中加载Map类型的属性时,后加载的Map的key-value对
将会被添加到先加载的Map中。打个比方,假如Spring Boot先从第一个配置 文件中加载的Map
集合包含两个key-value 对,接下来从第二个配置文件中加载的Map集合包含一个 key-value 对,
且该key-value对与之前的两个key value对不冲突,那么这个Map属性最终会包含三个key-value
对。
总结
==spring boot读取配置多个Profile文件中只允许一个List集合,后面的文件里的List集合将会覆盖前面的文件,而Map集合只要key不冲突就会将这个key value添加 ==
例如,有如下属性处理类
@ConfigurationProperties("crazyit")
public class CrazyitProperties
{
private final List<Book> list = new ArrayList<>();
private final Map<String, Book> map = new HashMap<>();
public List<Book> getList()
{
return this.list;
}
public Map<String, Book> getMap()
{
return this.map;
}
}
上面的属性处理类分别定义了List 和Map属性,其中还用到了一个Book类,该Book类包含
title和description两个属性。
为了启用CrazyitProperties 属性处理类,本例在主类上添加@ConfigurationPropertiesScan注解
来启用该属性处理类。
下面是本例的几份配置文件。
application-prod.yml
spring:
datasource:
# 指定连接prod数据库
url: jdbc:mysql://localhost:3306/prod?serverTimezone=UTC
username: root
password: 32147
crazyit:
list:
- title: a
description: b
- title: 疯狂Java讲义
description: 北京大学信息科学学院的Java推荐教材
map:
prod:
title: 产品级
description: 产品级
上面的配置文件仅对prod Profile 有效,该配置文件为list 属性配置了两个元素,为map属性
配置了一个key-value对。
application-banner.properties
crazyit.list[0].title = 疯狂Android讲义
crazyit.list[0].description = 最全面的Adnroid编程图书
crazyit.map["banner"].title = Banner级
crazyit.map["banner"].description = Banner级
上面的配置文件仅对banner Profile 有效,该配置文件为list属性配置了一个元素,为map属
性配置了一个key-value 对。
此处故意使用properties格式来配置这份配置文件,这是为了给读者提供*.yml和
.properties 两种配置格式的示例。在实际项目中,则建议要么统一用.yml配置格式,
要么统一用*.properties格式,不建议混着用。
application-server.yml
crazyit:
list:
- title: 疯狂Spring Boot终极讲义
description: “此书一出再无书”的Spring Boot
map:
server:
title: 服务器级
description: 服务器级
上面的配置文件仅对server Profle有效,该配置文件为list属性配置了一个元素,为map属性
配置了一个key-value对。
接下来定义与Profile无关的配置文件,在该配置文件中配置激活的Profile, 并配置Profile 组。
下面是该文件的内容。
application.yml
spring:
profiles:
# 定义Profile组,该组还包括banner和server两个Profile
group:
prod:
- banner
- server
# 将prod设为活动Profile
active: prod
上面的配置将当前活动Profile设为prod,且prod Profile 组包括banner和server两个Proilei
可见这三个Profile的加载顺序为:
prod一banner - server
对于list 属性而言,后配置的List 集合直接替换先配置的List 集合,因此最终生效的应该是
server Profile中配置的List集合。
对于map属性而言,后配置的Map的key-value对将会被添加到先配置的Map中,因此最终
生效的是prod、banner、 server 这三个Profile 所配置的key-value对的总和。
运行主类启动Spring Boot应用,访问“http://localhost:8080",可以看到如下输出:
{
"数据库":"prod",
"crazyit":{
"list":[
{
"title":"疯狂Spring Boot终极讲义",
"description":"“此书一出再无书”的Spring Boot"
}
],
"map":{
"server":{
"title":"服务器级",
"description":"服务器级"
},
"prod":{
"title":"产品级","description":"产品级"
},
"\"banner\"":{
"title":"Banner级",
"description":"Banner级"
}
}
},
"class":"产品场的控制器"
}
Spring Boot 允许使用三个减号(---
) 将一份*.yml配置文件分割成逻辑上的多个片段
(*.properties文件使用#---
进行分割),每个片段都会被加载成单独的配置。
当配置文件被分割成多个配置之后,接下来可通过如下属性指定“ 条件性”生效。
➢spring.config. activate.on-profile:指定此行配置以下的配置仅当指定的Profile激活时才有效。
该属性也支持使用取反运算符(!),比如“!dev” 表示非dev Profile时有效。
➢spring.config.activate.on-cloud-platform: 指定此行配置以下的配置仅当处于指定的云平台上
时才有效。
比如配置片段:
myprop=always-set
#---
spring. config. activate.on-profile=prod
otherprop=somet imes-set
上面的配置片段配置了
myprop属性为always-set, 这个属性总是有效的。此外,还配置了
otherprop属性为sometimes-set,只有当prod Profile处于活动状态时,该属性才有效。
再比如配置片段:
myprop: always-set
---
spring :
config:
activate:
on-cloud-platform: kubernetes
otherprop: sometimes-set
上面的配置片段配置了myprop 属性为always-set, 这个属性总是有效的。此外,还配置了
otherprop属性为sometimes-set,只有当该应用被部署在K8s云平台时,该属性才有效。
下面是本示例的完整配置文件。
spring:
datasource:
username: root
password: 32147
---
spring:
config:
activate:
on-profile: default
datasource:
# 指定连接deft数据库
url: jdbc:mysql://localhost:3306/deft?serverTimezone=UTC
---
spring:
config:
activate:
on-profile: dev
datasource:
# 指定连接dev数据库
url: jdbc:mysql://localhost:3306/dev?serverTimezone=UTC
---
spring:
config:
activate:
on-profile: prod
datasource:
# 指定连接prod数据库
url: jdbc:mysql://localhost:3306/prod?serverTimezone=UTC
---
spring:
config:
activate:
on-profile: test
datasource:
# 指定连接test数据库
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
上面的配置文件中配置了会一直生效的两个属性:spring.datasource.user.name和
spring. datasource.password,这两个属性指定了连接数据库的用户名和密码。
接下来的配置将上面的配置文件分成4个片段,每个片段都指定了spring.confg activate.on-proniec
属性,这意味着这些片段仅对特定的Profile 有效。
如果直接运行该应用,不指定活动Profle,则Spring Boot默认使用default作为活动Profile
访问“http://localhost:8080",将会看到如下输出:
{
"数据库":"deft"
"class":"默认场的控制器"
}
如果为应用程序配置如下命令行参数:
--spring. profiles .active=prod
再次运行该应用,此时Spring Boot将使用prod Pofile使用浏览器访:“http://localhost:8080”
将会看到如下输出:
{
"数据库":"prod"
"class":"产品场的控制器"
}
通过Spring Boot 提供的日志抽象层可以非常方便地管理应用的日志输出。
只要在项目中导入spring-boot-star.jar依赖,它就会传递 导入spring-boot-sater-logging.jar,
从IDEA的Maven面板上的依赖关系中可以看到如图2.7所示的依赖JAR包。
从图2.7可以看到,spring-boot-starter-loggingjar依赖如下三个JAR包。
➢logback-classic.jar: 它传递依赖于logback-core.jar和slf4j-api.jar。
➢log4j-to-slf4j.jar: 它传递依赖于log4j-api.jar和slf4j-apijar。
➢jul-to-slf4j.jar; 它传递依赖于slf4j-api.jar.
看到这里可能会有些头晕,因为Java领域的日志框架比较多,常见的日志框架就包括SLF4J、
Log4j、Log4j2、 Logback、 common-logging (JCL)、javautillogging (JUL)、JBoss Logging等。这
些日志框架又可分为:
➢门面类(抽象层) : SLF4J、JCL、 JBoss Logging。
➢日志实现: Log4j、Log4j2、 Logback、 JUL.
Spring Boot默认使用SLF4J+Logback的日志组合,其中SLF4J作为日志门面(应用程序输出
日志时也应该面向该API),Logback作为日志实现,开发者通常不需要直接操作日志实现的API。
因此,Spring Boot默认会添加SLF4J 依赖(slf4j-api.jar) 和Logback 依赖( logback-core.jar和
slf4j-api.jar).
由于SpringBoot框架需要整合大量第三方框架,而这些框架的底层可能会使用JCL、Log4j、
JUL等日志,因此Spring Boot还要提供对应的日志路由,将其他日志框架所生成的日志信息统一
路由给SLF4J来处理。所以,从上面的依赖关系中还看到了如下依赖。
<➢log4j-to-sIfjjar; 负责将Log4j日志路由到SLF4J。
➢jul-to-sIf4j.jar: 负责将JUL日志路由到SLF4J。
图2.8显示spring boOt口志抽家层的示意图。
虽然Spring Boot默认采用Logback作为底层日志实现,但通过配置完全可以将底层日志实现
改为使用其他框架。Spring Boot 允许将Logback依赖排除出去,添加其他日志实现(比如Log4j)
的依赖,这样即可将底层日志实现改为使用其他框架。
有一点需要说明的是:当把Spring Boot应用部署到Web服务器或应用服务器上时,JUL 生成
的日志将不会被路由到Spring Boot应用的日志中,这是为了避免将服务器或部署在服务器上的其
他应用程序的日志也路由到SpringBoot应用的日志中,否则会造成日志的混乱。
提示:
当把Sprig Boot应用部署到Web服务器或应用服务器上时,JUL 生成的日志将
不再被路由到SpringBoot应用的日志中。
下面通过一个例子来示范Spring Boot日志的用法。按照惯例,首先创建一个 Maven项目,
后复制前面项目中的pom.xml文件,并在该文件中只保留spring-boot-starter-web.jar依赖。
定义如下控制器类。
@RestController
public class HelloController
{
Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping
public Map<String, Object> hello()
{
logger.trace("-------TRACE级别的日志-------");
logger.debug("-------DEBUG级别的日志-------");
logger.info("-------INFO级别的日志-------");
logger.warn("-------WARN级别的日志-------");
logger.error("-------ERROR级别的日志-------");
return Map.of("hello", "Hello");
}
}
.上面5行粗体字代码执行了日志输出,它们使用的5个方法代表了5种不同的日志级别:
TRACE < DEBUG < INFO < WARN < ERROR
综合各种日志框架来看,它们总共支持如下几种日志级别:
ALL < TRACE < DEBUG < INFO < WARN < ERROR
其中,ALL代表输出所有日志; OFF代表关闭所有日志: FATAL 是Log4j增加的一种日志没
别,代表“致命错误”,它的级别比普通的ERROR (错误)级别更高。
提示
由于Spring Boot并不支持FATAL级别,因此FATAL级别会被自动转换为ERROR 级别,
日志系统有一个“潜规则”:只有当日志输出方法的级别高于或等于日志的设置级别时,该日
志才会实际输出。
举例来说,假如将日志级别设为INFO,只有当程序中使用info()、wam()、 error()方法输出日
志时,日志才会实际输出,而使用trace()、debug()方法输出的日志会被忽略;假如将日志级别设为
WARN,只有当程序中使用warn0、error()方法输 出日志时,日志才会实际输出,而使用trace()、
debug()、info()方法输出的日志会被忽略;假如将日志级别设为ERROR,只有当程序中使用error()
方法输出日志时,日志才会实际输出,而使用trace()、debug()、info()、warn()方法输出的日志都会
被忽略。
由此可见,将日志的级别设得越高(比如设为ERROR),应用程序输出的日志就越精简,应
用的运行性能就越好一道理很简单, 所有日志都要输出到I/O节点(文件或数据库),这些都是
有性能开销的;将日志的级别设得越低( 比如设为DEBUG),应用程序输出的日志就越详细,应
用运行过程的记录就保存得越完整,但性能也越低。因此,当项目处于开发、测试、试运行阶段时,
通常会将日志级别设得低一- 些,从而记录相对完整的运行过程,以便后期调试、优化应用:当项目
处于实际运行阶段时,通常会将日志级别设得高一些,从而保证良好的性能。
该示例的主程序并没有任何特别之处,调用SpringApplication的run()方法运行Spring Boot应
用即可。运行主类启动Spring Boot应用,访问“http:/localhost:8080/”测试上面的hello()方法,即
可在控制台看到如下日志输出:
从上面的输出可以看到,Spring Boot默认只输出INFO、WARN和ERROR级别的日志,这说
明SpringBoot应用默认设置的日志级别为INFO。
如果查看Spring Boot启动过程的话,则可以看到整个控制台输出如图2.9所示的日志。
从Spring Boot 启动日志可以清楚地看到,Spring Boot应用默认的日志级别是INFO,因此在
图2.9所示的控制台中只能看到INFO级别的日志;现在看不到ERROR级别的日志,那是因为应
用启动并没有出错,只有当程序出现错误时,Spring Boot才会输出ERROR级别的日志。
从图2.9还可以看到,SpringBoot输出的每条日志都包括如下信息。
➢日期和时间: 时间精确到毫秒。
➢日志级别: ERROR、 WARN、INFO、DEBUG或TRACE。
➢进程ID。
➢三个减号(–) :分隔符。
线程名:用方括号括起来的是线程名(在控制台输出时可能会被截断)。
日志名:通常就是输出日志的类的完整类名(为了便于阅读,包名经常被简写,比如o.s.w其实代表了org.springframework.web) 。
➢日志信息。
要改变日志的设置级别,有以下几种方式:
➢通过debug- true或trace=true等属性(可通过配置文件、命令行参数、系统变量、OS 环境变量等方式)改变整个SpringBoot核心的日志级别。
➢通过logging.level.=属性 ( 可通过配置文件、命令行参数、系统变量等方式)设置指定日志的日志级别。其中代表日志名,通常就是包名或全限定类名,而level则可以是trace、debug、 info、 warn 和error 等级别。
例如,为应用程序添加如下命令行参数:
-- trace
上面的命令行参数设置整个Spring Boot应用的日志级别为trace(当然,也可设为其他级别),
再次运行主类启动Spring Boot应用,此时可以看到如图2.10所示的日志。
从图2.10可以看到,此时输出了大量TRACE级别的日志。如果仔细查看这些日志,则可以
发现有不少前面介绍的内容,比如第一行 显示了当前应用的Profle的active属性为“[ ]”,这表明
设置激活的Profile,因此Spring Boot自动选择default作为活动Profle.
具体读取顺序
接下来的TRACE日志显示了SpringBoot尝试依次加载如下配置文件:
Processing imports [optional:file:./config/*/]
=======================================
file:./config/*/application.yaml
file:./config/*/application.yml
file:./config/*/application.xml
file:./config/*/application.properties
file:./config/*/application-default.yaml
file:./config/*/application-default.yml
file:./config/*/application-default.xml
file:./config/*/application-default.properties
===============================================
Processing imports [optional:file:./config/]
file:./config/application.yaml
file:./config/application.yml
file:./config/application.xml
file:./config/application.properties
file:./config/application-default.yaml
file:./config/application-default.yml
file:./config/application-default.xml
file:./config/application-default.properties
===============================================
Processing imports [optional:file:./]
file:./application.yaml
file:./application.yml
file:./application.xml
file:./application.properties
file:./application-default.yaml
file:./application-default.yml
file:./application-default.xml
file:./application-default.properties
===============================================
Processing imports [optional:classpath:/config/]
classpath:/config/application.yaml
classpath:/config/application.yml
classpath:/config/application.xml
classpath:/config/application.properties
classpath:/config/application-default.yaml
classpath:/config/application-default.yml
classpath:/config/application-default.xml
classpath:/config/application-default.properties
===============================================
[optional:classpath:/]
classpath:/application.yaml
classpath:/application.xml
classpath:/application.properties
classpath:/application-default.yaml
classpath:/application-default.yml
classpath:/application-default.xml
classpath:/application-default.properties
总结
优先加载的配置文件将会被后加载的配置文件覆盖,但只覆盖相同的属性,不相同的属性则会叠加,并且List集合只能出现一个,如果出现多个将会被后面的配置的List集合覆盖,而Map集合只要key不相同,则会将这个key-value添加这个Map集合中,如果出现key相同则将会被后面的配置Map集合中的key-value给覆盖
还记得Spring Boot 加载配置文件的几个路径吗?类加载路径的根路径→ 类加载路径下的
/config子目录→当前路径→当前路径下的/config子目录→当前路径下/config子目录的任意直接子
目录。
图2.10所显示的就是加载“当前路径下/config子目录的任意直接子目录”下的配置文件;在
加载配置文件时,先加载与Profile无关的配置文件,后加载当前活动Profile(default)对应的配置文件;
配置文件有两种形式: YAML (后缀可能是.yml、.yaml) 和属性文件(后缀为.properties)。
提示:
图2.10所显示的只是Spring Boot启动日志的一部分,通过仔细查看TRACE级别:
的启动日志,可以发现很多Spring Boot 的运行原理,TRACE级别的日志详细记录了!
Spring Boot的运行过程。
当启用trace模式时,Spring Boot的核心日志(包括嵌入式容器、Hibernate 和整个Spring产品
线)将被设为TRACE级别。需要说明的是,启用trace模式不会将其他程序组件的日志级别设为
TRACE。
当启用debug模式时,Spring Boot的核心日志( 包括嵌入式容器、Hibernate 和Spring Boot)
将被设为DEBUG级别。需要说明的是,启用debug模式不会将其他程序组件的日志级别设为
DEBUG。
访问“http://localhost:8080/”测试上面的hello()方法,在控制台依然会看到如下日志输出:
由此可见,通过debug= true或trace=true属性改变的只是Spring Boot核心的日志级别,对程序
组件本身的日志级别并没有影响。
若要设置程序组件本身的日志级别,则要通过logging,level.=属性来设置。
例如如下配置代码:
# 将根日志的级别设为WARN
logging.level.root=WARN
# 将org.springframework.web包及其包下的所有日志级别设为DEBUG
logging.level.org.springframework.web=DEBUG
# 将org.hibernate包及其包下的所有日志级别设为ERROR
logging.level.org.hibernate=ERROR
#将org.cazyit.app包及其子包下的所有日志级别设为DEBUG
logging.level.org.cazyit.app=DEBUG
#将org.crazyit. app .controller.HelloController类的日志级别设为TRACE
logging.level.org.crazyit.app.controlller.HelloController=TRACE
将上面应用的application.properties文件改为如下形式。
logging:
level:
# 将org.crazyit.app包及其子包下所有日志级别设为TRACE
org.crazyit.app: trace
删除命令行参数-trace (不再以trace模式启动应用),然后运行主类启动Spring Boot应用,再次访问“http://localhost:8080/”测试上面的hello()方法,在控制台依然会看到如下日志输出:
在掌握了上面的知识之后,不难发现:在Spring Boot应用中为任何第三方框架配置日志都祖
简单,只要将该框架的核心包的日志级别设为DEBUG即可。
比如经常有人问,Spring Boot怎么配置才能让MyBatis输出它执行的SQL语句呢?非常简单:
logging .level.=debug
也有人问: Spring Boot怎么配置才能看到Redis的详细执行过程?非常简单:
logging. level. io. lettuce.core=debug
Spring Boot 整合Redis默认使用Lettuce依赖,而io.lettuce.core 就是Lettuce组件核心API所
在的包。
还有人问:SpringBoot怎么配置才能看到MongoDB的详细执行过程?非常简单:
logging. level. com. mongodb=debug
上面com.mongodb就是MongoDB核心API所在的包。
从图2.10可以看到,TRACE和INFO日志颜色是绿色的。只要控制台支持ANSI颜色特性,Spring Boot就会以不同颜色来区分不同级别的日志,从而增加日志的可读性。不同日志级别对应的默认颜色如下。
➢FATAL:红色。
➢ERROR:红色。
➢WARN:黄色。
➢INFO: 绿色。
➢DEBUG: 绿色。
➢TRACE: 绿色。
Spring Boot 允许通过spring.output. ansi enabled属性设置是否用不同颜色来区分不同级别的日
志,该属性支持如下属性值。
➢always: 总是启用。
➢detect:自动检查。如果控制台支持ANSI颜色特性,则启用。这是默认值。
➢never:不启用。
如果要改变控制台的日志格式,则可通过logging.pttern.console
属性进行设置。该属性的默认
值为:
%clr (%d{${LOG_DATEFORMAT PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}) {faint}
%clr (${LOG_ LEVEL_PATTERN:-%5p}) %clr(${PID:- }) {magenta} %clr(---) (faint} %clr(
[%15.15t]) {faint}
%clr (%-40.401ogger{39}){cyan} $clr(:){faint} %m%n${LOG_ EXCEPTION_CONVERSION_WOR
D: -%wEx}
上面配置由多个“%clr(输出内容){颜色值}”片段组成,每个片段都代表一个输出元素, 其中{颜
色值}用于指定该片段的颜色。此处的颜色值支持如下几个值。
➢blue: 蓝色。
➢cyan:青色。
➢faint: 原色。
➢green: 绿色。
➢magenta: 紫红色。
➢red: 红色。
➢yellow: 黄色。
如果不指定颜色值,直接使用“%cIr(输出内容)”,则表明将该内容以当前日志级别对应的颜
色输出。
例如,在上面的默认配置中包含了如下片段。
➢%cIr(${LOG LEVEL PATTERN:-%5p}):表明以日志级别对应的颜色来输出日志级别字符串。
➢%cIr($ {PID:- }){magenta}:表明以紫红色来输出进程ID。
%(r(--){faint}: 表明以原色来输出三个减号(---) 。
假如希望应用在控制台输出日志时不要显示日期、时间,且以日志级别对应的颜色来输出日志
言息,还以蓝色输出三个减号(--)
, 则可将application.yml 文件改为如下形式。
logging:
level:
# 将org.crazyit.app包及其子包下所有日志级别设为TRACE
org.crazyit.app: trace
pattern:
console: "%clr(${LOG_LEVEL_PATTERN:%5p}) %clr(${PID:- }){magenta}
%clr(---){blue} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan}
%clr(:){faint} %clr(%m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx})"
上面的粗体字代码为控制台日志配置了日志格式。再次启动Spring Boot应用,访问
"http:localhost:8080”,即可在控制台看到日志输出
注意
有一点需要指出的是,logging.pattern.console
属性仅当使用Logback日志实现时才有效。
Spring Boot 默认只将日志输出到控制台,不输出到文件。如果要将日志输出到文件,则可为
Spring Boot设置如下两个属性之一。
logging.file: 设置日志文件。
logging pat:设置日志文件的目录。使用默认的spring.log作为文件名。
同样,Spring Boot默认只将INFO、WARN、ERROR三个级别的日志输出到文件。
当日志文件达到10MB时,会自动重新使用新的日志文件。若要改变这个设置,对于Logback
日志实现( Spring Boot默认采用该日志实现),则可直接使用aplication.properties(或application.yml)
设置:对于其他日志实现,则需要通过对应的日志设置文件来设置。比如采用Log4j日志实现,则
需要使用log4j.xml文件进行设置。
当使用Logback日志实现时,可通过表2.5所示的属性进行设置。
下面同样以一个例子来示范Spring Boot 日志的用法。按照惯例,首先创建一一 个Maven项目
然后复制前面项目中的pom.xml文件,并在该文件中只保留spring-boot-starter-web.jar 依赖。
定义如下控制器类。
@RestController
@Slf4j
public class HelloController
{
@GetMapping
public Map<String, Object> hello()
{
log.trace("-------TRACE级别的日志-------");
log.debug("-------DEBUG级别的日志-------");
log.info("-------INFO级别的日志-------");
log.warn("-------WARN级别的日志-------");
log.error("-------ERROR级别的日志-------");
return Map.of("hello", "Hello");
}
}
上面的控制器类使用了@SIf4j注解修饰,接下来程序就可“凭空”使用log对象的trace(、debug)
等方法来输出日志,那这个log对象是从哪里来的?
这是因为本例增加了Lombok 小工具,这个小工具专门通过各种注解来生成常用的代码,比如
如下常用注解。
➢@Getter: 为所有实例变量生成getter 方法。
➢@Setter: 为所有非final实例变量生成setter方法。
➢@ToString: 自动生成toString0方法。
➢@EqualsAndHashCode: 自 动生成equals(和hashCode(方法。
➢@AllArgsConstructor: 自动生成带所有参数的构造器。
➢@NoArgsConstructor: 自动生成无参数的构造器。
➢@Data: 自动生成一 个数据类,相当于@Getter、@Setter. @ToString、@EqualsAndHashCode和@NoArgsConstructor等注解的组合。
➢@Log、@Log4j、@Log4j2、 @Slf4j、 @XSlf4j、@CommonsLog、@JBossLog、@Flogger:
为对应的日志实现生成一个日志对象。
由于Lombok可以自动生成getter setter 方法和数据类,因此有不少项目选择添加Lombok这
个小工具,从而避免为应用数据类编写getter 和stter方法。不过一般不建议这么做, 毕竟使用IDE
工具自动生成getter()、stter()、 toString()、 equals()等 方法也很简单。
不建议的原因
https://blog.csdn.net/weixin_38405253/article/details/111829077
为了在应用中使用Lombok,需要做以下两件事情。
➢为项目添加Lombok依赖。例如,在pom.xml文件中添加如下配置:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
➢为IntelliJ IDEA添加Lombok插件。这是为了让IntelliJI IDEA在定义Java类时能识别Lombok
生成的代码。为IntelliJ IDEA安装插件,只要通过其主菜单“File”→“Settings”→“Plugins”
搜索到要安装的插件,然后安装对应的插件即可。
接下来将application.yml文件改为如下形式。
logging:
level:
# 将org.crazyit.app包及其子包下所有日志级别设为TRACE
org.crazyit.app: trace
file:
# 指定日志文件的输出目录,默认文件名为spring.log
# path: logs/
# 指定日志文件
name: my.log
运行主类启动Spring Boot应用,将会看到Spring Boot在应用的根目录下生成个 my.log 日志文件。
如果取消上面"path:logs/“行的注释,而将” name: my.log"行注释掉,再次运行主类启动Spring
Boot应用,将会看到Spring Boot在应用的根目录下生成一个logs 目录,并在该目录下生成了
spring.log文件。
将日志输出到文件时,可通过loggingpattem.fle属性指定日志格式。该属性的默认值为:
%{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS] }
$(LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40. 4010gger{39} :
%m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}
与控制台默认的日志格式相比,输出到文件的默认格式主要就是没有颜色设置。同样,该属性
也仅对Logback有效。
所谓日志组,就是将多个包、类组合在一起, 起一个名字,以后可直接对该组设志级别
对该组设置日志级别,就相当于同时为该组内的所有包及其子包、类统-一 设置了日志级别。
例如,为上面的示例再增加一-个控制器组件,其代码如下。
@RestController
@Slf4j
public class FkController
{
@GetMapping("/fk")
public Map<String, Object> hello()
{
log.trace("-------TRACE级别的日志-------");
log.debug("-------DEBUG级别的日志-------");
log.info("-------INFO级别的日志-------");
log.warn("-------WARN级别的日志-------");
log.error("-------ERROR级别的日志-------");
return Map.of("hello", "Hello");
}
}
上面的FCOntoller组件位于org fkjava. app, cotrollor包下,接下来就可在aplicationym文件
中将ogcazyjit app和org fjavap两个包配置为一个组, 并设置整个组的日志级别。现在将本例
的配置文件改为如下形式。
logging:
group:
# 将org.crazyit.app和org.fkjava.app两个包定义成fkapp组
fkapp: org.crazyit.app, org.fkjava.app
level:
# 将fkapp组对应的包及其子包的所有日志级别设为TRACE
fkapp: trace
上面第行粗体字代码将 org.crazyit.app 和org.fkjava.app 两个包定义为fkapp组,第二行粗体
字代码将fkapp组的日志级别设为TRACE。
有一-点需要提醒的是,由于FkController控制器位于org. fkjava.app.controller包下,它与应用
主类App的包(org.crazyit.app) 不存在子包关系,因此Spring Boot不会自动加载该控制器组件。
所以,将App类上的@SpringBootApplication注解改为如下形式:
@SpringBootApplication(scanBasePackages = {"org.crazyit.app", "org.fkjava.app"})
运行该示例,可以看到日志组内两个包下的所有日志级别都是TRACE。
如果想改变Spring Boot的底层日志实现(放弃Logback作为底层日志实现),则只需要如下两
步。
➊去掉Logback依赖库,添加新日志实现的依赖库。
❷在类加载路径的根路径下为新日志实现提供对应的配置文件。
Spring Boot 默认从类加载路径的根路径下加载日志框架的配置文件,也可通过logging.config
属性来设置新的加载路径。
Spring Boot既可根据底层依赖库自动选择合适的日志实现,也可通过org.springframework. boot.
logging.LoggingSystem属性显式指定日志实现。该属性的值可以是LoggingSystem实现类的全限定
类名(比如Log4J2LoggingSystem、LogbackLoggingSystem、Javal oggingSystem等类的全限定类名);
也可以将该属性的值指定为none,这意味着彻底关闭SpringBoot的日志系统。
注意
由于日志初始化会在ApplicationContext创建之前完成,因此不可能通过Spring
Boot配置文件来配置logging. config. org.springframework. bo.logging LoggingSystem
等日志控制属性,只能通过系统属性来设置这些属性。
根据所选日志系统的不同,Spring Boot会自动加载如表2.6所示的配置文件。
注意
Spring Boot 推荐使用带-spring的配置文件,比如对于Logback日志系统,使用
logback-spring.xml作为配置文件比用logback.xml 文件更好。此外,Spring Boot建议
尽量避免使用JUL日志系统实现,因为JUL的类加载机制会导致一些问题。
Spring Boot提供了如表2.7所示的属性(或系统属性)用于对日志进行定制。
前面已经通过示例介绍了上面的logging file.name、logging file.path、 logging pattern. console、
logging.pattern.fle属性,其他属性的说明也比较明了,此处不再通过示例介绍。
注意*
如果打算在日志属性中使用占位符,请记住要用Spring Boot语法,而不是底层
日志框架的语法。尤其是使用Logback 时,应该使用英文冒号(:)作为属性名和默
认值的分隔符,而不是使用冒号减号(:- )。
下面通过一个示例来介绍对Logback 日志进行定制。由于Logback 是Spring Boot默认选择的日志实现,因此Spring Boot为Logback提供了一些通用的配置文件,开发者只要导入这些配置
件即可使用文件中预定义的配置。这些文件都位于org/springframework/boot/logging/logback/路径下
其中常用的有如下几个。
defaults.xml: 提供了转换规则及各种通用配置。
console-appender.xml:定义了一个 ConsoleAppender,用于将日志输出到控制台。
file-appender.xml: 定义了-个RollingFileAppender, 用于将日志输出到文件。
下面是一份 典型的logback-spring.xml配置文件。
<configuration>
<include resource= "org/springframework/boot/logging/logback/defaults.xml"/>
<include resource=" org/springframework/boot/logging/logback/console-appender.xml"/>
<root level=" INFO">
<appender-ref ref="CONSOLE"/>
root>
<logger name="org.crazyit.app" leve1="DEBUG"/>
configuration>
在上面的配置文件中还可指定如下占位符。
${PID}: 代表当前进程ID.
${LOG_FILE}:代表是否通过外部配置设置了logging fle.name属性。
${LOG_PATH}:代表是否通过外部配置设置了logging.file.path 属性。
${LOG_EXCEPTION_CONVERSION_WORD}:代表是否通过外部配置设置了
logging.exception-conversion-word属性。
${ROLLING_FILE_NAME_PATTERN}: 代表是否通过外部配置设置了
logging.pattern.rolling-file-name属性。
假如项目已经上线,不再需要Logback在控制台生成日志,只保留文件日志即可,则可添加如
下logback-spring.xml配置文件。
重点
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}"/>
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<root level="INFO">
<appender-ref ref="FILE" />
root>
configuration>
上面配置显式指定了仅将日志输出到文件,不再输出到控制台,因此必须在application.yml文
件中指定logging file.name或loggingfile.path属性。例如如下配置文件。
logging:
level:
# 将org.crazyit.app包及其子包下所有日志级别设为TRACE
org.crazyit.app: trace
file:
# 指定日志文件的输出目录,默认文件名为spring.log
# path: logs/
# 指定日志文件
name: my.log
再次运行该项目,将看到该Spring Boot应用不会在控制台输出日志,仅在当前目录的mylog
文件中输出日志。
Log4j本身已经恨优秀,而Log4j2则完全是Log4j的重新设计,因此放弃Logback,.改为使用
Log4j2也是不错的技术选择。若要让Spring Boot底层使用Log4j2也很简单:正如前面所介绍的,
只需要去除Logback依赖库,并添加Log4j2依赖库即可。
将pom.xml文件的依赖部分改为如下形式。
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-log4j2artifactId>
dependency>
dependencies>
上面第一段粗体字配置去除了spring-boot-starter-logging.jar 依赖,这里并不是单独地去除
Logback依赖库,因为Spring Boot为Log4j2也提供了对应的Starter,所以可以直接去除Spring Boot
默认的日志Starter.
上面第二段粗体字配置添加了spring-boot-starter-log4j2.jar 依赖,它会传递性添加它所依赖的
Log4j2依赖库和SLF4J依赖库,从IntelliJ IDEA的Maven面板上的依赖关系中可以看到如图2.12
所示的依赖jar包
通过上面的配置,将项目的底层日志框架改成了Log4j2,但得益于Spring Boot日志机制的抽
象机制,上层程序使用日志没有任何改变,日志效果也没有任何改变一如果不是 有经验的开发者
则可能都不知道底层日志框架已从Logback改成了Log4j2.
如果要对Log4j2做自定义的详细配置,则既可通过log4j2.yml ( 或log4j2.yaml)进行配置
也可通过log4j2.json (或log4j2.jsn) 进行配置。log4j2.yml 或log4j2.json配置文件应遵守Log4j2
本身的语法。
Spring Boot 默认选择Logback作为底层日志实现,也对Logback进行了一些有用的扩展, 通
过logback-spring.xml文件可配置并使用这些扩展功能。
注意
若要使用Logback的扩展功能,就不能使用logback.xml配置文件,因为该配置文件的加载时机太早了,Spring Boot的其他基础功能还没来得及加载。
Spring Boot对Logback的扩展主要体现在如下两个方面。
➢与Profile相关的日志。
➢环境属性。
首先看与Profle相关的日志配置。前面已经说过,当项目处于开发阶段时,希望生成更多的
日志;当项目处于产品上线阶段时,希望生成较少的日志,从而提高运行速度。这个需求正好符治
前面2.23节介绍的Profile,而Logback也可设置与Profle相关的日志。
在logback-spring.xml文件中可指定一个
元素,该元素可指定一个 name属性,
该name属性可被指定为Profile名或Profile 表达式。当name属性被指定为Profile名时,表明该配
置仅当特定的Profile被激活时有效:当name属性被指定为Profile 表达式时,表明仅当该Profile
表达式有效时,
前活动Profile不是dev,
例如下面的配置文件。
<configuration>
<springProfile name="default | dev | test">
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
root>
<logger name="org.crazyit.app" level="DEBUG"/>
springProfile>
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
root>
<logger name="org.crazyit.app" level="INFO"/>
springProfile>
configuration>
上面的配置文件中包含了两个
default、dev、test有效,在该元素中配置的日志级别为DEBUG,这意味着当活动Profle为default、
dev、test 时,应用的日志级别为DEBUG;后一个
配置的日志级别为INFO,这意味着当活动Profile为prod时,应用的日志级别为INFO。
控制层controller
@RestController
@Slf4j
public class HelloController
{
private final DataSource dataSource;
@Autowired
public HelloController(DataSource dataSource)
{
this.dataSource = dataSource;
}
@GetMapping
public Map<String, String> hello() throws SQLException
{
log.trace("-------TRACE级别的日志-------");
log.debug("-------DEBUG级别的日志-------");
log.info("-------INFO级别的日志-------");
log.warn("-------WARN级别的日志-------");
log.error("-------ERROR级别的日志-------");
return Map.of("数据库",
dataSource.getConnection().getCatalog());
}
}
配置文件 application.yml
spring:
config:
activate:
on-profile: prod
datasource:
# 指定连接prod数据库
url: jdbc:mysql://localhost:3306/prod?serverTimezone=UTC
username: root
password: 123456
logging:
file:
# 指定日志文件的输出目录,默认文件名为spring.log
# path: logs/
# 指定日志文件
name: my.log
---
spring:
config:
activate:
on-profile: dev
datasource:
# 指定连接dev数据库
url: jdbc:mysql://localhost:3306/dev?serverTimezone=UTC
username: root
password: 123456
logging:
file:
# 指定日志文件的输出目录,默认文件名为spring.log
path: logs/
# 指定日志文件
# name: my.log
---
spring:
profiles:
active: prod
此外,Logback 的配置文件还可读取Spring Boot 的配置属性(通过aplicationproperte系
统属性、命令行参数等方式配置的属性)。在该配置文件中可通过sprigrpry…元素来获取配
置属性,该元素支持如下几个属性。
➢name:为读取到的属性值指定名字。
➢source: 指定读取Spring Boot的哪个配置属性。此处推荐使用“烤串”写法的名称,Spring
Boot解析该配置属性时能有效地使用宽松绑定。
➢scope:指定存储该配置属性的作用域。
➢defaultValue: 当该配置属性可能不存在时,此属性用于指定默认值。
例如如下配置片段:
<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host"
defaultValue="localhost"/>
<appender name= " FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender">
<remoteHost>${fluentHost} remoteHost>
---
appender>
SpringBoot最大的功劳就在于自动配置,它的自动配置功能能根据类加载路径下的JAR依赖自动配置基础设施。例如,当Spring Boot检测到类加载路径下包含了MySQL依赖,且容器中没有配置其他任何DataSource时,SpringBoot就会自动启动HSQLDB数据库。
Spring Boot 的自动配置大多会根据特定依赖库自动触发,启用自动配置需要使用
@EnableAutoConfiguration注解。注意,整个应用只需要添加一个该注解,因此,通常只要将该注
解添加到主配置类( SpringApplication所运行的配置类)即可。
前面已经说过,@SpringBootApplication 注解是@Configuration、@EnableAutoConfiguration 和
@ComponentScan这三个注解的组合体,因此,在实际项目中只要用@SpringBootApplication注解
修饰主配置类,也就自然为主配置类增加了@EnableAutoConfiguration注解,从而开启了自动配置。
Spring Boo自动配置通常有个原则 (只是通常,但也有特例):只有当容器中不存 在特定类
型的Bean或特定Bean时,SpringBoot自动配置才会配置该类型的Bean或特定Bean。
例如、在项目的类加载路径下添加spring boot-start-jdbc依赖,Spring Boot将会尝试在容器
中自动配置个DataSouree Bean:但如果显示在Spring容器中配置 了个DataSource Bean, 那么
Spring Boot就不再尝试去自动配置DataSource
.
看上去似乎很智能,对不对?其实原理很简单,Spring Boot提供了一个@ConditionalOnMissingBean
注解,该注解通常与@Bean注解同时使用,这意味着只有当某个Bean不存在时,才会创建@Bea
注解配置的Bean。
比如如下配置片段:
@ConditionalOnMissingBean({DataSource.class})
@Bean
public DataSource dataSource (){
return new XxxDataSource();
}
上面配置指定只有当容器中不存在类型为DataSource的Bean时,@Bean注解所配置的Bean
才会生效。
再比如如下配置片段:
@ConditionalOnMissingBean(name = "dataSource")
@Bean
public DataSource dataSource (){
return new XxxDataSource ();
}
上面配置指定只要容器中不存在ID为dataSource的Bean,@Bean注解所配置的Bean就会
生效。
由于Spring Boot的自动配置具有一定的透明性(有时候无法准确地知道Spring Boot自动配置了哪些Bean),因此Spring Boot为应用程序提供了“--debug
"开关。
如果通过“--debug
”开关启动SpringBoot应用,则将为核心组件开启DEBUG级别的图志,
并将自动配置的相关日志输出到控制台。
在某些情况下,如果希望应用禁用特定的自动配置类,则可通过@EnableAutoConfiguration注
解的如下属性来指定。
➢exclude: 该属性的值可以是一个Class 数组,用于禁用一个或多 个自动配置类。
➢excludeName:与前一个属性的作用基本相同,只不过它指定一个或者多个自动配置类的完整类名的字符串形式。
而在实际项目中通常使用@SpringBootApplication注解,该注解己包含了@EnableAutoConfiguration
注解,@SpringBootApplication 注解的exclude和excludeName属性就是@EnableAutoConfiguration
注解的这两个属性的别名。
例如,如下主类上的@SpringBootApplication注解可禁用DataSourceAutoConfiguration自动配
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
SpringApplication.run(App.class, args);
}
}
上面粗体字代码使用exclude属性指定了要禁用的自动配置类,如果使用excludeName属性,
则务必指定要禁用的自动配置类的全限定类名。
此外,Spring Boot也允许在application.propertes文件中通过spring autoconfigure exclude属性
来指定要禁用的自动配置类。例如如下配置:
spring.autoconfigure.exclude=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
上面的配置片段表示禁用了DataSourceAutoConfiguration、 Redis AutoConfiguration两个自动配
置类。
最后需要说明的是,虽然这些自动配置类都使用了public修饰,但这些自动配置类唯一应该被公开使用的是它们的类名一主要通过类名来禁用它们。这些类的类体部分(如内部配置类或Bean方法)仅供Spring Boot内部使用,Spring Boot官方不建议使用自动配置类的任何成员。
自动配置其实很简单,其本质就是在容器中预配置要整合的框架所需的基础Bean。
以整合常见的MyBatis为例,直接用Spring 整合MyBatis无非就是完成如下事情:
➢配置SqlSessionFactory Bean,当然, 该Bean需要注入一个DataSource。
➢配置SqlSessionTemplate Bean,将上面配置的SqlSessionFactory注入该Bean即可。
➢注册Mapper组件的自动扫描,就是相当于添加mybatis:scan.../元素。
所谓的自动配置,说穿了非常简单,无非就是由框架提供一个@Configuration 修饰的配置类(相
当于传统的XML配置文件),在该配置类中用@Bean预先配置默认的SqlSessionFactory、SqlSessionTemplate,并注册Mapper组件的自动扫描即可。
打开MyBatis为整合Spring Boot提供的自动配置类: MybatisAutoConfiguration 的源文件,可
以看到如下源代码:
/**
Mybatis 的Auto-Configuration 。 提供SqlSessionFactory和SqlSessionTemplate 。
如果使用org.mybatis.spring.annotation.MapperScan ,或者将配置文件指定为属性,则将考虑这些,
否则此自动配置将尝试根据根自动中或下的接口定义注册映射器配置包
*
* @author Eddú Meléndez
* @author Josh Long
* @author Kazuki Shimizu
* @author Eduardo Macarrón
*/
@org.springframework.context.annotation.Configuration
// 当SqlSessionFactory和SqlSessionFactoryBean存在时生效
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
//当单列DateSource Bean存在生效时
@ConditionalOnSingleCandidate(DataSource.class)
// 启用MybatisProperties属性处理类
@EnableConfigurationProperties(MybatisProperties.class)
//之后自动配置DataSourceAutoConfiguration 和 MybatisLanguageDriverAutoConfiguration
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
//实现InitializingBean接口 该接口中的afterPropertiesSet()生命周期方法
//将会在bean初始化后自动被调用
public class MybatisAutoConfiguration implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.class);
//MybatisProperties类负责加载配置属性
private final MybatisProperties properties;
//下面的变量用于保存Mybayis的拦截器和类型处理器
private final Interceptor[] interceptors;
private final TypeHandler[] typeHandlers;
private final LanguageDriver[] languageDrivers;
private final ResourceLoader resourceLoader;
private final DatabaseIdProvider databaseIdProvider;
private final List<ConfigurationCustomizer> configurationCustomizers;
//============================================================================
public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
this.properties = properties;
this.interceptors = interceptorsProvider.getIfAvailable();
this.typeHandlers = typeHandlersProvider.getIfAvailable();
this.languageDrivers = languageDriversProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = databaseIdProvider.getIfAvailable();
this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
}
// 重现InitializingBean接口 该接口中的afterPropertiesSet()方法
@Override
public void afterPropertiesSet() {
checkConfigFileExists();
}
//检查配置文件是否存在
private void checkConfigFileExists() {
if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
//根据MybatisProperties 读取configLocation加载MyBatis配置文件
Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
//如果resource.exists()为false指文件加载失败 则抛出异常
Assert.state(resource.exists(), /** 1 **/
"Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
}
}
// 配置SqlSessionFactory bean
@Bean
//当SqlSessionFactory不存在是配置
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
// 创建 SqlSessionFactoryBean 工厂 Bean
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
// 注入DataSource bean
factory.setDataSource(dataSource); /** 2 **/
factory.setVfs(SpringBootVFS.class);
// 如果存在Mybatis配置文件,则应用该配置文件
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
applyConfiguration(factory);
//应用MybatisPropertion 读取到的文件
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
//应用所有拦截器
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
//应用所有DatabaseIdProvide
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
//根据包名应用TypeAliases
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
//根据父类型应用TypeAliases
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
//根据包名应用TypeHandler
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
//应用所有TypeHandler
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
//设置Mapper的加载位置
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
Set<String> factoryPropertyNames = Stream
.of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
.collect(Collectors.toSet());
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
// Need to mybatis-spring 2.0.2+
factory.setScriptingLanguageDrivers(this.languageDrivers);
if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
defaultLanguageDriver = this.languageDrivers[0].getClass();
}
}
if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
// Need to mybatis-spring 2.0.2+
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
}
//返回SqlSessionFactory bean
return factory.getObject();
}
private void applyConfiguration(SqlSessionFactoryBean factory) {
Configuration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new Configuration();
}
if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
for (ConfigurationCustomizer customizer : this.configurationCustomizers) {
customizer.customize(configuration);
}
}
factory.setConfiguration(configuration);
}
//配置SqlSessionTemplate bean
@Bean
// 当SqlSessionTemplate不存在时配置生效
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
// 如果 executorType属性存在则使用该属性创建SqlSessionTemplate
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
/**
* 这将只扫描与 Spring Boot 相同的基本包。
* 如果您想要更多功能,您可以明确使用org.mybatis.spring.annotation.MapperScan但这将使类型化映射器正常工作,
* 开箱即用,类似于使用 Spring Data JPA 存储库
*/
//定义自动扫描Mapper组件的注册器类
public static class AutoConfiguredMapperScannerRegistrar
// 实现BeanFactoryAware 该接口可以访问Springr容器
//实现ImportBeanDefinitionRegistrar接口 可配置额外的Bean
implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
//用于保存获取到的Spring容器
private BeanFactory beanFactory;
//重写ImportBeanDefinitionRegistrar 接口中的方法
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!AutoConfigurationPackages.has(this.beanFactory)) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
return;
}
logger.debug("Searching for mappers annotated with @Mapper");
//获取自动配置要处理的包
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
}
//创建 BeanDefinitionBuilder Bean 定义生成器对象
//它通过反射方式创建任意类的实例
//此处就是帮助MapperScannerConfigure实例
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
//要为创建对象设置属性
builder.addPropertyValue("processPropertyPlaceHolders", true);
builder.addPropertyValue("annotationClass", Mapper.class);
builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
Stream.of(beanWrapper.getPropertyDescriptors())
// Need to mybatis-spring 2.0.2+
.filter(x -> x.getName().equals("lazyInitialization")).findAny()
.ifPresent(x -> builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}"));
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(),
builder.getBeanDefinition()); /** 3 **/
}
//重写该方法用于获取spring容器
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
}
/**
* 如果映射器注册配置或映射器扫描配置不存在,
* 则此配置允许基于与 Spring Boot 本身相同的组件扫描路径扫描映射器
*/
@org.springframework.context.annotation.Configuration
//导入AutoConfiguredMapperScannerRegistrar注册类
@Import(org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class)
//当MapperFactoryBean 和 MapperScannerConfigurer的 bean不存在时配置自动生效
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
//实现InitializingBean接口 该接口中的afterPropertiesSet()生命周期方法
//将会在bean初始化后自动被调用
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
//重写afterPropertiesSet()方法
@Override
public void afterPropertiesSet() {
logger.debug(
"Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
}
}
}
上面的MybatisAutoConfiguration是一个 典型的自动配置类,该类使用了如下注解修饰。
➢@Configuration: 被修饰的类变成配置类。
➢@ConditionalOnClass: 当某个类存在时生效。
➢@ConditionalOnSingleCandidate: 上下文中只有一个候选者bean时才注册配置
➢@EnableConfigurationProperties: 让使用了 @ConfigurationProperties 注解的类生效,并且将该类注入到 IOC 容器中,交由 IOC 容器进行管理
➢@EnableConfigurationProperties: 前面已有介绍,用于启动属性处理类。
@AutoConfigureAfter:指定该自动配置类必须在Xxx自动配置类生效之后。
+++++++++++++++++
此处指定该自动配置类必须在DataSourceAutoConfiguration、MybatisI anguageDriver AutoConfiguration两个类生效之后,其中DataSourceAutoConfiguration是Spring Boot 本身提供的自动配置类,它负责在容器中配置一个 DataSource Bean;而MybatisLanguageDriverAutoConfiguration
则
负责在容器中配置脚本语言驱动的Bean。很明显,它们都是MyBatis的基础设施。
====================================================================
重点:mybatis自动配置原理
该自动配置类实现了InitializingBean 生命周期接口
,该接口中的afterPropertiesSet()
生命周期方法将会在该Bean初始化完成后被自动调用,这也是Spring 框架的基本内容之一。该类实现了afterPropertiesSet()
方法,该方法调用checkConfigFileExists()
方法。
checkConfigFileExists()方法的代码也很简单,它调用Spring提供的ResourceLoader
来加载配置
文件(ResourceLoader和Resource 是Spring资源访问API),它根据MybatisProperties属性处理类
(该类就是本章2.23节所介绍的用@ConfigrationProperties注解修饰的类)所读取到的configIocation
属性来加载MyBatis配置文件一如果开发 者在aplication.yml中指定了configLocation属性,但
此处根据该属性加载不到MyBatis配置文件,上面①号(Assert.state()
方法)代码将抛出异常。
MybatisAutoConfiguration自动配置类接下来定义了sqlSessionFactory()
方法, 且该方法使用了
如下注解修饰
➢
@Bean:该注解用于配置Bean。
➢@ConditionalOnMissingBean: 指定bean不在上下文中时,才初始化当前bean
这个方法的作用再明显不过了:在容器中配置一个SqlSessionFactory,
因此该方法体的第一 行代码就创建了SqlSessionFactoryBean
对象,这是一个负责生成SqlSessionFactory的工厂Bean。 接下来的②号(factory.setDataSource(dataSource)
方法)代码用于为SqlSessionFactoryBean 注入DataSource ( SqlSessionFactory需要依赖DataSource),后面的代码主要就是将MybatisProperties 读取到的属性应用到SqlSessionFactoryBean上,该方法的最后-行代码返回了SqlSessionFactory。
MybatisAutoConfiguration类接下来又定义了sqlSessionTemplate()
方法, 且该方法同样使用了
@Bean、@ConditionalOnMissingBean 两个注解修饰。该方法的作用也是显而易见的:
它负责在容器中配置SqlSessionTemplate
重点:如何配置额外的bean
MybatisAutoConfiguration类接下来定义了一个AutoConfiguredMapperScannerRegistrar内部类
该内部类实现了BeanFactoryAware、 ImportBeanDefinitionRegistrar 接口
。BeanFactoryAware是接口注入方法(类似于ApplicationContextAware), 实现该接口可用于获取Spring容器。实现ImportBeanDefinitionRegistrar接口则可用于配置额外的Bean。
任何类实现了ImportBeanDefnitinRegitar接口之后,必然会实现该接口中的如下方法:
registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)
在实现该方法时可调用BeanDefinitionRegistry参数的如下方法向容器中配置Bean:
registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
该方法的第一个参数就是要配置的BeanID,第二个参数就是要配置的BeanDefinition它会以反射方式创建目标类的实例。
上面registerBeanDefinitions()方法先获取了自动配置要处理的包,并用DEBUG级别的日志输
出了这些包名。接下来的粗体字代码创建了一个BeanDefinitionBuilder 对象,并为之设置了processPropertyPlaceHolders、annotationClass、 basePackage、 lazylnitialization 属性,因此BeanDefinitionBuilder
的作用就是负责创建一个MapperSeannerConfigurer
对象。
// 3号处的代码
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition())
;
registerBeanDefinitions()
方法中的 3 号代码调用BeanDefinitionRegistry
的registerBeanDefinition()
方法向容器中配置了一个Bean,该Bean的ID是MapperSeannerConfigurer类的类名,该Bean由BeanDefinitionBuilder返回的BeanDefinition负责生成。(其实这个方法的2个参数第一个是MapperSeannerConfigurer类的类名,第二个是getBeanDefinition()
返回的BeanDefinition)
====================================================
注意: ImportBeanDefinitionRegistrar接口
ImportBeanDefinitionRegistrar接口的作用有点类似于@Configuration注解,实现该接口的类也相当于配置类,只不过这种配置类要通过registerBeanDetinitions()方法向容器中配置Bean;而@Configuration修饰的配置类则使用@Bean注解向容器中配置Bean。
====================================================
MybatisAutoConfiguration类接下来定义了.一个MapperScannerRegistrarNotFoundConfiguration
类,该类其实没有任何实际作用,因此该类所实现的afterPropertiesSet)方法仅仅 是输出日志。重
点是该类上的注解。
➢@Configuration:该注解将被修饰类变成配置类。
@Import:导入AutoConfiguredMapperScannerRegistrar类,这样才能让该配置类在容器中配
置MapperScannerConfigurer.
➢@ConditionalOnMissingBean :条件注解。
指定只有当MapperScannerConfigurer、MapperFactoryBean类型的Bean都不存在时,该配置类才生效。原因很简单,如果容器中已有MapperScannerConfigurer 类型的Bean,说明开发者已经手动配置了MapperScannerConfigurer,那么自然就不需要这个自动配置了;如果容器中已配置了MapperFactoryBean,
说明开发者打算用工厂Bean的方式来配置Mapper组件,那么就需要Mapper组件的自动扫描功能,自然也就需要自动配置的MapperScannerConfigurer.
所以说MapperScannerRegistrarNotFoundConfiguration类没有作用,但该类上的注解有作用,
其作用就是条件性地导入AutoConfiguredMapperScannerRegistrar
类一一该类负责在容器中配置
MapperScannerConfigurer。条件是:当开发者没有手动配置MapperScannerConfigurer,且不打算用工厂Bean的方式配置Mapper组件(打算使用自动扫描方式配置)时,该自动配置类才会在容器中自动配置MapperScannerConfigurer.
====================================================================
上面对MyBatis提供的自动配置类的源代码进行了非常详细的讲解。事实上,Spring Boot 框
架的所有自动配置类基本都是这个套路,这些配置类都会大量使用@Configuration、@Bean、
@Import及@Conditional条件注解等注解,也会大量使用Spring 框架及被整合框架的核心API。如果刚开始阅读这些源代码时存在一定的不适应,那是正常的;但如果静下心来认真看这些源代码还看不明白,那不是Spring Boot知识有缺陷,而是对Spring框架的API还不熟,建议先认真学习Spring框架本身。
========================================================
当开发完自动配置类之后,还需要使用META-INF/spring.factories
文件来定义自动配置类,应该在该文件中以“org.springframework.boot.autoconfigure.EnableAutoConfiguration"为key列出所有自动配置类。还是以MyBatis整合Spring Boot的自动配置为例,打开其JAR包中的META-INF/spring factories
文件,可以看到如下代码:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
上面列出了MybatisLanguageDriverAutoConfiguration类和MybatisAutoConfiguration类,它们就是MyBatis为整合Spring Boot所提供的自动配置类。前面详细讲解了MybatisAutoConfiguration类,而MybatisLanguageDriverAutoConfiguration自动配置类则用于在容器中配置各种脚本语言驱动的Bean,该自动配置类的源代码更简单,就是通过大量@Configuration和@Bean注解在容器中配置Bean,读者可自行查看该类的源代码。
========================================================
重点:xxxAutoConfiguration自动配置类注入spring.factorices文件中
自动配置类只能通过META-INF/spring factories进行加载,并确保它们处于一个特殊的包空间内,尤其是不能让它们变成普通@ComponentScan的目标。此外,自动配置类也不应该用@ComponentScan来扫描其他组件,如果确实需要加载其他配置文件,则应使用@Import显式指定重要加载的配置类。
如果要为自动配置类指定它们的加载顺序,Spring Boot则提供了如下两个注解。
@AutoCofigureAfter: 指定被修饰的类必须在-个或多个自动配置类加载之后加载。
@AuoCofijureBefore: 指定被修饰的类必须在一个或名个自动配置类加载之前加载。
如果自动配置包中包含多个自动配置类,且要求以特定的顺序来加载这些自动配置类,则可用@AutoConfigureOrder注解来修饰它们。@AutoConfigureOrder 注解完全类似于Spring 框架原有的
@Order注解,只不过它专门用于修饰自动配置类。
@AutoConfigureOrder 指定配置类初始化顺序,越小初始化越早
由此可见,创建自动配置的关键就是开发自动配置类,而开发自动配置类除了要熟练掌握Spring及被整合框架的API,还要熟练使用SpringBoot提供的条件注解。下面就来详细介绍Spring Boot的条件注解。
========================================================
条件注解可用于修饰@Configuration类或@Bean方法等,表示只有当特定条件有效时,被修
的配置类或配置方法才会生效。正是得益于条件注解的帮助,Spring Boot 的自动配置才能执行
似于如下的智能行为。
➢当SpringBoot检测到类加载路径包含某个框架时,会自动配置该框架的基础Bean。
➢只有当开发者没配置某些Bean时,SpringBoot才会在容器中自动配置对应的Bean.
➢只有当开发者配置了某些属性时,Spring Boot才会在容器中自动配置对应的Bean。
总结起来,Spring Boot的条件注解可支持如下几类条件。
以下均为重点:springboot里常用条件注解
➢类条件注解: @ConditionalOnClass、 @ConditionalOMissinglass
如何具体使用
➢Bean条件注解: @ConditionalOnMissingBean、CndionalOningleCandidate、@ConditionalOnBean、@ConditionalOnMissingFilterBean.
如何具体使用
如何具体使用@ConditionalOnMissingFilterBean
➢
属性条件注解: @ConditionalOnProperty。
如何具体使用
➢资源条件注解: @ConditionalOnResource。
如何具体使用
➢Web 应用条件注解: @ConditionalOnWebAplication、@ConditionalOnNotWebApplication.
@ConditionalOnW arDeployment。 如何具体使用
➢SpEL 表达式条件注解: @ConditionalOnExpression
. 如何具体使用
➢特殊条件注解: @ConditionalOnCloudPlatfom @ConditioalOnJava、@ConditionalOnJndi
@ConditionalOnRepositoryType。 如何具体使用
上面这些条件注解都是基于Spring的@Conditional条件注解
变化而来的。
================================================
@ConditionalOnClass、 @ConditionalOMissinglass具体使用
类条件注解有两个,即CondialonClass 和 @ConditionalOMissinglass,分别表示某些类存在或者不存在时被修饰的类或被修饰的方法生效。@ConditionalOnClass 注解可通过value或name属性
指定它所要求存在的类,其中value属性值是被检查类的Class对象,name 属性值是被检查类的字符串形式的全限定类名,既然是检查目 标类是否存在,那么通常用name属性值居多
;
@ConditionalOnMissingClass则只能通过value属性指定它所要求不存在的类,value 属性值只能是
被检查类的字符串形式的全限定类名,既然要确保该类不存在,那么该类对应的Class通常也就
不存在了。
下面通过例子来示范@ConditionalOnClass注解的用法。首先创建一个 Maven项目,然后复制前面项目中的pom.xml文件,仅保留spring-boot-starter.jar依赖库。
定义如下配置类。
@Configuration(proxyBeanMethods = false)
// 仅当com.mysql.cj.jdbc.Driver类存在时该配置类生效
@ConditionalOnClass(name = "com.mysql.cj.jdbc.Driver")
public class FkConfig
{
@Bean
public MyBean myBean()
{
return new MyBean();
}
}
上面配置类使用了@ConditionalOnClass(name = “com.mysql.cj.jdbc.Driver”)修饰,意味着只有com.mysql.cj.jdbc.Driver类存在时,该配置生效。此处的com.mysql.cj.jdbc.Driver类可以是任何的类。
主启动类
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
var ctx = SpringApplication.run(App.class, args);
System.out.println(ctx.getBean("myBean"));
System.out.println(ctx.getBean("fkConfig"));
}
}
如果未配置mysql驱动依赖则springboot无法找到com.mysql.cj.jdbc.Driver类,将会出现异常。
配置mysql驱动依赖运行结果如下:
@ConditionalOnMisingClass注解的用法与@ConditionalOnClass注解的用法大致相同,只不过
它要求的是被检查类不存在,这样被它修饰的配置类或方法才会生效。
=========================================================================
@ConditionalOnMissingBean、@ConditionalOnBean、 @ConditionalOnSingleCandidate
@ConditionalOnMissingBean、@ConditionalOnBean、 @ConditionalOnSingleCandidate 都用于要
求目标Bean存在或不存在(带Missing的注解要求目标Bean不存在)。它们大致可指定如下属性。
➢Class extends Annotation>[] annotation:指定要检查的Bean必须用该属性指定的注解修饰。
➢Class>[] ignored: 指定忽略哪些类型的Bean。 该属性及ignoredType 属性仅对@ConditionalOnMissingBean注解有效
。
➢String[] ignoredType:与ignored属性的作用相同,只不过该属性用字符串形式的全限定类名
。
➢String[] name: 指定要检查的Bean的ID
.
➢search:指定搜索目标Bean的搜索策略, 支持CURRENT(仅在容器中搜索)、ANCESTORS(仅在祖先容器中搜索)、ALL (在所有容器中搜索)三个枚举值
。
➢Class>[] value: 指定要检查的Bean的类型
。
➢String[] type: 与value属性的作用相同,只不过该属性用字符串形式的全限定类名
。
@ConditionalOnSingleCandidate注解相当于@ConditionalOnBean的增强版,它不仅要求被检查的Bean必须存在
,而且只能有一个“候选者”
,能满足byType依赖注入的条件。从上面的属性可以看出,如果指定value 或type 属性,则意味着@ConditionalOnBean、的Bean必须存在,而且只能有一个“候选者”一能满足 byType依赖注入的条件。
从上面的属性可以看出,如果指定value 或type 属性,则意味着@ConditionalOnBean、
@ConditionalOnMissingBean注解将根据目标Bean的类型进行检查(即要求特定类型的Bean必须存在或不存在);如果指定name属性,则意味着@ConditionalOnBean、@ConditionalOnMissingBean注解将根据目标Bean的ID进行检查(即要求特定ID的Bean必须存在或不存在);
如果指定annotation属性,则意味着@ConditionalOnBean、@ConditionalOnMissingBean注解将根据目标Bean所带的注解进行检查(即要求带特定注解的Bean必须存在或不存在)。
**如果@ConditionalOnBean、@ConditionalOnMissingBean 注解不指定任何属性,则默认根据目标Bean的类型进行检查,默认检查被修饰的方法所返回的Bean类型**
。例如如下代码片段:
@ConditionalOnMissingBean
@Bean
public MyService myService()
{
return new MyService();
}
上面配置意味着当容器中不存在MyService类型的Bean时,该配置方法就会生效。如果要检查具有特定ID的Bean是否存在,则需要指定name属性。例如如下代码片段:
@ConditionalOnMissingBean(name = "jdbcTemplateAutoConfiguration")
@Bean
public JdbcTemplateAutoConfiguration jdbcTemplateAutoConfiguration(){
return new JdbcTemplateAutoConfiguration();
}
上面配置意味着只要容器中不存在ID为jdbcTemplate的Bean,该配置方法就会生效。
=========================================================================
@ConditionalOnMissingFilterBean
@ConditionalOnMissingFilterBean注解相当于@ConditionalOnMissingBean的特殊版本,它专门用于检查容器中是否存在指定类型的javax.servlet.Filter
,因此它只能通过value属性指定其要检查的Filter的类型。
=========================================================================
@ConditionalOnProperty
@ConditionalOnProperty注解用于检查特定属性是否具有指定的属性值
。该注解支持如下属性。
String[]value;指定要检查的属性。
➢String[] name:指定value属性的别名
。
String havingValue:指定被检查属性必须具有的属性值
。
String prefix:自动为各属性名添加该属性指定的前缀
。
➢boolean matchIfMissing: 指定当属性未设置属性值时,是否通过检查
。
havingValue属性用于指定被检查属性必须具有的属性值,该属性值共有三种 true,false,foo
列如如下代码
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
// 只有当org.fkjava.test属性具有foo属性值时,下面配置方法才会生效
@ConditionalOnProperty(name = "test", havingValue = "foo",
prefix = "org.fkjava")
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}
上面粗体字注 解指定了name = "test"和 prefix = “org.fkjava”, 表明该注解要检查org. fkjava.test配置属性,且要求其属性值为foo (由havingValue属性指定)。
该示例的主类同样只是获取容器中的dateFormat Bean, 主程序比较简单,此处不再给出其代码,读者可自行参考本书配套代码。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
// 创建Spring容器、运行Spring Boot应用
var ctx = SpringApplication.run(App.class, args);
System.out.println(ctx.getBean("dateFormat"));
}
}
如果直接运行该程序, 则将看到如下异常:
NoSuchBeanDefiniti onException: No bean named ’ dateFormat’ available
通过该异常可以看到,此时dnteForma配置方法并未生效一这 是因为没有配置org. kjiva test属性的缘故。
在application.properties文件中添加如下配置:
org.fkjava.teste=foo
如果再次运行主类,则将看到如下输出:
java. text.SimpleDateFormat@ad508834
@ConditionalOnProperty(name = “test”, havingValue = “foo”,prefix = “org.fkjava”)
总结下来就是 在配置文件中前缀为 “org.fkjava”,属性为"test", 属性值为"foo"
=========================================================================
@ConditionalOnResource
@ConditionalOnResource注解的作用很简单,它要求指定的资源必须存在,其修饰的配置类或
方法才会生效
。使用该注解时只需指定resourees属性,该属性指定必须存在的资源。
=========================================================================
@ConditionalOnWebAplication、@ConditionalOnNotWebApplication.
@ConditionalOnWarDeployment
@ConditionalOnWebApplication要求当前应用必须是Web应用时
,其修饰的配置类或方法才会
生效。使用该注解时可通过type属性指定Web应用的类型,该属性支持如下三个枚举值。
➢
ANY:当前应用是任何Web应用时,该注解修饰的配置类或方法都会生效
。
➢
REACTIVE: 只有当应用是反应式Web应用时(Spring WcbFlux),该注解修饰的配置类或方法才会生效
。
➢SERVLET: 只有当应用是基于Servlet的Web应用时(Spring MVC),该注解修饰的配置类或方法才会生效
。
@ConditionalOnNotWebApplication则要求当前应用不是Web应用时
,该注解修饰的配置类或方法才会生效。
@ConditionalOnWarDeployment要求当前应用以传统WAR包方式被部署到Web服务器或应用服务器中时(不以独立Java程序的方式运行),该注解修饰的配置类或方法才会生效。
@ConditionalOnNotWebApplication和@ConditionalOnWarDeployment用起来更简单,它们都不
需要指定任何属性。
下面通过一个 例子来示范@ConditionalOnWebApplication注解的用法。首先创建一个 Maven项目,然后将前面项目中的pom.xml文件复制过来,并在本项目的pom.xml文件中增加如下两个依赖
spring-b0ot-starter- webflux.jar : Spring WebFlux (反应式Web应用)的依赖库。
➢spring-boot- starter- web.jar : Spring Web的依赖库。
配置类如下
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
// 只有当前应用是反应式Web应用时,该配置才会生效
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}
上面的粗体字注解要求当前应用是反应式Web应用,只有这样该注解修饰的方法才会生效,由于项目中同时添加了Spring WebFlux依赖和Sping Web依赖,因此该应用既可以是基于Servlet的Web应用,也可以是反应式Web应用,具体取决于SpingAplication的设置
下面是本例的启动类代码。
@SpringBootApplication
public class App
{
public static void main(String[] args)
{
var app = new SpringApplication(App.class);
// 设置Web应用的类型,如果不设置则使用默认的类型:
// 如果有Sping Web依赖,自动是基于Servlet的Web应用
// 如果有Sping WebFlux依赖,自动是反应式Web应用
app.setWebApplicationType(WebApplicationType.REACTIVE); // 1
// 创建Spring容器、运行Spring Boot应用
var ctx = app.run(args);
System.out.println(ctx.getBean("dateFormat"));
}
}
上面①号代码调用SpringApplication的方法将该应用设置为反应式Web应用,这样就可让上面配置类中的方法生效;如果将①号代码注释掉,Spring Boot将会自动决定该Web应用的类型:
如果Spring Boot找到了Spring Web依赖,那么该应用就是基于Servlet 的Web应用(不管是否存在Spring WebFlux依赖);只有在找不到Spring Web依赖、找得到Spring WebFlux依赖时,Spring Boot会自动将该应用设置为反应式Web应用。
因此,如果保留上面①号代码,运行该程序,则将看到如下输出:
java. text. SimpleDateFormat@ad508834
如果注释掉上面①号代码,运行该程序,则将看到如下异常:
NoSuchBeanDefini ti onException: NO bean named "dateFormat’ available
该异常说明@ConditionalOnWebApplication注解修饰的方法并未生效。
=========================================================================
@ConditionalOnExpression
@ConditionalOnExpression注解要求指定SpEL表达式的值为true,这样其所修饰的配置类或方法才会生效。
例如下面的配置类。
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
public User user()
{
return new User("fkjava", true);
}
@Bean
// 只有当user.active表达式为true时,该方法才生效
*******@ConditionalOnExpression("user.active")*****
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}
public class User
{
private String name;
private boolean active;
public User(String name, boolean active)
{
this.name = name;
this.active = active;
}
}
主启动类和上面不变
上面的粗体字注解要求user.active 表达式为true时,其修饰的方法才会生效。此处的user activev就是一个SpEL表达式,它负责解析容器中user 的active属性。由于在该配置类前面还配置了一个user Bean,
且该user的active属性为true,因此上面的dateFormat()方法会生效。
=========================================================================
@ConditionalOnCloudPlatfom @ConditioalOnJava、@ConditionalOnJndi.
@ConditionalOnRepositoryType
@ConditionalOnCloudPlatform注解要求应用被部署在特定云平台上
,这样其修饰的配置类或
方法才会生效。该注解可通过value属性指定它所要求的云平台,该value属性支持如下枚举值。
➢CLOUD FOUNDRY: 要求应用被部署在CLOUD_FOUNDRY云平台上时,该注解修饰的配置类或方法才会生效
。
➢HEROKU:要求应用被部署在HEROKU平台上时,该注解修饰的配置类或方法才会生效。
➢KUBERNETES: 要求应用被部署在K8s平台上时,该注解修饰的配置类或方法才会生效。
➢SAP:要求应用被部署在SAP云平台上时,该注解修饰的配置类或方法才会生效。
@CndtioalOnlava注解对目标平台的Java 版本进行检测
,它既可要求目标平台的Java 版本是某个具体的版本, 也可要求其高于或低于某个版本。使用该注解时可指定如下两个属性。
➢JavaVersion value:指定要求的Java版本。
➢ConditionalOnJava.Range range:该属性支持EQUAL_OR_NEWER (大于或等于value属性指定的版本)和OLDER_THAN (小于value 属性指定的版本)两个枚举值。如果不指定该属性,则要求目标平台的Java版本必须是value属性所指定的版本
。
下面的配置类示范了@ConditionalOnJava注解的用法。
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
// 只有当目标平台的Java版本是11或更新的平台时,该方法才生效
@ConditionalOnJava(value = JavaVersion.ELEVEN,
range = ConditionalOnJava.Range.EQUAL_OR_NEWER)
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}
主启动类不变
上面粗体字注解要求目标平台的Java版本必须高于或等于11时,该配置才会生效。因此,只
有用Java11或更新版本的Java运行该程序,才会看到容器中的dateFormat Bean。
@ConditionalOnJndi注解要求指定JNDI必须存在
,使用该注解时通过value 属性指定要检查
的JNDI.
@ConditionalOnRepositoryType
注解要求特定的Spring Data Repository被启用时,其修饰的配
置类或方法才会生效。
正如前面所讲的,所有自定义条件注解其实都是基于@Conditional而来的,使用@Conditional定义新条件注解关键就是要有一个Condition 实现类,该Conditon实现类就负责条件注解的处理逻辑,它所实现的matches()方法决定了条件注解的要求是否得到满足。
下面是自定义条件注解的Condition实现类的代码。
public class MyCondition implements Condition
{
@Override
public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata)
{
// 获取@ConditionalCustom注解的全部属性
Map<String, Object> map = metadata.getAnnotationAttributes(
ConditionalCustom.class.getName());
// 获取注解的value属性值(String[]数组)
String[] vals = (String[]) map.get("value");
Environment env = context.getEnvironment();
// 遍历每个属性值
for (Object val : vals)
{
// 如果某个属性值对应的配置属性不存在,返回false
if (env.getProperty(val.toString()) == null)
{
return false;
}
}
return true;
}
}
上面的Condition 实现类负责处理@ConditionalCustom 条件注解,因此它先获取了该注解的
value属性值,然后根据value 属性值( 属性值是数组)逐个地获取应用的配置属性,只要任意
个配置属性不存在,matches()方法就 会返回false。
从上面的逻辑可以看到,自定义条件注解的处理逻辑比较简单:就是要求value属性所指定的所有配置属性必须存在,至于这些配置属性的值是什么无所谓,这些配置属性是否有值也无所谓,
有了上面的Conditon实现类之后,按下来即可基于@Conditional来定义自定义条件注解,下面是自定义条件注解的代码。
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
***@Conditional(MyCondition.class)****
public @interface ConditionalCustom
{
String[] value() default {};
}
看到这个条件注解的代码了吧!其实其所有的代码都是固定的,只要通过粗体字代码指定该条件注解的Condition实现类即可,该Condition实现类就会负责该条件注解的判断逻辑。
下面的配置类示范了如何使用该自定义的条件注解.
@Configuration(proxyBeanMethods = false)
public class FkConfig
{
@Bean
// 只有当org.fkjava.test和org.crazyit.abc两个配置属性存在时才生效
***@ConditionalCustom({"org.fkjava.test", "org.crazyit.abc"})***
public DateFormat dateFormat()
{
return DateFormat.getDateInstance();
}
}
上面的粗体字代码要求应用必须有orgfkjva.test和org,cazyit.abc这两个配置属性,dateFormat()配置方法才能生效,至于这两个配置属性的值是什么,甚至是否有值都无所谓。
因此可在application.properties文件中添加如下配置。
org.fkjava.test=foo
org.crazyit.abc
有了上面两个配置属性之后(即使orgcrazyit.abce属性没有值),@ConditionalCustom 条件注解
修饰的方法才会生效。
开发自己的自动配置很简单,其实也就两步:
➊使用@Configuration 和条件注解定义自动配置类
。
❷在 META-INF/spring.factories文件中注册自动配置类。
为了清楚地演示Spring Boot自动配置的效果,避免引入第三方框架导致的额外复杂度,本例
先自行开发一个funny框架,该框架的功能是用文件或数据库保存程序的输出信息。
新建一个Maven项目,为该项目添加mysql-connector-java 和slf4j-api 两个依赖。由于该项目
是我们自己开发的框架,因此无须为该项目添加任何SpringBoot依赖。下面是该项目的pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>org.crazyitgroupId>
<artifactId>funnyartifactId>
<version>1.0-SNAPSHOTversion>
<name>funnyname>
<properties>
<maven.compiler.source>11maven.compiler.source>
<maven.compiler.target>11maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.22version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.30version>
<optional>trueoptional>
dependency>
dependencies>
project>
为这个框架项目开发如下类:
public class WriterTemplate
{
Logger log = LoggerFactory.getLogger(this.getClass());
private final DataSource dataSource;
private Connection conn;
private final File dest;
private final Charset charset;
private RandomAccessFile raf;
public WriterTemplate(DataSource dataSource) throws SQLException
{
this.dataSource = dataSource;
this.dest = null;
this.charset = null;
if (Objects.nonNull(this.dataSource))
{
log.debug("==========获取数据库连接==========");
this.conn = dataSource.getConnection();
}
}
public WriterTemplate(File dest, Charset charset) throws FileNotFoundException
{
this.dest = dest;
this.charset = charset;
this.dataSource = null;
this.raf = new RandomAccessFile(this.dest, "rw");
}
public void write(String message) throws IOException, SQLException
{
if (Objects.nonNull(this.conn))
{
// 查询当前数据库的funny_message表是否存在
ResultSet rs = conn.getMetaData().getTables(conn.getCatalog(), null,
"funny_message", null);
// 如果funny_message表不存在
if (!rs.next())
{
log.debug("~~~~~~创建funny_message表~~~~~~");
conn.createStatement().execute("create table funny_message " +
"(id int primary key auto_increment, message_text text)");
rs.close();
}
log.debug("~~~~~~输出到数据表~~~~~~");
// 插入要输出的字符串
conn.createStatement().executeUpdate("insert into " +
"funny_message values (null, '" + message + "')");
}
else
{
log.debug("~~~~~~输出到文件~~~~~~");
// 输出到文件
raf.seek(this.dest.length());
raf.write((message + "\n").getBytes(this.charset));
}
}
// 关闭资源
public void close() throws SQLException, IOException
{
if (this.conn != null)
{
this.conn.close();
}
if (this.raf != null)
{
this.raf.close();
}
}
}
该工具类其实很简单,就是根据是否传入DataSource 来决定输出目标:如果为该工具类传入DataSource,它就会向该数据源所连接的数据库中的funny_message表输出内容(如果该表不存在,该工具类将会自动建表);如果没有为该工具类传入DataSource,它就会向指定文件输出内容。
这个框架非常简单,它只有这一个工具类。
接下来执行如下命令:
mvn install
该命令会将该项目打包成JAR包,并安装到Maven的本地资源库中。
有了该框架之后,接下来为该框架开发自动配置。如果为整合现有的第三方框架开发自动配置,则可直接从这-步开始(因为框架已经存在了,直接为框架开发自动配置即可)。同样新建一个Maven项目,这个项目是自定义Starter 项目,因此必须要有Spring Boot支持,将前面Spring Boot项目中的pom.xml文件复制过来,保留其中的spring-boot starter依赖,并添加刚刚开发的funny框架的依赖。此外,由于该项目不是Spring Boot 应用,因此不需要主类,也不需要运行,故删除其中的spring boot-maven-plugin插件。
修改后的pom.xml文件内容如下。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.2version>
<relativePath/>
parent>
**<groupId>org.crazyitgroupId>
<artifactId>funny-spring-boot-starterartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>funny-spring-boot-startername>**
<properties>
<java.version>11java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
***<dependency>
<groupId>org.crazyitgroupId>
<artifactId>funnyartifactId>
<version>1.0-SNAPSHOTversion>
dependency>***
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
project>
上面第一行粗体字代码指定了该项目的artifactId为funny-spring-boot-starter,暗示它最终会被打包成一个Spring Boot Starter.上面第二段粗体字代码定义了该项目依赖org. crazyit:funy框架,也就是前面开发的框架。如果正在为其他第三方框架开发自动配置,则此处应该填写被整合的第三方框架的坐标。接下来定义如下自动配置类。
@Configuration
// 当WriterTemplate类存在时配置生效
@ConditionalOnClass(WriterTemplate.class)
// 启用FunnyProperties属性处理类
@EnableConfigurationProperties(FunnyProperties.class)
// 让该自动配置位于DataSourceAutoConfiguration自动配置之后处理
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class FunnyAutoConfiguration
{
// FunnyProperties类负责加载配置属性
private final FunnyProperties properties;
public FunnyAutoConfiguration(FunnyProperties properties)
{
this.properties = properties;
}
@Bean(destroyMethod = "close")
// 当单例的DataSource Bean存在时配置生效
@ConditionalOnSingleCandidate(DataSource.class)
// 只有当容器中没有WriterTemplate Bean时,该配置才会生效
@ConditionalOnMissingBean
// 通过@AutoConfigureOrder注解指定该配置方法
// 比下一个配置WriterTemplate的方法的优先级更高
@AutoConfigureOrder(99)
public WriterTemplate writerTemplate(DataSource dataSource) throws SQLException
{
return new WriterTemplate(dataSource);
}
@Bean(destroyMethod = "close")
// 只有当前面的WriterTemplate配置没有生效时,该方法的配置才会生效
@ConditionalOnMissingBean
@AutoConfigureOrder(199)
public WriterTemplate writerTemplate2() throws FileNotFoundException
{
File f = new File(this.properties.getDest());
Charset charset = Charset.forName(this.properties.getCharset());
return new WriterTemplate(f, charset);
}
}
FunnyAutoConfiguration这个自动配置类与其如出一辙 (事实上,很多注解都是直接从MybatisAutoConfiguration自动配置类上复制过来的)。
我们可以看到在FunnyAutoConfiguration类上同样使用了@Configuration、@ConditionalOnClass、
@EnableConfigurationProperties、@AutoConfigureAfter这4个注解,只不过此处使用FunnyProperties
类来负责读取配置属性。
在FunnyAutoConfiguration自动配置类中定义了两个@Bean方法,这两个@Bean方法都用于自动配置WriterTemplate。 为了指定它们的优先级,程序使用了@AutoConfigureOrder注解修饰它们,该注解指定的数值越小,优先级越高
。
FunnyAutoConfiguration自动配置类中的@Bean方法同样使用了@ConditionalOnMissingBean、@ConditionalOnSingleCandidate等条件注解修饰,从而保证只有当容器中不存在WriterTemplate时,该自动配置类才会配置WriterTemplate Bean,且优先配 置基于DataSource 的Writer Template。
上面的自动配置类还用到了FunnyProperties 属性处理类,该类很简单,其代码如下。
// 定义属性处理类
@ConfigurationProperties(prefix = FunnyProperties.FUNNY_PREFIX)
public class FunnyProperties
{
public static final String FUNNY_PREFIX = "org.crazyit.funny";
private String dest;
private String charset;
public String getDest()
{
return dest;
}
public void setDest(String dest)
{
this.dest = dest;
}
public String getCharset()
{
return charset;
}
public void setCharset(String charset)
{
this.charset = charset;
}
}
上面的属性处理类负责处理以“‘org.crazyit.funny” 开头的属性,这个“org.crazyit.funny” 是必要的,它相当于这一-组配置属性的“命名空间”,通过这个命名空间可以将这些配置属性与其他框架的配置属性区分开。相当于在配置文件配置属性的前缀
有了上面的自动配置类之后,接下来使用如下META-INF/spring.factories文件来注册自动配置类。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.crazyit.funny.autoconfigure.FunnyAutoConfiguration
接下来执行如下命令:
mvn install
该命令会将该项目打包成JAR包,并安装到Maven的本地资源库中。
根据Spring Boot官方推荐,一个个完整的Spring Boot Starter应该包含以下两个组件。
➢自动配置(auto-configure) 模块:包含自动配置类和META-INF/spring.factories文件。
➢Starter 模块:负责管理自动配置模块及其他第三方依赖。简而言之,添加本Starter就能开始使用该自动配置
。
根据该推荐不难看出,其实Starter并不包含任何class文件,它只负责管理依赖。如果查看Sping Boot官方提供的JAR就会发现,它所有的自动配置类的class都由spring boot-autoconfigure.jar提供,而各个xxx-starter.jar包内并未提供任何class文件,只是在这些JAR包下的相同路径下提供了一个xx-starterpom文件,该文件指定该Starter负责管理的自动依赖模块和第三方依赖。
Spring Boot还为自动配置包和Starter 包提供了推荐名。
➢自动配置包的推荐名: xxx-spring-boot。
➢Starter 包的推荐名: xxx-spring-boot-starter.
对于第三方Starter, Spring Boot建议不要使用spring-boot-starter-xxx这种命令方式,因为这种命令方式应该留给Spring官方使用。
如果不指望自己的自动配置模块后续得到Spring Boot官方的认同,则其实完全可以将自动配置模块和Starter模块打包成单一的包,前面我们开发的自动配置和Sarter就被打包成一个JAR包.有了自定义的Starter之后,接下来使用该Starter与使用Spring Boot官方Starter并没有任何区别。首先新建一个Maven项目,并用前面SpringBoot项目中的pom.xml文件替换本项目的pom.xm文件,在pom.xml文件的依赖管理中只使用如下代码片段来添加依赖。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.2version>
<relativePath/>
parent>
<groupId>org.crazyitgroupId>
<artifactId>funnytestartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>funnytestname>
<properties>
<java.version>11java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
properties>
<dependencies>
**<dependency>
<groupId>org.crazyitgroupId>
<artifactId>funny-spring-boot-starterartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>***
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<configuration>
<source>11source>
<target>11target>
configuration>
plugin>
plugins>
build>
project>
由于funny-spring-boot-starter 本身需要依赖spring boot-starter,因此不再需要显式配置依赖spring-boot-starter.
在添加了上面的funny-spring-boot-starter依赖之后,该Starter包含的自动配置生效,它会尝试在容器中自动配置WriterTemplate, 因此还需要在application.properties文件中进行配置。
org.crazyit.funny.dest=f:/abc-98765.txt
org.crazyit.funny.charset=UTF-8
# 指定连接数据库的信息
spring.datasource.url=jdbc:mysql://localhost:3306/dev?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
# 配置funny框架的日志级别为debug
logging.level.org.crazyit.funny = debug
该示例的主类很简单,它直接获取容器中的WriterTemplate Bean,并调用该Bean的write()方法执行输出。下面是该主类的代码。
@SpringBootApplication
public class App
{
public static void main(String[] args) throws IOException, SQLException
{
// 创建Spring容器、运行Spring Boot应用
var ctx = SpringApplication.run(App.class, args);
// 获取自动配置的WriterTemplate
WriterTemplate writerTemplate = ctx.getBean(WriterTemplate.class);
writerTemplate.write("自动配置其实很简单");
}
}
运行该程序,由于当前Spring容器中没有DataSource Bean, 因此FunnyAutoConfiguration将会自动配置输出到文件的WriterTemplate。因此,运行该程序,可以看到程序向“f:/abc-98765.txt"文件(由前面的org.crazyit.funny.dest属性配置)输出内容。
而且,由于将框架的日志级别设为DEBUG,因此还可在控制台看到如下输出:
此时为项目添加了spring-boot-starter-jdbc 依赖,该依赖将会在容器中自动配置-个 DataSource Bean,这个自动配置的DataSource Bean 将导致FunnyAutoConfiguration会自动配置输出到数据库的WriterTemplate。因此,运行该程序,可以看到程序向funny数据库的funny message表输出内容。
而且,由于将框架的日志级别设为DEBUG,因此还可在控制台看到如下输出:
~~~~~~输出到数据表~~~~~~
https://www.cnblogs.com/hello-shf/category/1456313.html
https://blog.csdn.net/guorui_java/article/details/107379648
https://blog.csdn.net/u013202238/article/details/107133200?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163108053016780255252950%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=163108053016780255252950&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-107133200.first_rank_v2_pc_rank_v29&utm_term=%40configurationpropertiesScan&spm=1018.2226.3001.4187
@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;
空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
https://blog.csdn.net/loongkingwhat/article/details/105745303
https://my.oschina.net/yejw/blog/1551697
https://blog.csdn.net/zombres/article/details/79260518
https://stackoverflow.com/questions/43482050/spring-boot-with-logback-springproperty