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的上下文中,从而避免大量的配置。

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