自动化装配的确有很大的便利性,但是却并不能适用在所有的应用场景,比如需要装配的组件类不是由自己的应用程序维护,而是引用了第三方的类库,这个时候自动装配便无法实现,Spring对此也提供了相应的解决方案,那就是通过显示的装配机制——Java配置和XML配置的方式来实现bean的装配。
1 Java配置类装配bean
我们还是借助上篇博文中的老司机开车的示例来讲解。Car接口中有开车的drive方法,该接口有两个实现——QQCar和BenzCar
package spring.impl; import spring.facade.Car; public class QQCar implements Car { @Override public void drive() { System.out.println("开QQ车"); } }
既然是通过Java代码来装配bean,那就是不是我们上一篇讲的通过组件扫描的方式来发现应用程序中的bean的自动装配机制了,而是需要我们自己通过配置类来声明我们的bean。我们先通过@Configuration注解来创建一个Spring的配置类,该类中包含了bean的创建细节——
import org.springframework.context.annotation.Configuration; import spring.facade.Car; import spring.impl.QQCar; /** * @Configuration 表明该类是Spring的一个配置类,该类中会包含应用上下文创建bean的具体细节 * @Bean 告诉Spring该方法会返回一个要注册成为应用上下文中的bean的对象 */ @Configuration public class CarConfig { @Bean public Car laoSiJi() { return new QQCar(); } }
以上类中创建的bean实例默认情况下和方法名是一样的,我们也可以通过@Bean注解的name属性自定义ID,例如 @Bean(name = "chenbenbuyi") ,那么在获取bean的时候根据你自己定义的ID获取即可。接着我们测试——
package spring.test; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import spring.config.CarConfig; import spring.facade.Car; public class CarTest { @Test public void carTest() { ApplicationContext context = new AnnotationConfigApplicationContext(CarConfig.class); //根据ID从容器容获取bean Car car = (Car) context.getBean("chenbenbuyi"); car.drive(); } }
以上测试能够成功输出,这就表明我们能够获取到QQCar的实例对象的,而这也是最简单的基于Java配置类来装配bean的示例了。但是你可能会说,明明是我们自己创建的Car的实例,怎么就成了Spring为我们创建的呢?好吧,我们把@Bean注解拿开,测试当然是无法通过,会抛NoSuchBeanDefinitionException异常。这里,你可能需要好好理解控制反转的思想了:因为现在对于bean创建的控制权我们是交给了Spring容器的,如果没有@Bean注解,方法就只是一个普通方法,方法体返回的实例对象就不会注册到应用上下文(容器)中,也就说,Spring不会为我们管理该方法返回的实例对象,当我们在测试类中向容器伸手要对象的时候,自然就找不到。
上述示例过于简单,现在,我们要更进一步,给简单的对象添加依赖,来完成稍微复杂一点的业务逻辑。车是需要老司机来开的,于是我们同上篇一样定义一个Man类,Man的工作就是开车——
package spring.impl; import spring.facade.Car; public class Man { private Car car;public Man(Car car) { this.car = car; } public void work() { car.drive(); } }
Car的对象实例是通过构造器注入,而Car的实例对象在配置类中通过方法laoSiJi()返回,所以我们在配置类中可以直接调用laoSiJi方法获取bean注入到Man的实例对象——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import spring.facade.Car; import spring.impl.BenzCar; import spring.impl.Man; @Configuration public class CarConfig { @Bean public Car laoSiJi() { return new BenzCar(); } @Bean public Man work() { return new Man(laoSiJi()); } }
测试类中通过上下文对象的getBean("work")方法就可以获取到Man的实例对象,从而完成对老司机开车的测试。或许,你会觉得,work方法是通过调用laoSiJi方法才获取的Car的实例的,实际上并非如此。因为有了@Bean注解,Spring会拦截所有对该注解方法的调用,直接返回该方法创建的bean,也即容器中的管理的bean。也就是说,laoSiJi方法返回的bean交给了Spring容器管理后,当其他地方需要实例对象的时候,是直接从容器中获取的第一次调用方法产生的实例对象,而不会重复的调用laoSiJi方法。我们可以如下测试——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import spring.facade.Car; import spring.impl.BenzCar; import spring.impl.Man; @Configuration public class CarConfig { @Bean public Car laoSiJi() { System.out.println("方法调用"); return new BenzCar(); } @Bean public Man work() { return new Man(laoSiJi()); } @Bean public Man work2() { return new Man(laoSiJi()); } }
如上测试你会发现,虽然我定义了两个方法来获取Man实例,但是控制台只输出了一次调用打印,即证明方法只在最初返回bean的时候被调用了一次,而后的实例获取都是直接从容器中获取的。这也就是默认情况下Spring返回的实例都是单例的原因:一旦容器中注册了实例对象,应用程序需要的时候,就直接给予,不用重复创建。当然,很多情况下我们不会如上面的方式去引入依赖的bean,而可能会通过参数注入的方式,这样你就可以很灵活的使用不同的装配机制来满足对象之间的依赖关系,比如下面这种自动装配的方式给Man的实例注入依赖的Car对象——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import spring.facade.Car; import spring.impl.Man; @Configuration @ComponentScan("spring.impl") public class CarConfig { @Bean public Man work(Car car) { return new Man(car); } }
当然,如果你喜欢去简就繁,也可以通过XML配置文件配置依赖的bean。下面再来看看XML的方式如何装配bean。
2 XML配置文件装配bean
使用XML配置文件的方式装配bean,首要的就是要创建一个基于Spring配置规范的XML文件,该配置文件以<beans>为根元素(相当于Java配置的@Configuration注解),包含一个或多个<bean>元素(相当于配置类中@Bean注解)。针对上文的汽车示例,如果改成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--> <bean class="spring.impl.BenzCar"></bean> </beans>
然后,从基于XML的配置文件中加载上下文定义,我们就能根据ID获取到对应的bean了——
package spring.test; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import spring.facade.Car; public class CarTest { @Test public void carTest() { ApplicationContext context = new ClassPathXmlApplicationContext("resource/applicationContext.xml"); //XML的方式如果没有明确给定ID,默认bean的ID会根据类的全限定名来命名,以#加计数序号的方式命名。 Car car = (Car)context.getBean("spring.impl.BenzCar#0"); car.drive(); } }
当然,示例中使用自动化的命名ID看起来逼格满满,但其实并不实用,如果需要引用bean的实例就有点操蛋了,实际应用中当然还是要借助<bean>的id属性来自定义命名。
2.1 构造器注入
给<bean>元素设置id属性,在构建另外的对象实例的时候,就可以很方便的引用,譬如上面基于Java的配置中的构造器注入,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="car" class="spring.impl.BenzCar"></bean> <bean id="man" class="spring.impl.Man"> <!--通过Man的构造器注入Car的实例对象--> <constructor-arg ref="car"></constructor-arg> </bean> </beans>
而有时候我们并不一定都是将对象的引用装配到依赖对象中,也可以简单的注入字面值——
package spring.impl; import spring.facade.Car; public class Man { private Car car; private String str;
public Man(String str ,Car car) { this.car = car; this.str = str; } public void work() { System.out.println(str); car.drive(); } }
<?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="car" class="spring.impl.BenzCar"></bean> <bean id="man" class="spring.impl.Man"> <!--分别注入字面值和对象的应用--> <constructor-arg value="陈本布衣"></constructor-arg> <constructor-arg ref="car"></constructor-arg> </bean> </beans>
接着,我们继续对已有代码做些改动,将注入的参数改为Car的List集合——
public Man(List<Car> cars) { this.cars = cars; }
那么配置文件就可以这样配置——
<?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="benzCar" class="spring.impl.BenzCar"></bean> <bean id="qqCar" class="spring.impl.QQCar"></bean> <bean id="man" class="spring.impl.Man"> <!--通过<list>子元素实现List集合对象的装配--> <constructor-arg> <list> <ref bean="benzCar"/> <ref bean="qqCar"/> </list> </constructor-arg> </bean> </beans>
如果是需要注入集合中的字面值,写法如下——
<?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="benzCar" class="spring.impl.BenzCar"></bean> <bean id="qqCar" class="spring.impl.QQCar"></bean> <bean id="man" class="spring.impl.Man"> <!--通过<list>子元素实现List集合字面值的装配--> <constructor-arg> <list> <value>这里直接填写字面值</value> <value>陈本布衣</value> </list> </constructor-arg> </bean> </beans>
我们可以采用同样的方式装配Set集合,只是Set集合会忽略掉重复的值,而且顺序也不保证。此处不做演示。
2.2 属性注入
构造器注入是一种强依赖注入,而很多时候我们并不倾向于写那种依赖性太强的代码,而属性的Setter方法注入作为一种可选性依赖,在实际的开发中是应用得非常多的。上面Man类如果要通过属性注入的方式注入Car的实例,就该是这样子——
package spring.impl; import spring.facade.Car; public class Man { private Car car; public void setCar(Car car) { this.car = car; } public void work() { car.drive(); } }
<?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="benzCar" class="spring.impl.BenzCar"></bean> <bean id="qqCar" class="spring.impl.QQCar"></bean> <bean id="man" class="spring.impl.Man"> <!--通过属性注入的方式注入Car的实例--> <property name="car" ref="benzCar"></property> </bean> </beans>
以上示例中,XML配置文件中属性注入的属性名必须要和Java类中Setter方法对应的属性名一致。而对于字面量的注入,和上面构造器的方式类似,只不过使用的元素名换成了<property>而已,下面仅做展示——
<bean id="man" class="spring.impl.Man"> <property name="str" value="字面量的注入"></property> <property name="list"> <list> <value>集合的字面量注入1</value> <value>集合的字面量注入2</value> </list> </property> </bean>
<bean id="benzCar" class="spring.impl.BenzCar"></bean> <bean id="qqCar" class="spring.impl.QQCar"></bean> <bean id="man" class="spring.impl.Man"> <!--属性注入的方式注入集合--> <property name="cars"> <list> <ref bean="qqCar"></ref> <ref bean="benzCar"></ref> </list> </property> </bean>
3 三种装配方式的混合使用
在同一个应用程序中,Spring常见的这三种装配方式我们可能都会用到,而对于不同的装配方式,他们之间如何实现相互引用从而整合到一起的呢?我们先看看Java配置类的引用问题。试想如果Java配置类中的bean数量过多,我们可能会考虑拆分。在本文的示例中,Man类实例的创建必须通过构造器注入Car的实例,如果把两个实例的产生分成两个配置类,那么在依赖注入的配置类中可以通过@Import注解引入被依赖的配置类——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import spring.facade.Car; import spring.impl.Man; @Configuration @Import(CarConfig.class) //通过@Import注解引入产生Car实例的配置类 public class ManConfig { @Bean public Man work(Car car) { return new Man(car); } }
但是如果Car的实例不是通过Java类配置的,而是通过XML方式配置的方式配置,我们只需通过@ImportResource注解将配置bean的XML文件引入即可,只不过这个时候要保证XML中被依赖的bean的id要和Java配置类中的形参保持一致——
package spring.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportResource; import spring.facade.Car; import spring.impl.Man; @Configuration @ImportResource("classpath:resource/applicationContext.xml") public class ManConfig { @Bean public Man work(Car car) { return new Man(car); } }
而如果bean是采用XML进行装配,如果需要装配的bean过多,我们当然还是会根据业务拆分成不同的配置文件,然后使用<improt>元素进行不同XML配置文件之间的引入,形如: <import resource="classpath:xxx.xml" /> ;而如果要在XML中引入Java配置,只需将Java配置类当成普通的bean在XML中进行声明即可,但是在测试的时候要注意开启组件扫描,因为加载XML配置的上下文对象只会加载XML配置文件中的bean定义,无法让基于Java配置类产生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.xsd"> <!--开启组件扫描,在测试的时候配置类才能向容器中注册类中声明的bean--> <context:component-scan base-package="spring"/> <!--XML中引入Java配置类:将配置类声明为bean--> <bean class="spring.config.CarConfig"></bean> <bean id="man" class="spring.impl.Man"> <constructor-arg ref="laoSiJi"></constructor-arg> </bean> </beans>
最后说一点,不管是Java配置还是XML配置,有个通常的做法就是创建一个比所有配置都更高层次的根配置类/文件,该配置不声明任何的bean,只用来将多个配置组合在一起,从而让配置更易于维护和扩展。好了,以上便是两种bean的装配方式的简单讲解,如有纰漏,欢迎指正,不胜感激。