spring boot 之自动装配

前言

在最初接触spring 的时候,还是使用xml进行装配,我记得很清楚,当时分别配置了spring-dao.xml , spring-service.xml , spring-controller.xml。然后把所有需要用到的扫包,注入bean,以及配置,全都一股脑的塞进xml中,虽然出发点很好,不用在java代码中硬编码了,但是xml的可读性并不是很好,那阵子,真是痛苦的要命。

正文

后来逐渐的接触到了spring boot,发现这个东西开发起来简单的要命,几乎自己不需要过多的考虑spring 与别的框架整合的问题,所有的一切spring boot已经帮我们做好了,只需要在application.properties 配置一下就完事。
但是spring boot 因为大部分的东西,已经不需要我们在着手了,所以对我们而言,“易学难精”。所以我决定好好把spring boot的原理搞清楚,今天就来说一说自动装配。
这里提前说一下,Spring boot的注入一共有四种方式:

1.通过 spring 模式注解 装配
2.通过@Enable* 注解进行装配
3.通过条件注解装配
4.通过工厂类去加载

下面我会逐一说到以上这几点。

通过 spring 模式注解 装配

模式注解的方式,尽管我们刚开始接触spring,spring mvc 框架,我们也应该会知道他们到底是怎么使用的。
以@Component为首 派生的 @Controller,@Service, @Repository。分别对应了我们工程中的controller层,业务层,和数据持久层,这里我就不做过多结束了,只要把注解放在类上就可以被自动的注入到spring 的context中。

通过@Enable* 注解进行装配

很多人会问为什么有了模式注解的装备,还需要@Enable* 这个注解呢?
这个问题问的非常好,纵观整个Web的发展,我们可以看出来,是一个化繁为简的模块化趋势。
所以@Enable* 也是具有同样道理的,例如@EnableWebMvc就是一个web组件的集合。
@Enable注解有很多种:
例如说 @EnableAutoConfiguration,@EnableWebMvc,@EnableCaching,@EnableAsync等。
@Enable
的注入主要又可以分成两种:

1.使用注解的方式注入
2.使用接口编程的方式注入
Tips:但是不管是哪一种方式都是通过 @Enable* 注解定义上的@Import注解引入的。

使用注解的实现(是在Serlet3.0出现的)就是说在@Enable*的@Import注解中,加载的那个类是被@Configuration注入的,例如@EnableWebMvc。
代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.web.servlet.config.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
//这就是上面说的哪个@Import注解,我们继续查看下DelegatingWebMvcConfiguration 这个类
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

DelegatingWebMvcConfiguration 代码如下(只是摘取了部分,有所省略):

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.web.servlet.config.annotation;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;

@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();

    public DelegatingWebMvcConfiguration() {
    }

    @Autowired(
        required = false
    )
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }

    }

    protected void configurePathMatch(PathMatchConfigurer configurer) {
        this.configurers.configurePathMatch(configurer);
    }

    protected void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        this.configurers.configureContentNegotiation(configurer);
    }

//省略了之后的方法

}

在这里可以很清楚的看到使用@Configuration(这个注解是替代原来在spring xml中注入而出现的注解)进行注入,虽然可以实现功能,但是每次只能注入指定的Bean。

使用编程的方式(是在Serlet3.1出现的)是在@Enable*的@Import注解中,加载的那个类,实现了ImportSelector接口,并且重写 selectImports(AnnotationMetadata importingClassMetadata)方法,可以将需要注入的class名称通过条件筛选后放在一个数组中通过返回值进行注入,这是一个我们一直在用的注解,但是为什么我们并不能在朝夕相处的spring boot 中找到相关的代码呢?因为Spring boot 对其进行了封装,下面我们慢慢顺藤摸瓜的去找一下。
新创建一个工程之后,映入眼帘的就是启动类 *Application.java:
代码如下:

package com.harry.springtest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringtestApplication {

    public static void main(String[] args) {SpringApplication.run(SpringtestApplication.class, args);
    }

}

在main方法中只有一个类的启动方法,那么我们就应该把关注点放在@SpringBootApplication上了
@SpringBootApplication 注解代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

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.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@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 {
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    Class<?>[] exclude() default {};

    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    String[] excludeName() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};
}

在这里我们可以看出来@SpringBootApplication是一个组合注解里面运用到了@EnableAutoConfiguration,我们在继续看一下@EnableAutoConfiguration:

package org.springframework.boot.autoconfigure;

//省略应该引入的包

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    private static final AutoConfigurationImportSelector.AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationImportSelector.AutoConfigurationEntry();
    private static final String[] NO_IMPORTS = new String[0];
    private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class);
    private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
    private ConfigurableListableBeanFactory beanFactory;
    private Environment environment;
    private ClassLoader beanClassLoader;
    private ResourceLoader resourceLoader;

    public AutoConfigurationImportSelector() {
    }
}
//省略以下的所有方法。

DeferredImportSelector继承了ImportSelector

spring 条件装配

条件装配一共有两种方法,可以供我们使用:

1.使用@Profile注解
2.使用@Conditional注解

@Profile只是一个站在大局上的条件装配,通过环境变量的某些值,或者是系统变量某些定值,我们可以使用不同的实现方法。这就类似于我们定义了一个接口,通过多态,定义了两个实现类实现了相同的这个接口,但是我们可以通过环境变量去决定,到底是使用哪一个实现类去实现这个方法。
@Conditional注解,就更佳的灵活一些,类似于我们平常代码中的if else 通过判断出来的boolean值,决定到底是不是应该注入这个bean。
@Conditional代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

我们可以看出来,需要往这个注解中加入一个值,这个值要实现Condition接口。
Condition接口的代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.context.annotation;

import org.springframework.core.type.AnnotatedTypeMetadata;

@FunctionalInterface
public interface Condition {
    //关键之处
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

关键的地方,就在这里了,我们在实现了这个接口的时候,得去重写这个match方法,如果需要注入的地方,就应该返回true,如果是觉得不符合条件,不应该注入的就返回false。
spring boot 通过封装 @Conditional 还有一些派生注解,如下:

@ConditionalOnBean(如果在当前context中存在这个bean,实例化bean)
@ConditionalOnClass(如果在classpath中有这个class,实例化 bean)
@ConditionalOnExpression(当表达式为true的时候,实例化一个bean)
@ConditionalOnMissingBean(如果在当前context中不存在这个bean,实例化一个bean)
@ConditionalOnMissingClass(如果在classpath中没有这个class,实例化一个bean)
@ConditionalOnNotWebApplication(不是web应用,实例化bean)

spring 工厂加载机制

在auto-configuration这个jar包下,或者其他自动注入的jar下,我们都可以看到spring.factories这个文件,例如说下图:


a.png

正是通过这个文件,我们把需要注入的类的全路径配在这里,但是问题来了,谁去读这个文件的呢?
在AutoConfigurationImportSelector.class中,我们可以看到以下方法:

    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;
    }

我们继续刨根问底的看看SpringFactoriesLoader.loadFactoryNames 这个方法:

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

继续看loadSpringFactories 这个方法:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            try {
                //就在这个地方将META-INF/spring.factories读入进来的。
                Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result = new LinkedMultiValueMap();

                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryClassName = ((String)entry.getKey()).trim();
                        String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        int var10 = var9.length;

                        for(int var11 = 0; var11 < var10; ++var11) {
                            String factoryName = var9[var11];
                            result.add(factoryClassName, factoryName.trim());
                        }
                    }
                }

                cache.put(classLoader, result);
                return result;
            } catch (IOException var13) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
            }
        }
    }

这下子终于真相大白了。

后记

通过这一次动手实践,根据源码追根溯源,我才终于明白,年轻真的得多看看源码,因为源码能带给我们的不光是在使用过程中减少bug的出现,以下解决bug的事件,更重要的是在设计上以及代码规范上的潜移默化,虽然说现在有一些还不是理解的很透彻,但是不积跬步,无以至千里。
在这里也要感觉 慕课网上小马哥的Spring Boot 2.0深度实践之核心技术篇
在收听的过程中受益匪浅。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容