Monday, 2 January 2012

Create Spring REST service for Google App Engine in 15 minutes

Here's how you setup a REST service deployed in the Google App Engine cloud in 15 minutes. The use case in this example is a highscore backend service for my Android game Othello Legends.

Requirements:
We want to create a REST interface for these resources representing a highscore service.


GET http://myapp.appspot.com/api/highscores/
Fetch all applications backed by the highscore service since we want to reuse this for multiple games.

GET http://myapp.appspot.com/api/highscores/myappname
Fetch a sorted listed of highscores for a particular application myappname.

POST http://myapp.appspot.com/api/highscores/myappname
Post a potential new highscore to service. If it makes it to the highscore list it will be saved in database. The data will be sent as query parameters.


Ingredients of the solution:
Google App Engine runs Java and Python. This example will use the Java infrastructure.
So what we'll do is to create a standard Java J2EE web application built for deployment in App Engine backed by a simple DAO to abstract the Google BigTable databases. By using Spring REST together with Jackson we can communicate with JSON in a RESTful manner with minimum effort.

Sounds complicated? Not at all, here's how you do it!

Prerequisities:
REST Implementation:

So to create an App Engine web app, click the New Web Application Project icon. Deselect Google Web Toolkit if you don't intend to use it.

Now, we're going to use Spring REST for the REST heavy weight lifting. Download Spring Framework 3 or later from http://www.springsource.org/download. While at it, download the Jackson JSON library from http://jackson.codehaus.org/. Put the downloaded jars in the /war/WEB-INF/lib/ folder and add them to the classpath of your web application.

Now, to bootstrap Spring to handle your incoming servlet requests you should edit the web.xml file of your web application found in war/WEB-INF/.




   api
   
      org.springframework.web.servlet.DispatcherServlet
   
   1

  

   api
   /api/*



   index.html



That will put Spring in charge of everything coming in under path /api/*. Spring must now which packages to scan for Spring annotated classes. We add a Spring configuration file for this and also add some Spring/Jackson config for specifying how to convert from our Java POJOs to JSON. Put this stuff in a file called api-servlet.xml in war/WEB-INF.


 

 
  
   
    
   
  
 

 

 
  
   
    
   
  
  
  
   
    
   
  
 




Without going into detail, this config pretty much tells Spring to convert POJOs to JSON as default using Jackson for servlet responses. If you're not interested in the details just grab it, but you must adjust the <context:component-scan base-package="se.noren.othello" /> to match your package names.

Now to the fun part, mapping Java code to the REST resources we want to expose. We need a controller class to annotate how our Java methods should map to the exposed HTTP URIs. Create something similar to

import java.util.Date;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PathVariable;
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.servlet.ModelAndView;

/**
 * Controller for Legends app family highscore services.
 */
@Controller
@RequestMapping("/highscores")
public class LegendsHighScoreController {
 private static final long serialVersionUID = 1L;

 @Autowired
 HighScoreService highScoresService;

 /**
  * @return Fetch all registered applications in the highscore database.
  */
 @RequestMapping(value = "/", method = RequestMethod.GET)
 public ModelAndView getAllApplications() {
  List<String> allApplications = highScoresService.getAllApplications();
  return new ModelAndView("highScoresView", BindingResult.MODEL_KEY_PREFIX + "applications", allApplications);
 }
 
 /**
  * Fetch all highscores for a particular application.
  * @param application Name of application
  * @return
  */
 @RequestMapping(value = "/{application}", method = RequestMethod.GET)
 public ModelAndView getAllHighScores(@PathVariable String application) {
  List<HighScore> allHighScores = highScoresService.getAllHighScores(application);
  return new ModelAndView("highScoresView", BindingResult.MODEL_KEY_PREFIX + "scores", allHighScores);
 }
 
 /**
  * Add a new highscore to the database if it makes it to the high score list.
  * @param application Name of application
  * @param owner Owner of the highscore
  * @param score Score as whole number
  * @param level Level of player reaching score.
  * @return The created score.
  */
 @RequestMapping(value = "/{application}", method = RequestMethod.POST)
 public ModelAndView addHighScores(@PathVariable String application,
                             @RequestParam String owner,
                             @RequestParam long score,
                             @RequestParam long level
                             ) {
  
  HighScore highScore = new HighScore(owner, score, application, new Date().getTime(), level);
  highScoresService.addHighScores(highScore);
  return new ModelAndView("highScoresView", BindingResult.MODEL_KEY_PREFIX + "scores", highScore);
 }
}


So what's the deal with all the annotations? They're pretty self explanatory once you start matching the Java methods to the three HTTP REST URIs we wanted to create, but in short:

  • @Controller - The usual Spring annotation to tell Spring that this is a controller class that should be managed by the Spring container. All RESTful stuff is contained within the this class.
  • @RequestMapping("/highscores") - This means that this controller class should accept REST calls under the path /highscores. Since we deployed the servlet under servlet mapping /api in the web.xml this means all REST resources resides under http://host.com/api/highscores
  • @Autowired HighScoreService highScoresService - Our backing service class to do real business logic. Agnostic that we're using a RESTful front.
  • @RequestMapping(value = "/{application}", method = RequestMethod.GET) public ModelAndView getAllHighScores(@PathVariable String application) -  A method annotated like this creates a REST resource /api/highscores/dynamicAppName where the value given for dynamicAppName is given via the path variable application. The request method specifies that this Java method will be called if this URI is requested via HTTP GET. All ordinary HTTP verbs are supported.
  • @RequestParam String owner - If you wish to pass query parameters like myvar1=foo&myvar2=bar you can use the request param annotation.
  • The Java class returned in the ModelAndView response will be automatically marshalled to JSON by Jackson on the same structure as the Java POJO.
Database
Google App Engine uses the Google BigTables behind the scenes to store data. You can abstract this by using the standard JPA annotations on your POJOs. The similar JDO standard can be used as well. I've used JDO in previous projects and it works very well. For this simple server application we will however use the query language to directly access the document database. Here's the code for the first method to fetch all highscores for a particular Legends application. The database can filter and sort via API methods in the query.


@Service
public class HighScoreServiceImpl implements HighScoreService {

 @Override
 public List<HighScore> getAllHighScores(String application) {
  ArrayList<HighScore> list = new ArrayList<HighScore>();
  DatastoreService datastore = DatastoreServiceFactory
    .getDatastoreService();

  // The Query interface assembles a query
  Query q = new Query("HighScore");
  q.addFilter("application", Query.FilterOperator.EQUAL, application);
  q.addFilter("score", FilterOperator.GREATER_THAN_OR_EQUAL, 0);
  q.addSort("score", SortDirection.DESCENDING);

  // PreparedQuery contains the methods for fetching query results
  // from the datastore
  PreparedQuery pq = datastore.prepare(q);

  for (Entity result : pq.asIterable()) {
   String owner = (String) result.getProperty("owner");
   Long date = (Long) result.getProperty("date");
   Long score = (Long) result.getProperty("score");
   Long level = (Long) result.getProperty("level");
   list.add(new HighScore(owner, score, application, date, level));
  }

  return list;
 }


That's pretty much it. Run the project locally by right-clicking it and choose Run As -> Web application. Once you are ready to go live create a cloud application by going to https://appengine.google.com/ and Create new application

Now in Eclipse, right click on your project and choose Google -> Deploy to Google App Engine.
You will be asked to supply the name you created in the App Engine administration interface. Wait a few seconds and the application will be deployed in the cloud.

19 comments:

  1. can you give a little more info to a newbie on how to add the spring and jackson (Put this stuff in a file called api-servlet.xml in war/WEB-INF.) I am on the back side of the learning curve here. or maybe I missed something

    ReplyDelete
    Replies
    1. Hi! Not sure which part you mean? If it's the configuration that confuses you. It's really simple, you put the xml above (<beans xmlns:context ... etc) in a file called api-servlet.xml inside your web application java project. If the web app is created via the Eclipse plugins you should already have a directory structure according to the Java servlet specification. And therefore a folder called WEB-INF. Put the file in that folder, et voila! Good luck

      Delete
  2. Hi Johan,

    Am following on your tutorial am am kind of lost here.

    Where is the class HighScore.java in the method
    public List getAllHighScores under the class HighScoreServiceImpl.java

    Also where are the method implementations of highScoresService.getAllApplications(); and highScoresService.getAllHighScores(application); under the class LegendsHighScoreController.java

    Your tutorial is nice but it is incomplete as it has missing parts.

    I would really appreciate your help on this.

    Am from Nairobi,Kenya.

    ReplyDelete
  3. Hi, the HighScore is a simple POJO according to the constructor demonstrated in the example and the other service backends are pretty much the same as the service getAllHighScores() I described. If you wish to know more about the database APIs against Google App Engine check out the documentation at http://code.google.com/intl/sv-SE/appengine/docs/java/datastore/overview.html and you find more advanced ways to make queries.

    ReplyDelete
  4. Hi Johan,
    I managed to recreate the HighScore POJO as a Persistable JDO object and successfully deployed it.I also created the methods in the controlled that i was asking you.

    However am now getting a new error as follows:
    WARNING: No file found for: /myappname/api/highscores

    I cannot be able to consume any service.Any idea what this is?

    ReplyDelete
    Replies
    1. Sounds like your servlet mappings are wrong if you get http 404. Pls check in your web.xml what servlet mapping you have set for the dispatcher servlet. If you have done according to the example above and used "api", your services should reside at localhost:9080/api/highscores/yourappname

      Delete
  5. Hey Johan, thanks for the tutorial, it would be really useful if you would attach all the sourcecode including the imports and all files. For noobs like me its easier to understand and then i dont have to write that POJO myself haha :) really i dont exactly know which import u are using for the Entity object for example, i guess its javax.persistence but since im a noob i have enough guesswork todo :)

    ReplyDelete
  6. Oh and i dont know which of the libs of the spring framework are needed, so im using all now :P same for jackson, jackson all is needed?

    ReplyDelete
  7. Hey its me again the anonymous appengine noob, it would be cool if you could also provide the code how you access this from your android application. Do you have to deserialize the model object you are using in android yourself to use it for an addHighscore call? or do you use Jackson? What if i want to post a byte[]?? no problemo?? im new to jackson also :/ Except for my gratefulness all i can do is provide you with a five star rating for ur game!

    ReplyDelete
    Replies
    1. Hi again! No you don't need to do it manually. I couldn't make Jackson work on Android which would had been my first choice. Fortunately there are a bunch of JSON marshallers. And there are even stuff coming with the Android default libraries. I used the org.json.JSONObject which is in the core libs. Nice and easy API for my purposes. I used Apache HttpClient for handling the http connections and JSONObject for marshalling between objects and strings. Here is my adapter class for this if you want inspiration. http://pastebin.com/em1vuU01
      Good luck!

      Delete
  8. Why do you use the http://jackson.codehaus.org/ instead of other (e.g. gson from google).
    can i do the same example without the JSON library and mapping (i didn't really get the POJOs translation to JSON)

    ReplyDelete
    Replies
    1. I used Jackson since that's what I'm familiar with. You could substitute with whatever library I guess, but maybe you need to write some glue code if the library is not supported by Spring. Don't know about gson.

      Delete
  9. Hi Johan
    I'm trying to get your example to work with Spring 3.1 and it seems the syntax has changed.
    It's complaining about and tags in the api-servlet.xml.
    Can you post the new file?

    org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 16 in XML document from ServletContext resource [/WEB-INF/api-servlet.xml] is invalid; nested exception is org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'bean'. One of '{"http://www.springframework.org/schema/context":include-filter, "http://www.springframework.org/schema/context":exclude-filter}' is expected.

    Thx

    ReplyDelete
  10. Or let me know what version of Spring you've used since I've tried 3.1 and 3.0.7 and both are failing with exceptions during the application startup due to issue with declaration of tag.

    Amir

    ReplyDelete
    Replies
    1. If I remember correctly I used Spring 3.0.5. Hope you'll find the mismatch, cheers!

      Delete
  11. Wheres the rest of the code? what a waste of time.

    ReplyDelete
  12. Fucking waste of time with this incomplete code ¬¬ There's no HighScore.java, no addHighScores method....

    ReplyDelete
    Replies
    1. Sorry to waste your time mate, the complete code is available at https://github.com/johannoren/OthelloLegendsBackend

      Delete
  13. This information is impressive; I am inspired with your post writing style & how continuously you describe this topic. After reading your post, thanks for taking the time to discuss this, I feel happy about it and I love learning more about this topic.android development course fees in chennai | android app development training in chennai

    ReplyDelete