Spring初学笔记(二):Bean的注入

关于Bean的注入

  在上一篇中,已经说到虽然注入确实可以降低类与类之间的耦合,但并没有解决调用者必须知道类的创建方法的问题,也可以说是没有实现调用者与类实现的解耦,我们也提到,为了实现两者的解耦,可以采取工厂模式,而事实上Spring也是这么做的,因此接下来我们就了解一下在Spring中Bean是如何被注入到对应实体类中的。

  Spring提供了两种类型来实现Bean容器,一个是bean工厂,另一个是应用上下文,其中bean工厂对大多数应用而言太低级,所以直接讨论如何使用应用上下文来获取Bean对象。

  Spring自带了多种类型的应用上下文,其中有:

    ① AnnotationConfigApplicationContext:从一个或多个基于java的配置类中加载上下文定义,适用于java注解的方式;

    ② ClassPathXmlApplicationContext:从类路径下的一个或多个xml配置文件中加载上下文定义,适用于xml配置的方式;

    ③ FileSystemXmlApplicationContext:从文件系统下的一个或多个xml配置文件中加载上下文定义,也就是说系统盘符中加载xml配置文件;

    ④ AnnotationConfigWebApplicationContext:专门为web应用准备的,适用于注解方式;

    ⑤ XmlWebApplicationContext:从web应用下的一个或多个xml配置文件加载上下文定义,适用于xml配置方式。

  这里我还没有看到Web应用,所以就只看一下前两个(第三个与第二个一样,只不过是变成从任意位置而不单单是从类路径下查找了),他们分别代表了两种不同的Bean装配方式:Java中装配和xml中装配,当然装配还有第三种方式:自动化装配,这也是最为推荐的一种方式。

  接下来,就先从Java中装配开始看起吧。

  我是使用Maven来构建的Spring项目,下面是需要引入的依赖

    <!-- 定义maven变量 -->
    <properties>
        <!-- spring -->
        <spring.version>5.1.4.RELEASE</spring.version>
    </properties>

    <dependencies>
        <!-- Spring IOC 核心容器 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/junit/junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>


        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-expression</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- Spring AOP 切面 模块 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.2</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
    </dependencies>

一、Java中装配

  对于可传入构造器的对象类型,共有:int、float之类的基本类型,自定义类型、列表集合等类型,我们先从无参构造看起:

    ①无参构造

   先定义一个接口

public interface Animal {
    public void eat();
}

    其次,我们定义其实现类:

public class Panda implements Animal {
    @Override
    public void eat() {
        System.out.println("熊猫吃竹子");
    }
}

    在使用java配置的时候,需要写一个配置类:

    配置类其实跟普通的类并没有什么区别,只不过需要加一些注释。先看一下配置类的书写,再对里面的注解一一理解:

    在同包下定义一个Java类,取名为ContextTestConfig:

@Configuration
public class ContextTestConfig {
    @Bean
    public Animal Panda(){
        return new Panda();
    }
}

    这样就算完成了一个配置类,其中有两个注解:@Configuration和@Bean

    @Configuration:表明这是一个配置类,在上面写的几种应用上下文类型中,有一种类型为:AnnotationConfigApplicationContext:从一个或多个基于java的配置类中加载上下文定义,适用于java注解的方式;当选用的应用上下文用这个类来生成的时候,就要传入对应的配置类来作为参数,从而使获得的应用上下文可以得到此配置类所配置的Bean对象。加上注解后变为配置类,可以启动组件扫描,用来将带有@Bean的实体进行实例化bean等。

    @Bean:表示要创建一个Bean对象,并将该对象交给Spring管理。看一下@Bean的源码,里面有这样的属性:

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
    @AliasFor("name")
    String[] value() default {};

    @AliasFor("value")
    String[] name() default {};

    /** @deprecated */
    @Deprecated
    Autowire autowire() default Autowire.NO;

    boolean autowireCandidate() default true;

    String initMethod() default "";

    String destroyMethod() default "(inferred)";
}

    其中,现在注意的是里面的name和value属性,他们是一个效果,都是表示给该Bean对象起一个别名,如果不写,那么默认Bean的id就是函数名。

    现在,配置已经完成了,来使用测试方法来看看装配有没有成功,新建一个ContextJavaTest类,代码如下:

public class ContextJavaTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(com.xiaoxin.ContextTest.ContextTestConfig.class);
        Animal panda = applicationContext.getBean("Panda",Panda.class);
        panda.eat();
    }
}

    输出正常,为“熊猫吃竹子”。

    我们再来试一试对应的name属性:现在我在配置类中的代码给@Bean给上name属性,再次测试:

