王春波 葛雷 文雪巍
【摘要】SpringBoot是由Pivotal團(tuán)隊(duì)在2013年開始研發(fā)、2014年4月發(fā)布第一個(gè)版本的全新開源的輕量級(jí)框架。它基于Spring4.0設(shè)計(jì),不僅繼承了Spring框架原有的優(yōu)秀特性,而且還通過簡化配置來進(jìn)一步簡化了Spring應(yīng)用的整個(gè)搭建和開發(fā)過程。該框架使用內(nèi)置的注解與Jackson插件可方便的將結(jié)果集序化成適合于移動(dòng)互聯(lián)網(wǎng)廠商作為業(yè)務(wù)接口的JSON格式字符串。由于框架的高度集成使得結(jié)果集序列化成JSON字符串時(shí)過于規(guī)則化,不能適用于較為復(fù)雜的業(yè)務(wù)場景,且對(duì)于復(fù)雜的結(jié)果集還會(huì)出現(xiàn)堆棧溢出錯(cuò)誤,筆者分析了SpringBoot消息序列化過程,結(jié)合業(yè)務(wù)場景,給出了如何靈活定義結(jié)果集序列化規(guī)則,如何避免堆棧溢出錯(cuò)誤的設(shè)計(jì)與實(shí)現(xiàn)方法,從而為使用SpringBoot框架作為開發(fā)技術(shù)棧的項(xiàng)目組提供了借鑒與參考。
【關(guān)鍵詞】SpringBoot? Jackson? 堆棧? JSON? 序列化
一、引言
Spring Boot是由Pivotal團(tuán)隊(duì)提供的全新框架,其設(shè)計(jì)目的是用來簡化新Spring應(yīng)用的初始搭建以及開發(fā)過程。該框架使用了特定的方式來進(jìn)行配置,從而使開發(fā)人員不再需要定義樣板化的配置。通過這種方式,Spring Boot致力于在蓬勃發(fā)展的快速應(yīng)用開發(fā)領(lǐng)域成為領(lǐng)導(dǎo)者。
SpringBoot框架中有兩個(gè)非常重要的策略:開箱即用和約定優(yōu)于配置。開箱即用,是指在開發(fā)過程中,通過在MAVEN項(xiàng)目的pom文件中添加相關(guān)依賴包,然后使用對(duì)應(yīng)注解來代替繁瑣的XML配置文件以管理對(duì)象的生命周期。這個(gè)特點(diǎn)使得開發(fā)人員擺脫了復(fù)雜的配置工作以及依賴的管理工作,更加專注于業(yè)務(wù)邏輯。約定優(yōu)于配置,是一種由SpringBoot本身來配置目標(biāo)結(jié)構(gòu),由開發(fā)者在結(jié)構(gòu)中添加信息的軟件設(shè)計(jì)范式。這一策略減少了開發(fā)人員需要做出決定的數(shù)量,同時(shí)減少了大量的XML配置,并且可以將代碼編譯、測試和打包等工作自動(dòng)化,但也降低了部分靈活性,增加了缺陷定位的復(fù)雜性。
對(duì)于復(fù)雜的項(xiàng)目需求,SpringBoot缺少靈活性的結(jié)果集轉(zhuǎn)換方式,不但不能降低開發(fā)人員勞動(dòng)強(qiáng)度和復(fù)雜度,相反極大的增加了開發(fā)人員的工作量以及出現(xiàn)去缺陷的可能。因此,需要找到一個(gè)確實(shí)有效方法允許開發(fā)人員靈活配置結(jié)果集序列化規(guī)則,但同時(shí)又不能破壞或改變SpringBoot框架原有的優(yōu)勢(shì)。
二、SpringBoot結(jié)果集序列化的現(xiàn)狀與問題
SpringBoot默認(rèn)使用Jackson插件進(jìn)行結(jié)果集序列化。SpringBoot允許開發(fā)者使用其他插件例如Fastjson替換默認(rèn)的Jackson進(jìn)行結(jié)果集序列化,但無論使用哪種插件都面臨著序列化規(guī)則僵化、不靈活的弊端。試想當(dāng)相同的視圖對(duì)象應(yīng)用于不同的請(qǐng)求接口時(shí),就會(huì)出現(xiàn)需要的請(qǐng)求字段不一樣場景,例如以下部門定義實(shí)體:
public class SysDepart {
private String departAddr; //部門地址
private String departPhone; //部門電話
private Integer departLevel; //部門級(jí)別
private String departName; //部門名稱
private String departExplain; //部門說明
private String departType; //部門類型
private SysSchool school; //歸屬學(xué)校
private SysDepart parentDepart ; //父級(jí)部
private List
}
針對(duì)獲取部門基本信息的接口,僅需要返回departAddr、departLevel、departName、departExplain、departType這個(gè)五個(gè)屬性字段即可;針對(duì)獲取部門員工列表的接口,僅需返回List
對(duì)于這類問題通常有以下4中解決辦法:
(1)返回全部屬性字段:即由接口發(fā)起方進(jìn)行二次判斷,這種解決辦法不僅增加了開發(fā)人員的工作量,也給系統(tǒng)帶來了額外開銷。特別的,如果視圖對(duì)象存在雙向關(guān)聯(lián)關(guān)系,即SysDepart擁有屬性字段SysSchool school,同時(shí)SysSchool擁有屬性字段List
(2)結(jié)果集二次處理:適當(dāng)增加注解,對(duì)結(jié)果集進(jìn)行二次處理。該方法雖能夠避免開發(fā)人員進(jìn)行多余的判斷,但卻不能避免首次結(jié)果集序列化JSON時(shí)發(fā)起的不需要的關(guān)聯(lián)查詢,以及避免序列化兩個(gè)構(gòu)成雙向一對(duì)多關(guān)系實(shí)體時(shí)出現(xiàn)的堆棧溢出錯(cuò)誤。
(3)Jackson提供@JsonFilter 注解實(shí)現(xiàn)結(jié)果集的動(dòng)態(tài)過濾,但該注解使用麻煩,需要一個(gè)對(duì)象寫多個(gè)子類以區(qū)分不同的結(jié)果集,對(duì)于更復(fù)雜關(guān)聯(lián)查詢則顯得捉襟見肘。
(4)無論是Jackson還是FastJson都提供了簡單過濾器,但這類過濾器僅能進(jìn)行全局配置,無法做到個(gè)性化輸出。
綜上所述,無論是SpringBoot官方還是插件提供商均未曾為該類問題提供方便有效的解決辦法。筆者充分分析SpringBoot結(jié)果集序列化過程,通過擴(kuò)展接口、自定義解析規(guī)則、增加必要注解,利用AOP編程思想編碼實(shí)現(xiàn)了解決該類問題的插件,插件命名為Power-filter。Power-filter可以根據(jù)不同的業(yè)務(wù)場景配置返回不同的結(jié)果集,不僅能夠有效避免雙向關(guān)聯(lián)實(shí)體bean因循環(huán)查詢導(dǎo)致的堆棧溢出問題,也能減少非必要查詢,降低開發(fā)人員的編碼復(fù)雜度的同時(shí)也提高系統(tǒng)的查詢效率。
三、Power-filter的設(shè)計(jì)與實(shí)現(xiàn)
(一)SpringBoot 消息序列化原理
SpringBoot使用消息轉(zhuǎn)換器對(duì)結(jié)果集進(jìn)行序列化,原理圖如圖1:
從圖2可知,客戶端發(fā)送消息時(shí),消息轉(zhuǎn)換器的作用是將對(duì)象序列化為某一格式報(bào)文,然后將報(bào)文發(fā)送另一端;接收消息時(shí),消息轉(zhuǎn)換器作用是將某一格式報(bào)文轉(zhuǎn)換為對(duì)象。
SpringBoot框架內(nèi)置了很多HTTP消息轉(zhuǎn)換器,不同消息類型轉(zhuǎn)換器處理不同Content-type類型數(shù)據(jù)。如MappingJackson2HttpMessageConverter處理請(qǐng)求類型為application/json類型數(shù)據(jù), StringHttpMessageConverter 處理類型為為 text/
html類型數(shù)據(jù)等。在框架內(nèi)部會(huì)根據(jù)不同請(qǐng)求類型值選擇不同類型轉(zhuǎn)換器進(jìn)行消息轉(zhuǎn)換。目前,結(jié)果集的序列化過程就是通過默認(rèn)名為MappingJackson2HttpMessageConverter的轉(zhuǎn)換器來實(shí)現(xiàn)的。
SpringBoot框架提供了HTTP消息轉(zhuǎn)換器的處理接口,允許開發(fā)者自定義消息序列化規(guī)則,因此可通過實(shí)現(xiàn)消息轉(zhuǎn)換器處理接口,用于替換原有處理application/json類型數(shù)據(jù)的轉(zhuǎn)換器來達(dá)到動(dòng)態(tài)過濾屬性字段的需求。
(二)Power-filter設(shè)計(jì)思路
Power-filter的設(shè)計(jì)目的是為了在原有的框架上擴(kuò)展功能,適應(yīng)多變的接口需求。通過深入分析 SpringBoot 消息序列化原理,結(jié)合需求,筆者得出如圖2的設(shè)計(jì)思路。
圖2中標(biāo)識(shí)①②③的區(qū)域是Power-filter插件實(shí)現(xiàn)的關(guān)鍵點(diǎn):①允許開發(fā)者根據(jù)不同業(yè)務(wù)接口需求靈活設(shè)置序列化規(guī)則。②請(qǐng)求發(fā)起時(shí),框架能夠獲取到針對(duì)該請(qǐng)求設(shè)置的過濾規(guī)則,并緩存它。③SpringBoot調(diào)用自定義的消息轉(zhuǎn)換器,獲取相應(yīng)規(guī)則,解析并應(yīng)用規(guī)則進(jìn)行消息序列化。
(三)Power-filter實(shí)現(xiàn)方法
針對(duì)圖2的設(shè)計(jì)思路中提到的三個(gè)關(guān)鍵點(diǎn),實(shí)現(xiàn)方式如下:
(1)對(duì)于①,筆者使用注解的方式進(jìn)行實(shí)現(xiàn)。Java 注解又稱 Java 標(biāo)注,它允許Java 語言中的類、方法、變量、參數(shù)和包等都可以被標(biāo)注。Power-filter新定義的注解如下:
注解1:PowerJsonFilter
@Retention(RUNTIME)
@Target(value = {ElementType.METHOD})
@Repeatable(value=PowerJsonFilters.class)
public @interface PowerJsonFilter {
Class<?> clazz();
String[] include() default {};// include為對(duì)象需要包含的字段
}
注解2:PowerJsonFilters
@Retention(RUNTIME)
@Target({METHOD })
public @interface PowerJsonFilters {
PowerJsonFilter[] value();
}
從代碼中可以看出,兩個(gè)注解只能使用在方法上,注解2是注解1的數(shù)組形態(tài),可支持多規(guī)則設(shè)置。注解1接收兩個(gè)參數(shù):
clazz:用于指示哪些實(shí)體bean需要消息序列化。如 clazz = SysDepart.class
include:為字符串?dāng)?shù)組類型,用于指示需要消息序列化的實(shí)體bean中哪些屬性可以被序列化,它的格式如下:include = {"字段1","字段2","字段m","字段x:{字段x-1, 字段x-2:{ 字段x-2-1,[字段y:{…}]}}","字段n"}。舉例說明該格式的含義,對(duì)于注解:@PowerJsonFilters({@PowerJsonFilter(clazz = SysDepart.class,include= {"departId","departName" ,"parentDepart:{departName,school:
{schoolName}}"}))。它的含義是:對(duì)于SysDepart實(shí)體bean,需要對(duì)其屬性"departId","departName","parentDepart"進(jìn)行序列化,特別的對(duì)于parentDepart屬性,其所屬類型不是簡單字符串、整形等常用數(shù)據(jù)類型,而是用戶自定義的實(shí)體,parentDepart:{departName,school:{schoolName}}意味著僅需要序列化parentDepart所屬實(shí)體的"departName","school"屬性。對(duì)于school屬性也做同樣的解析。該規(guī)則支持多重嵌套,只要開發(fā)人員設(shè)置合理,即可避免實(shí)體bean的雙向關(guān)聯(lián)查詢導(dǎo)致的堆棧溢出錯(cuò)誤。
(2)對(duì)于②,使用AOP編程思想,定義新的切入點(diǎn),實(shí)現(xiàn)流程圖如圖3:
(3)對(duì)于③,需要遵守SpringBoot框架的消息轉(zhuǎn)換器的接口規(guī)范進(jìn)行實(shí)現(xiàn),實(shí)現(xiàn)流程如圖4:
四、Power-filter應(yīng)用與對(duì)比測試
(一)Power-filter安裝與應(yīng)用
Power-filter的設(shè)計(jì)初衷是解決SpringBoot對(duì)結(jié)果集進(jìn)行序列化時(shí)遇到諸多不便問題,因此Power-filter僅是SpringBoot的有益補(bǔ)充,它的安裝使用依賴于SpringBoot框架。Power-filter的安裝使用步驟為:
(1)在SpringBoot啟動(dòng)方法上增加注解掃描范圍,例如:@SpringBootApplication(scanBasePackages = {"xxx.xxx.xxx","com.hanb.filterJson"})
(2)刪除原有消息轉(zhuǎn)換器,增加自定義的消息轉(zhuǎn)換器,代碼如下:
public void configureMessageConverters(List
for (int i = converters.size() - 1; i >= 0; i--) {
//找到并刪除默認(rèn)的消息轉(zhuǎn)換器
}
//聲明自定義消息轉(zhuǎn)換器并加入消息轉(zhuǎn)換器轉(zhuǎn)換鏈
PowerHttpMessageConverter myConverter=new PowerHttpMessageConverter();
converters.add(myConverter);
}
(3)在需要定義序列化規(guī)則的方法上增加注解,代碼示例如下:
@RestController
public class XxAction {
@PowerJsonFilters({@PowerJsonFilter(clazz = SysDepart.class,include =? {"departId","school","parentDepart:{departName,school:{schoolName}}"}),
@PowerJsonFilter(clazz = School.class,include = {"schoolAddr"})})
public Object queryDepart(參數(shù)列表)? throws Exception{
//業(yè)務(wù)邏輯代碼
}? }
通過(1)、(2)的安裝,(3)的應(yīng)用,程序運(yùn)行后即可得出規(guī)則設(shè)定的結(jié)果,如下示例:
[{"departId":1,"school":{"schoolAddr":"學(xué)院路"}},{"departId":2,"parentDepart":{"departName":"1級(jí)部門","school":{"schoolName":"財(cái)經(jīng)"}},"school":{"schoolName":"財(cái)經(jīng)"}}]
(二)SpringBoot使用Power-filter插件序列化效率前后對(duì)比
Power-filter能夠通過設(shè)置過濾規(guī)則靈活對(duì)結(jié)果集進(jìn)行過濾,為了對(duì)比應(yīng)用插件前后的效率,筆者在同一軟硬件環(huán)境下分別做了單表查詢結(jié)果集序列化,多表關(guān)聯(lián)查詢結(jié)果集序列化對(duì)比測試。
(1)單表查詢100條記錄序列化對(duì)比如圖5:
(2)多表關(guān)聯(lián)查詢100條記錄序列化對(duì)比如圖6。
通過對(duì)比測試可發(fā)現(xiàn),對(duì)于單表查詢,兩者對(duì)查詢結(jié)果的序列化耗時(shí)相差并不大,對(duì)于多表關(guān)聯(lián)查詢,框架由于應(yīng)用了過濾規(guī)則避免了無用屬性的關(guān)聯(lián)查詢與序列化,其效率大大提高。
五、總結(jié)
Power-filter是依賴于SpringBoot框架的插件,該插件使用AOP編程思想進(jìn)行編寫,主要做了以下3點(diǎn)工作:
(1)新增注解,用于對(duì)查詢結(jié)果集序列化規(guī)則進(jìn)行配置。
(2)新增過濾規(guī)則,使用堆數(shù)據(jù)結(jié)構(gòu)完成對(duì)實(shí)體bean的多層嵌套比對(duì)。
(3)實(shí)現(xiàn)自定義的消息轉(zhuǎn)換器,替換SpringBoot原生插件,完成結(jié)果集的序列化。
通過對(duì)比測試發(fā)現(xiàn),使用了Power-filter插件的SpringBoot有以下4點(diǎn)優(yōu)勢(shì):
(1)配置規(guī)則簡單易懂,學(xué)習(xí)成本極低。
(2)對(duì)結(jié)果集序列化按規(guī)則進(jìn)行配置,使得業(yè)務(wù)接口更加靈活,降低開發(fā)者的大量重復(fù)性工作。
(3)通過規(guī)則配置,可有效避免無用屬性字段的關(guān)聯(lián)查詢,節(jié)約了系統(tǒng)開銷。
(4)通過規(guī)則配置,可有效避免雙向關(guān)聯(lián)實(shí)體bean引起的循環(huán)查詢,從而導(dǎo)致的堆棧溢出錯(cuò)誤。
Power-filter作為SpringBoot的非原生插件,雖然有著自身的優(yōu)點(diǎn),但也存在著不足:
(1)需要做額外配置,插件方可生效。
(2)過濾規(guī)則暫不支持通配符配置,規(guī)則書寫略顯麻煩。
(3)規(guī)則配置方式單一,目前僅支持包含設(shè)置,不支持排除設(shè)置。
對(duì)于不足中的(2)和(3),筆者將繼續(xù)完善該插件,爭取早日彌補(bǔ)其不足。
總之,Power-filter作為SpringBoot的有益補(bǔ)充,雖然有些許不足,但仍能夠?yàn)槭褂肧pringBoot框架作為開發(fā)技術(shù)棧的項(xiàng)目組提供借鑒與參考,為其快速完成接口開發(fā)提供有效的解決方案。
參考文獻(xiàn):
[1]小馬哥. Spring Boot編程思想(核心篇)[M].北京:電子工業(yè)出版社,2019:155-187.
[2][美] Bruce Eckel.Think In Java)[M].北京:機(jī)械工業(yè)出版社,2007:162-199.
[3]王曉東,計(jì)算機(jī)算法設(shè)計(jì)與分析(第5版))[M],北京:電子工業(yè)出版社,2018:202-351.
作者簡介: 王春波(1978-),男,漢族,黑龍江海倫市人,碩士,信息系統(tǒng)項(xiàng)目高級(jí)管理師,研究方向:軟件工程、大數(shù)據(jù)理論、神經(jīng)網(wǎng)路算法;葛雷(1973-),男,漢族,黑龍江哈爾濱人,碩士,教授,研究方向:軟件工程、教育理論;文雪?。?979-),女,漢族,黑龍江哈爾濱人,碩士,教授,主研究方向:軟件工程、人工智能。