"Hello World!" tutorials

3. Supercalifragilisticexpialidocious version

In this tutorial, we will develop a "Hello World!" application using more advanced Spincast functionalities. We will :

  • Add an extra plugin.
  • Use a custom Request Context type.
  • Add an add-on to this custom Request Context type.
  • Use externalized configurations to change the port the Server will be started on.
  • Create an endpoint that receives a username and returns the HTML of the Github page associated with this username!

As you can see in the Quick version tutorial, it is really easy to start a Spincast application if you simply need the default functionalities. But you can go far beyond that...

Spincast is made from the ground up to be extensible, flexible. All the parts can be swapped or be extended since dependency injection is used everywhere and there are no private methods.

Spincast artifact

First, let's add the org.spincast:spincast-default Maven artifact to our pom.xml (or build.gradle) so we start with the default plugins. It is interesting to know that to have total control, we could also start with the org.spincast:spincast-core artifact instead and pick, one by one, which plugins to use. But, most of the time, you'll start with the default artifact :

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-default</artifactId>
    <version>2.2.0</version>
</dependency>

Adding a plugin

In this application, we're going to use a plugin which is not installed by default : Spincast HTTP Client. This plugin provides an easy way to make HTTP requests.

Adding a plugin is, in general, a two steps process.

1. First, we add its Maven artifact to our pom.xml (or build.gradle) :

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-plugins-http-client</artifactId>
    <version>2.2.0</version>
</dependency>

2. Then, since most plugins need to add or modify some bindings in the Guice context of an application, we have to register them. This is done using the "plugin(...)" method of the Bootstrapper :

public static void main(String[] args) {

    Spincast.configure()
            .plugin(new SpincastHttpClientPlugin())
            //...
            .init(args);
   //...
}

We'll come back to this bootstrapping part as it is not complete like this. For now, let's simply notice how the plugin is registered.

Custom Request Context type

Creating a custom Request Context type is optional but suggested. You can very well develop a complete and production ready Spincast application without one... But it is a powerful feature as it allows you to add functionalities to the Request Context objects that are passed to your Route Handlers when request are received. You can learn more about this in the Request Context section.

Note that if you use the Quick Start application as a template for your application, a custom Request Context type is already provided : you simply have to add add-ons to it, when required.

In this application, we will add a "httpClient()" add-on to our custom Request Context type, so we can easily make HTTP requests from our Route Handlers.

Here's the interface we are going to use for our custom Request Context :

public interface AppRequestContext extends RequestContext<AppRequestContext> {

    /**
     * Add-on to access the HttpClient factory
     */
    public HttpClient httpClient();
    
    //... other add-ons
}

And an implementation for it :

public class AppRequestContextDefault extends RequestContextBase<AppRequestContext>
                                      implements AppRequestContext {

    private final HttpClient httpClient;
    
    @AssistedInject
    public AppRequestContextDefault(@Assisted Object exchange,
                                    RequestContextBaseDeps<AppRequestContext> requestContextBaseDeps,
                                    HttpClient httpClient) {
        super(exchange, requestContextBaseDeps);
        this.httpClient = httpClient;
    }

    @Override
    public HttpClient httpClient() {
        return this.httpClient;
    }
}

Notice that we injected the HttpClient component (9) which is in fact a factory to start new HTTP requests. Our add-on method, "httpClient()" simply returns this factory (16).

You can learn more about the process of extending the Request Context type in the dedicated section of the documentation.

Controller and Route Handlers

Let's now create a controller, some Route Handlers, and use the new add-on we just added :

public class AppController {

    /**
     * Simple "Hello World" response on the "/" Route.
     */
    public void indexPage(AppRequestContext context) {
        context.response().sendPlainText("Hello World!");
    }

    /**
     * Route Handler for the "/github-source/${username}" Route.
     * 
     * We retrieve the HTML source of the GitHub page associated
     * with the specified username, and return it in a Json object.
     */
    public void githubSource(AppRequestContext context) {

        String username = context.request().getPathParam("username");

        String url = "https://github.com/" + username;

        String src = context.httpClient().GET(url).send().getContentAsString();

        JsonObject response = context.json().create();
        response.set("username", username);
        response.set("url", url);
        response.set("source", src);

        context.response().sendJson(response);
    }
}

Explanation :

  • 6 : A Route Handler for the requests hitting the index page. Notice that we receive an instance of our custom AppRequestContext type as a parameter!
  • 7 : Simple use of the "response()" add-on to send plain text.
  • 16 : A second Route Handler for a "/github-source/${username}" Route, where a dynamic parameter is used.
  • 18 : We retrieve the value of the "username" dynamic parameter from the request.
  • 22 : We use the new "httpClient()" add-on we added to our custom Request Context type to retrieve the HTML source of the Github page associated with that username.
  • 24-27 : We create a JsonObject as the response to send.
  • 29 : We send the response as "application/json".

Route definitions

In this tutorial, we won't define the Routes in the App class, but directly in the controller! Let's do this :

public class AppController {

    public void indexPage(AppRequestContext context) { ... }

    public void githubSource(AppRequestContext context) { ... }

    /**
     * Init method : we inject the Router and then add some Routes to it.
     */
    @Inject
    protected void init(Router<AppRequestContext, DefaultWebsocketContext> router) {

        router.GET("/").handle(this::indexPage);
        router.GET("/github-source/${username}").handle(this::githubSource);
    }   
}

As you can see the Router is dynamic, you can inject the Router in any component in order to add Routes to it. Here, our controller simply uses method reference to bind some of its own methods as Route Handlers.

You may also notice that the type of the injected Router is Router<AppRequestContext, DefaultWebsocketContext>, which is kind of ugly. It is so because we use a custom Request Context type, and all components related to routing have to be aware of it. We won't do it in this tutorial, but it's very easy to create a unparameterized version of those routing components so they are prettier and easier to deal with!

Configurations

We are going to change the port the Server is started on by overriding the default SpincastConfig binding and use externalized configurations. To do so, we create a custom class that extends the default SpincastConfigDefault implementation. Then, we use the special getters to find the values to use for our configurations :

public class AppConfig extends SpincastConfigDefault {

    @Inject
    protected AppConfig(SpincastConfigPluginConfig spincastConfigPluginConfig, @TestingMode boolean testingMode) {
        super(spincastConfigPluginConfig, testingMode);
    }

    /**
     * We change the port the Server will be started on.
     */
    @Override
    public int getHttpServerPort() {
        Integer port = getInteger("server.port");
        if (port == null) {
            throw new RuntimeException("The 'port' configuration is required!");
        }
        return port;
    }

    /**
     * It is recommended to *always* override the 
     * getPublicUrlBase() configuration!
     */
    @Override
    public String getPublicUrlBase() {
        return getString("api.baseUrl", super.getPublicUrlBase());
    }
}

In this example, we do not provide a classpath configuration file to provide default values for the externalized configurations, so it is required to provide an explicit one! This is a YAML file named by default "app-config.yaml", and has to be placed next to the .jar of the application before launching it. Here's what it should look like :

server: 
  port: 12345

api:
  baseUrl: http://localhost:12345

Application Guice module

Let's now create a Guice module for our application, and bind our custom components to it :

public class AppModule extends SpincastGuiceModuleBase {

    @Override
    protected void configure() {

        bind(AppController.class).asEagerSingleton();

        bind(SpincastConfig.class).to(AppConfig.class).in(Scopes.SINGLETON);

        // ... other bindings
    }
}

Explanation :

  • 6 : We bind our controller using asEagerSingleton() because it contains an init method in which Routes are defined and this must occur right when the application starts.
  • 8 : We change the default SpincastConfig binding so our custom implementation class is used instead of the default one.
  • 10 : Notice that, in this application, we don't have many components! But, in a real life application, this is where you are also going to bind your services, your repositories, your utilities.

The App class

The only missing piece is the App class itself, where the main(...) method and the Bootstrapper are defined :

public class App {

    public static void main(String[] args) {
    
        Spincast.configure()
                .module(new AppModule())
                .plugin(new SpincastHttpClientPlugin())
                .requestContextImplementationClass(AppRequestContextDefault.class)
                .init(args);
    }

    @Inject
    protected void init(Server server) {
        server.start();
    }
}

Explanation :

  • 3 : A standard main(...) method is the entry point of our application.
  • 5 : Spincast.configure() starts the bootstrapper, so we can configure and initialize our application.
  • 6 : We register our application module.
  • 7 : We register the Spincast HTTP Client plugin.
  • 8 : Since we use a custom Request Context type, we need to let Spincast know about it.
  • 9 : The init() method will create the Guice context, will bind the current App class in the context and will load it. We are also using it to bind the arguments passed to the main(...) method... Doing so, we can then inject them using the @MainArgs String[] key.
  • 12-13 : This init method is going to be called automatically when the Guice context is ready. We inject in it the Server. We don't need to inject the Router in this example since the Routes are defined in the controller.
  • 14 : We start the server.

Try it!

Download this application, and run it by yourself :

  • Download spincast-demos-supercalifragilisticexpialidocious.zip.
  • Unzip the file.
  • Enter the root directory using a terminal.
  • Compile the application using Maven :
    mvn clean package
  • Make sure you copy the configuration file "app-config.yaml", provided at the root of the sources, next to the generated .jar file, inside the "target" folder... Otherwise you are going to have an exception when starting the application!
  • Launch the application :
    java -jar target/spincast-demos-supercalifragilisticexpialidocious-2.2.0.jar

The application is then accessible at http://localhost:12345

Don't forget to try the "github-source" Route (you can change the "username" dynamic parameter) : http://localhost:12345/github-source/spincast.

Too complex?

Try the Quick "Hello World!" demo, for an easier version!