7、bean 的自动装配

  • 自动装配是使用 Spring 满足 bean 依赖的一种方式
  • Spring 会在应用上下文中为某个 bean 寻找其依赖的 bean,即在上下文自动寻找并自动给 bean 装配属性

由于在手动配置 xml 过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。采用自动装配将避免这些错误,并且使配置简单化。

在 Spring 中有三种装配方式:

  1. 在 xml 中显式的配置
  2. 在 Java 中显式的配置
  3. 隐式的自动装配 bean【重要】

Spring 的自动装配需要从两个角度来实现,或者说是两个操作:

  1. 组件扫描(component scanning):Spring 会自动发现应用上下文中所创建的 bean;
  2. 自动装配(autowiring):Spring 自动满足 bean 之间的依赖,也就是我们说的 IoC/DI;

组件扫描和自动装配组合发挥巨大威力,使得显示的配置降低到最少。

推荐不使用自动装配xml配置,而使用注解。

7.1 测试

环境搭建:一个人有两个宠物

public class Dog {
    public void shout(){
        System.out.println("汪~");
    }
}
public class Cat {
    public void shout(){
        System.out.println("喵~");
    }
}
public class People {
    private Cat cat;
    private Dog dog;
    private String name;
    
    set/get/toString...
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="cat" class="com.song.pojo.Cat"/>    
    <bean id="dog" class="com.song.pojo.Dog"/>
    
    <bean id="people" class="com.song.pojo.People">
        <property name="name" value="张三"/>
        <property name="cat" ref="cat"/>
        <property name="dog" ref="dog"/>
    </bean>
</beans>
public class MyTest {
    @Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        People people = context.getBean("people", People.class);
        people.getCat().shout(); // 喵~
        people.getDog().shout(); // 汪~
    }
}
结果正常输出

7.2 byName 自动装配

autowire byName (按名称自动装配)

修改bean配置,增加一个属性 autowire="byName",使用 ref 的 property 可以省略:

<bean id="cat" class="com.song.pojo.Cat"/>
<bean id="dog" class="com.song.pojo.Dog"/>

<!--
    byName: 会自动在容器上下文中查找,和自己对象 set 方法后面的值对应的 beanid
-->
<bean id="people" class="com.song.pojo.People" autowire="byName">
    <property name="name" value="张三"/>
    <!--<property name="cat" ref="cat"/>-->
    <!--<property name="dog" ref="dog"/>-->
</bean>

结论:

当一个bean节点带有 autowire byName 的属性时:

  1. 将查找其类中所有的 set 方法名,例如 setCat,获得将 set 去掉并且首字母小写的字符串,即 cat。
  2. 去 spring 容器中寻找是否有此字符串名称 id 的对象。
  3. 如果有,就取出注入;如果没有,就报空指针异常。

即,byName 必须保证 bean 的 id 唯一,并且这个 bean 要和自动注入的属性的 setXxx 方法的名字 xxx(将 set 去掉并且首字母小写) 一致。

7.3 byType 自动装配

autowire byType (按类型自动装配)

修改bean配置,增加一个属性 autowire="byType":

<bean id="cat" class="com.song.pojo.Cat"/>
<bean class="com.song.pojo.Dog"/>

<!--
    byType:会自动在容器上下文中查找,和自己对象 属性类型相同的 bean
-->
<bean id="people" class="com.song.pojo.People" autowire="byType">
    <property name="name" value="张三"/>
    <!--<property name="cat" ref="cat"/>-->
    <!--<property name="dog" ref="dog"/>-->
</bean>

结论:

  • byType 必须保证所有 bean 的 class 唯一,并且这个 bean 需要和自动注入的属性的类型一致。
  • byType 必须保证类型全局唯一。即:同一类型的对象,在spring容器中唯一,如果不唯一,会报不唯一的异常 NoUniqueBeanDefinitionException

注意:对于 byName 自动装配,自动注入的属性的 bean 的 id 是可以省略的。

7.4 使用注解实现自动装配

The introduction of annotation-based configuration raised the question of whether this approach is “better” than XML.

jdk1.5 开始支持注解,spring2.5 开始全面支持注解。

使用注解的准备工作:

  1. 导入约束:context 约束
  2. 配置注解的支持:<context:annotation-config/>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>
</beans>

@Autowried

直接在属性上使用即可,也可以在 set 方式上使用。

使用 Autowired 可以不用编写 set 方法,前提是这个自动装配的属性在 IOC(Spring)容器中存在,且符合类型 byType。

测试

  1. 在属性上使用 @Autowired 注解:
public class People {
    @Autowired
    @Qualifier(value = "cat1")
    private Cat cat;
    @Autowired
    private Dog dog;
    private String name;
    
    getter方法……
}
  1. 配置文件内容:
<!--开启注解支持-->
<context:annotation-config/>

<bean id="cat" class="com.song.pojo.Cat"/>
<bean id="dog" class="com.song.pojo.Dog"/>
<bean id="people" class="com.song.pojo.People"/>
  1. 测试结果:成功输出

总结

@Autowired 首先按类型(byType)自动装配;如果同一类型有多个 bean 存在,再按照属性名(byName)进行装配,即 bean 的 id 与属性名一致;如果属性名装配不成功,可以使用 @Qualifier(value = “xxx”)装配与 id 一致的属性名。

扩展

Autowired 源码:

public @interface Autowired { boolean required() default true; }

关于对象为空的问题

  • Autowired 的 required 属性
    required 属性默认为 true,对象必须存对象,不能为 null;如果为 false,则这个对象可以为 null。
// 如果显式定义了 Autowired 的 required 属性为false,说明这个对象可以为 null,否则不允许为空
@Autowired(required = false)
  • @Nullable
    字段标记了这个注解,说明这个字段可以为 null

@Qualifier

如果 @Autowired 自动装配的环境比较复杂,自动装配无法通过一个注解【@Autowired】完成的时候,可以使用@Qualifier(value = "xxx") 去配置 @Autowired 的使用,指定一个唯一的 bean 对象注入(byName)。

注意:@Qualifier(value = "xxx") 不能单独使用

测试

  1. 配置文件修改内容,保证类型存在,但名字不为类的默认名字!
<!--开启注解支持-->
<context:annotation-config/>
<bean id="cat1" class="com.song.pojo.Cat"/>
<bean id="cat111" class="com.song.pojo.Cat"/>
<bean id="dog2" class="com.song.pojo.Dog"/>
<bean id="dog222" class="com.song.pojo.Dog"/>
<bean id="people" class="com.song.pojo.People"/>
  1. 没有加 @Qualifier 测试,直接报错
  2. 在属性上添加 @Qualifier 注解
public class People {
    @Autowired(required = false)
    @Qualifier(value = "cat111")
    private Cat cat;
    @Autowired
    @Qualifier(value = "dog222")
    private Dog dog;
    private String name;
}
  1. 测试结果:成功输出

@Resource

@Resource 如果有指定的 name 属性,先按该属性进行 byName 方式查找装配;其次再进行默认的 byName 方式进行装配;如果以上都不成功,则按 byType 的方式自动装配;都不成功,则报异常。

public class People {
    @Resource(name = "cat2")
    private Cat cat;
    @Resource
    private Dog dog;
    private String name;
}

小结:

@Autowired 和 @Resource 异同:

  • 作用相同都是用注解方式注入对象,都是用来自动装配 bean 的,都可以放在属性字段,或写在 setter 方法上。
  • @Autowired (属于spring规范)默认通过 byType 的方式实现,而且必须要求这个对象存在(除非制定了 required 属性为 false),类型重复再通过 id 与属性名匹配(byName)找,都不合适则结合 @Qualifier 注解使用名字装配。
  • @Resource(属于 J2EE 复返) 默认通过 byName 的方式实现,名称可以通过name属性进行指定。如果没有指定name属性,当注解写在字段上时,默认取字段名进行按照名称查找,如果注解写在 setter 方法上默认取属性名进行装配。如果找不到与名称匹配的bean时,则通过 byType 实现自动装配。如果两个都找不到的情况下就报错。注意,如果 name 属性一旦指定,就只会按照名称进行装配。
  • 执行顺序不同:@Autowired 先 byType 再 byName;@Resource 先 byName 再 byType。

8、使用注解开发

  • 在 spring4 之后,想要使用注解形式,必须得要引入 aop 的包。
  • 使用注解需要导入 context 约束,增加注解的支持。
  • 必须指定要扫描的包,使这个包下的注解生效。扫描是为了类上的注解。

Spring 的配置文件 applicationContext.xml :

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!--指定要扫描的包,这个包下的注解就会生效-->
    <context:component-scan base-package="com.song"/>
    <context:annotation-config/>

</beans>

8.1 Bean 的实现(@Component)

在指定包下编写类,增加注解

// 等价于配置文件中 <bean id="user" class="com.song.pojo.User"/>
@Component  // 组件
public class User {
    public String name = "张三";
}

测试:

