New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IllegalAccessException when mocking package-private Spring controller #947

Open
natix643 opened this Issue Nov 1, 2018 · 6 comments

Comments

Projects
None yet
3 participants
@natix643

natix643 commented Nov 1, 2018

Issue description

Hello, I'm trying to mock a Spring controller using the new @SpringBean annotation and then call the mapped method using TestRestTemplate (also tried MockMvc with same results). This works only if the controller java class is public, but the test fails with IllegalAccessException if I change the access to package-private.

Bellow is a minimal Spring Boot project (I can provide a repo with an easy-to-run version if needed). I also tried using different tool versions, such as JDK 11, Spring Boot 2.0, Groovy 2.4 or adding a cglib dependency instead of relying on the default Byte Buddy with no difference.

How to reproduce

main/java:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@RestController
/* public */ class HelloController { // works with public, fails without
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

test/groovy

@SpringBootTest(webEnvironment = RANDOM_PORT)
class HelloControllerTest extends Specification {

    @SpringBean
    HelloController controller = Mock()

    @Autowired
    TestRestTemplate rest

    def "should mock it"() {
        given:
        controller.hello() >> "goodbye"

        expect:
        rest.getForObject("/hello", String) == "goodbye"
    }
}

Resulting exception:

java.lang.IllegalAccessException: Class org.spockframework.spring.mock.DelegatingInterceptor can not access a member of class mock.HelloController with modifiers "public"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102) ~[na:1.8.0_144]
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296) ~[na:1.8.0_144]
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288) ~[na:1.8.0_144]
	at java.lang.reflect.Method.invoke(Method.java:491) ~[na:1.8.0_144]
	at org.spockframework.spring.mock.DelegatingInterceptor.intercept(DelegatingInterceptor.java:53) ~[spock-spring-1.2-groovy-2.5.jar:1.2]
	at org.spockframework.mock.runtime.ByteBuddyInterceptorAdapter.interceptNonAbstract(ByteBuddyInterceptorAdapter.java:35) ~[spock-core-1.2-groovy-2.5.jar:1.2]
	at mock.HelloController$SpockMock$280894713.hello(Unknown Source) ~[na:na]
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_144]
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_144]
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_144]
	at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_144]
	at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:215) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:142) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:998) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:890) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:634) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:875) ~[spring-webmvc-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-5.1.2.RELEASE.jar:5.1.2.RELEASE]
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199) ~[tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:770) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_144]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_144]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.12.jar:9.0.12]
	at java.lang.Thread.run(Thread.java:748) [na:1.8.0_144]

Additional Environment information

Java/JDK

jdk1.8.0_144

Build tool version

Gradle

4.10.2

Build-tool dependencies used

Gradle

plugins {
    id 'java'
    id 'groovy'
    id 'org.springframework.boot' version '2.1.0.RELEASE'
    id 'io.spring.dependency-management' version '1.0.6.RELEASE'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.spockframework:spock-spring:1.2-groovy-2.5'
}
@leonard84

This comment has been minimized.

Member

leonard84 commented Nov 2, 2018

Yes, this is problem with the way @SpringBeans work, i.e. you actually have two mocks, one in the Spring Context and the other you create in the Specification. The mock in the Spring Context will find the mock of the Specification and delegate all calls to it. This is where the package-private access violation happens.

As a workaround you can use the old DetachedMockFactory style as this doesn't use delegation and thus will not suffer from package private visibility issues. Albeit being a bit more clunky in the usage.

But as far as I understand this is only a temporary workaround, if you switch to the module system those package private boundaries will be stronger enforced and not mockable anymore. Correct me if I'm wrong @raphw.

@raphw

This comment has been minimized.

Contributor

raphw commented Nov 3, 2018

Yes, for overriding a package-private method, the subclass must be defined by the same class loader. Just as with classes, packages are just considered equal if they are by the same class loader. If you cannot inject a class into a class loader, you cannot mock package-private types and methods.

@natix643

This comment has been minimized.

natix643 commented Nov 5, 2018

Could you overcome this by for example calling setAccessible(true) on the given class? Spring generally has no problem with reflectively calling package-private (or perhaps private) classes and methods, let alone Groovy. Not sure what role different classloaders have in this though.

Regarding the Java 9 module system, I don't think this is that much of an issue, because the current approach still boils down to declaring your modules as open (either to a specific framework module, or simply to the whole world), as there is no easy other way how to make these reflection-based frameworks work.

@raphw

This comment has been minimized.

Contributor

raphw commented Nov 5, 2018

The problem is that the related class loader methods are always modularized since Java 9. At the same time, setting methods accessible has no effect on the byte code dispatch. Other than that, if a module is open, Byte Buddy already offers means to inject a class.

@leonard84

This comment has been minimized.

Member

leonard84 commented Nov 5, 2018

I was wondering, shouldn't the line .transform(Transformer.ForMethod.withModifiers(SynchronizationState.PLAIN, Visibility.PUBLIC)) (org/spockframework/mock/runtime/ByteBuddyMockFactory.java:66) overwrite every method as public?

@raphw

This comment has been minimized.

Contributor

raphw commented Nov 6, 2018

Yes, but that does not matter if the base method is package-private which determines the virtual dispatch scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment