使用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
文件key
爲EnableAutoConfiguration
的所有值取出,然後這些值其實是類的全限定名,也就是自動配置類的全限定名,然後 Spring Boot 通過這些全限定名進行類加載(反射),將這些自動配置類添加到 Spring 容器中。
那這些自動配置類有哪些?發揮什麼作用呢?我們接着往下看,我們找到一個名爲spring-boot-autoconfigure-2.1.4.RELEASE.jar
的 jar 包,打開它的spring.factories
文件,發現這個文件有key
爲EnableAutoConfiguration
的鍵值對
# 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
的命名規則來取名的,這些自動配置類包含我了們常用的框架的自動配置類,比如aop
、elasticsearch
、redis
和web
等等,基本能滿足我們日常開發的需求。
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的上下文中,從而避免大量的配置。