Spring Configuration and Wiring
Overview
The configuration class brings everything together, registering states and transitions with the Brobot framework.
Initial State Verification (v1.1.0+)
Brobot now provides automatic initial state verification through configuration:
Configuration Properties
# Enable automatic verification on startup
brobot.startup.verify-initial-states = true
# States to verify (comma-separated)
brobot.startup.initial-states = PROMPT,WORKING
# Search all states if specified ones not found
brobot.startup.fallback-search = false
# Activate only the first found state
brobot.startup.activate-first-only = false
# Delay before verification (seconds)
brobot.startup.startup-delay = 2
How It Works
- Automatic Execution: When enabled, verification runs automatically after Spring context initialization
- State Search: Searches for configured states on the actual screen
- State Activation: Found states are automatically activated in StateMemory
- Mock Support: In mock mode, randomly selects states based on equal probability
Benefits
- No Boilerplate: No need to manually set initial states in code
- Configuration-Driven: Change behavior without recompiling
- Consistent: Same approach across all Brobot applications
- Testable: Works seamlessly with mock mode
StateRegistrationListener.java (Recommended Approach)
package com.claude.automator.config;
import com.claude.automator.states.PromptState;
import com.claude.automator.states.WorkingState;
import com.claude.automator.transitions.PromptTransitions;
import com.claude.automator.transitions.WorkingTransitions;
import io.github.jspinak.brobot.config.FrameworkInitializer;
import io.github.jspinak.brobot.model.transition.StateTransitionStore;
import io.github.jspinak.brobot.navigation.service.StateService;
import io.github.jspinak.brobot.statemanagement.StateMemory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* Registers states and transitions after the application is fully ready.
* This ensures the Brobot framework is initialized before states try to load images.
*/
@Component
@RequiredArgsConstructor
@Slf4j
public class StateRegistrationListener {
private final StateService stateService;
private final StateTransitionStore stateTransitionStore;
private final StateMemory stateMemory;
private final FrameworkInitializer frameworkInitializer;
private final WorkingState workingState;
private final PromptState promptState;
private final WorkingTransitions workingTransitions;
private final PromptTransitions promptTransitions;
@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
log.info("Application ready - registering states with StateService");
try {
// Register states with the StateService
stateService.save(workingState.getState());
stateService.save(promptState.getState());
// Register state transitions
stateTransitionStore.add(workingTransitions.getStateTransitions());
stateTransitionStore.add(promptTransitions.getStateTransitions());
log.info("States and transitions registered successfully");
// Important: Initialize the state structure after ALL states are registered
// This will properly set up the transitions joint table with all state IDs
// The first call in FrameworkLifecycleManager only had the Unknown state
frameworkInitializer.initializeStateStructure();
log.info("State structure initialized with transitions joint table");
// Set initial active state - using Prompt as starting state
Long promptStateId = stateService.getStateId(PromptState.Name.PROMPT.toString());
if (promptStateId != null) {
stateMemory.addActiveState(promptStateId);
log.info("Set PROMPT as initial active state with ID: {}", promptStateId);
}
} catch (Exception e) {
log.error("Error registering states: ", e);
}
}
}
Key Concepts
1. Why Use @EventListener Instead of @PostConstruct
The recommended approach uses @EventListener(ApplicationReadyEvent.class)
instead of @PostConstruct
for several important reasons:
- Framework Initialization Timing: Ensures Brobot framework is fully initialized before states try to load images
- Dependency Resolution: All Spring beans are completely wired and ready
- Error Prevention: Avoids initialization order issues that can occur with @PostConstruct
- Better Integration: Works seamlessly with Spring Boot's lifecycle
2. Critical Framework Initialization
// This MUST be called after ALL states are registered
frameworkInitializer.initializeStateStructure();
This second call to initializeStateStructure()
is crucial because:
- The first call during framework startup only knows about the Unknown state
- This call properly sets up the transitions joint table with all registered state IDs
- Without this, state transitions may not work correctly
3. Registration Order
- States first: Must exist before transitions reference them
- Transitions second: Need state IDs to function
- Framework structure initialization: Sets up internal mappings
- Initial state last: Requires states to be registered
Alternative Configuration Patterns
Bean-Based Configuration
@Configuration
public class BeanConfiguration {
@Bean
public StateTransitions promptTransitions(PromptState promptState, Action action) {
return new StateTransitions.Builder(PromptState.Name.PROMPT.toString())
// ... build transitions
.build();
}
}
Conditional Configuration
@Configuration
@ConditionalOnProperty(name = "claude.automator.enabled", havingValue = "true")
public class ConditionalStateConfiguration {
// Only loads when property is set
}
Profile-Based Configuration
@Configuration
@Profile("claude-automation")
public class ClaudeAutomationConfiguration {
// Active only in specific profile
}
Error Handling Strategies
Validation on Startup
@PostConstruct
public void validateConfiguration() {
// Verify all states are registered
if (stateService.getStateId(WorkingState.Name.WORKING.toString()) == null) {
throw new IllegalStateException("Working state not found!");
}
// Verify transitions exist
if (stateTransitionStore.getTransitions(WorkingState.Name.WORKING.toString()) == null) {
throw new IllegalStateException("Working transitions not found!");
}
}
Graceful Degradation
private void setInitialState() {
try {
Long stateId = stateService.getStateId(PromptState.Name.PROMPT.toString());
if (stateId != null) {
stateMemory.addActiveState(stateId);
}
} catch (Exception e) {
log.warn("Could not set initial state, continuing with defaults", e);
}
}
Configuration Best Practices
- Use constructor injection (via @RequiredArgsConstructor)
- Log important configuration steps
- Validate critical components
- Handle failures gracefully
- Keep configuration focused on wiring, not business logic
Additional Configuration Files
Custom Properties
Create src/main/resources/claude-automator.properties
:
# Claude Automator Settings
claude.automator.monitor.interval=2
claude.automator.monitor.initial-delay=5
claude.automator.icon.timeout=10.0
Loading Custom Properties
@ConfigurationProperties(prefix = "claude.automator")
@Component
@Getter
@Setter
public class ClaudeAutomatorProperties {
private MonitorProperties monitor = new MonitorProperties();
private IconProperties icon = new IconProperties();
@Getter
@Setter
public static class MonitorProperties {
private int interval = 2;
private int initialDelay = 5;
}
@Getter
@Setter
public static class IconProperties {
private double timeout = 10.0;
}
}
Next Steps
With everything configured, let's run the application and see it in action!