Spring学习笔记:Bean的装配方式

本文是自己学习的一个总结

目录


1、基于XML装配

在Spring官网中搜索bean,找到相应的xml基本架构,复制到我们xml中后,开始装配bean。

Spring官网:https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#spring-core。
​​在这里插入图片描述

1.1、构造器装配Bean和setter装配Bean

依赖注入的方式主要有三种,构造器注入,setter注入,接口注入。XML装配方式也有与这三者相对应的装配方式。不过因为接口注入很少见,所以这里就只记录前两种装配方式。

1.1.1 构造器装配Bean

先建立一个带有构造方法的类。

package org.company.think.in.spring.ioc.overview.domain;
public class User {
    private Long id;
    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

之后在XML文件中这样定义。我们在bean标签下使用<constructor-arg>标签,其中有两个属性

  • index:构造器中的第几个参数,从0开始。
  • value:该参数的值。

装配的例子如下

<!--该xml名为application-context.xml-->
<bean id="user1" class="org.company.think.in.spring.ioc.overview.domain.User">
    <constructor-arg index="0" value="1"/>
    <constructor-arg index="1" value="user"/>
</bean>

那么关于该bean的描述就存储在application-context.xml文件中,之后根据这个XML文件生成对应的容器,通过容器就可以取到我们描述的Bean实例。

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application-context.xml");
User user = (User) applicationContext.getBean("user1");

将这个user打印出来,确实与xml中定义的一致。
在这里插入图片描述

1.1.1.1、构造器参数和类型自动匹配

Java中构造器是可以有多个的。XML注入方式中,系统会根据参数的类型和顺序与类中的构造器进行匹配。尽管XML中参数的定义都是以字符串的形式(比如上述例子中的id是Long类型,但XML中是写成index=“0” value=“1”),但系统会为我们自动进行类型转换。

现在我们给User新增一个构造器,新的构造器和旧的一样,只是参数的位置调换了,代码如下。

package org.company.think.in.spring.ioc.overview.domain;
public class User {
    private Long id;
    private String name;
	//旧的构造器
    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }
	//新的构造器
	public User(String name, Long id) {
        this.id = id;
        this.name = name;
    }
}

然后XML文件中也新增一个bean,对应着新的构造器。

<bean id="user1" class="org.company.think.in.spring.ioc.overview.domain.User">
    <constructor-arg index="0" value="1"/>
    <constructor-arg index="1" value="user1"/>
</bean>
<bean id="user2" class="org.company.think.in.spring.ioc.overview.domain.User">
    <constructor-arg index="0" value="user2"/>
    <constructor-arg index="1" value="2"/>
</bean>

然后在取出这两个bean并打印,我们能发现Java自动帮我们根据参数的类型和顺序找到了对应的构造器。

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application-context.xml");
User user1 = (User) applicationContext.getBean("user1");
User user2 = (User) applicationContext.getBean("user2");
System.out.println(user1);
System.out.println(user2);

输出结果如下
在这里插入图片描述


1.1.2、通过setter注入装配

将上面例子的User类写成下面这样,建立属性的getter和setter方法。

package org.company.think.in.spring.ioc.overview.domain;
public class User {
    private Long id;
    private String name;
	//省略getter和setter
}

对于setter注入,我们使用<property>标签,对需要赋值的属性赋值即可。

<bean id="role" class="com.ssm.chapter9.pojo.Role">
    <property name="id" value="1"/>
    <property name="name" value="用户1"/>
</bean>






1.2、引用型赋值和集合型赋值

上述的例子中我们赋值的属性都是基本类型,比如Long,Integer,String。但是如果属性的类型是引用型或者集合,那就要使用其他方式了。

1.2.1、引用型赋值

如果User的定义如下

package org.company.think.in.spring.ioc.overview.domain;
public class User {
    private Long id;
    private String name;
    private Address address;
	//省略getter和setter
}

其中的Address是一个类。那这个属性的赋值就不能像之前一样"value = ···",我们要先保证容器中已有一个类型是Address的Bean,然后使用"ref = ···",其中ref是指类型是Address的Bean在容器中的id。

假设容器中已有一个类型是Address的Bean,id是address。引用型赋值的示例如下。

