Spring boot基础 - 依赖注入

依赖注入(Dependency injection,简称DI)。
依赖,指的是在一个bean对象中存在着对另一个bean对象的引用(通常是成员变量),也就是依赖关系。
注入,指的是在bean的创建过程中,spring会自动地完成它所依赖的bean的创建,然后将引用指向这个依赖对象,即赋值。

@Autowired注解

@Autowired注解是spring中最常见的实现依赖注入的方式。
@Autowired注解的解析和处理,主要由spring容器中的AutowiredAnnotationBeanPostProcessor这个bean后置处理器来完成。
以类的成员变量为例,@Autowired实现依赖注入主要有以下3种方式:

1、标注在属性上:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    @Autowired
    private BookService bookService;

这种情况,当BookController这个bean实例化之后,spring会扫描它内部的@Autowired注解,然后初始化的过程中进行统一处理。
对于bookService这个属性,会在bean factory中找到与其类型匹配的、已经创建好的单例bean对其赋值,如果找不到,就会先进行BookService类型的单例bean的创建(实例化+初始化),然后再对bookService这个属性赋值。

注意:@Autowired标注的变量类型,本身也得是spring bean,不然无法注册到spring容器,就无法进行自动注入。

2、标注在构造方法上:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    private BookService bookService;

    private OrderService orderService;

    //默认构造方法
    public BookController() {
    }

    //含参构造方法1
    @Autowired
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    //含参构造方法2
    public BookController(OrderService orderService) {
        this.orderService = orderService;
    }

这种情况下,spring会在这些自定义构造方法中,优先找到带有@Autowired注解的那个构造方法,来进行实例化;如果这里有多个@Autowired,程序就会报错,因为它不知道我们到底要哪个。

但是使用构造器实例化一定要依赖@Autowired注解吗?并不是,相关的源码片段如下:

		// Candidate constructors for autowiring?
		//寻找候选构造器
		Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
		//判断是否进行构造器自动注入
		if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
				mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
			return autowireConstructor(beanName, mbd, ctors, args);
		}

		// Preferred constructors for default construction?
		//获取merged bean definition中的属性,使用首选的构造器自动注入
		ctors = mbd.getPreferredConstructors();
		if (ctors != null) {
			return autowireConstructor(beanName, mbd, ctors, null);
		}

		// No special handling: simply use no-arg constructor.
		//兜底策略:使用无参的构造函数进行实例化
		return instantiateBean(beanName, mbd);

1)寻找候选的构造器,通常会选择@Autowired注解的构造器,或者一种不需要该注解的特殊场景:有且仅有一个构造函数,并且是含参的。

2)如果有候选构造器,或者merged bean definition中设置了自动注入模式为“AUTOWIRE_CONSTRUCTOR”,或者merged bean definition中constructorArgumentValues属性不为空(比如mybatis的mapper),则调用autowireConstructor方法。
autowireConstructor的大致策略:
根据constructorArgumentValues(构造器参数值)计算出需要的最小参数个数、然后结合候选构造器(如果为空,则获取bean中定义的所有构造器作为候选)的参数个数和类型,计算各构造器匹配度,选择最合适的构造器进行bean的实例化。

3)获取merged bean definition中设置的首选构造器,有则调用autowireConstructor方法。

4)兜底策略,如果以上方式均没有完成实例化,则使用无参数的构造函数进行实例化。

注意: spring在使用构造器进行依赖注入时,会先处理构造方法中的参数,确保参数bean的自动注入。比如示例里的bookService参数,如果之前没有实例化,这里就会先创建bean对象,再交给构造器使用。

3、标注在setter方法上:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    private BookService bookService;

    @Autowired
    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }

这种情况,跟直接标注在属性上的方式类似,也是在bean初始化的过程中完成的注入。
spring在拿到有@Autowired注解的setBookService方法后,同样是先解决了参数bookService的自动注入,确保参数对象创建之后,再调用setBookService方法,将值赋给bookService成员变量。

@Autowired注解的required属性

该注解只有一个required属性,默认为true,表示当前要依赖的这个bean必须存在于spring容器中,不然就报错。而当被显示设置为false时,则表示这个自动注入是可选的,找到类型匹配的bean就注入,没找到就不注入。
接下来分别在属性和方法上测试required属性为false的情况。

1、标注在属性上:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    @Autowired(required = false)
    private BookService bookService;

在示例代码中把BookService的实现类BookServiceImpl的@Service注解去掉,使BookServiceImpl无法被扫描到,从而无法注册到bean factory中。然后运行代码,发现程序正常运行,只不过通过BookController这个bean的get方法拿到bookService属性时,会发现是null,说明并没有强制进行依赖注入。

2、标注在构造函数上:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    private BookService bookService;

    @Autowired(required = false)
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

再次运行代码,发现报错了:
Parameter 0 of constructor in org.example.controller.BookController required a bean of type ‘org.example.service.BookService’ that could not be found. – 构造函数里的BookService,这个类型bean找不到。

不是说不强制自动注入吗?为什么这里没有依赖会报错呢?
其实控制台这里还打印了一句话:
f.a.AutowiredAnnotationBeanPostProcessor : Inconsistent constructor declaration on bean with name ‘bookController’: single autowire-marked constructor flagged as optional - this constructor is effectively required since there is no default constructor to fall back to: public org.example.controller.BookController(org.example.service.BookService)
– 单个autowire标记的构造函数标记为可选——这个构造函数实际上是必需的,因为没有默认构造函数可以回退。

