Spring-Bean的注册与注入方式

1、背景

IoC(Inversion of Control)控制反转,IoC是一种通过描述来生成或获取对象的技术。每一个需要管理的对象称为Spring Bean,而Spring管理这些Bean的容器称为Spring IoC容器,也就是我们所说的Spring应用程序上下文ApplicationContext。

IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。交由Spring容器统一进行管理,从而实现松耦合。

  1. 容器启动,创建和初始化IoC 容器
  2. 扫描包下所有class,通过反射解析class类的信息,包括注解信息
  3. 基于反射实例的对象,对其进行封装
  4. 将实例的对象放入集合中保存
  5. 可以通过getBean获取集合中对象

2、@SpringBootApplication

@SpringBootApplication开启了Spring的组件扫描和SpringBoot自动配置功能。实际上它是一个复合注解,包含3个重要的注解: @SpringBootConfiguration、@ComponentScan、@EnableAutoConfiguration

SpringBoot早期的版本中,需要在入口类同时添加这3个注解,但从SpringBoot1.2.0开始,只要在入口类添加@SpringBootApplication注解即可。我们如果想理解它的含义,可以从通过分析这3个注解开始。

2.1、@SpringBootConfiguration

@SpringBootConfiguration 是对 @Configuration 的简单封装,二者功能上没有太大差异。后者我们比较熟悉,所以 @SpringBootConfiguration 可以理解为配置类的注解,会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到spring容器中。

2.2、@ComponentScan

@ComponentScan主要就是定义扫描的路径,从中找出标识了需要装配的类自动装配到spring的bean容器中。默认扫描路径是当前路径,因为启动类是在项目的根路径,所以启动类中的该注解,默认扫描的当前项目路径。

那么怎么找出需要装配的类呢?我们知道正常装载类,是要在配置类下注册Bean的。但是我们日常使用的@Controller、@Service、@Repository、@Component、@Configuration却可以自动装载,就是因为@ComponentScan这个注解。@Component是个特殊的注解,它可以被@ComponentScan扫描装载,如果查看@Controller、@Service、@Repository、@Configuration等注解的源码就会发现,它们都继承@Component,因此同样也可以自动被扫描装载进容器中。

@ComponentScan注解的常用属性包括:
  • value/basePackages:定义扫描的路径。
  • includeFilters:加入扫描路径下,没有继承@Component注解的类加入spring容器。
  • excludeFilters:过滤出不用加入spring容器的类。

2.3、@EnableAutoConfiguration

项目开发时,经常通过maven等方式引入第三方jar包,那么也同样期望能够将第三方jar包中的Bean也加载进当前项目的Bean容器中。前面说过@ComponentScan可以将定义扫描路径下的Bean装载进容器中,因此可以手动的在@ComponentScan注解的value属性中,填上所有第三方jar包的扫描路径。理论上是这样的,但应该没有人这么干吧?一个项目依赖几十个jar包,那要配置多少扫描路径?

因此一般有另外的两种方法来实现需求,一般我们开发一个供调用的jar包应用(如starter),主要依托本身的配置类来注入Bean(@Configuration+@Bean 或 @ComponentScan+@Component),因此只要保证第三方应用的配置类能加载进当前的Bean容器即可。

@Import 和 spring.factories
  1. 第三方应用,定义一个注解(一般是Enable开头),通过 @Import 注解注入配置类。并且当前项目的配置类(建议启动类)中加上该注解。
//配置类
@Configuration
@EnableConfigurationProperties({ SecurityProperty.class})
@ComponentScan(basePackages = {"com.smec.mpaas.unicorn"})
public class UnicornAutoConfig { }
//注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Import({UnicornAutoConfig.class})
public @interface EnableUnicorn {
}
  1. 在第三方应用/META-INF 目录下创建 spring.factories 文件,并定义需要加载的配置类路径。当前项目的 @EnableAutoConfiguration
// mybatis 的 spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

我们重点谈谈第二种,因为用到了@EnableAutoConfiguration,注解的定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class[] exclude() default {};
    String[] excludeName() default {};
}

@EnableAutoConfiguration实现的关键在于引入了AutoConfigurationImportSelector,其核心逻辑为selectImports方法,借助AutoConfigurationImportSelector,它可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。

xxxAutoConfiguration条件注解

当springboot扫描到@EnableAutoConfiguration注解时则会将spring-boot-autoconfigure.jar/META-INF/spring.factories文件中org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的value里的所有xxxConfiguration类加载到IOC容器中。spring.factories文件里每一个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应用环境下起效
AutoConfigurationImportSelector类的逻辑

SpringBoot中EnableAutoConfiguration实现的关键在于引入了AutoConfigurationImportSelector,其核心逻辑为selectImports方法,逻辑大致如下:

  1. 从配置文件META-INF/spring.factories加载所有可能用到的自动配置类;
  2. 去重,并将exclude和excludeName属性携带的类排除;
  3. 过滤,将满足条件(@Conditional)的自动配置类返回;

3、Bean注册

IoC容器中主要就是管理Bean,那么在这之前,最重要的就是如何将自定义的Bean注册进IoC容器中。常见的有两种方式:(1)配置@Component注解,或者包含@Component的注解;(2)@Bean+@Configuration的组合。

3.1、@Component

@Component倾向于组件扫描和自动装配。前文说过了,主要是配合@ComponentScan注解,该注解会扫描指定路径下包含@Component注解的类,并将其注册进IoC容器。spring中有很多其他的注解也是依托@Component实现的,如:@Controller、@Service、@Repository、@Configuration等。

注册进IoC容器后,Bean的名字取决于两种情况:
  1. 如果在注解中指定了value,则为value的值
  2. 如果在注解中没有指定value,默认是类名的第一个字母小写命名(如:类名 UserDepartMerge,bean名 userDepartMerge)
@Component只有一个注解属性:
  • value:在代码中定义了别名,为bean起一个名字。

3.2、@Bean

@Bean修饰的方法,必须中配置类有@Configuration的类)中定义。在方法上打上注解@Bean,方法返回我们要创建的对象,即表示声明该方法返回的实例是受Spring管理的Bean。

相比较于@Component而言,@Bean的方式更加灵活。假如你要引入第三方库,可是如果你没有源代码,也就无法在其上添加@Component,自动设置也就无从下手,但@Bean会返回一个被spring认可的Bean。

@Configuration
public class BeanConfig {

    @Bean(initMethod = "initMethod",destroyMethod = "destroyMethod")
    public User user(){
        return new User();
    }
}
注册进IoC容器后,Bean的名字取决于两种情况:
  1. 如果在注解中指定了name,则为name属性的值
  2. 如果没有指定name,默认是方法名(示例中即:user)
@Bean的注解属性解析:
  • value/name:name和value两个属性是相同的含义的,在代码中定义了别名。为bean起一个名字,如果默认没有写该属性,那么就使用方法的名称为该bean的名称。
  • autowire:装配方式 有三个选项Autowire.NO (默认设置)/Autowire.BY_NAME/Autowire.BY_TYPE。指定bean的装配方式,根据名称和根据类型装配,一般不设置,采用默认即可。
  • initMethod:bean的初始化方法,直接指定方法名称(bean所对应类中,某个实例方法的名称)即可,不用带括号。
  • destroyMethod:bean的销毁方法,在调用IoC容器的close()方法时,会执行到该属性指定的方法。不过,只是单实例的bean才会调用该方法,如果是多实例的情况下,不会调用该方法。

4、Bean注入

bean的完全初始化过程比较复杂,简单来说:先通过构造方法生成一个实例,其次再初始化实例中属性的值。当bean已经被加载进IoC容器后,在有些开发过程中如果需要用到一些bean的实例,就要对其注入。
spring发展中,流行过三种注入方式,当有多个bean注入时,如果存在依赖关系,“谁先加载,谁后加载”有时就很重要。这和下面三种注入方式也有关系,一个经典的案例就是spring的循环依赖问题,这个后面会有单独一篇文章介绍,下文先打好基础。

4.1、基于字段注入

这是早期最常见的注入方式,借助 @Autowired 或 @Resource 来注入,注解可以加在字段上。

byName 和 byType

spring在获取bean时,有两种依赖途径:byName 和 byType。之前在介绍bean的注册时,一直强调bean被加载进IoC容器后命名,byName就是根据bean的名字去IoC容器中寻找bean注入。而byType是根据被注册bean的类型来寻找的,类型即bean对应的类或接口。

@Resource 注解

@Resource注解的路径是 javax.annotation;,它是属于J2EE的注解。
@Resource默认是按照byName方式注入的。
@Resource有两个重要属性:name,type:

  • 如果只指定了name属性,则按照byName方式注入;
  • 如果只指定了type属性,则按照byType方式注入;
  • 如果两个属性都没指定,则默认按照byName方式注入;
  • 如果两个属性都指定了,则要求按照byNamebyType两种方式都匹配,才会注入。

示例代码:

@Component
public class User {
    @Resource(name = "type",type = Type.class)
    private Type type;

}
@ Autowirec 注解

