天天看點

如何在 Spring 中自定義 scope

大家對于 Spring 的 scope 應該都不會預設。所謂 scope,字面了解就是“作用域”、“範圍”,如果一個 bean 的 scope 配置為 singleton,則從容器中擷取 bean 傳回的對象都是相同的;如果 scope 配置為prototype,則每次傳回的對象都不同。

一般情況下,Spring 提供的 scope 都能滿足日常應用的場景。但如果你的需求極其特殊,則本文所介紹自定義 scope 合适你。

Spring 内置的 scope

預設時,所有 Spring bean 都是的單例的,意思是在整個 Spring 應用中,bean的執行個體隻有一個。可以在 bean 中添加 scope 屬性來修改這個預設值。scope 屬性可用的值如下:

範圍 描述
singleton 每個 Spring 容器一個執行個體(預設值)
prototype 允許 bean 可以被多次執行個體化(使用一次就建立一個執行個體)
request 定義 bean 的 scope 是 HTTP 請求。每個 HTTP 請求都有自己的執行個體。隻有在使用有 Web 能力的 Spring 上下文時才有效
session 定義 bean 的 scope 是 HTTP 會話。隻有在使用有 Web 能力的 Spring ApplicationContext 才有效
application 定義了每個 ServletContext 一個執行個體
websocket 定義了每個 WebSocket 一個執行個體。隻有在使用有 Web 能力的 Spring ApplicationContext 才有效

如果上述 scope 仍然不能滿足你的需求,Spring 還預留了接口,允許你自定義 scope。

Scope 接口

org.springframework.beans.factory.config.Scope

接口用于定義scope的行為:

package org.springframework.beans.factory.config;import org.springframework.beans.factory.ObjectFactory;import org.springframework.lang.Nullable;public interface Scope {	Object get(String name, ObjectFactory<?> objectFactory);	@Nullable
	Object remove(String name);	void registerDestructionCallback(String name, Runnable callback);	@Nullable
	Object resolveContextualObject(String key);	@Nullable
	String getConversationId();

}      

一般來說,隻需要重新 get 和 remove 方法即可。

自定義線程範圍内的scope

現在進入實戰環節。我們要自定義一個Spring沒有的scope,該scope将bean的作用範圍限制在了線程内。即,相同線程内的bean是同個對象,跨線程則是不同的對象。

1. 定義scope

要自定義一個Spring的scope,隻需實作 

org.springframework.beans.factory.config.Scope

接口。代碼如下:

package com.waylau.spring.scope;import java.util.HashMap;import java.util.Map;import org.springframework.beans.factory.ObjectFactory;import org.springframework.beans.factory.config.Scope;/**
 * Thread Scope.
 * 
 * @since 1.0.0 2019年2月13日
 * @author <a href="https://waylau.com" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >Way Lau</a>
 */public class ThreadScope implements Scope {	
	private final ThreadLocal<Map<String, Object>> threadLoacal = new ThreadLocal<Map<String, Object>>() {		@Override
		protected Map<String, Object> initialValue() {			return new HashMap<String, Object>();
		}
	};	public Object get(String name, ObjectFactory<?> objectFactory) {
		Map<String, Object> scope = threadLoacal.get();
		Object obj = scope.get(name);		// 不存在則放入ThreadLocal
		if (obj == null) {
			obj = objectFactory.getObject();
			scope.put(name, obj);
			
			System.out.println("Not exists " + name + "; hashCode: " + obj.hashCode());
		} else {
			System.out.println("Exists " + name + "; hashCode: " + obj.hashCode());
		}		return obj;
	}	public Object remove(String name) {
		Map<String, Object> scope = threadLoacal.get();		return scope.remove(name);
	}	public String getConversationId() {		return null;
	}	public void registerDestructionCallback(String arg0, Runnable arg1) {
	}	public Object resolveContextualObject(String arg0) {		return null;
	}

}      

在上述代碼中,threadLoacal用于做線程之間的資料隔離。換言之,threadLoacal實作了相同的線程相同名字的bean是同一個對象;不同的線程的相同名字的bean是不同的對象。

同時,我們将對象的hashCode列印了出來。如果他們是相同的對象,則hashCode是相同的。

2. 注冊scope

定義一個AppConfig配置類,将自定義的scope注冊到容器中去。代碼如下:

package com.waylau.spring.scope;import java.util.HashMap;import java.util.Map;import org.springframework.beans.factory.config.CustomScopeConfigurer;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;/**
 * App Config.
 *
 * @since 1.0.0 2019年2月13日
 * @author <a href="https://waylau.com" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >Way Lau</a>
 */@[email protected] class AppConfig {	@Bean
	public static CustomScopeConfigurer customScopeConfigurer() {
		CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer();

		Map<String, Object> map = new HashMap<String, Object>();
		map.put("threadScope", new ThreadScope());		// 配置scope
		customScopeConfigurer.setScopes(map);		return customScopeConfigurer;
	}
}      

“threadScope”就是自定義ThreadScope的名稱。

3. 使用scope

接下來就根據一般的scope的用法,來使用自定義的scope了。代碼如下:

package com.waylau.spring.scope.service;import org.springframework.context.annotation.Scope;import org.springframework.stereotype.Service;/**
 * Message Service Impl.
 * 
 * @since 1.0.0 2019年2月13日
 * @author <a href="https://waylau.com" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >Way Lau</a>
 */@Scope("threadScope")@Servicepublic class MessageServiceImpl implements MessageService {	
	public String getMessage() {		return "Hello World!";
	}

}      

其中

@Scope("threadScope")

中的“threadScope”就是自定義ThreadScope的名稱。

4. 定義應用入口

定義Spring應用入口:

package com.waylau.spring.scope;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import com.waylau.spring.scope.service.MessageService;/**
 * Application Main.
 * 
 * @since 1.0.0 2019年2月13日
 * @author <a href="https://waylau.com" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >Way Lau</a>
 */public class Application {	public static void main(String[] args) {		@SuppressWarnings("resource")
		ApplicationContext context = 
            new AnnotationConfigApplicationContext(AppConfig.class);

		MessageService messageService = context.getBean(MessageService.class);
		messageService.getMessage();
 
		MessageService messageService2 = context.getBean(MessageService.class);
		messageService2.getMessage();
		
	}

}      

運作應用觀察控制台輸出如下:

Not exists messageServiceImpl; hashCode: 2146338580Exists messageServiceImpl; hashCode: 2146338580      

輸出的結果也就驗證了ThreadScope“相同的線程相同名字的bean是同一個對象”。

如果想繼續驗證ThreadScope“不同的線程的相同名字的bean是不同的對象”,則隻需要将Application改造為多線程即可。

package com.waylau.spring.scope;import java.util.concurrent.CompletableFuture;import java.util.concurrent.ExecutionException;import org.springframework.context.ApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;import com.waylau.spring.scope.service.MessageService;/**
 * Application Main.
 * 
 * @since 1.0.0 2019年2月13日
 * @author <a href="https://waylau.com" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >Way Lau</a>
 */public class Application {	public static void main(String[] args) throws InterruptedException, ExecutionException {		@SuppressWarnings("resource")
		ApplicationContext context = 
			new AnnotationConfigApplicationContext(AppConfig.class);

		CompletableFuture<String> task1 = CompletableFuture.supplyAsync(()->{            //模拟執行耗時任務
            MessageService messageService = context.getBean(MessageService.class);
			messageService.getMessage();
	 
			MessageService messageService2 = context.getBean(MessageService.class);
			messageService2.getMessage();            //傳回結果
            return "result";
        });
		
		CompletableFuture<String> task2 = CompletableFuture.supplyAsync(()->{            //模拟執行耗時任務
            MessageService messageService = context.getBean(MessageService.class);
			messageService.getMessage();
	 
			MessageService messageService2 = context.getBean(MessageService.class);
			messageService2.getMessage();            //傳回結果
            return "result";
        });
		
		task1.get();
		task2.get();
		
		
	}

}      

觀察輸出結果;

Not exists messageServiceImpl; hashCode: 1057328090Not exists messageServiceImpl; hashCode: 784932540Exists messageServiceImpl; hashCode: 1057328090Exists messageServiceImpl; hashCode: 784932540      

上述結果驗證ThreadScope“相同的線程相同名字的bean是同一個對象;不同的線程的相同名字的bean是不同的對象”

源碼

見https://github.com/waylau/spring-5-book 的 

s5-ch02-custom-scope-annotation

項目。

參考引用

  • 《Spring 5 開發大全》:

轉載于:https://blog.51cto.com/13689432/2361929