Several Problems of Implementing Private Method of Class in Spring General Solution of of Pro test

  • 2021-09-16 07:11:35
  • OfStack

In a real-world business scenario, you might need to test the private methods of the Spring implementation class.

Scene description:

For example, there are two functions a and b in XXXService.

The implementation class XXXServiceImpl implements functions a and b, and also includes private method functions c and d.

Write an XXXTestController to call the function c of XXXServiceImpl.

There are several problems:

1. If the interface is injected, the private class of the implementation class cannot be called.

2. If the implementation class is injected, the private method in the implementation class needs to be changed to public, and the @ EnableAspectJAutoProxy (proxyTargetClass = true) needs to be set to use CGLIB proxy mode

It is obviously not appropriate to define private methods that implement classes in the interface simply for testing or temporarily change private methods to public methods for testing.

Solution:

You can inject subclasses of the implementation class through CGLIB, and you can also use Aspect plug-in if you are an Gradle project. The type injected by weaving the section code into the implementation class in the compiler is the implementation class, and then set it to accessible through reflection to call private methods.

Scenario 1 uses the BeanUtils. findDeclaredMethod reflection method

Reflection call code:

BeanInvokeUtil


public class BeanInvokeUtil {
 
  public class InvokeParams {
 
//  Method name (private) 
    private String methodName;
 
//  Array of parameter list types 
    private Class<?>[] paramTypes;
//  Object called 
    private Object object;
 
//  Array of parameter lists (if not null , needs and paramTypes Correspondence) 
    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 {
    //  Parameter check 
    checkParams(invokeParams);
    //  Call 
    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");
    }
 
    //  Array of parameter types and array of parameters should correspond 
    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");
    }
  }
}

Usage:

When used, the implementation class is injected by CGLIB or the section code compiler is woven into the implementation class, and then Bean is injected.


@Autowired private XXXService xxxService;

Then fill in the called object, the private method to be called, the array of parameter types, and the array of parameters.


BeanInvokeUtil.invokePrivateMethod(new BeanInvokeUtil()
            .new InvokeParams(xxxService, "somePrivateMethod", null, null));

Note that the type of xxxService injected at this time is xxxServiceImpl.

If you need a return value, you can get the return value of the calling method.

Scenario 2: Use jdk and cglib tools to get real objects

Test class


public class Test {
  @Autowired
  ServiceImpl serviceImpl;

  @Test
  public void test() throws Exception {
  Class<?> clazz = Class.forName("ServiceImpl");
  Method method = clazz.getDeclaredMethod("MethodA");
  method.setAccessible(true);
  Object target = ReflectionUtil.getTarget(serviceImpl);
  //  Note that you can't use it directly here serviceImpl Because it has been spring Management, 
  //  Become serviceImpl The proxy class of the real instance, and there is no private method in the proxy class, so you need to get its real instance first 
  method.invoke(target);
  }
}

A tool class for obtaining a real instance of an spring proxy object, suitable for two dynamic proxy situations: jdk and cglib


public class ReflectionUtil {
  /**
   *  Get spring  Real instance of proxy object 
   * @param proxy  Proxy object 
   * @return
   * @throws Exception
   */
  public static Object getTarget(Object proxy) throws Exception {
    if(!AopUtils.isAopProxy(proxy)) {
      return proxy;// Is not a proxy object 
    }

    if(AopUtils.isJdkDynamicProxy(proxy)) {
      return getJdkDynamicProxyTargetObject(proxy);
    } else { //cglib
      return getCglibProxyTargetObject(proxy);
    }
  }

  private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
    Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
    h.setAccessible(true);
    Object dynamicAdvisedInterceptor = h.get(proxy);
    Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
    advised.setAccessible(true);
    Object target = ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
    return target;
  }

  private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
    Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
    h.setAccessible(true);
    AopProxy aopProxy = (AopProxy) h.get(proxy);
    Field advised = aopProxy.getClass().getDeclaredField("advised");
    advised.setAccessible(true);
    Object target = ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();
    return target;
  }
}

In addition, there is a better open source tool PowerMock https://github.com/powermock/powermock. Interested students can study the following

The above is the Spring implementation class private method test general program details, more about Spring class private method information please pay attention to other related articles on this site!


Related articles: