December 2009

REST vs. SOAP - Java Tutorial and Comparison

I had a discussion with my professor because the lecture on middleware covered SOAP but not REST.

In my opinion both have their strengths and weaknesses, but REST is way better in term of chaching. To prove that I implemented a simple server and client both with SOAP and RESTful HTTP. The results won't be very accurate but sufficient for comparing the magnitude of the performance. To make the comparison easier I used Java as programming language for both client and server.

SOAP

Implementing the Server

  1. Download the latest version (2.2.5) of Apache CFX and untar it
  2. Create a new Java project in Eclipse and add all JARs of Apaches CFX and its dependencies as referenced libraries.
  3. Write the interface for the server (it's basically a book collection)

        @WebService
        public interface BookCollection {
            void addBook(Book book);
            Book getBook(int isbn);
            Book[] getAllBooks();
            void editBook(Book book);
            void deleteBook(int isbn);
        }
     
  4. Write the data class

        public class Book {
            public int isbn;
            public String author;
            public String title;
        }
     

    You might want to encapsulate the fields. In Eclipse you can use the menu Source -> Generate Getters and Setters ...

  5. Write the implementation

        @WebService(
            endpointInterface = "de.livoris.restvssoap.BookCollection",
            serviceName = "MyBooks")
        public class MyBookCollection implements BookCollection {
     
            private Map<Integer, Book> books = new HashMap<Integer, Book>();
     
            @Override
            public void addBook(Book book) {
                books.put(book.getIsbn(), book);
            }
     
            @Override
            public void deleteBook(int isbn) {
                books.remove(isbn);
            }
     
            @Override
            public void editBook(Book book) {
                Book old = getBook(book.getIsbn());
                old.setAuthor(book.getAthor());
                old.setTitle(book.getTitle());
            }
     
            @Override
            public Book[] getAllBooks() {
                return books.values().toArray(new Book[books.size()]);
            }
     
            @Override
            public Book getBook(int isbn) {
                return books.get(isbn);
            }
        }
     
  6. Writing the actual SOAP-Server (better in a new Java project)

        public class Server {
            public static void main(String[] args) throws IOException {
                System.out.println("Starting Server");
                MyBookCollection myBooks = new MyBookCollection();
                String address = "http://localhost:9000/myBooks";
                Endpoint.publish(address, myBooks);
                System.in.read();
            }
        }
     
  7. The WSDL of the SOAP service is now accessible through http://localhost:9000/myBooks?wsdl.

Implementing the Client

  1. Apache CFX has a tool for generating a proxy from the WSDL. To generate the SOAP proxy for the book collection set the environment variables JAVA_HOME, CFX_HOME and PATH correctly, then type:

        mkdir -p BookSOAPClient/src
        wsdl2java -d BookSOAPClient/src http://localhost:9000/myBooks?wsdl
     

    You might want to rename the package afterwards.

  2. Now you need a list of books. An easy way is to download this CSV file and to import it using the opencsv library and the following code:

        private List<Book> books = new LinkedList<Book>();
     
        public void importFromFile(String path) throws IOException {
            CSVReader reader = new CSVReader(new FileReader(path));
            String [] nextLine = reader.readNext(); // skip first line
            while ((nextLine = reader.readNext()) != null) {
                // Reject books with no or invalid ISBN
                if (nextLine[1].matches("[0-9]+")) {
                    Book b = new Book();
                    b.setAuthor(nextLine[0]);
                    b.setIsbn(Integer.parseInt(nextLine[1]));
                    b.setTitle(nextLine[2]);
                    books.add(b);
                }
            }
        }
     
  3. Implement a dummy client that will add a book from the CSV file and then request all books until all books have been added.

        private BookCollection collection = MyBooks.getMyBookCollectionPort();
        private int adds = 0;
        private int gets = 0;
     
        public void work() {
            Book current;
            for (Book current : books) {
                collection.addBook(current); adds++;
                Book[] serverBooks = collection.getAllBooks(); gets++;
                for (Book b : serverBooks) {
                    collection.getBook(b.getIsbn()); gets++;
                  }
            }
        }
     
  4. Run the main method, that will import the CSV data, start the dummy client and measure the execution time.

        public static void main(String[] args) throws IOException {
            Client client = new Client();
            client.importFromFile("etc/books.csv");
            long start = System.currentTimeMillis();
            client.work();
            long end = System.currentTimeMillis();
            System.out.println("Execution time = " + (end - start) + "ms");
        }
     
  5. The result of running the client is shown below:

    IndicatorValue
    Add requests78
    Get requests3197
    Time25742ms

Caching

SOAP always uses HTTP POST requests for each message, therefore its neccessary for the caching web service to read the whole HTTP request including its body, to open the provided SOAP envelope and to unmarshall the XML that describes the message. In the end responding with a cached response is not different from normal responding. If you use Apache Axis or ASP.NET then you can enable caching inside your service class but if you want to use a caching proxy the only SOAP webservice cache I found was Ventus Proxy.

REST

Implementing the Server

  1. Add marshall and unmarshall methods to the Book class. For this example I use JSON with the Java library from json.org.

        public static Book fromJSON(JSONObject object) throws JSONException {
            Book result = new Book();
            result.setIsbn(object.getInt("isbn"));
            result.setAuthor(object.getString("author"));
            result.setTitle(object.getString("title"));
            return result;
        }
     
        public JSONObject toJSON() throws JSONException {
            JSONObject result = new JSONObject();
            result.put("isbn", getIsbn());
            result.put("author", getAuthor());
            result.put("title", getTitle());
            return result;
        }
     
  2. The next step is to create a the actual Server with the Java library Restlet.

    To do this, you have to create two different resources. One for the book collection and one for a single book. The resource for the book collection supports adding books and getting all books.

        public class BookCollectionResource extends ServerResource {
     
            // the underlying book collection
            protected static BookCollection books = new MyBookCollection();
     
            @Post
            public void add(Representation entity) throws Exception {
                JsonRepresentation rep = new JsonRepresentation(entity);
                Book book = Book.fromJSON(rep.getJsonObject()); 
                books.addBook(book);
                setStatus(Status.SUCCESS_CREATED);
            }
     
            @Get
            public JsonRepresentation getAll(Representation entity) throws Exception {
                JSONArray jsonArray = new JSONArray();
                for (Book book : books.getAllBooks()) {
                    jsonArray.put(book.toJSON());
                }
                setStatus(Status.SUCCESS_OK);
                return new JsonRepresentation(jsonArray);
            }
        }
     
  3. The resource for a single book uses the isbn parameter of the request to find the book and supports get, update and delete.

        public class BookResource extends ServerResource {
            // the underlying book object
            private Book book;
     
            @Override
            protected void doInit() throws ResourceException {
                super.doInit();
                setDimensions(new HashSet<Dimension>(Arrays.asList(Dimension.MEDIA_TYPE)));
                int isbn = Integer.parseInt(((String) getRequest().getAttributes().get("isbn")));
                book = BookCollectionResource.books.getBook(isbn);
            }
     
            @Get
            public JsonRepresentation getIt(Representation entity) throws Exception {
                if (book == null) {
                    setStatus(Status.CLIENT_ERROR_NOT_FOUND);
                    return new JsonRepresentation(new JSONObject());
                } else {
                    setStatus(Status.SUCCESS_OK);
                    JsonRepresentation rep = new JsonRepresentation(book.toJSON());
                    Calendar now = Calendar.getInstance();
                    now.add(Calendar.MINUTE, 30);
                    rep.setExpirationDate(now.getTime())); // assume that a book won't change very often
                    return rep;
                }
            }
     
            @Put
            public void storeBook(Representation entity) throws Exception {
                if (book == null) {
                    setStatus(Status.CLIENT_ERROR_NOT_FOUND);
                    return;
                }
                JsonRepresentation rep = new JsonRepresentation(entity);
                Book updated = Book.fromJSON(rep.getJsonObject()); 
                book.setTitle(updated.getTitle());
                book.setAuthor(updated.getAuthor());
            }
     
            @Delete
            public void deleteBook() {
                if (book == null) {
                    setStatus(Status.CLIENT_ERROR_NOT_FOUND);
                } else {
                    BookCollectionResource.books.deleteBook(book.getIsbn());
                    setStatus(Status.SUCCESS_NO_CONTENT);
                }
            }
        }
     
  4. Finally the code to start the server is needed.

        public static void main(String[] args) throws Exception {
            Component component = new Component();
            component.getServers().add(Protocol.HTTP, 9001);
            component.getDefaultHost().attach("/books", BookCollectionResource.class);
            component.getDefaultHost().attach("/books/{isbn}", BookResource.class);
            component.getLogService().setEnabled(false);
            component.start();
        }
     
  5. You can use your browser to access the books by navigating to http://localhost:9001/books.