这里说的很清楚了,方法是可选了,但是由于没有无参数的默认构造函数可以兜底,spring只能拿这个方法来构建实例,但是在bean factory中又没注册过BookService类型的bean,参数无法实例化,所以构造失败。

其实如果只有一个构造方法,这个@Autowired加不加、是否可选都一样,因为别无选择。所以我们还是需要加上无参数的构造方法:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    private BookService bookService;

    //默认构造方法
    public BookController() {
    }

    //含参构造方法
    @Autowired(required = false)
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

或者我们也可以把@Autowired(required = false)添加在构造函数的参数上。既然构造方法别无选择,那就把本来需要注入的参数改为可选:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    private BookService bookService;

    //含参构造方法
    public BookController(@Autowired(required = false) BookService bookService) {
        this.bookService = bookService;
    }

事实证明上面两种方式,程序都能正常启动。

3、最后试试标注在setter方法上:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    private BookService bookService;

    @Autowired(required = false)
    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }

程序正常启动,本地方法setBookService的@Autowired注解,尽管是可选,但是在bean的初始化阶段,程序本着能执行尽量执行的原则,处理方法参数,但此时发现有一个参数bean无法实例化,方法执行不了。得了,正好这个方法又是可选,那就不执行了。

但是如果方法的@Autowired注解,其required属性为true,一定要执行,又不想报错呢?
我们还是可以将@Autowired(required = false)的注解加在参数上,这样找不到的参数bean就会被置为null,然后传入方法。

@Primary和@Qualifier注解

@Autowired注解在自动注入的时候,是按照bean的类型进行查找和注入的,还是看这个示例代码:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    @Autowired
    private BookService bookService;

假如我们为BookService接口设计两个实现类,而且都注册成spring bean,那么在自动注入bookService属性的时候,就会找到这两个实现类bean,然后报错:
Description: Field bookService in org.example.controller.BookController required a single bean, but 2 were found
Action: Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

spring告诉我们,这个属性只需要一个bean进行注入,却找到了两个;建议把其中一个bean标记为@Primary,或者使用@Qualifier来指明使用哪个。

解决方案1,使用@Primary标注其中一个bean,作为主bean,这样每次自动注入时都会首选这个bean。

@Service
@Primary
public class BookServiceImpl2 implements BookService {

解决方案2,使用@Qualifier标注需要自动注入的依赖,同时设置@Qualifier的value属性,值为要使用的那个bean的名称。
这种方式相比@Primary更加灵活,两者同时使用时,@Qualifier的优先级也更高。

注意: 这里bean名称的填写,比如实现类BookServiceImpl2在容器中注册的bean,容器默认生成的beanName为首字母小写的"bookServiceImpl2"

@RestController
@RequestMapping(value = "/books")
public class BookController {

    @Autowired
    @Qualifier(value = "bookServiceImpl2")
    private BookService bookService;

@Resource注解

@Resource注解也是常见的用来实现依赖注入的注解。
@Resource注解的解析和处理,主要由spring容器中的CommonAnnotationBeanPostProcessor这个bean后置处理器来完成。

@Resource和@Autowired注解的主要区别在于:
1、归属问题:@Autowired是spring框架自己的注解,而@Resource是Java的通用注解。
2、注入方式:
@Autowired按照bean的类型进行注入,如果需要指定名称需要@Qualifier注解支持;
@Resource支持选择按bean的类型或名称进行注入,默认按照名称注入。
3、注解使用范围、参数类型等。

@Resource注解使用方式:
1、标注在属性上:

@RestController
@RequestMapping(value = "/books")
public class BookController {

    @Resource
    private BookService bookService;

这里BookService是个接口,其实现类BookServiceImpl已经在spring bean factory中注册。

1)只写一个@Resource注解,不设置属性。
这种情况下,spring会将 bookService 这个变量名当做默认的依赖bean名称,先去检查bean factory里是否有这个名字,
如果有就按bookService这个名字去找到一个bean,完成注入;
如果没有就回退到按类型BookService查找,最终找到BookServiceImpl这个bean,完成注入。

2)设置name属性,@Resource(name = “bookServiceImpl”)
这种情况下,spring认为你已经指定名字了,就不会再按类型兜底,而是直接拿这个名字去获取bean,能找到就注入,找不到就报错。
注意: 这里的name不能瞎写,根据它找到的bean,必须是BookService的实例(实现该接口),否则即使拿到或者创建出这个bean,也会在最后的类型检查中报错。

3)设置type属性,@Resource(type = BookServiceImpl.class)
这种情况下,和情况1)类似,只是在回退到按类型查找时,使用的类型不是依赖的类型BookService,而是注解中指定的type值。
注意: 这里的type不能瞎写,它必须是BookService的实现类,不然在注入元数据的组装阶段(注入开始之前)就会报错说类型不匹配。

4)同时设置name和type属性,@Resource(name = “bookServiceImpl”, type = BookServiceImpl.class)
这种情况下,和情况2)类似,直接拿这个name去获取bean,只是在最后拿到bean实例的时候,进行类型判断所使用的目标类型不是BookService,而是注解中指定的type值。同样的,type也不能瞎写,不然在注入元数据的组装阶段就会报错。

2、标注在普通方法上。
@Resource注解同样可以标注在setter方法上,在bean初始化的过程中,完成属性的依赖注入。

@RestController
@RequestMapping(value = "/books")
public class BookController {

    private BookService bookService;

    @Resource
    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }

你可能感兴趣的:(spring,boot,spring,java)