Don't be surprised : ApacheSpark and SparkJava - it's a 2 different technologies !
In this post a'm talking about SparkJava - simple rest framework for Java : http://sparkjava.com/
Related posts :
Spring Boot - simple example
SpringBoot with SpringData and H2 database
SpringBoot is a very good framework, but it takes as dependencies almost full stack of all Spring libraries. And if you are not planing to use them in your project - you will start thinking about more "compact" rest frameworks. SparkJava is one of them.
1. Goal
As a lazy developer I want to create a rest application with ability to add new entities in very simple way : by just extending "base" class :GenericController<Item> itemController=new GenericController<Item>("/item", Item.class, itemService);
2. Application structure
Standard "mave-based" application structure:3.Maven project(pom.xml) file
Just spark,slfj,gson and junit dependencies:<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.demien</groupId> <artifactId>sparktest</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.sparkjava</groupId> <artifactId>spark-core</artifactId> <version>2.0.0</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.2.4</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies> </project>
4. Domain objects
For my POJO data objects i created interface IPersistablepublic interface IPersistable { Long getId(); void setId(Long id); }
For having ability to operate ID field(by getId method) in controllers and test classes. So, all my domain objects are implementing this interface :
public class Item implements IPersistable { private Long id; private String name; private Long parentId;
public class Param implements IPersistable { private Long id; private String name; private String dataType; private Item item;
5. Controller
As I mentioned before, I'm too lazy, so I want to move common operations like "add", "get", "update", "delete" into one class :package com.demien.sparktest.controller; import com.demien.sparktest.util.JsonUtil; import com.demien.sparktest.domain.IPersistable; import com.demien.sparktest.service.GenericService; import spark.Request; import spark.Response; import spark.Spark; public class GenericController<T extends IPersistable> { private GenericService<T> service; private Class<T> cl; public GenericController(String basePath, Class<T> cl, GenericService<T> service) { this.cl=cl; this.service=service; Spark.get(basePath,this::getAll, JsonUtil::toJson); Spark.get(basePath+"/:id",this::getById, JsonUtil::toJson); Spark.get(basePath+"/test",this::test, JsonUtil::toJson); Spark.post(basePath,this::add, JsonUtil::toJson); Spark.put(basePath,this::update, JsonUtil::toJson); Spark.delete(basePath,this::delete, JsonUtil::toJson); } public Object test(Request request, Response response) { return "Hello world!"; } public Object getAll(Request request, Response response) { return service.getAll(); } public Object getById(Request request, Response response) { String id = request.params(":id"); return service.getById(Long.parseLong(id)); } public T restoreObjectFromRequest(Request request) { return (T)JsonUtil.toObject(request.body(),cl); } public Object add(Request request, Response response) { return service.add(restoreObjectFromRequest(request)); } public Object update(Request request, Response response) { return service.update(restoreObjectFromRequest(request)); } public Object delete(Request request, Response response) { service.delete(restoreObjectFromRequest(request)); return ""; } }
So, now, for my entities(item and param) I have just to extend this class, without creation of controllers :
GenericController<Item> itemController=new GenericController<Item>(ITEM_PATH, Item.class, itemService); GenericController<Param> paramController=new GenericController<Param>(PARAM_PATH, Param.class, paramService);
6. "Dummy" service.
It's just a simple demo project, so I decided not to use Hibernate, and created simple class for storing objects in a HashMap :package com.demien.sparktest.service; import com.demien.sparktest.domain.IPersistable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class GenericService<T extends IPersistable> { private Long maxId=0L; private Map<Long, T> storage=new HashMap<Long, T>(); public void clearStorage() { storage.clear(); } public T getById(Long id) { return storage.get(id); } public T add(T element) { Long id=element.getId(); if (id==null) { maxId++; element.setId(maxId); } else { if (maxId.longValue()<id.longValue()) { maxId=id+1; } } return update(element); } public List<T> getAll() { List<T> result=new ArrayList<T>(); for (T element:storage.values()) { result.add(element); } return result; } public T update(T element) { storage.put(element.getId(), element); return storage.get(element.getId()); } public void delete(T element) { storage.remove(element.getId()); } }
7. Main application file.
Spark is running just like regular java application. In main() procedure i have to "start" my controllers :package com.demien.sparktest; import com.demien.sparktest.controller.GenericController; import com.demien.sparktest.domain.Param; import com.demien.sparktest.service.GenericService; import spark.Spark; import com.demien.sparktest.domain.Item; public class App { public final static int SPARK_PORT=8080; public final static String APP_PATH="http://localhost:"+SPARK_PORT; public final static GenericService<Item> itemService=new GenericService<>(); public final static String ITEM_PATH="/item"; public final static GenericService<Param> paramService=new GenericService<>(); public final static String PARAM_PATH="/param"; public static void main(String[] args) { Spark.setPort(8080); GenericController<Item> itemController=new GenericController<Item>(ITEM_PATH, Item.class, itemService); GenericController<Param> paramController=new GenericController<Param>(PARAM_PATH, Param.class, paramService); } }
8. Utils
Also I had to create few simple utils :
8.1. JsonUtil - just for conversion json<=>object
package com.demien.sparktest.util; import com.google.gson.Gson; public class JsonUtil { public static String toJson(Object object) { return new Gson().toJson(object); } public static Object toObject(String json, Class<?> cl) { return new Gson().fromJson(json, cl); } }
8.2. RestTestUtil - for testing : sending requests
package com.demien.sparktest.util; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; public class RestTestUtil { public static class RequestResult { public final String body; public final int status; private RequestResult(int status, String body) { this.body = body; this.status = status; } } public static RequestResult sendRequest(String method, String path, String urlParameters) throws IOException { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setDoOutput(true); conn.setInstanceFollowRedirects(false); conn.setRequestMethod(method); if (urlParameters!=null) { byte[] postData = urlParameters.getBytes(StandardCharsets.UTF_8); int postDataLength = postData.length; conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); conn.setRequestProperty("charset", "utf-8"); conn.setRequestProperty("Content-Length", Integer.toString(postDataLength)); conn.setUseCaches(false); conn.getOutputStream().write(postData); } Reader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); StringBuilder sb = new StringBuilder(); for (int c; (c = in.read()) >= 0; ) sb.append((char) c); String responseBody = sb.toString(); int responseCode=conn.getResponseCode(); return new RequestResult(responseCode, responseBody); } public static RequestResult sendRequest(String method, String path) throws IOException { return sendRequest(method, path, null); } }
8.3 object populator - for testing : to "fill" test object with random generated data.
package com.demien.sparktest.util; import com.demien.sparktest.domain.IPersistable; import com.demien.sparktest.domain.Item; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Date; import java.util.List; public class ObjectPopulator { interface RandomGenerator { Object getRandomValue(); } enum DataType { Integer(() -> { return new Integer((int) (Math.random() * 1000)); }), Long(() -> { return new Long((long) (Math.random() * 1000)); }), Date(()-> { return new Date(new Date().getTime() - (int) (Math.random() * 1000 * 60 * 60 * 24 * 100)); }), String(() -> { StringBuffer result = new StringBuffer(); String[] letters = new String[]{"A", "B", "C", "D", "E", "F", "G"}; int length = (int) (Math.random() * 15) + 5; for (int i = 0; i < length; i++) { int pos = (int) (Math.random() * letters.length); result.append(letters[pos]); } return result.toString(); } ); private RandomGenerator generator; DataType(RandomGenerator generator) { this.generator = generator; } Object getRandomValue() { return generator.getRandomValue(); } } public static Object populate(IPersistable instance) throws IllegalAccessException { List<Field> fields = getAllFields(instance); for (Field eachField : fields) { eachField.setAccessible(true); String typeName=eachField.getType().getSimpleName(); if (eachField.getType().getTypeName().startsWith("com.demien")) { Object obj=null; try { obj=eachField.getType().newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } obj=populate((IPersistable) obj); eachField.set(instance, obj); } else { DataType dataType = DataType.valueOf(typeName); eachField.set(instance, dataType.getRandomValue()); } } return instance; } private static List<Field> getAllFields(Object instance) { Field[] fields = instance.getClass().getDeclaredFields(); List<Field> result = new ArrayList<Field>(); for (int i = 0; i < fields.length; i++) { if (!java.lang.reflect.Modifier.isFinal(fields[i].getModifiers()) && !java.lang.reflect.Modifier.isStatic(fields[i] .getModifiers())) { result.add(fields[i]); } } return result; } }
9. Generic integration test class
For base operations i created generic controller test class - other controllers will just extend it :package com.demien.sparktest; import com.demien.sparktest.domain.IPersistable; import com.demien.sparktest.service.GenericService; import com.demien.sparktest.util.JsonUtil; import com.demien.sparktest.util.ObjectPopulator; import com.demien.sparktest.util.RestTestUtil; import org.junit.*; import spark.Spark; import java.util.List; public abstract class GenericControllerIT<T extends IPersistable> { private String fullPath; private Class<T> cl; private GenericService<T> service; public GenericControllerIT(String basePath, Class<T> cl, GenericService<T> service){ this.fullPath= App.APP_PATH+basePath; this.cl=cl; this.service=service; } @BeforeClass public static void init() { App.main(null); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } @AfterClass public static void tearDown() { Spark.stop(); } @Before public void initTest() { service.clearStorage(); } T getTestObject() throws Exception { T testObject=cl.newInstance(); ObjectPopulator.populate(testObject); return testObject; } @Test public void addTest() throws Exception { T testObject=getTestObject(); RestTestUtil.RequestResult res= RestTestUtil.sendRequest("POST", fullPath, JsonUtil.toJson(testObject)); Assert.assertEquals(200, res.status); Assert.assertEquals(service.getById(testObject.getId()), testObject); } @Test public void getTest() throws Exception { T testObject=getTestObject(); service.add(testObject); RestTestUtil.RequestResult res= RestTestUtil.sendRequest("GET", fullPath +"/"+testObject.getId()); Assert.assertEquals(200, res.status); T receivedObject=(T)JsonUtil.toObject(res.body, cl); Assert.assertEquals(testObject, receivedObject); } @Test public void getAllTest() throws Exception { T testObject1=getTestObject(); service.add(testObject1); T testObject2=getTestObject(); service.add(testObject2); RestTestUtil.RequestResult res= RestTestUtil.sendRequest("GET", fullPath); Assert.assertEquals(200, res.status); List<T> receivedObjectList=(List<T>)JsonUtil.toObject(res.body, java.util.List.class); Assert.assertTrue(receivedObjectList.size()==2); } @Test public void updateTest() throws Exception { T testObject=getTestObject(); service.add(testObject); T updatedObject=getTestObject(); updatedObject.setId(testObject.getId()); RestTestUtil.RequestResult res= RestTestUtil.sendRequest("PUT", fullPath, JsonUtil.toJson(updatedObject)); Assert.assertEquals(200, res.status); T updatedObjectFromService=service.getById(testObject.getId()); Assert.assertEquals(updatedObject, updatedObjectFromService); } @Test public void deleteTest() throws Exception { T testObject=getTestObject(); service.add(testObject); RestTestUtil.RequestResult res= RestTestUtil.sendRequest("DELETE", fullPath, JsonUtil.toJson(testObject)); Assert.assertEquals(200, res.status); T deletedObject=service.getById(testObject.getId()); Assert.assertNull(deletedObject); } }
10. Integration test classes for ItemController and ParamController.
Now we can just extend GenericControlerIT class and all tests for base operation will be inherited :package com.demien.sparktest; import com.demien.sparktest.domain.Item; public class ItemControllerIT extends GenericControllerIT<Item> { public ItemControllerIT() { super(App.ITEM_PATH, Item.class, App.itemService); } }
package com.demien.sparktest; import com.demien.sparktest.domain.Param; public class ParamControllerIT extends GenericControllerIT<Param> { public ParamControllerIT() { super(App.PARAM_PATH, Param.class, App.paramService); } }