一、前言

  这几天为了更详细地了解Spring,我开始阅读Spring的官方文档。说实话,之前很少阅读官方文档,就算是读,也是读别人翻译好的。但是最近由于准备春招,需要了解很多知识点的细节,网上几乎搜索不到,只能硬着头皮去读官方文档。虽然我读的这个Spring文档也是中文版的,但是很明显是机翻,十分不通顺,只能对着英文版本,两边对照着看,这个过程很慢,也很吃力。但是这应该是一个程序员必须要经历的过程吧。

  在读文档的时候,我读到了一个叫做方法注入的内容,这是我之前学习Spring所没有了解过的。所以,这篇博客就参照文档中的描述,来讲一讲这个方法注入是什么,在什么情况下使用,以及简单谈一谈它的实现原理。

二、正文

2.1 问题分析

  在说方法注入之前,我们先来考虑一种实际情况,通过实际案例,来引出我们为什么需要方法注入。在我们的Spring程序中,可以将bean的依赖关系简单分为四种:

  1. 单例bean依赖单例bean
  2. 多例bean依赖多例bean
  3. 多例bean依赖单例bean
  4. 单例bean依赖多例bean

  前三种依赖关系都很好解决,Spring容器会帮我们正确地处理,唯独第四种——单例bean依赖多例beanSpring容器无法帮我们得到想要的结果。为什么这么说呢?我们可以通过Spring容器工作的方式来分析。

  我们知道,Springbean的作用域默认是单例的,每一个Spring容器,只会创建这个类型的一个实例对象,并缓存在容器中,所以对这个bean的请求,拿到的都是同一个bean实例。而对于每一个bean来说,容器只会为它进行一次依赖注入,那就是在创建这个bean,为它初始化的时候。于是我们可以开始考虑上面说的第四种依赖情况了。假设一个单例bean A,它依赖于多例bean BSpring容器在创建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配置

  为了演示查找方法注入,我们需要几个具体的类,假设我们有两个类UserCar,而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>

  好,到此为止,我们就配置完成了,下面就该测试一下通过usergetCar方法拿到的多个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注解即可,UserCar的配置如下:

@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文档中了解。最后,若以上描述存在错误或不足,欢迎指正,共同进步。

四、参考

Spring方法注入的使用与实现原理的更多相关文章

  1. spring 方法注入、方法替换

    spring 提供了很多的注入方式,set注入.构造注入.p命名空间.c命名空间.字段注入等等,这些没啥可说的了. 方法注入 因为开发时我需要spring管理一个实例,但是这个实例并非单例,应该每一次 ...

  2. spring 方法注入

    package com.haut.grain.junit.test; public  class Command {private Object state;public void setState( ...

  3. Spring、Spring依赖注入与编码剖析Spring依赖注入的原理

    Spring依赖注入 新建PersonIDao 和PersonDao底实现Save方法: public interface PersonIDao { public void save(); } pub ...

  4. 模拟spring框架注入实现原理

    这个我是参见了别人的一些东西,不是原创! 定义一些抽象的方法: package com.huxin.springinject.dao; public interface Person { public ...

  5. Spring揭秘 读书笔记 四----方法注入

    我们知道,拥有prototype类型scope的bean,在请求方每次向容器请求该类型对象的时候,容器都会返回一个全新的该对象实例. 我们看下面的例子: public class MockNewsPe ...

  6. Spring依赖注入原理分析

    在分析原理之前我们先回顾下依赖注入的概念: 我们常提起的依赖注入(Dependency Injection)和控制反转(Inversion of Control)是同一个概念.具体含义是:当某个角色( ...

  7. Spring第六弹—-依赖注入之使用构造器注入与使用属性setter方法注入

    所谓依赖注入就是指:在运行期,由外部容器动态地将依赖对象注入到组件中. 使用构造器注入   1 2 3 4 <constructor-arg index=“0” type=“java.lang. ...

  8. (转)编码剖析Spring依赖注入的原理

    http://blog.csdn.net/yerenyuan_pku/article/details/52834561 Spring的依赖注入 前面我们就已经讲过所谓依赖注入就是指:在运行期,由外部容 ...

  9. Spring依赖注入:@Autowired,@Resource和@Inject区别与实现原理

    一.spring依赖注入使用方式 @Autowired是spring框架提供的实现依赖注入的注解,主要支持在set方法,field,构造函数中完成bean注入,注入方式为通过类型查找bean,即byT ...

随机推荐

  1. stand up meeting 12-8

    根据计划今天项目组成员和travis老师毕然同学进行了最后一次关于design和feature的确认meeting. 项目design和UI的改动较大,feature改动较小,需对UI进行重新整合,对 ...

  2. B - How Many Equations Can You Find dfs

    Now give you an string which only contains 0, 1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9.You are asked to add the sig ...

  3. div3--D - Distinct Characters Queries

    题目链接:https://codeforces.com/contest/1234/problem/D 题目大意: 对于给定的字符串,给出n个查询,查询时输入3个数啊,a,b,c,如果说a==1,则将位 ...

  4. 如何在非 sudo 用户下运行 docker 命令?

    当我们在一台 Linux 系统中安装了 Docker 后, 有时候会遇到下面这样的错误, 我们在运行 docker 的命令时必须加上 sudo, 例如: sudo docker ps, 但是我们其实更 ...

  5. Python 实用冷门知识整理

    1.print 打印带有颜色的信息 大家知道 Python 中的信息打印函数 print,一般我们会使用它打印一些东西,作为一个简单调试. 但是你知道么,这个 Print 打印出来的字体颜色是可以设置 ...

  6. JVM致命错误日志详解

    目录 文件描述 文件位置 文件头 错误信息记录 JVM运行信息 崩溃原因 错误信息 线程描述 线程信息 信号信息 计数器信息 机器指令 内存映射信息 线程堆栈 其他信息 进程描述 线程列表 虚拟机状态 ...

  7. 0day笔记(1)PE文件格式与虚拟文件内存的映射

    PE文件格式 PE 文件格式把可执行文件分成若干个数据节(section),不同的资源被存放在不同的节中. 一个典型的 PE 文件中包含的节如下: .text 存放着二进制的机器代码 .data 初始 ...

  8. swoole学习--登录模块

    使用swoole+thinkphp6.0+redis 结合开发的登录模块,做完之后有几点感悟: 1.不要相信任务数据,包括请求的外部接口,特别是超时者部分,尽可能的交给task完成. 2.原来可以在入 ...

  9. ERROR 2003 (HY000): Can't connect to MySQL server on '192.168.33.10' (111) 解决方法

    谷歌了一下之后,原来是在mysql的my.cnf中有下面一段代码: # Instead of skip-networking the default is now to listen only on ...

  10. 探索ORACLE之ASM概念(完整版)

    探索ORACLE之ASM概念(完整版) 本文出自https://www.jb51.net/article/43527.htm ASM是Oracle 10g R2中为了简化Oracle数据库的管理而推出 ...