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
Java components for compile time injection #7170
Java components for compile time injection #7170
Conversation
|
||
@Override | ||
default Langs langs() { | ||
play.api.Configuration configuration = new play.api.Configuration(configuration()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a private method to avoid repeatedly creating an play.api.Configuration
.
This is ready to be reviewed. There is still some logger configuration aspects that can be improved (see documentation examples), but there is PR #7187 for that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty cool.
It would be nice if there was some way to get Dagger to recognize the methods on the module without @Provides
annotations, like Macwire does, but I don't think that's possible.
|
||
An alternative approach is to use compile time dependency injection. At its simplest, compile time DI can be achieved by manually constructing and wiring dependencies. Other more advanced techniques and tools exist, such as [Dagger](https://google.github.io/dagger/). All of these can be easily implemented on top of constructors and manual wiring, so Play's support for compile time dependency injection is provided by providing public constructors and factory methods as API. | ||
|
||
> **Note**: If you're new to compile-time DI or DI in general, it's worth reading Adam Warski's [guide to DI in Scala](https://di-in-scala.github.io/) that discusses compile-time DI in general. While this is an explanation for Scala developers, it could also give you some insights about the advantages of Compile Time Injection. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to adapt this doc a bit more to the Java audience. The only real compile-time DI framework for Java currently is Dagger, and it's a bit different from what's recommended by Adam Warski, so linking this may confuse people.
Also is the cake pattern well understood by Java devs? Can another term be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. Not happy with this also. Will take a look at Dagger docs and see if they have something we can link here.
/** | ||
* Helper to provide the Play built in components. | ||
*/ | ||
public interface BuiltInComponents extends BaseComponents, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better organization to move these to a play.components
package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I though about that, but decide to keep like this because Scala API is just play.api.BuiltInComponents
and I didn't what to move it.
|
||
@Override | ||
public Injector injector() { | ||
// TODO do we need to register components like the scala version? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this injector is used to build the application, it will still be a problem when we call app.injector.instanceOf[Foo]
for the global state APIs. We could just choose not to have the injector be overridable at all and make this work only without global state, but it's not hard to get it working.
We probably should write a helper method to get the default injector instance that we call from both places.
Also, we need to remember to set/unset the ObjectMapper
using Json.setObjectMapper
. That will definitely still be in the API even if we recommend moving to something non-static for JSON.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The trick point here is a circular dependency between some Java components (see injector()
calls inside Java version of BuiltInComponents
) and the injector itself (components that we need to add to have an injector on par with the Scala version).
To have a common injector initialization method, we would have to completely re-instantiate the Scala version of the components, but this shouldn't happen completely apart from the Java version (per instance, we have to honor httpFilters
implementation) and we are then back to the circular dependencies here.
What I'm thinking is: if we are using dependency injection, we shouldn't touch the global state, right? Apparently this is current not true, for both compile-time and runtime di. For example, the following test:
package play;
import org.junit.Test;
import play.components.BodyParserComponents;
import play.filters.components.HttpFiltersComponents;
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Results;
import play.routing.Router;
import play.routing.RoutingDsl;
import play.test.Helpers;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.Assert.assertThat;
public class BuiltInComponentsFromContextTest {
class TestBuiltInComponentsFromContext extends BuiltInComponentsFromContext implements
HttpFiltersComponents,
BodyParserComponents {
public TestBuiltInComponentsFromContext(ApplicationLoader.Context context) {
super(context);
}
@Override
public Router router() {
return new RoutingDsl(defaultScalaParser(), javaContextComponents())
.GET("/").routeTo(() -> Results.ok("index"))
.build();
}
}
@Test
public void shouldProvideAApplication() {
ApplicationLoader.Context context = ApplicationLoader.create(Environment.simple());
Application application = new TestBuiltInComponentsFromContext(context).application();
Helpers.running(application, () -> {
Http.RequestBuilder request = Helpers.fakeRequest(Helpers.GET, "/");
Result result = Helpers.route(application, request);
assertThat(result.status(), equalTo(Helpers.OK));
});
}
}
It fails because we are still touching the global state:
[error] Test play.BuiltInComponentsFromContextTest.shouldProvideAApplication failed: java.lang.RuntimeException: java.lang.InstantiationException: play.api.http.HttpConfiguration, took 1.404 sec
[error] ...
[error] Caused by: java.lang.InstantiationException: play.api.http.HttpConfiguration
[error] ...
[error] at play.api.mvc.SessionCookieBaker.COOKIE_NAME(Session.scala:75)
[error] at play.api.mvc.SessionCookieBaker.COOKIE_NAME$(Session.scala:75)
[error] at play.api.mvc.Session$.COOKIE_NAME(Session.scala:118)
Long story short: Session.COOKIE_NAME
is global state that shouldn't be touched. Removing it will make the test pass even if are not adding components to the injector.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyway, accessing global state inside injected components is out of the scope here. I will address it in another PR.
This seems like a great idea. The only question I have is how to deal with singletons, i.e. stateful objects that we want exactly one instance of. In scala the solution is to use a lazy val instead of a def. What's the answer for Java users? Should we provide some kind of helper? Are there any of the core components that should be made singleton? If we clearly address the singleton problem this LGTM. |
@gmethvin good question. Since interfaces don't have state, we have two options:
I more inclined to the second option, but still, users can decide to implement the interfaces without using
Maybe WDYT? |
I think it's fine to do it in As far as which components should be singletons, we could take the safe approach and just do it for anything marked Anything in the Basically anything that creates thread pools, is expensive to create, or holds state should be a singleton. There may be others I haven't thought of here. |
The BuiltInComponents will required almost the same from implementations. Most of the methods have a default implementation using the conversions between Java and Scala types.
Also add a convenient method to get an instance of the scala version of HttpErrorHandler.
According to if they are annotated with @singleton or not
@gmethvin take a look again, regarding the singleton behavior. The components that are annotated as singletons, but were not implemented here such as are:
Anyway, this is ready to be reviewed again. |
I think we should use a lazy initialization scheme, similar to what's described here: https://stackoverflow.com/a/29138089 The reason is that prevents us having to re-order the instantiations in the constructor if we change dependencies. (Though this isn't a problem for the end user as long as we get the order correct.) We could expose a private Supplier<PersonService> personService = lazily(() -> new PersonService(configuration()));
public PersonService personService() {
return personService.get();
} |
// The order here matters | ||
this.context = context; | ||
|
||
this._injector = lazy(this::createInjector); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't these be initialized above? Would be more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean at field declaration?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes
this._application = lazy(this::createApplication); | ||
} | ||
|
||
private class LazySupplier<T> implements Supplier<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be a static class somewhere else. Maybe in play.libs.F
? Then we could provide a static factory like LazySupplier.lazy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM on the code/APIs. I think we should look at the documentation and perhaps provide some specific advice regarding Dagger, since in Java it's not exactly nice to wire it manually. But I think we can merge now and revisit that later.
See #7276. |
} | ||
|
||
private Injector createInjector() { | ||
// TODO do we need to register components like the scala version? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcospereira Actually this is going to cause a bug because we use injector()
in the DefaultJavaHandlerComponents
to obtain instances. So we either need to add those things to the injector, or provide another way to provide action and body parser class instances. See https://github.com/playframework/playframework/blob/master/framework/src/play/src/main/scala/play/core/j/JavaAction.scala#L169
Status
This is ready to be reviewed.
play.api.BuiltInComponentsFromContext
play.api.NoHttpFiltersComponents
(maybe this should be done in a serie of smaller pull requests)*Components
for subprojectsPurpose
Java components APIs similar to what we have in Scala for Compile Time Injection.