转载

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

一、前言

  这几天为了更详细地了解 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-4.3.21官方文档——方法注入
原文  http://www.cnblogs.com/tuyang1129/p/12882500.html
正文到此结束
Loading...