如何在 Spring 中自定義 scope

你們對於 Spring 的 scope 應該都不會默認。所謂 scope,字面理解就是「做用域」、「範圍」,若是一個 bean 的 scope 配置爲 singleton,則從容器中獲取 bean 返回的對象都是相同的;若是 scope 配置爲prototype,則每次返回的對象都不一樣。java

通常狀況下,Spring 提供的 scope 都能知足平常應用的場景。但若是你的需求極其特殊,則本文所介紹自定義 scope 合適你。git

Spring 內置的 scope

默認時,全部 Spring bean 都是的單例的,意思是在整個 Spring 應用中,bean的實例只有一個。能夠在 bean 中添加 scope 屬性來修改這個默認值。scope 屬性可用的值以下:github

範圍 描述
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。web

Scope 接口

org.springframework.beans.factory.config.Scope接口用於定義scope的行爲:spring

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 方法便可。websocket

自定義線程範圍內的scope

如今進入實戰環節。咱們要自定義一個Spring沒有的scope,該scope將bean的做用範圍限制在了線程內。即,相同線程內的bean是同個對象,跨線程則是不一樣的對象。session

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">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是不一樣的對象。app

同時,咱們將對象的hashCode打印了出來。若是他們是相同的對象,則hashCode是相同的。socket

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">Way Lau</a>
 */
@Configuration
@ComponentScan
public 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">Way Lau</a>
 */
@Scope("threadScope")
@Service
public 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">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: 2146338580
Exists 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">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: 1057328090
Not exists messageServiceImpl; hashCode: 784932540
Exists messageServiceImpl; hashCode: 1057328090
Exists messageServiceImpl; hashCode: 784932540

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

源碼

https://github.com/waylau/spring-5-books5-ch02-custom-scope-annotation項目。

參考引用

相關文章
相關標籤/搜索