【Spring】关于 Spring 中 bean 实例的作用域 Scope(单例中注入原型?)

【Spring】关于 Spring 中 bean 实例的作用域 Scope(单例中注入原型?)

  • 前言
  • 不同作用域实例间的互相依赖
    • 基于代理处理
      • 基于 `Method Lookup` 机制处理
  • 自定义 Scope
    • Scope
      • SimpleMapScope
      • SimpleThreadScope
      • AbstractRequestAttributesScope
    • 自定义 Scope 的注册
      • demo
  • 总结

前言

Spring 中的使用过程中,Scope 作用域的概念一定程度上是透明、弱化的,因为绝大多数的场景我们都是默认使用 Singleton 即单例的,它拥有最 “长” 的生命周期,几乎伴随容器的全生命周期,但其他生命周期比如 Spring 内置的 Prototype 原型和 Spring MVC 拓展的 Request Session 也是不容忽视的

本章节两个方面入手,简单聊一下 Scope 的部分内容:

  • 不同作用域实例间的互相依赖
  • 自定义 Scope

这部分内容在 BeanFactorySpring AOP 等内容的铺垫下更容易理解,可以移步对应专栏了解

不同作用域实例间的互相依赖

如果我们在 Singleton 实例中注入另一个 Singleton 实例,不难理解没有任何问题,但是如果在不同作用域实例间互相依赖,比如: Singleton 实例中注入另一个其他作用域实例如 Prototype,这在不做额外处理的情况下可能达不到我们预期的语义

因为单例的创建一般只有一次,这包括它的属性填充,依此其属性尽管是
一个 Prototype 实例,但对该实例的方法调用也永远作用于这一个实
例上,这可能达不到预期的目的

比如这段代码:

@Configuration
public class ScopeProxyDemo {

    @Component
    @Scope(
            scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE
    )
    public static class PortoTypeBean {

        private double random = Math.random();

        public void m() {
            System.out.println(random);
        }
    }

    @Component
    public static class Main {

        @Autowired
        PortoTypeBean portoTypeBean;

        public void portoTypeMethod() {
            portoTypeBean.m();
        }
    }

    @Test
    public void test() {
        AnnotationConfigApplicationContext context
                = new AnnotationConfigApplicationContext(ScopeProxyDemo.class);
        Main bean = context.getBean(Main.class);

		// 此时尽管 bean 为 原型 实例,但每次调用方法都指向同一个实例
        bean.portoTypeMethod();
        bean.portoTypeMethod();
    }

}

在单例 Main 中注入原型实例 PortoTypeBean,如果期望对 PortoTypeBean 的调用每次都作用于一个新的实例(符合原型实例的语义),则当前的类是做不到的

于是,我们有两种模式来解决这个问题

  • 基于代理处理
  • 基于 Method Lookup 机制处理

基于代理处理

只需要给上述示例中 Prototype 实例的 Scope 注解指定 proxyMode 属性即可,如下:

@Configuration
public class ScopeProxyDemo {

    @Component
    @Scope(
            scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE
            , proxyMode = ScopedProxyMode.TARGET_CLASS
    )
    public static class PortoTypeBean {

        private double random = Math.random();

        public void m() {
            System.out.println(random);
        }
    }

    @Component
    public static class Main {

        @Autowired
        PortoTypeBean portoTypeBean;

        public void portoTypeMethod() {
            portoTypeBean.m();
        }
    }

    @Test
    public void test() {
        AnnotationConfigApplicationContext context
                = new AnnotationConfigApplicationContext(ScopeProxyDemo.class);
        Main bean = context.getBean(Main.class);
		
		// 此时将达到预期的语义
        bean.portoTypeMethod();
        bean.portoTypeMethod();
    }

}
  • 我们为其指定了属性 proxyMode = ScopedProxyMode.TARGET_CLASS,默认情况下该属性值为 ScopedProxyMode.DEFAULT,不会对容器中获取的对应实例进行代理
  • 指定 ScopedProxyMode.INTERFACESScopedProxyMode.TARGET_CLASS 时,创建的是一个 ScopedProxyFactoryBean 实例,则对该代理对象的方法调用会指向对应的域对象(比如 PROTOTYPE 作用域下就是一个全新的实例)
  • 关于为什么上述场景下会创建 ScopedProxyFactoryBean 实例,因为注册上述示例的 BeanDefinition 时会基于它们的注解元数据判断是否需要代理,具体细节可见下文:

【源码】Spring —— BeanDefinition 解读2

关于为什么 ScopedProxyFactoryBean 代理的实例可以拥有上述能
力,这里面涉及到 Spring AOP Introduction 相关的内容,不做
深入,有兴趣可以从 DelegatingIntroductionInterceptor 入手 

基于 Method Lookup 机制处理

这是 Spring 提供的一种机制,仅给出示例代码,不做深入了解

@Configuration
public class ScopeLookupDemo {

    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public static class B {
        public B() {
            System.out.println("init ...");
        }
    }

    @Component
    public static class A {

        @Lookup
        public B b() {
            return null;
        }
    }

    @Test
    public void test() {
        AnnotationConfigApplicationContext context
                = new AnnotationConfigApplicationContext(ScopeLookupDemo.class);
        A bean = context.getBean(A.class);
        System.out.println(bean.b());
        System.out.println(bean.b());
    }
}
  • 关键就在于 @Lookup 注解,基于此机制也可以达到我们预期的目的
  • 该方法明显没有前者来的好用,所以仅供参考

自定义 Scope

Scope

public interface Scope {

	// 核心方法,当前作用域找不到时会基于 ObjectFactory#getObject 方法获取
	// 这个 ObjectFactory#getObject 在容器中会帮我们调用 AbstractBeanFactory#createBean
	Object get(String name, ObjectFactory<?> objectFactory);

