Aspectran Beans is a powerful Inversion of Control (IoC) container built into the core of the Aspectran framework. Inspired by the robust concepts of Spring Beans (IoC, DI, etc.), it has been redesigned from the ground up to align with Aspectran’s core philosophy of being POJO-based, simple, and fast in development and startup speed.
1. Core Concepts: IoC and DI
The core of Aspectran Beans is to help you write cleaner, more modular, and easier-to-test code by managing your application’s objects (called “beans”).
IoC (Inversion of Control): Instead of you creating and managing the lifecycle of your objects, the Aspectran container does it for you. You just define the objects, and the framework instantiates, configures, and assembles them at the appropriate time. This “inversion” of control allows you to focus solely on your business logic.
DI (Dependency Injection): This is the primary mechanism for implementing IoC. Instead of an object creating its own dependencies (
new MyService()
), it receives them from an external source (the IoC container). This reduces the coupling between components, making them easier to manage, test, and reuse.
2. Basics: Bean Definition and Scopes
Automatic Detection with @Component
The easiest way to register a bean is to add the @Component
annotation to a class. At application startup, Aspectran’s classpath scanner will automatically detect it and register it as a bean.
package com.example.myapp.service;
import com.aspectran.core.component.bean.annotation.Component;
@Component
public class MyService {
public String getMessage() {
return "Hello from MyService!";
}
}
Explicit Definition with @Bean
The @Bean
annotation is used to explicitly declare a bean and specify detailed attributes like its ID or scope. It can be applied to a class or a factory method.
- On a class: Can be used with
@Component
to specify the bean’s ID.@Component @Bean(id = "anotherService") public class AnotherService { /* ... */ }
- On a factory method: Useful for complex initialization logic or for registering third-party library objects as beans. Create a method that returns an object within a
@Component
class and annotate it with@Bean
.@Component public class AppConfig { @Bean public SomeLibraryClient someLibraryClient() { return new SomeLibraryClient("api.example.com", "your-api-key"); } }
In-depth Analysis of Bean Scopes
Bean scopes control the lifecycle and visibility of a bean instance. They can be set with the @Scope
annotation.
Scope | Description | Lifecycle | Primary Use Case |
---|---|---|---|
singleton | Single instance in the context | Entire application | Stateless services, DAOs |
prototype | New instance per request | Managed by GC | Stateful objects, Builders |
request | New instance per request | Single Activity execution | Request-related data handling |
session | New instance per session | Single user session | User-specific data management |
singleton
(default): Only one instance is created and shared within the IoC container.prototype
: A new instance is created each time the bean is injected or requested. The container does not manage the lifecycle after creation.request
: A single instance is maintained within the scope of anActivity
execution (e.g., an HTTP request). The currentActivity
must support aRequestAdapter
.session
: A single instance is maintained within the scope of a user session. The currentActivity
must support aSessionAdapter
.
import com.aspectran.core.component.bean.annotation.Scope;
import com.aspectran.core.context.rule.type.ScopeType;
@Component
@Bean
@Scope(ScopeType.PROTOTYPE)
public class MyPrototypeBean { /* ... */ }
3. Core: Dependency Injection
The @Autowired
annotation is used to inject dependencies between beans.
Constructor Injection (Recommended)
This is the best way to make dependencies immutable and ensure that an object is in a complete state when it is created.
@Component
public class MyController {
private final MyService myService;
@Autowired
public MyController(MyService myService) {
this.myService = myService;
}
}
Field and Setter Injection
Useful for injecting optional dependencies, but constructor injection should be considered first.
- Setter Injection: Annotate a
public
setter method with@Autowired
. - Field Injection: Can only be injected into
public
fields and is not recommended.
Resolving Ambiguity with @Qualifier
When there are multiple beans of the same type, you can use @Qualifier("beanId")
to specify the particular bean to inject.
public interface NotificationService { /* ... */ }
@Component @Bean("email")
public class EmailNotificationService implements NotificationService { /* ... */ }
@Component @Bean("sms")
public class SmsNotificationService implements NotificationService { /* ... */ }
@Component
public class OrderService {
private final NotificationService notificationService;
@Autowired
public OrderService(@Qualifier("email") NotificationService notificationService) {
this.notificationService = notificationService;
}
}
Injecting Configuration Values with @Value
You can use the @Value
annotation to inject the evaluation result of an AsEL expression (usually an external configuration value).
@Component
public class AppInfo {
private final String appVersion;
@Autowired
public AppInfo(@Value("%{app^version:1.0.0}") String appVersion) {
this.appVersion = appVersion;
}
}
Collection Injection (List<T>
, Map<String, T>
)
You can inject all beans that implement the same interface at once into a List
or Map
. This is very useful for implementing patterns like the Strategy Pattern.
// Injects all NotificationService implementations
@Component
public class NotificationManager {
private final List<NotificationService> services;
private final Map<String, NotificationService> serviceMap;
@Autowired
public NotificationManager(List<NotificationService> services) {
this.services = services; // [EmailNotificationService, SmsNotificationService]
this.serviceMap = services.stream()
.collect(Collectors.toMap(s -> s.getClass().getSimpleName(), s -> s));
}
public void sendToAll(String message) {
for (NotificationService service : services) {
service.send(message);
}
}
}
Optional Dependency Injection (Optional<T>
)
When you need to inject a bean that may not exist, such as one that is only active in a specific profile, you can use java.util.Optional<T>
.
@Component
public class MainService {
private final Optional<OptionalService> optionalService;
@Autowired
public MainService(Optional<OptionalService> optionalService) {
this.optionalService = optionalService;
}
public void doSomething() {
optionalService.ifPresent(service -> service.performAction());
}
}
4. Advanced Features
Environment-specific Configuration with @Profile
The @Profile
annotation allows you to register a bean only when a specific profile (e.g., dev
, prod
) is active.
// Mock service to be used only in the development environment
@Component
@Profile("dev")
public class MockNotificationService implements NotificationService { /* ... */ }
// Service that sends actual SMS in the production environment
@Component
@Profile("prod")
public class RealSmsNotificationService implements NotificationService { /* ... */ }
The active profile can be specified in the Aspectran configuration.
Creating Complex Beans with FactoryBean
Implement the FactoryBean
interface when the creation logic is very complex or requires encapsulation. The object returned by the getObject()
method is registered as the actual bean.
@Component
@Bean("myProduct")
public class MyProductFactory implements FactoryBean<MyProduct> {
@Override
public MyProduct getObject() throws Exception {
// Complex creation and configuration logic
return new MyProduct();
}
}
Accessing the Framework with Aware
Interfaces
By implementing Aware
interfaces like ActivityContextAware
, a bean can access Aspectran’s internal objects (e.g., ActivityContext
).
@Component
public class MyAwareBean implements ActivityContextAware {
private ActivityContext context;
@Override
public void setActivityContext(ActivityContext context) {
this.context = context;
}
}
Event Handling (Publish-Subscribe)
Aspectran provides a publish-subscribe event handling mechanism for loose coupling between components (beans) within the application. This allows you to easily implement scenarios where the result of a specific logic needs to be propagated to multiple other components without creating direct dependencies, simply through events.
Creating an Event Listener (@EventListener
)
You can easily create a listener to receive and handle events using the @EventListener
annotation.
- Annotate the method that will handle the event with
@EventListener
. - The method must have exactly one parameter, and the type of this parameter becomes the type of the event to subscribe to.
- When the framework starts, it finds methods annotated with
@EventListener
in beans registered with@Component
and automatically registers them as event listeners.
Example: A listener that handles an order completion event
// 1. Define the event (POJO)
public class OrderCompletedEvent {
private final String orderId;
public OrderCompletedEvent(String orderId) {
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
// 2. Define the event listener bean
@Component
public class OrderEventListener {
@EventListener
public void handleOrderCompleted(OrderCompletedEvent event) {
// This method is called when an OrderCompletedEvent is published.
System.out.println("Order [" + event.getOrderId() + "] has been completed.");
// ... Subsequent processing logic such as decreasing stock, sending notifications, etc. ...
}
@EventListener
public void handleAnyObject(Object event) {
// You can declare the type as Object to receive all types of events.
}
}
Publishing an Event (EventPublisher
)
Events are published through the EventPublisher
interface. You just need to inject a bean of this type and call the publish()
method.
- Define an event publishing bean and inject it.
- Call the
publish(Object event)
method to publish an event. - The
EventPublisher
checks the type of the published event object and propagates the event to all@EventListener
s that subscribe to that event.
Example: Publishing an order completion event from an order service
// Explicitly define the order event publishing bean
@Component
public class OrderEventPublisher extends InstantActivitySupport {
// Publish order completed event
public void publish(OrderCompletedEvent orderCompletedEvent) {
getEventPublisher().publish(orderCompletedEvent);
}
// Publish order canceled event
public void publish(OrderCanceledEvent orderCanceledEvent) {
getEventPublisher().publish(orderCanceledEvent);
}
}
@Component
public class OrderService {
private final EventPublisher orderEventPublisher;
@Autowired
public OrderService(OrderEventPublisher orderEventPublisher) {
this.orderEventPublisher = orderEventPublisher;
}
public void completeOrder(String orderId) {
// ... Order completion processing logic ...
System.out.println("Processing completion for order [" + orderId + "]");
// Create and publish the event
OrderCompletedEvent event = new OrderCompletedEvent(orderId);
this.orderEventPublisher.publish(event);
}
public void cancelOrder(String orderId) {
// ... Order cancellation processing logic ...
System.out.println("Processing cancellation for order [" + orderId + "]");
// Create and publish the event
OrderCanceledEvent event = new OrderCanceledEvent(orderId);
this.orderEventPublisher.publish(event);
}
}
By using the event mechanism, the OrderService
can focus on its core responsibility without needing to know what tasks should be performed after an order is completed. Other components interested in the event will continue the work through @EventListener
, greatly improving the system’s flexibility and extensibility.
Asynchronous Method Execution (@Async
)
Using the @Async
annotation, you can execute long-running tasks asynchronously in a separate thread, allowing the current request processing thread to return immediately without being blocked. This feature is implemented through Aspectran’s bean proxy.
Basic Usage of @Async
When you add the @Async
annotation to a bean’s method, that method will be called asynchronously in a separate thread. The return type must be void
or an implementation of java.util.concurrent.Future
.
@Component
@Bean("myAsyncTaskService")
public class MyAsyncTaskService {
@Async
public void doSomething() {
// This code runs in a separate thread.
}
@Async
public Future<String> doSomethingAndReturn() {
// Executes the task and returns the result via a Future object.
return new CompletableFuture<>(() -> "Hello from async task!");
}
}
Asynchronous Context and ProxyActivity
- When an
@Async
method is called, if there is noActivity
in the current thread, a new lightweight context for executing advice, aProxyActivity
, is created. - If multiple
@Advisable
methods are called in a chain within a single asynchronous task, the initially createdProxyActivity
instance is continuously shared within that thread. This allows for maintaining a consistent context within the unit of work. - If
@Async
is called in a thread where anActivity
already exists, theProxyActivity
is created by wrapping the existingActivity
. In this case, it shares the originalActivity
’s data (ActivityData
), enabling data exchange between the asynchronous task and the caller.
Caution when using CompletableFuture
If you use combinations of CompletableFuture
that execute code in a new thread pool, such as CompletableFuture.supplyAsync()
or thenApplyAsync()
, within an @Async
method, Aspectran’s Activity
context will not be propagated to that thread. In other words, calling getCurrentActivity()
in the new thread created by CompletableFuture
will result in a NoActivityStateException
.
It is safer to handle all tasks synchronously within the thread created by @Async
and wrap only the final result with CompletableFuture.completedFuture()
.
@Async
public Future<String> correctUsage() {
// This block runs in the thread managed by @Async, so it can access the Activity context
getCurrentActivity().getActivityData().put("key", "value");
// Use CompletableFuture only for returning the result
return CompletableFuture.completedFuture("some-result");
}
@Async
public Future<String> wrongUsage() {
// Wrong usage example: Cannot access the Activity context inside supplyAsync
return CompletableFuture.supplyAsync(() -> {
// This block runs in a separate thread, so
// calling getCurrentActivity() will cause a NoActivityStateException.
getCurrentActivity().getActivityData().put("key", "value"); // Exception occurs!
return "some-result";
});
}
Using a Custom Executor
If you want to apply a separate thread pool policy instead of the default Executor, you can define a bean of type AsyncTaskExecutor
yourself and specify the bean’s ID or class in the @Async
annotation.
// Use an Executor registered with the ID "myCustomExecutor"
@Async("myCustomExecutor")
public void doSomethingWithCustomExecutor() {
// ...
}
5. Bean Lifecycle Management
Complete Lifecycle Sequence
A singleton bean is created and destroyed in the following order:
- Instantiation: Constructor call
- Dependency Injection: Inject dependencies into fields and setters annotated with
@Autowired
- Aware Interface Processing: Call the
set*()
methods ofAware
interfaces - Post-Initialization Callbacks:
- Call methods annotated with
@Initialize
- Call the
initialize()
method of theInitializableBean
interface
- Call methods annotated with
- (Bean is ready to use)
- Pre-Destruction Callbacks:
- Call methods annotated with
@Destroy
- Call the
destroy()
method of theDisposableBean
interface
- Call methods annotated with
Annotation-based Callbacks: @Initialize
& @Destroy
@Initialize
: Executes initialization logic after all dependencies have been injected.@Destroy
: Executes cleanup logic just before the bean is destroyed.
@Component
public class LifecycleBean {
@Initialize
public void setup() { /* ... */ }
@Destroy
public void cleanup() { /* ... */ }
}
Interface-based Callbacks: InitializableBean
& DisposableBean
You can also achieve the same purpose by directly implementing the framework interfaces.
@Component
public class LifecycleBean implements InitializableBean, DisposableBean {
@Override
public void initialize() throws Exception { /* ... */ }
@Override
public void destroy() throws Exception { /* ... */ }
}
6. Configuration
Enabling Annotation-based Configuration
To enable beans using annotations, you must specify the base packages to scan in the context.scan
parameter of Aspectran’s main configuration file (in APON format).
context: {
scan: [
com.example.myapp
]
}
XML-based Configuration
XML provides high flexibility by allowing you to define the configuration and relationships of beans without changing the source code.
Basic Definition and Dependency Injection
Define a bean with the <bean>
element and set dependencies using the <argument>
(constructor injection) and <property>
(setter injection) child elements.
<bean id="myService" class="com.example.myapp.service.MyService"/>
<bean id="myController" class="com.example.myapp.controller.MyController">
<!-- Constructor argument injection -->
<argument>#{myService}</argument>
<!-- Setter property injection -->
<property name="timeout" value="5000"/>
</bean>
Conditional Item Grouping with Profiles
If you need to activate or deactivate multiple <argument>
or <property>
elements together only in a specific profile, you can use the <arguments>
or <properties>
wrapper elements. By specifying the profile
attribute on these wrapper elements, all contained <item>
elements become dependent on that profile.
<bean id="dbConnector" class="com.example.DbConnector">
<properties profile="dev">
<item name="url" value="jdbc:h2:mem:devdb"/>
<item name="username" value="sa"/>
</properties>
<properties profile="prod">
<item name="url" value="jdbc:mysql://prod.db.server/main"/>
<item name="username" value="prod_user"/>
</properties>
</bean>
The recommended style is to use <argument>
/<property>
for defining individual items and <arguments>
/<properties>
only for grouping multiple items according to a profile.
Component Scanning (<bean scan="...">
)
You can also enable component scanning in XML using <bean scan="...">
.
<!-- Scan the 'com.example.myapp' package and all its sub-packages -->
<bean scan="com.example.myapp.**"/>
Inner Beans and Nesting Limits
You can define an anonymous inner bean that will only be used as a property of another bean. To prevent excessive configuration complexity, Aspectran limits the maximum nesting of inner beans to 3 levels (depth).
<bean id="outerBean" class="com.example.OuterBean">
<properties>
<item name="inner">
<!-- Inner bean without an ID (level 1) -->
<bean class="com.example.InnerBean">
<!-- ... -->
</bean>
</item>
</properties>
</bean>
Combining Annotations and XML Configuration
You can use annotation-based component scanning and explicit XML-based bean definitions together. It is common to use component scanning as the default and use XML to override specific beans or register external libraries. If a bean with the same ID is defined in both, the configuration that is loaded later may take precedence, and you can force an override with the <bean important="true">
attribute.
7. Best Practices and Pitfalls
Prefer Constructor Injection
- Immutability: You can use
final
fields, ensuring that the bean’s state does not change. - Explicit Dependencies: All dependencies required for an object to function are clearly exposed in the constructor.
- Circular Reference Prevention: When using constructor injection, if a circular reference occurs where bean A and B need each other, an error will occur at application startup, allowing you to discover and resolve the problem immediately.
Avoid Circular Dependencies
A circular dependency can be a sign of a design problem. It may mean that two classes have too many responsibilities, so consider refactoring to move responsibilities to a third class. In unavoidable cases, using setter injection can solve the circular reference problem.
Understand the Lifecycle of prototype
Beans
After the container creates and injects dependencies into a prototype
scope bean, it no longer manages it. Therefore, destruction-related callbacks like @Destroy
or DisposableBean
are not called. If a prototype
bean holds important resources like a database connection, you must manually call the logic to release those resources.
Singleton Beans and State
Since a singleton bean has only one instance throughout the application, it can be accessed by multiple threads simultaneously. If a singleton bean has a mutable state (e.g., member variables), concurrency issues can arise. It is best to design singleton beans to be stateless. If state is absolutely necessary, use ThreadLocal
or carefully implement synchronization.