BeanFactory的XML之旅

偷梁换柱之术

在学习以下内容之前,先提一下有关bean的scope的使用“陷阱”,特别是prototype在容器中的使用,以此引出本节将要介绍的Spring容器较为独特的功能特性:方法注入(Method Injection)以及方法替换(Method Replacement)。

我们知道,拥有prototype类型scope的bean,在请求方每次向容器请求该类型对象的时候,容器都会返回一个全新的该对象实例。为了简化问题的叙述,我们直接将FXNews系统中的FXNewsBean定义注册到容器中,并将其scope设置为prototype。因为它是有状态的类型,每条新闻都应该是新的独立个体;同时,我们给出MockNewsPersister类,使其实现IFXNewsPersister接口,以模拟注入FXNewsBean实例后的情况。这样,我们就有了下方代码所展示的类声明和相关配置。

    public class MockNewsPersister implements IFXNewsPersister {
        private FXNewsBean newsBean;
    
        public void persistNews(FXNewsBean bean) {
            persistNewes();
        }
    
        public void persistNews() {
            System.out.println("persist bean:" + getNewsBean());
        }
    
        public FXNewsBean getNewsBean() {
            return newsBean;
        }
    
        public void setNewsBean(FXNewsBean newsBean) {
            this.newsBean = newsBean;
        }
    }
    

配置为

    
    <bean id="newsBean" class="..domain.FXNewsBean" singleton="false">
    </bean>
      
    <bean id="mockPersister" class="..impl.MockNewsPersister">
    	<property name="newsBean">
      		<ref bean="newsBean"/>
    	</property>
    </bean>

当多次调用MockNewsPersister的persistNews时,你猜会得到什么结果?如下代码可以帮助我们揭开答案:

    
    BeanFactory container = new XmlBeanFactory(new ClassPathResource("..")); 
    MockNewsPersister persister = (MockNewsPersister)container.getBean("mockPersister"); 
    persister.persistNews();
    persister.persistNews();
    

输出:

   persist bean:..domain.FXNewsBean@1662dc8
    persist bean:..domain.FXNewsBean@1662dc8

从输出看,对象实例是相同的,而这与我们的初衷是相悖的。因为每次调用persistNews都会调用getNewsBean()方法并返回一个FXNewsBean实例,而FXNewsBean实例是prototype类型的,因此每次不是应该输出不同的对象实例嘛

好了,问题实际上不是出在FXNewsBean的scope类型是否是prototype的, 而是出在实例的取得方式上面。虽然FXNewsBean拥有prototype类型的scope,但当容器将一个FXNewsBean的实例注入MockNewsPersister之后,MockNewsPersister就会一直持有这个FXNewsBean实例的引用。虽然每次输出都调用了getNewsBean()方法并返回了FXNewsBean的实例,但实际上每次返回的都是MockNewsPersister持有的容器第一次注入的实例。这就是问题之所在。换句话说,第一个实例注入后,MockNewsPersister再也没有重新向容器申请新的实例。所以,容器也不会重新为其注入新的FXNewsBean类型的实例。

知道原因之后,我们就可以解决这个问题了。解决问题的关键在于保证getNewsBean()方法每次从容器中取得新的FXNewsBean实例,而不是每次都返回其持有的单一实例。

1. 方法注入

Spring容器提出了一种叫做方法注入(Method Injection)的方式,可以帮助我们解决上述问题。我们所要做的很简单,只要让getNewsBean方法声明符合规定的格式,并在配置文件中通知容器,当该方法被调用的时候,每次返回指定类型的对象实例即可 。方法声明需要符合的规格定义如下:

    <public|protected> [abstract] <return-type> theMethodName(no-arguments);

也就是说,该方法必须能够被子类实现或者覆写,因为 容器会为我们要进行方法注入的对象使用Cglib动态生成一个子类实现,从而替代当前对象。既然我们的getNewsBean()方法已经满足以上方法声明格式,剩下唯一要做的就是配置该类,配置内容如下所示:

    <bean id="newsBean" class="..domain.FXNewsBean" singleton="false"></bean>
    
    <bean id="mockPersister" class="..impl.MockNewsPersister">
        <lookup-method name="getNewsBean" bean="newsBean" />
    </bean>

Note

通过<lookup-method>的name属性指定需要注入的方法名,bean属性指定需要注入的对象,当getNewsBean方法被调用的时候,容器可以每次返回一个新的FXNewsBean类型的实例。所以,这个时候,我们再次检查执行结果,输出的实例引用应该是不同的。

2. 殊途同归

除了使用方法注入来达到“每次调用都让容器返回新的对象实例”的目的,还可以使用其他方式达到相同的目的。下面给出其他两种解决类似问题的方法,供读者参考。

  • 使用BeanFactoryAware接口

我们知道,即使没有方法注入,只要在实现getNewsBean()方法的时候,能够保证每次调用BeanFactory的getBean("newsBean"),就同样可以每次都取得新的FXNewsBean对象实例。

Spring框架提供了一个BeanFactoryAware接口,容器在实例化实现了该接口的bean定义的过程中,会自动将容器本身注入该bean。这样,该bean就持有了它所处的BeanFactory的引用。

