Spring学习笔记(二):Spring Bean装配

本部分讲述如何将 Bean 注入 Spring IoC容器中。

全部章节传送门:
Spring学习笔记(一):Spring IoC 容器
Spring学习笔记(二):Spring Bean 装配
Spring学习笔记(三): Spring 面向切面
Spring学习笔记(四): Spring 数据库编程
Spring学习笔记(五): Spring 事务管理

依赖注入的方式

依赖注入可以分为3种方式:

  • 构造器注入。
  • setter注入。
  • 接口注入。

其中构造器注入和 setter 注入是主要的方式。

构造器注入

构造器注入依赖于构造方法的实现,构造方法可以是有参或者无参的。

创建一个实体类 Role 。

package com.wyk.springdemo.pojo;

public class Role {
    private Long id;
    private String roleName;
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }

    public Role(String roleName, String note) {
        this.roleName = roleName;
        this.note = note;
    }
}

在配置文件中添加 Bean 。其中 index 指定参数的位置,而 value 设置值。


    
    

构造器注入还是比较简单的,缺点是当参数较多时,构造方法比较复杂。

使用 setter 注入

setter 注入是 Spring 中最主流的注入方式,它会首先调用无参的构造方法,然后使用 setter 注入为其设置对应的值。

为前面的实体类添加一个无参构造方法。

public Role() {}

然后在配置文件中添加 Bean 。


    
    

接口注入

当资源来自外界的时候,可以通过采用接口注入的方式获取它,比如数据库连接资源。

装配 Bean 概述

下面讲述如何将 Bean 装配到 Spring IoC 容器中,常用的方法有3种:

  • 在XML中显示配置。
  • 在 Java 的接口和类中实现配置。
  • 隐式 Bean 的发现机制和自动装配原则。

在实际开发中,建议使用的方式是:

  • 基于约定由于配置的原则,最优先的应该是通过隐式 Bean 的发现机制和自动装配原则。
  • 在无法使用自动装配原则的情况下优先考虑 Java 接口和类中实现配置。
  • 最后考虑XML配置。

通过 XML 配置装配 Bean

使用 XML 装配 Bean 需要引入对应的 XML 模式(XSD)。




装配简易值

看一个简单的 Bean 。


    
    
    

简单说明一下。

  • id 属性是 Spring 找到这个 Bean 的编号,不过 id 不是一个必需的属性,如果没声明它,那么 Spring 将会采用 “全限定名#{number}”的格式生成编号,例如上面的例子,如果没有声明 id , 那么它的编号就是“com.wyk.springdemo.pojo.Drinks#0”。
  • class 是一个类全限定名。
  • property定义类的属性。

如果需要注入一些自定义的类,则可以通过 ref 属性。


    
    
    


    
    

装配集合

本部分讲述如何装配复杂元素,比如Set、Map、List等。

创建一个 Bean 。

package com.wyk.springdemo.pojo;

import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

public class ComplexAssembly {
    private Long id;
    private List list;
    private Map map;
    private Properties props;
    private Set set;
    private String[] array;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List getList() {
        return list;
    }

    public void setList(List list) {
        this.list = list;
    }

    public Map getMap() {
        return map;
    }

    public void setMap(Map map) {
        this.map = map;
    }

    public Set getSet() {
        return set;
    }

    public void setSet(Set set) {
        this.set = set;
    }

    public String[] getArray() {
        return array;
    }

    public void setArray(String[] array) {
        this.array = array;
    }

    public Properties getProps() {
        return props;
    }

    public void setProps(Properties props) {
        this.props = props;
    }
}

在 XML 中装配这个实体。


    
    
        
            value-list-1
            value-list-2
            value-list-3
        
    
    
        
            
            
            
        
    
    
        
            value-prop-1
            value-prop-2
            value-prop-3
        
    
    
        
            value-set-1
            value-set-2
            value-set-3
        
    
    
        
            value-array-1
            value-array-2
            value-array-3
        
    

简单介绍一下集合类型的属性如何装配:

  • List 属性通过元素进行装配,然后通过元素设置值。
  • Map 属性通过元素进行装配,然后通过元素设置值,只是entry包含一个key和一个value。
  • Properties 属性通过元素进行装配,然后通过多个元素设置值,每个prop包含一个key。
  • Set 属性通过元素进行装配,然后通过元素设置值。
  • 数组通过元素进行装配,然后通过元素设置值。

