反射實現批量驗證DAO層接口

場景描述

之前寫過一篇通過AOP在DAO層做分表插件的文章,最近要將其實際用在生產中。因為目前項目的DAO層是從之前的項目框架中整體遷移過來的,有很大的變化,為了避免疏漏,所以要對新mapper的接口做測試驗證以及問題修復。首先想到的是部署測試環境,逐個驗證請求接口,再一一處理有問題的部分。目前也確實是如此做的,好在之前遷移是使用自己寫的程序統一轉換的,錯誤相對來說比較少,主要是一些遺漏的補充,如resultMap等,所以驗證和修復效率比較高,沒遇到特別棘手的問題。

回到正題,以前有想法是通過反射來構造默認參數和值,來統一的驗證單個調用的可用性,恰好最近也有時間,所以做了個初步的實現,這裡簡單介紹下這種方式,懂行大佬輕拍。

思路描述

一個調用或者稱為方法,都有幾個要素:返回值、參數、方法名稱。比如: var func(param a, param b)。我們這裡只關注參數,而參數要關注的,就是參數的類型和具體值,且這裡的參數類型和值是嚴格相關的。舉個例子:對象的值,就是個新對象;基本類型如int,long 可以用Integer代替;泛型List還得關注包裝的具體類型List<string>,其他如boolean,enum,也需要不同的處理。總結來說,就是根據方法中參數類型生成對應的值,然後方法用這些參數值,來進行調用,最終可以驗證方法的可用性。/<string>

那為什麼要這麼做呢? 在工作中,對接口,方法的測試和驗證是非常常見的內容之一,除非要驗證接口的邏輯功能或者關注返回值,沒必要每個接口都自己手動構造有效參數來驗證,非常繁瑣。在項目中就遇到過對近70個接口做遷移後的使用驗證,這非常考驗耐性。所以,如果能自動生成默認值,至少對這個接口方法來說參數是有效的,寫一個公共工具,就可以批量進行不同接口的驗證。目前這個實現只是一個初步的構建,是對偏自動化測試的嘗試,可以基於此做延伸,實現更復雜的功能。


反射實現批量驗證DAO層接口


代碼和處理過程解析

<code>
//由於項目是springboot引入測試
@RunWith(SpringRunner.class)
@SpringBootTest
public class MapperTest {

//這裡是對應的mapper類,因為有很多,所以從idea複製引用出來
//可以直接在包名上右鍵批量複製引用(References)
String mappers = "com.xxx.xxx.xxx.mapper.CashXxxMapper\\n";
List<string> names = Lists.newArrayList(mappers.split("\\n"));

@Resource
private ApplicationContext context;//用來獲取bean,即DAO層的各種mapper

@Test
public void mapperTest() {
for (String s : names) {
try {
Class> clazz = Class.forName(s);
Method[] methods = clazz.getMethods();
Object obj = context.getBean(clazz);
for (Method method : methods) {
//用來存儲參數的值
List<object> values = Lists.newArrayList();
//拿到方法
for (Parameter parameter : method.getParameters()) {
String type = parameter.getType().getName();
Object o;
//基本類型和數字
if (type.equals("long") || type.equals("int") || type.contains("lang.Long") || type.contains("lang.Integer")) {
o = new Integer(2);
//列表泛型的處理
} else if (type.contains("util.List")) {
String rawType = parameter.getParameterizedType().getTypeName().replace("java.util.List", "");

Object raw;
//封裝類型處理
if (rawType.contains("lang.Integer") || rawType.contains("lang.Long") || rawType.equals("long") || rawType.equals("int")) {
raw = new Integer(3);
} else {
raw = Class.forName(rawType).getDeclaredConstructor().newInstance();
}
ArrayList list = new ArrayList();
list.add(raw);
o = list;
} else if (type.contains("java.util.Date")) {
//項目裡邊用到
o = new Date();
} else {
o = Class.forName(type).getDeclaredConstructor().newInstance();
//過濾其他元類型
if (!type.contains("java.lang")) {
try {
//對象參數
fieldsFill(o);
} catch (Exception e) {
System.err.println(type);
}
}
}
values.add(o);
}
try {
method.invoke(obj, values.toArray());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
// context.getBean()
}

//填充對象參數,可以看到和上面比較重複
private void fieldsFill(Object object) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = object.getClass();
Field[] fields = clazz.getDeclaredFields();
for (Field f : fields) {
int modifier = f.getModifiers();

//exclude static final
//前綴修飾符,很好理解是一個數字
if (Modifier.isFinal(modifier) && Modifier.isStatic(modifier)) {
continue;
}
String type = f.getType().getName();
//業務字段特殊需要,基於用thrift生成的對象
if (type.contains("_") || type.contains("metaDataMap") || type.contains("Enum")) {
continue;
}
f.setAccessible(true);
Object fo;
if (type.equals("long") || type.equals("int") || type.contains("lang.Long") || type.contains("lang.Integer")) {
fo = new Integer(2);
} else if (type.contains("List")) {
fo = new ArrayList<>();
} else if (type.equals("boolean")) {
continue;
} else {
fo = Class.forName(type).getDeclaredConstructor().newInstance();
}
f.set(object, fo);
}
}
}

/<object>/<string>/<code>

補充內容

因為這個實現是和數據庫mapper操作有關的,為了方便驗證結果,在本地運行中,對執行SQL加了日誌,可以很方便的追蹤結果。由於項目使用的druid連接數據源,之前在jdbc連接串後面加&profileSQL=true參數失敗,所以在application.yml配置文件添加了

<code>mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
/<code>

也能起到記錄執行SQL的作用。結果如下:

<code>SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7da40bf4] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3f36c191] will not be managed by Spring
==> Preparing: select `cash`, `event`, `create_time` from `xxx_record_2` WHERE `biz_type` = ? AND `user_id` = ? ORDER BY `create_time` DESC LIMIT ?, ?
==> Parameters: 2(Integer), 2(Long), 2(Integer), 2(Integer)
<== Total: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7da40bf4]
/<code>

總結

如上,通過這個實現,對Java反射有了更深的瞭解,功能確實非常強大,而且連private static final 修飾的字段也可以修改。總體來說,實現了最開始要求的目標,不過中間生成默認值的部分還是不夠優雅,可以看到有些情況是類似遞歸的,而且if條件也可以優化下,提高可讀性。再者後續最好能找些優秀的框架代碼多看看,反射相關在通用框架裡使用還是非常多的,當然也不只這一點,也可以用來學習更好的實現方式和代碼規範。


分享到:


相關文章: