Background
P1Lapchart is a web application that dynamically renders pretty, and sometimes interactive, lap chart data that plots each car’s race position (y axis) by lap (x axis). It consists of a client component that does the rendering (the view in MVC) and a back end web service component that fetches the raw data from a separate web source, produces cooked data (model) which it provides to clients. A diagram of its architecture can be found here.
When I initially wrote the P1Lapchart web service, I chose to use node.js and express.js. It seemed like a good, focused, server technology stack for what I needed to do, which was – create a RESTful web service that would provide JSON lapchart data to client applications that could perform interesting visualizations. Recently I ported the web services stack to Java and used the Spring 3.2 MVC framework. This post records my observations and may be useful if you code in JavaScript or Java and are curious how the other side lives.
Goals of this Post
- This post is intended to be a short side-by-side survey of both first-hand approaches and is not a step-by-step guide on how to setup and create these two stacks. Some good starting points are the guides provided by the respective frameworks sites.
- When I first got started, there seemed to be a puzzling array of technologies from which to choose, making the start a somewhat daunting task. This post records these two stacks that I both have had success with.
- Additionally, I’ll point out some of the post-hello-world issues with the frameworks that I ran into and how I resolved them. We always seem to get stuck there 🙂
- Although this isn’t a JavaScript versus Java stack point, I’ll also discuss Amazon Web Services deployment issues I ran into.
Server
Here’s a summary of the sets of popular server-side technologies I used to create each of the two stacks. You can actually see my complete sources on GitHub. I’ll refer to the two server implementations as the JavaScript stack and the Java stack.
As of Sept 2013, the P1Lapchart backend web service is running the Java stack in the P1Software AWS cloud simply because it has the latest new feature I added (logging). In the future I’ll probably switch back and forth as each stack catches up with whatever updates I make.
Type | JavaScript Stack | Java Stack |
---|---|---|
GitHub Repo | p1lapchart | p1lapchartj |
Language | JavaScript | Java |
Web Server | node.js | Tomcat |
Backend Web Framework | express.js | Spring MVC |
REST Data Format | JSON | JSON |
JSON Library | jQuery | Jackson |
IAAS | AWS EC2 | AWS Elastic Beanstalk |
REST Producer
I started by examining the popular approaches for how to write a REST server that produces JSON data for a client. I chose JavaScript and Java server side technology stacks.
- JavaScript stack – express.js is a popular REST server platform and although the node.js ecosystem that it extends is a relatively young technology, useful documentation wasn’t too difficult to find on the web. One aspect that was particularly appealing is since both client and server are written in JavaScript, the same libraries can potentially be used by both clients and servers. In my past enterprise work experience I’ve run into implementation incompatibilities (bugs) caused by various software components not being able to use the same library because they were written in different languages. Even more intriguing is the potential of being able to move the libraries between the client-server boundary as pursued by the Airbnb folks. Here’s the snippet from JavaScript code that shows how to route HTTP GETs for the resource http://p1lapchart-javascript-service/api/eventlapchart/id to an inline callback function that takes the request and fills the JSON in the response.
12 var express = require('express'); 13 var app = express(); . . . 75 app.get('/api/eventlapchart/:id', function(req, res) { 76 var id = req.params.id; 77 var sourceurl = getSource(id); . . . 84 var json = enhance(data, sourceurl); 85 res.json(json); . . . 93 });
- Input – The path’s id is automatically mapped to an attribute with the same name in req‘s param attribute (see lines 75/76). When the GET is received, the second argument to app.get is an anonymous callback, function(req, res) at line 75, which is asynchronously called. Most node calls as asynchronous, so you should not perform lengthy operations inside them. Also, if you don’t normally read JavaScript, this manner of indentating and embedding inline anonymous function as an argument isn’t unusual.
- Output – In the callback simply pass the produced plain old JavaScript object, named json above, that enhance() produces (#84) to res.json (#85). That’s it! We’ll examine the enhance function I created, in the “REST Consumer” section below.
What’s rather nice (or limiting depending upon your situation) is that there is no external configuration file that creates this routing – it’s all captured in the code above.
As a side note – well after I started this stack, I found and enrolled in the free Stanford Startup Engineering course (quite excellent). When I saw that they too chose node, I got the warm fuzzies.
- Java stack – Spring MVC 3.2 also supports REST magic, commonly through @RequestMapping and @ResponseBody annotations. Here’s a snippet of the Java code where I’ve named my class P1LapchartController which is annotated with @Controller.
22 @Controller 23 public class P1LapchartController implements InitializingBean { . . . 43 @RequestMapping(value="/api/{id}", method=RequestMethod.GET) 44 @ResponseBody 45 public JsonNode getByID(@PathVariable String id, HttpServletRequest req, HttpServletResponse resp) { 46 JsonNode json = null; 48 String sourceurl = getSource(id); . . . 55 json = enhance(mylaps_json, sourceurl); 66 return json; 67 } . . . 128}
- Input – The path’s id is mapped to the arbitrarily named getByID function parameter via @RequestMapping’s curly brace {id} (#43). Unlike the earlier JavaScript express implementation, we don’t need to obtain id from req, it’s passed as a parameter using the @PathVariable annotation.
- Output – Also notice that the JSON to emit doesn’t need to be placed into the resp object. Instead @RequestBody indicates the getByID function directly returns the response’s JSON object using a plain old Java object, arbitrarily named json above. In my case, json happens to be a JsonNode class which you probably don’t need to use unless your function needs to be a REST client (more about this later in the “REST Consumer” section). So rather than using an Spring MVC view renderer, the model (json object) is directly written to the HTTP response as JSON.
The controller annotations is recognized due to a bit of standard servlet magic in the web.xml file that looks like this. It apparently can be made unnecessary via @EnableAutoConfiguration.
<servlet> <servlet-name>p1lapchartj<servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>p1lapchartj</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
The annotations perform the mapping is recognized by Spring magic in the p1lapchartj-servlet.xml file by this line.
<mvc:annotation-driven />
REST Consumer
The P1Lapchart web service gets its raw data through http://mylaps.com/api/eventlapchart?id=id which, you guessed it, returns lap chart information for the race indicated by id as plain old JSON objects. So the P1Lapchart web service is not only a REST server, but it is also a REST client that fetches its data from a 3rd party site.
- JavaScript stack – Adding more detail to our earlier app.get handler, we see that it is using jQuery’s getJSON function to GET the data from sourceurl, which contains the mylaps.com url string. data (#82) is just a plain old JavaScript object, deserialized from the JSON originating from the mylaps.com server. My enhance function (#84) massages data and returns it as a plain old JavaScript object, which I’ve arbitrarily named json. As mentioned earlier, this object is then passed to the res.json function.
75 app.get('/api/eventlapchart/:id', function(req, res) { 76 var id = req.params.id; 77 var sourceurl = getSource(id); 78 if (cache[id] != null) { 79 console.log('getJSON("' + sourceurl + '") cached'); 80 res.json(cache[id]); 81 } else { 82 $.getJSON(sourceurl, function(data) { 83 console.log('getJSON("' + sourceurl + '") success'); 84 var json = enhance(data, sourceurl); 85 res.json(json); 86 cache[id] = json; 87 }) 88 .fail(function(jqXJR, textStatus, errorThrown) { 89 console.log('getJSON("' + sourceurl + '") failed: ' + textStatus); 90 res.json({"p1meta": {"status": 404}}); 91 }); 92 } 93 });
- Java stack – Spring 3.2 uses the RestTemplate class’s getForObject function (#53) to perform a client-side call to the same mylaps.com sourceurl we saw in the JavaScript code. Because I am getting a 3rd party JSON object from mylaps.com containing some attributes that I know and care about, but others I don’t, I was not able to easily declare a custom Java class for it to map to, serving as my MVC model. I therefore used the Jackson library’s JsonNode class so that my enhance function (#55) can easily navigate and massage this “arbitrarily” structured object. The proper Jackson renderer is magically dispatched. Note that the corresponding JavaScript implementation above does not have this additional step since its objects are already unstructured. We’ll talk about addCORSHeaders (#58) next.
45 public JsonNode getByID(@PathVariable String id, HttpServletRequest req, HttpServletResponse resp) { 46 JsonNode json = null; 47 try { 48 String sourceurl = getSource(id); 49 if ((json = cache.get(sourceurl)) != null) { 50 logger.audit(req.getRemoteAddr(), id, "cached"); 51 } else { 52 RestTemplate restTemplate = new RestTemplate(); 53 String mylaps_json = restTemplate.getForObject(sourceurl, String.class); 54 logger.audit(req.getRemoteAddr(), id, "live"); 55 json = enhance(mylaps_json, sourceurl); 56 cache.put(sourceurl, json); 57 } 58 addCORSHeaders(resp); 59 } catch (Exception e) { 60 try { 61 json = mapper.readTree("{'p1meta': {'status': 404}}"); 62 } catch (Exception ee) { 63 logger.error("mapper.readTree", ee); 64 } 65 } 66 return json; 67 }
CORS
In both stacks, I had to deal with cross origin resource sharing (CORS). The web server that originates the index.html page (below) is from one domain but it contains JavaScript code (see the Client section below) that uses resources (the JSON objects) that are served up from servers in another domain (AWS servers running my server stack). Web browsers forbid this cross-domain interaction, which is what CORS addresses.
- JavaScript stack – The express.js HTTP server does not natively deal with this so I coded the server-side CORS sequence in app.js. I later found a node cors package that I haven’t yet tried. Here’s the relevant app.js allowCrossDomain function that I installed (#23) to filter the responses.
16 var allowCrossDomain = function (req, res, next) { // Allow (CORS) cross-domain requests 17 res.header('Access-Control-Allow-Origin', '*'); 18 res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 19 res.header('Access-Control-Allow-Headers', 'Content-Type'); 20 next(); 21 } 22 app.configure(function () { 23 app.use(allowCrossDomain); 24 });
- Java stack – Tomcat 7 describes a new CORS filter feature, but I was not successful in getting it to work. So again, it was – write some code in my controller that calls (#58 above) this …
34 public static void addCORSHeaders(HttpServletResponse resp) { 35 resp.addHeader("Access-Control-Allow-Origin", "*"); 36 resp.addHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE"); 37 resp.addHeader("Access-Control-Allow-Headers", "Content-Type"); 38 }
Cloud Deployment
Once the code was working locally, I then deployed it to the cloud. There’s lots of good documentation on Amazon Web Services which I won’t go into here. You can try for free for a year. Since I had already delved into AWS’s database services, so when it came time to chose a cloud infrastructure to deploy on I chose AWS over Heroku (which I later used in the Stanford course).
Here’s some quick notes on issues I ran into.
- JavaScript stack – Was my first attempt at using EC2. I used a micro instance.
- Remember to use the forever package which restarts your node app if it isn’t running for some reason.
- I later tried to upgrading my EC2 deployment to an Elastic Beanstalk to take advantage of load balancing and auto scaling. At the time I was semi-distracted with other things and wasn’t successful in getting it running. I suspect I had to capture my machine image with the other built node packages I installed.
- Java stack – Having not been able to move my JavaScript stack to Elastic Beanstalk, I was successful deploying my newer Java stack to Elastic Beanstalk. I was looking forward to this self-managed environment that would potentially take care of load balancing and auto scaling.
- Having to only push a single .war file with all your dependencies is nice. This gets in your way if you have multiple applications, each in its own .war (with different path routes) since beanstalk only accepts a single war. I’m researching best practices for addressing this.
- To save money, I’ve only been running with one EC2 instance. When I update my app by uploading a new .war, there is a service outage. Beanstalk does not seem to have an option that temporarily spins up a new instance allowing for continuous availability during updates. After I bit of research, I found Deploying Versions with Zero Downtime.
I haven’t yet driven large loads through both stacks and compare. That’s what I’d like to do in the near future.
Client
To get a full stack perspective, I’ll briefly talk about the web client, which I actually started developing first. In my enterprise employment, I had been working mostly on the server side, so I didn’t in get to experiment much with visualizations (which I love) during my normal work. I poked around on the client side first and decided to use Mike Bostock’s excellent D3.js library for data manipulation.
I’ve only produced a single client implementation that is web-based which we saw at the top of this post. Since the web service model has no preconceived notions of the client, someone could say write native mobile app. For completeness, here’s my web client technology stack …
Type | Browser Stack |
---|---|
GitHub Repo | p1lapchart index.html |
Language | JavaScript |
Visualizations | D3.js |
REST Data Format | JSON |
Controls | jQuery |
- The index.html code is mostly D3 code so I won’t describe any of it here (the Dashing D3.js guide is pretty good). While you might expect to see a jQuery getJSON call (as was done in the JavaScript web service code) to access the web service itself, you won’t find it. D3 has its own wrapper to retrieve external JSON data, which is what I use to access the P1Lapchart web service.
- For some unknown reason, clicking on a line to highlight only works on Internet Explorer and FireFox. I haven’t gotten to the bottom of yet, of why this interactivity doesn’t work on Chrome and Safari.
- jQuery was only used to write input controls.
Summary
Hopefully this gives you a taste for the technologies that I used to create running JavaScript and Java REST web services. I have no deep allegiances to either camp.
- JavaScript stack – I very much like the simplicity of node and using JavaScript on both client and server. Dealing with REST/JSON objects in JavaScript was especially nice, particularly since the P1Lapchart service is both a REST client as well as server. Building full stack software and having to deal with only one language and its libraries was appealing. I found myself having to context switch when transitioning between writing Java/Spring server and JavaScript client.
- Java stack – In general, I found that writing the corresponding P1Lapchart code in Java/Spring was “fussier” and a bit more long winded. On the other hand, in another project I’ve found that server-oriented libraries (e.g., Apache Commons) were readily found as that world is more mature. The corresponding server-oriented JavaScript libraries were more of a challenge to find as I imagine JavaScript traditionally addresses browser needs.
What do you think? I’d love to hear from you.