实现了BeanFactoryAware接口的MockNewsPersister:

    
    public class MockNewsPersister implements IFXNewsPersister, BeanFactoryAware {
        private BeanFactory beanFactory;
    
        public void setBeanFactory(BeanFactory bf) throws BeansException {
            this.beanFactory = bf;
        }
    
        public void persistNews(FXNewsBean bean) {
            persistNews();
        }
    
        public void persistNews() {
            System.out.println("persist bean:" + getNewsBean());
        }
    
        public FXNewsBean getNewsBean() {
            return beanFactory.getBean("newsBean");
        }
    }

实际上,方法注入动态生成的子类,完成的是与以上类似的逻辑,只不过实现细节上不同而已。

  • 使用ObjectFactoryCreatingFactoryBean

ObjectFactoryCreatingFactoryBean是Spring提供的一个FactoryBean实现,它返回一个ObjectFactory实例。从ObjectFactoryCreatingFactoryBean返回的这个ObjectFactory实例可以为我们返回容器管理的相关对象。实际上,ObjectFactoryCreatingFactoryBean实现了BeanFactoryAware接口,它返回的ObjectFactory实例只是特定于与Spring容器进行交互的一个实现而已。使用它的好处就是,隔离了客户端对象对BeanFactory的直接引用。

现在,我们使用ObjectFactory取得FXNewsBean的实例,下方代码给出了对应这种方式的MockNewsPersister实现声明。

    public class MockNewsPersister implements IFXNewsPersister {
        private ObjectFactory newsBeanFactory;
    
        public void persistNews(FXNewsBean bean) {
            persistNews();
        }
    
        public void persistNews() {
            System.out.println("persist bean:" + getNewsBean());
        }
    
        public FXNewsBean getNewsBean() {
            return newsBeanFactory.getObject();
        }
    
        public void setNewsBeanFactory(ObjectFactory newsBeanFactory) {
            this.newsBeanFactory = newsBeanFactory;
        }
    }

有了以上的类定义之后,我们应该为MockNewsPersister注入相应的ObjectFactory,这也正是ObjectFactoryCreatingFactoryBean闪亮登场的时候,下方代码给出了对应的配置内容。

    <bean id="newsBean" class="..domain.FXNewsBean" singleton="false">
    </bean>
    
    <bean id="newsBeanFactory" class="org.springframework.beans.factory.config.ObjectFactoryCreatingFactoryBean">
        <property name="targetBeanName">
            <idref bean="newsBean" />
        </property>
    </bean>
    
    <bean id="mockPersister" class="..impl.MockNewsPersister">
        <property name="newsBeanFactory">
            <ref bean="newsBeanFactory" />
        </property>
    </bean>

3. 方法替换

与方法注入只是通过相应方法为主体对象注入依赖对象不同,方法替换更多体现在方法的实现层面上,它可以灵活替换或者说以新的方法实现覆盖掉原来某个方法的实现逻辑。基本上可以认为,方法替换可以帮助我们实现简单的方法拦截功能。要知道,我们现在可是在不知不觉中迈上了AOP的大道哦!

假设某天我看FXNewsProvider不爽,想替换掉它的getAndPersistNews方法默认逻辑,这时,我就可以用方法替换将它的原有逻辑给替换掉。

首先,我们需要给出org.springframework.beans.factory.support.MethodReplacer的实现类,在这个类中实现将要替换的方法逻辑。假设我们只是简单记录日志,打印简单信息,那么就可以给出一个类似下方代码所示的MethodReplacer实现类。

    public class FXNewsProviderMethodReplacer implements MethodReplacer {
        private static final transient Log logger = LogFactory.getLog(FXNewsProviderMethodReplacer.class);
    
        public Object reimplement(Object target, Method method, Object[] args) throws Throwable {
            logger.info("before executing method[" + method.getName() + "] on Object[" + target.getClass().getName() + "].");
            System.out.println("sorry,We will do nothing this time.");
            logger.info("end of executing method[" + method.getName() + "] on Object[" + target.getClass().getName() + "].");
            return null;
        }
    }

有了要替换的逻辑之后,我们就可以把这个逻辑通过<replaced-method>配置到FXNewsProvider的bean定义中,使其生效,配置内容如下方代码所示。

    <bean id="djNewsProvider" class="..FXNewsProvider">
        <constructor-arg index="0">
            <ref bean="djNewsListener" />
        </constructor-arg>
        <constructor-arg index="1">
            <ref bean="djNewsPersister" />
        </constructor-arg>
    	  <!--关键在下面行-->
        <replaced-method name="getAndPersistNews" replacer="providerReplacer">
      	</replaced-method>
      
    </bean>
    
    <bean id="providerReplacer" class="..FXNewsProviderMethodReplacer">
    </bean>
    <!--其他bean配置-->
     ...

我们把FXNewsProvidergetAndPersistNews方法逻辑给完全替换掉了。现在该方法基本上什么也没做。

最后需要强调的是,这种方式刚引入的时候执行效率不是很高。而且,当你充分了解并应用SpringAOP之后,我想你也不会再回头求助这个特色功能。不过,怎么说这也是一个选择,场景合适的话,为何不用呢?哦,如果要替换的方法存在参数,或者对象存在多个重载的方法,可以在<replaced-method>内部通过<arg-type>明确指定将要替换的方法参数类型。