(二)Spring自动装配
0 Views with
本文字数:4,428 字 | 阅读时长 ≈ 19 min

(二)Spring自动装配

0 Views with
本文字数:4,428 字 | 阅读时长 ≈ 19 min

Spring自动装配

为了减少XML的配置数量。Spring提供了几种技巧来解决这一问题:

自动装配(autowiring): 有助于减少<property>元素和<constroctor-arg>元素,让Spring自动识别如何装配Bean的依赖关系

自动检测(autodiscovery): 让Spring自动识别那些类需要被配置成Spring Bean,从而减少对<bean>元素的使用

自动装配Bean属性

4种类型的自动装配

byName自动装配

通过byName元素Spring可以实现自动查找其他Bean中是否有相同的属性名称,有则进行装配。体会下面的案例:

EmpService.java

package demo2;
public class EmpService {
    //注入Dao
    private EmpDao empDao;
    public void setEmpDao(EmpDao empDao) {
        System.out.println("EmpService --> EmpDao");
        this.empDao = empDao;
    }
}

EmpDao.java

package demo2;
public class EmpDao {
}

spring.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="empService" class="demo2.EmpService" autowire="byName"/>
    <bean id="empDao" class="demo2.EmpDao"/>
</beans>

Test测试类

@Test
public void run(){
    //加载Spring上下文
    ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
    ac.getBean("empService");
}

打印结果:

情况一:如果通过之前的方式通过ref来注入一个引用Bean呢?我们改变上述spring.xml中的代码:

<bean id="empDao" class="demo2.EmpDao"/>
<bean id="empService" class="demo2.empService">
    <property name="empDao" ref="empDao"/>
</bean>

结果和上述相同。

情况二:如果我们改变EmpService.javaempDao的数据类型

package demo2;
public class EmpService {
    //注入Dao
    private String empDao;
    public void setEmpDao(String empDao) {
        System.out.println("EmpService --> EmpDao");
        this.empDao = empDao;
    }
}

打印结果:

观察发现autowire="byName"自动装配的含义就是:

​ 若BeanA中存在与BeanB的idid="empDao"相同的属性(且存在setter方法)private EmpDao empDao;Spring就能自动进行装配。

​ 那么,开始我一直疑惑这个byName注入不是查找的bean名称和另一个bean的属性名称相同就能注入吗,其实并不是。首先我们要明白注入是注入另一个类或接口什么的,Spring让这两个bean之间产生了联系,即我们定义一个普通类型的参数private String empDao,即使名称和另一个Bean相同empDao,但是它只是一个属性,怎么能让一个普通的参数注入到另一个Bean中呢?(注意这里我是在自动注入的前提下说的,当然是可以实现注入的)。

缺点: byName的缺点就是首先要存在这个和另一个Bean的id相同的属性,其次,如果多个Bean中都存在这个属性,那Spring都会将这些Bean装配进去。如下案例:

新创建一个UserDao.java

package demo2;
public class EmpDao {
}

EmpService.java

package demo2;
public class EmpService {
    //注入Dao
    private EmpDao empDao;
    public void setEmpDao(EmpDao empDao) {
        System.out.println("EmpService --> EmpDao");
        this.empDao = empDao;
    }
    private UserDao userDao;
    public void setUserDao(UserDao userDao) {
        System.out.println("EmpService --> UserDao");
        this.userDao = userDao;
    }
}

spring.xml中注入该Bean:

<bean id="userDao" class="demo2.UserDao"/>

观察结果:

如上就体现了这中注入方法的缺陷,就是如果多个Bean的id名称都和另一个bean的属性有相同的,那么Spring会全部注入。

byType自动装配

byType自动装配的方法和byName装配方式类似,区别在于byType找的是相同的数据类型,而byName找的是相同的名称

EmpService.java

package demo2;
public class EmpService {
    //注入Dao
    private EmpDao emp;
    public void setEmpDao(EmpDao emp) {
        System.out.println("EmpService --> EmpDao");
        this.emp = emp;
    }
}

