Spring 自动装配方式和懒加载(注解版)

1,026 阅读10分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第17天,点击查看活动详情

1.自动装配注解

1.1 @Autowired注解

@Autowired 注解,可以对类成员变量、方法和构造函数进行标注,完成自动装配的工作。@Autowired 注解可以放在类,接口以及方法上。

package org.springframework.beans.factory.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.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
	boolean required() default true;
}

@Autowired 注解说明:

(1)默认优先按照类型去容器中找对应的组件,找到就赋值;

(2)如果找到多个相同类型的组件,再将属性名称作为组件的id到IoC容器中进行查找。

@Autowired在使用Idea的时候,不推荐使用该注解了,也改变了我的习惯,推荐使用构造方法的方式

1.2 @Qualifier注解

@Autowired是根据类型进行自动装配的,如果需要按名称进行装配,则需要配合@Qualifier 注解使用。

@Qualifier注解源码如下所示。

package org.springframework.beans.factory.annotation;
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;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
	String value() default "";
}

1.3 @Primary注解

在Spring中使用注解,常使用@Autowired, 默认是根据类型Type来自动注入的。但有些特殊情况,对同一个接口,可能会有几种不同的实现类,而默认只会采取其中一种实现的情况下, 就可以使用@Primary注解来标注优先使用哪一个实现类。

@Primary注解的源码如下所示。

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 Primary {

}

2.测试案例

2.1 测试@Autowired注解

这里,我们以之前项目中创建的dao、service和controller为例进行说明。dao、service和controller的初始代码分别如下所示,经典的MVC结构,应该使用接口编程方式,我这里只是简单测试。

  • dao(mapper)
package com.hanpang.person.dao;
import org.springframework.stereotype.Repository;
@Repository
public class PersonDao {
}
  • service
package com.hanpang.person.service;
import com.hanpang.person.dao.PersonDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PersonService {
    @Autowired
    private PersonDao personDao;
    //个人推荐构造方法的方式,这里只是演示Autowried注解
}
  • controller
package com.hanpang.person.controller;
import com.hanpang.person.service.PersonService;
import org.springframework.stereotype.Controller;

@Controller
public class PersonController {
    @Autowired
    private PersonService personService;
}

可以看到,我们在Service中使用@Autowired注解注入了Dao,在Controller中使用@Autowired注解注入了Service。为了方便测试,我们在PersonService类中生成一个toString()方法,如下所示。

package com.hanpang.person.service;
import com.hanpang.person.dao.PersonDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PersonService {
    @Autowired
    private PersonDao personDao;

    @Override
    public String toString() {
        return personDao.toString();
    }
}

这里,我们在PersonService类的toString()方法中直接调用personDao的toString()方法并返回。为了更好的演示效果,我们在项目的 com.hanpang.config 包下创建AutowiredConfig类,如下所示。

package om.hanpang.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
//可以使用通配符表达式:com.hanpang.person开头
@ComponentScan(value = {
        "com.hanpang.person.dao", 
        "com.hanpang.person.service", 
        "com.hanpang.person.controller"})
public class AutowiredConfig {

}

接下来,我们来测试一下上面的程序,我们在项目的src/test/java目录下的 com.hanpang.spring.test 包下创建AutowiredTest类,如下所示。

package com.hanpang.spring.test;
import com.hanpang.config.AutowiredConfig;
import com.hanpang.person.service.PersonService;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AutowiredTest {
    @Test
    public void testAutowired01(){
        //创建IOC容器
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AutowiredConfig.class);
        PersonService personService = context.getBean(PersonService.class);
        System.out.println(personService);
        context.close();
    }
}

测试方法比较简单,这里,我就不做过多说明了。接下来,我们运行AutowiredTest类的testAutowired01()方法,得出的输出结果信息如下所示。

com.hanpang.person.dao.PersonDao@10e92f8f

可以看到,输出了PersonDao信息。

那么问题来了:我们在PersonService类中输出的PersonDao,和我们直接在Spring IOC容器中获取的PersonDao是不是同一个对象呢?

我们可以在AutowiredTest类的testAutowired01()方法中添加获取PersonDao对象的方法,并输出获取到的PersonDao对象,如下所示。

@Test
public void testAutowired01(){
    //创建IOC容器
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AutowiredConfig.class);
    PersonService personService = context.getBean(PersonService.class);
    System.out.println(personService);
    PersonDao personDao = context.getBean(PersonDao.class);
    System.out.println(personDao);
    context.close();
}

我们再次运行AutowiredTest类的testAutowired01()方法,输出的结果信息如下所示。

com.hanpang.person.dao.PersonDao@10e92f8f
com.hanpang.person.dao.PersonDao@10e92f8f

可以看到,我们在PersonService类中输出的PersonDao对象和直接从IOC容器中获取的PersonDao对象是同一个对象。

如果在Spring容器中存在对多个PersonDao对象该如何处理呢?

首先,为了更加直观的看到我们使用@Autowired注解装配的是哪个PersonDao对象,我们对PersonDao类进行改造,为其加上一个remark字段,为其赋一个默认值,如下所示。

package com.hanpang.person.dao;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Repository;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Repository
public class PersonDao {
    private String remark = "1";
}

接下来,我们就在AutowiredConfig类中注入一个PersonDao对象,并且显示指定PersonDao对象在IOC容器中的bean的名称为personDao2,并为PersonDao对象的remark字段赋值为2,如下所示。

  @Bean("personDao2")
  public PersonDao personDao(){
      return new PersonDao("2");
  }

目前,在我们的IOC容器中就会注入两个PersonDao对象。那此时, @Autowired注解装配的是哪个PersonDao对象呢?

接下来,我们运行AutowiredTest类的testAutowired01()方法,输出的结果信息如下所示。

PersonDao{remark='1'}

可以看到,结果信息输出了1,说明: @Autowired注解默认优先按照类型去容器中找对应的组件,找到就赋值;如果找到多个相同类型的组件,再将属性名称作为组件的id到IoC容器中进行查找。

那我们如何让@Autowired装配personDao2呢?  这个问题问的好,其实很简单,我们将PersonService类中的personDao全部修改为personDao2,如下所示。

package com.hanpang.person.service;
import com.hanpang.person.dao.PersonDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PersonService {
    @Autowired
    private PersonDao personDao2;
    @Override
    public String toString() {
        return personDao2.toString();
    }
}

此时,我们再次运行AutowiredTest类的testAutowired01()方法,输出的结果信息如下所示。

PersonDao{remark='2'}

可以看到,此时命令行输出了personDao2的信息。

2.2 测试@Qualifier注解

从测试@Autowired注解的结果来看: @Autowired注解默认优先按照类型去容器中找对应的组件,找到就赋值;如果找到多个相同类型的组件,再将属性名称作为组件的id到IoC容器中进行查找。

如果IOC容器中存在多个相同类型的组件时,我们可不可以显示指定@Autowired注解装配哪个组件呢?有些小伙伴肯定会说:废话!你都这么问了,那肯定可以啊!没错,确实可以啊!此时,@Qualifier注解就派上用场了!

在之前的测试案例中,命令行输出了 PersonDao{remark='2'} 说明@Autowired注解装配了personDao2,那我们如何显示的让@Autowired注解装配personDao呢?

比较简单,我们只需要在PersonService类上personDao2字段上添加@Qualifier注解,显示指定@Autowired注解装配personDao,如下所示。

@Qualifier("personDao")
@Autowired
private PersonDao personDao2;

此时,我们再次运行AutowiredTest类的testAutowired01()方法,输出的结果信息如下所示。

PersonDao{remark='1'}

可以看到,此时尽管字段的名称为personDao2,但是我们使用了@Qualifier注解显示指定@Autowired注解装配personDao对象,所以,最终的结果输出了personDao对象的信息。

2.3 测试容器中无组件的情况

如果IOC容器中无相应的组件,会发生什么情况呢?此时,我们删除PersonDao类上的@Repository注解,并且删除AutowiredConfig类中的personDao()方法上的@Bean注解,如下所示。

package com.hanpang.person.dao;

public class PersonDao {
    private String remark = "1";

    public String getRemark() {
        return remark;
    }

    public void setRemark(String remark) {
        this.remark = remark;
    }

