場景描述
之前寫過一篇通過AOP在DAO層做分表插件的文章,最近要將其實際用在生產(chǎn)中。因為目前項目的DAO層是從之前的項目框架中整體遷移過來的,有很大的變化,為了避免疏漏,所以要對新mapper的接口做測試驗證以及問題修復(fù)。首先想到的是部署測試環(huán)境,逐個驗證請求接口,再一一處理有問題的部分。目前也確實是如此做的,好在之前遷移是使用自己寫的程序統(tǒng)一轉(zhuǎn)換的,錯誤相對來說比較少,主要是一些遺漏的補(bǔ)充,如resultMap等,所以驗證和修復(fù)效率比較高,沒遇到特別棘手的問題。
回到正題,以前有想法是通過反射來構(gòu)造默認(rèn)參數(shù)和值,來統(tǒng)一的驗證單個調(diào)用的可用性,恰好最近也有時間,所以做了個初步的實現(xiàn),這里簡單介紹下這種方式,懂行大佬輕拍。
思路描述
一個調(diào)用或者稱為方法,都有幾個要素:返回值、參數(shù)、方法名稱。比如: var func(param a, param b)。我們這里只關(guān)注參數(shù),而參數(shù)要關(guān)注的,就是參數(shù)的類型和具體值,且這里的參數(shù)類型和值是嚴(yán)格相關(guān)的。舉個例子:對象的值,就是個新對象;基本類型如int,long 可以用Integer代替;泛型List還得關(guān)注包裝的具體類型List<String>,其他如boolean,enum,也需要不同的處理??偨Y(jié)來說,就是根據(jù)方法中參數(shù)類型生成對應(yīng)的值,然后方法用這些參數(shù)值,來進(jìn)行調(diào)用,最終可以驗證方法的可用性。
那為什么要這么做呢? 在工作中,對接口,方法的測試和驗證是非常常見的內(nèi)容之一,除非要驗證接口的邏輯功能或者關(guān)注返回值,沒必要每個接口都自己手動構(gòu)造有效參數(shù)來驗證,非常繁瑣。在項目中就遇到過對近70個接口做遷移后的使用驗證,這非??简?zāi)托?。所以,如果能自動生成默認(rèn)值,至少對這個接口方法來說參數(shù)是有效的,寫一個公共工具,就可以批量進(jìn)行不同接口的驗證。目前這個實現(xiàn)只是一個初步的構(gòu)建,是對偏自動化測試的嘗試,可以基于此做延伸,實現(xiàn)更復(fù)雜的功能。
代碼和處理過程解析
//由于項目是springboot引入測試
@RunWith(SpringRunner.class)
@SpringBootTest
public class MapperTest {
//這里是對應(yīng)的mapper類,因為有很多,所以從idea復(fù)制引用出來
//可以直接在包名上右鍵批量復(fù)制引用(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) {
//用來存儲參數(shù)的值
List<Object> values = Lists.newArrayList();
//拿到方法
for (Parameter parameter : method.getParameters()) {
String type = parameter.getType().getName();
Object o;
//基本類型和數(shù)字
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<", "").replace(">", "");
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 {
//對象參數(shù)
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()
}
//填充對象參數(shù),可以看到和上面比較重復(fù)
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
//前綴修飾符,很好理解是一個數(shù)字
if (Modifier.isFinal(modifier) && Modifier.isStatic(modifier)) {
continue;
}
String type = f.getType().getName();
//業(yè)務(wù)字段特殊需要,基于用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);
}
}
}
補(bǔ)充內(nèi)容
因為這個實現(xiàn)是和數(shù)據(jù)庫mapper操作有關(guān)的,為了方便驗證結(jié)果,在本地運行中,對執(zhí)行SQL加了日志,可以很方便的追蹤結(jié)果。由于項目使用的druid連接數(shù)據(jù)源,之前在jdbc連接串后面加&profileSQL=true參數(shù)失敗,所以在application.yml配置文件添加了
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
也能起到記錄執(zhí)行SQL的作用。結(jié)果如下:
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]
總結(jié)
如上,通過這個實現(xiàn),對Java反射有了更深的了解,功能確實非常強(qiáng)大,而且連private static final 修飾的字段也可以修改。總體來說,實現(xiàn)了最開始要求的目標(biāo),不過中間生成默認(rèn)值的部分還是不夠優(yōu)雅,可以看到有些情況是類似遞歸的,而且if條件也可以優(yōu)化下,提高可讀性。再者后續(xù)最好能找些優(yōu)秀的框架代碼多看看,反射相關(guān)在通用框架里使用還是非常多的,當(dāng)然也不只這一點,也可以用來學(xué)習(xí)更好的實現(xiàn)方式和代碼規(guī)范。