CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-io-micronaut--micronaut-context

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

Pending
Overview
Eval results
Files

scopes.mddocs/

Scope Management

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.

Capabilities

@Refreshable Scope

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
}

@ThreadLocal Scope

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);
    }
}

@ScopedProxy Annotation

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
}

RefreshEvent

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));
    }
}

RefreshScope Implementation

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();
}

Thread-Local Scope Implementation

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);
}

Advanced Scope Usage

Combining Scopes with Configuration

// 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();
        }
    }
}

Scope Testing

// 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

docs

events.md

index.md

runtime.md

scheduling.md

scopes.md

server.md

shutdown.md

tile.json