Context module for the Micronaut Framework that extends the micronaut-inject module with additional bean container services such as job scheduling with @Scheduled, event listeners with @EventListener, and immutable configuration properties
—
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.
Custom scope that allows beans to be refreshed when configuration properties change, enabling dynamic reconfiguration without application restart.
/**
* Annotation for beans that should be refreshed when configuration changes
* Creates proxy beans that can be invalidated and recreated on demand
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope
@ScopedProxy
public @interface Refreshable {
/**
* Configuration key prefixes that trigger refresh of this bean
* Empty array means refresh on any configuration change
* @return Array of configuration prefixes to watch
*/
String[] value() default {};
}Usage Examples:
import io.micronaut.runtime.context.scope.Refreshable;
import io.micronaut.context.annotation.ConfigurationProperties;
// Database configuration that refreshes when database properties change
@Refreshable("database")
@ConfigurationProperties("database")
@Singleton
public class DatabaseConfig {
private String url;
private String username;
private String password;
private int maxConnections;
// Getters and setters...
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
// When database.* properties change, this bean is recreated
}
// Service that depends on refreshable configuration
@Refreshable
@Singleton
public class DatabaseService {
private final DatabaseConfig config;
private DataSource dataSource;
public DatabaseService(DatabaseConfig config) {
this.config = config;
this.dataSource = createDataSource(config);
}
// This service will be recreated when its dependencies are refreshed
public Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
}
// Multiple configuration prefixes
@Refreshable({"cache", "redis"})
@Singleton
public class CacheService {
// Refreshes when cache.* or redis.* properties change
}
// Refresh on any configuration change
@Refreshable
@Singleton
public class FlexibleService {
// Refreshes when any configuration property changes
}Scope that stores bean instances in thread-local storage, providing isolated instances per thread.
/**
* Scope annotation for thread-local bean storage
* Each thread gets its own instance of the bean
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope
@ScopedProxy
public @interface ThreadLocal {
/**
* Enable lifecycle support for thread-local beans
* When true, beans will receive lifecycle callbacks (PostConstruct, PreDestroy)
* @return true to enable lifecycle support
*/
boolean lifecycle() default false;
}Usage Examples:
import io.micronaut.runtime.context.scope.ThreadLocal;
// Thread-local context for request processing
@ThreadLocal
@Singleton
public class RequestContext {
private String requestId;
private String userId;
private Instant startTime;
private Map<String, Object> attributes = new HashMap<>();
public void initialize(String requestId, String userId) {
this.requestId = requestId;
this.userId = userId;
this.startTime = Instant.now();
this.attributes.clear();
}
// Each thread gets its own instance
public String getRequestId() { return requestId; }
public String getUserId() { return userId; }
public void setAttribute(String key, Object value) { attributes.put(key, value); }
public Object getAttribute(String key) { return attributes.get(key); }
}
// Thread-local with lifecycle support
@ThreadLocal(lifecycle = true)
@Singleton
public class ThreadLocalResourceManager {
private Connection connection;
private ExecutorService executor;
@PostConstruct
public void initialize() {
// Called once per thread when first accessed
this.connection = createConnection();
this.executor = Executors.newSingleThreadExecutor();
}
@PreDestroy
public void cleanup() {
// Called when thread terminates (if lifecycle = true)
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
logger.warn("Error closing connection", e);
}
}
if (executor != null) {
executor.shutdown();
}
}
}
// Service using thread-local dependencies
@Singleton
public class RequestProcessor {
private final RequestContext requestContext;
public RequestProcessor(RequestContext requestContext) {
this.requestContext = requestContext; // Proxy injected
}
public void processRequest(HttpRequest request) {
// Initialize thread-local context
requestContext.initialize(
UUID.randomUUID().toString(),
extractUserId(request)
);
// Process request - each thread has isolated context
String requestId = requestContext.getRequestId();
System.out.println("Processing request: " + requestId);
}
}Meta-annotation indicating that a scope should use proxy-based implementation.
/**
* Meta-annotation indicating that a scope should use proxies
* Enables lazy evaluation and dynamic behavior for custom scopes
*/
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ScopedProxy {
// Marker annotation for proxy-based scopes
}Event published when configuration refresh occurs, allowing fine-grained control of refresh behavior.
/**
* Event fired when configuration refresh occurs
* Allows listeners to respond to configuration changes
*/
public class RefreshEvent extends ApplicationEvent {
/**
* Create refresh event for specific configuration changes
* @param changes Map of changed configuration keys and their new values
*/
public RefreshEvent(Map<String, Object> changes);
/**
* Create refresh event for full configuration refresh
* Triggers refresh of all @Refreshable beans
*/
public RefreshEvent();
/**
* Get the configuration changes that triggered this refresh
* @return Map of changed properties, or empty for full refresh
*/
public Map<String, Object> getSource();
}Usage Examples:
import io.micronaut.runtime.context.scope.refresh.RefreshEvent;
import io.micronaut.runtime.event.annotation.EventListener;
@Singleton
public class RefreshEventListener {
@EventListener
public void onRefresh(RefreshEvent event) {
Map<String, Object> changes = event.getSource();
System.out.println("Configuration refresh triggered");
System.out.println("Changed properties: " + changes.keySet());
// Handle specific property changes by checking the changes map
if (changes.containsKey("database.url")) {
System.out.println("Database URL changed to: " + changes.get("database.url"));
System.out.println("Reconnecting to database...");
}
if (changes.containsKey("cache.ttl")) {
System.out.println("Cache TTL changed to: " + changes.get("cache.ttl"));
System.out.println("Clearing cache...");
}
// Check for full refresh (when "all" key is present)
if (changes.containsKey("all")) {
System.out.println("Full configuration refresh detected");
}
}
}
// Programmatic refresh triggering
@Singleton
public class ConfigurationManager {
@Inject
private ApplicationEventPublisher<RefreshEvent> eventPublisher;
public void refreshConfiguration() {
// Trigger full refresh
eventPublisher.publishEvent(new RefreshEvent());
}
public void refreshDatabaseConfig(Map<String, Object> newDatabaseProps) {
// Trigger partial refresh for database properties
eventPublisher.publishEvent(new RefreshEvent(newDatabaseProps));
}
}The actual scope implementation for refreshable beans.
/**
* Scope implementation for refreshable beans
* Manages bean lifecycle and invalidation on configuration changes
*/
@Singleton
public class RefreshScope implements CustomScope<Refreshable> {
/**
* Get or create bean instance for the refresh scope
* @param creationContext Bean creation context
* @param bean Bean definition
* @param identifier Scope identifier
* @param provider Bean instance provider
* @return Bean instance (may be cached or newly created)
*/
@Override
public <T> T getOrCreate(BeanCreationContext creationContext,
BeanDefinition<T> bean,
BeanIdentifier identifier,
Provider<T> provider);
/**
* Remove bean instance from scope (invalidate)
* @param creationContext Bean creation context
* @param bean Bean definition
* @param identifier Scope identifier
* @return Removed bean instance, if any
*/
@Override
public <T> Optional<T> remove(BeanCreationContext creationContext,
BeanDefinition<T> bean,
BeanIdentifier identifier);
/**
* Refresh specific beans based on configuration prefixes
* @param configurationPrefixes Prefixes that changed
*/
public void refresh(String... configurationPrefixes);
/**
* Refresh all beans in this scope
*/
public void refreshAll();
}The actual scope implementation for thread-local beans.
/**
* Custom scope implementation for thread-local bean storage
* Each thread maintains its own bean instances
*/
@Singleton
public class ThreadLocalCustomScope implements CustomScope<ThreadLocal> {
/**
* Get or create thread-local bean instance
* @param creationContext Bean creation context
* @param bean Bean definition
* @param identifier Scope identifier
* @param provider Bean instance provider
* @return Thread-local bean instance
*/
@Override
public <T> T getOrCreate(BeanCreationContext creationContext,
BeanDefinition<T> bean,
BeanIdentifier identifier,
Provider<T> provider);
/**
* Remove bean from current thread's scope
* @param creationContext Bean creation context
* @param bean Bean definition
* @param identifier Scope identifier
* @return Removed bean instance, if any
*/
@Override
public <T> Optional<T> remove(BeanCreationContext creationContext,
BeanDefinition<T> bean,
BeanIdentifier identifier);
/**
* Clear all thread-local instances for current thread
*/
public void clear();
/**
* Check if lifecycle callbacks should be invoked
* @param bean Bean definition
* @return true if lifecycle is enabled for this bean
*/
public boolean isLifecycleEnabled(BeanDefinition<?> bean);
}// Configuration class that refreshes database connections
@Refreshable("datasource")
@ConfigurationProperties("datasource")
@Singleton
public class DataSourceConfiguration {
private String url;
private String username;
private String password;
private int maxPoolSize = 10;
// Configuration will be reloaded when datasource.* properties change
@Bean
@Refreshable("datasource")
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(maxPoolSize);
return new HikariDataSource(config);
}
}
// Thread-local database transaction manager
@ThreadLocal(lifecycle = true)
@Singleton
public class TransactionManager {
private Transaction currentTransaction;
@PostConstruct
public void initialize() {
System.out.println("Transaction manager initialized for thread: "
+ Thread.currentThread().getName());
}
public void begin() {
if (currentTransaction != null) {
throw new IllegalStateException("Transaction already active");
}
currentTransaction = new Transaction();
}
public void commit() {
if (currentTransaction == null) {
throw new IllegalStateException("No active transaction");
}
currentTransaction.commit();
currentTransaction = null;
}
@PreDestroy
public void cleanup() {
if (currentTransaction != null) {
currentTransaction.rollback();
}
}
}// Testing refreshable beans
@MicronautTest
public class RefreshableScopeTest {
@Inject
private RefreshScope refreshScope;
@Inject
private ApplicationEventPublisher<RefreshEvent> eventPublisher;
@Test
public void testConfigurationRefresh() {
// Change configuration
System.setProperty("database.url", "jdbc:postgresql://new-host:5432/db");
// Trigger refresh
eventPublisher.publishEvent(new RefreshEvent(
Map.of("database.url", "jdbc:postgresql://new-host:5432/db")
));
// Verify beans are refreshed
// Fresh instances should use new configuration
}
}
// Testing thread-local beans
@MicronautTest
public class ThreadLocalScopeTest {
@Inject
private RequestContext requestContext;
@Test
public void testThreadIsolation() throws InterruptedException {
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
requestContext.initialize("req-1", "user-1");
return requestContext.getRequestId();
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
requestContext.initialize("req-2", "user-2");
return requestContext.getRequestId();
});
// Each thread should have its own context
assertThat(future1.get()).isEqualTo("req-1");
assertThat(future2.get()).isEqualTo("req-2");
}
}Install with Tessl CLI
npx tessl i tessl/maven-io-micronaut--micronaut-context