<bean id="role" class="com.ssm.chapter9.pojo.Role">
    <property name="id" value="1"/>
    <property name="name" value="总经理"/>
    <property name="Address" ref="address"/>
</bean>

1.2.2、集合类赋值

如果属性的类型是集合,那要分好几种情况,比如Map,List。类型不同赋值方式也不同。

1.2.2.1、List类型

List类型的话,我们使用<list>标签代替value或者ref。在<list>下则正常使用value或ref对元素赋值。

<bean id="id" class="全限定性类名">
    <property name="list">
        <list>
            <value>"1"</value>
            <ref bean="myDataSource"/>
        </list>
    </property>
</bean>

1.2.2.2、Set类型

Set类型的话,我们使用<set>标签代替value或者ref。在<set>下则正常使用value或ref对元素赋值。

<bean id="id" class="全限定性类名">
    <property name="list">
        <set>
            <value>"1"</value>
            <ref bean="myDataSource"/>
        </set>
    </property>
</bean>

1.2.2.3、Map类型

Map类型我们使用<map>标签代替value或者ref。在<map>标签下使用<entry>子标签代表元素。<entry>下的key属性和value属性则是元素的key和value。如果key和value的类型是引用型,则使用key-ref和value-ref。

<bean id="id" class="全限定性类名">
    <property name="map">
        <map>
        	<!--key和value是基本类型-->
            <entry key="0" value="value1"/>
            <!--key和value是引用类型-->
            <entry key-ref="1" value-ref="class1"/>
        </map>
    </property>
</bean>

假设User的定义如下

package org.company.think.in.spring.ioc.overview.domain;
public class User {
    private Long id;
    private String name;
    private Map<Integer, String> map;
	//省略getter和setter
}

开始装配User。

<bean id="user1" class="org.company.think.in.spring.ioc.overview.domain.User">
    <property name="map">
        <map>
            <entry key="0" value="map1"/>
            <entry key="1" value="map2"/>
        </map>
    </property>
</bean>

然后初始化容器并输出User的map。

BeanFactory beanFactory = new ClassPathXmlApplicationContext("META-INF/test.xml");
User user1 = (User) beanFactory.getBean("user1");
System.out.println(user1.getMap());

输出结果如下
在这里插入图片描述

1.3、命名空间赋值

所谓的命名空间其实就是上面提到的构造器注入、setter注入,只是语法上更为简单简洁。

1.3.1、c命名空间

c命名空间就是构造器注入,我个人是将c看成contructor的简称。

要使用c命名空间,要先引入一些东西。我们在spring官方文档中可以直接搜索『 c: 』,直接复制过来。
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#spring-core
在这里插入图片描述

c命名空间是<bean>的一个属性,不是子标签。c命名空间有两种用法,一种是指明构造器参数的名称进行复制,另一种是指明构造器参数的位置顺序赋值。

1.3.1.1、指定参数名称(c:name)

假设User类的定义如下

package org.company.think.in.spring.ioc.overview.domain;
public class User {
    private Long id;
    private String name;
    public User(Long id, String name) {
	    this.id = id;
	    this.name = name;
    }
}

使用c命名空间指定参数名称的方法如下

<!-- 如果name的类型是引用,则要写成c:name-ref="引用名称"-->
<bean id="user2" class="org.company.think.in.spring.ioc.overview.domain.User"
c:name="user1" c:id="1"/>

这样写就简介得多了。我们初始化容器后输出user2看看结果。

BeanFactory beanFactory = new ClassPathXmlApplicationContext("META-INF/test.xml");
User user2 = (User) beanFactory.getBean("user2");
System.out.println(user2);

输出如下在这里插入图片描述

1.3.1.2、指定参数位置顺序

我们也可以指定参数顺序,还是上面的例子。

<!-- 同样的,如果参数的类型是引用,则要写成c:_0-ref="引用名称"-->
<bean id="user2" class="org.company.think.in.spring.ioc.overview.domain.User"
          c:_0="1" c:_1="user"/>

1.3.2、p命名空间

p命名空间就是setter注入。基本的setter注入的写法中使用<property>来为成员属性赋值,我认为p应该就是property的缩写。

setter注入没有所谓的顺序,所以p命名空间只能指定成员属性名,用法与c命名空间相似。

当然,使用之前也要先引入p命名空间,在官方文档中搜索搜索『 p: 』,然后复制相关引用到自己的XML中。
在这里插入图片描述
使用方式如下。

