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
PersonandPersonController, respectively, and add them to a Martini package'scodedirectory. - Wait for the classes to compile and;
- Send a request to the endpoint at
/person/new2:
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
@Controllerand@ResponseBody.@Controlleris 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
@ResponseBodyannotation, 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.@RestControllerapplies@ResponseBodyto 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-nameandlast-namequery parameters and port them to thefirstNameandlastNamemethod 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
consumesproperty of a (handler class or method's4)@RequestMappingannotation exists and the request'sContent-Typeheader matches any of theconsumesproperty's values, then the matching value is assumed to be the request'sContent-Type. - If the request has a non-
nullquery parameter nameddataFormatand it matches any of the valid values (json,xml, anddefault), then the matching value's equivalentContent-Typeis used. IfdataFormatis an empty string, then it uses the respectiveContent-Typeof the pre-configured defaultContent-Type5. - If the request's
Content-Typeheader exists, then this is assumed to be theContent-Typeof the request. - If the request's
Acceptheader exists, then this is assumed to be theContent-Typeof 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 namebodyare 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.DataWrapperparameters are populated with parsed XML or JSON data. Martini uses Groovy'sJsonSluperorXmlSlurperto parse the request's content and assigns the parsed content toDataWrapper'sdataproperty. Thedataproperty'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. Stringmethod parameters with the nameinternalIdare 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).Stringmethod parameters with the namepathare assigned the value of the request's URI.Stringmethod parameters with the namemethodare assigned theStringrepresentation of the request's HTTP method.MartiniPackagemethod parameters are set to hold the object representation of the Martini package containing the Groovy controller class.RuleMetadatamethod parameters are assigned the object which holds the details of the monitor rule that the request matched.- A
Mapmethod parameter namedparameterswould 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:
voidHttpEntityResponseEntityHttpHeadersCallable<?>DeferredResultListenableFutureResponseBodyEmitterSseEmitterStreamingResponseBody
@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.
- If the
producesproperty of a (handler class or method's7)@RequestMappingannotation exists and the request'sAcceptheader matches any of theproducesproperty's values, then the matching value is assumed to be the response'sContent-Type. - If the request has a non-
nullquery parameter nameddataFormatand it matches any of the valid values (json,xml, anddefault), then the matching value's equivalentContent-Typeis used. IfdataFormatis an empty string, then it uses the respectiveContent-Typeof the pre-configured defaultContent-Type4. - If the request's
Acceptheader exists, then this is assumed to be the appropriateContent-Typeof 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
HttpMessageConverters and then specifying theproducesproperty of the@RequestMappingannotation. ↩ -
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 toapiby default. ↩ -
Because
@Controlleris an implementation of@Component. ↩ -
If defined, the
consumesproperty of a method's@RequestMappingannotation takes precedence over that of a class's. ↩↩ -
The default format is set via the
api.rest.default-content-typeinstance property. ↩ -
Service calls are made through HTTP requests or Martini endpoints. ↩
-
If defined, the
producesproperty of a method's@RequestMappingannotation takes precedence over that of a class's. ↩