spring.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="empService" class="demo2.EmpService" autowire="byType"/>
    <bean id="empDao" class="demo2.EmpDao"/>
</beans>

Test测试类

@Test
public void run(){
    //加载Spring上下文
    ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
    ac.getBean("empService");
}

打印结果:

观察:上面EmpDaobean的id是empDao,但EmpService中存在的属性是private EmpDao emp;,注意不是empDao,那么当我们配置了autowire="byType"后,因为数据类型相同,Spring仍可将EmpDao注入到EmpService中。

缺点

​ 如果Spring找到了多个Bean的,他们的类型与需要自动装配的属性的类型都匹配,那么Spring会抛出异常,而不是选择注入哪个。如下情况:

其他不变,我们在spring.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="empService" class="demo2.EmpService" autowire="byType"/>
    <bean id="empDao" class="demo2.EmpDao"/>
    <bean id="dao" class="demo2.EmpDao"/>
</beans>

打印结果:

可以看到,当我们在spring.xml中注入两个类型相同的Bean(当然名称不能相同),此时就会报错。

为解决这一错误,Spring提供了pimaryautowire-candidate两个属性。

spring.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="empService" class="demo2.EmpService" autowire="byType"/>
    <bean id="empDao" class="demo2.EmpDao"/>
    <bean id="dao" class="demo2.EmpDao" autowire-candidate="false"/>
</beans>

constructor自动装配

如果要通过构造器来装配Bean,那我们可以移除<constructor-arg>元素,采用Spring自动注入的方式:(注意这里我们改变了以上的代码逻辑,增加了一个Emp接口,让EmpDaoUserDao都继承这个接口)

Emp.java

package demo2;
public interface Emp {
}

EmpDao.java

package demo2;
public class EmpDao implements Emp {
}

UserDao.java

package demo2;
public class UserDao implements Emp {
}

EmpService.java

package demo2;
public class EmpService {
    private Emp emp;
    public void setEmp(Emp emp) {
        System.out.println("执行...");
        this.emp = emp;
    }
    public EmpService(Emp emp) {
        System.out.println("这是构造参数...");
        this.emp = emp;
    }
}

同时这种方式也存在和byType一样的缺点:

spring.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:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="empService" class="demo2.EmpService" autowire="constructor"/>
    <bean id="empDao" class="demo2.EmpDao"/>
    <bean id="userDao" class="demo2.UserDao"/>
</beans>

结果仍报错:

最佳自动装配—>由Spring决定怎么装配

<bean id="empService" class="demo2.EmpService" autowire="autodetect"/>

默认自动装配

<beans ....
    default-autowire="xxx"       
>

使用注解自动装配

从Spring2.5开始可以使用注解实现自动装配,注解装配和使用XML中的autowire本身没有太大区别,但是注解装配可以实现更小颗粒度的装配。但是Spring默认是禁用注解装配的,所以我们首先要在spring.xml中启用注解装配。最简单的方式就是启用Spring的context命名空间配置中的<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
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    <context:annotation-config/>
</beans>

Spring3支持几种不同的自动装配注解:

使用@Autowired

如下案例:

spring.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
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context-3.0.xsd">

    <context:annotation-config/>
    <bean id="empService" class="demo2.EmpService"/>
    <bean id="empDao" class="demo2.EmpDao"/>
</beans>

EmpService.java

package demo2;
import org.springframework.beans.factory.annotation.Autowired;
public class EmpService {
    private Emp emp;
    @Autowired
    public void setEmp(Emp emp) {
        System.out.println("执行...");
        this.emp = emp;
    }
    public void play(){
        emp.play();
    }
}

Emp.java

package demo2;
public interface Emp {
    void play();
}

EmpDao.java

package demo2;
public class EmpDao implements Emp {
    public void play() {
        System.out.println("EmpDao: play...");
    }
}

Test测试类

@Test
public void run(){
    //加载Spring上下文
    ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
    EmpService es = (EmpService) ac.getBean("empService");
    es.play();
}

除了上面标记在setter方法上,还可以直接标记到私有属性上:

package demo2;
import org.springframework.beans.factory.annotation.Autowired;
public class EmpService {
    @Autowired
    private Emp emp;
    public void setEmp(Emp emp) {
        System.out.println("执行...");
        this.emp = emp;
    }
    public void play(){
        emp.play();
    }
}

可以看到,使用这种方式仍可以完成注入,并且我们发现将@Autowired直接标记到私有属性上,并不会执行setter方法,但是仍完成了注入,这其实就是@Autowired的一大优势,它简化了setter方法的书写,采用Java的反射机制完成的注入。

缺点:采用@Autowired注解注入固然方便,但是也存在缺点,即被标注的属性或参数必须是可注入的,如下列情况,我们将emp的数据类型改变(注意这里要注释掉play方法,因为数据类型改变了也就不能再调用play方法):

package demo2;
import org.springframework.beans.factory.annotation.Autowired;
public class EmpService {
    @Autowired
//    private Emp emp;
    private String emp;
    /*public void play(){
        emp.play();
    }*/
}

此时就会报错NoSuchBeanDefinitionException,因为我们认为属性不一定要被装配,返回null值也好,从而避免抛出异常,那么我们就可以在@Autowired中添加required属性,表示是否一定要进行装配:

package demo2;
import org.springframework.beans.factory.annotation.Autowired;
public class EmpService {
    @Autowired(required = false)
//    private Emp emp;
    private String emp;
    /*public void play(){
        emp.play();
    }*/
}

这样就不会再出现保报错的情况。

注意: required属性可以用于@Autowired注解所使用的任意地方,但当使用构造器进行装配时,只有一个构造器可以将@Autowiredrequired属性设置为true,其他都必须设置为required=false。此外,使用@Autowired标记多个构造器时,Spring会从所有瞒住装配条件的构造器中选择入参最多的构造器。

限定歧义性的依赖

对于出现多个Bean满足注入条件的解决,上面也介绍了一系列的方法,即使是上面的@Autowired细颗粒度的注入方式仍然无法解决多个Bean满足注入条件的情况,所以Spring提供了一个更好的方式解决这一问题:

使用@Qualifier("beanName")限定注入哪个Bean

spring.xml

<bean id="empService" class="demo2.EmpService"/>
    <bean id="empDao" class="demo2.EmpDao"/>
    <bean id="userDao" class="demo2.UserDao"/>

此时,因为EmpDaoUserDao都实现了Emp接口,所以empDaouserDao两个Bean都瞒住注入条件,此时就会产生NoSuchBeanDefinitionException的错误。

EmpService.java

package demo2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class EmpService {
    @Autowired(required = false)
    @Qualifier("empDao")
    private Emp emp;
    public void play(){
        emp.play();
    }
}

即这里我们限定了将empDao注入,而不是注入userDao,这样就会避免出现错误。

@Inject基于标准的自动装配

为了统一各种依赖注入框架编程模型,JCP(Java Community Process)发布了一套Java的依赖注入规范,@Inject注解则是其核心部件,该注解和@Autowired几乎相同,但对于一些特殊情况,@Inject也有自己的处理办法:

  1. 首先@Inject没有required属性,所以被标记的属性必须是可注入的

    1. 歧义性的Bean定义:@Qualifier —> @Named

在注解注入中使用表达式

@Value

自动检测Bean

当在Spring配置中增加了<context:annotation-config>时,我们希望Spring特殊对待我们所定义的Bean里的某一组注解,并使用这些注解指导Bean的装配,由此产生了 <context:component-scan元素。它能大大简化对<bean>的配置,通过在<context:component-scan>元素中指定要扫描指定包下的Bean对象,如:

<?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
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    <context:component-scan base-package="demo2"/>
</beans>

那么<context:component-scan>又是如何知道哪些类需要注册为Spring Bean呢?

为自动检测标注Bean

<context:component-scan>会查找使用构造型(stereotype)注解所标注的类

@Component —>通用的构造性注解,标识该类为Spring组件。

@Controller —>标识将该类定义为Spring MVC controller

@Repository —>标识将该类定义为数据仓库