public void test(){
   ApplicationContext Context =
       new ClassPathXmlApplicationContext("applicationContext.xml");
   User user = (User) Context.getBean("user");
   System.out.println(user.name); // 张三
}

注意:@Component("xxx") 可以指定 id

  • 如果是 @Component,即没有指定 id,那么这个 bean 的 id 就是当前类的首字母小写的字符串
  • 如果是 @Component("xxx"),即指定了 id,那么 getBean 时只能使用这个 id,即Context.getBean("xxx")

8.2 属性注入(@Value)

可以在属性名上添加注解,也可以在setter方法上添加。

@Component
public class User {
    // 相当于 <property name="name" value="张三"/>
    //@Value("张三")
    public String name;

    @Value("张三")
    public void setName(String name) {
        this.name = name;
    }
}

8.3 衍生的注解

@Component 的三个衍生注解,在 web 开发中,会按照 mvc 三层架构分层。

  • dao 层【@Repository】
  • service 层【@Service】
  • web 层【@Controller】

这四个注解功能都是一样的,都是代表将某个类注册到 Spring 中,装配 bean。

8.4 自动装配

前面已经学过。

8.5 作用域(@Scope)

  • singleton:默认的,Spring会采用单例模式创建这个对象。关闭工厂 ,所有的对象都会销毁。
  • prototype:多例模式。关闭工厂 ,所有的对象不会销毁。内部的垃圾回收机制会回收
@Component
@Scope("prototype")
public class User {
    //@Value("张三")
    public String name;

    @Value("张三")
    public void setName(String name) {
        this.name = name;
    }
}

8.6 小结

xml 和注解比较

  • xml 更加万能,适用于任何场合,结构清晰,维护简单方便
  • 注解,不是自己提供的类使用不了,维护相对复杂,开发简单方便

xml 与注解整合开发

  • xml 用来管理 bean
  • 注解只负责完成属性的注入
  • 使用时需要注意:必须让注解生效,需要开启注解的支持 <context:annotation-config/>

9、使用 java 的方式配置 Spring

完全不使用 Spring 的 xml 配置,全权交给 java来做。

javaConfig 是 Spring 的一个子项目,它通过 Java 类的方式提供 Bean 的定义信息,在 Spring4 之后它成为了一个核心功能。这种纯 java 的配置方式,在 SpringBoot 中随处可见。

测试:

  1. 实体类
// 注解意思:这个类被 Spring 接管了,注册到了容器中
@Component // 其实不加这个注解也可以测试成功
public class User {
    private String name;

    @Value("zhangsan") //属性注入值
    public void setName(String name) {
        this.name = name;
    }
    
    getter/toString……
}
  1. 新建一个 config 配置包,编写一个 javaConfig 配置类
//这个也会被 Spring 容器托管,注册到容器中,因为它本来就是一个 @Component
// @Configuration 代表这是一个配置类,和之前的 beans.xml 一样
@Configuration
@ComponentScan("com.song.pojo")
@Import(MyConfig2.class) //导入合并其他配置类,类似于配置文件中的 import 标签
public class MyConfig {

    // 通过方法注册一个 bean,相当于之前写的一个 bean 标签
    // 方法的名字,相当于 bean 标签中的 id 属性
    // 方法的返回值,相当于 bean 标签中的 class 属性
    @Bean
    public User getUser(){
        return new User(); // 返回要注入 bean 的对象
    }
}
@Configuration  //代表这是一个配置类
public class MyConfig2 {
}
  1. 测试类
public class MyTest {
    public static void main(String[] args) {
        // 如果完全使用了配置类方式去做,就只能通过 AnnotationConfig 上下文来获取容器,通过配置类的class对象加载
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
        User getUser = (User) context.getBean("getUser"); // 参数为方法名       
        System.out.println(getUser.getName()); // zhangsan
        
        User user = (User) context.getBean("user"); // 参数为 类名的首字母小写
        System.out.println(user.getName()); // zhangsan
        
        System.out.println(getUser == user);//fasle
    }
}

思考:

发现以下两种方式的注解操作都可以正确输出

  1. 只在 MyConfig 类中加 @Bean 注解,context.getBean("getUser") 参数需要是 @Bean 注解时的方法名;
  2. 不加 @Bean 注解,在 User 类中加 @Component 注解,在 MyConfig 类中加 @ComponentScan("xxx.xxx.xxx") 注解,context.getBean("user") 参数需要是类名的首字母小写字符串。
  3. 如上面的测试案例,将 @Bean 和 @Component 注解都加上,通过 getBean 得到的对象是两个不同的对象,也就是说,在 Spring 容器中存在着该类的两个对象。