    @Override
    public String toString() {
        return "PersonDao{" +
                "remark='" + remark + ''' +
                '}';
    }
}
package com.hanpang.config;

import com.hanpang.person.dao.PersonDao;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
//可以使用通配符表达式:com.hanpang.person开头
@ComponentScan(value = {
        "com.hanpang.person.dao", 
        "com.hanpang.person.service", 
        "com.hanpang.person.controller"})
public class AutowiredConfig {
    public PersonDao personDao(){
        PersonDao personDao = new PersonDao();
        personDao.setRemark("2");
        return personDao;
    }
}

此时IOC容器中不再有personDao,我们再次运行AutowiredTest类的testAutowired01()方法,输出的结果信息如下所示。

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.hanpang.person.dao.PersonDao' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Qualifier(value=personDao), @org.springframework.beans.factory.annotation.Autowired(required=true)}

可以看到,Spring抛出了异常,未找到相应的bean对象,我们能不能让Spring不报错呢?  那肯定可以啊!Spring的异常信息中都给出了相应的提示。

{@org.springframework.beans.factory.annotation.Qualifier(value=personDao), @org.springframework.beans.factory.annotation.Autowired(required=true)}

解决方案就是在PersonService类的@Autowired添加一个属性required=false,如下所示。

@Qualifier("personDao")
@Autowired(required = false)
private PersonDao personDao2;

并且我们修改下PersonService的toString()方法,如下所示。

@Override
public String toString() {
    return "PersonService{" +
        "personDao2=" + personDao2 +
        '}';
}

此时,还需要将AutowiredTest类的testAutowired01()方法中直接从IOC容器中获取personDao的代码删除,如下所示。

@Test
public void testAutowired01(){
    //创建IOC容器
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AutowiredConfig.class);
    PersonService personService = context.getBean(PersonService.class);
    System.out.println(personService);
    context.close();
}

此时,我们再次运行AutowiredTest类的testAutowired01()方法,输出的结果信息如下所示。

PersonService{personDao2=null}

可以看到,当为@Autowired添加属性required=false后,即使IOC容器中没有对应的对象,Spring也不会抛出异常。此时,装配的对象就为null。

测试完成后,我们再次为PersonDao类添加@Repository注解,并且为AutowiredConfig类中的personDao()方法添加@Bean注解。

2.4 测试@Primary注解

在Spring中,对同一个接口,可能会有几种不同的实现类,而默认只会采取其中一种实现的情况下, 就可以使用@Primary注解来标注优先使用哪一个实现类。

首先,我们在AutowiredConfig类的personDao()方法上添加@Primary注解,此时,我们需要删除PersonService类中personDao字段上的@Qualifier注解,这是因为@Qualifier注解为显示指定装配哪个组件,如果使用了@Qualifier注解,无论是否使用了@Primary注解,都会装配@Qualifier注解标注的对象。

设置完成后,我们再次运行AutowiredTest类的testAutowired01()方法,输出的结果信息如下所示。

PersonService{personDao2=PersonDao{remark='2'}}

可以看到,此时remark的值为2,装配了AutowiredConfig类中注入的personDao。

接下来,我们为PersonService类中personDao字段再次添加@Qualifier注解,如下所示。

@Qualifier("personDao")
@Autowired(required = false)
private PersonDao personDao;

此时,我们再次运行AutowiredTest类的testAutowired01()方法,输出的结果信息如下所示。

PersonService{personDao=PersonDao{remark='1'}}

可以看到,此时,Spring装配了使用@Qualifier标注的personDao。

3. 懒加载

懒加载就是Spring容器启动的时候,先不创建对象,在第一次使用(获取)bean的时候,创建并使用对象,有点像单例模式中的懒汉模式?

3.1 非懒加载模式

此时,我们将PersonConfig类的配置修改成单实例bean,如下所示。

package com.hanpang.config;

import com.hanpang.model.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class PersonConfig {
    @Bean("person")
    public Person person(){
        System.out.println("给容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

接下来,在SpringBeanTest类中创建testAnnotationConfig()方法,如下所示。

@Test
public void testAnnotationConfig(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    System.out.println("IOC容器创建完成");
}

运行SpringBeanTest类中的testAnnotationConfig5()方法,输出的结果信息如下所示。

给容器中添加Person....
IOC容器创建完成

可以看到,单实例bean在Spring容器启动的时候就会被创建,并加载到Spring容器中。

3.2 懒加载模式

我们在PersonConfig的person()方法上加上@Lazy注解将Person对象设置为懒加载,如下所示。

package com.hanpang.config;

import com.hanpang.model.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

@Configuration
public class PersonConfig {

    @Lazy
    @Bean("person")
    public Person person(){
        System.out.println("给容器中添加Person....");
        return new Person("binghe002", 18);
    }
}

此时,我们再次运行SpringBeanTest类中的testAnnotationConfig()方法,输出的结果信息如下所示。

IOC容器创建完成

可以看到,此时,只是打印出了“IOC容器创建完成”,说明此时,只创建了IOC容器,并没有创建bean对象。

那么,加上@Lazy注解后,bean是何时创建的呢?我们在SpringBeanTest类中的testAnnotationConfig()方法中获取下person对象,如下所示。

@Test
public void testAnnotationConfig5(){
    ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
    System.out.println("IOC容器创建完成");
    Person person = (Person) context.getBean("person");
}

此时,我们再次运行SpringBeanTest类中的testAnnotationConfig5()方法,输出的结果信息如下所示。

IOC容器创建完成
给容器中添加Person....

说明,我们在获取bean的时候,创建了bean对象并加载到Spring容器中。

那么,问题又来了,只是第一次获取bean的时候创建bean对象吗?多次获取会不会创建多个bean对象呢?我们再来完善下测试用例,在在SpringBeanTest类中的testAnnotationConfig()方法中,再次获取person对象,并比较两次获取的person对象是否相等,如下所示。

IOC容器创建完成
给容器中添加Person....
true

从输出结果中,可以看出使用@Lazy注解标注后,单实例bean对象只是在第一次从Spring容器中获取对象时创建,以后每次获取bean对象时,直接返回创建好的对象。

总结:懒加载,也称延时加载。仅对单例bean生效。单例bean是在Spring容器启动的时候加载的,添加@Lazy注解后就会延迟加载,在Spring容器启动的时候并不会加载,而是在第一次使用此bean的时候才会加载,但当你多次获取bean的时候不会重复加载,只是在第一次获取的时候会加载,这不是延迟加载的特性,而是单例Bean的特性。