一、 问题描述


       在大部分情况下,容器中的bean都是singleton类型的。


       如果一个singleton bean要引用另外一个singleton bean,或者一个非singleton bean要引用另外一个非singleton bean时,通常情况下将一个bean定义为另一个bean的property值就可以了。不过对于具有不同生命周期的bean来说这样做就会有问题了,比如在调用一个singleton类型bean A的某个方法时,需要引用另一个非singleton(prototype)类型的bean B,对于bean A来说,容器只会创建一次,这样就没法在需要的时候每次让容器为bean A提供一个新的的bean B实例


二、 解决方案

       对于上面的问题Spring提供了三种解决方案:

  • 放弃控制反转。

           通过实现ApplicationContextAware接口让bean A能够感知bean 容器,并且在需要的时候通过使用getBean("B")方式向容器请求一个新的bean B实例。

  • Lookup方法注入。

            Lookup方法注入利用了容器的覆盖受容器管理的bean方法的能力,从而返回指定名字的bean实例。

  • 自定义方法的替代方案。

            该注入能使用bean的另一个方法实现去替换自定义的方法。



三、 实现案例

3.1 放弃IOC

 

    接口类:

package learn.frame.spring.scope.dropioc;

public interface Command {
    public Object execute();
}

 

     实现类:

package learn.frame.spring.scope.dropioc;

public class AsyncCommand implements Command {

    @Override
    public Object execute() {
        return this;
    }

}

    业务类:

     ApplicationContextAware和BeanFactoryAware差不多,用法也差不多,实现了ApplicationContextAware接口的对象会拥有        一个ApplicationContext的引用,这样我们就可以已编程的方式操作ApplicationContext。看下面的例子。

public class CommandManager implements ApplicationContextAware {

    //用于保存ApplicationContext的引用,set方式注入   
    private ApplicationContext applicationContext;

    //模拟业务处理的方法   
    public Object process() {
        Command command = createCommand();
        return command.execute();
    }

    //获取一个命令   
    private Command createCommand() {
        return (Command) this.applicationContext.getBean("asyncCommand"); //   
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;//获得该ApplicationContext引用   
    }
}

 

beans-dropioc.xml

    单例Bean commandManager的process()方法需要引用一个prototype(非单例)的bean,所以在调用process的时候先通过            createCommand方法从容器中取得一个Command,然后在执行业务计算。

     scope="prototype"


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans   
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
	<!-- 通过scope="prototype"界定该bean是多例的 -->
	<bean id="asyncCommand" class="learn.frame.spring.scope.dropioc.AsyncCommand"
		scope="prototype"></bean>
	
	<bean id="commandManager" class="learn.frame.spring.scope.dropioc.CommandManager">
	</bean> 
	
</beans>


    测试类:

package org.shupeng.learn.frame.spring.scope;

import java.util.ArrayList;

import org.junit.Before;
import org.junit.Test;
import learn.frame.spring.scope.dropioc.CommandManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class TestCommandManagerDropIOC {
    private ApplicationContext context;

    @Before
    public void setUp() throws Exception {
        context = new ClassPathXmlApplicationContext("beans-dropioc.xml");
    }

    @Test
    public void testProcess() {
        CommandManager manager = (CommandManager) context.getBean("commandManager",
                CommandManager.class);
        System.out.println("第一执行process,Command的地址是:" + manager.process());
        System.out.println("第二执行process,Command的地址是:" + manager.process());
    }
}

 

 Test结果:

第一执行process,Command的地址是:learn.frame.spring.scope.dropioc.AsyncCommand@187c55c
第二执行process,Command的地址是:learn.frame.spring.scope.dropioc.AsyncCommand@ae3364

     通过控制台输出看到两次的输出借中的Command的地址是不一样的,因为我们为asyncCommand配置了scope="prototype"属性,这种方式就是使得每次从容器中取得的bean实例都不一样。

  业务代码和Spring Framework产生了耦合。

 

3.2 Look方法注入

    这种方式Spring已经为我们做了很大一部分工作,要做的就是bean配置和业务类。

 

    新的业务:

package learn.frame.spring.scope.lookup;

import learn.frame.spring.scope.dropioc.Command;

public abstract class CommandManager {

    //模拟业务处理的方法   
    public Object process() {
        Command command = createCommand();
        return command.execute();
    }

    //获取一个命令   
    protected abstract Command createCommand();

}

 

beans-lookup.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans   
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
	<!-- 通过scope="prototype"界定该bean是多例的 -->
	<bean id="asyncCommand" class="learn.frame.spring.scope.dropioc.AsyncCommand"
		scope="prototype"></bean>
		
	<bean id="commandManager" class="learn.frame.spring.scope.lookup.CommandManager">  
            <lookup-method name="createCommand" bean="asyncCommand"/>  
        </bean>
</beans>


 

    变化部分:

  • 修改CommandManager类为abstract的,修改createCommand方法也为abstract的。
  • 去掉ApplicationContextAware的实现及相关set方法和applicationContext变量定义
  • 修改bean配置文件,在commandManager Bean中增加<lookup-method name="createCommand" bean="asyncCommand"/>。

    测试类:

package learn.frame.spring.scope;

import org.junit.Before;
import org.junit.Test;
import learn.frame.spring.scope.lookup.CommandManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestCommandManagerLookup {
    private ApplicationContext context;

    @Before
    public void setUp() throws Exception {
        context = new ClassPathXmlApplicationContext("beans-lookup.xml");
    }

    @Test
    public void testProcess() {
        CommandManager manager = (CommandManager) context.getBean("commandManager",
                CommandManager.class);
        System.out.println("第一执行process,Command的地址是:" + manager.process());
        System.out.println("第二执行process,Command的地址是:" + manager.process());
    }
}

 

 测试结果:

