转载

Spring方法注入的使用与实现原理

一、前言

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

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

二、正文

2.1 问题分析

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

  1. 单例 bean 依赖单例 bean ;
  2. 多例 bean 依赖多例 bean ;
  3. 多例 bean 依赖单例 bean ;
  4. 单例 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 文档中了解。最后,若以上描述存在错误或不足,欢迎指正,共同进步。

四、参考

  • Spring-4.3.21官方文档——方法注入
原文  http://www.cnblogs.com/tuyang1129/p/12882500.html
正文到此结束
Loading...