Because you can

Keywords: Java Rest API Frameworks Micro-services Spring Eclipse microprofile

This week I was teaching about TCP, and I told the students that it is possible to implement a web sever from this layer. You only have to implement the HTTP protocol to parse HTTP requests and provide the HTTP responses.

Then one student asked: "what is the point of doing that if you already have Apache?". There are several (non excluding) responses to that question:

  1. You will learn a lot.
  2. Because you can.
  3. This will bring you the posibility to extend this prototype in a future.
  4. This is the differente between being a user of other's tools and creating new tools.

I did this journey some years ago.

Last week I wanted to develop a micro framework to create REST APIs generating a fat jar that runs the server. These were the requirements:

After three days of intense coding and refactoring I completed the framework and some sample applications.

This is an example of an interface that defines a REST API:

package edu.uv.cs.rest.client;

import java.util.List;
import edu.uv.cs.rest.lib.FileBody;
import edu.uv.cs.rest.lib.JSONBody;
import edu.uv.cs.rest.lib.MultipartBody;
import edu.uv.cs.rest.lib.PathVar;
import edu.uv.cs.rest.lib.Param;
import edu.uv.cs.rest.lib.ApiDoc;
import edu.uv.cs.rest.lib.RestEndPoint;

public interface UserEndPoint{
   @RestEndPoint(method="GET",url="/api/users")
   @ApiDoc(cli="curl http://$HOST:$PORT/api/users",
        desc="To get all users")
   public List<User> get();

   @RestEndPoint(method="GET",url="/api/users_query")
   @ApiDoc(cli="curl http://$HOST:$PORT/api/users_query?edad=18",
        desc="To get users with age equal or bigger than value of parameter edad")
   public List<User> getEdadMayorQue(@Param(name="edad") int edad);

   @RestEndPoint(method="GET",url="/api/user/{id}")
   @ApiDoc(cli="curl http://$HOST:$PORT/api/user/1",
        desc="Returns the user with the id provided in the path")
   public User get(@PathVar(name="id") String id);

   @RestEndPoint(method="PUT",url="/api/user")
   @ApiDoc(cli="curl -X PUT -d '{\"name\":\"juan\",\"email\":\"juan@dev.null\",\"year\":2000}' http://$HOST:$PORT/api/user",
        desc="Adds the user passed in the body of the request")
   public User put(@JSONBody User s);

   @RestEndPoint(method="DELETE",url="/api/user/{id}")
   @ApiDoc(cli="curl -X DELETE http://$HOST:$PORT/api/user/1",
        desc="Deletes the user with the id provided in the path")
   public void delete(@PathVar(name = "id") String id);

   @RestEndPoint(method="POST",url="/api/user/{id}")
   @ApiDoc(cli="curl -X POST -d '{\"edad\":32}' http://$HOST:$PORT/api/user/1",
        desc="Modififies properties of the user with id provided in the path")
   public void modify(@PathVar(name="id") String id, @JSONBody User s);

   @RestEndPoint(method="POST",url="/api/user/{id}/photo")
   @ApiDoc(cli="curl -F\"file=@/path/file.jpg\" http://$HOST:$PORT/api/user/1/photo",
        desc="Adds a photo to the user with id provided in the path. The image is uploaded in the body of the request as multipart/form-data")
   public void addPhoto(@PathVar(name="id") String id, @FileBody MultipartBody MultipartBody);
}

In this code I am using some interfaces that are defined in the framework:

The implementation of the interface is a POJO, that only contains business logic:

package edu.uv.cs.rest.client;

import java.io.File;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.Vector;
import java.util.stream.Collectors;
import org.slf4j.*;
import edu.uv.cs.rest.lib.MultipartBody;
import edu.uv.cs.rest.lib.NotFoundException;

// This stores the information in memory, so it does not persists
// It is only a proof of concept.
public class UserEndPoint_impl implements UserEndPoint {
   private static Logger LOG = LoggerFactory.getLogger(UserEndPoint_impl.class.getName());
   private HashMap<String, User> data;

   public UserEndPoint_impl() {
      data = new HashMap<>();
   }

   @Override
   public void delete(String id) {
      data.remove(id);
   }

   @Override
   public void modify(String id, User s) {
      User old = data.get(id);
      if (old == null)
         throw new NotFoundException("Student with id: " + id + "not found");
      if (s.getName() != null)
         old.setName(s.getName());
      if (s.getEmail() != null)
         old.setEmail(s.getEmail());
   }

   public void addPhoto(String id, MultipartBody multipartBody) {
      User s = data.get(id);
      if (s == null)
         throw new NotFoundException("User with id: " + id + "not found");
      else {
         try {
            File f = multipartBody.saveMultipartFile("/tmp/images/users", true);
            s.setPhoto(f.getAbsolutePath());
         } catch (Exception ex) {
            throw new RuntimeException("Error saving uploaded image");
         }
      }
   }

   @Override
   public List<User> get() {
      return new Vector<User>(data.values());
   }

   @Override
   public List<User> getEdadMayorQue(int edad) {
      int year = Calendar.getInstance().get(Calendar.YEAR);
      LOG.debug("Edad solicitada: {}", edad);
      LOG.debug("Año actual: {}", year);

      return new Vector<User>(data.values().stream().filter(u -> ((year - u.getYear())>=edad)).collect(Collectors.toList()));
   }

   @Override
   public User get(String id) {
      User u = data.get(id);
      if (u == null)
         throw new NotFoundException("User with id " + id + " not found");
      return u;
   }

   @Override
   public User put(User u) {
      u.setId(UUID.randomUUID().toString());
      data.put(u.getId(), u);
      return u;
   }
}

As can be seen in this code, the framework provides the exception NotFoundException that are properly treated to give the corresponding response to the client. RuntimeException will be treated as an internal server error.

Finally, the class that registers this component in the framework and starts the server:

package edu.uv.cs.rest.client;

import edu.uv.cs.rest.lib.DispatchServer;
import edu.uv.cs.rest.lib.Dispatcher;

public class RestServer{
   public static void main(String[] args) throws Exception{
      // Create the dispatcher providing the implementation of the interface
      Dispatcher requestDispatcher = new Dispatcher(UserEndPoint_impl.class);
      // Register the dispatcher in the server and specify the port
      DispatchServer ds = new DispatchServer(requestDispatcher, 9000);
      ds.start();
   }
}

When the server starts it exposes automatically:

rest-api-doc.png

rest-api-metrics.png

The final fat jar is only about 1MB of size so very suitable for micro-services running in limited devices.

This can be nicely integrated with a Nginx acting as a reverse proxy and dealing with the HTTPS requests.

The framework is not finished, and when I had some time I will add, just for fun and to learn, some features such as:

Returning to the original student question: when you start a journey doing something from scratch you will learn a lot and this can open new possibilities to create original things.

Goto index

Date: 27/11/2020

Author: Juan GutiƩrrez Aguado

Emacs 27.1 (Org mode 9.4.4)

Validate