<input id="ohw05"></input>
  • <table id="ohw05"><menu id="ohw05"></menu></table>
  • <var id="ohw05"></var>
  • <code id="ohw05"><cite id="ohw05"></cite></code>
    <label id="ohw05"></label>
    <var id="ohw05"></var>
  • crane:字典項與關聯數據處理的新思路

    CRANE

    前言

    在我們日常開發中,經常會遇到一些煩人的數據關聯和轉換問題,比如典型的:

    • 對象屬性中個有字典 id,需要獲取對應字典值并填充到對象中;
    • 對象屬性中有個外鍵,需要關聯查詢對應的數據庫表實體,并獲取其中的指定屬性填充到對象中;
    • 對象屬性中有個枚舉,需要將枚舉中的指定屬性填充到對象中;

    實際場景中這種聯查的需求可能遠遠不止這些,這個問題的核心有三點:

    • 填充的數據源是不確定的:可能是來自于 RPC 接口,可能是枚舉類,也可能是數據庫里的配置表,甚至是配置文件;
    • 填充對象是不確定的:可能是普通的對象,但是也可能是 Collection 集合,或者 Map 集合,甚至可能是個 JsonNode,或者有一個嵌套結構;
    • 填充的字段的不確定的:同樣的數據源,但是可能這個接口返回的對象只需要填其中的一個字段,但是另一個接口需要填另外的兩個字段;

    基于上述三點,我們在日常場景中很容易遇到下圖的情況:

    image-20220626150755256

    本文將推薦一個基于 spring 的工具類庫 crane,它被設計用來通過類似 MapStruts 的注解配置,完成這種麻煩的關聯數據填充/轉換操作的處理。

    倉庫地址:https://gitee.com/CreateSequence/crane

    文檔:https://gitee.com/CreateSequence/crane/wikis/pages

    一、crane 是用來做什么的?

    1、舉個例子

    在開始前,我們先舉個例子,假如我們有一個實體類 PersonVOPersonDO

    @Data
    public class PersonVO {
        private Integer id;
        private String personName;
    }
    
    @Data
    public class PersonDO {
        private Integer id;
        private String name;
    }
    

    然后手頭有一批待處理的 PersonVO 對象,我們需要從 PersonService 中根據 PersonVO.id 獲取 PersonDO 集合,然后最后把 PersonDO.name 回填到 PersonVO.personName 中:

    List<PersonVO> targets = new ArrayList<>;
    
    // 對targets按id分組
    Map<Integer, PersonVO> targetMap = new HashMap<>();
    targets.forEach(t -> targetMap.put(t.getId(), t));
    
    // 對sources按id分組
    List<PersonDO> sources = personService.getByIds(targetMap.keySet());
    Map<Integer, PersonDO> sourcesMap = new HashMap<>();
    sources.forEach(s -> sourcesMap.put(s.getId(), s));
    
    // 填充屬性
    targets.forEach((pid, target) -> {
        PersonDO source = sourcesMap.get(pid);
        if(source != null) {
            target.setPersonName(source.getName())
        }
    })
    

    總結一下,如果我們要手動處理,則無論如何避免不了四個步驟:

    • 從目標對象中拿到 key 值;
    • 根據 key 值從接口或者方法獲得 key 值對應的數據源;
    • 將數據源根據 key 值分組;
    • 遍歷目標對象,根據 key 值獲取到對應的數據源,然后根據根據需要挨個 set 數據源的屬性值;

    2、使用crane解決上述問題

    針對上述的情況,假如使用 crane ,則我們可以這么做:

    第一步,為被填充的 PersonVO 添加注解,配置字段:

    @Data
    public class PersonVO {
        @AssembleMethodSource(namespace = "person", props = @Prop(src = "name", ref = "personName"))
        private Integer id;
        private String personName;
    }
    

    第二步,在提供數據源的 PersonService 中為 getByIds 方法也添加一個注解,配置數據源:

    public class PersonService {
        @MethodSourceBean.Mehtod(namespace = "person", sourceType = PersonDO.class, sourceKey = "id")
        public List<PersonDO> getByIds(Set<Integer> ids) {
            // return somthing......
        }
    }
    

    第三步,使用 crane 提供的 OperateTemplate 輔助類在代碼里完成填充:

    List<PersonVO> targets = new ArrayList<>;
    operateTemplate.process(targets);
    

    或者直接在方法注解上添加一個注解,返回值將在切面中自動填充:

    @ProcessResult(PersonVO.class)
    public List<PersonVO> getPersonVO() {
        // return PersonVO list......
    }
    

    相比起純手工填充,crane 帶來的好處是顯而易見的,PersonService 中用一個注解配置好了數據源后,就可以在任何需要的實體類上用一行注解搞定填充字段的需求。

    當然,示例中原始的手動填充的寫法仍然有很多優化的余地。不過對應的, crane 的功能也不僅只有這些,crane 還支持配置更多的數據源,不僅是接口,還能是本地緩存,枚舉;關于 key 的映射關系,不止提供示例中的一對一,還支持一對多;而其中的字段映射,也支持更多的玩法,這些都會在下文一一介紹。

    二、如何引入

    crane 依賴于 springboot 環境,假如你是 springboot 項目,則只需要引入依賴:

    <dependency>
        <groupId>top.xiajibagao</groupId>
        <artifactId>crane-spring-boot-starter</artifactId>
        <version>${last-version}</version>
    </dependency>
    

    last-version 則是 crane 的版本號,截止至本文發布時,crane 的最新版本是 0.5.7

    然后在啟動類添加 @EnableCrane 注解啟用配置:

    @EnableCrane
    @SpringBootApplication
    public class Application {
        public static void main(String[] args) {
            SpringApplication.run(Application.class, args);
        }
    }
    

    即可使用 crane 的全部功能。

    三、配置使用

    字段配置是 crane 最核心的配置,它一般由三部分組成:

    • 指定的 key 值字段;
    • 要使用的數據源容器;
    • 數據源與對象中字段的映射配置;

    這對上述三點,crane 的最常見的寫如下:

    public class UserVO {
        @Assemble(
            container = UserContainer.class, // 根據userId的值去UserContainer獲取數據源
            props = { @Prop(src = "name", ref = "userName") } // 獲取到數據源對象后,再把數據源對象User的name字段值映射到UserVO的userName上
        )
        private Integer userId; // 注解在userId上,則userId就是key字段,他的值就是key值
        private String userName;
    }
    

    容器是另一部分內容,將在后文詳細的介紹,這里我們先簡單理解為根據 key 值獲取數據源的地方。

    從注解的字段獲得 key 值,然后再將 key 值從 container 指定的容器中轉換為對應數據源后,crane 會根據 props 配置自動的將數據源的字段映射到待處理對象上。

    1、字段映射

    Assemble#props 中使用 @Prop 注解聲明一對字段的映射關系。與 MapStruts@Mapping 注解很像,@Prop#src 用于指定數據源字段,@Prop#ref 指定引用字段,兩者實際都允許為空。

    不指定數據源字段

    當不指定 src 時,即不指定數據源字段,此時填充使用的數據源就是數據源對象本身,比如:

    public class UserVO {
        @Assemble(
            container = UserContainer.class, 
            props = @Prop(ref = "userInfo")
        )
        private Integer userId;
        private User userInfo;
    }
    

    該操作將直接把作為數據源對象的 User 實例直接填充至 UserVO.userInfo 中。

    不指定引用字段

    當不指定 ref 時,crane 會認為引用字段就是 key 字段,比如:

    public class UserVO {
        @Assemble(
            container = UserContainer.class, 
            props = @Prop(src = "age")
        )
        private Integer userAge;
    }
    

    假如此時 UserVO.userAge 實際對應的值是 User.id ,則根據 key 值從容器中獲取了數據源對象 User 后,此處 userAge 將被替換為 User.age 的值。

    不指定任何字段

    不指定任何字段,效果等同于將 key 字段值替換為對應數據源對象。

    比如,我們有一個特定的容器 EvaluationContainer,他允許將分數的轉為評價,比如 90 =》優、80 =》 良......則我們可以有:

    public class UserVO {
        @Assemble(container = EvaluationContainer.class)
        private String score;
    }
    

    執行操作后,score 會被轉為對應的“優”,“良”......評價。

    2、特殊類型的字段映射

    crane 還支持處理一些特別的數據類型的字段映射,比如集合、枚舉或者一些基本數據源類型,這里以常見的 Collection 集合為例:

    比如,假設我們現在有一個根據 部門 id 查詢員工對象集合 EmpUser 的容器 EmpContainer,現在我們需要根據 DeptVO.id 填充該部門下全部員工的姓名,則有配置:

    public class DeptVO {
        @Assemble(container = EmpContainer.class, props = @prop(src = "name", ref = "userNames"))
        private Integer id;
        private List<String> userNames;
    }
    

    根據 DeptVO.deptId 從容器中獲得了 List<EmpUser>,然后 crane 會遍歷元素,嘗試從元素中取出每一個 EmpUser.name,然后組裝成新的集合作為數據源。

    image-20220426155412651

    實際上,這樣的操作也適用于數組。

    其余數據類型的處理方式具體可以參見文檔。

    3、將字段映射配置抽離為模板

    有時候,尤其對象的字段大多都來自于關聯查詢時,我們需要在 key 字段上配置的注解就會變得及其臃腫,尤其是當有多個對象需要使用相同的配置時,這個情況會變得更加嚴重。

    因此, crane 允許通過 @PropsTemplate將字段配置單獨的分離到某個特定的類,然后再通過 @Assemble#propTemplates屬性引用模板配置。

    比如,針對一個通過 id 換取 User對象的 UserContainer 數據源容器,我們現在有這樣一組配置:

    public class UserVO {
        @Assemble(container = UserContainer.class, props = {
            @prop(src = "name", ref = "userName"),
            @prop(src = "age", ref = "userAge"),
            @prop(src = "sex", ref = "userSex")
        })
        private Integer id;
        private String userName;
        private Integer userAge;
        private Integer userSex;
    }
    

    我們可以使用一個單獨的配置類或者配置接口,去承擔一部分繁瑣的字段配置:

    @PropsTemplate({
        @prop(src = "name", ref = "userName"),
        @prop(src = "age", ref = "userAge")
    })
    public interface UserPropTemplates {};
    

    接著我們通過引入配置好的字段模板,即可以將原本的注解簡化為:

    public class UserVO {
        @Assemble(container = UserContainer.class, propTemplates = { UserPropTemplates.class })
        private Integer id;
        private String userName;
        private Integer userAge;
        private Integer userSex;
    }
    

    一個操作配置允許引入多個模板,并且同時允許在模板的基礎上繼續通過 @Assemble#props 屬性額外配置字段映射。

    模板配置允許通過配置類的繼承/實現關系傳遞,即在父類 A 通過 @PropTemplate 配置了字段映射,則在配置操作時引入子類 B 作為配置模板,將一并引入父類 A 上的配置。

    4、處理字段中的嵌套對象

    基本使用

    在實際場景中,很容易出現這樣的情況:

    假如我們有一個 UserContainer,允許根據 User.id獲得對應的名稱,

    public class User {
        @Assemble(container = User.class, props = @Prop(ref = "userName"))
        private Integer Id;
        private String userName;
        // 需要填充的嵌套集合
        private List<User> subordinate;
    }
    

    我們有一個員工從屬關系的樹結構,我們手頭持有一個根節點,但是實際上實例內部有一大堆嵌套的實例需要進行填充。

    在 crane 中,通過 @Disassemble 注解標記嵌套字段,在處理時將按廣度優先自動把他展開鋪平后一并處理:

    public class User {
        @Assemble(container = User.class, props = @Prop(ref = "userName"))
        private Integer id;
        private String userName;
        @Disassemble(User.class)
        private List<User> subordinate;
    }
    

    crane 支持處理任意層級的單個對象、數組或Collection集合,也就是說,哪怕是這樣的結構也是允許的:

    private List<List<User[]>> subordinate;
    

    image-20220426174556372

    動態類型

    有時候不可避免的會存在無法確定字段類型的場景,比如典型的泛型:

    public class ResultWrapper<T> {
        @Disassemble
        private T data;
    }
    

    在這種情況是無法直接確定 data 字段的類型的,此時使用 @Disassemble 注解可以不在 value 或者 targetClass 上直接指定具體的類型,crane 將在執行操作時通過反射獲得 data 的實際類型,然后再通過指定的解析器去獲取該類型的對應配置。

    5、通過類注解配置

    上述介紹都是基于類屬性上的 @Assemble@Disassemble 注解完成的,實際上 crane 也支持通過類上的 @Operations注解配置操作。

    基本使用

    比如,我們現有如下情況:

    Child 繼承了 Parent,但是在使用 Child 實例時又需要根據 id 填充 userNameuserAge,此時并不方便直接修改 Parent

    public class Parent {
        private String id;
        private String userName;
        private Integer userAge;
    }
    
    public class Child extends Parent {}
    

    因此,我們允許在 Child 中如此配置:

    @Operations(
        assembles = @Assemble(key = "id", container = UserContainer.class, props = {
            @prop(src = "name", ref = "userName"), 
            @prop(src = "age", ref = "userAge")
        })
    )
    public class Child extends Parent {}
    

    現在效果等同于在 Parent 類中直接注解:

    public class Parent {
        @Assemble(container = UserContainer.class, props = {
            @prop(src = "name", ref = "userName"),
            @prop(src = "age", ref = "userAge"),
            @prop("user") // 將user對象直接映射到待處理對象的user字段上
        })
        private String id;
        private String userName;
        private Integer userAge;
    }
    

    這個配置僅對 Child 有效,而不會影響到 Parent

    key字段別名

    由于配置允許通過繼承父類或實現父接口獲得,因此有可能會出現 key 字段名稱不一致的情況,比如:

    現有配置接口 FooInterface,指定了一個以 id 為 key 字段的裝配操作,但是別名允許為 userIduid

    @Operations(
        assembles = @Assemble(key = "id", aliases = { "userId, uid" }, container = UserContainer.class, props = {
            @prop(src = "name", ref = "userName"), 
            @prop(src = "age", ref = "userAge")
        })
    )
    public interface FooInterface
    

    現有 Child 實現了該接口,但是該類中只有 userId 字段而沒有 id 字段,此時配置是照樣生效的:

    public class Foo implements FooInterface {
        private Integer userId;
    }
    

    當一次操作中同時配置的 key 與多個別名,則將優先尋找 key 字段,若不存在則再根據順序根據別名查找至少一個真實存在的別名字段。

    配置繼承與繼承排除

    @Operations 注解允許使用在普通類或者接口類上,并且允許通過實現與繼承的方式傳遞配置。

    假如現在存在以下類繼承結構:

    image-20220528142143794

    且上述兩個接口與三個類上全都存在 @Operations 注解,此時在默認情況下,我們可以分析以下類 E 的配置情況:

    • 不做任何特殊配置,類 E 將繼承 A,B,C,D 上的全部注解配置;
    • 若將 E 的 @Operations#enableExtend() 屬性改為 false,則類 E 將不繼承任何父類或實現的接口上的配置,僅保留類 E 上的配置;
    • 若在 @Operation#extendExcludes() 配置了排除繼承,則:
      1. 若排除接口 B,且類 E 上的 @Operations#enableExtend() 屬性為 true,此時類 E 將繼承除接口 B 以外的所有配置,即獲得 A,C,D,E 的配置;
      2. 若排除類 C,且類 E 上的 @Operations#enableExtend() 屬性為 true,此時類 E 將不再繼承類 C 上及其繼承/實現樹上的配置,但是仍然可以通過接口 D 獲得接口 B 的配置,此時類 E 僅 B,D,E 三個類的配置;
      3. 若類 C 上的 @Operations#enableExtend() 屬性為 false,且類 E 上的 @Operations#enableExtend() 屬性為 true,則此時 E 將不會通過類 C 獲得 A 與 B 的配置,因為 C 并沒有繼承父類和父接口的配置,此時 E 將擁有 B,C,D,E 四組配置;

    6、分組填充

    參照 Spring Validation 的分組校驗,crane 也提供了操作分組的功能,它允許以與 Validation 類似的方式,對裝配操作進行分組,然后在操作的時候僅處理指定分組中的操作,比如:

    @Assemble(
        container = UserContainer.class, 
        groups = { UserGroup.class, AdminGroup.class }, // 當指定分組為 UserGroup 或 AdminGroup 時填充 userName 字段
        props = @prop(src = "name", ref = "userName")
    )
    @Assemble(
        container = UserContainer.class, 
        groups = { AdminGroup.class },  // 僅當指定分組為 AdminGroup 時填充 role 字段
        props = @prop(src = "role", ref = "role")
    )
    private Integer id;
    

    然后可以在相關的操作入口中指定本次操作的分組即可。

    該功能一個比較典型的應用場景是一個接口同時對內對外,但是有些敏感的信息在對外的時候應該是不展示的,此時即可通過分組完成。

    7、排序填充

    裝配操作允許通過 spring 提供的 @Order 注解對裝配操作的執行順序進行排序,與 spring 排序規則一樣,value 越小越靠前。

    對字段配置排序

    比如,現在我們有一個組合操作,即先根據 userId 獲取 deptId,然后再根據 deptId 獲取 empUsers

    public class UserVO {
        
        @Order(0)
        @Assemble(container = UserContainer.class, props = @Prop(src = "deptId", ref = "deptId"))
        private Integer userId;
        
        @Order(1)
        @Assemble(container = EmpContainer.class, props = @Prop(ref = "empUsers"))
        private Integer deptId;
        private List<User> empUsers;
    }
    

    按上述配置,根據 userId 填充 deptId 的操作將會優先執行,然后才會執行根據 deptId 填充 empUsers字段。

    對類配置排序

    當使用類注解 @Operations 配置操作時,@Order 注解只能加在所配置的類上,同一個類上聲明的裝配操作優先級都與該注解一致,也就說,使用 @Operations時,只支持不同類上的操作配置的排序,不支持同一類上的操作排序。

    比如:

    @Order(0)
    @Operations(assembles = @Assemble(container = UserContainer.class, props = @Prop(src = "deptId", ref = "deptId")))
    public interface AssembleDeptConfig {}
    
    @Order(1)
    @Operations(assembles = @Assemble(container = EmpContainer.class, props = @Prop(ref = "empUsers")))
    public interface AssembleEmpConfig {}
    
    @Operations(enableExtend = true)
    public class UserVO implements AssembleEmpConfig, AssembleDeptConfig {
        private Integer userId;
        private Integer deptId;
        private List<User> empUsers;
    }
    

    這種情況下,AssembleDeptConfig 上的操作配置就會優先于 AssembleEmpConfig 執行。

    8、數據源預處理

    crane 允許在通過 @Prop 注解配置字段映射時,使用 @Prop#exp@Prop#expType 配置 SpEL 表達式,然后利用表達式從容器中獲取的原始的數據源進行預處理。

    比如我們在字段配置一章中提到過的內省容器。通過內省容器,我們可以獲取到待處理對象本身,然后我們先獲取待處理對象的userName字段值,然后根據性別動態的將其替換為原值+“先生/女生”:

    @Assemble(
        container = IntrospectContainer.class, props = @Prop(
            ref = "userName", 
            exp = "sex == 1 ? #source.name + '先生' : #source.name + '女士'", // 根據性別,在name后追加“先生”或者“女士”
            expType = String.class // 表達式返回值為String類型
        )
    )
    private String sex;
    private String name;
    

    根據 sex字段從容器中獲取的數據源,將先經過表達式的處理,然后將返回指定類型的結果,這個結果將作為新的數據源參與后續處理。

    表達式上下文中默認注冊了以下變量,允許直接在表達式中引用:

    • #source:原始數據源對象;
    • #target:待處理對象;
    • #key:key字段的值;
    • #src@Prop#src指定的參數值;
    • #ref@Prop#ref指定的參數值;

    若有需要,也可以自行注冊 ExpressionPreprocessingInterceptor.ContextFactory,在 SpEL 表達式上下文中注冊更多變量和方法。

    9、自定義注解

    crane 深度結合的 spring 的提供的元注解機制,用戶可以基于已有注解,自由的 diy 新注解以更進一步的簡化開發。

    首先簡單的介紹一下 spring 的元注解機制。在 java 中,元注解指能用在注解上的注解,由于 java 的注解本身不支持繼承,因此 spring 借助 AnnotationElementUtils 等工具類對 java 的元注解機制進行了擴展,實現了一套類似繼承的注解組合機制,即 A 注解用在了注解 B 上時,注解 B 也可以被認為是一個特殊的 A 注解。

    在 crane 中,允許被這樣作為元注解使用的注解皆以 @MateAnnotation 標記。

    假設現在存在有如下字段配置:

    @Assemble(container = UserContainer.class, props = {
        @prop(src = "name", ref = "userName"),
        @prop(src = "age", ref = "userAge")
    })
    private Integer id;
    

    我們可以上述 @Assemble 配置作為元注解,創建一個 @AssembleUser 注解:

    @Assemble(container = UserContainer.class, props = {
        @prop(src = "name", ref = "userName"),
        @prop(src = "age", ref = "userAge")
    })
    @Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface AssembleUser {}
    

    然后將原本的配置替換為:

    @AssembleUser
    private Integer id;
    

    即可實現與之前完全一樣的效果。

    四、數據源

    在 crane 中,任何將能夠將 key 轉換為對應的數據源的東西都可以作為容器,crane 提供了五個默認的容器實現,它們可以覆蓋絕大部分的場景下的數據源:

    • 鍵值對緩存:對應容器 KeyValueContainer,允許根據 namesapce 和 key 注冊和獲取任何數據;
    • 枚舉:對應容器 EnumDictContainer,允許向容器中注冊枚舉類,然后通過指定的 namesapce 和 key 獲得對應的枚舉實例;
    • 實例方法:對應容器 MethodContainer,允許通過注解簡單配置,將任意對象實例的方法作為數據源,通過 namespace 和 key 直接調用方法獲取填充數據。適用于任何基于接口或本地方法的返回值進行填充的場景;
    • 內省容器:對應容器 BeanIntrospectContainerKeyIntrospectContainer,允許直接將當前填充的對象作為數據源。適用于一些字段同步的場景;

    接下來我們看看怎么使用。

    1、將鍵值對緩存作為數據源

    鍵值對容器KeyValueContainer基于一個雙重 Map 集合實現,本質上是一個基于本地緩存的數據源。在使用前,我們需要在容器中注冊鍵值對,然后在字段注解上通過 namespace 與 key 進行引用。

    比如,現有一個很典型的性別字典項:

    Map<Integer, Object> gender = new HashMap<>();
    gender.put(0, "女");
    gender.put(1, "男");
    keyValueContainer.register("sex", gender);
    

    然后再在待處理對象中引用:

    @Assemble(
        container = keyValueContainer.class, // 指定使用鍵值對容器
        namespace = "sex", // namespace為上文指定的sex
        props = @Prop("sexName") // 從命名空間sex中根據sex字段值獲取對應的value,并填充到sexName字段
    )
    private Integer sex;
    private String sexName;
    

    也可以使用 @AssembleKV 簡化寫法:

    @AssembleKV(namespace = "sex",  props = @Prop("sexName"))
    private Integer sex;
    private String sexName;
    

    2、將枚舉作為數據源

    枚舉容器EnumDictContainer用于處理枚舉類型的數據源。與鍵值對一樣,使用前我們需要先向容器注冊要使用的枚舉。

    注冊枚舉

    舉個例子,我們手頭有個 Gender 枚舉:

    @Data
    @RequiredArgsConstructor
    public enum Gender {
        MALE(1, "男"),
        FEMALE(0, "女");
        private final Integer id;
        private final String desc;
    }
    

    則可以按如下方法注冊:

    // namespace為gender,并且以枚舉項的id屬性作為key值
    enumDictContainer.register(Gender.class, "gender", Gender::id);
    // namespace為Gender類的非全限定名Gender,并且以枚舉項的 Enum#name() 返回值作為key值
    enumDictContainer.register(Gender.class);
    

    基于注解注冊

    當然,如果覺得手動指定 namespace 和 key 麻煩,也可以通過注解完成,現在我們為 Gender 枚舉類添加注解:

    @EnumDict.Item(typeName = "gender", itemNameProperty = "id") // 指定namespace為gender,然后以id的值作為key
    @Data
    @RequiredArgsConstructor
    public enum Gender {
        MALE(1, "男"),
        FEMALE(0, "女");
        private final Integer id;
        private final String desc;
    }
    

    然后再在容器中注冊,就會自動根據類上的注解獲取 namespace 和枚舉實例的 key 值了:

    enumDictContainer.register(Gender.class);
    

    使用

    當我們將枚舉注冊到枚舉容器后,使用時僅需在 @Assemble注解中引用即可:

    @Assemble(
        container = EnumDictContainer.class, // 指定使用枚舉容器
        namespace = "gender", // namespace為上文指定的gender
        props = @Prop(src = "name", ref = "genderName") // 獲取Gender枚舉中的name字段值,并填充到genderName字段
    )
    private Integer gender;
    private String genderName;
    

    注冊后的枚舉會被解析為 BeanMap 并緩存,我們可以像處理對象一樣簡單的通過屬性名獲取對應的值。

    也可以用 @AssembleEnum 注解簡化寫法:

    @AssembleEnum(namespace = "gender", props = @Prop(src = "name", ref = "genderName"))
    private Integer gender;
    private String genderName;
    

    3、將實例方法作為數據源

    方法容器MethodContainer是基于 namespace 隔離,將各個類實例中的方法作為數據源的容器。

    在使用方法容器之前,我們需要先使用 @MethodSourceBean.Method注解作為數據源的方法,然后再使用@MethodSourceBean注解該方法所在的類實例。

    注冊方法

    比如,我們需要將一個根據用戶 id 批量查詢用戶對象的接口方法作為數據源:

    @MethodSourceBean
    public class UserService {
        // 該方法對應的命名空間為user,然后指定返回值類型為User.class, key字段為id
        @MethodSourceBean.Mehtod(namespace = "user", sourceType = User.class, sourceKey = "id")
        public List<User> getByIds(List<Integer> ids) {
            // 返回user對象集合
        }
    }
    

    當然,如果這個方法來自父類,無法顯式的使用注解聲明數據源方法,也允許通過類注解聲明:

    @ContainerMethodBean(
        @ContainerMethodBean.Method(namespace = "user", name = "getByIds", sourceType = User.class, sourceKey = "id")
    )
    public class UserService extend BaseService<User> {}
    

    當項目啟動時,crane 將從 Spring 容器中獲取被 @ContainerMethodBean注解的類,并獲取其中被注解的方法,并根據指定的 namespace 注冊到方法容器對應的命名空間。

    使用

    當我們使用時,與其他容器保持一致:

    @Assemble(
        container = MethodSourceContainer.class, // 指定使用鍵值對容器
        namespace = "user", // namespace為上文指定的user
        props = @Prop("userBean") // 從命名空間user中獲取方法getByIds,然后將userId對應的user對象填充到userBean字段中
    )
    private Integer userId;
    private User userBean;
    

    當然,也可以通過 @AssembleMethodSource 注解簡化寫法:

    @MethodSource(namespace = "user", props = @Prop("userBean"))
    private Integer userId;
    private User userBean;
    

    多對一

    容器總是默認方法返回的集合中的對象與 key 字段的值是一對一的,但是也可以調整為一對多。

    比如我們現在有一批待處理的 Classes 對象,需要根據 Classes#id字段批量獲取Student對象,然后根據Student#classesId字段填充到對應的 Classes 對象中:

    @MethodSourceBean.Mehtod(
        namespace = "student", sourceType = Student.class, sourceKey = "classesId",
        mappingType = MappingType.ONE_TO_MORE // 聲明待處理對象跟Student通過classesId構成一對多關系
    )
    public List<Student> listIds(List<Integer> classesIds) {
        // 查詢Student對象
    }
    

    然后在待處理對象中引用:

    @Assemble(
        container = MethodSourceContainer.class,
        namespace = "student",
        props = @Prop("students")
    )
    private Integer classesId;
    private List<Student> students;
    

    4、將待處理對象本身作為數據源

    有些時候,我們會有一些字段同步的需求,待處理對象內省容器 BeanIntrospectContainer 就是用來干這件事的,不僅如此,它適用于任何需要對待處理對象本身進行處理的情況。

    待處理對象內省容器BeanIntrospectContainer的數據源就是待處理對象本身,它用于需要對待處理對象本身進行處理的情況。

    比如簡單的同步一下字段:

    // 將對象中的name字段的值同步到userName字段上
    @Assemble(container = BeanIntrospectContainer.class, props = @Prop("userName")
    private String name;
    private String userName;
    

    也可以用于處理集合取值:

    // 將對象中的users集合中全部name字段的值同步到userNames字段上
    @Assemble(container = BeanIntrospectContainer.class, props = @Prop(src = "name", ref = "userNames"))
    private List<User> users;
    private List<String> userNames;
    

    或者配合 SpEL 預處理數據源的功能處理一些字段:

    @Assemble(
        container = BeanIntrospectContainer.class, props = @Prop(
            ref = "name", 
            exp = "sex == 1 ? #source.name + '先生' : #source.name + '女士'", // 根據性別,在name后追加“先生”或者“女士”
            expType = String.class
        )
    )
    private String sex;
    private String name;
    

    也提供了 @AssembleBeanIntrospect 注解,效果等同于:

    @Assemble(container = BeanIntrospectContainer.class)
    

    5、將key值作為數據源

    待處理 key 字段內省容器KeyIntrospectContainerBeanIntrospectContainer 基本一致,主要的不同在于 KeyIntrospectContainer 的數據源是待處理對象本此操作所對應的 key 字段值。

    除了跟 BeanIntrospectContainer 差不多的用法以外,由于操作的數據源對象本身變為了 key 字段的值,因此也有了一些特別的用處:

    // 將Type枚舉的desc字段賦值給typeName字段
    @Assemble(container = KeyIntrospectContainer.class, props = @Prop(src = "desc", ref = "typeName"))
    private TypeEnum type;
    private String typeName;
    

    如果是 JsonNode,還可以這樣:

    // 使用type字段對應枚舉的desc字段替換其原本的值
    @Assemble(container = KeyIntrospectContainer.class, props = @Prop(src = "desc"))
    private TypeEnum type;
    

    默認提供了 @AssembleKeyIntrospect 注解,效果等同于

    @Assemble(container = KeyIntrospectContainer.class)
    

    五、切面

    完成了數據源和字段的配置以后,就需要在代碼中執行填充的操作。crane 總共提供了三個入口:

    • 在方法上添加 @ProcessResult 注解,然后通過 AOP 自動對方法返回值進行填充;
    • ObjectMapper 中注冊 DynamicJsonNodeModule 模塊,然后使用該 ObjectMapper 實例序列號對象時自動填充;
    • 使用 crane 注冊到 spring 容器中的 OperateTemplate 手動的調用;

    第二種會在下一節介紹,而第三種沒啥特別的,這里主要介紹一些基于切面的方法返回值自動填充。

    使用

    默認情況下,crane 會自動把切面注冊到 spring 容器中,因此使用時,若方法所在類的實例已經被 spring 容器管理,則只需要在方法上添加注解就行了:

    // 自動填充返回的 Classroom 對象
    @ProcessResult(Classroom.class)
    public Classroom getClassroom(Boolean isHandler) {
        return new Classroom();
    }
    

    切面支持處理單個對象,一維對象數組與一維的對象 Collection 集合。

    表達式校驗

    切面還允許根據 SpEL 表達式動態的判斷本次方法調用是否需要對返回值進行處理:

    @ProcessResult(
        targetClass = Classroom.class
        condition = "!#result.isEmpty && !#isHandle" // 當返回值為空集合,且isHandle參數不為true時才處理返回值
    ) 
    public List<Classroom> getClassroom(Boolean isHandle) {
        return Collections.emptyList();
    }
    

    這里的 SpEL 表達式中默認可以通過 #參數名 的方式引用入參,或者通過 #result 的方式獲取返回值。

    自定義組件

    此外,切面注解中還可以自行自定一些 crane 的組件和參數,包括且不局限與分組,執行器等:

    @ProcessResult(
        targetClass = Classroom.class,
        executor = UnorderedOperationExecutor.class,
        parser = BeanOperateConfigurationParser.class,
        groups = { DefaultGroup.class }
    )
    public List<Classroom> getClassroom(Boolean isHandler) {
        return Collections.emptyList();
    }
    

    不同的組件會產生不同的效果,比如 executor ,當指定為 AsyncUnorderedOperationExecutor.class 時 crane 會根據本次所有操作對應的容器的不同,異步的執行填充,而指定為 SequentialOperationExecutor 時將支持按順序填充。

    這里更多詳細內容可以參考文檔。

    六、Json支持

    上述例子都以普通的 JavaBean 為例,實際上 crane 也支持直接處理 JsonNode。若要啟用 Json 支持,則需要引入 crane-jackson-implement 模塊,其余配置不需要調整。

    <dependency>
        <groupId>top.xiajibagao</groupId>
        <artifactId>crane-jackson-implement</artifactId>
        <version>${last-version}</version>
    </dependency>
    

    crane-jackson-implement 版本與 crane-spring-boot-starter 版本一致,截止本文發布時,版本號為 0.5.7

    1、配置

    配置 ObjectMapper

    引入模塊后 crane 將會自動向 spring 容器中注冊必要的組件,包括 DynamicJsonNodeModule 模塊,該模塊是實現 JsonNode 填充的核心。用戶可以自行指定該模塊要注冊到哪個 ObjectMapper 實例。

    一般情況下,都會直接把該模塊注冊到 spring-web 提供的那個 ObjectMapper 中,也就是為 Controller 添加了 @RestController 注解、或者為方法添加 @ResponseBody 注解后,Controller 中接口返回值自動序列化時使用的 ObjectMapper

    比如,我們現在已經引入了 spring-web 模塊,則可以在配置類中配置:

    @Configuration
    public class ExampleCraneJacksonConfig {
    
        @Primary
        @Bean
        public ObjectMapper serializeObjectMapper(DynamicJsonNodeModule dynamicJsonNodeModule) {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.registerModule(dynamicJsonNodeModule); // 注冊動態json模塊
            return objectMapper;
        }
    
    }
    

    配置字段操作

    針對 JsonNode 的配置會跟普通的 JavaBean 有點區別。我們以一個普通的 JavaBean 配置為例:

    public class Foo {
        @Assemble(
            container = UserContainer.class, 
            props = @prop(src = "name", ref = "userName")
        )
        private String id;
        private String userName;
        
        @Disassemble(Foo.class)
        private List<Foo> foos;
    }
    

    首先,需要為序列化時進行數據填充的類添加 @ProcessJacksonNode 注解:

    @ProcessJacksonNode
    public class Foo {
        ......
    }
    

    然后,在 @Assemble@Disassemble 指定使用 Jackson 的操作者:

    @Assemble(
        container = UserContainer.class, 
        props = @prop(src = "name", ref = "userName"), 
        assembler = JacksonAssembler.class
    )
    private String id;
    private String userName;
    
    @Disassemble(targetClass = Foo.class, , disassembler = JacksonDisassembler.class)
    private List<Foo> foos;
    

    至此對象序列化時的填充配置就全部完成了。

    2、使用

    當使用注冊了 DynamicJsonNodeModule 模塊的 ObjectMapper 序列化對象時就會自動觸發填充。

    假如 ObjectMapper 被用于 Controller 自動序列化,則 Controller 中接口的返回值就會自動填充。而當 ObjectMapper 單獨使用時,調用 valueToTree 方法,或者 writeValueAsString 方法都會觸發自動填充。

    由于 JsonNode 的特殊性,相比普通的 JavaBean,它可以直接添加或替換對象的屬性值。

    追加字段

    假如我們有如下待序列化的對象,該對象只有一個 id 字段:

    @ProcessJacksonNode
    public class Foo {
        private String id;
    }
    

    我們可以根據 id 動態添加 name 和 age 字段:

    @ProcessJacksonNode
    public class Foo {
        @Assemble(assembler = JacksonAssembler, container = UserContainer.class, props = {
            @prop(src = "name", ref = "userName"), 
            @prop(src = "age", ref = "userAge")
        })
        private String id;
    }
    

    在序列化后得到如下 json 串:

    {
        "id": 1,
        "userName": "foo",
        "userAge": 12
    }
    

    替換字段

    由于 JsonNode 本身相當于一個大 Map 集合,因此我們可以無視 Class 中的類型,直接替換指定字段的值:

    @ProcessJacksonNode
    public class Foo {
        @Assemble(assembler = JacksonAssembler, container = KeyValueContainer.class, namespace = "sex")
        private Integer sex;
    }
    

    序列化后得到:

    {
        "sex": "男"
    }
    

    同理,如果是數據源容器中提供的數據源是對象也可以直接替換為對象:

    {
        "sex": {
            "id": 1,
            "name": "男"
        }
    }
    

    結語

    crane 的功能和特性不止本文所描述的這些,它還支持借助 reflectasm 庫將 JDK 原生的反射替換為字節碼調用優化性能,還支持各種緩存和基于配置文件的預加載等等.......

    它算是作者日常開發中面對這種頻繁的數據關聯需求總結出的一個解決方案,它的原型目前已經在公司生成環境投入使用。實際上,crane 肯定是不能適用于所有場景的,但是如果有類似需要在后臺處理字典項、配置項或者需要關聯數據的需求,使用 crane 能大大的提高開發效率。

    好吧不演了,這篇文章實際上就是菜雞作者鼓起勇氣推廣自己開源項目求使用求 start 的一篇軟文。crane 作為一個仍然還不完善的開源的項目,還需要更多人的使用與反饋,如果各位看官有興趣,可以去倉庫了解一下,點個 start,如果覺得有意思,或者有什么自己的想法,也歡迎提出 issues 或者直接加群討論!

    CRANE

    倉庫地址:https://gitee.com/CreateSequence/crane

    文檔:https://gitee.com/CreateSequence/crane/wikis/pages

    posted @ 2022-06-27 11:49  Createsequence  閱讀(117)  評論(0編輯  收藏  舉報
    国产美女a做受大片观看