Describe the bug
When attempting to inject a dependency that implements an interface, if you use the class rather than the interface it throws a java.lang.IllegalStateException whereas using the interface is fine.
This only seems to be an issue if you try to make a "helper" method that lives outside of a @Test function.
This is also not a problem if you're injecting a class that doesn't implement an interface.
Expected behavior
You should be able to inject either the interface or a specific implementation, and creating a helper method outside of a JUnit test should be fine for either.
Actual behavior
I have an interface called TestInterface, and an implementation called TestInterfaceImpl and the TestInterfaceImpl is annotated with @ApplicationScoped.
I have a series of tests that inject this dependency in order to call various methods on the dependency, using the context for the rest of the DI. If I attempt to retrieve the dependency within a JUnit test (annotated with @Test) then both of these methods work:
@Test
fun testOne() {
val dep = CDI.current().select(TestInterface::class.java).get()
// Do some stuff - this works
}
@Test
fun testTwo() {
val dep = CDI.current().select(TestInterfaceImpl::class.java).get()
// Do some stuff - this works
}
I have ended up repeating this across all of my tests, so wanted to take this out to a helper function. This sits within the class, but is not part of any @Test function:
private fun getDependency() = CDI.current().select(TestInterface::class.java).get()
@Test
fun testThree() {
val dep = getDependency()
// Do some stuff - this works
}
This above function is fine, but the following does not work and results in the below stack trace.
private fun getDependency() = CDI.current().select(TestInterfaceImpl::class.java).get()
@Test
fun testFour() {
val dep = getDependency()
// This doesn't work :(
}
Stack trace:
Caused by: java.lang.NoSuchMethodError: com.example.TestInterfaceImpl: method <init>()V not found
at com.example.TestInterfaceImpl_ClientProxy.<init>(EngineerSummaryServiceImpl_ClientProxy.zig:21)
at com.example.TestInterfaceImpl_Bean.<init>(EngineerSummaryServiceImpl_Bean.zig:118)
at io.quarkus.arc.setup.Default_ComponentsProvider.addBeans2(Default_ComponentsProvider.zig:1140)
at io.quarkus.arc.setup.Default_ComponentsProvider.getComponents(Default_ComponentsProvider.zig:43)
at io.quarkus.arc.impl.ArcContainerImpl.<init>(ArcContainerImpl.java:102)
at io.quarkus.arc.Arc.initialize(Arc.java:20)
... 82 more
Unable to locate CDIProvider
java.lang.IllegalStateException: Unable to locate CDIProvider
at javax.enterprise.inject.spi.CDI.findAllProviders(CDI.java:121)
at javax.enterprise.inject.spi.CDI.getCDIProvider(CDI.java:82)
at javax.enterprise.inject.spi.CDI.current(CDI.java:64)
...
This is also _not_ a problem if you're injecting a class that doesn't implement an interface.
To Reproduce
Here's a reproducer: https://github.com/bnjns/quarkus-bug-7541
Just run
$ ./gradlew clean test
Configuration
N/A
Screenshots
N/A
Environment (please complete the following information):
uname -a or ver: Darwin Beans.local 19.3.0 Darwin Kernel Version 19.3.0: Thu Jan 9 20:58:23 PST 2020; root:xnu-6153.81.5~1/RELEASE_X86_64 x86_64java -version: openjdk version "1.8.0_232"Additional context
Using Kotlin 1.3.61, compiled down to Java 1.8
Using Gradle (6.0.1)
Thanks for reporting!
This one looks like a lot of fun :)
We would definitely need a reproducer for this, so when you get some time, it would be wonderful if you could create one and add it to the description
@geoand I do like to make your lives interesting! And I must admit it took me a while to track it down to the class/interface issue so looks like it might be a bit of a challenge 馃槀
Here's a reproducer, which I've added to the description: https://github.com/bnjns/quarkus-bug-7541
Thanks a lot!
So I took a look at this and you can easily get past the problem by changing getService to:
private fun getService(): ServiceOne = CDI.current().select(ServiceOneImpl::class.java).get()
in TestingUsingImplementationWithHelperMethod.
I am not absolutely sure what the problem is, but my guess is that it has to do with Kotlin's non-nullable types.
Since the workaround is so easy I don't think it warrants any further exploration and I'll go ahead and close this issue. Feel free to reopen if you feel otherwise
Hmm, I see what you mean - making sure you explicitly state the return type as the interface rather than letting Kotlin infer the type as the implementation?
For me, I don't think this workaround does work. My particular use case was possible wanting to test a "helper" method on the implementation that isn't declared on the interface - so you need to inject the implementation and use that as the return type.
I'll reopen since the issue probably goes deeper than I originally posted.
The even weirder thing is that a subsequent ./gradlew test (after the first one) passes... Also the tests seem to pass when run from the IDE.
Since this has a lot of moving parts and there are workarounds, I probably won't spend any time on it unless / until I get some downtime.
In the meantime, anyone else if free to take a stab at it :)
The even weirder thing is that a subsequent ./gradlew test (after the first one) passes... Also the tests seem to pass when run from the IDE.
Yep, we've had this problem so many times 馃槀
I'm happy using a workaround for now, but would be grand if this could get tackled at some point 馃檪
I've found that this issue seems to go even further - if you have a class that uses a combination of dependencies that are classes or interfaces you get the same problem.
So say you have DepA and DepB and DepA is a class, DepB is an interface for which there is an implementation DepBImpl. If you create something that uses these, ConsumerImpl and depend on DepA and DepB you get the same problem, _unless_ you make ConsumerImpl implement an interface (Consumer) and use
CDI.current().select(Consumer::class.java).get()
I'll try to find some time to add this to the reproducer this evening.
Full example that fails:
@ApplicationScoped
class DepA
interface DepB {
}
@ApplicationScoped
class DepBImpl : DepB
@ApplicationScoped
class ConsumerImpl(
private val depA: DepA,
private val depB: DepB
)
private fun getConsumer(): ConsumerImpl = CDI.current().select(ConsumerImpl::class.java).get()
Full example that works:
@ApplicationScoped
class DepA
interface DepB {
}
@ApplicationScoped
class DepBImpl : DepB
interface Consumer {
}
@ApplicationScoped
class ConsumerImpl(
private val depA: DepA,
private val depB: DepB
) : Consumer
private fun getConsumer(): Consumer = CDI.current().select(Consumer::class.java).get()
This gradle/kotlin combo is really "nutritious". I'm not a kotlin expert but I noticed few details. First of all your @ApplicationScoped services do not declare a no-args constructor and so quarkus has to enhance the bytecode to overcome a limitation of CDI client proxies as described in the docs. And so if we see NoSuchMethodError it's very likely a classloading issue - quarkus attempts to work with non-enhanced bytecode. Also Unable to locate CDIProvider indicates that ArcCDIProvider was set to the static field of a different javax.enterprise.inject.spi.CDI class.
Interesting. I guess we really should be using the noarg plugin to do that during build automatically for things like @ApplicationScoped services.
I'll give that a try and see if it makes a difference.
Edit: Turns out it makes no difference :(
I just tested the reproducer out on 1.3.0.CR2 and it seems to work.
Yes, indeed. That confirms my theory of a class loading issue. @bnjns could you try to upgrade to 1.3.0.CR2?
@mkouba Yep, just tested it with 1.3.0CR2 and it works, thanks 馃檪
Thanks for checking, closing
Most helpful comment
Thanks for reporting!
This one looks like a lot of fun :)
We would definitely need a reproducer for this, so when you get some time, it would be wonderful if you could create one and add it to the description