SpringBoot之自動配置原理

使用starter簡化依賴配置

Spring提供了一系列starter來簡化Maven配置。其核心原理也就是Maven和Gradle的依賴傳遞方案。當我們在我們的pom文件中增加對某個starter的依賴時,該starter的依賴也會自動的傳遞性被依賴進來。而且,很多starter也依賴了其他的starter。例如web starter就依賴了tomcat starter,並且大多數starter基本都依賴了spring-boot-starter。

Spring自動配置

Spring Boot會根據類路徑中的jar包、類,爲jar包裏的類自動配置,這樣可以極大的減少配置的數量。簡單點說就是它會根據定義在classpath下的類,自動的給你生成一些Bean,並加載到Spring的Context中。自動配置充分的利用了spring 4.0的條件化配置特性,能夠自動配置特定的Spring bean,用來啓動某項特性。

條件化配置

假設你希望一個或多個bean只有在某種特殊的情況下才需要被創建,比如,一個應用同時服務於中美用戶,要在中美部署,有的服務在美國集羣中需要提供,在中國集羣中就不需要提供。在Spring 4之前,要實現這種級別的條件化配置是比較複雜的,但是,Spring 4引入了一個新的@Conditional註解可以有效的解決這類問題。

@Bean
@Conditional(ChinaEnvironmentCondition.class)
public ServiceBean serviceBean(){
    return new ServiceBean();
}

當@Conditional(ChinaEnvironmentCondition.class)條件的值爲true的時候,該ServiceBean纔會被創建,否則該bean就會被忽略。

@Conditional指定了一個條件。他的條件的實現是一個Java類——ChinaEnvironmentCondition,要實現以上功能就要定義ChinaEnvironmentCondition類,並繼承Condition接口並重寫其中的matches方法。

class ChinaEnvironmentCondition implements Condition{
    public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

        Environment env = context.getEnvironment();
        return env.containProperty("ENV_CN");
    }
}

在上面的代碼中,matches方法的內容比較簡單,他通過給定的ConditionContext對象進而獲取Environment對象,然後使用該對象檢查環境中是否存在ENV_CN屬性。如果存在該方法則直接返回true,反之返回false。

當該方法返回true的時候,就符合了@Conditional指定的條件,那麼ServiceBean就會被創建。反之,如果環境中沒有這個屬性,那麼這個ServiceBean就不會被創建。

除了可以自定義一些條件之外,Spring 4本身提供了很多已有的條件供直接使用,如:

@ConditionalOnBean
@ConditionalOnClass
@ConditionalOnExpression
@ConditionalOnMissingBean
@ConditionalOnMissingClass
@ConditionalOnNotWebApplication

Spring Boot應用的啓動入口

自動配置充分的利用了spring 4.0的條件化配置特性,那麼,Spring Boot是如何實現自動配置的?Spring 4中的條件化配置又是怎麼運用到Spring Boot中的呢?

這要從Spring Boot的啓動類說起。Spring Boot應用通常有一個名爲*Application的入口類,入口類中有一個main方法,這個方法其實就是一個標準的Java應用的入口方法。

一般在main方法中使用SpringApplication.run()來啓動整個應用。值得注意的是,這個入口類要使用@SpringBootApplication註解聲明。@SpringBootApplication是Spring Boot的核心註解,他是一個組合註解。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // 略
}

@SpringBootApplication是一個組合註解,它主要包含@SpringBootConfiguration、@EnableAutoConfiguration等幾個註解。

也就是說可以直接在啓動類中使用這些註解來代替@ SpringBootApplication註解。 關於Spring Boot中的Spring自動化配置主要是@EnableAutoConfiguration的功勞。該註解可以讓Spring Boot根據類路徑中的jar包依賴爲當前項目進行自動配置。

至此,我們知道,Spring Boot的自動化配置主要是通過@EnableAutoConfiguration來實現的,因爲我們在程序的啓動入口使用了@SpringBootApplication註解,而該註解中組合了@EnableAutoConfiguration註解。所以,在啓動類上使用@EnableAutoConfiguration註解,就會開啓自動配置。

那麼,本着刨根問底的原則,當然要知道@EnableAutoConfiguration又是如何實現自動化配置的,因爲目前爲止,我們還沒有發現Spring 4中條件化配置的影子。

EnableAutoConfiguration

其實Spring框架本身也提供了幾個名字爲@Enable開頭的Annotation定義。比如@EnableScheduling、@EnableCaching、@EnableMBeanExport等,@EnableAutoConfiguration的理念和這些註解其實是一脈相承的。

@EnableScheduling是通過@Import將Spring調度框架相關的bean定義都加載到IoC容器。

