Spring方法注入的使用与实现原理
一、前言
这几天为了更详细地了解Spring
,我开始阅读Spring
的官方文档。说实话,之前很少阅读官方文档,就算是读,也是读别人翻译好的。但是最近由于准备春招,需要了解很多知识点的细节,网上几乎搜索不到,只能硬着头皮去读官方文档。虽然我读的这个Spring
文档也是中文版的,但是很明显是机翻,十分不通顺,只能对着英文版本,两边对照着看,这个过程很慢,也很吃力。但是这应该是一个程序员必须要经历的过程吧。
在读文档的时候,我读到了一个叫做方法注入的内容,这是我之前学习Spring
所没有了解过的。所以,这篇博客就参照文档中的描述,来讲一讲这个方法注入是什么,在什么情况下使用,以及简单谈一谈它的实现原理。
二、正文
2.1 问题分析
在说方法注入之前,我们先来考虑一种实际情况,通过实际案例,来引出我们为什么需要方法注入。在我们的Spring
程序中,可以将bean
的依赖关系简单分为四种:
- 单例
bean
依赖单例bean
; - 多例
bean
依赖多例bean
; - 多例
bean
依赖单例bean
; - 单例
bean
依赖多例bean
;
前三种依赖关系都很好解决,Spring
容器会帮我们正确地处理,唯独第四种——单例bean
依赖多例bean
,Spring
容器无法帮我们得到想要的结果。为什么这么说呢?我们可以通过Spring
容器工作的方式来分析。
我们知道,Spring
中bean
的作用域默认是单例的,每一个Spring
容器,只会创建这个类型的一个实例对象,并缓存在容器中,所以对这个bean
的请求,拿到的都是同一个bean
实例。而对于每一个bean
来说,容器只会为它进行一次依赖注入,那就是在创建这个bean
,为它初始化的时候。于是我们可以开始考虑上面说的第四种依赖情况了。假设一个单例bean A
,它依赖于多例bean B
,Spring
容器在创建A
的时候,发现它依赖于B
,且B
是多例的,于是容器会创建一个新的B
,然后将它注入到A
中。A
创建完成后,由于它是单例的,所以会被缓存在容器中。之后,所有访问A
的代码,拿到的都是同一个A
对象。而且,由于容器只会为bean
执行一次依赖注入,所以我们通过A
访问到的B
,永远都是同一个,尽管B
被配置为了多例,但是并没有用。为什么会这样?因为多例的含义是,我们每次向Spring
容器请求多例bean
,都会创建一个新的对象返回。而B
虽然是多例,但是我们是通过A
访问B
,并不是通过容器访问,所以拿到的永远是同一个B
。这时候,单例bean
依赖多例bean
就失败了。
那要如何解决这个问题呢?解决方案应该不难想到。我们可以放弃让Spring
容器为我们注入B
,而是编写一个方法,这个方法直接向Spring
容器请求B
;然后在A
中,每次想要获取B
时,就调用这个方法获取,这样每次获取到的B
就是不一样的了。而且我们这里可以借助ApplicationContextAware
接口,将context
对象(也就是容器)存储在A
中,这样就可以方便地调用getBean
获取B
了。比如,A
的代码可以是这样:
class A implements ApplicationContextAware {
// 记录容器的引用
private ApplicationContext context;
// A依赖的多例对象B
private B b;
/**
* 这是一个回调方法,会在bean创建时被调用
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
this.context = applicationContext;
}
public B getB() {
// 每次获取B时,都向容器申请一个新的B
b = context.getBean(B.class);
return b;
}
}
但是,上面的做法真的好吗?答案显然是不好。Spring
的一个很大的优点就是,它侵入性很低,我们在自己编写的代码中,几乎看不到Spring
的组件,一般只会有一些注解。但是上面的代码中,却直接耦合了Spring
容器,将容器存储在类中,并显式地调用了容器的方法,这不仅增加了Spring
的侵入性,也让我们的代码变得不那么容易管理,也变得不再优雅。而Spring
提供的方法注入机制,就是用了实现和上面类似的功能,但是更加地优雅,侵入性更低。下面我们就来看一看。
2.2 方法注入的功能
什么是方法注入?其实方法注入和AOP
非常类似,AOP
用来对我们定义的方法进行增强,而方法注入,则是用来覆盖我们定义的方法。通过Spring
提供的方法注入机制,我们可以对类中定义的方法进行替换,比如说上面的getB
方法,正常情况下,它的实现应该是这样的:
public B getB() {
return b;
}
但是,为了实现每次获取B
时,能够让Spring
容器创建一个新的B
,我们在上面的代码中将它修改成了下面这个样子:
public B getB() {
// 每次获取B时,都向容器申请一个新的B
b = context.getBean(B.class);
return b;
}
但是,我们之前也说过,这种方式并不好,因为这直接依赖于Spring
容器,增加了耦合性。而方法注入可以帮助我们解决这一点。方法注入能帮我们完成上面的替换,而且这种替换是隐式地,由Spring
容器自动帮我们替换。我们并不需要修改编写代码的方式,仍然可以将getB
方法写成第一种形式,而Spring
容器会自动帮我们替换成第二种形式。这样就可以在不增加耦合的情况下,实现我们的目的。
2.3 方法注入的实现原理
那方法注入的实现原理是什么呢?我之前说过,方法注入和AOP
类似,不仅仅是功能类似,实际上它们的实现方式也是一样的。方法注入的实现原理,就是通过CGLib的动态代理。关于AOP
的实现原理,可以参考我的这篇博客:浅析Spring中AOP的实现原理——动态代理。
如果我们为一个类的方法,配置了方法注入,那么在Spring
容器创建这个类的对象时,实际上创建的是一个代理对象。Spring
会使用CGLib
操作这个类的字节码,生成类的一个子类,然后覆盖需要修改的那个方法,而在创建对象时,创建的就是这个子类(代理类)的对象。而具体覆盖成什么样子,取决于我们的配置。比如说Spring
提供了一个具体的方法注入机制——查找方法注入,这种方法注入,可以将方法替换为一个查找方法,它的功能就是去Spring
容器中获取一个特定的Bean
,而获取哪一个bean
,取决于方法的返回值以及我们指定的bean
名称。
比如说,上面的getB
方法,如果我们对它使用了查找方法注入,那么Spring
容器会使用CGLib
生成A
类的一个子类(代理类),覆盖A
类的getB
方法,由于getB
方法的返回值是B
类型,于是这个方法的功能就变成了去Spring
容器中获取一个B
,当然,我们也可以通过bean
的名称,指定这个方法查找的bean
。下面我就通过实际代码,来演示查找方法注入。
2.4 查找方法注入的使用
(一)通过xml配置
为了演示查找方法注入,我们需要几个具体的类,假设我们有两个类User
和Car
,而User
依赖于Car
,它们的定义如下:
public class User {
private String name;
private int age;
// 依赖于car
private Car car;
// 为这个方法进行注入
public Car getCar() {
return car;
}
// 省略其他setter和getter,以及toString方法
}
public class Car {
private int speed;
private double price;
// 省略setter和getter,以及toString方法
}
好,现在有了这两个类,我们可以开始进行方法注入了。我们模拟之前说过的依赖关系——单例bean
依赖于多例bean
,将User
配置为单例,而将User
依赖的Car
配置为多例。则配置文件如下:
<!-- 将user的作用域定义为singleton -->
<bean id="user" class="cn.tewuyiang.pojo.User" scope="singleton">
<property name="name" value="aaa" />
<property name="age" value="28" />
<!--
配置查找方法注入,替换getCar方法,让他成为从spring容器中查找car的一个工厂方法
name指定了需要进行方法注入的方法,而bean则指定了这个方法被覆盖后,是用来查找哪个bean的
-->
<lookup-method name="getCar" bean="car" />
</bean>
<!-- 将car的作用域定义为prototype -->
<bean id="car" class="cn.tewuyiang.pojo.Car" scope="prototype">
<property name="price" value="9999.35" />
<property name="speed" value="100" />
</bean>
好,到此为止,我们就配置完成了,下面就该测试一下通过user
的getCar
方法拿到的多个car
,是不是不相同。如果方法注入没有生效,那么按理来讲,我们调用getCar
方法返回的应该是null
,因为我们并没有配置将car的值注入user中。但是如果方法注入生效,那么我们通过getCar
,就可以拿到car
对象,因为它将去Spring
容器中获取,而且每次获取到的都不是同一个。测试方法如下:
@Test
public void testXML() throws InterruptedException {
// 创建Spring容器
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
// 获取User对象
User user = context.getBean(User.class);
// 多次调用getCar方法,获取多个car
Car c1 = user.getCar();
Car c2 = user.getCar();
Car c3 = user.getCar();
// 分别输出car的hash值,看是否相等,以此判断是否是同一个对象
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
// 输出user这个bean所属类型的父类
System.out.println(user.getClass().getSuperclass());
}
上面的测试逻辑应该很好理解,除了最后一句,为什么需要输出user
这个bean
所属类型的父类。因为我前面说过,方法注入通过CGLib
动态代理实现,而CGLib
动态代理的原理就是生成类的一个子类。我们为User
类使用了方法注入,所以我们拿到的user
这个bean
,应该是一个代理bean
,并且它的类型是User
的子类。所以我们输出这个bean
的父类,来判断是否和我们之前说的一样。输出结果如下:
1392906938
708890004
255944888
class cn.tewuyiang.pojo.User // 父类果然是User
可以看到,我们果然能够通过getCar
方法,获取到bean
,并且每一次获取到的都不是同一个,因为hashcode
不相等。同时,user
这个bean
的父类型果然是User
,说明user
这个bean
确实是CGLib
生成的一个代理bean
。到此,也就证明了我们之前的叙述。
(二)通过注解配置
上面通过xml
的配置方式,大致了解了查找方法注入的使用,下面我们再来看看使用注解,如何实现。其实使用注解的方式更加简单,我们只需要在方法上使用@Lookup
注解即可,User
和Car
的配置如下:
@Component
public class User {
private String name;
private int age;
private Car car;
// 使用Lookup注解,告诉Spring这个方法需要使用查找方法注入
// 这里直接使用@Lookup,则Spring将会依据方法返回值
// 将它覆盖为一个在Spring容器中获取Car这个类型的bean的方法
// 但是也可以指定需要获取的bean的名字,如:@Lookup("car")
// 此时,名字为car的bean,类型必须与方法的返回值类型一致
@Lookup
public Car getCar() {
return car;
}
// 省略其他setter和getter,以及toString方法
}
@Component
@Scope("prototype") // 声明为多例
public class Car {
private int speed;
private double price;
// 省略setter和getter,以及toString方法
}
可以看到,通过注解配置方法注入要简单的多,只需要通过一个@Lookup
注解即可实现。测试方法与之前类似,结果也一样,我就不贴出来了。
(三)为抽象方法使用方法注入
实际上,方法注入还可以应用于抽象方法。既然方法注入的目的是替换原来的方法,那么原来的方法是否有实现,也就不重要了。所以方法注入也能用在抽象方法上面。但是有人可能会想一个问题:抽象方法只能在抽象类中,那这个类被定义为抽象类了,Spring
容器如何为它创建对象呢?我们之前说过,使用了方法注入的类,Spring
会使用CGLib
生成它的一个代理类(子类),Spring
创建的是这个代理类的对象,而不会去创建源类的对象,所以它是不是抽象的并不影响工作。如果配置了方法注入的类是一个抽象类,则方法注入机制的实现,就是去实现它的抽象方法。我们将User
类改为抽象,如下所示:
// 就算为抽象类使用了@Component,Spring容器在创建bean时也会跳过它
@Component
public abstract class User {
private String name;
private int age;
private Car car;
// 将getCar声明为抽象方法,它将会被代理类实现
@Lookup
public abstract Car getCar();
// 省略其他setter和getter,以及toString方法
}
以上方式,方法注入仍然可以工作。
(四)final方法和private方法无法使用方法注入
CGLib
实现动态代理的方法是创建一个子类,然后重写父类的方法,从而实现代理。但是我们知道,final
方法和private
方法是无法被子类重写的。这也就意味着,如果我们为一个final
方法或者一个private
方法配置了方法注入,那生成的代理对象中,这个方法还是原来那个,并没有被重写,比如像下面这样:
@Component
public class User {
private String name;
private int age;
private Car car;
// 方法声明为final,无法被覆盖,代理类中的getCar还是和下面一样
@Lookup
public final Car getCar() {
return car;
}
// 省略其他setter和getter,以及toString方法
}
我们依旧使用下面的测试方法,但是,在调用c1.hashCode
方法时,抛出了空指针异常。说明getCar
方法并没有被覆盖,还是直接返回了car
这个成员变量。但是由于我们并没有为user
注入car
,所以car == null
。
@Test
public void testConfig() throws InterruptedException {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AutoConfig.class);
User user = context.getBean(User.class);
Car c1 = user.getCar();
Car c2 = user.getCar();
Car c3 = user.getCar();
// 运行到这里,抛出空指针异常
System.out.println(c1.hashCode());
System.out.println(c2.hashCode());
System.out.println(c3.hashCode());
user.spCar();
user.spCar();
user.spCar();
System.out.println(user.getClass().getSuperclass());
}
三、总结
以上大致介绍了一下方法注入的作用,实现原理,以及重点介绍了一下查找方法注入的使用。查找方法注入可以将我们的一个方法,覆盖成为一个去Spring
容器中查找特定bean
的方法,从而解决单例bean
无法依赖多例bean
的问题。其实,方法注入能够注入任何方法,而不仅仅是查找方法,但是由于任何方法注入使用的不多,所以这篇博客就不提了,感兴趣的可以自己去Spring
文档中了解。最后,若以上描述存在错误或不足,欢迎指正,共同进步。