refcodes-rest: RESTful services using lambdas

README

The REFCODES.ORG codes represent a group of artifacts consolidating parts of my work in the past years. Several topics are covered which I consider useful for you, programmers, developers and software engineers.

What is this repository for?

With this artifact you easily create your serverless RESTful services and REST driven Java clients the Bare-Metal way. Ready-to-use JDK client and server implementations are configured through explicit builders. Lambda expressions bound to a REST driven Java endpoint equate to your RESTful service, as easy as that.

Quick start archetype

For a jump start into developing Java driven command line tools, I created some fully pre-configured Maven Archetypes available on Maven Central. Those Maven Archetypes already provide means to directly create native executables, bundles as well as launchers and support out of the box command line argument parsing as well as out of the box property file handling.

Use the refcodes-archetype-alt-rest archetype to create a bare metal REST driven Java application within just one source code file:

Please adjust my.corp with your actual Group-ID and myapp with your actual Artifact-ID:

1
2
3
4
5
6
7
mvn archetype:generate \
  -DarchetypeGroupId=org.refcodes \
  -DarchetypeArtifactId=refcodes-archetype-alt-rest \
  -DarchetypeVersion=4.9.9 \
  -DgroupId=my.corp \
  -DartifactId=myapp \
  -Dversion=0.0.1

Using the defaults, this will generate a RESTful server and client application by harnessing the refcodes-rest toolkit.

How do I get set up?

To get up and running, include the following dependency (without the three dots “…”) in your pom.xml:

1
2
3
4
5
6
7
8
9
<dependencies>
	...
	<dependency>
		<artifactId>refcodes-rest</artifactId>
		<groupId>org.refcodes</groupId>
		<version>4.9.9</version>
	</dependency>
	...
</dependencies>

The artifact is available from Maven Central. The source code is hosted at Bitbucket, and the API documentation is available at javadoc.io.

Alternative HTTP server providers

The JdkHttpRestServer obtains its underlying HttpServer through the official JDK HttpServerProvider SPI. The JDK implementation is used by default and is part of the jdk.httpserver module, so there is no Maven dependency to exclude.

For the lightweight Robaho HTTP server, add the following runtime dependency to the application:

1
2
3
4
5
6
<dependency>
	<groupId>io.github.robaho</groupId>
	<artifactId>httpserver</artifactId>
	<version>${io.github.robaho.httpserver.version}</version>
	<scope>runtime</scope>
</dependency>

Alternatively, the Jetty HTTP SPI provides a Jetty-backed implementation of the same JDK API:

1
2
3
4
5
6
<dependency>
	<groupId>org.eclipse.jetty</groupId>
	<artifactId>jetty-http-spi</artifactId>
	<version>${org.eclipse.jetty.version}</version>
	<scope>runtime</scope>
</dependency>

With exactly one external provider and an intact service descriptor, the JDK discovers the implementation automatically. The provider can also be selected explicitly before the first HttpServer is created:

1
-Dcom.sun.net.httpserver.HttpServerProvider=robaho.net.httpserver.DefaultHttpServerProvider

or:

1
-Dcom.sun.net.httpserver.HttpServerProvider=org.eclipse.jetty.http.spi.JettyHttpServerProvider

The selection is JVM-wide and cached on first use. Shaded executable JARs must preserve and merge META-INF/services entries, for example with Maven Shade’s ServicesResourceTransformer; using the explicit system property is another deterministic option. Only one external provider should be included unless the application selects one explicitly.

RESTful server

  1. Instantiate the RESTful server
  2. Register your lambda expressions
  3. Start the RESTful server

Within three simple steps you implement your lightweight Java based RESTful server. Below, you see the three steps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	... 
	public static void main( String[] args ) {
		// STEP 1: Build and configure the server:
		HttpRestServer theServer = JdkHttpRestServer.builder().withPort( 8080 ).build();
		// STEP 2: Register your lambda expressions:
		theServer.onGet( "/say/${name}=*", ( aRequest, aResponse ) -> {
			String theName = aRequest.getWildcardReplacement( "name" );
			aResponse.getHeaderFields().withContentType( MediaType.APPLICATION_JSON ).withAddCookie( "greeting", "Hello " + theName + "!" );
			aResponse.setResponse( "Hello " + theName + "!" );
		} ).open();
		// STEP 3: Open the server to listen on port 8080:
		theServer.open();
	}
	...

