https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-factory-scopes 2024 https://www.baeldung.com/spring-boot-singleton-vs-beans @Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON) 2023 https://www.baeldung.com/spring-bean-scopes Scope iface Creating custom scope Threading scopes public class MyScope implements Scope { @Override public Object get(String name, ObjectFactory objectFactory) { } @Override public Object remove(String name) { } @Override public void registerDestructionCallback(String name, Runable callback) { } @Override public Object resolveContextualObject(String key) { } @Override public String getConversationId() { } } // Singleton private Map singletons = new ConcurrentHashMap<>(); @Override public Object get(String name, ObjectFactory objectFactory) { Object result = singletons.get(name) if (result == null) { result = objectFactory.getObject(); singletons.put(name, result); } return result; } // Prototype @Override public Object get(String name, ObjectFactory objectFactory) { return objectFactory.getObject(); } They could look like this. But there is no actual implementation for those scopes. // Custom Scope implementation public class CustomScope implements Scope { private Map storage = new ConcurrentHashMap(); @Override public Object get(String name, ObjectFactory objectFactory) { Object object = storage.get(name); if (object == null) { object = objectFactory.getObject(); storage.put(name, object); } return object; } @Override public Object remove(String name) { return storage.remove(name); } ... } @Configuration public class CustomScopeConfig { @Bean public CustomScope customScope() { return new CustomScope(); } @Bean public CustomScopeConfigurer customScopeConfigurer(CustomScope customScope) { // ? CustomScopeConfigurer is a native Spring ? CustomScopeConfigurer configurer = new CustomScopeConfigurer(); Map scopes = Maps.newHashMap(); scopes.put("custom", customScope); configurer.setScopes(scopes); return configurer; } @Bean @Scope("custom") public Logger logger() { return new Logger(); } } // 2-nd way to do this: ConfigurableListableBeanFactory::registerScope // Runner ApplicationContext ctx = new AnnotationConfigApplicationContext(CustomScopeConfig.class); Logger logger = ctx.getBean(Logger.class); System.out.println(logger); CustomScope customScope = ctx.getBean(CustomScope.class); customScope.remove(logger); logger = ctx.getBean(Logger.class); System.out.println(logger); // Output ...scopes.entity.Logger@6a472554 ...scopes.entity.Logger@7ff2a664 // Problem What about injection? We need proxy! What about handling multiple beans of the same scope? // 1-st problem solution is simple: // Config @Bean @Scope(value="custom", proxyMode=ScopedProxyMode.TARGET_CLASS) public Logger logger() { return new Logger(); } // Runner ApplicationContext ctx = new AnnotationConfigApplicationContext(CustomScopeConfig2.class); Logger logger = ctx.getBean(Logger.class); System.out.println(logger); System.out.println(logger.getClass()); // Output ...scopes.entity.Logger@184cf7cf class ...scopes.entity.Logger$$EnhancerBySpringCGLIB$$103d1ffd Now proxying will be handled by CGLib (which is able to override classes and not only interfaces) (JDK can only create proxy for i-faces) // 2-nd problem solution: public class ScopeWrapper implements BeanFactoryPostProcessor { private Scope scope; private String scopeName; private List scopeBeanDefinitions; public ScopeWrapper(Scope scope, String scopeName) { this.scope = scope; this.scopeName = scopeName; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { scopeBeanDefinitions = Lists.newArrayList(beanFactory.getBeanDefinitionNames()).stream() .filter(beanDefinitionName -> { BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName); return scopeName.equals(beanDefinition.getScope()); }) .collect(Collectors.toList()); } public void clear() { for (String beanDefinitionName : scopeBeanDefinitions) { scope.remove(beanDefinitionName); } } } @Configuration public class ScopeWrapperConfig { public CustomScope customScope() {...} public ScopeWrapper customScopeWrapper(CustomScope customScope) {...} public CustomScopeConfigurer simpleThreadScopeConfigurer(CustomScope customScope) {...} public Logger logger() {...} public Service service() {...} } // Runner ApplicationContext ctx = new AnnotationConfigApplicationContext(ScopeWrapperConfig.class); Logger logger = ctx.getBean(Logger.class); System.out.println(logger); Service service = ctx.getBean(Service.class); System.out.println(service); ScopeWrapper scopeWrapper = ctx.getBean(ScopeWrapper.class); scopeWrapper.clear(); logger = ctx.getBean(Logger.class); System.out.println(logger); service = ctx.getBean(Service.class); System.out.println(service); // Output ...scopes.entity.Logger@5762806e ...scopes.entity.Service@17c386de ...scopes.entity.Logger@5af97850 ...scopes.entity.Service@5ef60048 // Problem I have http-interface application. And it's multithreaded. I need the Request scope. /// org.springframework.context.support.SimpleThreadScope private final ThreadLocal> threadScope = new NamedThreadLocal>("SimpleThreadScope") { @Override protected Map initialValue() { return new HashMap(); } } @Override public Object get(String name, ObjectFactory objectFactory) { Map scope = this.threadScope.get(); Object object = scope.get(name); if (object == null) { object = objectFactory.getObject(); scope.put(name, object); } return object; } @Override public Object remove(String name) { Map scope = this.threadScope.get(); return scope.remove(name); } // 1. get() - populates bean in ThreadLocal map // 2. remove() - removes bean from ThreadLocal map // Thread scope logging // Model public class Logger { private List events; public Logger() { this.events = Lists.newArrayList(); } public void addEvent(String event) { this.events.add(event); } public List getEvents() { return this.events; } } // Config @Configuration public class SimpleThreadScopeConfig { @Bean public SimpleThreadScope simpleThreadScope() { return new SimpleThreadScope(); } @Bean public CustomScopeConfigurer simpleThreadScopeConfigurer(SimpleThreadScope simpleThreadScope) { CustomScopeConfigurer configurer = new CustomScopeConfigurer(); Map scopes = Maps.newHashMap(); scopes.put("thread", simpleThreadScope); configurer.setScope(scopes) return configurer; } @Bean @Scope(value="thread", proxyMode=ScopedProxyMode.TARGET_CLASS) public Logger logger() { return new Logger(); } } // Runner ApplicationContext ctx = new AnnotationConfigApplicationContext(SimpleThreadScopeConfig.class); Runnable r = () -> { Logger logger = ctx.getBean(Logger.class); logger.addEvent("Hello, i'm thread " + Thread.currentThread().getId()); System.out.println(logger.getEvents()); }; new Thread(r).start(); new Thread(r).start(); // Output [Hello, i'm thread 12] [Hello, i'm thread 11] // Problem What about thread pools??? // Thread pool and thread scope // Runner ApplicationContext ctx = new AnnotationConfigApplicationContext(SimpleThreadScopeConfig.class); Runnable r = () -> { Logger logger = ctx.getBean(Logger.class); logger.addEvent("Hello, i'm thread " + Thread.currentThread().getId()); }; ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(r); executor.shutdown(); executor.awaitTermination(10, TimeUnit.SECONDS); System.out.println(ctx.getBean(Logger.class).getEvents()); // Output [] // Problem Thread pool thread is not associated with the current thread. // Wrap Runnable and Callable private Callable wrapCallable(Callable callable) { Map callingThreadContext = scope.getCurrentThreadContext(); return () -> { scope.replaceContext(callingThreadContext); try { return callable.call(); } finally { scope.removeContext(); } } } private Runnable wrapRunnable(Runnable runnable) { Map callingThreadContext = scope.getCurrentThreadContext(); return () -> { scope.replaceContext(callingThreadContext); try { return runnable.run(); } finally { scope.removeContext(); } } } // from the Spring src: public void replaceContext(Map context) { this.threadScope.set(context); } public void removeContext() { this.threadScope.remove(); } public Map getCurrentThreadContext() { return this.threadScope.get(); } // AssociatableExecutorFactory @SuppressWarnings({"rawtypes", "unchecked"}) public ExecutorService wrap(ExecutorService executor) { // // 1. Proxying ExecutorService // return (ExecutorService) Proxy.newProxyInstance( ExecutorService.class.getClassLoader(), new Class[] { ExecutorService.class }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object [] args) throws Throwable { if (args != null) { // // 2. Iterate over the arguments of the method call // for (int i = 0; i < args.length; ++i) { // // 3. Wrap collection or single object // if (Collection.class.isAssibnableFrom(arg.getClass())) { args[i] = ((Collection) arg).stream().map((listItem) -> { return tryToWrapSingleObject(listItem); }) .collect(Collectors.toList()); } else { args[i] = tryToWrapSingleObject(arg); } } } return method.invoke(executor, args); } private Object tryToWrapSingleObject(Object arg) { if (Callable.class.isAssignableFrom(arg.getClass())) { return wrapCallable((Callable) arg); } else if (Runnable.class.isAssignableFrom(arg.getClass())) { return wrapRunnable((Runnable) arg); } else { return arg; } } private Callable wrapCallable(Callable callable) { ... } private Runnable wrapRunnable(Runnable runnable) { ... } } ); } // Thread pool and thread scope // Runner ApplicationContext ctx = new AnnotationConfigApplicationContext(AssociatableThreadScopeConfig.class); Runnable r = () -> { Logger logger = ctx.getBean(Logger.class); logger.addEvent("Hello, i'm thread " + Thread.currentThread().getId()); }; AssociatableExecutorFactory executorFactory = ctx.getBean(AssociatableExecutorFactory.class); ExecutorService executor = executorFactory.wrap(Executors.newSingleThreadExecutor()); executor.execute(r); executor.shutdown(); executor.awaitTermination(10, TimeUnit.SECONDS); System.out.println(ctx.getBean(Logger.class).getEvents()); // Output [Hello, i'm thread 11] Current thread is 1 // Readl HTTP server application case // // 1. Define scope // @Configuration private static class Config { @Bean @Scope("request") public Logger logger() ... @Bean public RequestScope requestScope() ... @Bean CustomScopeConfigurere requestScope() ... ... } // // 2. Create request filter // private static class RequestFilter { @Autowired private ScopeWrapper requestScopeWrapper; public void filterRequest(Object request) { requestScopeWrapper.clear(); requestScopeWrapper.getScope().put("request", request); } } // // 3. Create response filter // private static class ResponseFilter { @Autowired private Logger logger; @Autowired private Object request; // this will be proxy public void filterResponse(Object response) { System.out.println(logger.getEvents()); } } // // 4. Inject your "request" scope bean // private static class UserService { @Autowired private Logger logger; // this will be proxy public void registerUser(Object user) { // login logger.addEvent("User " + user + " was registered."); } } // Conclusion 1. Custom scopes are needed to modify bean lifetime 2. Always think about concurrency, when creating custom scopes 3. Reuse Spring infrastructure to create custom scopes