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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章