JIT bindings for abstract classes via providing abstract methods
- Dominant language
- Java
- Stars
- 12.7k
- Forks
- 1.7k
- Avg merge
- 11m
- Merged PRs (30d)
- 2
Description
Setter injection and `InjectionListener` are called after instance constructor, so there's no way to use the injected services in the constructor.
## Use case
I want to inject `IdGenerator` service, however, I do not want to clutter the constructor, and I need the service to be available in the constructor invocation:
```kotlin
abstract class BaseListValueTypeDefinition @Inject constructor(
private val idGenerator: IdGenerator
)
@Singleton
class YesNo @Inject constructor(idGenerator: IdGenerator): BaseListValueTypeDefinition(idGenerator) {
val yes by listValue() // <-- this is handled by Kotlin's delegated properties
val no by listValue()
}
```
Kotlin initializes delegated properties in the constructor, so I need `IdGenerator` during constructor invocation.
The above works, however, `@Inject constructor(idGenerator: IdGenerator)` is a pure noise for the consumer since they never access `idGenerator` directly. They use `listValue()` API, so the preferred way would be
```kotlin
abstract class BaseListValueTypeDefinition {
@get:Inject
abstract protected val idGenerator: IdGenerator // assume that Guice would implement the method by providing the dependency
}
@Singleton
// abstract is needed since we want to ask Guice to implement "IdGenerator getIdGenerator()" method
abstract class YesNo: BaseListValueTypeDefinition {
val yes by listValue()
val no by listValue()
}
```
As a workaround, I use non-abstract classes (to make class eligible for JIT injection), I use dummy implementation like `throw new UnsupportedOperationException();`, and then I use `MethodInterceptor` to actually return the "injected" service without running `MethodInvocation`.
Just in case, here's the workaround:
```kotlin
open class Entity {
@get:Inject
open val name: String
get() = TODO() // default implementation throws, however, it is intercepted and implemented by Guice
val description = "descr<$name>" // this calls `getName` in the Entity instance constructor
}
@Guice(modules = [InterceptorModule::class])
class InterceptTest @Inject constructor(val test: Entity) { // <-- inject `Entity`
@Test
fun hello() {
println("name: ${test.name}, description: ${test.description}")
}
}
class InterceptorModule: AbstractModule() {
override fun configure() {
bindInterceptor(
Matchers.any(),
Matchers.annotatedWith(Inject::class.java), // Ideally I would like to implement only non-implemented methods
MethodInterceptor {
"intercepted" // this would be the relevant Provider.get()
}
)
}
}
// prints: "name: intercepted, description: descr"
```
## Suggestion
Currently, JIT binding is not available for `abstract class` (the error is https://github.com/google/guice/wiki/MISSING_IMPLEMENTATION)
It would be nice if Guice could "implement" certain abstract methods by providing the relevant dependencies.
Then certain cases would be simpler/easier to understand.
For instance, if `class CustomerOrder extends Order`, then adding a new injectable dependency to `Order` would require adding it to the constructor of `Order`. Then, the declaration of `CustomerOrder` constructor would be cluttered by the services which `CustomerOrder` does not use on its own.
What if Guice supported implementing abstract method so users could write:
```java
abstract class Order {
@Inject
abstract IdGenerator getIdGenerator();
}
```
```java
abstract class CustomerOrder extends Order {
@Inject // note: `IdGenerator` is not mentioned in the constructor
CustomerOrder(@Named("customerName") String name) {
// ...
// getIdGenerator() can be used even in constructor since it is implemented by Guice
}
}
```
Pros:
* Less clutter in the constructor arguments: only explicitly needed services are passed in constructor arguments
* Injected services can be used in the constructor (e.g. `CustomerOrder` can use `IdGenerator` which is not possible when `setter` injection is used)
Cons:
* Class generation at runtime
* Direct instantiation (without Guice) might become slightly harder in certain cases, however, instantiation via anonymous subclass would still work, and the compiler would highlight the list of methods to be implemented "to provide dependencies"
* Figuring out "non-implemented" methods might be non-trivial when "non-implemented" methods come from interfaces. Of course, the computation is only once per class, however, it would still harder than a mere `method.isAbstract()`
Contributor guide
Assessment
This issue has not been assessed yet.