上面讨论了各个集合的装载,当集合的元素是实体对象的时候,情况会更复杂。

首先创建2个实体类,一个是前面使用过的 Role 类,另一个是下面的 User 类。

package com.wyk.springdemo.pojo;

public class User {
    private Long id;
    private String userName;
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

然后创建一个复杂一些的实体类。

package com.wyk.springdemo.pojo;

import java.util.List;
import java.util.Map;
import java.util.Set;

public class UserRoleAssembly {
    private Long id;
    private List list;
    private Map map;
    private Set set;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List getList() {
        return list;
    }

    public void setList(List list) {
        this.list = list;
    }

    public Map getMap() {
        return map;
    }

    public void setMap(Map map) {
        this.map = map;
    }

    public Set getSet() {
        return set;
    }

    public void setSet(Set set) {
        this.set = set;
    }
}

然后在 XML 文件中装配实体。


    
    


    
    


    
    
    


    
    
    


    
    
        
            
            
        
    
    
        
            
            
        
    
    
        
            
            
        
    

从上例可以看出:

  • List 属性通过元素定义注入,然后通过多个元素的 Bean 属性去引用定义好的 Bean。
  • Map 属性通过元素定义注入,然后通过多个元素的key属性去引用定义好的 Bean 作为键, value属性去引用定义好的 Bean 作为值。
  • Set 属性通过元素定义注入,然后通过多个元素的 Bean 属性去引用定义好的 Bean。

命名空间装配

Spring 还提供了对应的命名空间的定义,只是在使用命名空间的时候需要先引入对应的命名空间和 XSD 文件。

前面讲到可以通过构造器和 setter 进行依赖注入,在有了p命名空间和c命名空间时我们可以简单的把它们当做 Bean 的一个属性来进行定义。先看一个例子。



    
    

使用p命名空间和c命名空间时需要先声明使用对应的命名空间,即在beans元素上加入xmlns:p="http://www.springframework.org/schema/p" 和xmlns:c="http://www.springframework.org/schema/c"。

其中,使用c进行构造器注入,c:_0代表第一个参数,以此类推,如果想引用其它 Bean,添加-ref即可,比如 c:_1-ref 。

使用p进行 setter 注入,比如p:id="2",以2为值,使用的setId方法设置,同样,如果引用其它 Bean , 使用-ref,比如 p:id-ref。

补充一下,如果在IDEA中报错URI is not registered,需要在File →Settings→Schemas and DTDs中添加命名空间的引用。

还可以使用util命名空间xmlns:util="http://www.springframework.org/schema/util" (对应java.util包)引入集合类型。


    张三
    李四
    王五

通过注解装配 Bean

通过注解的方式可以减少 XML 的配置,注解功能更为强大,它既能实现 XML 的功能,也提供了自动装配的功能。符合“约定优于配置”的开发原则。

Spring 中,有两种发现 Bean 的方式:

  • 组件扫描。通过定义资源的方式,让 Spring IoC 容器扫描对应的包,从而把 Bean 装配进来。
  • 自动装配。通过注解定义,使得一些依赖关系可以通过注解完成。

使用@Component装配 Bean

首先定义一个实体类。

package com.wyk.beanDemo.pojo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component(value="role")
public class Role {
    @Value("1")
    private Long id;
    @Value("xiaoming")
    private String roleName;
    @Value("haha")
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

注意其中的注解。

  • 注解 @Component 代表 Spring IoC 会把这个类扫描成 Bean ,其中的 value 属性代表这个类在 Spring 中的 id 。也可以简写成 @Component("role") ,甚至直接写成 @Component ,这时id会默认成首字母小写的类名。
  • 注解 @Value 代表值的注入。

这时候还需要一个 Java Config告诉 Spring 去哪里扫描 Bean 。

package com.wyk.beanDemo.pojo;

import com.wyk.beanDemo.service.impl.RoleServiceImpl;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class PojoScan {

}

需要注意的是, @ComponentScan 代表进行扫描,默认扫描的当前的包,因此实体和配置所在的包必须一致。

最后,需要通过 Spring IoC 容器的实现类 AnnotationConfigApplicationContext 去生成容器。

package com.wyk.beanDemo;

import com.wyk.beanDemo.pojo.PojoScan;
import com.wyk.beanDemo.pojo.Role;
import com.wyk.beanDemo.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class mainApp {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(PojoScan.class);
        Role role = context.getBean(Role.class);
        System.err.println(role.getId());
    }
}

上面的 @ComponentScan 只扫描当前包路径,有很大的局限。这里讲一下它的2个配置项:

  • basePackages 。它的值是 Java 包的数组,Spring 会根据它的配置扫描对应的包和子包。
  • basePackageClasses 。它可以配置多个类,Spring 会根据配置的所在的包,为包和子包扫描装配对应配置的 Bean 。

创建一个接口。

package com.wyk.beanDemo.service;

import com.wyk.beanDemo.pojo.Role;

public interface RoleService {
    public void printRoleInfo(Role role);
}

再创建一个接口的实现类, @Component 代表它是一个 Spring 的 Bean 。

package com.wyk.beanDemo.service.impl;

import com.wyk.beanDemo.pojo.Role;
import com.wyk.beanDemo.service.RoleService;
import org.springframework.stereotype.Component;

@Component
public class RoleServiceImpl implements RoleService {

    public void printRoleInfo(Role role) {
        System.out.println(role.getId());
        System.out.println(role.getRoleName());
        System.out.println(role.getNote());
    }
}

修改配置类。

package com.wyk.beanDemo.pojo;

import com.wyk.beanDemo.service.impl.RoleServiceImpl;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackageClasses = {Role.class, RoleServiceImpl.class})
//@ComponentScan(basePackages = {"com.wyk.beanDemo.pojo","com.wyk.beanDemo.service"})
public class PojoScan {

}

这里使用 basePackages 或者 basePackageClasses 均可,一般来说,basePackages 可读性较好,优先使用它;但在大量需要重构的工程中,尽量不要使用 basePackages ,因为很多时候修改包名需要反复配置,而IDE不会进行提示,而使用 basePackageClasses 的话,IDE会报错提示,并且可以轻松处理这些错误。

另外,建议只使用一个 @ComponentScan 注解,因为如果采用多个,Spring 会为每一个 @ComponentScan 注解生成一个新的对象,这往往不是我们需要的。如果同一个 @ComponentScan 注解定义重复的包或者存在其子包的定义,也不会配置生成多个对象。

进行测试。

package com.wyk.beanDemo;

import com.wyk.beanDemo.pojo.PojoScan;
import com.wyk.beanDemo.pojo.Role;
import com.wyk.beanDemo.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class mainApp {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(PojoScan.class);
        Role role = context.getBean(Role.class);
        RoleService roleService = context.getBean(RoleService.class);
        roleService.printRoleInfo(role);
    }
}

自动装配 @Autowired

所谓自动装配技术,是由 Spring 自己发现对应的 Bean ,自动完成装配工作的方式,它会应用到注解 @Autowired ,这时候 Spring 会根据类型去寻找定义的 Bean 然后将其注入。

创建一个接口 RoleService2 。

package com.wyk.beanDemo.service;

public interface RoleService2 {
    void printRoleInfo();
}

这个接口采用了 Spring 推荐的接口方式,这样更为灵活,因为定义和实现进行了分离,接下来创建实现类。

package com.wyk.beanDemo.service.impl;

import com.wyk.beanDemo.pojo.Role;
import com.wyk.beanDemo.service.RoleService2;
import org.springframework.beans.factory.annotation.Autowired;

public class RoleServiceImpl2 implements RoleService2 {

    @Autowired
    private Role role;

    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
        this.role = role;
    }

    public void printRoleInfo() {
        System.out.println(role.getId());
        System.out.println(role.getRoleName());
        System.out.println(role.getNote());
    }
}

在这里使用了 @Autowired 注解, Spring 会按照类型找到定义的实例,将其注入。

IoC 容器有时候会寻找失败,在默认情况下寻找失败会抛出异常,如果不希望抛出异常,可以通过配置项 required 来改变它,设置为 @Autowired(required=false)。

@Autoeired 除可以配置在属性上,还可以配置到方法上,常见的 Bean 的 setter 方法也可以使用它来完成注入。

@Autowired
public void setRole(Role role) {
    this.role = role;
}

自动装配的歧义性

在一些情况下,一个接口可能会有多个实现类,这样在注入的时候,会由于无法判断使用哪个实现类,导致注入失败。

为了消除歧义性,Spring 提供了2个注解 @Primary 和 @Qualifier 。

注解 @Primary 代表首要,当 Spring 通过一个接口或抽象类注入对象的时候,该注解会告诉 Spring 首先使用哪个类。

