Spring方法注入的使用与实现原理
1|0一、前言
这几天为了更详细地了解Spring,我开始阅读Spring的官方文档。说实话,之前很少阅读官方文档,就算是读,也是读别人翻译好的。但是最近由于准备春招,需要了解很多知识点的细节,网上几乎搜索不到,只能硬着头皮去读官方文档。虽然我读的这个Spring文档也是中文版的,但是很明显是机翻,十分不通顺,只能对着英文版本,两边对照着看,这个过程很慢,也很吃力。但是这应该是一个程序员必须要经历的过程吧。
在读文档的时候,我读到了一个叫做方法注入的内容,这是我之前学习Spring所没有了解过的。所以,这篇博客就参照文档中的描述,来讲一讲这个方法注入是什么,在什么情况下使用,以及简单谈一谈它的实现原理。
2|0二、正文
2|12.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|22.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|32.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|42.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的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()); }
3|0三、总结
以上大致介绍了一下方法注入的作用,实现原理,以及重点介绍了一下查找方法注入的使用。查找方法注入可以将我们的一个方法,覆盖成为一个去Spring容器中查找特定bean的方法,从而解决单例bean无法依赖多例bean的问题。其实,方法注入能够注入任何方法,而不仅仅是查找方法,但是由于任何方法注入使用的不多,所以这篇博客就不提了,感兴趣的可以自己去Spring文档中了解。最后,若以上描述存在错误或不足,欢迎指正,共同进步。
4|0四、参考
Spring-4.3.21官方文档——方法注入
EOF
本文作者:特务依昂
本文链接:https://www.cnblogs.com/tuyang1129/p/12882500.html