§Action composition
This chapter introduces several ways to define generic action functionality.
§Reminder about actions
Previously, we said that an action is a Java method that returns a play.mvc.Result
value. Actually, Play manages internally actions as functions. An action provided by the Java API is an instance of play.mvc.Action
. Play builds a root action for you that just calls the proper action method. This allows for more complicated action composition.
§Composing actions
Here is the definition of the VerboseAction
:
public class VerboseAction extends play.mvc.Action.Simple {
public CompletionStage<Result> call(Http.Context ctx) {
log.info("Calling action for {}", ctx);
return delegate.call(ctx);
}
}
You can compose the code provided by the action method with another play.mvc.Action
, using the @With
annotation:
@With(VerboseAction.class)
public Result verboseIndex() {
return ok("It works!");
}
At one point you need to delegate to the wrapped action using delegate.call(...)
.
You also mix several actions by using custom action annotations:
@Security.Authenticated
@Cached(key = "index.result")
public Result authenticatedCachedIndex() {
return ok("It works!");
}
Note: Every request must be served by a distinct instance of your
play.mvc.Action
. If a singleton pattern is used, requests will be routed incorrectly during multiple request scenarios. For example, if you are using Spring as a DI container for Play, you need to make sure that your action beans are prototype scoped.Note:
play.mvc.Security.Authenticated
andplay.cache.Cached
annotations and the corresponding predefined Actions are shipped with Play. See the relevant API documentation for more information.
§Defining custom action annotations
You can also mark action composition with your own annotation, which must itself be annotated using @With
:
@With(VerboseAnnotationAction.class)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface VerboseAnnotation {
boolean value() default true;
}
Your Action
definition retrieves the annotation as configuration:
public class VerboseAnnotationAction extends Action<VerboseAnnotation> {
public CompletionStage<Result> call(Http.Context ctx) {
if (configuration.value()) {
log.info("Calling action for {}", ctx);
}
return delegate.call(ctx);
}
}
You can then use your new annotation with an action method:
@VerboseAnnotation(false)
public Result verboseAnnotationIndex() {
return ok("It works!");
}
§Annotating controllers
You can also put any action composition annotation directly on the Controller
class. In this case it will be applied to all action methods defined by this controller.
@Security.Authenticated
public class Admin extends Controller {
...
}
Note: If you want the action composition annotation(s) put on a
Controller
class to be executed before the one(s) put on action methods setplay.http.actionComposition.controllerAnnotationsFirst = true
inapplication.conf
. However, be aware that if you use a third party module in your project it may rely on a certain execution order of its annotations.
§Passing objects from action to controller
You can pass an object from an action to a controller by utilizing the context args map.
public class PassArgAction extends play.mvc.Action.Simple {
public CompletionStage<Result> call(Http.Context ctx) {
ctx.args.put("user", User.findById(1234));
return delegate.call(ctx);
}
}
Then in an action you can get the arg like this:
@With(PassArgAction.class)
public static Result passArgIndex() {
Object user = ctx().args.get("user");
return ok(Json.toJson(user));
}
§Debugging the action composition order
To see in which order the actions of the action composition chain will be executed, please add the following to logback.xml
:
<logger name="play.mvc.Action" level="DEBUG" />
You will now see the whole action composition chain with the according annotations (and their associated method/controller) in the logs:
[debug] play.mvc.Action - ### Start of action order
[debug] play.mvc.Action - 1. ...
[debug] play.mvc.Action - 2. ...
[debug] play.mvc.Action - ### End of action order
§Using Dependency Injection
You can use runtime Dependency Injection or compile-time Dependency Injectiontogether with action composition.
§Runtime Dependency Injection
For example, if you want to define your own result cache solution, first define the annotation:
@With(MyOwnCachedAction.class)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface WithCache {
String key();
}
And then you can define your action with the dependencies injected:
public class MyOwnCachedAction extends Action<WithCache> {
private final AsyncCacheApi cacheApi;
@Inject
public MyOwnCachedAction(AsyncCacheApi cacheApi) {
this.cacheApi = cacheApi;
}
@Override
public CompletionStage<Result> call(Http.Context ctx) {
return cacheApi.getOrElseUpdate(configuration.key(), () -> delegate.call(ctx));
}
}
Note: As stated above, every request must be served by a distinct instance of your
play.mvc.Action
and you must not annotate your action as a@Singleton
.
§Compile-time Dependency Injection
When using compile-time Dependency Injection, you need to manually add your Action
supplier to JavaHandlerComponents
. You do that by overriding method javaHandlerComponents
in BuiltInComponents
:
public class MyComponents extends BuiltInComponentsFromContext
implements NoHttpFiltersComponents, CaffeineCacheComponents {
public MyComponents(ApplicationLoader.Context context) {
super(context);
}
@Override
public Router router() {
return Router.empty();
}
@Override
public MappedJavaHandlerComponents javaHandlerComponents() {
return super.javaHandlerComponents()
// Add action that does not depends on any other component
.addAction(VerboseAction.class, VerboseAction::new)
// Add action that depends on the cache api
.addAction(MyOwnCachedAction.class, () -> new MyOwnCachedAction(defaultCacheApi()));
}
}
Note: As stated above, every request must be served by a distinct instance of your
play.mvc.Action
and that is why you add ajava.util.function.Supplier<Action>
instead of the instance itself. Of course, you can have aSupplier
returning the same instance every time, but this is not encouraged.