In this tutorial, we will develop a "Hello World!" application using more advanced Spincast functionalities. We will :
Request Context
type.
add-on
to this
custom Request Context
type.
externalized configurations
to change the port the Server will
be started on.
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.
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>
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.
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.
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 :
Route Handler
for
the requests hitting the index page. Notice that we receive an
instance of our custom AppRequestContext
type as a parameter!
"response()" add-on
to send plain text.
Route Handler
for
a "/github-source/${username}"
Route, where a dynamic parameter
is used.
"username"
dynamic
parameter from the request.
"httpClient()" add-on
we added to
our custom Request Context
type to retrieve the HTML source of the Github page associated
with that username.
"application/json"
.
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!
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
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 :
Routes
are
defined and this must occur right when the application starts.
SpincastConfig
binding so our custom
implementation class is used instead of the default one.
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 :
main(...)
method is the entry point of our application.
Spincast.configure()
starts the bootstrapper, so we can configure and initialize our application.
Spincast HTTP Client
plugin.
Request Context
type,
we need to let Spincast know about it.
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.
Router
in this example since the Routes
are defined in the
controller.
Download this application, and run it by yourself :
mvn clean package
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!
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.
Try the Quick "Hello World!" demo, for an easier version!