bootdefault和configuration_springboot常用注解、包引入和自动配置功能解读
springboot使用起來確實很方便,做到開箱即用,減少了許多繁瑣的配置。不過在使用過程中時常會想,為啥會這樣方便,springboot為我們做哪些工作。或者是我們在使用的過程中,會遇到springboot不滿足的情況,我們要去了解內部實現機制,然后才能改進。
過去我們對這種方式確很少去了解,或者是了解了一些但是沒有徹底搞清楚。今天我們就學習一下springboot相關的幾個問題,希望能夠揭開一些疑問。相信對這些看似比較基礎的知識地理解,會給我們設計程序帶來好的思路。
注解
spring最開始大量使用xml進行配置,當然也支持注解進行配置,springboot做了很多自動化的工作,進行默認配置,在此過程中將注解發揮到極致。所以在此之前先回顧一下注解的基本知識。
注解為代碼添加信息提供了一種形式化的方法,使得我們可以在后面某個時刻非常方便地使用這些數據。java5引入注解的,有需要多好處,完整地描述程序需要的信息,相比于增加其他非java語言的文件對程序描述,這樣使得代碼可讀性變差,并且不容易檢查。可以生成新的描述符文件設置是新的類定義,可以減少許多重復的模板代碼。
注解定義和常見的注解
java中內置了幾個常見的注解,這也是我們經常在代碼中見到的。
@Override 用來表示覆蓋父類中的方法,如果方法簽名寫錯,編譯器將會報錯,該注解是可選的。
@Deprecated 表示廢棄的方法或者字段,如果程序中使用了會報出警告。
@SuppressWarnings 關閉不當的編譯器警告信息
注解的定義需要使用到元注解,顧名思義,元注解就是用來定義注解的注解,例如:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test{}
這樣就定義了一個注解,注解的定義和接口定義非常類似,編譯后也會生成一個class文件。如果像上面的注解一樣,不包含任何元素,叫做標記注解。可以包含元素,就類似于接口的方法定義。但是和方法定義又有一些區別,訪問權修飾符為默認或者public,類型為八種基本類型和String,Enum,Class,annotations類型,以及它們的數組。
值得說明的是如果為annotation類型,說明這個注解是嵌套注解。成員名字自定義,另外有一點不同的是,相比接口方法的定義,這里可以設置默認值:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test{
public String value() default "some value";
public int test();//沒有默認值
}
如果成員里面有一個名字是value,并且是唯一一個被賦值的成員,那么不需要寫出成員名字直接賦值,例如:
@Test("a")
public void hello(){
…
}
四種元注解:
@Target 表示注解作用的對象,ElementType參數包括:
CONSTRUCTOR,構造器聲明
FIELD,域聲明
LOCAL_VARIABLE,局部變量聲明
METHOD,方法聲明
PACKAGE,包聲明
PARAMETER,參數聲明
TYPE,類,接口包括注解類型或enum聲明,對于注解類型特別要提到的是,這樣可以自定義注解的注解。
@Retention Retention是保留的意思,這個注解說的是被注解的注解被保留的級別,RetentionPolicy可選有:
SOURCE,保留在源碼中,也就是說編譯成class文件不會有該注解,被編譯器丟棄了
CLASS,保留在class文件中,但是在虛擬機運行的時候會丟棄該注解
RUNTIME,保留在運行期,一般都是通過反射機制讀取注解的信息
@Documented 表示注解包含在javadoc中
@Inherited 表示子類可以繼承父類的注解
上面說到RUNTIME的注解,類都實現了AnnotatedElement接口,具有getAnnotation()的實現方法,可以獲取到某個類型的注解:
Test test = method.getAnnocation(Test.class);
@Enable*注解
既然我們搞清楚了注解的基本使用方法,那么讓我們還看一看springboot是怎樣使用注解的。對于很多功能的啟用的開關是加上了@Enable*的注解,例如開啟eureka:
@EnableDiscoveryClient
以此為例,搞清楚這類注解的使用原理,首先看這個注解的定義:
@Target(ElementType.TYPE) //作用在類上面
@Retention(RetentionPolicy.RUNTIME) //jvm使用該注解
@Documented //javadoc包含該注解
@Inherited //子類可以繼承該注解
@Import(EnableDiscoveryClientImportSelector.class) //自定義元注解
public @interface EnableDiscoveryClient {
/**
* If true, the ServiceRegistry will automatically register the local server.
*/
boolean autoRegister() default true; //將該應用注冊到eureka server
}
元素就一個boolean類型的,直接說明的是這里使用了一個自定義的元注解,也就是該注解的@Target是ElementType.TYPE,其中的TYPE指的是一個注解定義的類:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
/**
* {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
* or regular component classes to import.
*/
Class>[] value();
}
這個@Import注解非常重要,可以看到有一個唯一的默認的元素,這個元素名字叫value,后面為元素賦值的時候,可以不寫value,類型是一個class數組。我們知道這個@Import注解是為了引入一個類,下面來看一下注解處理器做了什么操作:
package org.springframework.context.annotation;
/**
* Recursively collect all declared {@code @Import} values. Unlike most
* meta-annotations it is valid to have several {@code @Import}s declared with
* different values; the usual process of returning values from the first
* meta-annotation on a class is not sufficient.
*
For example, it is common for a {@code @Configuration} class to declare direct
* {@code @Import}s in addition to meta-imports originating from an {@code @Enable}
* annotation.
* @param sourceClass the class to search
* @param imports the imports collected so far
* @param visited used to track visited classes to prevent infinite recursion
* @throws IOException if there is any problem reading metadata from the named class
*/
private void collectImports(SourceClass sourceClass, Set imports, Set visited)
throws IOException {
if (visited.add(sourceClass)) {
for (SourceClass annotation : sourceClass.getAnnotations()) {
String annName = annotation.getMetadata().getClassName();
if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {
collectImports(annotation, imports, visited);
}
}
imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
}
}
上面的代碼片段是從spring中摘錄出來,將@Import注解中value的類加到集合中,后續生成bean進行加載。也就是說會把EnableDiscoveryClientImportSelector這個類生成bean加載到上下文中。
@Order(Ordered.LOWEST_PRECEDENCE - 100)
public class EnableDiscoveryClientImportSelector
extends SpringFactoryImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
String[] imports = super.selectImports(metadata);
AnnotationAttributes attributes = AnnotationAttributes.fromMap(
metadata.getAnnotationAttributes(getAnnotationClass().getName(), true));
boolean autoRegister = attributes.getBoolean("autoRegister");
if (autoRegister) {
List importsList = new ArrayList<>(Arrays.asList(imports));
importsList.add("org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration");
imports = importsList.toArray(new String[0]);
}
return imports;
}
@Override
protected boolean isEnabled() {
return new RelaxedPropertyResolver(getEnvironment()).getProperty(
"spring.cloud.discovery.enabled", Boolean.class, Boolean.TRUE);
}
@Override
protected boolean hasDefaultFactory() {
return true;
}
}
這個類由于繼承了SpringFactoryImportSelector,spring會進行import操作,在這個類里面,引入了org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration,這個類,所以使得該應用開啟了eureka客戶端的功能。
自動配置
通過源碼可以發現@SpringBootApplication注解定義中有,有元注解@EnableAutoConfiguration,讓我們看一下這個定義:
package org.springframework.boot.autoconfigure;
@SuppressWarnings("deprecation")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
所有自動配置功能都由@AutoConfigurationPackage和@Import(EnableAutoConfigurationImportSelector.class)這兩個注解來決定。
@AutoConfigurationPackage
這個注解的源碼如下:
package org.springframework.boot.autoconfigure;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* Indicates that the package containing the annotated class should be registered with
* {@link AutoConfigurationPackages}.
*
* @author Phillip Webb
* @since 1.3.0
* @see AutoConfigurationPackages
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
通過注釋可以知道,這個注解的作用是將被該注解進行注解的類所在的包進行掃描。EnableAutoConfiguration被注解了,而該類在org.springframework.boot.autoconfigure包下面,所以會掃描該包下的類,聲明成bean的,將會生成bean。
/**
* {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
* configuration.
*/
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
可以看到將BasePackages注冊在了注冊中心,BasePackages中含有包名:
public static void register(BeanDefinitionRegistry registry, String... packageNames) {
if (registry.containsBeanDefinition(BEAN)) {
BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
ConstructorArgumentValues constructorArguments = beanDefinition
.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0,
addBasePackages(constructorArguments, packageNames));
}
else {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(BasePackages.class);
beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0,
packageNames);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN, beanDefinition);
}
}
這個bean的名字叫做AutoConfigurationPackages,需要說明的是,此時并沒有掃描包。
正如該類的注釋所說,是為了存儲需要自動配置的包,以便于后面的掃描器進行掃描。
* Class for storing auto-configuration packages for reference later (e.g. by JPA entity
* scanner).
也許此時會有個疑問,為啥不用@ComponentScan進行包掃描?
實際上,在org.springframework.boot.autoconfigure包下面,并不是所有的組件都需要包掃描,只有幾個組件的集成才需要,例如cassandra、jpa等,一般都是這樣的代碼:
@Bean
@ConditionalOnMissingBean
public CassandraMappingContext cassandraMapping(
CassandraCustomConversions conversions) throws ClassNotFoundException {
CassandraMappingContext context = new CassandraMappingContext();
List packages = EntityScanPackages.get(this.beanFactory)
.getPackageNames();
if (packages.isEmpty() && AutoConfigurationPackages.has(this.beanFactory)) {
packages = AutoConfigurationPackages.get(this.beanFactory);
}
if (!packages.isEmpty()) {
context.setInitialEntitySet(CassandraEntityClassScanner.scan(packages));
}
if (StringUtils.hasText(this.properties.getKeyspaceName())) {
context.setUserTypeResolver(new SimpleUserTypeResolver(this.cluster,
this.properties.getKeyspaceName()));
}
context.setCustomConversions(conversions);
return context;
}
上述代碼說明,如果存在@EntityScan注解的包,則只需要scan該包即可,如果不存在則將autoconfigure都scan了。
需要說明的是該注解,不是每個組件的自動配置都會用到,只有一部分用到,比較重要的是下面的自動配置功能。
EnableAutoConfigurationImportSelector的作用
下面是最關鍵的源碼:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
try {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List configurations = getCandidateConfigurations(annotationMetadata,
attributes);
configurations = removeDuplicates(configurations);
configurations = sort(configurations, autoConfigurationMetadata);
Set exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return configurations.toArray(new String[configurations.size()]);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass().equals(AutoConfigurationImportSelector.class)) {
return getEnvironment().getProperty(
EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class,
true);
}
return true;
}
這個類會選擇哪些類會被引入并且生成一個Bean,如果屬性spring.boot.enableautoconfiguration為false,將會關閉自動配置。如果開啟的話,會將META-INFO目錄下的配置文件的配置讀進來,選擇需要引入的Bean。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...
以RabbitMq進行舉例說明,需要引入RabbitAutoConfiguration這個Bean。
RabbitAutoConfiguration的作用
下面是該類的定義的一部分:
@Configuration
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {
@Configuration
@ConditionalOnMissingBean(ConnectionFactory.class)
@Configuration注解說明是一個配置Bean。
@EnableConfigurationProperties(RabbitProperties.class),該注解的源碼如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
/**
* Convenient way to quickly register {@link ConfigurationProperties} annotated beans
* with Spring. Standard Spring Beans will also be scanned regardless of this value.
* @return {@link ConfigurationProperties} annotated beans to register
*/
Class>[] value() default {};
}
其中EnableConfigurationPropertiesImportSelector在生成Bean的時候,如果發現沒有RabbitProperties這個bean,會生成這個bean,然后我們又發現這個bean有@ConfigurationProperties,凡事有這個注解的Bean都會將配置文件的屬性映射到這個Bean的字段上。
@ConfigurationProperties(prefix = "spring.rabbitmq")
public class RabbitProperties {
所以此時會有RabbitProperties這個Bean存在。基于此會生成許多其他配置Bean,例如CachingConnectionFactory,這個Bean就含有文件的配置信息,供后面RabbitMq連接通信使用。這樣就完成了自動配置。
@Conditional條件注解
上面的注解中用到了條件注解,是值得關注的地方。上面通過自動配置,得到了RabbitMq的連接配置Bean也就是CachingConnectionFactory,這個類的定義是在org.springframework.amqp.rabbit.connection中,實際上使用rabbit的核心類可能是在另外一個jar包中,也就是說了配置類,但是沒有rabbitmq的操作類也沒有作用,怎樣確保有核心類,然后再進行加載配置Bean呢?還有一個問題需要思考,如果用戶自定義了一個連接配置Bean,而不是使用自動配置Bean?這些問題該怎么解決呢?
實際上springboot定義了非常靈活的條件注解:
@ConditionalOnMissingBean(ConnectionFactory.class)
再去看看這個注解的定義:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnMissingBean {
/**
* The class type of bean that should be checked. The condition matches when each
* class specified is missing in the {@link ApplicationContext}.
* @return the class types of beans to check
*/
Class>[] value() default {};
/**
* The class type names of bean that should be checked. The condition matches when
* each class specified is missing in the {@link ApplicationContext}.
* @return the class type names of beans to check
*/
String[] type() default {};
/**
@Conditional這個注解是spring中定義的,它的成員是一些條件類,比如現在是OnBeanCondition.class。這里面核心的方法是判斷ConditionalOnMissingBean中的value這個Bean是否存在:
if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
BeanSearchSpec spec = new BeanSearchSpec(context, metadata,
ConditionalOnMissingBean.class);
MatchResult matchResult = getMatchingBeans(context, spec);
if (matchResult.isAnyMatched()) {
String reason = createOnMissingBeanNoMatchReason(matchResult);
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnMissingBean.class, spec)
.because(reason));
}
matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, spec)
.didNotFind("any beans").atAll();
}
除了這些條件以外,還有其他條件,列舉如下:
@ConditionalOnJava 系統的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean;
@ConditionalOnMissingBean 容器中不存在指定Bean;
@ConditionalOnExpression 滿足SpEL表達式指定
@ConditionalOnClass 系統中有指定的類
@ConditionalOnMissingClass 系統中沒有指定的類
@ConditionalOnSingleCandidate 容器中只有一個指定的Bean,或者這個Bean是首選Bean
@ConditionalOnProperty 系統中指定的屬性是否有指定的值
@ConditionalOnResource 類路徑下是否存在指定資源文件
@ConditionalOnWebApplication 當前是web環境
@ConditionalOnNotWebApplication 當前不是web環境
@ConditionalOnJndi JNDI存在指定項
這樣我們就把整個自動配置大致流程搞清楚了。
4+
總結
以上是生活随笔為你收集整理的bootdefault和configuration_springboot常用注解、包引入和自动配置功能解读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ServiceStack学习之一准备工作
- 下一篇: 常用装机必备 绿色免安装软件合集 破解