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


四、参考

posted @ 2020-05-13 15:14  特务依昂  阅读(3962)  评论(0编辑  收藏  举报