Application Lifecycle
Understanding the Sproogy application lifecycle is crucial for knowing when to initialize resources, when dependencies are available, and how graceful shutdown works.
Lifecycle Overview
Phase 1: Validation & Configuration
1.1 Annotation Validation
Sproogy verifies your main class has @SproogyApplication:
@SproogyApplication(basePackage = "com.example")
public class Main {
public static void main(String[] args) {
SproogyApp.run(Main.class, args);
}
}
What's checked:
@SproogyApplicationannotation is presentbasePackageis specified
If invalid:
Exception: Main class must be annotated with @SproogyApplication
1.2 Environment Loading
Sproogy loads environment variables from .env:
// EnvLoader.load() is called automatically
EnvLoader.load();
// Now environment variables are available
String port = EnvLoader.get("SOCKET_PORT"); // "8443"
Search order:
.envfile in working directory- System environment variables (override .env)
Example .env:
SOCKET_PORT=8443
DB_URL=jdbc:mysql://localhost:3306/mydb
DB_USER=admin
DB_PASSWORD=secret
Phase 2: Component Discovery
2.1 Component Scanning
Sproogy scans for components in:
- Your package (from
basePackage) - Framework packages (
com.sproogy.*)
Scanned annotations:
@Component@Service@Controller@Repository@Configuration
Example:
@SproogyApplication(basePackage = "com.example")
// Scans: com.example.*, com.sproogy.*
2.2 Pre-DI Module Loading (SproogyLoader)
Before dependency injection, modules can prepare resources using the SproogyLoader interface.
Use case: JPA module needs to scan entities and configure Hibernate BEFORE DI creates repositories.
How it works:
- Sproogy uses Java ServiceLoader to discover
SproogyLoaderimplementations - Calls
loader.load(components, env)for each loader - Loaders can modify configuration, scan for additional classes, etc.
Example: JPA Module
The JPA module implements SproogyLoader:
// Located in: code/jpa/src/main/resources/META-INF/services/com.sproogy.SproogyLoader
com.sproogy.jpa.loader.JpaLoader
public class JpaLoader implements SproogyLoader {
@Override
public void load(Set<Class<?>> components, Map<String, String> env) {
// 1. Scan for @Entity classes
Set<Class<?>> entities = findEntities(components);
// 2. Configure Hibernate with entities
Configuration hibernateConfig = new Configuration();
for (Class<?> entity : entities) {
hibernateConfig.addAnnotatedClass(entity);
}
// 3. Store configuration for later use
// (When EntityManagerFactory bean is created)
}
}
Creating your own loader:
// 1. Implement interface
public class MyCustomLoader implements SproogyLoader {
@Override
public void load(Set<Class<?>> components, Map<String, String> env) {
System.out.println("Custom loader running before DI!");
// Prepare resources, modify configuration, etc.
}
}
// 2. Register via ServiceLoader
// File: src/main/resources/META-INF/services/com.sproogy.SproogyLoader
com.example.MyCustomLoader
Phase 3: Dependency Injection
3.1 Dependency Graph Construction
Sproogy analyzes constructor parameters and @Autowired fields to build a dependency graph.
Example:
@Controller
public class UserController {
@Autowired
private UserService userService;
}
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {}
Dependency graph:
UserController → UserService → UserRepository
3.2 Circular Dependency Detection
Sproogy detects cycles at startup (fail-fast):
@Service
public class A {
@Autowired private B b;
}
@Service
public class B {
@Autowired private A a; // ❌ Cycle detected at startup!
}
Error:
Exception: Circular dependency detected:
A → B → A
3.3 Topological Sort
Dependencies are instantiated leaf-first:
Order:
1. UserRepository (no dependencies)
2. UserService (depends on UserRepository)
3. UserController (depends on UserService)
3.4 Bean Instantiation
For each bean (in dependency order):
- Constructor invocation: Sproogy calls the
@Autowiredconstructor (or no-arg constructor) - Field injection: Inject
@Autowiredfields via reflection - Value injection: Inject
@Valueand@Envfields - Registration: Store in
ApplicationContext
Phase 4: Post-DI Initialization
4.1 Bootstrap Hooks (SproogyBootStrap)
After all beans are created, modules can initialize components using the SproogyBootStrap interface.
Use case: Server module needs to initialize controller routes AFTER DI creates controllers.
How it works:
- Sproogy uses ServiceLoader to discover
SproogyBootStrapimplementations - Calls
bootstrap.init(context)for each bootstrap - Bootstraps can access the fully-initialized
ApplicationContext
Example: Server Module
// Located in: code/socket/server/src/main/resources/META-INF/services/com.sproogy.SproogyBootStrap
com.sproogy.socket.server.bootstrap.ServerBootStrap
public class ServerBootStrap implements SproogyBootStrap {
@Override
public void init(ApplicationContext context) {
// 1. Find all @Controller beans
List<Object> controllers = findControllers(context);
// 2. Scan for @AppMapping methods
for (Object controller : controllers) {
for (Method method : controller.getClass().getDeclaredMethods()) {
if (method.isAnnotationPresent(AppMapping.class)) {
String endpoint = method.getAnnotation(AppMapping.class).value();
// Register route: endpoint → method
RouteRegistry.register(endpoint, controller, method);
}
}
}
// 3. Initialize filter chain
// ...
}
}
Creating your own bootstrap:
// 1. Implement interface
public class MyCustomBootstrap implements SproogyBootStrap {
@Override
public void init(ApplicationContext context) {
System.out.println("Custom bootstrap running after DI!");
// Access beans, initialize resources, etc.
MyService myService = context.getBean(MyService.class);
myService.initialize();
}
}
// 2. Register via ServiceLoader
// File: src/main/resources/META-INF/services/com.sproogy.SproogyBootStrap
com.example.MyCustomBootstrap
4.2 Server Startup
If a ServerFactory bean exists, Sproogy automatically starts the server:
@Component
public class MyServerFactory implements ServerFactory {
@Override
public Server createServer() {
// Return configured SSL server
return new DefaultServer(...);
}
}
What happens:
ServerFactory.createServer()is calledserver.startServer()is invoked (non-blocking)- Shutdown hook is registered to call
server.stopServer()on JVM exit
Phase 5: Graceful Shutdown
When the JVM exits (Ctrl+C, kill signal, etc.), Sproogy gracefully shuts down the server.
Shutdown Sequence
- Stop accepting new connections:
serverSocket.close() - Wait for active requests: Threads handling current requests are allowed to finish
- Close client sockets: All active connections are closed
- Log shutdown: "Server stopped successfully"
Shutdown hook registration:
// Automatically registered by ServerFactory
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.stopServer();
}));
Custom Shutdown Logic
Add shutdown hooks for your own resources:
@Component
public class DatabaseService {
private HikariDataSource dataSource;
public DatabaseService() {
this.dataSource = new HikariDataSource(config);
// Register shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Closing database connections...");
dataSource.close();
}));
}
}
Bean Lifecycle Timing
| Event | When It Happens | What's Available |
|---|---|---|
| Constructor called | During Phase 3 (DI) | Constructor parameters (injected dependencies) |
@Autowired fields injected | After constructor | All injected dependencies |
SproogyBootStrap.init() | After all beans created | Full ApplicationContext |
| Server starts | After bootstraps | Everything |
| Application runs | After server starts | Everything |
| Shutdown hooks | On JVM exit | Everything (being destroyed) |
Lazy Initialization
By default, all beans are created at startup. Use lazy = true to defer creation:
@Component(lazy = true)
public class HeavyResource {
public HeavyResource() {
System.out.println("Expensive initialization...");
// Load large file, connect to external service, etc.
}
}
@Service
public class MyService {
@Autowired
private HeavyResource resource; // Created only when MyService is created
}
Timing:
- Without
lazy: Created during Phase 3 (DI) - With
lazy: Created when first injected or retrieved
Execution Order Example
Let's trace a complete application startup:
// Main.java
@SproogyApplication(basePackage = "com.example")
public class Main {
public static void main(String[] args) {
System.out.println("[1] main() called");
SproogyApp.run(Main.class, args);
System.out.println("[9] SproogyApp.run() returned");
}
}
// MyLoader.java
public class MyLoader implements SproogyLoader {
@Override
public void load(Set<Class<?>> components, Map<String, String> env) {
System.out.println("[3] SproogyLoader.load() - before DI");
}
}
// MyService.java
@Service
public class MyService {
public MyService() {
System.out.println("[5] MyService constructor");
}
@Autowired
private MyRepository repository;
// Field injected here (no way to log it)
}
// MyRepository.java
@Repository
public interface MyRepository extends JpaRepository<User, Long> {}
// MyBootstrap.java
public class MyBootstrap implements SproogyBootStrap {
@Override
public void init(ApplicationContext context) {
System.out.println("[7] SproogyBootStrap.init() - after DI");
}
}
// MyServerFactory.java
@Component
public class MyServerFactory implements ServerFactory {
@Override
public Server createServer() {
System.out.println("[8] ServerFactory.createServer()");
return new DefaultServer(...);
}
}
Console output:
[1] main() called
[2] Loading .env file
[3] SproogyLoader.load() - before DI
[4] Building dependency graph
[5] MyService constructor
[6] Injecting @Autowired fields
[7] SproogyBootStrap.init() - after DI
[8] ServerFactory.createServer()
[9] SproogyApp.run() returned
Server running on port 8443...
Common Patterns
Pattern 1: Initialization Order Control
If ServiceA must initialize before ServiceB:
@Service
public class ServiceA {
public ServiceA() {
System.out.println("ServiceA init");
}
}
@Service
public class ServiceB {
private final ServiceA serviceA;
@Autowired
public ServiceB(ServiceA serviceA) {
// ServiceA constructor has already run
System.out.println("ServiceB init");
this.serviceA = serviceA;
}
}
Sproogy guarantees ServiceA is created before ServiceB (dependency graph).
Pattern 2: Resource Initialization in Bootstrap
For resources that need the full ApplicationContext:
public class CacheBootstrap implements SproogyBootStrap {
@Override
public void init(ApplicationContext context) {
CacheManager cache = context.getBean(CacheManager.class);
cache.loadInitialData(); // Needs all services to be ready
}
}
Pattern 3: Cleanup on Shutdown
Always clean up resources:
@Component
public class ConnectionPool {
private final List<Connection> connections = new ArrayList<>();
public ConnectionPool() {
// Initialize connections
for (int i = 0; i < 10; i++) {
connections.add(createConnection());
}
// Register cleanup
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
connections.forEach(Connection::close);
}));
}
}
Next Steps
Now that you understand the lifecycle, explore all available annotations:
Or start building with the Getting Started guide: