Crane4j Data Assembly Framework
repository·dev·Indexed 42 days ago
https://github.com/opengoofy/crane4jA lightweight, annotation-driven data assembly framework for Java that simplifies populating object fields from sources such as Enums, constants, methods, and databases. It features a Spring Boot starter, SpEL engine support, and extension modules for Jackson, Redis, MyBatis-Plus, and native SQL via DataSource. Key annotations include @Assemble, @AssembleEnum, @AssembleMethod, @AssembleConstant, @AssembleKey, @Disassemble, and @AutoOperate for automatic filling.
What's inside Crane4j
- Crane4j provides several independent extension modules designed to integrate its core functionality with popular third-party libraries. Each module is decoupled; you only need to include the specific extension module and its corresponding third-party dependency to use its features.
Key features and supported scenarios of crane4j
devcrane4j is designed to handle complex data assembly scenarios beyond simple lookups. Supported capabilities include:
- Multiple simultaneous operations: Filling many different types of related data at once.
- Complex data structures: Recursive filling for nested tree-like structures.
- Diverse data sources: Fetching data via RPC interfaces, ORM frameworks, or local caches.
- Optional field filling: Controlling which fields are filled based on the specific use case (e.g., masking sensitive info in public APIs).
- Conditional filling: Filling fields only under certain conditions.
- Caching: Utilizing local or DB-based caching for data sources.
- Asynchronous filling: Performing field filling asynchronously to improve efficiency when dealing with high-latency network IO.
Key Features of Crane4j
devCrane4j provides several key capabilities for managing data association and field filling:
- Out-of-the-box usage: Integrates with SpringBoot auto-configuration. You can enable framework functionality with a single annotation, avoiding tedious manual configuration.
- Simple API: Provides easy-to-understand APIs and comprehensive annotation-based configuration.
- Extensibility: Leverages Spring dependency injection, allowing you to easily replace default components with custom logic.
- Versatile Scenarios: Supports grouped or nested filling, as well as one-to-many or many-to-many batch filling. The granularity of operation can be as fine as an individual field.
- Rich Data Source Support: Supports dictionaries, enums, constant classes, and executable methods by default. Additional data sources can be added easily.
- Powerful Extensions: Includes features such as automatic filling, multi-threaded filling, caching, and dynamic expressions. It also supports extensions like
mybatis-plus.
What is a ContainerProvider and how does it work?
devA
ContainerProvideris a component used to retrieve data source containers. It functions similarly to a SpringFactoryBean, managing containers that have complex creation processes or providing a flexible way to integrate third-party framework data sources.For example, in the MybatisPlus extension,
crane4jusesMybatisPlusQueryContainerProviderto access MP's query functionality. This allows you to obtain special method containers built onBaseMapper#selectList, where calling the container is equivalent to calling the mapper method.Crane4jGlobalConfigurationitself is an implementation of aContainerProvider.What is crane4j and Field Filling?
devcrane4j is a simple and easy-to-use field filling framework. It allows you to quickly fill related fields based on foreign keys or code values using simple annotations. It supports various data sources such as dictionaries, enums, and methods.
Concept: Field Filling
Field filling (also known as data assembly or data aggregation) is the process of querying related information from various data sources (RPC interfaces, enums, dictionaries, or databases) using a foreign key and mapping those attributes onto the final returned object.
For example, converting a dictionary code (like
0) into a human-readable value (likeFEMALE) is a typical field filling operation.Access the Crane4j Global Configuration
devThe
Crane4jGlobalConfigurationclass is the central hub of the framework, storing all runtime components and configuration information. It is typically a singleton.- In Spring environments: The configuration is automatically registered in the Spring container. You can access it via
@Autowired. - In non-Spring environments: You must manually create it using
SimpleCrane4jGlobalConfiguration.create().
// In Spring environment @Autowired private Crane4jGlobalConfiguration configuration; // In non-Spring environment Crane4jGlobalConfiguration configuration = SimpleCrane4jGlobalConfiguration.create();- In Spring environments: The configuration is automatically registered in the Spring container. You can access it via
Handle cascading relations at the data source level
devSpecifying an execution order via
@Assemble(sort = ...)causes all operations on that object to become synchronous. Even if multiple operations use the same data source, they will be executed as separate steps, which may impact performance.To optimize performance, you can handle the cascading logic directly within a Method Container. Instead of multiple sequential queries, you write a single method that retrieves the entire hierarchy (e.g., fetching all parent departments for a list of child IDs in one go) and returns a combined result set.
In this approach, the
@Assembleannotation simply maps the pre-assembled fields from the container's result to the target object.// 1. Define a Method Container that performs the complex join/hierarchy logic @Component public class DeptService { @ContainerMethod( container = "thirdDept", resultType = Map.class, resultKey = "thirdDeptId" ) public List<Map<String, Object>> queryDeptWithParentByThirdDept(List<Integer> thirdDeptIds) { // Logic to query 3rd, 2nd, and 1st level depts and assemble them into a Map // ... return results; } } // 2. Reference the pre-assembled properties in your entity public class Emp { private Integer firstIdDeptId; private String firstDeptName; private Integer secondDeptId; private String secondDeptName; @Assemble( container = "thirdDept", prop = { "thirdDeptName", "secondDeptId", "secondDeptName", "firstIdDeptId", "firstIdDeptName" }, ) private Integer thirdDeptId; private String thirdDeptName; }Use Object Introspection to synchronize fields
devWhen no data source container is specified, crane4j uses the object itself as the data source via object introspection. This is a special handling mode used when the container is empty.
This feature is primarily used to synchronize redundant alias fields by automatically setting the value of one field onto another based on mapping annotations.
public class Foo { @Assemble(props = @Mapping(ref = "alias")) private String name; private String alias; }Use @AssembleMp for data source assembly
devThe
@AssembleMpannotation is used to configure aBaseMapperquery as a data source for assembly operations. The behavior depends on how you configure themapper,selects, andwhereattributes.Query Modes
- By Primary Key (All Fields): Use only
mapper. It performsselect * from table where id in ?. - By Primary Key (Specific Fields): Use
mapperandselects. It performs a selective query and groups by the primary key (marked with@TableId). - By Foreign Key (All Fields): Use
mapperandwhere. It performsselect * from table where foreign_key in ?and groups by the specified foreign key. - By Foreign Key (Specific Fields): Use
mapper,selects, andwhere. It performs a selective query and groups by the specified foreign key.
Note: If you specify
selectsbut omit the foreign key field, Crane4j will automatically append the foreign key field to the SQL query to ensure grouping works correctly.// Example: Querying specific fields by primary key @AssembleMp( mapper = "fooMapper", selects = {"userName", "userAge"} ) private Integer id;- By Primary Key (All Fields): Use only
Use Object Containers for complex key matching
devBy default, a
Containerin Crane4j queries data sources using a specific key. However, if you need to match data based on multiple keys or complex custom conditions, you can use an Object Container.To implement an Object Container:
- Annotate the target class with
@Assemblebut do not specify a key value. This tells Crane4j to pass the object being filled directly as the key to the container. - Define the container using
Containers.forLambda. The container's logic should accept the target object (or a collection of them) and return a dataset grouped by the object instance itself.
Warning: The data returned by the
Containermust be grouped by the input object instance itself. If you have overriddenequalsorhashCodein your target class, ensure the grouping logic remains consistent with those implementations.// 1. Target class with @Assemble (no key specified) @Assemble(container = "foo_info", props = @Mapping(ref = "name")) @Data public class Foo { private Integer id; private String code; private String name; } // 2. Container definition using forLambda Container<Foo> objectContainer = Containers.forLambda("foo_info", fooList -> fooList.stream().collect(Collectors.toMap( foo -> foo, foo -> foo.getId() + "#" + foo.getCode() )) );- Annotate the target class with
Use the Operator Interface for flexible data filling
devThe
Operatorinterface allows you to perform data filling (assembly) on arbitrary objects without requiring them to have specific annotations or a common parent class. This is particularly useful for fillingMaporJSONObjecttypes where annotations cannot be applied directly to the class.To use it:
- Define an interface annotated with
@Operator. - Define abstract methods with
@Assembleto configure how the parameters should be filled. - Inject the interface into your service or create a proxy manually.
// 1. Declare the operator interface @Operator private interface OperatorInterface { @Assemble(key = "id", container = "user", props = @Mapping("name")) void fill(Collection<Map<String, Object>> targets); } // 2. Inject and use the proxy @Autowired private OperatorInterface operator; public void example() { List<Map<String, Object>> targets = new ArrayList<>(); operator.fill(targets); // Performs the filling operation }- Define an interface annotated with
How recursive executors work for nested filling
devStandard executors perform disassembly (extracting nested objects via
@Disassemble) before assembly (filling properties via@Assemble). This can cause issues when the disassembly step needs data that hasn't been assembled yet.To solve this for tree-like structures or deeply nested objects, use a Recursive Executor. These executors follow a pattern of:
- Execute Assembly $\rightarrow$ 2. Execute Disassembly $\rightarrow$ 3. Recursively execute Assembly on the extracted nested objects.
Available Recursive Executors
DisorderedBeanOperationRecursiveExecutor: Recursive version of the unordered executor.OrderedBeanOperationRecursiveExecutor: Recursive version of the ordered executor.AsyncBeanOperationRecursiveExecutor: Recursive version of the async executor.
Warning: Recursive executors are implemented using a stack. While stack overflow is unlikely, deep hierarchies will impact performance due to repeated queries at every level. Use them only when necessary.
@Component public class NodeService { @ContainerMethod(namespace = "children", resultType = Node.class) public List<Node> selectChildrenNodesById(String nodeId) { // Query child nodes by ID } } @Data public static class Node { // Fill children, then recursively repeat for the children @Assemble(container = "children", prop = ":children") private final Integer id; private String name; @Disassemble(type = Node.class) private List<Node> children; } @Service public class ExampleService { // Use the ordered recursive executor to build the tree @AutoOperate(executorType = OrderedBeanOperationRecursiveExecutor.class) public Node getTree(String id) { return new Node(0); } }