Spring/Boot注解笔记

Spring配置与自动装配详解
本文深入探讨Spring框架中配置信息的读取方法,包括@Value和@ConfigurationProperties注解的使用,以及如何通过@EnableAutoConfiguration实现二方包的自动配置与Bean注册。

1. 获取配置信息

1.1 @Value

① 用途

Spring提供了获取properties文件中的配置值的方法,通过注解@Value的方式可以直接读取配置文件的配置值。

② 配置

applicationContext.xml配置:

<bean id="configProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
    <property name="locations">
        <list>
            <value>classpath:config/c1.properties</value>
            <value>classpath:config/c2.properties</value>
        </list>
    </property>
</bean>

③ 使用

方式1:通过EL表达式类似的语法 p r o p e r t y : d e f a u l t v a l u e 方 式 获 取 配 置 属 性 , 不 需 要 指 定 配 置 文 件 的 加 载 对 象 实 例 ( B e a n ) 。 b e a n c l a s s 要 使 用 P r e f e r e n c e s P l a c e h o l d e r C o n f i g u r e r 。 示 例 : ‘ @ V a l u e ( &quot; { property : default_value } 方式获取配置属性,不需要指定配置文件的加载对象实例(Bean)。bean class要使用PreferencesPlaceholderConfigurer。示例: `@Value(&quot; property:defaultvalueBeanbeanclass使PreferencesPlaceholderConfigurer:@Value("{jdbc.driverClass}")`

方式2:通过OGNL表达式类似语法 #{ obj.property? : default_value } 方式获取配置属性,可以指定配置文件的加载对象实例(Bean),配置bean class要使用PropertiesFactoryBean。示例: @Value("#{configProperties['jdbc.driverClass']")

1.2 @ConfigurationProperties

对于yaml配置文件,@Value仍然可以读取相关配置信息,但不适用于复杂类型的读取,只适用于字符串的读取,比如Map,List,Set这种集合类型,SpringBoot提供了另外一种读取方式,即使用POJO的方式来直接读取配置信息。

示例:

Yaml配置文件class-conf.yaml

class:
    no: 1
    name: 一班
    students:
        -   id: 1
            name: Bob
            age: 18
            friends:
                -   name: Tom
                    age: 18
                -   name: Ami
                    age: 17
        -   id: 2
            name: Tom
            friends: 
                -   name: Bob
                    age: 18
        -   id: 3
            name: Ami
            age: 17
            friends:
                -   name: Bob
                    age: 18
        

接受配置信息的java类:

package com.example;
@Data
@ConfigurationProperties(prefix = "class", ignoreInvalidFields = true)
public class ClassProperties{
    private Integer no;
    private String name;
    Set<Student> students;
    
    @Data
    public static class Student{
        private Integer id;
        private String name;
        private Integer age;
        private List<Map<String, Object>> friends;
    }
}

测试读取:

package com.example;

import com.example.ClassProperties.Student;
import java.util.List;
import javax.annotation.Resource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Configuration;

@SpringBootTest(classes = ConfigReadTest.class)
@Configuration
//加载指定配置文件,默认会加载application.yaml
@PropertySource("classpath:class-conf.yaml")
//指定读取注入配置信息类
@EnableConfigurationProperties(ClassProperties.class)
public class ConfigReadTest{
    @Resource ClassProperties classProperties;
    @Test
    void test(){
        Assertions.assertNotNull(classProperties);
        List<Student> students = classProperties.getStudents();
        Assertions.assertNotNull(students);
        Assertions.assertTrue(!students.isEmpty());
        Assertions.assertNotNull(students.get(0).getName());
        Assertions.assertNotNull(students.get(0).getFriends());
    }
}

2. 对外提供二方包Bean注册

2.1 @EnableAutoConfiguration

先看源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.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 {};

}

其中最关键的是@Import(AutoConfigurationImportSelector.class),借助AutoConfigurationImportSelector@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。

借助于Spring框架原有的一个工具类:SpringFactoriesLoader的支持,@EnableAutoConfiguration才可以智能的自动配置!

AutoConfigurationImportSelector类中可以看到通过 SpringFactoriesLoader.loadFactoryNames方法
把 spring-boot-autoconfigure.jar/META-INF/spring.factories中每一个xxxAutoConfiguration文件都加载到容器中了。

