简介
在前面的文章里,我已经对依赖注入的基本概念做了一个介绍。我们已经知道了依赖注入的意义和目的。但是在牵涉到具体实现的时候,我们有好几种不同的选择,其中就有自动关联(autowire),java代码关联(java config)以及传统的xml文件配置关联。本文针对这几种形式结合具体的示例做一个讨论和总结。
问题场景介绍
假设我们有如下的一个问题,在下图中,我们有一个Service接口,定义特定的业务逻辑。在一个具体的实现中,BusinessService需要使用到第三方的数据,这里用接口DAO表示。为了保证松耦合和方便测试,有一个典型的实现BusinessDAO。
当然,因为根据我们具体情况的需要,我们完全可以定义其他的实现。现在的问题是,如果需要采用依赖注入的方式让上述的BusinessService, BusinessDAO一起工作,同时在我们的业务代码里又没有对它们的耦合,具体该有几种详细的实现呢?具体实现的细节改如何呢?我们就一一看过来。
XML配置文件关联
从最传统的这种方式开始。在上文中我们也已经举出过几个示例。一般我们将一个简单java对象定义为一个bean。而我们常用代码里对象之间的依赖关系通过构造函数或者属性方法的方式注入设置。在这种方式里,每个java对象就是一个单独定义的单元,从它们自身的定义里更多的情况下它们对于外界的依赖是一个抽象的接口或者抽象基类。而通过我们定义的关联关系,使得它们在最终被使用和运行的时候,各种具体的依赖关系已经构造好了。
现在需要的就是按照前面图中定义的关系来构造示例。首先定义的Service接口如下:
package com.yunzero;
public interface Service {
void doBusiness();
}
对应的一个实现BusinessService如下:
package com.yunzero.impl;
import com.yunzero.DAO;
import com.yunzero.Service;
public class BusinessService implements Service {
private DAO dao;
public void setDao(DAO dao) {
this.dao = dao;
}
@Override
public void doBusiness() {
System.out.println("Business impl in business service.");
System.out.println(dao.getId());
}
}
这部分的代码其实就是一个通过属性注入的方式产生了一个对抽象接口DAO的依赖。而DAO的实现如下:
package com.yunzero;
public interface DAO {
int getId();
}
现在需要再定义的就是DAO的一个实现BusinessDAO:
package com.yunzero.impl;
import com.yunzero.DAO;
public class BusinessDAO implements DAO {
@Override
public int getId() {
return 0;
}
}
有了这几步实现之后,剩下的就是将它们给拼在一起形成一个完整的应用。采用xml关联的方式该怎么来实现呢?我们先定义一个bean配置文件sampleContext.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="service" class="com.yunzero.impl.BusinessService"> <property name="dao" ref="dao"/> </bean> <bean id="dao" class="com.yunzero.impl.BusinessDAO"/> </beans>
我们来看它详细的定义。首先最外层的是<beans> 节点,然后有一个定义id为service的bean。它对应的具体实现是BusinessService。留意到前面代码里BusinessService对DAO接口的属性设置依赖。那里定义了一个setDAO的方法。在这里可以通过设置<property>属性将真正的实现关联进来。在这里是ref引用的dao。而id为dao的bean则是BusinessDAO的具体实例。这样,通过这么一个定义各个需要的bean以及它们之间的依赖,它们的依赖注入配置基础就弄好了。在实际代码中要构造出这些定义好的对象则比较简单,一个使用它们的代码实例如下:
package com.yunzero;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class App
{
public static void main( String[] args )
{
AbstractApplicationContext ctx = new ClassPathXmlApplicationContext("sampleContext.xml");
Service service = ctx.getBean(Service.class);
service.doBusiness();
ctx.close();
}
}
上述代码里使用ApplicationContext来获取对象定义的容器。这里获取Service对象采用的是ctx.getBean(class)的方式。以前也有直接获取bean名字,然后再做一个强制类型转换的方式。和那种比起来,这种方式的代码更加简洁一些。
如果这个时候运行代码,输出的结果如下:
Mar 30, 2015 10:02:21 PM org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@7106e68e: startup date [Mon Mar 30 22:02:21 CST 2015]; root of context hierarchy
Mar 30, 2015 10:02:21 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [sampleContext.xml]
Business impl in business service.
0
Mar 30, 2015 10:02:22 PM org.springframework.context.support.ClassPathXmlApplicationContext doClose
INFO: Closing org.springframework.context.support.ClassPathXmlApplicationContext@7106e68e: startup date [Mon Mar 30 22:02:21 CST 2015]; root of context hierarchy
完整实现的详细代码会放在后面的附件里。
上述的这种方式可以说是最常用办法之一。它的特点在于非常的简单直观。任何需要定义以及依赖的bean对象都会通过这种方式定义出来。当然,它也有一些不足的地方。如果我们后续的代码作一些变动,这里依赖定义的都是字符串,比较容易出错,而且有时候代码改动之后还要改配置,整个过程显得比较繁琐。
自动关联
正因为有上述的问题,spring引入了另外一种注入的方式。这种方式相对来说更加简单一些。可以说实现了零配置。以前面的示例为基础,我们定义的接口还是没有任何变化,但是具体针对接口的实现。它们稍微有一点变化。比如BusinessService和BusinessDAO的实现分别如下:
package com.yunzero.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.yunzero.DAO;
import com.yunzero.Service;
@Component
public class BusinessService implements Service {
private DAO dao;
@Autowired
public void setDao(DAO dao) {
this.dao = dao;
}
@Override
public void doBusiness() {
System.out.println("Business impl in business service.");
System.out.println(dao.getId());
}
}
package com.yunzero.impl;
import org.springframework.stereotype.Component;
import com.yunzero.DAO;
@Component
public class BusinessDAO implements DAO {
@Override
public int getId() {
return 0;
}
}
这些代码和前面的实现基本上一样,除了一个地方,就是这两个实现的类定义的地方多了一个@Component的标注。这就相当于告诉spring这是一个已经定义好的bean。我们也可以在@Component里面增加对bean的自定义信息。这样,对于一个定义的抽象接口,@Component相当于标注这个类就是对应该接口的一个注入实现。那么,对于那些还依赖其他bean的情况呢?
在前面的代码里有一个DAO的依赖,而且在对dao元素的setDAO()方法里有一个标注@Autowired,这就相当于将一个DAO的实现和BusinessDAO关联起来了。而怎么找这个DAO的具体实现呢?和前面的一样,有一个BusinessDAO的实现并且它也被标注为@Component。
有了这些bean和它们之间关联的定义了,spring里该怎么使用它们呢?虽然我们定义了这些component以及它们之间的关联关系,但是spring并不是那么聪明的就知道。它需要去扫描所有这些类才知道哪些类是哪些接口的实现以及关联了哪些类。所以和前面的xml文件配置类似,它需要一个指定配置信息的地方。为了省略硬性的xml文件配置,可以定义一个专门用来扫描的类,比如这里我们可以定义一个类ServiceConfig:
package com.yunzero.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages="com.yunzero")
public class ServiceConfig {
}
它的作用相当于替换了前面的配置文件。这个类其实本身并不实现任何特殊的业务逻辑。仅仅相当于起到一个类似于配置文件的作用。在实际项目中应当尽量将这类文件放在一个单独的包里。另外,在ServiceConfig的实现里,它有一个@ComponentScan的标注。这个标注用来指示spring扫描包的目录。一般来说spring默认扫描该类所在的包以及下面所包含的子包。这里采用自定义指定包路径的方式。
经过这些修改,我们使用自动配置的运行代码如下:
package com.yunzero;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import com.yunzero.config.ServiceConfig;
public class App
{
public static void main( String[] args )
{
AbstractApplicationContext ctx = new AnnotationConfigApplicationContext(ServiceConfig.class);
Service service = ctx.getBean(Service.class);
service.doBusiness();
ctx.close();
}
}
采用这种方法的要点在于将配置类ServiceConfig.class作为参数提供给AnnotationConfigApplicationContext。其他的使用方式则基本上一样。
这样,采用自动化配置的方式就完成了。总的来说,这种方式就是将替换抽象接口的bean采用@Component标注好,然后将抽象依赖之间的关联用@Autowired标注好。最后用一个配置类告诉spring去哪些包从上往下的去扫描。
Java配置代码关联
除了上述的两种方法以外,还要一种办法就是配置代码关联。采用自动化配置的方法固然好,它可以做到不用写任何配置文件,但是也有一个问题。就是如果我们需要引用某些第三方的类库时,由于那些库的源代码并不在我们的掌握之中,它们并没有作那些@Component标注,这个时候再采用原来的办法就不可行了。如果我们希望能够解决这个问题的同时还能够尽可能少的使用配置文件,可以借鉴一部分前面ServiceConfig类的方法。我们这里定义一个如下的SampleConfig类:
package com.yunzero.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.yunzero.DAO;
import com.yunzero.Service;
import com.yunzero.impl.BusinessDAO;
import com.yunzero.impl.BusinessService;
@Configuration
public class SampleConfig {
@Bean
public Service businessService() {
BusinessService service = new BusinessService();
service.setDao(businessDAO());
return new BusinessService();
}
@Bean
public DAO businessDAO() {
return new BusinessDAO();
}
}
注意到,这里这个类的定义没有添加@ComponentScan标注。这里只是针对每个Bean的创建采用专门@Bean修饰的方法单独拎了出来。这里的方法相当于定义了一系列的工厂方法。对于每个bean的创建就通过返回对应的对象。@Bean在这里起到一个标注和定义bean的作用,而且创建的这个bean的id和定义的方法名相同。
具体对这种情况的使用和前面的一样,这里就不再赘述。
总结
采用xml配置文件、自动配置和java 配置类是最主要的几种注入方法。通常来说,采用自动配置的方式最简单省事,一方面因为它只需要在代码里添加一点标注,而且不需要去定义任何配置文件,另外,因为这些标注是和代码关联的,它具有强类型相关性,能够更快的检测的错误,这样可以避免在配置文件中产生的错误。 当然,从个人的角度来说,采用xml文件的方式可以实现一个更加理想的配置和代码分离,保证代码编译之后基本上不需要做任何改动,可以做到代码和配置的完全隔离。
参考材料
http://www.amazon.com/Spring-Action-Craig-Walls/dp/161729120X/ref=sr_1_1?s=books&ie=UTF8&qid=1427984830&sr=1-1&keywords=spring+in+action+5th+edition