@Autowired注解的路径是org.springframework.beans.factory.annotation;,它是属于Spring的注解。
@Autowired并不能指定注入方式是byName还是byType,默认只能按照byType方式注入。如果想通过byName方式注入,得配合@Qualifier注解一起使用:

  • 默认按照byType方式注入;
  • 配合@Qualifier注解使用,可以实现byName方式注入;
  • 如果按照byType匹配到多个bean,则会自动将属性名视为bean的名称,通过byName方式匹配注入。

示例代码中,有个Action的接口,两个实现类Human、Dog。因为Action类型能匹配两个bean,根据属性名dog会匹配到dog:

//Action
@Component
public interface Action {
    String run();
}
//Human
@Component
public class Human implements Action{
    @Override
    public String run() {
        return "Human run!";
    }
}
//Dog
@Component
public class Dog implements Action{
    @Override
    public String run() {
        return "Dog run!";
    }
}
//Controller
@RestController
public class DemoController {
    @Autowired
    private Action dog;

    @GetMapping("/demo")
    public String demo() {
        return dog.run();
    }
}

4.2、基于setter注入

基于setter的注入,就是添加属性的setter方法。setter方法上的@Autowired注解可以加,也可以不加,不会影响功能实现,加了可能只是有助于区分其他普通的setter方法。

@Component
public class User {
    private Type type;

    //@Autowired
    public void setType(Type type) {
        this.type = type;
    }
}

上文代码中,User 和 Type 两个类都加上了@Component的注解,即都已经被注入IoC容器中。User在创建实例的时候只执行自身的构造方法即可,但在初始化type属性数据时会调用set方法,会注入Type类的实例。所以会先执行User构造方法,再执行Type构造方法。

相较于字段注入的方式来说,改善了部分问题,但不明显。而且如果需要注入的bean多,settter方法就要写一堆,代码结构很糟糕。所以一般更推荐构造器注入

4.3、构造器注入

下列代码是不是很熟悉?Angular中依赖注入组件,就是这种写法。在User创建实例时,就依赖于Type的实例。因此先执行Type的构造方法,再执行User的构造方法。

@Component
public class User {
    private final Type type;

    public User(Type type) {
        this.type = type;
    }
}

这个构造器注入的方式啊,能够保证注入的组件不可变(final),并且确保需要的依赖不为空。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。

4.4、比较

基于字段注入的方式,会使代码看起来更整洁,但是对于空指针等问题无法及时避免。基于构造器注入的方式,貌似是前者短板的最好替代。我不建议考虑setter注入方式。

我们当在字段上使用@Autowired时,IDE一般都会给我们一个警告 Field injection is not recommended,不推荐使用字段注入。这个提示是spring framerwork 4.0以后开始出现的,spring 4.0开始就不推荐使用属性注入,改为推荐构造器注入setter注入

@Autowired因为默认是byType注入,经常会因为注入的类型是接口,在初始化属性数据时,却发现IoC容器中并没有加载实现类。然后就出现了空指针的错误,包括还有可能隐藏的循环依赖错误。因此在很多的开发规范中,都推荐用构造器注入,替代@Autowired

但为啥对于在字段上使用@Resource,IDE不警告呢?个人认为毕竟@Resource的属性很多,可以定制化实现很多功能,这些灵活的功能,构造器注入方式替代不了。不像@Autowired那么鸡肋。

5、Aware属性注入

Spring中有很多继承于aware中的接口,下面列出了一部分:

  • ApplicationContextAware
  • ApplicationEventPublisherAware
  • BeanClassLoaderAware
  • BeanFactoryAware
  • BeanNameAware
  • EnvironmentAware
  • ImportAware
  • MessageSourceAware
  • NotificationPublisherAware
  • ResourceLoaderAware
  • ServletConfigAware
  • ServletContextAware

aware,英文翻译是“知道的,意识到的”。可以理解成,如果实现了这些 xxxAware的接口,就能感知到被修饰的xxx属性。如:实现BeanNameAware接口,能获取到当前bean的名称。实现ApplicationContextAware接口,能获取到ApplicationContex。

这些接口中都有且只有一个去掉接口名中的Aware后缀的设置方法,例如BeanNameAware接口只有一个void setBeanName(String var1)的方法。

所以可以理解成,这些Aware接口的作用,是让开发者实现相应的 属性注入。如下例中,通过单例模式实现一个获取全局ApplicationContext的方法,可以手动获取IoC容器和容器中的bean。

@Component
public class ApplicationContextAwareImpl implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext curApplicationContext) {
        if (applicationContext == null) {
            applicationContext = curApplicationContext;
        }
    }

    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

}

你可能感兴趣的:(springboot)