This is the core recipe of the chapter. We will detail how to use the Spring MVC method-handlers for HTTP methods that we haven't covered yet: the non-readonly ones.
We will see the returned status codes and the HTTP standards driving the use of the PUT
, POST
, and DELETE
methods. This will get us to configure HTTP-compliant Spring MVC controllers.
We will also review how request-payload mapping annotations such as @RequestBody
work under the hood and how to use them efficiently.
Finally, we open a window on Spring transactions, as it is a broad and important topic in itself.
Following the next steps will present the changes applied to two controllers, a service and a repository:
v7.x.x
. Then, run a maven clean install
on the cloudstreetmarket-parent
module (right-click on the module and go to Run as… | Maven Clean and then again go to Run as… | Maven Install) followed by a Maven Update
project to synchronize Eclipse with the maven configuration (right-click on the module and then go to Maven | Update Project…).Maven clean
and Maven install
commands on zipcloud-parent
and then on cloudstreetmarket-parent
. Then, go to Maven | Update Project.UsersController
and a newly created TransactionController
.UserController
is given here:@RestController @RequestMapping(value=USERS_PATH, produces={"application/xml", "application/json"}) public class UsersController extends CloudstreetApiWCI{ @RequestMapping(method=POST) @ResponseStatus(HttpStatus.CREATED) public void create(@RequestBody User user, @RequestHeader(value="Spi", required=false) String guid, @RequestHeader(value="OAuthProvider", required=false) String provider, HttpServletResponse response) throws IllegalAccessException{ ... response.setHeader(LOCATION_HEADER, USERS_PATH + user.getId()); } @RequestMapping(method=PUT) @ResponseStatus(HttpStatus.OK) public void update(@RequestBody User user, BindingResult result){ ... } @RequestMapping(method=GET) @ResponseStatus(HttpStatus.OK) public Page<UserDTO> getAll(@PageableDefault(size=10, page=0) Pageable pageable){ return communityService.getAll(pageable); } @RequestMapping(value="/{username}", method=GET) @ResponseStatus(HttpStatus.OK) public UserDTO get(@PathVariable String username){ return communityService.getUser(username); } @RequestMapping(value="/{username}", method=DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable String username){ communityService.delete(username); } }
@RestController @ExposesResourceFor(Transaction.class) @RequestMapping(value=ACTIONS_PATH + TRANSACTIONS_PATH, produces={"application/xml", "application/json"}) public class TransactionController extends CloudstreetApiWCI<Transaction> {
(The GET
method-handlers given here come from previous recipes.)
@RequestMapping(method=GET) @ResponseStatus(HttpStatus.OK) public PagedResources<TransactionResource> search( @RequestParam(value="user", required=false) String userName, @RequestParam(value="quote:[\d]+", required=false) Long quoteId, @RequestParam(value="ticker:[a-zA-Z0-9-:]+", required=false) String ticker, @PageableDefault(size=10, page=0, sort={"lastUpdate"}, direction=Direction.DESC) Pageable pageable){ Page<Transaction> page = transactionService.findBy(pageable, userName, quoteId, ticker); return pagedAssembler.toResource(page, assembler); } @RequestMapping(value="/{id}", method=GET) @ResponseStatus(HttpStatus.OK) public TransactionResource get(@PathVariable(value="id") Long transactionId){ return assembler.toResource( transactionService.get(transactionId)); }
(The PUT
and DELETE
method-handlers introduced here are non-readonly methods.)
@RequestMapping(method=POST) @ResponseStatus(HttpStatus.CREATED) public TransactionResource post(@RequestBody Transaction transaction) { transactionService.hydrate(transaction); ... TransactionResource resource = assembler.toResource(transaction); response.setHeader(LOCATION_HEADER, resource.getLink("self").getHref()); return resource; } @PreAuthorize("hasRole('ADMIN')") @RequestMapping(value="/{id}", method=DELETE) @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable(value="id") Long transactionId){ transactionService.delete(transactionId); } }
hydrate
method in the post
method prepares the Entity for underlying service uses. It populates its relationships from IDs received in the request payload.hydrate
method in transactionServiceImpl
:@Override public Transaction hydrate(final Transaction transaction) { if(transaction.getQuote().getId() != null){ transaction.setQuote( stockQuoteRepository.findOne( transaction.getQuote().getId())); } if(transaction.getUser().getId() != null){ transaction.setUser(userRepository.findOne(transaction.getUser().getId())); } if(transaction.getDate() == null){ transaction.setDate(new Date()); } return transaction; }
@Transactional(readOnly = true)
. Check the following TransactionServiceImpl
example:@Service @Transactional(readOnly = true) public class TransactionServiceImpl implements TransactionService{ ... }
@Transactional
annotation:@Override @Transactional public Transaction create(Transaction transaction) { if(!transactionRepository.findByUserAndQuote(transaction.getUser(), transaction.getQuote()).isEmpty()){ throw new DataIntegrityViolationException("A transaction for the quote and the user already exists!"); } return transactionRepository.save(transaction); }
IndexRepositoryImpl
):@Repository @Transactional(readOnly = true) public class IndexRepositoryImpl implements IndexRepository{ @PersistenceContext private EntityManager em; @Autowired private IndexRepositoryJpa repo; ... @Override @Transactional public Index save(Index index) { return repo.save(index); } ... }
First, let's quickly review the different CRUD services presented in the controllers of this recipe. The following table summarizes them:
URI |
Method |
Purpose |
Normal response codes |
---|---|---|---|
|
GET |
Search transactions |
200 OK |
|
GET |
Get a transaction |
200 OK |
|
POST |
Create a transaction |
201 Created |
|
DELETE |
Delete a transaction |
204 No Content |
|
POST |
Logs in a user |
200 OK |
|
GET |
Get all |
200 OK |
|
GET |
Get a user |
200 OK |
|
POST |
Create a user |
201 Created |
|
PUT |
Update a user |
200 OK |
|
DELETE |
Delete a user |
204 No Content |
To understand the few decisions that have been taken in this recipe (and to legitimate them), we must shed some light on a few points of the HTTP specification.
Before starting, feel free to visit Internet standards track document (RFC 7231) for HTTP 1/1 related to Semantics and Content:
https://tools.ietf.org/html/rfc7231
In the HTTP specification document, the request methods overview (section 4.1) states that it is a requirement for a server to support the GET
and HEAD
methods. All other request methods are optional.
The same section also specifies that a request made with a recognized method name (GET
, POST
, PUT
, DELETE
, and so on) but that doesn't match any method-handler should be responded with a 405 Not supported
status code. Similarly, a request made with an unrecognized method name (nonstandard) should be responded with a 501 Not implemented
status code. These two statements are natively supported and auto-configured by Spring MVC.
The document introduces introduces the Safe and Idempotent qualifiers that can be used to describe a request method. Safe methods are basically readonly methods. A client using such a method does not explicitly requests a state change and cannot expect a state change as a result of the request.
As the Safe word suggests, such methods can be trusted to not cause any harm to the system.
An important element is that we are considering the client's point of view. The concept of Safe methods don't prohibit the system from implementing "potentially" harmful operations or processes that are not effectively read only. Whatever happens, the client cannot be held responsible for it. Among all the HTTP methods, only the GET
, HEAD
, OPTIONS
, and TRACE
methods are defined as safe.
The specification makes use of the idempotent qualifier to identify HTTP requests that, when identically repeated, always produce the same consequences as the very first one. The client's point of view must be considered here.
The idempotent HTTP methods are GET
, HEAD
, OPTIONS
, TRACE
(the Safe methods) as well as PUT
and DELETE
.
A method's idempotence guarantees a client for example that sending a PUT request can be repeated even if a connection problem has occurred before any response is received.
The POST
methods are usually associated with the creation of resources on a server. Therefore, this method should return the 201 (Created)
status code with a location header that provides an identifier for the created resource.
However, if there hasn't been creation of resource, a POST
method can (in practice) potentially return all types of status codes except 206 (Partial Content)
, 304 (Not Modified)
, and 416 (Range Not Satisfiable)
.
The result of a POST
can sometimes be the representation of an existing resource. In that case, for example, the client can be redirected to that resource with a 303
status code and a Location
header field. As an alternative to POST
methods, PUT
methods are usually chosen to update or alter the state of an existing resource, sending a 200 (OK)
or a 204 (No Content)
to the client.
Edge cases with inconsistent matches raise errors with 409 (Conflict)
or 415 (Unsupported Media Type)
.
Edge cases of no match found for an update should induce the creation of the resource with a 201 (Created)
status code.
Another set of constraints applies on the DELETE
requests that are successfully received. Those should return a 204 (No Content)
status code or a 200 (OK)
if the deletion has been processed. If not, the status code should be 202 (Accepted)
.
In Chapter 4, Building a REST API for a Stateless Architecture, we have presented the RequestMappingHandlerAdapter
. We have seen that Spring MVC delegates to this bean to provide an extended support to @RequestMapping
annotations.
In this perspective, RequestMappingHandlerAdapter
is the central piece to access and override HttpMessageConverters
through getMessageConverters()
and setMessageConverters(List<HttpMessageConverter<?>> messageConverters)
.
The role of @RequestBody
annotations is tightly coupled to HttpMessageConverters
. We will introduce the HttpMessageConverters
now.
HttpMessageConverters
, custom or native, are bound to specific mime types. They are used in the following instances:
Accept
request header mime types, they serve the @ResponseBody
annotation's purposes (and indirectly @RestController
annotations that abstract the @ResponseBody
annotations).Content-Type
request header mime types, these converters are called when the @RequestBody
annotation are present on a method handler argument.More generally, HttpMessageConverters
match the following HttpMessageConverter
interface:
public interface HttpMessageConverter<T> { boolean canRead(Class<?> clazz, MediaType mediaType); boolean canWrite(Class<?> clazz, MediaType mediaType); List<MediaType> getSupportedMediaTypes(); T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; }
The getSupportedMediaTypes()
method returns the list of mediaTypes
(mime types) that a specific converter supports. This method is mainly used for reporting purposes and for the canRead
and canWrite
implementations. These canRead
and canWrite
eligibility methods are used by the framework to pick up at runtime the first HttpMessageConverter
that either:
Content-Type
request header for the given Java class targeted by @RequestBody
Accept
request header for the Java class that the HTTP response-payload will correspond to (
the Type targeted by @ResponseBody
)With the latest versions of Spring MVC (4+), a few extra HttpMessageConverters
come natively with the framework. We have thought that summarizing them would be helpful. The following table represents all the native HttpMessageConverters
, the mime types, and the Java Types they can be associated with. Short descriptions, mostly coming from the JavaDoc, give more insight about each of them.
URI |
Supported MediaTypes (by default) |
Convert to/from |
---|---|---|
|
Can READ/WRITE application/x-www-form-urlencoded, Can READ multipart/form-data. |
|
For part conversions, it also embeds (by default) | ||
|
Can READ/WRITE application/x-www-form-urlencoded, Can READ multipart/form-data. |
|
This converter extends | ||
|
Can READ/WRITE application/x-www-form-urlencoded, Can READ multipart/form-data. |
|
This converter extends | ||
|
Can READ all media types that are supported by the registered image readers. Can WRITE the media type of the first available registered image writer. |
|
|
Can READ */*, WRITE with application/octet-stream. |
|
|
CAN READ/WRITE application/json, application/*+json. |
|
Uses the Google Gson library's | ||
|
Can READ XML collections. |
|
This converter can read collections that contain classes annotated with | ||
|
Can READ/WRITE XML |
|
This converter can read classes annotated with | ||
|
Can READ/WRITE application/json, application/*+json. |
|
Uses Jackson 2.x ObjectMapper. This converter can be used to bind with typed beans or untyped HashMap instances. (Jackson 2 must present on the classpath.) | ||
|
Can READ/WRITE application/xml, text/xml, application/*+xml. |
|
This uses the Jackson 2.x extension component for reading and writing XML encoded data (https://github.com/FasterXML/jackson-dataformat-xml). (Jackson 2 must be present on the classpath.) | ||
|
Can READ/WRITE text/xml application/xml. |
|
This uses Spring's Marshaller and Unmarshaller abstractions (OXM). | ||
|
Can READ/WRITE text/plain. |
|
This uses | ||
|
Can READ application/json, application/xml, text/plain and application/x-protobuf. Can WRITE application/json, application/xml, text/plain and application/x-protobuf, text/html. |
|
This uses Google protocol buffers (https://developers.google.com/protocol-buffers) to generate message Java classes you need to install the | ||
|
Can READ/WRITE */*. |
|
The Java Activation Framework (JAF), if available, is used to determine the content-type of written resources. If JAF is not available, application/octet-stream is used. | ||
|
Can READ/WRITE application/rss+xml. |
|
This converter can handle Channel objects from the ROME project (https://github.com/rometools). (ROME must be present on the classpath.) | ||
|
Can READ/WRITE application/atom+xml. |
|
This can handle Atom feeds from the ROME project (https://github.com/rometools). (ROME must be present on the classpath.) | ||
SourceHttpMessageConverter |
Can READ/WRITE text/xml, application/xml, application/*-xml. |
|
|
Can READ/WRITE */*. |
|
In this recipe, the MappingJackson2HttpMessageConverter
is used extensively. We used this converter for both the financial transaction creation/update side and the User-Preferences update side.
Alternatively, we used AngularJS to map an HTML form to a built json object whose properties match our Entities. Proceeding this way, we POST
/PUT
the json
object as the application/json
mime type.
This method has been preferred to posting an application/x-www-form-urlencoded
form content, because we can actually map the object to an Entity. In our case, the form matches exactly a backend resource. This is a beneficial result (and constraint) of a REST design.
The @RequestPart
annotation can be used to associate part of a multipart/form-data
request with a method argument. It can be used with argument Types such as org.springframework.web.multipart.MultipartFile
and javax.servlet.http.Part
.
For any other argument Types, the content of the part is passed through an HttpMessageConverter
just like @RequestBody
.
The @RequestBody
annotation has been implemented to handle the user-profile picture. Here's our sample implementation from the UserImageController
:
@RequestMapping(method=POST, produces={"application/json"}) @ResponseStatus(HttpStatus.CREATED) public String save( @RequestPart("file") MultipartFile file, HttpServletResponse response){ String extension = ImageUtil.getExtension(file.getOriginalFilename()); String name = UUID.randomUUID().toString().concat(".").concat(extension); if (!file.isEmpty()) { try { byte[] bytes = file.getBytes(); Path newPath = Paths.get(pathToUserPictures); Files.write(newPath, bytes, StandardOpenOption.CREATE); ... ... response.addHeader(LOCATION_HEADER, env.getProperty("pictures.user.endpoint").concat(name)); return "Success"; ... }
The file part of the request is injected as an argument. A new file is created on the server filesystem from the content of the request file. A new Location
header is added to the Response with a link to the created image.
On the client side, this header is read and injected as background-image
CSS property for our div (see user-account.html
).
The recipe highlights the basic principles we applied to handle transactions across the different layers of our REST architecture. Transaction management is a whole chapter in itself and we are constrained here to present just an overview.
To build our transaction management, we kept in mind that Spring MVC Controllers are not transactional. Under this light, we cannot expect a transaction management over two different service calls in the same method handler of a Controller. Each service call starts a new transaction, and this transaction is expected to terminate when the result is returned.
We defined our services as @Transactional(readonly="true")
at the Type level, then methods the that need Write access override this definition with an extra @Transactional
annotation at the method level. The tenth step of our recipe presents the Transactional changes on the TransactionServiceImpl
service. With the default propagation, transactions are maintained and reused between Transactional services, repositories, or methods.
By default, abstracted Spring Data JPA repositories are transactional. We only had to specify transactional behaviors to our custom repositories, as we did for our services.
The eleventh step of our recipe shows the Transactional changes made on the custom repository IndexRepositoryImpl
.
As mentioned earlier, we configured a consistent transaction management over the different layers of our application.
Our coverage is limited and we advise you to find external information about the following topics if you are not familiar with them.
Four properties/concepts are frequently used to assess the transaction's reliability. It is therefore useful and important to keep them in mind when designing transactions. Those properties are Atomicity, Consistency, Isolation and Durability. Read more about ACID transactions on the Wikipedia page:
We only defined local transactions in the application. Local transactions are managed at the application level and cannot be propagated across multiple Tomcat servers. Also, local transactions cannot ensure consistency when more than one transactional resource type is involved. For example, in a use case of database operations associated with messaging, when we rollback a message that couldn’t have been delivered, we might need to also rollback the related database operations that have happened beforehand. Only global transactions implementing 2-step commits can take on this kind of responsibility. Global transactions are handled by JTA transaction manager implementations.
Read more about the difference in this Spring reference document:
http://docs.spring.io/spring/docs/2.0.8/reference/transaction.html
Historically, JTA transaction managers were exclusively provided by J2EE/JEE containers. With application-level JTA transaction manager implementations, we now have other alternatives such as Atomikos (http://www.atomikos.com), Bitronix (https://github.com/bitronix/btm), or JOTM (http://jotm.ow2.org/xwiki/bin/view/Main/WebHome) to assure global transactions in J2SE environments.
Tomcat (7+) can also work along with application-level JTA transaction manager implementations to reflect the transaction management in the container using the TransactionSynchronizationRegistry
and JNDI datasources.
https://codepitbull.wordpress.com/2011/07/08/tomcat-7-with-full-jta
Performance and useful metadata benefits can be obtained from these three headers that are not detailed in the recipe.