@EnableMBeanExport是通過@Import將JMX相關的bean定義加載到IoC容器。

@EnableAutoConfiguration也是藉助@Import的幫助,將所有符合自動配置條件的bean定義加載到IoC容器。

下面是EnableAutoConfiguration註解的源碼:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({EnableAutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    //略
}

觀察@EnableAutoConfiguration可以發現,這裏Import了@EnableAutoConfigurationImportSelector,這就是Spring Boot自動化配置的“始作俑者”。

至此,我們知道,至此,我們知道,由於我們在Spring Boot的啓動類上使用了@SpringBootApplication註解,而該註解組合了@EnableAutoConfiguration註解,@EnableAutoConfiguration是自動化配置的“始作俑者”,而@EnableAutoConfiguration中Import了@EnableAutoConfigurationImportSelector註解,該註解的內部實現已經很接近我們要找的“真相”了。

EnableAutoConfigurationImportSelector

protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    } else {
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); 
        configurations = this.removeDuplicates(configurations);
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        this.checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = this.filter(configurations, autoConfigurationMetadata);
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
    }
}

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}

有一個名爲getAutoConfigurationEntry的方法,這個方法發揮的作用是掃描ClassPath下的所有jar包的spring.factories文件,將spring.factories文件keyEnableAutoConfiguration的所有值取出,然後這些值其實是類的全限定名,也就是自動配置類的全限定名,然後 Spring Boot 通過這些全限定名進行類加載(反射),將這些自動配置類添加到 Spring 容器中。

那這些自動配置類有哪些?發揮什麼作用呢?我們接着往下看,我們找到一個名爲spring-boot-autoconfigure-2.1.4.RELEASE.jar的 jar 包,打開它的spring.factories文件,發現這個文件有keyEnableAutoConfiguration的鍵值對

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\

......

也就是這個jar包有自動配置類,可以發現這些自動配置配都是以xxxAutoConfiguration的命名規則來取名的,這些自動配置類包含我了們常用的框架的自動配置類,比如aopelasticsearchredisweb等等,基本能滿足我們日常開發的需求。

Configuation

我們從spring-boot-autoconfigure-1.5.1.RELEASE.jar中的spring.factories文件隨便找一個Configuration,看看他是如何自動加載bean的。

RedisAutoConfiguration:

@Configuration
@ConditionalOnClass({ JedisConnection.class, RedisOperations.class, Jedis.class })
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {

	/**
	 * Redis connection configuration.
	 */
	@Configuration
	@ConditionalOnClass(GenericObjectPool.class)
	protected static class RedisConnectionConfiguration {

		private final RedisProperties properties;

		private final RedisSentinelConfiguration sentinelConfiguration;

		private final RedisClusterConfiguration clusterConfiguration;

		public RedisConnectionConfiguration(RedisProperties properties,
				ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
				ObjectProvider<RedisClusterConfiguration> clusterConfiguration) {
			this.properties = properties;
			this.sentinelConfiguration = sentinelConfiguration.getIfAvailable();
			this.clusterConfiguration = clusterConfiguration.getIfAvailable();
		}

		@Bean
		@ConditionalOnMissingBean(RedisConnectionFactory.class)
		public JedisConnectionFactory redisConnectionFactory()
				throws UnknownHostException {
			return applyProperties(createJedisConnectionFactory());
		}

		protected final JedisConnectionFactory applyProperties(
				JedisConnectionFactory factory) {
			configureConnection(factory);
			if (this.properties.isSsl()) {
				factory.setUseSsl(true);
			}
			factory.setDatabase(this.properties.getDatabase());
			if (this.properties.getTimeout() > 0) {
				factory.setTimeout(this.properties.getTimeout());
			}
			return factory;
		}

		private void configureConnection(JedisConnectionFactory factory) {
			if (StringUtils.hasText(this.properties.getUrl())) {
				configureConnectionFromUrl(factory);
			}
			else {
				factory.setHostName(this.properties.getHost());
				factory.setPort(this.properties.getPort());
				if (this.properties.getPassword() != null) {
					factory.setPassword(this.properties.getPassword());
				}
			}
		}

		private void configureConnectionFromUrl(JedisConnectionFactory factory) {
			String url = this.properties.getUrl();
			if (url.startsWith("rediss://")) {
				factory.setUseSsl(true);
			}
			try {
				URI uri = new URI(url);
				factory.setHostName(uri.getHost());
				factory.setPort(uri.getPort());
				if (uri.getUserInfo() != null) {
					String password = uri.getUserInfo();
					int index = password.indexOf(":");
					if (index >= 0) {
						password = password.substring(index + 1);
					}
					factory.setPassword(password);
				}
			}
			catch (URISyntaxException ex) {
				throw new IllegalArgumentException("Malformed 'spring.redis.url' " + url,
						ex);
			}
		}

		protected final RedisSentinelConfiguration getSentinelConfig() {
			if (this.sentinelConfiguration != null) {
				return this.sentinelConfiguration;
			}
			Sentinel sentinelProperties = this.properties.getSentinel();
			if (sentinelProperties != null) {
				RedisSentinelConfiguration config = new RedisSentinelConfiguration();
				config.master(sentinelProperties.getMaster());
				config.setSentinels(createSentinels(sentinelProperties));
				return config;
			}
			return null;
		}

		/**
		 * Create a {@link RedisClusterConfiguration} if necessary.
		 * @return {@literal null} if no cluster settings are set.
		 */
		protected final RedisClusterConfiguration getClusterConfiguration() {
			if (this.clusterConfiguration != null) {
				return this.clusterConfiguration;
			}
			if (this.properties.getCluster() == null) {
				return null;
			}
			Cluster clusterProperties = this.properties.getCluster();
			RedisClusterConfiguration config = new RedisClusterConfiguration(
					clusterProperties.getNodes());

			if (clusterProperties.getMaxRedirects() != null) {
				config.setMaxRedirects(clusterProperties.getMaxRedirects());
			}
			return config;
		}

		private List<RedisNode> createSentinels(Sentinel sentinel) {
			List<RedisNode> nodes = new ArrayList<RedisNode>();
			for (String node : StringUtils
					.commaDelimitedListToStringArray(sentinel.getNodes())) {
				try {
					String[] parts = StringUtils.split(node, ":");
					Assert.state(parts.length == 2, "Must be defined as 'host:port'");
					nodes.add(new RedisNode(parts[0], Integer.valueOf(parts[1])));
				}
				catch (RuntimeException ex) {
					throw new IllegalStateException(
							"Invalid redis sentinel " + "property '" + node + "'", ex);
				}
			}
			return nodes;
		}

		private JedisConnectionFactory createJedisConnectionFactory() {
			JedisPoolConfig poolConfig = (this.properties.getPool() != null
					? jedisPoolConfig() : new JedisPoolConfig());

			if (getSentinelConfig() != null) {
				return new JedisConnectionFactory(getSentinelConfig(), poolConfig);
			}
			if (getClusterConfiguration() != null) {
				return new JedisConnectionFactory(getClusterConfiguration(), poolConfig);
			}
			return new JedisConnectionFactory(poolConfig);
		}

		private JedisPoolConfig jedisPoolConfig() {
			JedisPoolConfig config = new JedisPoolConfig();
			RedisProperties.Pool props = this.properties.getPool();
			config.setMaxTotal(props.getMaxActive());
			config.setMaxIdle(props.getMaxIdle());
			config.setMinIdle(props.getMinIdle());
			config.setMaxWaitMillis(props.getMaxWait());
			return config;
		}

	}

	/**
	 * Standard Redis configuration.
	 */
	@Configuration
	protected static class RedisConfiguration {

		@Bean
		@ConditionalOnMissingBean(name = "redisTemplate")
		public RedisTemplate<Object, Object> redisTemplate(
				RedisConnectionFactory redisConnectionFactory)
				throws UnknownHostException {
			RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
			template.setConnectionFactory(redisConnectionFactory);
			return template;
		}

		@Bean
		@ConditionalOnMissingBean(StringRedisTemplate.class)
		public StringRedisTemplate stringRedisTemplate(
				RedisConnectionFactory redisConnectionFactory)
				throws UnknownHostException {
			StringRedisTemplate template = new StringRedisTemplate();
			template.setConnectionFactory(redisConnectionFactory);
			return template;
		}

	}

}

RedisProperties:

@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {

	/**
	 * Database index used by the connection factory.
	 */
	private int database = 0;

	/**
	 * Redis url, which will overrule host, port and password if set.
	 */
	private String url;

	/**
	 * Redis server host.
	 */
	private String host = "localhost";

	/**
	 * Login password of the redis server.
	 */
	private String password;

	/**
	 * Redis server port.
	 */
	private int port = 6379;

	/**
	 * Enable SSL.
	 */
	private boolean ssl;

	/**
	 * Connection timeout in milliseconds.
	 */
	private int timeout;

	private Pool pool;

	private Sentinel sentinel;

	private Cluster cluster;

	public int getDatabase() {
		return this.database;
	}

	public void setDatabase(int database) {
		this.database = database;
	}

	public String getUrl() {
		return this.url;
	}

	public void setUrl(String url) {
		this.url = url;
	}

	public String getHost() {
		return this.host;
	}

	public void setHost(String host) {
		this.host = host;
	}

	public String getPassword() {
		return this.password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public int getPort() {
		return this.port;
	}

	public void setPort(int port) {
		this.port = port;
	}

	public boolean isSsl() {
		return this.ssl;
	}

	public void setSsl(boolean ssl) {
		this.ssl = ssl;
	}

	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	public int getTimeout() {
		return this.timeout;
	}

	public Sentinel getSentinel() {
		return this.sentinel;
	}

	public void setSentinel(Sentinel sentinel) {
		this.sentinel = sentinel;
	}

	public Pool getPool() {
		return this.pool;
	}

	public void setPool(Pool pool) {
		this.pool = pool;
	}

	public Cluster getCluster() {
		return this.cluster;
	}

	public void setCluster(Cluster cluster) {
		this.cluster = cluster;
	}

	/**
	 * Pool properties.
	 */
	public static class Pool {

		/**
		 * Max number of "idle" connections in the pool. Use a negative value to indicate
		 * an unlimited number of idle connections.
		 */
		private int maxIdle = 8;

		/**
		 * Target for the minimum number of idle connections to maintain in the pool. This
		 * setting only has an effect if it is positive.
		 */
		private int minIdle = 0;

		/**
		 * Max number of connections that can be allocated by the pool at a given time.
		 * Use a negative value for no limit.
		 */
		private int maxActive = 8;

		/**
		 * Maximum amount of time (in milliseconds) a connection allocation should block
		 * before throwing an exception when the pool is exhausted. Use a negative value
		 * to block indefinitely.
		 */
		private int maxWait = -1;

		public int getMaxIdle() {
			return this.maxIdle;
		}

		public void setMaxIdle(int maxIdle) {
			this.maxIdle = maxIdle;
		}

		public int getMinIdle() {
			return this.minIdle;
		}

		public void setMinIdle(int minIdle) {
			this.minIdle = minIdle;
		}

		public int getMaxActive() {
			return this.maxActive;
		}

		public void setMaxActive(int maxActive) {
			this.maxActive = maxActive;
		}

		public int getMaxWait() {
			return this.maxWait;
		}

		public void setMaxWait(int maxWait) {
			this.maxWait = maxWait;
		}

	}

	/**
	 * Cluster properties.
	 */
	public static class Cluster {

		/**
		 * Comma-separated list of "host:port" pairs to bootstrap from. This represents an
		 * "initial" list of cluster nodes and is required to have at least one entry.
		 */
		private List<String> nodes;

		/**
		 * Maximum number of redirects to follow when executing commands across the
		 * cluster.
		 */
		private Integer maxRedirects;

		public List<String> getNodes() {
			return this.nodes;
		}

		public void setNodes(List<String> nodes) {
			this.nodes = nodes;
		}

		public Integer getMaxRedirects() {
			return this.maxRedirects;
		}

		public void setMaxRedirects(Integer maxRedirects) {
			this.maxRedirects = maxRedirects;
		}

	}

	/**
	 * Redis sentinel properties.
	 */
	public static class Sentinel {

		/**
		 * Name of Redis server.
		 */
		private String master;

		/**
		 * Comma-separated list of host:port pairs.
		 */
		private String nodes;

		public String getMaster() {
			return this.master;
		}

		public void setMaster(String master) {
			this.master = master;
		}

		public String getNodes() {
			return this.nodes;
		}

		public void setNodes(String nodes) {
			this.nodes = nodes;
		}

	}

}

看到上面的代碼,終於找到了我們要找的東西——Spring 4的條件化配置。

在決定對哪些bean進行自動化配置的時候,使用了一個條件註解:@ConditionalOnClass

只有滿足這種條件的時候,對應的bean纔會被創建。這樣做的好處是什麼?這樣可以保證某些bean在沒滿足特定條件的情況下就可以不必初始化,避免在bean初始化過程中由於條件不足,導致應用啓動失敗。

spring相關的一些條件註解:https://blog.csdn.net/qq_24313635/article/details/86626779

總結

至此,我們可以總結一下Spring Boot的自動化配置的實現:

通過Spring 4的條件配置決定哪些bean可以被配置,將這些條件定義成具體的Configuation,然後將這些Configuation配置到spring.factories文件中,作爲key: org.springframework.boot.autoconfigure.EnableAutoConfiguration的值,這時候,容器在啓動的時候,由於使用了EnableAutoConfiguration註解,該註解Import的EnableAutoConfigurationImportSelector會去掃描classpath下的所有spring.factories文件,然後進行bean的自動化配置。

所以,如果我們想要自定義一個starter的話,可以通過以上方式將自定義的starter中的bean自動化配置到Spring的上下文中,從而避免大量的配置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章