Implementing the Client

  1. You need to create a new Java Project and reference the Project with the Book and the BookCollection class, as well as Restlet and JSON jar files.

  2. Create a class that implements the BookCollection interface and that uses the Restlet library and the methods toJSON and fromJSON of the Book class to create HTTP requests.

        public class BookCollectionProxy implements BookCollection {
     
            private static final String SERVICE_URL = "http://localhost:9001/books";
     
            ClientResource books = new ClientResource(SERVICE_URL);
     
            @Override
            public void addBook(Book book) {
                try {
                    Representation rep = new JsonRepresentation(book.toJSON());
                    books.post(rep);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
     
            @Override
            public void deleteBook(int isbn) {
                ClientResource res = new ClientResource(SERVICE_URL + "/" + isbn);
                res.delete();
            }
     
            @Override
            public void editBook(Book book) {
                try {
                    ClientResource res = new ClientResource(SERVICE_URL + "/" + book.getIsbn());
                    Representation rep = new JsonRepresentation(book.toJSON());
                    res.put(rep);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
     
            @Override
            public Book[] getAllBooks() {
                try {
                    JsonRepresentation json = new JsonRepresentation(books.get());
                    JSONArray array = json.getJsonArray();
                    Book[] result = new Book[array.length()];
                    for (int i = 0; i < array.length(); i++) {
                        result[i] = Book.fromJSON(array.getJSONObject(i));
                    }
                    return result;
                } catch (Exception e) {
                    printStackTrace();
                }
                return null;
            }
     
            @Override
            public Book getBook(int isbn) {
                try {
                    ClientResource res = new ClientResource(SERVICE_URL + "/" + isbn);
                    JsonRepresentation rep = new JsonRepresentation(res.get());
                    return Book.fromJSON(rep.getJsonObject());
                } catch (Exception e) {
                    e.printStackTrace();
                    return null;
                }
            }
        }
     
  3. You can now use the client with the dummy client as shown above with REST.

    IndicatorValue
    Add requests78
    Get requests3159
    Time23873ms

Caching

Because a REST request is nothing more then a normal HTTP request, every caching HTTP proxy can be used to improve the performance. In the example I use Apache HTTP Server.

  1. The first step is to install Apache. If you use linux then apache2 will propably be in a package repository, otherwise you might download the source from the official website or a distribution like XAMPP.

  2. Configure apache2 to act as an caching proxy.

    The following configuration create a virtual host, that will serve at http://localhost:9002/ and will forward request that are not cached to http://localhost:9001/.

        Listen 9002
     
        <VirtualHost *:9002>
            Servername localhost
     
            Location /
                Order allow,deny
                Allow from all
            /Location
     
            ProxyRequests Off
     
            ProxyPass / http://localhost:9001/
            ProxyPassReverse / http://localhost:9001/
     
            # Enable memory caching
            CacheEnable mem http://localhost:9001/books
     
            # Limit the size of the cache to 16 Megabyte
            MCacheSize 16384
        </VirtualHost>
     
  3. After changing the SERVICE_URL in the BookCollectionProxy to http://localhost:9002/books all request go through the Apache HTTP Server that will automatically cache some of the books and returning their JSON representation without even contacting the Restlet Server. This speeds up the processing as shown in the table below.

    IndicatorValue
    Add requests78
    Get requests3159
    Time16632ms

Results

In both examples Jetty (a small embeddable web server) was used and not a full blown application server like Tomcat that would probably be much faster. Comparing the three solutions SOAP, REST and REST-Caching the last one has by far the best performance. And that is exactly why I think REST is a very good solution for scalable web applications even though REST has disadvantages because it is very restricted and therefore sometimes hard to use for a general purpose interface.

Berlin

Berlin - Pankow

Wechsel zu Drupal

Mir gefaellt Textpattern sehr gut, aber andererseits ist es doch problematisch, wenn man mehrere Blogs und dazu noch statischen Content fuer meine Projekte auf einer einzigen Seite verwalten moechte. Nachdem ich erfolglos probiert hatte, Textpattern in ein anderen System einzubetten und sogar schon mit Frames rumgespielt hatte, kam ich schiesslich zu dem Entschluss, dass ein neues und flexibleres System her muss.

Ich habe mir auf opensourceCMS ein paar Systems angesehen, aber letztendlich hat mich der Chaosradio Express Podcast zum Thema Drupal ueberzeugt.

Die Hauptseite (livoris.de) laeuft jetzt mit Drupal und dieser Blog wird dort nun auch in englisch weitergefuehrt werden, wobei ich moeglicherweise auch ein paar Sachen recylcen werde. Jedenfalls wird das hier der letzte Eintrag im Livoris Coding Blog sein.

(originally posted 2009-12-07)