	/**
	 * 从当前作用域移除,同时移除注册的销毁回调(但不执行)
	 */
	@Nullable
	Object remove(String name);

	// 销毁回调注册
	void registerDestructionCallback(String name, Runnable callback);

	@Nullable
	Object resolveContextualObject(String key);

	// 会话 id,通常为 null
	@Nullable
	String getConversationId();

}
  • 顶层 Scope 接口,我们要自定义 Scope 首先要提供对应的实现类
  • 其中核心方法是 get,它的主要逻辑是先从当前作用域中找,找不到就依赖于 ObjectFactory#getObject
  • 在容器中,ObjectFactory#getObject 自然是委托到 AbstractBeanFactory#createBean 方法来创建新的实例,具体可参考 AbstractBeanFactory#doGetBean 方法

SimpleMapScope

public class SimpleMapScope implements Scope, Serializable {

	// 基于 Map 管理,这个模式就类似于单例了,除非手动清除该 map
	private final Map<String, Object> map = new HashMap<>();

	// ...

	// 先从 map 获取,否则 objectFactory.getObject()
	@Override
	public Object get(String name, ObjectFactory<?> objectFactory) {
		synchronized (this.map) {
			Object scopedObject = this.map.get(name);
			if (scopedObject == null) {
				scopedObject = objectFactory.getObject();
				this.map.put(name, scopedObject);
			}
			return scopedObject;
		}
	}

	// ...

}
  • Spring 提供的内置实现 SimpleMapScope
  • 它基于 Map 管理作用域实例,除非手动清除或者内存溢出,否则这类似于单例

SimpleThreadScope

public class SimpleThreadScope implements Scope {

	// 基于 NamedThreadLocal 管理,即以线程为作用域
	private final ThreadLocal<Map<String, Object>> threadScope =
			new NamedThreadLocal<Map<String, Object>>("SimpleThreadScope") {
				@Override
				protected Map<String, Object> initialValue() {
					return new HashMap<>();
				}
			};

	// ...

}
  • Spring 提供的内置实现 SimpleThreadScope
  • 它基于 NamedThreadLocal 管理作用域实例,即每个实例以线程为生命周期

AbstractRequestAttributesScope

public abstract class AbstractRequestAttributesScope implements Scope {

	@Override
	public Object get(String name, ObjectFactory<?> objectFactory) {
		
		// 从 RequestAttributes 中获取,获取不到则 objectFactory.getObject()
		RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
		Object scopedObject = attributes.getAttribute(name, getScope());
		if (scopedObject == null) {
			scopedObject = objectFactory.getObject();
			attributes.setAttribute(name, scopedObject, getScope());
			Object retrievedObject = attributes.getAttribute(name, getScope());
			if (retrievedObject != null) {
				scopedObject = retrievedObject;
			}
		}
		return scopedObject;
	}

	// ...

}
  • Spring web 提供的抽象基类 AbstractRequestAttributesScope
  • 它基于 RequestAttributes 管理作用域实例,这里就不深入解读,可以理解为:对于 Request 它就是 Request#getAttribute,对于 Session 它就是 Session#getAttribute
  • RequestScopeSessionScope 就基于此实现

自定义 Scope 的注册

  • 这里的注册值得自然是注册到 Spring IoC Container,让它可以基于此 Scope 来创建对于作用域的实例
  • 注册的方法为 ConfigurableBeanFactory#registerScope,它在 AbstractBeanFactory 有实现:基于一个 Map 管理

demo

public class ScopeRegisterDemo {

    // @Scope 可作为 元注解 使用
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Scope("thread")
    public @interface ThreadScope {

        @AliasFor(annotation = Scope.class, attribute = "proxyMode")
        ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
    }

    public static class ThreadBean {
        public ThreadBean() {
            System.out.println("ThreadBean init ...");
        }
        public void m() {}
    }

    public static class Main {

        ThreadBean threadBean;

        public Main(ThreadBean threadBean) {
            this.threadBean = threadBean;
        }

        // 由不同线程调用,则会创建新的作用域实例
        public void m() {
            new Thread(threadBean::m).start();
        }
    }

    @Configuration
    public static class Config {

        @Bean
        // 代理以注入单例实现预期语义
        @ThreadScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
        public ThreadBean threadBean() {
            return new ThreadBean();
        }

        @Bean
        public Main main() {
            return new Main(threadBean());
        }
    }

    @Test
    public void test() {
        AnnotationConfigApplicationContext context
                = new AnnotationConfigApplicationContext();

        // ConfigurableBeanFactory#registerScope 注册作用域
        ConfigurableBeanFactory beanFactory = context.getBeanFactory();
        beanFactory.registerScope("thread", new SimpleThreadScope());
        context.register(Config.class);

        // 先注册在手动 refresh
        context.refresh();

        // 达到预期语义
        Main bean = context.getBean(Main.class);
        bean.m();
        bean.m();
    }
}

这段 demo 元素较多:

  • @Scope 注解可作为 元注解,因此我们可以以示例中的模式定义自己的 作用域注解,集合 @AliasFor 注解暴露属性
  • 单例中注入其他作用域实例可以代理以符合预期语义
  • 使用 ConfigurableBeanFactory#registerScope 方法注册对应的 SimpleThreadScope,它会为每一个不同的线程创建对应的实例
  • 测试结果符合预期语义

总结

本文基于部分源码及大量示例,讨论了:

  • Singleton 实例下注入其他作用域实例并保持语义的两种方式
  • 自定义 Scope 及其注册

你可能感兴趣的:(Spring,spring,java)