Bean Collection
The BeanCollection
class in PySpring is designed to help integrate third-party code, especially when developers cannot modify the code directly. Below is a breakdown of its key features and functionality.
Purpose and Key Features
-
Bean Management: The
BeanCollection
helps organize and manage a collection of beans (components) within a class. It provides a way to access and control these beans efficiently. -
Scanning for Beans: The
scan_beans()
method scans the current class for methods that create beans. These methods are identified by their names, which by default start with the identifiercreate
(e.g.,createMyBean
). -
Bean Creation: Beans are created using methods whose return types are annotated. The return type of a bean creation method indicates the type of the bean being created.
-
BeanView: Each bean is represented by a
BeanView
object, which holds:- The bean itself
- The bean's name
- The bean creation function
- A validation method (
is_valid_bean()
) to check if the bean's name matches the name of the class it returns. - Error Handling: Common errors are handled through exceptions, such as:
BeanConflictError
: Raised when a bean with the same name already exists.InvalidBeanError
: Raised when the bean's name doesn't match the class name.
Properties Integration
BeanCollection
can also work with properties, which are loaded before the beans are created. This is a key aspect of how BeanCollection
integrates with the broader PySpring framework.
How Properties Interact with BeanCollection:
-
Properties Loading: PySpring loads properties from a designated properties file using the
Properties
class and_PropertiesLoader
. This happens before the initialization of the Inversion of Control (IoC) container. -
Accessing Properties: Once loaded, properties are accessible via the
get_properties()
method in theApplicationContext
. The properties are stored in thesingleton_properties_instance_container
. -
Dependency Injection: PySpring's dependency injection mechanism ensures that any required properties are injected into a bean before it is created. This is done by inspecting the type annotations of the bean creation function.
How it Works:
-
Configuration: The application is configured to load properties from a specific file as defined in
app-config.json
. -
Loading Properties: During initialization,
PySpringApplication
uses the_PropertiesLoader
to load properties into a dictionary. The keys in the dictionary match the keys defined in theProperties
class. Theload_properties()
method ofApplicationContext
then makes these properties available. -
Bean Creation with Properties: When a bean is created via a method in
BeanCollection
, PySpring checks for any dependencies, including properties. If a property is declared as a dependency, it will be injected into the bean before the bean is created.
Example:
Suppose you have a properties class and a bean collection:
from py_spring_core import Properties, BeanCollection
class MyProperties(Properties):
__key__ = "my_properties"
my_config_value: str
class MyBeanCollection(BeanCollection):
properties: MyProperties
@classmethod
def create_my_bean(cls) -> MyBean:
return MyBean(cls.properties.my_config_value)
class MyBean:
def __init__(self, config_value: str):
self.config_value = config_value`
MyBeanCollection
creates aMyBean
instance and depends onMyProperties
to fetch a configuration value.- When the
ApplicationContext
initializes the bean, it first loads the properties from the properties file and injects theMyProperties
instance into thecreateMyBean
method.
How BeanCollection Works
-
Identifying Bean Creation Functions: The
scan_beans()
method identifies bean creation methods by checking if their names start with a specific identifier, which iscreate
by default. For example, a method namedcreateMyBean
would be recognized as a bean creation method. -
Creating
BeanView
Instances: For each method found,scan_beans()
creates aBeanView
instance. This object stores information about the bean, such as the creation function and the bean's class type. -
Bean Validation: The
is_valid_bean()
method withinBeanView
checks whether the name of the bean matches the name of the class it is supposed to create. For instance, if a method is annotated to return an object of typeMyBean
, the bean's name must also beMyBean
. -
Dependency Injection: While
BeanCollection
is responsible for creating bean instances, dependency injection ensures that any required dependencies (including properties) are automatically injected into the beans.
Integration with PySpring
-
Registration:
BeanCollection
classes are registered within theApplicationContext
. This allows PySpring to manage and inject these beans into other components of the application. -
Singleton Management: Beans created by
BeanCollection
are treated as singletons by theApplicationContext
, meaning that only one instance of each bean is created and reused. -
Usage: Beans managed by
BeanCollection
can be accessed through theApplicationContext
using theget_bean()
method.
Benefits of Using BeanCollection
-
Loose Coupling: The application code does not need to know how third-party objects are created, promoting separation of concerns and flexibility.
-
Centralized Management: Beans are managed in one central location, making it easier to find and maintain them.
-
Simplified Integration:
BeanCollection
provides a standardized approach to integrating third-party or external code without needing to modify it. -
Dependency Injection: Beans can take advantage of PySpring's dependency injection, allowing them to access other registered components, controllers, or beans.
-
Properties Support:
BeanCollection
can load properties before bean creation, making it possible to configure beans dynamically using values from external properties files.
Beans Interacting with Other Components
Beans created within a BeanCollection
can interact with other components and properties defined within the project, not just the properties used during their initial creation. PySpring's dependency injection mechanism allows these beans to access other registered entities (components, beans, and properties) after they are instantiated.
How This Interaction Works:
-
Dependency Injection: When a bean is created, PySpring examines the type hints of the bean creation method, as well as the class definitions of other components, for type annotations. It uses these annotations to inject the necessary dependencies into the bean, including other beans, properties, and components.
-
Component Interaction: In your example,
ExampleService
has type hints forExampleProperties
andMyBean
. WhenExampleService
is instantiated, PySpring uses dependency injection to inject the correct instances of these dependencies. Additionally,AnotherExampleService
depends onExampleService
, which allows it to access properties viaExampleService
. -
Lifecycle Methods: Components like
ExampleService
andAnotherExampleService
have lifecycle methods (post_construct
andpre_destroy
) that PySpring automatically calls. Thepost_construct
method is executed after a component is initialized, meaning all its dependencies have been injected. At this point, the component can begin using the injected objects and properties.
class MyProperties(Properties):
key = "my_properties"
my_config_value: str
# this may be a third party class that you cannot modify
class MyBean:
def __init__(self, config_value: str):
self.config_value = config_value
class MyBeanCollection(BeanCollection):
properties: MyProperties
@classmethod
def createMyBean(cls) -> MyBean:
return MyBean(cls.properties.my_config_value)
class ExampleProperties(Properties):
key = "example"
value: str
class ExampleService(Component):
example_properties: ExampleProperties
my_bean: MyBean
def post_construct(self) -> None:
logger.info(f"Example value: {self.example_properties.value}")
def pre_destroy(self) -> None:
logger.info("Pre destroy method called")
class AnotherExampleService(Component):
example_service: ExampleService
def post_construct(self) -> None:
logger.info("AnotherExampleService post construct called")
logger.info(f"Example value: {self.example_service.example_properties.value}")
How It Works in This Example:
-
Properties Loading: The application loads properties for
MyProperties
andExampleProperties
from the properties file. -
Bean Creation:
MyBeanCollection
creates an instance ofMyBean
, injectingMyProperties
during its creation. -
Component Instantiation: PySpring initializes
ExampleService
andAnotherExampleService
, injecting dependencies via type annotations. - Dependency Injection:
ExampleService
receives an instance ofExampleProperties
and an instance ofMyBean
.AnotherExampleService
receives an instance ofExampleService
.
- Lifecycle Hook:
- The
post_construct
method inExampleService
logs the value ofexample_properties.value
, demonstrating that injected properties are available. It also shows that the injectedMyBean
is accessible. - The
post_construct
method inAnotherExampleService
logs the value of the sameexample_properties.value
viaexample_service
, demonstrating access to an injected component's properties.
- The
Key Takeaways:
-
Beans Interact with Components: Beans created by a
BeanCollection
can interact with any component registered within the application context. This is because all these classes (beans, components, and controllers) are managed by the application context. -
Properties Sharing: Properties loaded from a properties file are not only used for bean creation but can also be injected into components. This enables dynamic configuration of the application.
-
Dependency IoC Container: PySpring builds a dependency IoC container for all registered entities. This allows the framework to inject objects into other entities (properties, beans, components) that depend on them.
-
Flexibility: This approach provides a flexible architecture where components and beans can depend on each other, allowing the creation of modular and interconnected applications.
Summary
The BeanCollection
class provides a structured method to integrate external code into a PySpring application by treating them as managed beans. This approach enhances modularity, maintainability, and reduces tight coupling with third-party code. Additionally, it supports properties loading and dependency injection, allowing for more flexible and configurable bean creation.