Spring Boot的很多特性都是基于spring framework来实现的,例如我们熟知的特性,也是它的核心特性:组件自动装配。它能够根据依赖的jar包自动配置Spring Boot的应用,例如: 如果类路径中存在DispatcherServlet类,就会自动配置springMvc相关的Bean。
而你需要的仅仅是编写一个启动类,加上@ SpringBootApplication
注解,执行它的main方法,接下来的就交给Sping Boot:
@ SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
为什么说Spring Boot很多特性是基于Spring framework实现的呢,在进入主题之前,需要先了解一下模式注解
spring的模式注解:
一种用于声明在应用中扮演"组件"角色的注解,即标注了这个注解,表明一个类等在应用中扮演组件。比如@ Repository
标注在一个类上,说明这个类是扮演的仓储角色。
而@ Component
是作为通用模式组件,任何被@ Component
标注的组件均为组件扫描的候选对象。类似地,凡是被 @ Component
标注的注解,如 @ Service
,当任何组件标注它时,也被视作组件扫描的候选对象。
Spring Framework注解 | 场景说明 | 起始版本 |
---|---|---|
@ Repository | 数据仓储 | 2.0 |
@ Component | 通用组件 | 2.5 |
@ Service | 服务 | 2.5 |
@ Controller | 控制器 | 2.5 |
@ Configuration | 配置类 | 3.0 |
spring2.5之前,使用的是xml配置的方式实现:
<context:component-scan base-package=\"com.wxpay.*\"/>
spring3.1之后,可以使用注解实现:
@ ComponentScan(\"com.wxpay.*\")
模式注解的两种属性:
@ component
,@ Repository
注解,都有value签名,保留了签名的一致性,这就是注解的派生性,换句话说,就是被@ component
标注的注解就具备了@ component
的功能SecondLevelRepository
,我们的SecondLevelRepository
派生于FirstLevelRepository
,这就是层次性严格上讲 注解是没有派生性和层次性的,之所以这样讲,是为了方便理解,因为在spring中的很多注解都是有着派生性和层次性的结构
可能前面的说法有些不够明确,大家可能听的云里雾里的,接下来我们用一个例子来说明一下模式注解的特性。
首先我们先定义一个注解,使用@ Repository
标注:
@ Target({ElementType.TYPE})
@ Retention(RetentionPolicy.RUNTIME)
@ Documented
@ Repository
public @ interface FirstLevelRepository {
String value() default \"\";
}
然后定义一个类,使用上面的注解标注:
@ FirstLevelRepository(value = \"myFirstLevelRepository\")
public class MyFirstLevelRepository {
}
最后再定义一个引导类,这儿使用了@ ComponentScan
扫描上面的类:
@ ComponentScan(\"com.lingxiao.springboot.Repository\")
public class RepositoryBootstrap {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext =
new SpringApplicationBuilder(RepositoryBootstrap.class).web(WebApplicationType.NONE).run(args);
MyFirstLevelRepository firstLevelRepository = applicationContext
.getBean(\"myFirstLevelRepository\", MyFirstLevelRepository.class);
System.out.println(\"获取到的bean\" + firstLevelRepository);
applicationContext.close();
}
}
最终也是成功获取到了MyFirstLevelRepository
,这里可以看出,被@ Repository
标注的注解 @ FirstLevelRepository
,就像是继承关系,是具备@ Repository
的功能的。
然后我们再定义一个注解,使用@ FirstLevelRepository
来标注它:
@ Target({ElementType.TYPE})
@ Retention(RetentionPolicy.RUNTIME)
@ Documented
@ FirstLevelRepository
public @ interface SeondLevelRepository {
String value() default \"\";
}
同样的方法,使用这个注解标注上面的类MyFirstLevelRepository
,运行引导类RepositoryBootstrap
,也是可以成功获取到bean的。这也印证了模式注解的派生性和层次性。
好了,上面的例子已经说明模式注解的特性了,我们接下来可以看@ SpringBootApplication
这个注解了·,进去看看源码:
源码版本为 spring-boot-2.2.5.RELEASE
@ Target(ElementType.TYPE)
@ Retention(RetentionPolicy.RUNTIME)
@ Documented
@ Inherited
@ ComponentScan(excludeFilters = { @ Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@ Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
@ SpringBootConfiguration
@ EnableAutoConfiguration
public @ interface SpringBootApplication {
}
可以看到@ SpringBootApplication
是一个组合注解
@ ComponentScan
,配置自动扫包,没什么好说的
再点开@ SpringBootConfiguration
,我们可以看见:
@ Configuration
public @ interface SpringBootConfiguration {
}
@ SpringBootConfiguration
被@ Configuration
,说明@ SpringBootConfiguration
以及被它标注的@ SpringBootApplication
是模式注解
然后是@ EnableAutoConfiguration
,这是接下来要讲的@ Enable模块装配
,也是实现自动装配的核心
使用@ Enable模块装配,可以配置激活哪些模块
框架实现 | 注解模块 | |
---|---|---|
Spring Framework | @ EnableWebMvc | |
@ EnableTransationManagement | ||
@ EnableCaching | Caching模块 | |
@ EnableMBeanExport | JMX模块 | |
@ EnableAsync | 异步处理模块 | |
@ EnableWebFlux | Web Flux模块 | |
Spring Boot | @ EnableAutoConfiguration | 自动装配模块 |
@ EnableOAuth2Sao | OAuth2单点登录模块 |
有两种方式实现@ Enable
先定义一个Bean:
@ Configuration
public class HelloWorldConfiguration {
@ Bean
public String helloWorld() { // 方法名即 Bean 名称
return \"Hello,World\";
}
}
然后再定义一个注解,使用@ Import
导入刚刚定义的bean:
@ Retention(RetentionPolicy.RUNTIME)
@ Target(ElementType.TYPE)
@ Documented
@ Import(HelloWorldConfiguration.class)
public @ interface EnableHelloWorld {
}
完成上面两步之后,我们只要用@ EnableHelloWorld
标注在某个类上时,这个Bean就会加载到Spring容器中。
@ EnableHelloWorld
public class EnableHelloWorldBootstrap {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableHelloWorldBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
// helloWorld Bean 是否存在
String helloWorld =
context.getBean(\"helloWorld\", String.class);
System.out.println(\"获取到的bean: \" + hello);
// 关闭上下文
context.close();
}
}
运行查看效果:
可以看到这个bean确实已经加载到Spring容器中了。
实现ImportSelector
接口,实现它的selectImports
方法,返回的是一个string类型的数组,数组里面存放的类名:
public class HelloWorldImportSelector implements ImportSelector {
@ Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[]{HelloWorldConfiguration.class.getName()};
}
}
然后将上面定义的注解改一下:
@ Target({ElementType.TYPE})
@ Retention(RetentionPolicy.RUNTIME)
@ Documented
//@ Import({HelloWorldConfiguration.class}) enable注解驱动的方式
@ Import({HelloWorldImportSelector.class}) //接口编程的方式
public @ interface EnableHelloWorld {
}
可以看到,基于接口的方式实现更加灵活,我们可以在selectImports
中做一些判断,根据需要返回不同的类名数组,然后再根据类名进行装配。
我们再回头看一下@ EnableAutoConfiguration
这个注解:
@ Target(ElementType.TYPE)
@ Retention(RetentionPolicy.RUNTIME)
@ Documented
@ Inherited
@ AutoConfigurationPackage
@ Import(AutoConfigurationImportSelector.class)
public @ interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = \"spring.boot.enableautoconfiguration\";
}
再点进@ AutoConfigurationPackage
看一下
@ Target(ElementType.TYPE)
@ Retention(RetentionPolicy.RUNTIME)
@ Documented
@ Inherited
@ Import(AutoConfigurationPackages.Registrar.class)
public @ interface AutoConfigurationPackage {
}
使用@ Import
来给Spring容器中导入一个组件 ,这里导入的是Registrar.class
运行springboot项目,debug看一下,可以发现metadata是被@ SpringBootApplication
标注的类,再看看new PackageImport(metadata).getPackageName()
的值:
这个值就是扫描的包路径,也就是说,默认扫描的包路径是引导类所在的包以及子包。
接着我们再看EnableAutoConfiguration
也使用了@ import
,导入了 AutoConfigurationImportSelector.class,从这个类的名字我们就可以知道,它指定是实现了ImportSelector
接口,重写了selectImports方法,所以我们直接看selectImports:
@ Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);
return tringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
可以看到返回结果是通过autoConfigurationEntry
的getConfigurations()
获取的,所以我们直接看getAutoConfigurationEntry
方法:
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
同理,这里我们进入getCandidateConfigurations
方法看一下:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, \"No auto configuration classes found in META-INF/spring.factories. If you \"
+ \"are using a custom packaging, make sure that file is correct.\");
return configurations;
}
想必英文不太好的童鞋从这里面也能看出一些端倪,没错,最终加载的配置类会从META-INF/spring.factories
中获取,也就是说,Spring Boot在启动的时候会从类路径下的META-INF/spring.factories
文件中将指定的值作为配置类导入到容器中,也就实现了自动配置。
条件装配注解有两种:
Spring注解 | 场景说明 | 起始版本 |
---|---|---|
@ Profile | 配置化条件装配 | 3.1 |
@ Conditional | 编程条件装配 | 4.0 |
在4.0之后@ profile也变成了@ Conditional来实现
应用场景:一般用于生产环境和开发环境之间的切换,就是在类或者方法上添加注解并设置环境标识比如java7
、java8
,我们以一个简单的多整数求和来演示如何使用
首先定义一个接口:
public interface CalCulateService {
/**
* 整数求和
* @ param args
* @ return
*/
Integer sum(Integer... args);
}
然后分别实现一个JDK7和JDK8的求和方法。
首先是JDK7的实现:
/**
* java7的方式实现求和
*/
@ Profile(\"Java7\")
@ Service
public class Java7CalCulateServiceImpl implements CalCulateService {
@ Override
public Integer sum(Integer... args) {
System.out.println(\"java7的方式求和\");
int sum = 0;
for (int i = 0; i < args.length; i++) {
sum+=args[i];
}
return sum;
}
}
然后是JDK8的实现:
@ Profile(\"Java8\")
@ Service
public class Java8CalCulateServiceImpl implements CalCulateService {
@ Override
public Integer sum(Integer... args) {
System.out.println(\"java8的方式求和\");
int sum = Stream.of(args).reduce(0,Integer::sum);
return sum;
}
}
然后创建一个启动类,在启动容器的时候,使用.profiles(\"Java8\")
来指定使用哪个版本来计算:
@ SpringBootApplication(scanBasePackages = \"com.lingxiao.springboot.service\")
public class CalCulateServiceBootstrap {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext =
new SpringApplicationBuilder(CalCulateServiceBootstrap.class)
.web(WebApplicationType.NONE)
.profiles(\"Java8\")
.run(args);
CalCulateService calCulateService = applicationContext
.getBean(CalCulateService.class);
System.out.println(\"求和: \" + calCulateService.sum(1,2,3,4,5));
applicationContext.close();
}
}
运行结果:
可以看到确实是java8的方式实现的求和
在创建bean时,增加一系列条件限制,只有当所有指定的条件都满足是,组件才可以注册。在使用@ Conditional
时,进行条件判断的类必须实现Condition
接口
现在我们自定义一个条件装配,创建一个注解:
/**
* 编程方式实现条件装配
*/
@ Retention(RetentionPolicy.RUNTIME)
@ Target({ElementType.TYPE,ElementType.METHOD})
@ Documented
@ Conditional(OnSystemPropertyCondition.class)
public @ interface ConditionalOnSystemProperty {
/**
* java系统属性名称
* @ return
*/
String name();
/**
* java系统属性值
*/
String value();
}
在这个注解中定义了两个属性name
和value
,这两个属性在实现条件判断的OnSystemPropertyCondition
中会用到。
创建条件判断类:
/**
* 编程方式实现条件装配
*/
public class OnSystemPropertyCondition implements Condition {
/**
*
* @ param context
* @ param metadata 源信息
* @ return
*/
@ Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(ConditionalOnSystemProperty.class.getName());
List<Object> nameObj = attributes.get(\"name\");
String name = \"\";
if (!CollectionUtils.isEmpty(nameObj)) {
name = String.valueOf(attributes.get(\"name\").get(0));
}
String value = \"\";
if (!CollectionUtils.isEmpty(nameObj)) {
value = String.valueOf(attributes.get(\"value\").get(0));
}
String property = System.getProperty(name);
return value.equals(property); //条件满足才会返回装配
}
}
使用metadata.getAllAnnotationAttributes
获取注解中的属性值,然后判断是否满足条件,满足条件之后才会装配。
创建引导类:
public class ConditionalOnSystemPropertyBootstrap {
@ Bean
@ ConditionalOnSystemProperty(name = \"java.vm.specification.version\",value = \"1.8\")
public String helloWorld(){
return \"hello world\";
}
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(ConditionalOnSystemPropertyBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
String helloWorld = context.getBean(\"helloWorld\", String.class);
System.out.println(\"bean name: \"+helloWorld);
context.close();
}
}
在这个引导类中我们申明了一个helloWorld的bean,在这个方法上添加了条件装配注释,其中java.vm.specification.version
是获取的java虚拟机版本,如果满足虚拟机为1.8的条件,这个bean就会被加载到spring容器中
当前版本为1.8时,运行代码:
正常加载没有问题,当我们切换版本之后,再次运行:
发现报错了,spring找不到这个bean,也就是说bean没有满足被加载的条件
罗里吧嗦了这么多,我们看看如何实现Spring Boot的自动装配
而Spring Boot的自动装配是结合了上面所说的技术,所以我们可以基于Spring Framework的知识点实现自动装配,步骤如下:
首先创建一个类:
/**
* 自动装配
*/
@ Configuration //模式注解
@ EnableHelloWorld //Enable模块装配
@ ConditionalOnSystemProperty(name = \"java.vm.specification.version\",value = \"1.8\") //条件装配
public class HelloWorldAutoConfiguration {
}
可以看到,修饰的三个注解都是基于Spring Framework的,现在只需要配置spring.factories文件即可,我们在resouries目录下新建文件:
内容为key-value的格式,key为@ EnableAutoConfiguration
,value为自动装配类的包路径:
# 自动装配
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
com.lingxiao.springboot.configuration.HelloWorldAutoConfiguration
然后再创建一个启动类:
@ EnableAutoConfiguration
public class EnableAutoConfigurationBootstrap {
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableAutoConfigurationBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
String hello = context
.getBean(\"helloWorld\", String.class);
System.out.println(\"获取到的bean: \" + hello);
context.close();
}
}
运行结果也是能正确获取到bean。这里梳理一下整个流程:
@ EnableAutoConfiguration
激活自动装配,Spring Boot会去spring.factories
文件中解析需要自动装配的类 HelloWorldAutoConfiguration
HelloWorldAutoConfiguration
的时候会去判断是否满足装配要求,这里是jdk1.8的环境,所以是满足要求的HelloWorldAutoConfiguration
是被@ EnableHelloWorld
标注了,@ EnableHelloWorld
通过@ Import({HelloWorldImportSelector.class})
的方式引入了HelloWorldImportSelector
HelloWorldImportSelector
的selectImports
方法中返回了HelloWorldConfiguration
的类名HelloWorldConfiguration
中加载helloWorld
源码已上传至gitee:点击访问
参考:
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.lingxiaomz.top/articleContent/?id=104722153606669090
内容来源于网络,如有侵权,请联系作者删除!