<bean id="user2" class="org.company.think.in.spring.ioc.overview.domain.User"
          p:name="user2" p:id="1"/>




1.3.3、util命名空间

上面的c命名空间和p命名空间都是只能对基本类型和引用进行赋值。集合类型就得使用util命名空间。

同样的,util命名空间使用之前要先引入,我们在官方文档中搜索『 util: 』,复制相关引用。

在这里插入图片描述
其实util命名空间和原来的写法相比并没有简介到哪里去,util命名空间还有其他用途,不过这里不过多介绍了。

下面看看使用方法。假设User的定义如下。

package org.company.think.in.spring.ioc.overview.domain;
import java.util.Map;

public class User {
    private Long id;
    private String name;
    private Map<User, String> map;
    //省略setter和getter
}

我们使用util命名空间对其中的map赋值。

<bean id="user1" class="org.company.think.in.spring.ioc.overview.domain.User">
    <property name="map">
        <util:map id="map">
            <entry key="0" value="map1"/>
            <entry key="1" value="map2"/>
            <entry key="2" value="map3"/>
        </util:map>
    </property>
</bean>

对于list和set,就替换成util:list和util:set即可。

util还有一些子属性可以了解

1.3.3.1、指定成员类型

在util:map中,我们可以使用map-class属性指定map的具体类型。比如下面的例子,我们指定map的类型是Hashmap。

<bean id="user1" class="org.company.think.in.spring.ioc.overview.domain.User">
    <property name="map" map-class="java.util.HashMap">
        <util:map id="map">
            <entry key="0" value="map1"/>
            <entry key="1" value="map2"/>
            <entry key="2" value="map3"/>
        </util:map>
    </property>
</bean>

同时也可以使用key-type和value-type指定key和value的类型。List和Set也有类似的属性。

1.4、通过静态方法

我们可以在类中定义一个静态方法,将该方法的返回对象作为Bean。
假设User的定义如下

package org.company.think.in.spring.ioc.overview.domain;

public class User {
    private Long id;
    private String name;
	
	public static User createUser() {
		User user = new User();
		user.setId(1L);
		user.setName("user1");
		return user;
	}
    //省略setter和getter
}

类中定义了一个静态方法,返回一个User对象。在XML中我们可以获取这个静态方法的返回对象作为Bean。

XML中可以使用<beam>的bean-factory属性使用到这个静态方法,例子如下

<bean id="user" class="org.company.think.in.spring.ioc.overview.domain.User"
factory-method="createUser"/>

1.5、通过实例方法

我们可以指定一个类,然后调用该类的一个实例方法,并将该方法的返回值作为Bean。

User类定义如以往一样,成员变量是id和name。我们定义一个新的类,其中有一个实例方法返回值为User。

public class DefaultUserFactory implements UserFactory{
    public User createUser() {
        User user = new User();
        user.setId(1L);
        user.setName("user");
        return user;
    }
}

之后我们在XML声明DefaultUserFactory的一个Bean,然后利用这个Bean的实例方法实例化User类型的Bean。

<!-- 先要有工厂类的Bean-->
<bean id="userFactory" class="org.company.think.in.spring.bean.factory.DefaultUserFactory"/>
<!-- factory-bean指定工厂类,factory-method指定工厂的生产方法-->
<bean id="user-by-instance-method" factory-bean="userFactory" factory-method="createUser"/>

之后生成容器输出user-by-instance-method的bean信息。

BeanFactory beanFactory = new ClassPathXmlApplicationContext("META-INF/bean-creation-context.xml");
User user = beanFactory.getBean("user-by-instance-method", User.class);
System.out.println(user);

输出结果如下
在这里插入图片描述


1.6、通过FactoryBean

我们可以借助FactoryBean完成Bean的实例化。

定义一个类,使其实现FactoryBean接口,复写其中的getObject()和getObjectType()两个方法(其实还有一个isSingleton(),但是这个方法已经默认实现了,默认是单例,一般不用我们操心)。

其中,getObject()定义的是Bean的具体信息,返回值就是Bean。getObjectType()的返回值则是Bean的类型。

之后在XML中定义这个实现FactoryBean的类即可

例子如下

