0
# Scope Management
1
2
Advanced bean scope management beyond singleton and prototype scopes. Provides custom scopes including refreshable configurations that reload on property changes and thread-local storage for request isolation.
3
4
## Capabilities
5
6
### @Refreshable Scope
7
8
Custom scope that allows beans to be refreshed when configuration properties change, enabling dynamic reconfiguration without application restart.
9
10
```java { .api }
11
/**
12
* Annotation for beans that should be refreshed when configuration changes
13
* Creates proxy beans that can be invalidated and recreated on demand
14
*/
15
@Target({ElementType.TYPE, ElementType.METHOD})
16
@Retention(RetentionPolicy.RUNTIME)
17
@Scope
18
@ScopedProxy
19
public @interface Refreshable {
20
/**
21
* Configuration key prefixes that trigger refresh of this bean
22
* Empty array means refresh on any configuration change
23
* @return Array of configuration prefixes to watch
24
*/
25
String[] value() default {};
26
}
27
```
28
29
**Usage Examples:**
30
31
```java
32
import io.micronaut.runtime.context.scope.Refreshable;
33
import io.micronaut.context.annotation.ConfigurationProperties;
34
35
// Database configuration that refreshes when database properties change
36
@Refreshable("database")
37
@ConfigurationProperties("database")
38
@Singleton
39
public class DatabaseConfig {
40
private String url;
41
private String username;
42
private String password;
43
private int maxConnections;
44
45
// Getters and setters...
46
public String getUrl() { return url; }
47
public void setUrl(String url) { this.url = url; }
48
49
// When database.* properties change, this bean is recreated
50
}
51
52
// Service that depends on refreshable configuration
53
@Refreshable
54
@Singleton
55
public class DatabaseService {
56
57
private final DatabaseConfig config;
58
private DataSource dataSource;
59
60
public DatabaseService(DatabaseConfig config) {
61
this.config = config;
62
this.dataSource = createDataSource(config);
63
}
64
65
// This service will be recreated when its dependencies are refreshed
66
public Connection getConnection() throws SQLException {
67
return dataSource.getConnection();
68
}
69
}
70
71
// Multiple configuration prefixes
72
@Refreshable({"cache", "redis"})
73
@Singleton
74
public class CacheService {
75
// Refreshes when cache.* or redis.* properties change
76
}
77
78
// Refresh on any configuration change
79
@Refreshable
80
@Singleton
81
public class FlexibleService {
82
// Refreshes when any configuration property changes
83
}
84
```
85
86
### @ThreadLocal Scope
87
88
Scope that stores bean instances in thread-local storage, providing isolated instances per thread.
89
90
```java { .api }
91
/**
92
* Scope annotation for thread-local bean storage
93
* Each thread gets its own instance of the bean
94
*/
95
@Target({ElementType.TYPE, ElementType.METHOD})
96
@Retention(RetentionPolicy.RUNTIME)
97
@Scope
98
@ScopedProxy
99
public @interface ThreadLocal {
100
/**
101
* Enable lifecycle support for thread-local beans
102
* When true, beans will receive lifecycle callbacks (PostConstruct, PreDestroy)
103
* @return true to enable lifecycle support
104
*/
105
boolean lifecycle() default false;
106
}
107
```
108
109
**Usage Examples:**
110
111
```java
112
import io.micronaut.runtime.context.scope.ThreadLocal;
113
114
// Thread-local context for request processing
115
@ThreadLocal
116
@Singleton
117
public class RequestContext {
118
private String requestId;
119
private String userId;
120
private Instant startTime;
121
private Map<String, Object> attributes = new HashMap<>();
122
123
public void initialize(String requestId, String userId) {
124
this.requestId = requestId;
125
this.userId = userId;
126
this.startTime = Instant.now();
127
this.attributes.clear();
128
}
129
130
// Each thread gets its own instance
131
public String getRequestId() { return requestId; }
132
public String getUserId() { return userId; }
133
public void setAttribute(String key, Object value) { attributes.put(key, value); }
134
public Object getAttribute(String key) { return attributes.get(key); }
135
}
136
137
// Thread-local with lifecycle support
138
@ThreadLocal(lifecycle = true)
139
@Singleton
140
public class ThreadLocalResourceManager {
141
142
private Connection connection;
143
private ExecutorService executor;
144
145
@PostConstruct
146
public void initialize() {
147
// Called once per thread when first accessed
148
this.connection = createConnection();
149
this.executor = Executors.newSingleThreadExecutor();
150
}
151
152
@PreDestroy
153
public void cleanup() {
154
// Called when thread terminates (if lifecycle = true)
155
if (connection != null) {
156
try {
157
connection.close();
158
} catch (SQLException e) {
159
logger.warn("Error closing connection", e);
160
}
161
}
162
if (executor != null) {
163
executor.shutdown();
164
}
165
}
166
}
167
168
// Service using thread-local dependencies
169
@Singleton
170
public class RequestProcessor {
171
172
private final RequestContext requestContext;
173
174
public RequestProcessor(RequestContext requestContext) {
175
this.requestContext = requestContext; // Proxy injected
176
}
177
178
public void processRequest(HttpRequest request) {
179
// Initialize thread-local context
180
requestContext.initialize(
181
UUID.randomUUID().toString(),
182
extractUserId(request)
183
);
184
185
// Process request - each thread has isolated context
186
String requestId = requestContext.getRequestId();
187
System.out.println("Processing request: " + requestId);
188
}
189
}
190
```
191
192
### @ScopedProxy Annotation
193
194
Meta-annotation indicating that a scope should use proxy-based implementation.
195
196
```java { .api }
197
/**
198
* Meta-annotation indicating that a scope should use proxies
199
* Enables lazy evaluation and dynamic behavior for custom scopes
200
*/
201
@Target({ElementType.ANNOTATION_TYPE})
202
@Retention(RetentionPolicy.RUNTIME)
203
public @interface ScopedProxy {
204
// Marker annotation for proxy-based scopes
205
}
206
```
207
208
### RefreshEvent
209
210
Event published when configuration refresh occurs, allowing fine-grained control of refresh behavior.
211
212
```java { .api }
213
/**
214
* Event fired when configuration refresh occurs
215
* Allows listeners to respond to configuration changes
216
*/
217
public class RefreshEvent extends ApplicationEvent {
218
219
/**
220
* Create refresh event for specific configuration changes
221
* @param changes Map of changed configuration keys and their new values
222
*/
223
public RefreshEvent(Map<String, Object> changes);
224
225
/**
226
* Create refresh event for full configuration refresh
227
* Triggers refresh of all @Refreshable beans
228
*/
229
public RefreshEvent();
230
231
/**
232
* Get the configuration changes that triggered this refresh
233
* @return Map of changed properties, or empty for full refresh
234
*/
235
public Map<String, Object> getSource();
236
237
}
238
```
239
240
**Usage Examples:**
241
242
```java
243
import io.micronaut.runtime.context.scope.refresh.RefreshEvent;
244
import io.micronaut.runtime.event.annotation.EventListener;
245
246
@Singleton
247
public class RefreshEventListener {
248
249
@EventListener
250
public void onRefresh(RefreshEvent event) {
251
Map<String, Object> changes = event.getSource();
252
System.out.println("Configuration refresh triggered");
253
System.out.println("Changed properties: " + changes.keySet());
254
255
// Handle specific property changes by checking the changes map
256
if (changes.containsKey("database.url")) {
257
System.out.println("Database URL changed to: " + changes.get("database.url"));
258
System.out.println("Reconnecting to database...");
259
}
260
261
if (changes.containsKey("cache.ttl")) {
262
System.out.println("Cache TTL changed to: " + changes.get("cache.ttl"));
263
System.out.println("Clearing cache...");
264
}
265
266
// Check for full refresh (when "all" key is present)
267
if (changes.containsKey("all")) {
268
System.out.println("Full configuration refresh detected");
269
}
270
}
271
}
272
273
// Programmatic refresh triggering
274
@Singleton
275
public class ConfigurationManager {
276
277
@Inject
278
private ApplicationEventPublisher<RefreshEvent> eventPublisher;
279
280
public void refreshConfiguration() {
281
// Trigger full refresh
282
eventPublisher.publishEvent(new RefreshEvent());
283
}
284
285
public void refreshDatabaseConfig(Map<String, Object> newDatabaseProps) {
286
// Trigger partial refresh for database properties
287
eventPublisher.publishEvent(new RefreshEvent(newDatabaseProps));
288
}
289
}
290
```
291
292
### RefreshScope Implementation
293
294
The actual scope implementation for refreshable beans.
295
296
```java { .api }
297
/**
298
* Scope implementation for refreshable beans
299
* Manages bean lifecycle and invalidation on configuration changes
300
*/
301
@Singleton
302
public class RefreshScope implements CustomScope<Refreshable> {
303
304
/**
305
* Get or create bean instance for the refresh scope
306
* @param creationContext Bean creation context
307
* @param bean Bean definition
308
* @param identifier Scope identifier
309
* @param provider Bean instance provider
310
* @return Bean instance (may be cached or newly created)
311
*/
312
@Override
313
public <T> T getOrCreate(BeanCreationContext creationContext,
314
BeanDefinition<T> bean,
315
BeanIdentifier identifier,
316
Provider<T> provider);
317
318
/**
319
* Remove bean instance from scope (invalidate)
320
* @param creationContext Bean creation context
321
* @param bean Bean definition
322
* @param identifier Scope identifier
323
* @return Removed bean instance, if any
324
*/
325
@Override
326
public <T> Optional<T> remove(BeanCreationContext creationContext,
327
BeanDefinition<T> bean,
328
BeanIdentifier identifier);
329
330
/**
331
* Refresh specific beans based on configuration prefixes
332
* @param configurationPrefixes Prefixes that changed
333
*/
334
public void refresh(String... configurationPrefixes);
335
336
/**
337
* Refresh all beans in this scope
338
*/
339
public void refreshAll();
340
}
341
```
342
343
### Thread-Local Scope Implementation
344
345
The actual scope implementation for thread-local beans.
346
347
```java { .api }
348
/**
349
* Custom scope implementation for thread-local bean storage
350
* Each thread maintains its own bean instances
351
*/
352
@Singleton
353
public class ThreadLocalCustomScope implements CustomScope<ThreadLocal> {
354
355
/**
356
* Get or create thread-local bean instance
357
* @param creationContext Bean creation context
358
* @param bean Bean definition
359
* @param identifier Scope identifier
360
* @param provider Bean instance provider
361
* @return Thread-local bean instance
362
*/
363
@Override
364
public <T> T getOrCreate(BeanCreationContext creationContext,
365
BeanDefinition<T> bean,
366
BeanIdentifier identifier,
367
Provider<T> provider);
368
369
/**
370
* Remove bean from current thread's scope
371
* @param creationContext Bean creation context
372
* @param bean Bean definition
373
* @param identifier Scope identifier
374
* @return Removed bean instance, if any
375
*/
376
@Override
377
public <T> Optional<T> remove(BeanCreationContext creationContext,
378
BeanDefinition<T> bean,
379
BeanIdentifier identifier);
380
381
/**
382
* Clear all thread-local instances for current thread
383
*/
384
public void clear();
385
386
/**
387
* Check if lifecycle callbacks should be invoked
388
* @param bean Bean definition
389
* @return true if lifecycle is enabled for this bean
390
*/
391
public boolean isLifecycleEnabled(BeanDefinition<?> bean);
392
}
393
```
394
395
## Advanced Scope Usage
396
397
### Combining Scopes with Configuration
398
399
```java { .api }
400
// Configuration class that refreshes database connections
401
@Refreshable("datasource")
402
@ConfigurationProperties("datasource")
403
@Singleton
404
public class DataSourceConfiguration {
405
private String url;
406
private String username;
407
private String password;
408
private int maxPoolSize = 10;
409
410
// Configuration will be reloaded when datasource.* properties change
411
412
@Bean
413
@Refreshable("datasource")
414
public DataSource dataSource() {
415
HikariConfig config = new HikariConfig();
416
config.setJdbcUrl(url);
417
config.setUsername(username);
418
config.setPassword(password);
419
config.setMaximumPoolSize(maxPoolSize);
420
return new HikariDataSource(config);
421
}
422
}
423
424
// Thread-local database transaction manager
425
@ThreadLocal(lifecycle = true)
426
@Singleton
427
public class TransactionManager {
428
429
private Transaction currentTransaction;
430
431
@PostConstruct
432
public void initialize() {
433
System.out.println("Transaction manager initialized for thread: "
434
+ Thread.currentThread().getName());
435
}
436
437
public void begin() {
438
if (currentTransaction != null) {
439
throw new IllegalStateException("Transaction already active");
440
}
441
currentTransaction = new Transaction();
442
}
443
444
public void commit() {
445
if (currentTransaction == null) {
446
throw new IllegalStateException("No active transaction");
447
}
448
currentTransaction.commit();
449
currentTransaction = null;
450
}
451
452
@PreDestroy
453
public void cleanup() {
454
if (currentTransaction != null) {
455
currentTransaction.rollback();
456
}
457
}
458
}
459
```
460
461
### Scope Testing
462
463
```java { .api }
464
// Testing refreshable beans
465
@MicronautTest
466
public class RefreshableScopeTest {
467
468
@Inject
469
private RefreshScope refreshScope;
470
471
@Inject
472
private ApplicationEventPublisher<RefreshEvent> eventPublisher;
473
474
@Test
475
public void testConfigurationRefresh() {
476
// Change configuration
477
System.setProperty("database.url", "jdbc:postgresql://new-host:5432/db");
478
479
// Trigger refresh
480
eventPublisher.publishEvent(new RefreshEvent(
481
Map.of("database.url", "jdbc:postgresql://new-host:5432/db")
482
));
483
484
// Verify beans are refreshed
485
// Fresh instances should use new configuration
486
}
487
}
488
489
// Testing thread-local beans
490
@MicronautTest
491
public class ThreadLocalScopeTest {
492
493
@Inject
494
private RequestContext requestContext;
495
496
@Test
497
public void testThreadIsolation() throws InterruptedException {
498
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
499
requestContext.initialize("req-1", "user-1");
500
return requestContext.getRequestId();
501
});
502
503
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
504
requestContext.initialize("req-2", "user-2");
505
return requestContext.getRequestId();
506
});
507
508
// Each thread should have its own context
509
assertThat(future1.get()).isEqualTo("req-1");
510
assertThat(future2.get()).isEqualTo("req-2");
511
}
512
}
513
```