Crane4j Data Assembly Framework

repository·dev·Indexed 42 days ago

https://github.com/opengoofy/crane4j

A 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.

Tokens
75.3K
Snippets
180
Records
244
Agent score
52%

What's inside Crane4j

  1. Overview of crane4j extension modules

    dev
    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.
  2. Key features and supported scenarios of crane4j

    dev

    crane4j 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.
  3. Key Features of Crane4j

    dev

    Crane4j 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.
  4. What is a ContainerProvider and how does it work?

    dev

    A ContainerProvider is a component used to retrieve data source containers. It functions similarly to a Spring FactoryBean, 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, crane4j uses MybatisPlusQueryContainerProvider to access MP's query functionality. This allows you to obtain special method containers built on BaseMapper#selectList, where calling the container is equivalent to calling the mapper method.

    Crane4jGlobalConfiguration itself is an implementation of a ContainerProvider.

  5. What is crane4j and Field Filling?

    dev

    crane4j 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 (like FEMALE) is a typical field filling operation.

  6. Access the Crane4j Global Configuration

    dev

    The Crane4jGlobalConfiguration class 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();
  7. Handle cascading relations at the data source level

    dev

    Specifying 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 @Assemble annotation 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;
    }
  8. Use Object Introspection to synchronize fields

    dev

    When 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;
    }
  9. Use @AssembleMp for data source assembly

    dev

    The @AssembleMp annotation is used to configure a BaseMapper query as a data source for assembly operations. The behavior depends on how you configure the mapper, selects, and where attributes.

    Query Modes

    • By Primary Key (All Fields): Use only mapper. It performs select * from table where id in ?.
    • By Primary Key (Specific Fields): Use mapper and selects. It performs a selective query and groups by the primary key (marked with @TableId).
    • By Foreign Key (All Fields): Use mapper and where. It performs select * from table where foreign_key in ? and groups by the specified foreign key.
    • By Foreign Key (Specific Fields): Use mapper, selects, and where. It performs a selective query and groups by the specified foreign key.

    Note: If you specify selects but 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;
  10. Use Object Containers for complex key matching

    dev

    By default, a Container in 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:

    1. Annotate the target class with @Assemble but do not specify a key value. This tells Crane4j to pass the object being filled directly as the key to the container.
    2. 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 Container must be grouped by the input object instance itself. If you have overridden equals or hashCode in 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()
        ))
    );
  11. Use the Operator Interface for flexible data filling

    dev

    The Operator interface 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 filling Map or JSONObject types where annotations cannot be applied directly to the class.

    To use it:

    1. Define an interface annotated with @Operator.
    2. Define abstract methods with @Assemble to configure how the parameters should be filled.
    3. 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
    }
  12. How recursive executors work for nested filling

    dev

    Standard 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:

    1. 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);
    	}
    }