@Component("roleSerivce3")
@Primary
public class RoleServiceImpl3 implements RoleService {
}

如果有多个实现类都标注了 @Primary , Spring 会抛出异常,@Primary只能解决首要性问题,无法解决选择性问题。

@Qualifier 注解改为使用按照名称查找的方法,而不是按照类型,这样就可以消除歧义性。

public class RoleController {
    @Autowired
    @Qualifier("roleService3")
    private RoleService roleService;

    ...
}

装载带有参数的构造方法类

对于带有参数的构造方法,也允许我们直接通过注解进行注入。

public RoleController(@Autowired RoleService roleService) {
    this.roleService = roleService;
}

使用 @Bean 装配 Bean

前面通过 @Component 装配 Bean 只能注解到类上,注解 @Bean 可以注解到方法上,适用于第三方包引入不方便添加 @Component 的类。

@Bean(name="dataSource")
public DataSource getDataSource() {
    ...
}

@Bean 有4个配置项:

  • name: 是一个字符串数组,允许配置多个 BeanName 。
  • autowire: 标志是否是一个引用的 Bean 对象,默认值是 autowire.NO 。
  • initMethod: 自定义初始化方法。
  • destroyMethod: 自定义销毁方法。

装配的混合使用

在现实中,一般的使用方法是对自己工程中所开发的类尽量使用注解的方法,对于引入的第三方包或者服务,尽量使用 XML 方式, 这样的好处是可以尽量减少对第三方包或者服务细节的理解,也更加清晰和明朗。

在程序中,可以通过注解 @ImportResource引入 XML 文件,其中配置的内容是一个数组可以引入多个 XML 文件。

@ComponentScan
@ImportResource({"classpath:spring-dataSource.xml"})
public class ApplicationConfig {

}

然后我们就可以使用 @Autowired 注入在 spring-dataSource.xml 中定义的 Bean 了。

如果有多个类似 ApplicationConfig 的配置类需要注入,可以使用 @Import 注解。

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

}

同样,可以通过 import 元素在一个 XML 文件中引入其他 XML 文件。


也许你希望使用 XML 加载 Java 配置类,不过目前 Spring 不支持。但是 Spring 支持通过 XML 的配置扫描主机的包,只要通过定义扫描的包就可以了。


使用 Profile

在软件开发中,有很多情况下需要准备多套环境,Spring 也会对这样的场景进行支持,在 Spring 中我们可以定义 Bean 的 Profile 。

使用注解 @Profile 配置

首先看注解 @Profile 如何配置,下面举个例子,配置两个环境,一个用于开发(dev),一个用于测试(test)。

@Component
public class ProfileDataSource {

    @Bean(name="devDataSource")
    @Profile("dev")
    public DataSource getDevDataSource() {
        ...
    }

    @Bean(name="testDataSource")
    @Profile("test")
    public DataSource getTestDataSource() {
        ...
    }
}

使用 XML 定义 Profile

在 XML 中 beans 标签中添加 Profile 属性即可,但直接添加会导致整个文件都属于同一个 Profile , 所以一般放在内部,这样可以在一个 XML 文件中配置多个 Profile 。



        ...
    
    
        ...
    

启动 Profile

如果直接启动程序, Profile 相关的 Bean 并不会加载到 Spring 之中,这是因为需要自行激活 Profile 。激活 Profile 的方法有以下5种:

  • 在使用 Spring MVC 的情况下可以配置 Web 上下文参数,或者DispatchServlet参数。
  • 作为 JNDI 条目。
  • 配置环境变量。
  • 配置 JVM 启动参数
  • 在集成测试环境中使用 @ActiveProfiles 。

下面对几种常用的方法进行介绍。

在测试代码中激活 Profile , 需要使用注解 @ActiveProfiles 。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=ProfileConfig.class)
@ActiveProfiles("dev")
public class ProfileTest {
    @Autowired 
    private DataSource dataSource;
    @Test
    public void test() {
        ...
    }
}

如果需要在服务器上运行,最好还是配置 Java 虚拟机的启动项,关于定制 Profile 的参数存在两个:

  • spring.profiles.default: 默认启动的 Profile ,如果系统没有配置有关 Profile 参数的时候,那么它将启动。
  • spring.profiles.active: 启动的 Profile ,如果配置了它,那么 spring.profiles.default将失效。

比如需要启动 test ,可以在 JVM 中配置:

JAVA_OPTS="-Dspring.profiles.active=test"