package org.company.think.in.spring.bean.factory;
import org.company.think.in.spring.ioc.overview.domain.User;
import org.springframework.beans.factory.FactoryBean;

public class UserFactoryBean implements FactoryBean {
    @Override
    public Object getObject() throws Exception {
        User user = new User();
        user.setId(1L);
        user.setName("user");
        return user;
    }
    @Override
    public Class<?> getObjectType() {
        return User.class;
    }
}

XML中的定义如下

<bean id="user-by-factory-bean" class="org.company.think.in.spring.bean.factory.UserFactoryBean"/>

我们就可以通过getBean(“user-by-factory-bean”)得到User类的bean。

1.7、通过接口回调

bean的初始化过程中有一个接口回调的过程。只要bean实现了一系列以Aware结尾的接口,那就可以将一些信息注入到这个bean之中。比如BeanFactoryAware和ApplicationContextAware,bean实现这两个接口以后,就可以获取到当前的BeanFactory和ApplicationContext信息(一般情况下,一个普通的bean只包含自身的信息,比如成员变量,是不会包含容器相关信息的。但只要实现这两个接口,bean中就包含容器相关信息)。

我们看看示例。

public class Test implements BeanFactoryAware, ApplicationContextAware {
    public BeanFactory beanFactory;

    public ApplicationContext applicationContext;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(Test.class);
        applicationContext.refresh();

        Test test = applicationContext.getBean(Test.class);

        System.out.println(applicationContext == test.applicationContext);
        System.out.println(applicationContext.getBeanFactory() == test.beanFactory);

        applicationContext.close();
    }
}


上述例子的Test实现了BeanFactoryAware, ApplicationContextAware两个接口,并且覆写的方法是将当前容器赋值给Test的成员变量beanFactory和applicationContext。那在main函数中启动容器后,判断容器applicationContext,beanFactory是否分别与test.applicationContext和test.beanFactory是同一对象时,输出的结果就应该为true。下面是输出结果。
在这里插入图片描述




2、基于注解装配Bean

使用注解是一个更为简洁的方式,它是直接在类加注解,比起xml方式更为直观。

编写注解装配一般要经历两个过程,一是在目标类上加入相关注解;二是指定扫描类,扫描类中会指定范围,该范围内被标注的类才会被装配成Bean。

2.1、使用@Component装配Bean

我们可以使用@Component标注类,表明这个类需要被装配成一个Bean。同时可以直接使用@Value对属性进行赋值。

示例如下

package org.company.think.in.spring.ioc.overview.domain;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Map;

@Component
public class User {
    @Value("1")
    private Long id;
    @Value("user1")
    private String name;
}

这样一个Bean就标注好了,之后是需要定义扫描类,不过在这里要多讲一些关于@Component的事。

2.1.1、@Component的属性value

Component有一个属性value可以设置,它是表示该类被扫描成Bean以后,在容器中的名字,相当于是XML中的id。如果value没有设置,系统会默认该类在容器中的id为首字母小写后的类名。

2.1.2、与@Component相关的注解

为了更贴合业务,Sping还提供了@Controller,@Service和@Repository来标注控制层,服务层和持久层三层架构的Bean。似乎在目前的Spring框架中,这三个注解与@Component没有太多区别,但是文档还是建议不同层的的Bean使用对应的注解。

一方面在语义上能让人们更好区分被标注的Bean属于哪一层;另一方面在以后的版本(5.2.7以后)中,很可能会对这些不同层的注解专门实现一些定制化的需求。所以强烈建议不同层的Bean使用对应层的注解标注。

2.2、使用@ComponentScan标注扫描Bean

注解标注类以后,还需要定义扫描类。我们使用@ComponnetScan进行扫描。

首先我们先定义一个类,这个类可以是一个空类。然后我们在这个类上添加@ComponentScan注解

package org.company.think.in.spring.ioc.overview.domain;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
public class PojoConfig {
}

这个类很简单,但是有一点是要注意的。

注意第一行代码,这个类的包名必须和需要装配的类的包名是一样的。因为@ComponentScan扫描默认是扫描当前包的路径,所以包名必须和需要装配的类相同。

从这里就可以理解为什么需要装配的类分散在不同的包时,使用注解不容易管理,需要使用XML的原因。

2.2.1、@ComponentScan的属性

@ComponentScan的属性有两个,这两个都是用来指定扫描范围的。

2.2.1.1、basePackages

这个属性是用来设置扫描的包名。扫描范围是改包以及改包的子包下的所有类。要注意到basePackages是复数,所以我们可以指定多个包进行扫描。

package org.company.think.in.spring.ioc.overview.domain;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackages = {"org.company.think.in.spring.ioc.overview.domain", "org.company.think.in.spring.ioc.overview.dto")}
public class PojoConfig{
}

这段代码代表着Spring IoC会扫描包org.company.think.in.spring.ioc.overview.domain和org.company.think.in.spring.ioc.overview.dto下所有的类(包括它们子包的所有类)。

2.2.2.2、basePackageClasses

该属性指定的是,容器只对该属性指定的类,以及该类的子类进行进行扫描,如果这个类被注解标注要成为Bean,那该类就会被变成Bean放到容器中维护。

package org.company.think.in.spring.ioc.overview.domain;
import org.springframework.context.annotation.ComponentScan;
//Role和RoleImpl是定义好的类或接口
@ComponentScan(basePackageClasses = {User.class, Address.class})
public class PojoConfig{
}

2.3、@Autowired

使用@Componnet注解时,对于成员变量的赋值我们是使用@Value注解。但是@Value只能完成一般类型的赋值,对于引用类型的赋值,就要使用到@Autowired。

@Component
public class User {
    @Value("1")
    private Long id;
    @Value("user1")
    private String name;
    @Autowired
    private Address address;
}

我们使用@Autowired后,系统会在容器中已注册的Bean中去寻找与@Autowired标注类型一致的Bean,然后将这个Bean赋值给@Autowired标注的成员变量。

2.3.1、@Autowired的属性

@Autowired只有一个属性,required。该属性只能赋值true或false。true表示如果容器中没有找到想对应类型的Bean,那就报错,@Autowired标注的该成员变量必须指向一个实例;false表示即使没有找到相对应类型的Bean也不会报错。

2.3.2、@Autowired注解方法

@Autowired注解方法多半用于setter方法上,当然其他方法也可以,只要@Autowired标注方法的参数类型是引用即可。对于@Autowired标注的方法,这个方法的类在实例化成Bean时,会自动执行方法(即使方法不是构造方法,也不是静态方法),并且系统会在容器中寻找和方法参数类型相同的Bean传值到方法中执行。

下面是使用的例子。

@Component
public class User {
    @Value("1")
    private Long id;
    @Value("user1")
    private String name;
    private Address address;
	@Autowired
	public void setter(Address address) {
		this.address = address;
	}
}

2.3.3、@Autowired的歧义性

@Autowired是根据类型赋值,但如果容器中符合条件的Bean不止一个的话,计算机会糊涂,会不知道应该使用哪个Bean进行赋值。

在这种情况下,我们可以使用@Primary和@Qualifier两个注解来解决这个问题

2.3.3.1、@Primary

被@Primary标注的类在@Autowired自动装配中具有优先权。比如有两个类继承同一个接口,这时候容器类就有两个RoleService类

@Component
@Primary
public class RoleService1 implements RoleService{
    @Override
    public void RoleService(Role role) {
        System.out.println(role.sword + "1");
    }
}

@Component
public class RoleService2 implements RoleService{
    @Override
    public void RoleService(Role role) {
        System.out.println(role.sword + "2");
    }
}

当需要自动装配RoleService时,因为RoleService1标注了@Primary,所以系统会有限选择RoleService1进行装配。

要注意,@Primary可以同时标注多个Bean的。上述例子中RoleService1标注@Primary,那RoleService2也可以同时标注@Primary。这时候RoleService1和RoleService2就具有同样的优先权,歧义性依然存在,系统依然会报错。

2.3.3.2、@Qualifier

@Primary只是解决了首要性的问题,容器依然是根据类型来自动装配。而@Qualifier是根据名称查找来进行装配,完美地消除了歧义性。

@Qualifier紧跟着@Autowired标注,并且指定名称,指定Bean在容器中的名称,指定要这个Bean,这样就不会再有歧义性的问题。使用方式如下

public class User {
    @Value("1")
    private Long id;
    @Value("user1")
    private String name;
    @Autowired
    @Qualifier("address")
    private Address address;
}



2.4、 使用@Bean装配