第一执行process,Command的地址是:learn.frame.spring.scope.dropioc.AsyncCommand@5bb966
第二执行process,Command的地址是:learn.frame.spring.scope.dropioc.AsyncCommand@1e903d5

 控制台打印出的两个Command的地址不一样,说明实现了。

 


<lookup-method>标签中的name属性就是commandManager Bean的获取Command实例(AsyncCommand)的方法,也就createCommand方法,bean属性是要返回哪种类型的Command的,这里是AsyncCommand。



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


  •       被注入方法不一定是抽象的,如果被注入方法是抽象的,动态生成的子类(这里就是动态生成的CommandManager的子类)会实现该方法。否则,动态生成的子类会覆盖类里的具体方法。
  • 为了让这个动态子类得以正常工作,需要把CGLIB的jar文件放在classpath里,这就是我们引用cglib包的原因。
  • Spring容器要子类化的类(CommandManager)不能是final的,要覆盖的方法(createCommand)也不能是final的。


Lookup方法注入干净整洁,易于扩展,更符合Ioc规则,所以尽量采用这种方式。

四、 原理分析(bean的scope属性范围)


       scope用来声明IOC容器中的对象应该处的限定场景或者说该对象的存活空间,即在IOC容器在对象进入相应的scope之前,生成并装配这些对象,在该对象不再处于这些scope的限定之后,容器通常会销毁这些对象。

 

       Spring容器最初提供了两种bean的scope类型:singleton和prototype,但发布2.0之后,又引入了另外三种scope类型,即request,session和global session类型。不过这三种类型有所限制,只能在web应用中使用,也就是说,只有在支持web应用的ApplicationContext中使用这三个scope才是合理的。

       可以使用bean的singleton或scope属性来指定相应对象的scope,其中,scope属性只能在XSD格式的文档生命中使用,类似于如下代码所演示的形式:

 


DTD:
<bean id ="mockObject1" class="..." singleton="false" />
XSD:
<bean id ="mockObject1" class="..."   scope="prototype" />


       注意:这里的singleton和设计模式里面的单例模式不一样,标记为singleton的bean是由容器来保证这种类型的bean在同一个容器内只存在一个共享实例,而单例模式则是保证在同一个Classloader中只存在一个这种类型的实例。

 

4.1. singleton

      singleton类型的bean定义,在一个容器中只存在一个实例,所有对该类型bean的依赖都引用这一单一实例。singleton类型的bean定义,从容器启动,到他第一次被请求而实例化开始,只要容器不销毁或退出,该类型的bean的单一实例就会一直存活。

       通常情况下,如果你不指定bean的scope,singleton便是容器默认的scope,所以,下面三种配置,形式实际上达成的是同样的效果:


DTD or XSD:
<bean id ="mockObject1" class="..." />
DTD:
<bean id ="mockObject1" class="..." singleton="true" />
XSD:
<bean id ="mockObject1" class="..."   scope="singleton" />


 

4.2 prototype

       scope为prototype的bean,容器在接受到该类型的对象的请求的时候,会每次都重新生成一个新的对象给请求方。

       虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不在拥有当前对象的引用,请求方需要自己负责当前对象后继生命周期的管理工作,包括该对象的销毁。也就是说,容器每次返回请求方该对象的一个新的实例之后,就由这个对象“自生自灭”了。

    可以用以下方式定义prototype类型的bean:

DTD:
<bean id ="mockObject1" class="..." singleton="false" />
XSD:
<bean id ="mockObject1" class="..."   scope="prototype" />

 

4.3 request ,session和global session

      这三个类型是spring2.0之后新增的,他们不像singleton和prototype那么通用,因为他们只适用于web程序,通常是和XmlWebApplicationContext共同使用。

 

     request:

 

<bean id ="requestPrecessor" class="...RequestPrecessor"   scope="request" />

       Spring容器,即XmlWebApplicationContext 会为每个HTTP请求创建一个全新的RequestPrecessor对象,当请求结束后,该对象的生命周期即告结束。当同时有10个HTTP请求进来的时候,容器会分别针对这10个请求创建10个全新的RequestPrecessor实例,且他们相互之间互不干扰,从不是很严格的意义上说,request可以看做prototype的一种特例,除了场景更加具体之外,语意上差不多。

 

 

        session

       对于web应用来说,放到session中最普遍的就是用户的登录信息,对于这种放到session中的信息,我们我们可以使用如下形式的制定scope为session:

 


<bean id ="userPreferences" class="...UserPreferences"   scope="session" />


        Spring容器会为每个独立的session创建属于自己的全新的UserPreferences实例,他比request scope的bean会存活更长的时间,其他的方面真是没什么区别。

 

     global session:

 


<bean id ="userPreferences" class="...UserPreferences"   scope="globalsession" />


      global session只有应用在基于porlet的web应用程序中才有意义,他映射到porlet的global范围的session,如果普通的servlet的web 应用中使用了这个scope,容器会把它作为普通的session的scope对待。

(我只是听说过porlet这个词,好像是和servlet类似的一种java web技术,大家以后遇到的时候可以搜一下!)

 

五、 新的扩展(注解方式)

    自Spring3.x开始,增加了@Async这样一个注解,Spring 文档里是这样说的:

 

The @Async annotation can be provided on a method so that invocation of that method will occur asynchronously. </br>
In other words, the caller will return immediately upon invocation and the actual execution of the method will </br>
occur in a task that has been submitted to a Spring TaskExecutor.

 

就是说让方法异步执行。

 

 

参考文档:

spring配置文件中scope属性

Spring方法注入非单例bean的调用

Spring向单例中注入非单例实例——方法注入【新增加注解方式】