在大部分情况下需要启动 Web 服务器,如果使用的是 Spring MVC,那么可以设置 Web 环境参数或者 DispatcherServlet参数来选择对应的Profile,比如可以在 web.xml中进行配置。

......


    spring.profiles.active
    test

......


    dispatcher
    org.springframework.web.servlet.DispatcherServlet
    2
    
        spring.profiles.active
        test
    

......

加载属性文件

在 Spring 项目中,属性(properties)文件经常用做配置文件,比如使用 properties 文件配置数据库文件。

jdbc.database.driver=com.mysql.jdbc.Driver
jdbc.database.url=jdbc:mysql://localhost:3306/wyk
jdbc.database.username=wyk
jdbc.database.password=123123

在 Spring 中可以通过注解或者 XML 的方式加载属性文件。

使用注解方式加载属性文件

Spring 提供注解 @PropertySource 来加载属性文件,它有以下配置项:

  • name: 字符串,配置这次属性配置的名称。
  • value: 字符串数组,可以配置多个属性文件。
  • ignoreResourceNotFound: boolean值,默认为 false ,其含义为如果找不到对应的属性文件是否进行忽略处理,由于默认值为 false ,所以在默认情况下找不到配置文件会抛出异常。
  • encoding: 编码,默认为空。
@Configuraton
@PropertySource(value={"classpath:database-config.properties"},ignoreResourceNotFound=true)
public class ApplicationConfig {

}

然后可以通过环境来获取对应的配置属性。

ApplicationContext context = new AnnotationConfigApplicaitonContext(ApplicationConfig.class);
String url = context.getEnvironment().getProperty("jdbc.database.url");

但是这种方式没有解析属性占位符(即${})的能力,因此更加推荐的方式是使用一个属性文件解析类(PropertySourcesPlcaholderConfigurer)进行处理。

首先修改 Java 配置类。

@Configuraton
@ComponentScan
@PropertySource(value={"classpath:database-config.properties"},ignoreResourceNotFound=true)
public class ApplicationConfig {
    @Bean
    public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }
}

这样就可以通过 @Value 注解使用属性占位符。

@Component
public class DataSourceBean {
    @Value("${jdbc.database.driver}")
    private String driver;

    @Value("${jdbc.database.url}")
    private String url;

    @Value("${jdbc.database.username}")
    private String username;

    @Value("${jdbc.database.password}")
    private String password;

    ...
}

使用 XML 方式加载属性文件

使用 XML 方式加载属性文件,只需要使用元素加载一些配置项即可。



        

上面的方式在属性文件较多时会导致 location 属性较长。还可以在 bean 标签中加载属性文件,这样可以分别加载多个属性文件,这时需引入 PropertyPlaceholderConfigurer 类。


    
        
            classpath:quartz.properties
            classpath:db.properties        
    

条件化装配 Bean

在某些情况下不需要去装配 Bean ,比如当某些配置文件不存在的时候。这个时候需要条件化判断,Spring 提供了注解 @Conditonal 去配置类,但是这些类需要实现 Condition (org.springframework.context.annotation.Condition)接口。

@Bean
@Conditional({DataSourceCondition.class})
public DataSource getDataSource() {
    @Value("${jdbc.database.driver}") String driver;
    @Value("${jdbc.database.url}") String url;
    @Value("${jdbc.database.username}") String username;
    @Value("${jdbc.database.password}") String password;

    ...
}

这里通过 @Value 引入了属性文件中的配置,但是我们无法确定属性是否存在,因此通过 @Conditional 注解引入一个类 DataSourceCondition 去进行判断。

public class DataSourceCondition implements Condition {
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        return env.containsProperty("jdbc.database.driver") 
        && env.containsProperty("jdbc.database.url") 
        && env.containsProperty("jdbc.database.username") 
        && env.containsProperty("jdbc.database.password")
    }
}

Spring Bean 作用域

Spring 框架支持以下五个作用域,分别为singleton、prototype、request、session和global session,5种作用域说明如下所示。

作用域 描述
singleton 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,默认值
prototype 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean()
request 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境
session 同一个HTTP Session共享一个Bean,不同Session使用不同的Bean,仅适用于WebApplicationContext环境
global-session 一般用于Portlet应用环境,该运用域仅适用于WebApplicationContext环境

在 Bean 中的添加方式如下。


    
    

Spring 表达式

待补充

你可能感兴趣的:(Spring学习笔记(二):Spring Bean装配)