現實的業務場景中,可能需要對Spring的實現類的私有方法進行測試。
場景描述:
比如XXXService裏有 兩個函數a、函數b。
而實現類XXXServiceImpl中實現了函數a、函數b,還包含私有方法函數c和函數d。
要寫一個XXXTestController來調用XXXServiceImpl的函數c。
面臨幾個問題:
1、如果注入接口,則無法調用實現類的私有類。
2、如果注入實現類,則需要將實現類裏的私有方法改爲公有的,而且需要設置@EnableAspectJAutoProxy(proxyTargetClass = true)使用CGLIB代理方式
如果單純爲了測試而接口中定義實現類的私有方法或者爲了測試而將私有方法臨時改爲公有方法,顯然不太合適。
解決方案:
那麼如何解決這個問題呢?是否可以封裝一個通用的解決方案呢?
可以通過CGLIB注入實現類的子類,如果是Gradle項目也可以使用Aspect插件將切面代碼在編譯器織入實現類中注入的類型則爲實現類,然後通過反射設置爲可訪問來調用私有方法。
反射調用代碼:
BeanInvokeUtil
public class BeanInvokeUtil {
public class InvokeParams {
// 方法名稱(私有)
private String methodName;
// 參數列表類型數組
private Class<?>[] paramTypes;
// 調用的對象
private Object object;
// 參數列表數組(如果不爲null,需要和paramTypes對應)
private Object[] args;
public InvokeParams() {
super();
}
public InvokeParams(Object object, String methodName, Class<?>[] paramTypes, Object[] args) {
this.methodName = methodName;
this.paramTypes = paramTypes;
this.object = object;
this.args = args;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class<?>[] getParamTypes() {
return paramTypes;
}
public void setParamTypes(Class<?>[] paramTypes) {
this.paramTypes = paramTypes;
}
public Object getObject() {
return object;
}
public void setObject(Object object) {
this.object = object;
}
public Object[] getArgs() {
return args;
}
public void setArgs(Object[] args) {
this.args = args;
}
}
public static Object invokePrivateMethod(InvokeParams invokeParams) throws InvocationTargetException, IllegalAccessException {
// 參數檢查
checkParams(invokeParams);
// 調用
return doInvoke(invokeParams);
}
private static Object doInvoke(InvokeParams invokeParams) throws InvocationTargetException, IllegalAccessException {
Object object = invokeParams.getObject();
String methodName = invokeParams.getMethodName();
Class<?>[] paramTypes = invokeParams.getParamTypes();
Object[] args = invokeParams.getArgs();
Method method;
if (paramTypes == null) {
method = BeanUtils.findDeclaredMethod(object.getClass(), methodName);
} else {
method = BeanUtils.findDeclaredMethod(object.getClass(), methodName, paramTypes);
}
method.setAccessible(true);
if (args == null) {
return method.invoke(object);
}
return method.invoke(object, args);
}
private static void checkParams(InvokeParams invokeParams) {
Object object = invokeParams.getObject();
if (object == null) {
throw new IllegalArgumentException("object can not be null");
}
String methodName = invokeParams.getMethodName();
if (StringUtils.isEmpty(methodName)) {
throw new IllegalArgumentException("methodName can not be empty");
}
// 參數類型數組和參數數組要對應
Class<?>[] paramTypes = invokeParams.getParamTypes();
Object[] args = invokeParams.getArgs();
boolean illegal = true;
if (paramTypes == null && args != null) {
illegal = false;
}
if (args == null && paramTypes != null) {
illegal = false;
}
if (paramTypes != null && args != null && paramTypes.length != args.length) {
illegal = false;
}
if (!illegal) {
throw new IllegalArgumentException("paramTypes length != args length");
}
}
}
使用方式:
使用時通過CGLIB方式注入實現類或者將切面代碼編譯器織入實現類的方式,然後注入Bean。
@Autowired private XXXService xxxService;
然後填入調用的對象,待調用的私有方法,參數類型數組和參數數組。
BeanInvokeUtil.invokePrivateMethod(new BeanInvokeUtil()
.new InvokeParams(xxxService, "somePrivateMethod", null, null));
注意這時注入的xxxService的類型爲 xxxServiceImpl。
如果需要返回值,可以獲取該調用方法的返回值。
如果有更好的解決方案,歡迎評論探討。