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:
- You will learn a lot.
- Because you can.
- This will bring you the posibility to extend this prototype in a future.
- This is the differente between being a user of other's tools and creating new tools.
I did this journey some years ago.
- I coded a static HTTP server in Java (available in Github).
- Then I extended it to run processes (similar to CGI), with the process generating the HTTP response (available in Github).
- Finally, I extended it to run Java code (similar to Servlets) using the reflection API.
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:
- The framework must use annotations (similar to Spring or Eclipse Microprofile).
- The framework must expose metrics.
- The framework must expose some kind of API documentation.
- The framework should be really simple to use.
- I must start from an empty project.
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:
@RestEndPoint
to specify the HTTP method and where it is exposed@APIDoc
to provide a description and a command line interface call to consume the endpoint@JSONBody
to indicate that the body of the request must be passed as an argument to the method@PathVar
to indicate that a part of the URL must be passed as an argument to the method@Param
a parameter sent in the URL (GET) or in the body (other methods).@FileBody
to deal with files passed as multipart in the body.
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:
- in the URL
/api/doc
the documentation specified with the@APIDoc
annotations:
- in the URL
/api/metrics
the following metrics: the mean execution time of each method and the number of calls. This is capture after making 19 calls to/api/users
:
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:
- Add config (from file, from secrets, from a cache, etc.).
- Add authentication with tokens.
- Consider the possibility to generate the documentation of the API in the constructor and store it in the Nginx server.
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.