其中的每个xxxAutoConfiguration配置类一般都会有下面的条件注解:

  • @ConditionalOnClass : classpath中存在该类时起效
  • @ConditionalOnMissingClass : classpath中不存在该类时起效
  • @ConditionalOnBean : DI容器中存在该类型Bean时起效
  • @ConditionalOnMissingBean : DI容器中不存在该类型Bean时起效
  • @ConditionalOnSingleCandidate : - DI容器中该类型Bean只有一个或@Primary的只有一个时起效
  • @ConditionalOnExpression : SpEL表达式结果为true时
  • @ConditionalOnProperty : 参数设置或者值一致时起效
  • @ConditionalOnResource : 指定的文件存在时起效
  • @ConditionalOnJndi : 指定的JNDI存在时起效
  • @ConditionalOnJava : 指定的Java版本存在时起效
  • @ConditionalOnWebApplication : Web应用环境下起效
  • @ConditionalOnNotWebApplication : 非Web应用环境下起效

SpringFactoriesLoader属于Spring框架私有的一种扩展方案(类似于Java的SPI方案java.util.ServiceLoader),其主要功能就是从指定的配置文件META-INF/spring-factories加载配置,spring-factories是一个典型的java properties文件,只不过Key和Value都是Java类型的完整类名。

对于@EnableAutoConfiguration来说,SpringFactoriesLoader的用途稍微不同一些,其本意是为了提供SPI扩展的场景,而在@EnableAutoConfiguration场景中,它更多提供了一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名org.springframework.boot.autoconfig.EnableAutoConfiguration作为查找的Key,获得对应的一组@Configuration类。

SpringFactoriesLoader是一个抽象类,类中定义的静态属性定义了其加载资源的路径public static final String FACTORIES_RESOURCE_LOCATION = “META-INF/spring.factories”,此外还有三个静态方法:

  • loadFactories:加载指定的factoryClass并进行实例化。
  • loadFactoryNames:加载指定的factoryClass的名称集合。
  • instantiateFactory:对指定的factoryClass进行实例化。

在loadFactories方法中调用了loadFactoryNames以及instantiateFactory方法。源码如下:

public static <T> List<T> loadFactories(Class<T> factoryClass, @Nullable ClassLoader classLoader) {
	Assert.notNull(factoryClass, "'factoryClass' must not be null");
	ClassLoader classLoaderToUse = classLoader;
	if (classLoaderToUse == null) {
		classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
	}
	List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
	if (logger.isTraceEnabled()) {
		logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
	}
	List<T> result = new ArrayList<>(factoryNames.size());
	for (String factoryName : factoryNames) {
		result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
	}
	AnnotationAwareOrderComparator.sort(result);
	return result;
}

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
	String factoryClassName = factoryClass.getName();
	return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
	MultiValueMap<String, String> result = cache.get(classLoader);
	if (result != null) {
		return result;
	}

	try {
		Enumeration<URL> urls = (classLoader != null ?
				classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
				ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
		result = new LinkedMultiValueMap<>();
		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();
			UrlResource resource = new UrlResource(url);
			Properties properties = PropertiesLoaderUtils.loadProperties(resource);
			for (Map.Entry<?, ?> entry : properties.entrySet()) {
				String factoryClassName = ((String) entry.getKey()).trim();
				for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
					result.add(factoryClassName, factoryName.trim());
				}
			}
		}
		cache.put(classLoader, result);
		return result;
	}
	catch (IOException ex) {
		throw new IllegalArgumentException("Unable to load factories from location [" +
				FACTORIES_RESOURCE_LOCATION + "]", ex);
	}
}	
private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
	try {
		Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
		if (!factoryClass.isAssignableFrom(instanceClass)) {
			throw new IllegalArgumentException(
					"Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
		}
		return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance();
	}
	catch (Throwable ex) {
		throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
	}
}

loadFactories方法首先获取类加载器,然后调用loadFactoryNames方法获取所有的指定资源的名称集合、接着调用instantiateFactory方法实例化这些资源类并将其添加到result集合中。最后调用AnnotationAwareOrderComparator.sort方法进行集合的排序。

总结:

对外发布的二方包,需要对外提供一些bean的配置、注册、扫描等,如果仅仅是简单打包,外部依赖项目是需要针对此二方包额外配置一些Configuration加载,或者包扫描等,如果外部项目并不知道二方包内有什么,就无法针对性的进行配置。所以就需要二方包通过某种方式,让外部依赖项目可以无感知的、全自动的加载二方包的配置类,从而做到自动化配置。

要达到此目的,仅需要两步:

  1. 编写配置类并注解@Configuration@SpringBootConfiguration
  2. 在resources/META-INF/spring.factories中指定xxAutoConfiguration,并写上步骤1编写的配置类全限定类名。

打包对外发布即可。

外部依赖项目仅需要在项目启动入口(通常是ApplicationStartup)写上注解@SpringBootApplication(因为此注解已继承了@EnableAutoConfiguration);测试类上则需要打上注解@EnableAutoConfiguration

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT研究僧大师兄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值