对于Java而言,很多时候都需要引入第三方的包(jar文件),并且这些包都没有源码,或者源码是只读状态无法修改。这时候就无法使用注解将第三方的类变为开发环境的Bean。

这时候可以使用@Bean注解来解决这个问题。@Bean可以注解到方法上,并且将标注方法的返回值作为Bean放到容器中。比如我们要使用DBCP数据源,这时候就可以使用@Bean标注来装配数据源的Bean

@Bean
public DataSource getDataSource() {
	Properites props = new Properties();
	props.setProperty("drivers", "com.mysql.jdbc.Driver");
	props.setProperty("url", "jdbc:mysql://localhost:3306/mysql");
	props.setProperty("username", "root");
	porps.setProperty("password", "123456");
	DataSource dataSource = null;
	try {
		dataSource = BasicDataSourceFactory.createDateSource(props);
	}	catch(Exception e) {
		e.printStackTrace();
	}
	return dataSource;
}

2.4.1、注意事项

在使用@Bean注解方法的时候,要保证被注解方法的类被相关注解标注了。当扫描类扫描到这个类时,要让系统知道这个类需要被扫描。如果这个类没有被任何注解标注,只是一个普通的类,那系统扫描到这个类时,会认为这个类不需要被装配,那么其中被@Bean标注的方法也会被略过。@Bean就没有起到任何作用。

比如下面这个例子

public class Data {
    @Bean(name = "sword1")
    public Sword getSword(){
        Sword sword = new Sword();
        sword.setDamage(10);
        sword.setLength(10);
        return sword;
    }
}

容器扫描到Data类时会直接略过,最终容器中不会有一个name为sword1的Bean。

要想要@Bean起作用,就可以用下面两种方式。

  • 对Data加上@Configuration注解。
  • 通过AnnotationConfigApplicationContext生成容器是,将Data类也放到方法参数中。
//PojoConfig.class是扫描类
ApplicationContext ac = new AnnotationConfigApplicationContext(PojoConfig.class, Data.class);





2.5、引入其他容器资源,@Import和@ImportResource

有时候我们已经有了一些容器资源来源,比如已经写好的XML文件,或者被@ComponentScan标注的定义好的扫描类,我们想在这些资源上再加上一些Bean的定义组成新的容器,同时又不想再重复定义这些容器,那我们就可以通过引入的方式。

对于XML而言,我们就只能引入XML文件资源,在beans标签下使用import标签,例子如下

<?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:util="http://www.springframework.org/schema/util"

       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">

    <import resource="dependency-lookup-context.xml"/>

    <bean id="userRepository" class="org.company.think.in.spring.ioc.overview.repository.UserRepository">
        <property name="users">
            <util:list>
                <ref bean="superUser"/>
                <ref bean="user"/>
            </util:list>
        </property>
    </bean>
</beans>

对于通过注解生成的容器,我们可以引入其他注解的资源,也可以引入XML定义的资源。

2.5.1、引入XML资源

使用@ImportResource即可。在扫描类中加入@ImportResource,该注解的属性就是XML的项目路径。加上这个注解之后,该注解中指定的XML文件中定义的Bean就会被引入到由该扫描类生成的容器中。

使用例子如下

@ComponentScan
@ImportResource({"bean1.xml", "bean2.xml"})
public class PojoConfig {
}

通过这样通过该扫描类生成的容器中,也会存在这bean1.xml和bean2.xml中定义的Bean。

2.5.2、引入注解资源

和@ImportResource一样,在相同的位置使用@Import即可。@Import中的属性是其他的扫描类

@ComponentScan
@Import({ApplicationConfig2.class, ApplicationConfig3.class})
public class PojoConfig {
}

@Import和@ImportResource是可以同时使用的

2.5.3、Bean重复定义的问题

引入资源时,很有可能会对同一个类对此扫描 ,或者XML中多次定义,这时就涉及到一个类被多次定义或者多次扫描时,容器中存在关于这个类的多个Bean还是一个Bean。

对于XML而言,无论重复与否,定义了多少次就产生多少个Bean,这点比较简单。

对于注解而言,同一个扫描类多次扫描了一个类,那关于这个类只会生成一个Bean;如果是多个扫描类扫描到了同一个类,那每个扫描类会分别产生一个关于这个类的Bean。

2.6、分组注入

2.6.1、基于@Qualifier分组注入

@Autowired可以标注在集合上,比如

@Autowired
private Collection<User> users;

容器会将所有User类的bean注入到集合users中。

在对集合进行@Autowired注入时,我们还只选择一部分User类注入到集合中,而不是将所有User类都注入到集合。只需要再加一个注解@Qualifier

@Autowired
@Qualifier
private Collection<User> users;


我们可以用下面代码验证一下,Test类中@Autowired四个User类,其中有两个额外加上了@Qualifier注解。

public class Test {
    @Autowired
    private Collection<User> users;

    @Autowired
    @Qualifier
    private Collection<User> qualifierUsers;

    @Bean
    @Qualifier
    public User user1() {
        User user = new User();
        user.setId(1L);
        return user;
    }

    @Bean
    @Qualifier
    public User user2() {
        User user = new User();
        user.setId(2L);
        return user;
    }

    @Bean
    public User user3() {
        User user = new User();
        user.setId(3L);
        return user;
    }

    @Bean
    public User user4() {
        User user = new User();
        user.setId(4L);
        return user;
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(Test.class);
        applicationContext.refresh();

        Test test = applicationContext.getBean(Test.class);

        System.out.println(test.users);
        System.out.println(test.qualifierUsers);

        applicationContext.close();
    }


程序的执行结果如下
在这里插入图片描述

可以看出,只有@Autowired标注的users中包含了所有User类的bean。而加上了@Qualifier的qualifierUsers中则只有@Qualifier标注的bean。


2.6.2、继承@Qualifier实现分组注入

我们可以基于@Qualifier对集合进行更精细地注入。创建一个新的注解继承@Qualifier

@Target({ElementType.FIELD, ElementType.METHOD})
@Inherited
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface UserGroup {
}

与使用@Qualifier进行分组注入的方式一样,将@UserGroup标注在集合和bean上。

public class Test {
    @Autowired
    private Collection<User> users;

    @Autowired
    @Qualifier
    private Collection<User> qualifierUsers;

    @Autowired
    @UserGroup
    private Collection<User> userGroup;

    @Bean
    public User user1() {
        User user = new User();
        user.setId(1L);
        return user;
    }

    @Bean
    public User user2() {
        User user = new User();
        user.setId(2L);
        return user;
    }

    @Bean
    @Qualifier
    public User user3() {
        User user = new User();
        user.setId(3L);
        return user;
    }

    @Bean
    @Qualifier
    public User user4() {
        User user = new User();
        user.setId(4L);
        return user;
    }

    @Bean
    @UserGroup
    public User user5() {
        User user = new User();
        user.setId(5L);
        return user;
    }

    @Bean
    @UserGroup
    public User user6() {
        User user = new User();
        user.setId(6L);
        return user;
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(Test.class);
        applicationContext.refresh();

        Test test = applicationContext.getBean(Test.class);

        System.out.println(test.users);
        System.out.println(test.qualifierUsers);
        System.out.println(test.userGroup);
        
        applicationContext.close();
    }
}



Test类中有6个类型为User的bean。user1和user2没有额外注解,user3和user4有@Qualifier注解,user5和user6有@UserGroup注解。

当我们输出user,qualifierUsers和userGroup时,可以预测user中包含所有的bean。qualifierUsers中包含被@Qualifier标注的bean,因为@UserGroup继承@Qualifier,所以qualifierUsers也包含@UserGroup标注的bean。而userGroup只包含@UserGroup标注的bean。
在这里插入图片描述

3、基于properties文件装配

基于properties文件装配bean只能在BeanDefinitionRegistry的实现类中注册,比较常见的实现类有AnnotationConfigApplicationContext和DefaultListableBeanFactory。主要是因为properties文件装配要使用到PropertiesBeanDefinitionReader,而这个类唯一构造器的参数就是BeanDefinitionRegistry
在这里插入图片描述

PropertiesBeanDefinitionReader的源代码

3.1、 properties文件装配规则

首先,我们先在资源目录下定义properties文件,定义的规则在PropertiesBeanDefinitionReader的官方文档中有写。
在这里插入图片描述

文档上的例子

定义很简单,例子也浅显易懂。这里就不再去解释了。


3.2、封装properties文件成Java类并装入beanFactory

步骤可简单分为四步

  1. 使用Resource获取properties文件信息。
  2. 使用EncodedResource加工Resource,使系统知道以正确的编码方式解读properties文件信息(中文环境通常使用UTF-8,默认是ASCII)。
  3. 新建PropertiesBeanDefinitionReader对象,并在构建方法中和指定容器关联(容器类型必须是DefaultListableBeanFactory)。
  4. 使用刚刚新建的PropertiesBeanDefinitionReader对象通过beanDefinitionReader.loadBeanDefinitions方法加载存储着bean信息的EncodedResource。

做完以上四步以后,PropertiesBeanDefinitionReader关联的beanFactory中就有properties文件中定义的bean了。

这是示例。我们先定义properties文件在这里插入图片描述

properties定义bean示例

执行以下代码

DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 实例化基于 Properties 资源 BeanDefinitionReader
String location = "META-INF/user.properties";
// 第一步:使用Resource基于 ClassPath 加载 properties 资源
Resource resource = new ClassPathResource(location);
// 第二步:指定字符编码 UTF-8
EncodedResource encodedResource = new EncodedResource(resource, "UTF-8");
// 第三步:实例化PropertiesBeanDefinitionReader并关联beanFactory
PropertiesBeanDefinitionReader beanDefinitionReader = new PropertiesBeanDefinitionReader(beanFactory);
// 第四步:将encodedResource中的信息放入beanFactory中。
int beanNumbers = beanDefinitionReader.loadBeanDefinitions(encodedResource);
System.out.println("已加载 BeanDefinition 数量:" + beanNumbers);
// 通过 Bean Id 和类型进行依赖查找
User user = beanFactory.getBean("user", User.class);
System.out.println(user);

下面是运行结果
在这里插入图片描述


4、基于API装配

基于API装配在实际开发中不常用,太过繁琐。不过Spring框架内注入内建bean的时候基本都是使用API的方式,所以想要阅读Spring源码的话的,这些API也有必要了解。

4.1、构建BeanDefinition然后手动注入

这方面的细节可以看这篇文章,https://blog.csdn.net/sinat_38393872/article/details/106820610。

4.2、使用BeanDefinitionReader系列

BeanDefinitionReader系列有三个,PropertiesBeanDefinitionReader、AnnotatedBeanDefinitionReader和XmlBeanDefinitionReader。这三个的用法都是类似的,PropertiesBeanDefinitionReader在基于properties文件装配中介绍过了,下面就只介绍AnnotatedBeanDefinitionReader。

AnnotatedBeanDefinitionReader可以直接将一个类直接注册到一个容器中。使用方式和基于properties装配的PropertiesBeanDefinitionReader类似,主要是以下两个步骤

  1. 新建AnnotatedBeanDefinitionReader对象,并在构建方法中和指定容器关联(容器类型必须是DefaultListableBeanFactory)。
  2. 使用刚刚新建的PropertiesBeanDefinitionReader对象使用register方法直接加载某个类到容器中。

示例如下。

DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 基于 Java 注解的 AnnotatedBeanDefinitionReader 的实现
AnnotatedBeanDefinitionReader beanDefinitionReader = new AnnotatedBeanDefinitionReader(beanFactory);
int beanDefinitionCountBefore = beanFactory.getBeanDefinitionCount();
// 注册当前类(非 @Component class)
beanDefinitionReader.register(AnnotatedBeanDefinitionParsingDemo.class);
int beanDefinitionCountAfter = beanFactory.getBeanDefinitionCount();
int beanDefinitionCount = beanDefinitionCountAfter - beanDefinitionCountBefore;
System.out.println("已加载 BeanDefinition 数量:" + beanDefinitionCount);
// 普通的 Class 作为 Component 注册到 Spring IoC 容器后,通常 Bean 名称为 annotatedBeanDefinitionParsingDemo
// Bean 名称生成来自于 BeanNameGenerator,注解实现 AnnotationBeanNameGenerator
AnnotatedBeanDefinitionParsingDemo demo = beanFactory.getBean("annotatedBeanDefinitionParsingDemo",
        AnnotatedBeanDefinitionParsingDemo.class);
System.out.println(demo);

猜你喜欢

转载自blog.csdn.net/sinat_38393872/article/details/106864240