Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get the class of a field of type T?

I write JUnit tests for some Spring MVC Controllers. The initialization of the JUnit test is common for all my Controllers tests, so I wanted to create an abstract class that does this initialization.

Thus, I created the following code:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath*:spring/applicationContext-test.xml", "classpath*:spring/spring-mvc-test.xml" })
@Transactional
public abstract class AbstractSpringMVCControllerTest<T> {

    @Autowired
    protected ApplicationContext applicationContext;

    protected MockHttpServletRequest request;

    protected MockHttpServletResponse response;

    protected HandlerAdapter handlerAdapter;

    protected T controller;

    @SuppressWarnings("unchecked")
    @Before
    public void initContext() throws SecurityException, NoSuchFieldException {
        request = new MockHttpServletRequest();
        response = new MockHttpServletResponse();
        handlerAdapter = applicationContext.getBean(AnnotationMethodHandlerAdapter.class);
        // Does not work, the problem is here...    
        controller = applicationContext.getBean(T);
    }

}

The idea is to create, for each controller I want to test a JUnit test class that extends my AbstractSpringMVCControllerTest. The type given in the extends declaration is the class of the Controller.

For example, if I want to test my AccountController, I will create the AccountControllerTest class like that:

public class AccountControllerTest extends AbstractSpringMVCControllerTest<AccountController> {

    @Test
    public void list_accounts() throws Exception {
        request.setRequestURI("/account/list.html");
        ModelAndView mav = handlerAdapter.handle(request, response, controller);
        ...
    }

}

My problem is located in the last line of the initContext() method of the abstract class. This abstract class declares the controller object as a T object, but how can say to the Spring Application Context to return the bean of type T?

I've tried something like that:

    Class<?> controllerClass = this.getClass().getSuperclass().getDeclaredField("controller").getType();
    controller = (T) applicationContext.getBean(controllerClass);

but controllerClass returns the java.lang.Object.class class, not AccountController.class.

Of course, I can create a public abstract Class<?> getControllerClass(); method, which will be overriden by each JUnit Controller test class, but I prefer to avoid this solution.

So, any idea?

like image 307
Romain Linsolas Avatar asked Jun 22 '12 15:06

Romain Linsolas


3 Answers

This is possible if your subclasses of AbstractSpringMVCControllerTest bind T at compile time. That is, you have something like

public class DerpControllerTest extends AbstractSpringMVCControllerTest<DerpController> { }

rather than

public class AnyControllerTest<T> extends AbstractSpringMVCControllerTest<T> { }

I'm guessing you probably have the former. In this case, the type of T is erased from the Class object for AbstractSpringMVCControllerTest at runtime, but the Class object for DerpControllerTest does provide a way to know what T is, since it bound T at compile time.

The following classes demonstrate how to access the type of T:

Super.java

import java.lang.reflect.ParameterizedType;
public abstract class Super<T> {

    protected T object;

    @SuppressWarnings("unchecked")
    public Class<T> getObjectType() {
        // This only works if the subclass directly subclasses this class
        return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
    }

}

Sub.java

public class Sub extends Super<Double> {
}

Test.java

public class Test {
    public static void main(String...args) {
        Sub s = new Sub();
        System.out.println(s.getObjectType()); // prints "Class java.lang.Double"
    }
}
like image 189
matts Avatar answered Nov 07 '22 05:11

matts


This is different from the type erasure we normally see. With type erasure, we don't know the parameter of the current class (the one you get with getClass()), but you can get those in super class / super interface (those you get with getGenericSuperxxxxx()) because this is part of the type declaration.

This won't give your the type of controller field, but I hope this is enough for your purpose.

Code:

public class A<P> {
}

import java.lang.reflect.ParameterizedType;

public class B extends A<String> {

    public  static void main(String[] arg) {
        System.out.println(
                ((ParameterizedType)B.class.getGenericSuperclass()).getActualTypeArguments()[0]);
    }
}

Output:

class java.lang.String

In your case, it would be

Class controllerClass = (Class)( ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0]);

Something to notes:

If the class B is also parameterized like this:

public class B<X> extends A<X> {}

This won't work. Or if you have another class extends B, it will have problem too. I won't go into all those cases, but you should get the idea.

like image 44
J-16 SDiZ Avatar answered Nov 07 '22 06:11

J-16 SDiZ


You can't because at runtime, due to ERASURE, the JVM cannot know the class of your "controller" attribute. It is considered as Object...

like image 43
doanduyhai Avatar answered Nov 07 '22 05:11

doanduyhai