Skip to content

Toro Cloud Dev Center


Creating HTTP endpoints via Spring controllers

Aside from creating ad hoc REST endpoints and Gloop REST or SOAP APIs, it's also possible to expose services via Groovy-based Spring controllers! Martini comes bundled with Spring which is why creating beans and adding controllers are inherently supported in Martini.

This page will discuss how you can use Spring to create RESTful web services. You should head to this document if you intend to serve web content via Spring MVC.

Example

Below is a simple controller with a service that accepts GET requests at /person/new.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping('person')
class PersonController {

    @RequestMapping(method = RequestMethod.GET, value = 'new')
    Person generate(@RequestParam('first-name') String firstName, @RequestParam('last-name') String lastName) {
        String id = UUID.randomUUID().toString()
        return new Person(id:id, firstName:firstName, lastName:lastName)
    }

}

The response of this endpoint will be a new Person, whose class is defined as:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import com.fasterxml.jackson.annotation.JsonProperty

class Person {

    String id

    @JsonProperty('first-name')
    String firstName

    @JsonProperty('last-name')
    String lastName

}

The response will be represented in a certain format like, but not limited to, JSON1:

1
2
3
4
5
{
  "id": "569e97e9-0d29-43ec-b8d5-c505d3ee6a8y",
  "first-name": "John",
  "last-name": "Doe"
}

To try this out:

  1. Create the Groovy classes Person and PersonController, respectively, and add them to a Martini package's code directory.
  2. Wait for the classes to compile and;
  3. Send a request to the endpoint at /person/new2:
1
2
3
curl -X GET \
  '<http|https>://<host>:<port>/<api-prefix>/person/new?first-name=John&last-name=Doe' \
  -H 'Cache-Control: no-cache'

Breakdown

So, what really happened up there?

In Spring, you can create RESTful web services via annotations. This is precisely what we did in the example and below are the annotations we used and their purposes:

  • @RestController

    According to Spring, this is a convenience annotation that applies both @Controller and @ResponseBody.

    @Controller is used to indicate that the annotated class will serve the role of a controller. Controller classes will be scanned by the dispatcher3 for mapped methods (methods annotated with @RequestMapping).

    The @ResponseBody annotation, on the other hand, is used to indicate that the value returned by the annotated method should be serialized to the response body through an HttpMessageConverter. @RestController applies @ResponseBody to all of the @RestController-annotated class's methods and therefore writes directly to the response body as opposed to resolving a view and then rendering the corresponding HTML template.

  • @RequestMapping

    As stated in its class documentation page, this is an annotation used for mapping web requests onto handler classes or handler methods. In other words, it specifies which HTTP requests would be handled by a controller and its method (using paths). This is why you were able to access the web service at /person/new.

    For HTTP method-specific variants, you may use the following method-level annotations:

  • @RequestParam

    According to Spring's documentation, this annotation is used to indicate that a method parameter must be bound to a web request parameter. This is why we were able to get the values of the first-name and last-name query parameters and port them to the firstName and lastName method parameters (respectively).

@PathVariable

Although unused in the example, another frequently used annotation when creating RESTful web services is @PathVariable. It works like @RequestParam, annotated to method parameters, but is for binding URI template variables (specified in @RequestMapping annotations) to method parameters.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RestController
@RequestMapping('person')
class PersonController {

    @GetMapping('/{id}')
    Person findPerson(@PathVariable String id) {
        // ...
    }

}

With the code above, a request to /person/569e97e9-0d29-43ec-b8d5-c505d3ee6a8y would mean that the id method parameter would be later on assigned the value of "569e97e9-0d29-43ec-b8d5-c505d3ee6a8y".

Request Content-Type detection

When determining the Content-Type of a request, Martini goes beyond simply checking the request's Content-Type header. It iterates through certain steps (described below) in order, to check for a successful match. Once a match is determined, it skips all the other succeeding steps and proceeds to deduce the request's Content-Type according to the specifics of the step it matched.

  1. If the consumes property of a (handler class or method's4) @RequestMapping annotation exists and the request's Content-Type header matches any of the consumes property's values, then the matching value is assumed to be the request's Content-Type.
  2. If the request has a non-null query parameter named dataFormat and it matches any of the valid values (json, xml, and default), then the matching value's equivalent Content-Type is used. If dataFormat is an empty string, then it uses the respective Content-Type of the pre-configured default Content-Type5.
  3. If the request's Content-Type header exists, then this is assumed to be the Content-Type of the request.
  4. If the request's Accept header exists, then this is assumed to be the Content-Type of the request.

If none of the options above matched, then an error is thrown.

Parameter mapping

On top of the variable mapping Spring already does, Martini adds a couple of important changes to the default Spring MVC implementation:

  • InputStream, Reader, byte[], CharSequence, or String-typed method parameters with the name body are assigned the content of the request or a request parameter called body. If neither is present and your code is annotated with XML or JSON, then the value set by the annotation is used; otherwise, an exception is thrown.
  • DataWrapper parameters are populated with parsed XML or JSON data. Martini uses Groovy's JsonSluper or XmlSlurper to parse the request's content and assigns the parsed content to DataWrapper's data property. The data property's data type varies depending on the parsed content but your code doesn't have to worry about that; such is the dynamic nature of Groovy.
  • If a required path variable is missing, Martini will attempt to retrieve it from the request parameter map.
  • String method parameters with the name internalId are used as the service call's6 Tracker document ID. Tracker documents represent recorded service calls. Setting the call's ID is useful if you intend to update an existing Tracker document. If it isn't set, Martini will create a new Tracker document for the service invocation (if it's set to track the request).
  • String method parameters with the name path are assigned the value of the request's URI.
  • String method parameters with the name method are assigned the String representation of the request's HTTP method.
  • MartiniPackage method parameters are set to hold the object representation of the Martini package containing the Groovy controller class.
  • RuleMetadata method parameters are assigned the object which holds the details of the monitor rule that the request matched.
  • A Map method parameter named parameters would be filled in with the following entries:
    • "request" -> contains the request object
    • "response" -> contains the response object
    • "internalId" -> contains the Tracker document ID of the service call
    • "path" -> the request URI
    • "martiniPackage" -> the Martini package which contains the service
    • "method" -> the request HTTP method
    • "body" -> the body of the request

Supported handler method return types

Spring is pretty flexible when it comes to return types; it includes support for the following:

@ResponseBody annotation

As explained above, handler methods should be annotated with @ResponseBody if their return values must be sent as the body of the HTTP response.

For the complete and detailed list, please refer to this document from Spring which enumerates supported method return types.

As said in the linked Spring document, you can always return custom types and like the types above; registered HttpMessageConverters will take care of the conversion work for you (as long as you annotate the method with @ResponseBody). For convenience, Martini has included the APIResponse class which you may return from your RESTful web service handler methods.

Response Content-Type resolution

Similar to when resolving HTTP request Content-Types, Martini also performs a certain ordered series of checks in order to determine the appropriate HTTP response Content-Type. Once a check matches, all the other proceeding checks are ignored.

  1. If the produces property of a (handler class or method's7) @RequestMapping annotation exists and the request's Accept header matches any of the produces property's values, then the matching value is assumed to be the response's Content-Type.
  2. If the request has a non-null query parameter named dataFormat and it matches any of the valid values (json, xml, and default), then the matching value's equivalent Content-Type is used. If dataFormat is an empty string, then it uses the respective Content-Type of the pre-configured default Content-Type4.
  3. If the request's Accept header exists, then this is assumed to be the appropriate Content-Type of the response.

If none of the options above matched, then an error is thrown.


  1. You may choose other formats by creating and registering your own custom HttpMessageConverters and then specifying the produces property of the @RequestMapping annotation. 

  2. You must prepend the path with the root URL where your Martini instance is hosted at (e.g. localhost:8080) and the prefix of your API endpoints, which is set to api by default. 

  3. Because @Controller is an implementation of @Component

  4. If defined, the consumes property of a method's @RequestMapping annotation takes precedence over that of a class's. 

  5. The default format is set via the api.rest.default-content-type instance property

  6. Service calls are made through HTTP requests or Martini endpoints

  7. If defined, the produces property of a method's @RequestMapping annotation takes precedence over that of a class's.