The TinyRestfulServer demo application uses the HttpRestServer for setting up a RESTful service including command line arguments parsing.

REST client

  1. Instantiate the REST client
  2. Register your lambda expression for the response
  3. Fire the client’s REST request

Again, within three simple steps you implement your lightweight Java based REST client. Below, you see the three steps:

1
2
3
4
5
6
7
8
9
10
11
	... 
	public static void main( String[] args ) {
		// STEP 1: Build and configure the client:
		HttpRestClient theClient = JdkHttpRestClient.builder().build();
		// STEP 2: Define our request, including the response consumer:
		theClient.onGet( "http://mydomain:8080/say", ( aResponse ) -> {
			... = aResponse.getResponse( SomeType.class );
		} ).open();
		// STEP 3: Process the request addressed to port 8080 of domain "mydomain"
	}
	...

Snippets of interest

Below find some code snippets which demonstrate the various aspects of using the refcodes-rest artifact (and , if applicable, its offsprings). See also the example source codes of this artifact for further information on the usage of this artifact.

How do I get started with the RESTful server?

Above you saw how to set up your own RESTful service with the explicit JdkHttpRestServer builder. Each builder creates an independent server, so one application may run multiple HttpRestServer instances on different ports.

Let’s do it the Bare-Metal way, which is not very complicated either and which lets us instantiate as many HttpRestServer instances as we want:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
public static void main( String[] args ) {
	// STEP 1: We instantiate our HttpRestServer:
	HttpRestServer theRestServer = JdkHttpRestServer.builder().withPort( 8080 ).build();
	// STEP 2: We register our lambda expression:
	theRestServer.onGet( "/say/${name}=*", ( aRequest, aResponse ) -> {
		String theName = aRequest.getWildcardReplacement( "name" );
		aResponse.getHeaderFields().withContentType( MediaType.APPLICATION_JSON ).withAddCookie( "greeting", "Hello " + theName + "!" );
		aResponse.setResponse( "Hello " + theName + "!" );
	} ).open();
	// STEP 3: We open the HttpRestServer instance on port 8080:
	theRestServer.open();
}
...

The Locator-Pattern

Did you notice the Locator-Pattern "/say/${name}=*" above when registering your lambda? Subscribing your lambda expressions for incoming REST requests on specific locators, you may use a common wildcard syntax to define the lambda’s Locator-Pattern:

  • A single asterisk (*) matches zero or more characters within a locator name.
  • A double asterisk (**) matches zero or more characters across directory levels.
  • A question mark (?) matches exactly one character within a locator name.

The single asterisk (*), the double asterisk (**) and the question mark (?) we refer to as wildcard: You get an array with all the substitutes of the wildcards using the RestRequestEvent class’ getWildcardReplacements() method.

You may name a wildcard by prefixing it with “${someWildcardName}=”. For example a named wildcard may look as follows: “${arg1}=*” or “${arg2}=**” or “${arg3}=?” or as of the example above "/say/${name}=*". When your lambda is being invoked, you can retrieve the wildcard substitution by the name of the wildcard which has been substituted (by parts of the incoming locator). You can get the text substituting a named wildcard using the RestRequestEvent class’ getWildcardReplacement(String) method.

As of refcodes-rest version 1.1.3 a placeholder “${arg1}” with no wildcard assignment “=” is equivalent to “${arg1}=*”. In the example above you could have used "/say/${name}" instead of "/say/${name}=*"!

Error handling

In case an Exception gets thrown while your lambda is processing an HTTP-Response due to an incoming HTTP-Request, you may either handle it manually in your lambda expression or you may use a predefined behavior or you may even register a custom global Exception handler. The predefined behavior is configured as follows:

1
2
3
...
HttpRestServer theRestServer = JdkHttpRestServer.builder().withPort( 8080 ).build().withHttpExceptionHandling( HttpExceptionHandling.REPLACE );
...

The behavior is specified by the enumeration HttpExceptionHandling. You currently have the following options on how an exceptional situation is handled:

  • HttpExceptionHandling.REPLACE: Create a new HTTP-Response with the according HTTP-Status-Code and the error description fields.
  • HttpExceptionHandling.UPDATE: Keep the so far processed HTTP-Response as is except the HTTP-Status-Code and also update all error description fields. Useful when you want to preserve already set fields.
  • HttpExceptionHandling.MERGE: Keep the so far processed HTTP-Response as is except update the actual HTTP-Status-Code, add only non-existing fields if possible with the error description fields. Useful when you manually prepared the HTTP-Response yourself.
  • HttpExceptionHandling.KEEP: Do not modify the so far processed HTTP-Response except update the actual HTTP-Status-Code. Useful when you manually prepared the HTTP-Response yourself.
  • HttpExceptionHandling.EMPTY: Create a new HTTP-Response with the according HTTP-Status-Code and and an empty body.

In case the Media-Type used is Application/JSON, then the error description fields generated by the predefined exception handler looks as follows:

1
2
3
4
5
6
7
8
9
{
	"status": {
		"exception": "org.refcodes.web.NotFoundException",
		"code": "404",
		"alias": "NOT FOUND",
		"message": "There is none endpoint for handling resource locator </repl/sessionsx> with HTTP-Method <GET>.",
		"timestamp": "1525436619219"
	}
}

This custom global exception handler is represented by an instance of the functional interface HttpExceptionHandler being registered to your HttpRestServer. You may register it using the lambda notation:

1
2
3
4
5
6
7
...
HttpRestServer theRestServer = JdkHttpRestServer.builder().withPort( 8080 ).build().withHttpExceptionHandler( ( request, response, exception, statusCode ) -> {
	// ...
	response.setResponse( new HttpBodyTable().withPut( "message", exception.getMessage() ) );
	// ...
} );
...

Using HTTP-Request and HTTP-Response interceptors

The HttpServerInterceptor and its parents PreHttpServerInterceptor as well as PostHttpServerInterceptor are explained by the example of the CorrelationServerInterceptor:

1
2
3
4
...
HttpRestServer theRestServer = JdkHttpRestServer.builder().withPort( 8080 ).build();
theRestServer.addHttpInterceptor( new CorrelationServerInterceptor() );
...

The CorrelationServerInterceptor is an implementation of the HttpServerInterceptor interface and defines two methods, the preIntercept method as well as the postIntercept method: The former being invoked before the HTTP-Request is handed over to your business logic and the latter one being invoked after your business logic has finished with an HTTP-Response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CorrelationServerInterceptor implements HttpServerInterceptor {

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void preIntercept( HttpServerRequest aRequest, HttpServerResponse aResponse ) {
		String theRequestId = aRequest.getHeaderFields().getRequestId();
		if ( theRequestId != null && theRequestId.length() != 0 ) {
			Correlation.REQUEST.setId( theRequestId );
		}
		else {
			theRequestId = Correlation.REQUEST.pullId();
		}
		aResponse.getHeaderFields().putRequestId( theRequestId );
		String theSessionId = aRequest.getHeaderFields().getSessionId();
		if ( theSessionId != null && theSessionId.length() != 0 ) {
			Correlation.SESSION.setId( theSessionId );
		}
		else {
			theSessionId = Correlation.SESSION.pullId();
		}
		aResponse.getHeaderFields().putSessionId( theSessionId );
	}
	
	// ...

The above code intercepts incoming HTTP-Requests with means to, amongst others, prepare the according HTTP-Responses. Usually only the header fields of an HTTP-Response are prepared as any other portions of the HTTP-Response should solely be managed by your business logic. The below code intercepts with the outgoing HTTP-Responses, while still having access by the according HTTP-Requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	// ...

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void postIntercept( HttpServerRequest aRequest, HttpServerResponse aResponse ) {
		String theRequestId = aResponse.getHeaderFields().getRequestId();
		if ( theRequestId != null && theRequestId.length() != 0 ) {
			Correlation.REQUEST.setId( theRequestId );
		}
		else {
			theRequestId = aRequest.getHeaderFields().getRequestId();
			if ( theRequestId == null || theRequestId.length() == 0 ) {
				theRequestId = Correlation.REQUEST.pullId();
			}
			aResponse.getHeaderFields().putRequestId( theRequestId );
		}
		String theSessionId = aResponse.getHeaderFields().getSessionId();
		if ( theSessionId != null && theSessionId.length() != 0 ) {
			Correlation.SESSION.setId( theSessionId );
		}
		else {
			theSessionId = aRequest.getHeaderFields().getSessionId();
			if ( theSessionId == null || theSessionId.length() == 0 ) {
				theSessionId = Correlation.SESSION.pullId();
			}
			aResponse.getHeaderFields().putSessionId( theSessionId );
		}
	}
}

Correlation-IDs are used to identify HTTP-Requests and HTTP-Responses passed between your zoo of microservices and correlate them.

In the example above, the Session-TID as well as the Request-TID are set according to the header fields’s settings and passed on the HTTP-Response.

You may also just add the pre-processing or the post-processing part of an HttpServerInterceptor by using its parents PreHttpServerInterceptor as well as PostHttpServerInterceptor:

1
2
3
4
5
...
HttpRestServer theRestServer = JdkHttpRestServer.builder().withPort( 8080 ).build();
theRestServer.addPreHttpInterceptor( ( req, res ) -> { System.out.println( "HTTP-Request: Session-ID = " + req.getHeaderFields().getSessionId() + ", Request-ID = " + req.getHeaderFields().getRequestId() ); } );
theRestServer.addPostHttpInterceptor( ( req, res ) -> { System.out.println( "HTTP-Response: Session-ID = " + res.getHeaderFields().getSessionId() + ", Request-ID = " + res.getHeaderFields().getRequestId() ); } );
...

How do I get started with the REST client?

Above you saw how to set up your own REST client with the explicit JdkHttpRestClient builder. Each builder creates an independent HttpRestClient, optionally with its own base URL, transport settings and interceptors.

Let’s do it the Bare-Metal way, which is not very complicated either and which lets us instantiate as many HttpRestClient instances as we want:

1
2
3
4
5
6
7
8
9
10
11
...
public static void main( String[] args ) {
	// STEP 1: We instantiate our HttpRestClient:
	HttpRestClient theRestClient = JdkHttpRestClient.builder().build();
	// STEP 2: We register our lambda expression:
	theRestClient.onPost( "http://mydomain:8080/say", ..., ( aResponse ) -> {
		String theResponse = aResponse.getResponse( String.class );
	} ).open();
	// STEP 3: We opened the caller so it fires the request to port 8080 of domain "mydomain"
}
...

Using HTTP-Request and HTTP-Response interceptors

The HttpClientInterceptor and its parents PreHttpClientInterceptor as well as PostHttpClientInterceptor are explained by the example of the CorrelationClientInterceptor:

1
2
3
4
...
HttpRestClient theRestClient = JdkHttpRestClient.builder().build();
theRestClient.addHttpInterceptor( new CorrelationClientInterceptor() );
...

The CorrelationClientInterceptor is an implementation of the HttpClientInterceptor interface and defines two methods, the preIntercept method as well as the postIntercept method: The former being invoked before the HTTP-Request is handed over to your business logic and the latter one being invoked after your business logic has finished with an HTTP-Response:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CorrelationClientInterceptor implements HttpClientInterceptor {

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void preIntercept( HttpClientRequest aRequest, HttpClientResponse aResponse ) {
		String theRequestId = aRequest.getHeaderFields().getRequestId();
		if ( theRequestId != null && theRequestId.length() != 0 ) {
			Correlation.REQUEST.setId( theRequestId );
		}
		else {
			theRequestId = Correlation.REQUEST.pullId();
		}
		aResponse.getHeaderFields().putRequestId( theRequestId );
		String theSessionId = aRequest.getHeaderFields().getSessionId();
		if ( theSessionId != null && theSessionId.length() != 0 ) {
			Correlation.SESSION.setId( theSessionId );
		}
		else {
			theSessionId = Correlation.SESSION.pullId();
		}
		aResponse.getHeaderFields().putSessionId( theSessionId );
	}
	
	// ...

The above code intercepts incoming HTTP-Requests with means to, amongst others, prepare the according HTTP-Responses. Usually only the header fields of an HTTP-Response are prepared as any other portions of the HTTP-Response should solely be managed by your business logic. The below code intercepts with the outgoing HTTP-Responses, while still having access by the according HTTP-Requests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
	// ...

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void postIntercept( HttpClientRequest aRequest, HttpClientResponse aResponse ) {
		String theRequestId = aResponse.getHeaderFields().getRequestId();
		if ( theRequestId != null && theRequestId.length() != 0 ) {
			Correlation.REQUEST.setId( theRequestId );
		}
		else {
			theRequestId = aRequest.getHeaderFields().getRequestId();
			if ( theRequestId == null || theRequestId.length() == 0 ) {
				theRequestId = Correlation.REQUEST.pullId();
			}
			aResponse.getHeaderFields().putRequestId( theRequestId );
		}
		String theSessionId = aResponse.getHeaderFields().getSessionId();
		if ( theSessionId != null && theSessionId.length() != 0 ) {
			Correlation.SESSION.setId( theSessionId );
		}
		else {
			theSessionId = aRequest.getHeaderFields().getSessionId();
			if ( theSessionId == null || theSessionId.length() == 0 ) {
				theSessionId = Correlation.SESSION.pullId();
			}
			aResponse.getHeaderFields().putSessionId( theSessionId );
		}
	}
}

Correlation-IDs are used to identify HTTP-Requests and HTTP-Responses passed between your zoo of microservices and correlate them.

In the example above, the Session-TID as well as the Request-TID are set according to the header fields’s settings and passed on the HTTP-Response.

You may also just add the pre-processing or the post-processing part of an HttpClientInterceptor by using its parents PreHttpClientInterceptor as well as PostHttpClientInterceptor:

1
2
3
4
5
...
HttpRestClient theRestClient = JdkHttpRestClient.builder().build();
theRestClient.addPreHttpInterceptor( ( req, res ) -> { System.out.println( "HTTP-Request: Session-ID = " + req.getHeaderFields().getSessionId() + ", Request-ID = " + req.getHeaderFields().getRequestId() ); } );
theRestClient.addPostHttpInterceptor( ( req, res ) -> { System.out.println( "HTTP-Response: Session-ID = " + res.getHeaderFields().getSessionId() + ", Request-ID = " + res.getHeaderFields().getRequestId() ); } );
...

Bits and pieces

The RESTful server’s bits and pieces

The REST client’s bits and pieces

Examples

Please refer to the example source code for more examples on the usage of this artifact.

See also the blog post Bare-Metal REST with just a few lines of codes!

Eureka support

The refcodes-rest-ext-eureka artifact implements Eureka REST operations and provides sidecars for registering RESTful services with Eureka as well as clients for discovering them. Discovery supports NONE, RANDOM, ROUND_ROBIN and RANDOM_STICKY; the CUSTOM strategy requires an application-provided implementation.

See the refcodes-rest-ext-eureka artifact’s example source code on how to enable your RESTful services to use Eureka service discovery.

By default the Eureka client uses the MyOwn data-center descriptor without consulting Amazon metadata. Select the Amazon data-center type explicitly to use the AWS IMDSv2 metadata provider. An unknown service alias is returned unchanged so it may still be resolved through DNS.

Useful information may be puzzled together from resources such as Eureka REST operations, JSON format to register service with Eureka or the eureka-client.properties file.

Contribution guidelines

  • Report issues
  • Finding bugs
  • Helping fix bugs
  • Making code and documentation better
  • Enhancing the code

Who do I talk to?

Licensing Philosophy

This project follows a dual-licensing model designed to balance openness, pragmatism and fair attribution.

You may choose between the LGPL v3.0 or later and the Apache License v2.0 when using this software.

The intention behind this model is simple:

  • Enable use in both open-source and proprietary projects
  • Keep the codebase approachable and reusable
  • Ensure that improvements to the library itself remain available to the community
  • Preserve clear attribution to the original author and the ecosystem

Under the LGPL v3.0+, you are free to use this library in any application. If you modify the library itself, those modifications must be made available under the same license and must retain proper attribution.

Alternatively, the Apache License v2.0 allows broad use, modification and distribution, including commercial usage, provided that copyright notices and the accompanying NOTICE file are preserved.

This dual-licensing approach intentionally avoids artificial barriers while discouraging closed, uncredited forks of the core library. Contributions, improvements and refinements are encouraged to flow back into the project, benefiting both the community and downstream users.

For licensing questions, alternative licensing arrangements or commercial inquiries, please contact the copyright holder.

Latest updates

2023-04-29

Added client-side interceptors as well as server-side interceptors support.