Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Play Framework 2.5.1 routing and dependency injection (for Java)

I have this in my "routes" file:

POST        /accounts/        controllers.AccountsController.createOneAccount

And in my AccoutsController.java:

package controllers;

import com.google.inject.Inject;
import play.Application;
import play.mvc.Controller;
import play.mvc.Result;
import services.AccountService;
import java.io.IOException;

public class AccountsController extends Controller {
    @Inject
    private Application application;
    final String host = application.configuration().getString("db.default.host");
    final int port = application.configuration().getInt("db.default.port");
    final String dbName = application.configuration().getString("db.default.dbname");

    @Inject
    private AccountService accountService;
    public Result createOneAccount() throws IOException {
        return accountService.createOneAccount(request().body().asJson());
    }
}

This code is compiling fine, but in runtime I got error like this:

ProvisionException: Unable to provision, see the following errors: 1) Error injecting constructor, java.lang.NullPointerException at controllers.AccountsController.(AccountsController.java:11)
while locating controllers.AccountsController for parameter 1 at router.Routes.(Routes.scala:28) while locating router.Routes while locating play.api.inject.RoutesProvider while locating play.api.routing.Router for parameter 0 at play.api.http.JavaCompatibleHttpRequestHandler.(HttpRequestHandler.scala:200) while locating play.api.http.JavaCompatibleHttpRequestHandler while locating play.api.http.HttpRequestHandler for parameter 4 at play.api.DefaultApplication.(Application.scala:221) at play.api.DefaultApplication.class(Application.scala:221) while locating play.api.DefaultApplication while locating play.api.Application 1 error

I can resolve this by adding @ to routes file:

POST        /accounts/        @controllers.AccountsController.createOneAccount

but I am not sure about why I need to do this, and how to avoid the '@'. Please give some suggestions.

like image 928
Roger Chien Avatar asked Apr 14 '16 21:04

Roger Chien


1 Answers

First, see this answer to understand the difference between using or not @ in your routes file:

https://stackoverflow.com/a/34867199/4600

Then, as stated by Play 2.5.x migration docs:

Routes are now generated using the dependency injection aware InjectedRoutesGenerator, rather than the previous StaticRoutesGenerator which assumed controllers were singleton objects.

So, starting at Play 2.5.0, controllers use dependency injection by default and you don't need @ to make them use dependency injection.


Now lets see what is happening in your case. First of all, let me say that constructor injection is the preferred way of injecting dependencies. Guice even recommends (as a best practice) to combine final fields with constructor injection to minimize mutability. Guice docs also recommends that you try to inject only direct dependencies. In your case, you are using application to access a configuration. Why not inject the configuration object instead? This will make your dependencies more clear (which will make, per instance, testing easier).

So, following this recommendations, your code would be rewritten to:

package controllers;

import com.google.inject.Inject;
import play.Configuration;
import play.mvc.Controller;
import play.mvc.Result;
import services.AccountService;
import java.io.IOException;

public class AccountsController extends Controller {

    private final Configuration configuration;
    private final AccountService accountService;

    private final String host;
    private final int port;
    private final String dbName;

    @Inject
    public AccountsController(Configuration configuration, AccountService accountService) {
        this.configuration = configuration;
        this.accountService = accountService;
        // initialize config variables
        this.host = configuration.getString("db.default.host");
        this.port = configuration.getInt("db.default.port");
        this.dbName = configuration.getString("db.default.dbname");
    }

    public Result createOneAccount() throws IOException {
        return accountService.createOneAccount(request().body().asJson());
    }
}

But why the field injection was breaking?

We first need to understand object initialization. According to Java specs:

Just before a reference to the newly created object is returned as the result, the indicated constructor is processed to initialize the new object using the following procedure:

  1. Assign the arguments for the constructor to newly created parameter variables for this constructor invocation.

  2. If this constructor begins with an explicit constructor invocation (§8.8.7.1) of another constructor in the same class (using this), then evaluate the arguments and process that constructor invocation recursively using these same five steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason; otherwise, continue with step 5.

  3. This constructor does not begin with an explicit constructor invocation of another constructor in the same class (using this). If this constructor is for a class other than Object, then this constructor will begin with an explicit or implicit invocation of a superclass constructor (using super). Evaluate the arguments and process that superclass constructor invocation recursively using these same five steps. If that constructor invocation completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, continue with step 4.

  4. Execute the instance initializers and instance variable initializers for this class, assigning the values of instance variable initializers to the corresponding instance variables, in the left-to-right order in which they appear textually in the source code for the class. If execution of any of these initializers results in an exception, then no further initializers are processed and this procedure completes abruptly with that same exception. Otherwise, continue with step 5.

  5. Execute the rest of the body of this constructor. If that execution completes abruptly, then this procedure completes abruptly for the same reason. Otherwise, this procedure completes normally.

Special attention to step 4 which explains that your variables are initialized during the object initialization.

Why is this important? Because Guice first create objects (and then all the steps above will happen) and later performs the injection binds (see Guice Bootstrap and Guice InjectionPoints for more details). So, your fields are requiring, at object initialization, variables (application) that aren't injected yet resulting in a NullPointerException.

like image 187
marcospereira Avatar answered Oct 02 '22 07:10

marcospereira