现实的业务场景中,可能需要对Spring的实现类的私有方法进行测试。
场景描述:
比如XXXService里有 两个函数a、函数b。
而实现类XXXServiceImpl中实现了函数a、函数b,还包含私有方法函数c和函数d。
要写一个XXXTestController来调用XXXServiceImpl的函数c。
面临几个问题:
1、如果注入接口,则无法调用实现类的私有类。
2、如果注入实现类,则需要将实现类里的私有方法改为公有的,而且需要设置@EnableAspectJAutoProxy(proxyTargetClass = true)使用CGLIB代理方式
如果单纯为了测试而接口中定义实现类的私有方法或者为了测试而将私有方法临时改为公有方法,显然不太合适。
解决方案:
那么如何解决这个问题呢?是否可以封装一个通用的解决方案呢?
可以通过CGLIB注入实现类的子类,如果是Gradle项目也可以使用Aspect插件将切面代码在编译器织入实现类中注入的类型则为实现类,然后通过反射设置为可访问来调用私有方法。
下面是我的解决方案
另外还有一个更好的开源工具 PowerMock https://github.com/powermock/powermock,感兴趣的同学可以研究一下
反射调用代码:
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。
如果需要返回值,可以获取该调用方法的返回值。
如果有更好的解决方案,欢迎评论探讨。