Introduction
For the last couple of days I've been working to get a simple REST service up and running at Google AppEngine using the Google Cloud Endpoints framework presented at Google IO 2012. The introduction at the page just linked to in addition to the documentation gave the impression that this was a very simple and straight forward framework to use. It might be, but I still used three days to get a hello world application up and running. The reason for this is compound. I may be an idiot, but I will also put part of the blame on an incomplete documentation by Google. The Google Cloud Endpoints techonology ("Endpoints" from now) is still only in semi-closed beta though, so I should probably not expect more. In any case, I am writing this blog post so that others will only use the single hour needed to get a hello world app up and running instead of the full three days* I used.Access, installation and environment
The Endpoints technology is available for all to use - you do not need to join the trusted testers group other than for getting access to the documentation. You should still of course do this. I will also refer to this documentation in this post. I'm just saying that the framework is indeed embedded in the AppEngine SDK v1.7 which I am using.
I will be using the following IDE, framework and SDK versions in this example. Installation of these are out of the scope of this post but I will provide a couple of links:
- Java Development Kit (JDK) 1.6 SE JDK 1.7 is not fully supported by AppEngine
- AppEngine SDK v1.7.4
- Eclipse Juno EE
After installing the AppEngine SDK and the plugin for Eclipse you shall have the Google Toolbar available as well. Use this as a verification that you're done with the installation phase.
Creating the project
- Go to File > New other or hit Ctrl+N in Eclipse. Select Google > Web Application Project.
- Enter a project name, a package name and uncheck the use of Google Web Toolkit which is a GUI framework we won't be needing for a back-end solution. I will be using "com.example" as package name in this example. Whenever I refer to this package name later, replace with your.
- Delete /src/com.example/HelloWorldServlet.java and /war/index.html as we will not be using these.
- Create a new class /src/com.example/HelloWorldEndpoint.java. Let the content be the following:
package com.example; import com.google.api.server.spi.config.Api; @Api(name = "example") public class HelloWorldEndpoint { public Container getThing() { Container c = new Container(); c.Text = "Hello world!"; return c; } public class Container { public String Text; } }
- Edit /war/WEB-INF/web.xml and set the init-param SystemServiceServlet to point to HelloWorldEndpoint instead of HelloWorldServlet. You can delete the HelloWorld-servlet definition entierly, leaving the final web.xml to look like this (namespaces in web-app tag removed for brevity):
SystemServiceServlet com.google.api.server.spi.SystemServiceServlet services com.example.HelloWorldEndpoint SystemServiceServlet /_ah/spi/*
Testing
Launch your app by selecting Run > Run while for instance HelloWorldEndpoint.Java file is open. Jetty Application Server will launch and the Console in Eclipse will display a bunch of output. One of the final lines should be this, which indicates you're good to go:INFO: The admin console is running at http://localhost:8888/_ah/admin
Your port may vary from 8888. Use this info to launch a web browser at the following URL, replacing 8888 with whatever port your server is running at:
http://localhost:8888/_ah/api/example/v1/containerThe result should be the following:
How URL's are generated
UPDATE 11th Feb 2013: The following section was written without knowledge of the path and httpMethod-parameters of the @ApiMethod annotation. See the documentation. While the below is still true, it's far less important to know how URL's are generated because you're not bound to it.
Now this is the part that got me. The official Google Cloud Endpoint documentation for Java (as per February 5th 2013) does not specify in detail how the URL's are generated. Instead it encourages you to go ahead and create the client libraries and consume the service that way. While that is the end goal for me as well, I like to know the intermediate steps so that I can debug better and possibly open up for creating clients not supported by the client-code-generating library, like Windows Phone.
The URL's to access your services in a HTTP REST-manner*** is generated in a combination of class/method annotation and code convention. This is the general rule from the point of view of a URL:
http://host:port/ServiceRoot/ApiName/Version/Entity/param1/param2/paramN
Keyword | Maps to |
---|---|
ServiceRoot | As configured in <url-pattern> of the servlet mapping in web.xml |
ApiName | The name-parameter of the @Api annotation of the class mapped in services class in web.xml (API-class from now). |
Version | The version parameter of the @Api annotation of the class mapped in services class in web.xml. (Default: "v1") |
Entity | The class name in lowercase which an API method returns - except for remove-methods, where it's taken from the postfix of the method name (sigh...) |
Param1/2/N | (Optional) Value of the N'th @Named annotation of any method parameter of type String or int |
In addition to this, the prefix of any method name in the API-class will determine to which HTTP request method it will respond. The full table of these prefixes, from my knowledge is:
Prefix | HTTP method | Return constraint | Input constraint |
---|---|---|---|
get | GET | Any class T | None or any number of @Named parameter of type String/int |
list | GET | T[] or List<T> | None or any number of @Named parameter of type String/int |
insert | POST | Any class T1 | One class T2**** and any number of @Named parameter of type String/int |
update | PUT | Any class T1 | One class T2 and any number of @Named parameter of type String/int |
remove | DELETE | Any class T | None or any number of @Named parameter of type String/int |
****) Should, but need not, be equal to type T1
In all of the examples, class T is typically any business entity class. It cannot be System.String.
- The framework relies heavily on reflection and code convention to find out which methods to expose and how
- You do not need to annotate any methods, although the @ApiMethod can be used for convenience (see client library generation)
- All parameters must be annotaded with @Named.
- If you break any of these rules the framework will give you a 404 error without any further explanation.
A couple of examples:
1. From the hellow world-example with one get-method, this is how the framework maps it to a URL:
CODE
URL
HTTP GET http://localhost:8888/_ah/api/example/v1/container
2. An insert method with GET-parameter in addition to object payload
CODE
URL
HTTP POST http://localhost:8888/_ah/api/example/v2/container/4381294
3. Remove method
CODE
URL
HTTP DELETE http://localhost:8888/_ah/api/example/v1/container/4381294
No it's not a mistake that I've now red-boxed the method postfix "Container" in "removeContainer". The remove method does not follow the same code convention as the others. Thank you Google. I sincerely hope this is a bug.
Troubleshooting
Finally I'd like to mention a couple of pointers as to where to look if you can't make this work. These are pitfalls I've gone into. So, in no particular order:- Look for errors messages in the Jetty error console
- Make sure war/WEB-INF/yourapname-v1.api is autogenerated after each compilation. Try removing the @Named-annotation in front of a random input parameter for a method. You will notice that the .api file is not generated. This will result in all of your methods returning 404-errors.
- Make sure you always shut down the server before launching a new version on the dev-server. Or else your old code is still running and serving. See illustration:
really congratulations for your tutorial !
ReplyDelete..Google is often too simple into his documentation and tutorials offered .. if we see that most of GDG, GTUG, Hackathon and other are repetitive with exercises posed by Google, the level of detail of your development is really important..
best regards
@Mlaynes
http://mlaynessanchez.blogspot.com
Thanks so much for this tutorial! It has really helped me a lot and hopefully lot of GAE beginners.
ReplyDeleteI followed all your suggestions but somehow i still get 404 errors. Here is a sample of my code and .api file. Please help me if you can.
@Api (name="Myapp", version="v1")
public class TestEndpoint {
private static PersistenceManager getPersistenceManager() {
return PersistenceManagerUtil.get().getPersistenceManager();
}
public Test insert(@Named("username") String username, @Named("password") String password){
Test test=null;
test.setUsername(username);
test.setPassword(password);
PersistenceManager pm = getPersistenceManager();
pm.makePersistent(test);
pm.close();
return test;
}
}
.api file is as follows
"Myapp.testEndpoint.insert" : {
"path" : "test/{username}/{password}",
"httpMethod" : "POST",
"scopes" : [ ],
"audiences" : [ ],
"clientIds" : [ ],
"rosyMethod" : ".TestEndpoint.insert",
"request" : {
"parameters" : {
"username" : {
"type" : "string",
"required" : true
},
"password" : {
"type" : "string",
"required" : true
}
},
"body" : "empty"
},
"response" : {
"body" : "autoTemplate(backendResponse)"
}
}
}
Its working finally! :) As seen above, i had initialised test obj to null and so there was a NullPointerException which curl did not show but the APIs explorer showed.
DeleteSuggest people to test the APIs on the APIs explorer instead of curl (which shows 404 for every error )
If you run "curl -i" you should get full header with correct response code. But which client to use is pretty much a personal prefefrence. Fiddler2 and "Invoke-RestMethod" in PowerShell are two other options
ReplyDeletehttp://ido-green.appspot.com/CloudEndpoints/CloudEndpointsWebBlogPost.html
ReplyDeleteRead this tutorial.
Really good blog:)I felt this very helpful,,,Even I faced same problem by trying Rest service in GAE but it didnt work ,then I choosed End points as last solution it worked for me Thank you.
ReplyDeleteI searched everywhere for a clean working example and failed to find it till now.
ReplyDeleteThank you sir
Thank you!!! I killed a lot of time on this. It is a first example that actually works!
ReplyDeleteThanks... same here.. I've been trying to understand how to create REST service on GAE for almost 2 days. Read pages of documentation, tried different ways, tutorials...but nothing. This one finally works well, it's well exlpained and simple :D
ReplyDeleteJust one thing. I had 404 thrown always, after a while I realized the problem is that my JDO/JPA version was v2... I changed it to v1 and finally worked.
Really? You had to use v2 of JDA/JPO? I had that problem with Jersey, but not with GCE. Own blog entry regarding Jersey:
ReplyDeletehttp://www.nilzorblog.com/2013/02/a-helloworld-example-of-jersey-rest.html
v1 works well enough - I've actually got an app in production using that (www.chineseshowdown.com) . There's a couple of features I miss though, and you'll have a hard time getting support if you ever need it.
Hi i have problem with this API here si my code
ReplyDeletepackage com.example;
import com.google.api.server.spi.config.Api;
import com.google.api.server.spi.config.Named;
@Api(name = "example", version="v1")
public class HelloWorldEndpoint {
public Container getThing() {
Container c = new Container();
c.Text = "Hello world!";
return c;
}
public Container insertContainer(Container c, @Named("name")int name){
Container cr = new Container();
cr.Text = "Hello "+ name;
return cr;
}
public class Container {
public String Text;
}
}
and when I call http://localhost:8888/_ah/api/example/v1/container/67 i got Error 404
and here is header
Remote Address:127.0.0.1:8888
Request URL:http://localhost:8888/_ah/api/example/v1/container/67
Request Method:GET
Status Code:404 Not Found
Request Headersview source
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-US,en;q=0.8,bs;q=0.6,hr;q=0.4,sl;q=0.2,sr;q=0.2
Cache-Control:max-age=0
Connection:keep-alive
Host:localhost:8888
User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36
Response Headersview source
Cache-Control:no-cache
Content-Length:83
Content-Type:text/html; charset=iso-8859-1
Date:Sun, 14 Sep 2014 11:29:18 GMT
Expires:Fri, 01 Jan 1990 00:00:00 GMT
Server:Development/1.0
using chrome. So any help is welcome :)
Milos: Try specifying the endpoint name manually using @ApiMethod over getThing(). Maybe Google have changed the spec since I wrote this blog, I don't know.
DeleteOther than that I suggest you ask the question at stackoverflow.com as well - might get a lot better answers quicker there!