//Panda类修改后的代码
@Bean("Panda02") public Animal Panda(){ return new Panda(); }

    发现报错了,错误为:

    也就是没有找到一个叫"Panda"的Bean对象,因为此时在配置类中,Panda对象已经改名为Panda02了,当把ContextJavaTest中名字改为Panda02后,又可以找到对应的Bean对象,正常执行。

    这是无参的情况,若是有参数的情况下,在Java中配置其实也是很方便的:

   ②含参构造和Set方法

    现在假设有Person这样一个接口和Man的实现类,对应代码为:

public interface Person {
    public void give();
    public void playWithAnimal();
}
public class Man implements Person {
    Animal animal;
    public Man(Animal animal) {
        this.animal = animal;
    }
    @Override
    public void give() {
        System.out.println("给动物食物");
    }
    @Override
    public void playWithAnimal() {
        give();
        animal.eat();
    }
}

    现在,Man对象在创建的时候,必须要传入一个Animal的对象,有了实现类和创建逻辑以后,我们看配置类如何配置:

@Bean("Man")
    public Person Man(){
        return new Man(new Panda());
    }

    只需要这样配置,就可以在测试代码中直接获得对应的类了。

    如果不是用构造器传入,而是用set方法传入,是一样的:

Animal animal;
    public Man(){}

    public void setAnimal(Animal animal) {
        this.animal = animal;
    }

    那么只需在配置类里,用创建对象的方法写上即可,如果是列表之类的,也是同样的办法,这也是Java配置要比xml配置好的地方,配置类其实跟创建对象是一样的,只是加注释来表明是一个配置类而已。

二、xml装配

  这种方法就是直接在xml中配置对应的bean对象。

    在resources下创建xml文件夹,内放ContextTest.xml

   文件结构如图:

 

     在创建好文件结构后,就可以开始写xml文件了

    在一开始,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-3.2.xsd">
<beans>

    如果要使用无参构造的话,直接加入一个Bean标签即可:

<bean id="Panda" class="com.xiaoxin.ContextTest.Panda"/>

    如果使用含参构造函数的话,可以使用<constructor-arg>标签来注入对应属性

<bean id="Man" class="com.xiaoxin.ContextTest.Man">
        <constructor-arg name="animal" ref="Panda"></constructor-arg>
</bean>

    如果注入的是对象的话,那么需要使用ref来引入,当然,引入的对象也必须是在xml中定义的bean。

    如果注入只是字面值的话,可以使用value,比如给Man注入一个Name属性: 

<bean id="Man" class="com.xiaoxin.ContextTest.Man">
        <constructor-arg name="animal" ref="Panda"></constructor-arg>
        <constructor-arg name= "name" value="小新"></constructor-arg>
</bean>

     这些都是直接使用构造器构造,也可以使用set方法来注入属性:

<bean id="Man2" class="com.xiaoxin.ContextTest.Man">
        <property name="animal" ref="Panda"></property>
        <property name="name" value="小新"></property>
</bean>

    其实跟上面没啥区别,只不过把<constructor-arg>标签换成了<property>标签。

    如果还需要使用列表等注入,里面有专门的<list>、<set>等标签来使用:

    现在创建一个Woman类:

public class Woman implements Person {
    List<Animal> animals;
    List<String> strs;
    public void setStrs(List<String> strs) {
        this.strs = strs;
    }
    public void setAnimals(List<Animal> animals) {
        this.animals = animals;
    }
    @Override
    public void give() {
    }
    @Override
    public void playWithAnimal() {
    }
}

    在这个类中,有String类型的列表,也有自定义对象类型的列表,那么注入方法为:

<bean id="Woman" class="com.xiaoxin.ContextTest.Woman">
        <property name="animals">
            <list>
                <ref bean="Panda"/>
            </list>
        </property>
        <property name="strs">
            <list>
                <value>一盒</value>
            </list>
        </property>
</bean>

    如果是构造器实现注入,也只需要把标签改一下即可。

三、注解装配

  注解装配是最为推荐的类型,配置起来也很方便。

    使用注解装配需要几个方面:

    ①在创建的实体类上加@Component注解,在@Component上,可以用name属性来设定id,如果没有的话,默认为类名首字母小写为这个bean对象的id。

    ②在需要注入的属性上加@Autowired,这个会首先进行类型匹配,如果只有一个类型的话,那可以直接匹配,但如果有多个类型的类,那么spring会不知道注入哪一个,因此如果所依赖的接口有多个实现类的话,必须有另外的方式来匹配。

   ③需要配置扫描包,有两种方式:在applicationContext.xml中进行配置,或者直接在对应类包上使用@ComponentScan注解。

 

    现在有以下的几个类:

    定义一个Animal_2接口:

public interface Animal_2 {
    public void doSomething();
}

 

   对应的有一个Ear类:

@Component
public class Ear {
    public void move(){
        System.out.println("耳朵动");
    }
}

    还有一个Animal_2的实现类:

@Component
public class Cat implements Animal_2{
    private Ear ear;
    private String name;
    @Autowired
    public void setEar(Ear ear) {
        this.ear = ear;
    }
    @Value("小黄")
    public void setName(String name) {
        this.name = name;
    }
    public void doSomething(){
        ear.move();
        System.out.println(name+"猫猫");
    }
}

   这些类都有@Component注解,表示这是一个组件,当扫描的时候,可以作为Bean对象被扫描到。

   对应的还有@Service、@Mapper、@Controller等,他们其实跟@Component的功能是一样的,只是分成那么多以后,可以清楚地分清楚层次结构。

     

     在这几个类中,有两个注解:一个是@Autowired,另一个是@Value。

     @Autowired注解对应的是自动注入Bean对象,推荐使用在set方法上,而不是对象名上。

     而@Value也是用于注入的,它所注入的是基本类型的值。

 

    在配置扫描包的时候,有两种方法:

    第一个是基于xml文件来进行配置,在applicationContext.xml中配置:

<context:component-scan base-package="com.xiaoxin.ContextTest"></context:component-scan>

    其中base-package表示要扫描的包

    第二种方法是使用注解来配置,在对应的包下面建立类:Config

@ComponentScan
public class Config {
}

   使用这个代码,会自动扫描该类所在包下的所有组件。

  下面我们进行测试:

  测试使用junit进行测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
//        locations = {"classpath:applicationContext.xml"}
    classes = com.xiaoxin.ContextTest.Config.class
)
public class ContextAnnotationTest {
    @Autowired
    private Animal_2 animal;
    @Test
    public void name() {
        animal.doSomething();
    }
}

  其中@Runwith和@ContextConfiguration经常在一起使用,@ContextConfiguration里面有两个类型,分别表示使用xml和使用注解设置包扫描的两种配置方法。

  但如果有多个Animal_2的实现类的时候,如何自动装配呢?

  这里假设我们还有一个Animal_2的实现类:

@Component
public class Dog implements Animal_2 {
    @Override
    public void doSomething() {
        System.out.println("我是狗狗");
    }
}

  那么这个时候,如果再次运行测试代码,发现无法正常运行:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.xiaoxin.ContextTest.ContextAnnotationTest':
Unsatisfied dependency expressed through field 'animal'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.xiaoxin.ContextTest.Animal_2' available: expected single matching bean but found 2: cat,dog

 

  大体意思是说,没有找到合适的bean对象,她所需要的Animal_2有两个已经注册的Bean对象,他不知道该选用哪一个。

  此时的解决方法就是:

  1、在@Autowired注解下,再添加@Qualifier(value = "xx")的注解,来显式表明使用的是哪个bean对象。

  上面也提到了,每个Bean对象,如果没有特别写明名称的话,它的id就是类名且第一个字母小写。

  因此在这里,Cat类的Bean对象,id就是cat,Dog类的Bean对象,id就是dog。

  也可以修改对应Bean对象的id,在@Component(Value = "xx"),可以自己定义对应Bean对象的ID。

  2、上面的方法如果只是使用@Qualifier而不对Bean修改ID的话,会存在一个问题:如果对应的类名发生改变,那么对应的@Qualifier里的内容也需要调整,否则会找不到Bean对象,解决方法是在@Component上赋值,或者也可以为Bean对象添加@Qualifier注解,表明其限定符。  

  3、在自己最需要的类上加@Primary注解,表示这个默认的首选Bean

  

  这样使用注解装配,确实很方便,但可以发现一个问题,就是我们写的类只有一个,如果使用xml或者Java装配的话,我们可以创建很多同一类的对象,只要附上不同的ID即可,但在注解装配中,我们只能配置一个,这就会使得有的时候不能满足我们的要求,因此也需要部分使用xml或Java配置。

  还有一种情况,就是假设我们有多个xml文件和多个java配置类,我们如何将他们合在一起,共同去配置装载Bean对象呢?

  对于这个问题,要认清楚就是,不管是三种装配方式的哪一种,他们实质上都是注册Bean对象的方法,也就是说三种方法,不管怎么配置,只要被扫描到了,就都可以有效。

  再回顾使用注解自动装配,我们可以发现,所谓的自动装配,并不是全自动的,使用注解完成了Bean对象的配置以后,我们还有一个任务就是配置扫描包:而扫描包的配置并不是自动的,也是需要使用xml或者java配置类的注解来完成的,而在使用的时候,也是需要配置对应的xml文件或者配置类,才能扫描到对应的Bean对象

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
//        locations = {"classpath:applicationContext.xml"}
    classes = com.xiaoxin.ContextTest.Config.class
)

  这样,应该可以明白了:其实怎么配置Bean对象并不重要,哪种方法简单哪种方法就可以使用,他们都是相同的作用,即把Bean对象注册到容器以供使用,而对于上面所说的三种配置方法同时使用的问题,其本质其实变成了怎么让多个配置xml文件或者Java类,能够都被扫描到。

  一、在JavaConfig配置类中引入xml配置

     现在新建一个包,不妨取名为ContextTest02,里面有这两个类:

public class Fish {
   private String name;
    public Fish(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

    有鱼类,还有猫类,其中猫类中有一个鱼类对象:

public class Cat {
    Fish fish ;
    public Cat(Fish fish) {
        this.fish = fish;
    }
    public void eat(){
        System.out.println("猫猫吃"+fish.getName());
    }
}

    定义好这两个类后,就可以开始测试了:

    我们先看看多个配置类怎么合并在一起扫描:

    在Config01中,我们定义一个鱼的Bean对象,并赋ID为"fish01":

@Configuration
public class Config01 {
    @Bean(name = "fish01")
    public Fish GetFish(){
        return new Fish("黄花鱼");
    }
}

 

    在Config02中,我们定义一个猫的Bean对象,并且传入fish01:

@Configuration
public class Config02 {
    @Bean("cat01")
    public Cat GetCat(Fish fish01){
        return new Cat(fish01);
    }
}

 

    现在这样的状况下,还没有进行包扫描,在Config02中并没有查找到对应的fish01,因此下一步我们要建立两者的关联:

    现在不妨先测试一下,到底是否可以直接执行:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
        classes = Config02.class
)
public class TestAll {
    @Autowired
    @Qualifier("cat01")
    private Cat cat;

    @Test
    public void test(){
        cat.eat();
    }
}

    在测试中,因为cat01对象是在Config02包下的 所以直接扫描02包试一下:

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: 
Error creating bean with name 'cat01' defined in com.xiaoxin.ContextTest02.Config02:
Unsatisfied dependency expressed through method 'GetCat' parameter 0;
nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'com.xiaoxin.ContextTest02.Fish' available:
expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

 

    使用失败,原因是没有找到Cat01Bean对象所依赖的Fish对象,这是因为Config01没有被扫描到,它所设置的Fish对象没有被创建。

    解决方法:在Config02包中,引入Config01配置类:

@Import(Config01.class)

    再次运行,发现打印出结果了。

猫猫吃黄花鱼

    这样确实解决了问题,但仔细想想会发现这样很繁琐,每一个依赖的需要单独导入,如果Fish对象被好几个配置类依赖,那就要写好几次,因此我们可以改进代码,创建一个更高级的配置类,它只负责把所有的配置文件、配置类合并在一起,统一扫描:

@Configuration
@Import({Config02.class,Config01.class})
public class ConfigAll {
}

 

    这样配置以后,所有的配置类都可以往里面添加,扫描的时候,只需要扫描这个总的配置类,就可以了,在测试代码中,配置的配置类改为:

@ContextConfiguration(
        classes = ConfigAll.class
)

    这样实现了Java配置类之间的合并,而对于xml文件,也是一样,可以使用注解:@ImportResource

@Configuration
@Import({Config02.class,Config01.class})
@ImportResource("classpath:xml/ConfigXml01.xml")
public class ConfigAll {
}

    在上图的文件结构上,在xml文件夹下配置对应的xml配置文件,导入即可。

 

    二、使用xml文件导入:

    也是一样的,推荐建立一个总的文件,只是用来引入合并:

    使用<bean>来引入Java配置类

    使用<import>引入xml配置文件

<bean class="com.xiaoxin.ContextTest02.Config01"></bean>
    <bean class="com.xiaoxin.ContextTest02.Config02"></bean>
    <import resource="ConfigXml01.xml"></import>

    在测试代码上,导入的配置改为xml文件:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(
//        classes = ConfigAll.class
        locations = {"classpath:xml/ConfigXmlAll.xml"}
)

    即可

 

posted @ 2020-02-17 20:55  小新而已  阅读(929)  评论(0编辑  收藏  举报