这几天为了更详细地了解 Spring
,我开始阅读 Spring
的官方文档。说实话,之前很少阅读官方文档,就算是读,也是读别人翻译好的。但是最近由于准备春招,需要了解很多知识点的细节,网上几乎搜索不到,只能硬着头皮去读官方文档。虽然我读的这个 Spring
文档也是中文版的,但是很明显是机翻,十分不通顺,只能对着英文版本,两边对照着看,这个过程很慢,也很吃力。但是这应该是一个程序员必须要经历的过程吧。
在读文档的时候,我读到了一个叫做方法注入的内容,这是我之前学习 Spring
所没有了解过的。所以,这篇博客就参照文档中的描述,来讲一讲这个方法注入是什么,在什么情况下使用,以及简单谈一谈它的实现原理。
在说方法注入之前,我们先来考虑一种实际情况,通过实际案例,来引出我们为什么需要方法注入。在我们的 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
提供的 方法注入
机制,就是用了实现和上面类似的功能,但是更加地优雅,侵入性更低。下面我们就来看一看。
什么是方法注入?其实方法注入和 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
容器会自动帮我们替换成第二种形式。这样就可以在不增加耦合的情况下,实现我们的目的。
那方法注入的实现原理是什么呢?我之前说过,方法注入和 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
。下面我就通过实际代码,来演示查找方法注入。
为了演示查找方法注入,我们需要几个具体的类,假设我们有两个类 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方法 }
以上方式,方法注入仍然可以工作。
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
文档中了解。最后,若以上描述存在错误或不足,欢迎指正,共同进步。