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 |
|
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 |
|
The response will be represented in a certain format like, but not limited to, JSON1:
1 2 3 4 5 |
|
To try this out:
- Create the Groovy classes
Person
andPersonController
, respectively, and add them to a Martini package'scode
directory. - Wait for the classes to compile and;
- Send a request to the endpoint at
/person/new
2:
1 2 3 |
|
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:
-
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. -
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:
-
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
andlast-name
query parameters and port them to thefirstName
andlastName
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 |
|
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.
- If the
consumes
property of a (handler class or method's4)@RequestMapping
annotation exists and the request'sContent-Type
header matches any of theconsumes
property's values, then the matching value is assumed to be the request'sContent-Type
. - If the request has a non-
null
query parameter nameddataFormat
and it matches any of the valid values (json
,xml
, anddefault
), then the matching value's equivalentContent-Type
is used. IfdataFormat
is an empty string, then it uses the respectiveContent-Type
of the pre-configured defaultContent-Type
5. - If the request's
Content-Type
header exists, then this is assumed to be theContent-Type
of the request. - If the request's
Accept
header exists, then this is assumed to be theContent-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
, orString
-typed method parameters with the namebody
are assigned the content of the request or a request parameter calledbody
. 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'sJsonSluper
orXmlSlurper
to parse the request's content and assigns the parsed content toDataWrapper
'sdata
property. Thedata
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 nameinternalId
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 namepath
are assigned the value of the request's URI.String
method parameters with the namemethod
are assigned theString
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 namedparameters
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:
void
HttpEntity
ResponseEntity
HttpHeaders
Callable<?>
DeferredResult
ListenableFuture
ResponseBodyEmitter
SseEmitter
StreamingResponseBody
@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
HttpMessageConverter
s 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-Type
s, 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.
- If the
produces
property of a (handler class or method's7)@RequestMapping
annotation exists and the request'sAccept
header matches any of theproduces
property's values, then the matching value is assumed to be the response'sContent-Type
. - If the request has a non-
null
query parameter nameddataFormat
and it matches any of the valid values (json
,xml
, anddefault
), then the matching value's equivalentContent-Type
is used. IfdataFormat
is an empty string, then it uses the respectiveContent-Type
of the pre-configured defaultContent-Type
4. - If the request's
Accept
header exists, then this is assumed to be the appropriateContent-Type
of the response.
If none of the options above matched, then an error is thrown.
-
You may choose other formats by creating and registering your own custom
HttpMessageConverter
s and then specifying theproduces
property of the@RequestMapping
annotation. ↩ -
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 toapi
by default. ↩ -
Because
@Controller
is an implementation of@Component
. ↩ -
If defined, the
consumes
property of a method's@RequestMapping
annotation takes precedence over that of a class's. ↩↩ -
The default format is set via the
api.rest.default-content-type
instance property. ↩ -
Service calls are made through HTTP requests or Martini endpoints. ↩
-
If defined, the
produces
property of a method's@RequestMapping
annotation takes precedence over that of a class's. ↩