Giving Spring Some REST: Craig Walls Twitter: @habuma @springsocial
Giving Spring Some REST: Craig Walls Twitter: @habuma @springsocial
Craig Walls
[email protected]
Twitter: @habuma @springsocial
http://github.com/habuma
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Why REST?
Key piece of the Modern Application puzzle
More APIs / Fewer pages
Humans and browsers consume pages
Everything can consume APIs
(incl. browsers, JS, mobile apps, other apps,etc)
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Twitter: @habuma
Blog: http://www.springinaction.com
@Inject
public BooksController(BookRepository bookRepository) {
! this.bookRepository = bookRepository;! !
}
!
!
!
!
!
}
@RequestMapping(method=RequestMethod.GET)
public @ResponseBody List<Book> allBooks() {
! return bookRepository.findAll();
}
GET http://host/app/books
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Message Converters
StringHttpMessageConverter
Reads text/* into String; writes String into text/plain
FormHttpMessageConverter
Reads/writes application/x-www-form-urlencoded from/to MultiValueMap<String,String>
ByteArrayMessageConverter
Reads */* into byte[]; writes Object as application/octet-stream
Jaxb2RootElementHttpMessageConverter
Reads/writes text/xml or application/xml from/to JAXB-annotated objects
MappingJacksonHttpMessageConverter /
MappingJackson2HttpMessageConverter
Reads/writes application/json from/to Objects
SourceHttpMessageConverter
Reads/writes text/xml/application/xml from/to javax.xml.transform.Source
ResourceHttpMessageConverter
Reads/writes org.springframework.core.io.Resource objects
[AtomFeed|RssChannel]HttpMessageConverter
Reads/writes Rome Feed and RssChannels (application/atom+xml | rss+xml)
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
!
}
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public @ResponseBody Book bookById(@PathVariable("id") long id) {
! return bookRepository.findOne(id);
}
GET http://host/app/books/{id}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
@RequestMapping(method=RequestMethod.POST)
public @ResponseBody Book postBook(@RequestBody Book book) {
! return bookRepository.save(book);
}!
POST http://host/app/books
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
@RequestMapping(value="/{id}" method=RequestMethod.PUT)
public void updateBook(@PathVariable("id") long id,
@RequestBody Book book) {
! bookRepository.save(book);
}
PUT http://host/app/books/{id}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Deleting a Resource
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
@RequestMapping(value="{id}", method=RequestMethod.DELETE)
public void deleteBook(@PathVariable("id") long id) {
! bookRepository.delete(id);
}
DELETE http://host/app/books/{id}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public @ResponseBody Book bookById(@PathVariable("id") long id) {
! return bookRepository.findOne(id);
}
Twitter: @habuma
Blog: http://www.springinaction.com
@RequestMapping(method=RequestMethod.POST)
public @ResponseBody Book postBook(@RequestBody Book book) {
! return bookRepository.save(book);
}!
Twitter: @habuma
Blog: http://www.springinaction.com
Returning a ResponseEntity
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
}
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<?> bookById(@PathVariable("id") long id) {
Book book = bookRepository.findOne(id);
if (book != null) {
return new ResponseEntity<Book>(book, HttpStatus.OK);
} else {
Error error = new Error("Book with ID " + id + " not found");
return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
}
}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Returning a ResponseEntity
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
@RequestMapping(value="/{id}", method=RequestMethod.GET)
public ResponseEntity<Book> bookById(@PathVariable("id") long id) {
Book book = bookRepository.findOne(id);
if (book != null) {
return new ResponseEntity<Book>(book, HttpStatus.OK);
}
throw new BookNotFoundException(id);
}
@ExceptionHandler(BookNotFoundException.class)
public ResponseEntity<Error> bookNotFound(BookNotFoundException e) {
Error error = new Error("Book with ID " + id + " not found");
return new ResponseEntity<Error>(error, HttpStatus.NOT_FOUND);
}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Returning a ResponseEntity
@Controller
@RequestMapping("/books")
public class BooksController {
!
...
!
!
!
!
!
!
!
!
!
@RequestMapping(method=RequestMethod.POST)
public ResponseEntity<Book> postBook(@RequestBody Book book) {
! Book newBook = bookRepository.save(book);
! ResponseEntity<Book> bookEntity =
new ResponseEntity<Book>(newBook, HttpStatus.CREATED);
! String locationUrl =
ServletUriComponentsBuilder.fromCurrentContextPath().
! ! ! path("/books/" + newBook.getId()).build().toUriString();
! bookEntity.getHeaders().setLocation(URI.create(locationUrl));
! return bookEntity;
}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Linking Resources
HATEOAS
Hypermedia As The Engine Of Application State
Responses carry links to related endpoints
API is self-descriptive
Client can learn about the API
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Self-Describing API
{
"links" : [
{
"rel" : "self",
"href" : "http://localhost:8080/BookApp/books/5"
},
{
"rel" : "all",
"href" : "http://localhost:8080/BookApp/books/"
},
{
"rel" : "author",
"href" : "http://localhost:8080/BookApp/authors/2"
}
],
"id" : 5,
"title" : "Spring in Action",
...
}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Defining a Resource
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Assembling a Resource
public class BookResourceAssembler
extends ResourceAssemblerSupport<Book, BookResource> {
public BookResourceAssembler() {
super(BooksController.class, BookResource.class);
}
public BookResource toResource(Book book) {
return createResource(book);
}
public BookResource instantiateResource(Book book) {
return new BookResource(book);
}
}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Asynchronous Controllers
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Handling messages
@Controller
public class PortfolioController {
...
!
!
!
!
!
@MessageMapping(value="/app/trade")
public void executeTrade(Trade trade, Principal principal) {
! trade.setUsername(principal.getName());
! this.tradeService.executeTrade(trade);
}
...
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Handling subscriptions
@Controller
public class PortfolioController {
...
!
!
@SubscribeEvent("/app/positions")
public List<PortfolioPosition> getPortfolios(Principal principal)
throws Exception {
Portfolio portfolio =
this.portfolioService.findPortfolio(principal.getName());
!
!
!
}
return portfolio.getPositions();
...
}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
@MessageExceptionHandler
@ReplyToUser(value="/queue/errors")
public String handleException(Throwable exception) {
! return exception.getMessage();
}
...
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
OAuth
Supported by Facebook,
Twitter: @habuma
Blog: http://www.springinaction.com
OAuth 1.0a
Twitter, LinkedIn, Evernote, Flickr,Yammer,Yelp!, ...
OAuth 2
Facebook, Foursquare, Google, GitHub, Instagram, ...
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
OAuth 2
Twitter: @habuma
Blog: http://www.springinaction.com
Twitter: @habuma
Blog: http://www.springinaction.com
Implicit Grant
Reduced round-trips
No refresh token support
Commonly used by inbrowser JavaScript apps or
widgets
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Authorization server
Resource server
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Twitter: @habuma
Blog: http://www.springinaction.com
Authorization Server
Resource Server
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
<oauth:client-details-service id="clientDetails">
<oauth:client client-id="tonr" secret="secret"
resource-ids="sparklr"
authorized-grant-types="authorization_code,implicit"
authorities="ROLE_CLIENT"
scope="read,write" />
</oauth:client-details-service>
<bean id="tokenServices"
class="o.sf.security.oauth2.provider.token.DefaultTokenServices">
<property name="tokenStore" ref="tokenStore" />
<property name="supportRefreshToken" value="true" />
<property name="clientDetailsService" ref="clientDetails"/>
</bean>
<bean id="tokenStore"
class="o.sf.security.oauth2.provider.token.InMemoryTokenStore" />
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
<authentication-manager id="clientAuthenticationManager"
xmlns="http://www.springframework.org/schema/security">
! <authentication-provider user-service-ref="clientDetailsUserService" />
</authentication-manager>
<bean id="clientDetailsUserService"
class="o.sf.security.oauth2.provider.client.ClientDetailsUserDetailsService">
<constructor-arg ref="clientDetails" />
</bean>
<bean id="oauthAuthenticationEntryPoint"
class="o.sf.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
<property name="realmName" value="sparklr2" />
</bean>
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
<bean id="accessDecisionManager"
class="org.springframework.security.access.vote.UnanimousBased"
xmlns="http://www.springframework.org/schema/beans">
<constructor-arg>
<list>
<bean class="o.sf.security.oauth2.provider.vote.ScopeVoter" />
<bean class="o.sf.security.access.vote.RoleVoter" />
<bean class="o.sf.security.access.vote.AuthenticatedVoter" />
</list>
</constructor-arg>
</bean>
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
RestTemplate
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Using RestTemplate
RestTemplate restTemplate = new RestTemplate();
Book book = new Book("Spring in Action", "Gregg Walls");
Book newBook = restTemplate.postForObject(
"http://host/app/books", book, Book.class);
book = restTemplate.getForObject(
"http://host/app/books/{id}", Book.class, newBook.getId());
book.setAuthor("Craig Walls");
restTemplate.put("http://host/app/books/{id}",
Book.class, book.getId());
restTemplate.delete("http://host/app/books/{id}",
Book.class, book.getId());
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Oh no!
WARNING: POST request for "https://api.twitter.com/1/statuses/update.json" resulted in 401 (Unauthorized); invoking error
handler
org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
!
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:75)
!
at org.springframework.web.client.RestTemplate.handleResponseError(RestTemplate.java:486)
!
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:443)
!
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:401)
!
at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:279)
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
@Bean
public ConnectController connectController() {
! return new ConnectController(connectionFactoryLocator, connectionRepository);
}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
<facebook:config app-id="${facebook.clientId}"
app-secret="${facebook.clientSecret}"
app-namespace="socialshowcase" />
<twitter:config app-id="${twitter.consumerKey}"
app-secret="${twitter.consumerSecret}"/>
<social:jdbc-connection-repository/>!
<bean id="connectController"
class="org.springframework.social.connect.web.ConnectController"
autowire="constructor" />
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
!
!
!
!
!
@RequestMapping(value="/twitter/tweet", method=RequestMethod.POST)
public String postTweet(String message) {
! twitter.timelineOperations().updateStatus(message);
! return "redirect:/twitter";
}
@Inject
public TwitterTimelineController(Twitter twitter) {
! this.twitter = twitter;
}
Email: [email protected]
Twitter: @habuma
Blog: http://www.springinaction.com
Thank you!
REST Books Sample (a work in progress)
https://github.com/habuma/rest-books
Spring HATEOAS
https://github.com/SpringSource/spring-hateoas
http://www.springsource.org/spring-security-oauth
Spring Social
http://www.springsource.org/spring-security-oauth
http://www.springsource.org/spring-data/rest
https://github.com/SpringSource/rest-shell