所谓依赖注入就是通过Spring的IOC容器来管理对象的创建、销毁以及对象之间的依赖关系。在编程中,我们经常会遇到A类依赖B类的情况,这时我们就需要在A类中声明一个B类的引用,然后在程序中new一个B类的对象,让B类引用指向B类对象的内存地址,而依赖注入的出现简化了这种维护依赖关系的频繁操作,改由Spring的IOC容器来进行维护,在IOC容器中注册过的Bean实例会在Spring容器初始化时就注入到某个类的依赖中,而这种依赖注入的操作是使用Java的反射机制来实现的。
所谓控制反转就是IOC容器的依赖注入让接口主动获取实现变成了容器主动将实现注入到接口中,实现了控制关系的反转。我们在开发中,经常会遇到当前模块调用其他模块的接口并new一个具体实现类的情况,这样就很容易让当前模块与具体的实现产生了耦合,不符合面向接口编程和面向抽象编程的思想。而且,如果我们需要更换接口的实现时,可能需要在系统中修改很多地方,这增大了出错的风险,也不利于后期的系统维护。而依赖注入的出现则刚好解决了这些问题,它实现了系统模块之间的松耦合,满足了面向接口编程和面向抽象编程的思想。
public interface HelloWorld {
void say();
}
@Service
public class HelloWorldEnglishImpl implements HelloWorld {
@Override
public void say() {
System.out.println("Hello World");
}
}
@Component
public class HelloWorldTest {
private HelloWorld helloWorld;
public void setHelloWorld(HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}
public void doSay(){
helloWorld.say();
}
}
class Test{
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring/applicationContext-test.xml");
HelloWorldTest test = (HelloWorldTest) context.getBean("helloWorldTest");
test.doSay();
//打印结果:Hello World
}
}
附:@Component、@Repository、@Service、@Controller的基本含义
@Repository:用于注解持久层
@Service:用于注解业务层
@Controller:用于注解控制层
@Component:如果不属于上面三层的,则可以使用这个注解
首先,更改配置文件中的测试bean使用byName进行自动装配,其他不变,如下:
此时,运行main方法报错,这时我们将测试类中的setHelloWorld方法名更改为setHelloWorldEnglish,再执行main方法成功,如下:
public void setHelloWorldEnglish(HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}
很多人以为,byName的自动装配是跟类中的属性有关,其实不是,使用byName进行自动装配时,是利用Java的反射机制获取自动装配类中的set方法名,去掉set后将其首字母小写再到IOC容器中查找是否有对应的beanId,如果有则查看该bean的类型与set方法的参数类型是否匹配,匹配上了则调用set方法进行依赖注入。因此,byName的自动装配跟set方法名和参数类型有关,跟属性名无关。可以尝试将setHelloWorldEnglish中的H小写,如:sethelloWorldEnglish,其他不变,依然可以注入成功。也可以任意更改set方法名和beanId标识进行自动装配测试。
现在我们将配置文件中beanId为helloWorldEnglish的bean注释掉,再运行发现报错,但我们将测试类中的set方法名更改为setHelloWorldEnglishImpl之后再运行,发现运行成功,如下:
public void setHelloWorldEnglishImpl(HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}
在上面的配置中,我们虽然没有显式的声明HelloWorldEnglishImpl的bean实例,但我们配置了扫描test包,IOC容器在加载时会将此包中的所有类都扫描到IOC容器中,当有bean实例使用byName进行自动装配时,它将执行byName的装配逻辑。不同的是由于没有显式的声明beanId,因此所有注册到IOC容器中的bean实例的Id都将使用类名的首字母小写,即此时实现类HelloWorldEnglishImpl的默认beanId为helloWorldEnglishImpl,所以当我们将set方法的方法名更改为setHelloWorldEnglishImpl时就可以注入成功了。
现在我们将测试类的bean在配置文件中也注释掉,然后在测试类的setHelloWorldEnglishImpl方法上添加@Resource注解,这代表为此set方法执行byName自动装配策略,如下:
@Resource
public void setHelloWorldEnglishImpl(HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}
但是如果我们将 setHelloWorldEnglishImpl方法更改为setHelloWorldEnglish之后发现依然可以注入成功,这是由于@Resource同时也支持byType自动装配策略,当使用byName策略无法进行自动装配时,它将使用byType策略。现在我们可以将@Resource注解直接注解到属性上,如下:
@Resource
private HelloWorld helloWorld;
/*@Resource
public void setHelloWorldEnglish(HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}*/
运行main方法,发现依然可以注入成功 。我们虽然注释掉了set方法,但程序在运行时,Java的反射机制会自动的帮我们生成一个名为setHelloWorld的方法,但HelloWorldEnglishImpl的默认beanId为helloWorldEnglishImpl与helloWorld不匹配,这时,@Resource将使用byType进行自动装配。我们这时可以将注释掉的beanId为的helloWorldEnglish的注释去掉,然后运行main方法,如下:
这时运行报错,因为此时@Resource既无法使用byName进行自动装配,也无法使用byType进行自动装配,至于为何,看完byType的装配逻辑你就明白了。
这时我们将beanId修改为helloWorld,其他不变,发现注入成功,此时@Resource使用的是byName注入,如下:
然后,我们再将beanId修改为helloWorldEnglishImpl,其他不变,此时@Resource使用的是byType注入,如下:
更改配置文件中的测试bean使用byType进行自动装配,其他不变,运行main方法注入成功,如下:
使用byType进行自动装配时,是利用Java的反射机制获取测试类中的set方法的参数类型,寻找此接口类型的实现类或子类,找到后将其首字母小写与IOC容器中的beanId进行匹配,匹配成功后则调用set方法进行依赖注入。因此,byType自动装配与方法名无关,与属性名无关,与set方法的参数类型有关。如果将beanId修改为helloWorldEnglish或者其它则注入失败。
现在我们将配置文件中的bean全部注释掉,还有set方法也注释掉,然后在属性上直接添加Autowired注解,运行main方法发现注入成功,如下:
@Autowired
private HelloWorld helloWorld;
/*public void setHelloWorldEnglish(HelloWorld helloWorld) {
this.helloWorld = helloWorld;
}*/
此时,接口参数的实现类的首字母小写与IOC容器中默认beanId为helloWorldEnglishImpl的bean相匹配,因此注入成功。那么@Autowired注解是否支持byName装配呢?现在我们将配置文件中的beanId为helloWorldEnglishImpl的注释放开,然后将beanId修改为helloWorld,运行main方法发现注入成功,如下:
这时@Autowired注解使用的就是byName装配,由于HelloWorld接口的实现类 HelloWorldEnglishImpl的类名首字母小写在IOC容器中找不到对应的bean,它就会使用byName策略,Java的反射机制生成的set方法名小写为helloWorld,在IOC容器中可以找到对应的bean实例,则byName装配成功。如果我们将beanId修改为helloWorldEnglish,运行main方法,则注入失败,因为此时@Autowired注解无论使用byType还是byName都无法找到对应的bean实例。
如果接口有多个实现类怎么办,答案肯定是报错,因为Spring不知道你要给接口注入哪个实现类。这时我们可以在set方法上添加@Qualifier注解来解决这个问题。(先把配置文件中的配置注释掉,然后再添加一个接口实现类,最后在测试类的属性上添加@Qualifier注解)如下:
@Service
public class HelloWorldChineseImpl implements HelloWorld{
@Override
public void say() {
System.out.println("你好世界");
}
}
@Qualifier("helloWorldEnglishImpl")
@Autowired
private HelloWorld helloWorld;
@Qualifier注解是为了告诉Spring容器在自动装配时如果有多个实现类应该使用哪个实现类进行依赖注入。
@Resource是jdk提供的注解,它默认使用byName进行装配,byName无法装配则使用byType;
@Autowired是spring提供的注解,@Autowired默认使用byType进行装配,byType无法装配则使用byName,如果接口有多个实现类,需要配合@Qualifier注解使用。
参考链接:
1、Spring Ioc、依赖注入原理
2、Spring自动装配之byName和byType【Spring入门】
3、Spring源码学习--@Autowired注解和启动自动扫描的三种方式
4、为什么使用IOC容器