上篇博文讲Spring的IOC容器时说道,虽然容器功能强大,但容器本身只是个空壳,需要我们主动放入装配对象,并告诉它对象之间的协作关系,然后容器才能按照我们的指示发挥它的魔力,完成装配bean的使命。这里,我们把Spring创建应用对象之间的协作关系的行为成为装配。Spring提供了很多装配bean的方式供我们在开发中选择,我们常用到的有三种装配机制:自动装配、Java注解和XML配置。通常我们将第一种称为隐式的装配机制,后面两种为显示的装配机制。实际应用中,基于便利性考虑,首选的肯定是隐式的自动化装配机制,只有当需要注入的bean的源码不是由自己的程序来维护,而是引入第三方的应用组件的时候,才考虑显示的方式装配bean。当然,各种装配方式在实际应用中是可以自由选择搭配的,编码过程中也不必拘泥哪一种,适用就好。本篇博文先来讲述隐式的装配机制——bean的自动化装配。
你一定很好奇Spring是怎么来实现其自动化装配机制的,其实Spring主要通过下面两个方面来实现:
- 组件扫描——通过开启组件扫描功能让Spring可以自动发现应用上下文中的bean;
- 自动装配——自动满足组件之间的依赖关系。
下面,我们分别来看看Spring如何通过组件扫描和自动装配来为我们的应用程序自动化的装配bean。我们先定义一个汽车接口:
1 package spring.facade; 2 3 public interface Car { 4 void drive(); 5 }
组件扫描
组件扫描的要义在于通过扫描控制,让Spring自动的去发现应用程序中的bean。不过程序中的对象那么多,Spring怎么知道哪些对象是需要它去管理创建的呢?这就涉及到Spring的一个组件注解——@Component,被该注解标注的类即为Spring的组件类,Spring容器加载过程中会自动的为该类创建bean(PS:实际上Spring的组件注解按照语义化的分类还有@Controller @Repository @Service等等,分别作用于控制层、持久层和业务层,此处仅是举例演示,不做区分讲解)。所以,我们可以将接口的一个实现标注上该注解,表明实现类是要被Spring创建实例的——
1 package spring.impl; 2 3 import org.springframework.stereotype.Component; 4 import spring.facade.Car; 5 6 @Component 7 public class QQCar implements Car { 8 @Override 9 public void drive() { 10 System.out.println("开QQ车"); 11 } 12 }
不过,Spring的注解扫描默认是不开启的,所以我们还需要显示的配置注解启动。这里同样有两种方式,Java注解和XML的方式,我们分别展示出来——
Java配置类CarConfig :
package spring.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @ComponentScan public class CarConfig { }
XML配置文件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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="spring"/> beans>
接下来我们编写测试类,看看Spring是不是自动的去发现了我们注解为组件的bean并为我们创建了对象——
1 package spring.test; 2 3 import org.junit.Test; 4 import org.junit.runner.RunWith; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.test.context.ContextConfiguration; 7 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 import spring.config.CarConfig; 9 import spring.impl.QQCar; 10 import static org.junit.Assert.assertNotNull; 11 12 /** 13 * 注解释义: 14 * @RunWith(SpringJUnit4ClassRunner.class) 测试在Spring环境中运行 15 * @ContextConfiguration 上下文配置注解,指定配置文件(Java类或XML文件)的位置 16 */ 17 @RunWith(SpringJUnit4ClassRunner.class) 18 //@ContextConfiguration(classes = CarConfig.class) //加载Java配置类的方式 19 @ContextConfiguration(locations = "classpath:resource/applicationContext.xml") //加载XML配置的方式 20 public class CarTest { 21 @Autowired 22 private QQCar car ; 23 24 @Test 25 public void carTest(){ 26 assertNotNull(car); 27 } 28 }
虽然现在的编程趋势是越来越多的使用Java注解的方式,但是上面的测试你会发现,通过XML注解的方式能够测试成功,而Java注解的方式却是失败的,测试会抛出NoSuchBeanDefinitionException的异常,表示没有QQCar的组件定义,也就是Spring没有发现它,Why? 原因也很简单,那就是基于Java注解的方式启动的注解扫描默认情况下只能扫描配置类所在的包以及其的子包,如果要明确扫描其它包中的组件,需要在启动扫描的注解 @ComponetScan 中显示的注明,如改成 @ComponentScan("spring.impl"),上诉的测试就能通过了。如果有多个包要扫描,可以这样配置:@ComponentScan(basePackages = {"spring.impl","spring.test"}) 不过这样字符串的表示方式是类型不安全的,而且写死包名的方式不利于代码重构,我们可以指定包中所含的类或接口来指定要扫描的包,于是可以这样标注: @ComponentScan(basePackageClasses = QQCar.class) ,多个包同样可以用{}来以数组形式的表示。不过这样对重构依然不友好,最好的方式就是在要扫描的包中定义一个空标接口,该接口仅仅用来指定包扫描的范围,如此将重构的影响降到最低。
自动装配
前文的讲述只是阐明如何将一个类定义成Spring的组件并启动Spring的组件扫描,而且我们已经通过测试证实,Spring确实扫描到了我们指定的组件类并为我们创建了对象。不过,创建的对象只是独立的存在,并没有和其他对象产生依赖协作;实际应用中,对象之间的依赖协作是再常见不过了,而要将Spring通过组件扫描为我们创建的对象根据实际业务建立起相互的依赖协作,就需要利用Spring的自动装配。便于演示,我们再定义一个Man类,Man的工作就是开车,我们先通过构造器注入的方式来满足依赖,看Spring是否会给我们自动注入我们需要的Car的实例对象——
package spring.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import spring.facade.Car; @Component public class Man { private Car car; public Man() { } @Autowired public Man(QQCar car) { this.car = car; } public void work() { car.drive(); } }
测试方法——
1 package spring.test; 2 3 import org.junit.Test; 4 import org.junit.runner.RunWith; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.test.context.ContextConfiguration; 7 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 import spring.config.CarConfig; 9 import spring.impl.Man; 10 11 @RunWith(SpringJUnit4ClassRunner.class) 12 @ContextConfiguration(classes = CarConfig.class) 13 public class CarTest { 14 15 @Autowired 16 Man man; 17 18 @Test 19 public void carTest() { 20 man.work(); 21 } 22 }
如以上代码,测试当然是成功的,在测试类中,Man作为组件类被Spring扫描并创建了一个对象实例,该实例调用work方法的时候,需要Car的实例对象,而我们在有参构造函数上通过 @Autowired 注解表明了对象的依赖关系,程序运行过程中,Spring会自动为我们注入Car的实例对象来满足对象依赖,这就是自动装配的精要所在。实际上,不只是构造器上可以用 @Autowired 注解,在属性的Setter方法上,甚至普通的方法上,都可以用@Autowired 注解来满足对象之间的依赖,实现自动注入的功能——
1 package spring.impl; 2 3 4 import org.springframework.beans.factory.annotation.Autowired; 5 import org.springframework.stereotype.Component; 6 import spring.facade.Car; 7 8 @Component 9 public class Man { 10 private Car car; 11 12 public Man() { 13 } 14 //构造器实现自动装配 15 // @Autowired 16 public Man(QQCar car) { 17 this.car = car; 18 } 19 20 //Setter方法实现自动装配 21 // @Autowired 22 public void setCar(QQCar car) { 23 this.car = car; 24 } 25 //普通方法实现自动装配 26 @Autowired 27 public void insertCar(QQCar car) { 28 this.car = car; 29 } 30 31 public void work() { 32 car.drive(); 33 } 34 }
我们将Man类中添加不同的方法测试,依然是可以成功的。不过有一点要注意,在非构造器实现自动装配的时候,虽然我们没有自己new对象,但Spring创建实例会通过Man的默认的构造器,此时的Man类中如果定义了有参构造器,就一定要把默认构造器构造出来,不然会抛无默认构造器的异常,记住:一定养成类中写默认构造器的习惯,便于扩展。
自动装配的歧义性
如果你足够细心,你会发现博主上面满足自动装配的测试代码中,注入的Car并没有采用多态的写法,代码显得很低级。其实我是为了测试通过,故意注入了具体的实现,实际业务中当然不会这么局限的去写代码。因为博主Car的接口还有一个奔驰车的实现类BenzCar,如果用多态的写法,自动装配会有产生歧义性问题,会抛 NoUniqueBeanDefinitionException 异常。那么,面对这种歧义性,如何去解决呢?你一定知道Spring容器管理的每个bean都会有一个ID作为唯一标识,在上面的示例中,我们描述QQCar类为Spring的组件的时候并没有明确的设置ID,但是Spring默认会将组件类的类名首字母小写来作为bean的ID,而我们也可根据我们自己的业务需要自定义ID标识——
package spring.impl;
import org.springframework.stereotype.Component;
import spring.facade.Car; //这里指定 chenbenbuyi 为组件的ID @Component("chenbenbuyi") public class QQCar implements Car { @Override public void drive() { System.out.println("开QQ车"); } }
可是测试发现,这并没有解决接口参数在自动装配时的歧义性问题,因为在组件上自定义ID是一种后发行为,当你让Spring在装配阶段从多个接口实现中选择要自动注入的对象实例时,Spring无法选择——就好比你只跟我说你要开一辆车,每辆车也都有唯一的车牌号,但我还是不知道你要开什么车。怎么办呢?这里有多种解决方案,我们可以通过 @Primary注解将我们明确需要自动注入的实现类标注为首选的bean,就想这样——
1 package spring.impl; 2 3 import org.springframework.context.annotation.Primary; 4 import org.springframework.stereotype.Component; 5 import spring.facade.Car; 6 7 @Component 8 @Primary 9 public class BenzCar implements Car { 10 @Override 11 public void drive() { 12 System.out.println("开奔驰车"); 13 } 14 }
当自动装配的时候,Spring面对歧义性时,会优先选择被标注为首选的bean进行自动注入。当然,我们还可以采用限定符注解,在使用@Autowired 完成自动装配的时候限定只让某个bean作为自动注入的bean——
1 package spring.impl; 2 3 4 import org.springframework.beans.factory.annotation.Autowired; 5 import org.springframework.beans.factory.annotation.Qualifier; 6 import org.springframework.stereotype.Component; 7 import spring.facade.Car; 8 9 @Component 10 public class Man { 11 private Car car; 12 13 public Man() { 14 } 15 //普通方法实现自动装配 16 @Autowired 17 @Qualifier("chenbenbuyi") //限定ID为 chenbenbuyi 的bean被装配进来 18 public void insertCar(Car car) { 19 this.car = car; 20 } 21 22 public void work() { 23 car.drive(); 24 } 25 }
自此,关于Spring的自动装配就阐述得差不多了,下一节系列文章会接着讲解Spring的另外两种常用的装配机制——Java注解和XML配置。博文所述皆为原创,如要转载,请注明出处;如果阐述得不恰当的地方,欢迎指教,不胜感激。