@Service —>标识将该类定义为服务

使用@Component标注的任意自定义注解

举例:

spring.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
          http://www.springframework.org/schema/beans/spring-beans.xsd
          http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context-3.0.xsd">
    <context:component-scan base-package="demo2"/>
</beans>

如上我们只需要开启注解支持.

EmpService.java

package demo2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class EmpService {
    @Autowired(required = false)
    private Emp emp;
    @Bean
    public EmpDao empDao(){
        return new EmpDao(userDao());
    }
    public void play(){
        emp.play();
    }
}

如上,我们首先使用@Configuration标记的Java类,就等价于XML配置中的<beans>元素。使用@Bean就等价于<bean>元素,(@Bean告知Spring这个方法将返回一个对象,该对象应该被注册为Spring应用上下文中的一个Bean。方法名将作为该Bean的ID。在该方法中所实现的所有逻辑本质上都是为了创建Bean)这样我们就实现了将一个JavaBean对象交付给Spring管理,再添加@Autowired就完成了自动注入的功能。这是注入一个简单对象,那么我们怎么注入一个引用呢?

package demo2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class EmpService {
    @Qualifier("empDao")
    @Autowired(required = false)
    private Emp emp;
    @Bean
    public UserDao userDao(){
        return new UserDao();
    }
    @Bean
    public EmpDao empDao(){
        return new EmpDao(userDao());
    }
}

向另一个Bean中注入一个引用,只需要在此Bean的返回值中调用另外一个被@Bean标记的方法名即可完成注入,但是仍要考虑歧义性问题(使用@Qualifier进行限定)

总结

综上:我们已经介绍了Spring自动装配和注解开发的相关知识,下面我们来回顾一下。

自动装配

注解开发

自动装配提供的注解:

​ @Autowired — required=false — @Qualifier(“beanName”)

​ @Inject — @Named

Spring基于Java的注解开发:

​ @Configuration( <beans>

​ @Bean( <bean>

注意问题:

  1. 以上的自动装配的方式都无法避免多个Bean同时满足注入条件的情况,但是Spring针对不同的注入方式提供了不同的解决办法:

    • byName会对满足条件的Bean都进行封装,不加考虑。
    • byType对遇到多个满足注入条件的Bean时会抛出异常,同时给出primary(仅对首选Bean有意义)、autowire-candidate="false"(忽略某个Bean的自动装配)。(注:我们要明白Spring默认将所有Bean都设置为primary="true"这样就没有什么首选Bean可言。所以此时我们最好采用autowire-candidate忽略某个Bean的自动装配。
    • constructor自动装配和byType有一样的局限性,会报错。
    • 综合以上,提出了最佳的自动装配方式:autowire="autodetect"。Spring首先会尝试使用constructor自动装配,如果没有与构造器相匹配的Bean,Spring将尝试使用byType自动装配。

      1. 注解开发有常用的三个自动装配方式:1.@Autowired(Spring自带的注解); 2.Inject(基于标准的自动装配);3.@Resource
    • @Autowired(默认按照类型装配)

      1. 避免@Autowired标注的属性或参数是不可装配的,提供required=false属性。
      2. 限定歧义性的依赖(多个Bean的情况),提供@Qualifier("beanName")缩小选择范围,指定某一个Bean
    • @Inject注解自动装配和@Autowired注解相似,但是对上述解决方案给出的注解不同,如以下:

      1. @Inject限定所标记的属性必须是可装配的,不然就会报错
      2. @Qualifier —> @Named
    • @Resource(默认按照名称装配)

      是jdk1.6支持的注解


交流

如果大家有兴趣,欢迎大家加入我的Java交流群:671017003 ,一起交流学习Java技术。博主目前一直在自学JAVA中,技术有限,如果可以,会尽力给大家提供一些帮助,或是一些学习方法,当然群里的大佬都会积极给新手答疑的。所以,别犹豫,快来加入我们吧!


联系

If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.

如果你觉得这篇文章帮助到了你,你可以帮作者买一杯果汁表示鼓励