Статьи

Приступая к работе с Spark: можно создать легкое RESTful-приложение также на Java

Недавно я писал сервис RESTful с использованием Spark , веб-фреймворка для Java (который не связан с Apache Spark). Когда мы планировали написать это, я был готов к неизбежной лавинской Javaesque- интерфейсам, стандартному коду и глубоким иерархиям. Я был очень удивлен, обнаружив, что альтернативный мир существует и для разработчиков, ограниченных Java.

В этом посте мы увидим, как создать приложение RESTful для блога, используя JSON для передачи данных. Посмотрим:

  • как создать простой Hello world в Spark
  • как указать расположение объекта JSON, ожидаемого в запросе
  • как отправить запрос на публикацию для создания нового сообщения
  • как отправить запрос на получение, чтобы получить список сообщений

Мы не будем видеть, как вставить эти данные в БД. Мы просто сохраним список в памяти (в моем реальном сервисе я использовал sql2o ).

Несколько зависимостей

Мы будем использовать Maven, поэтому я начну с создания нового pom.xml, добавив несколько вещей. В принципе:

  • искра
  • Джексон
  • Ломбок
  • гуайява
  • Easymock (используется только в тестах, не представлен в этом посте)
  • Gson
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<dependencies>
   <dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.12</version>
     <scope>test</scope>
   </dependency>
   <dependency>
     <groupId>com.sparkjava</groupId>
     <artifactId>spark-core</artifactId>
     <version>2.1</version>
   </dependency>
   <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-core</artifactId>
     <version>2.5.1</version>
   </dependency>
   <dependency>
     <groupId>com.fasterxml.jackson.core</groupId>
     <artifactId>jackson-databind</artifactId>
     <version>2.5.1</version>
   </dependency>
   <dependency>
     <groupId>org.projectlombok</groupId>
     <artifactId>lombok</artifactId>
     <version>1.16.2</version>
     <scope>provided</scope>
   </dependency>
     <dependency>
       <groupId>org.sql2o</groupId>
       <artifactId>sql2o</artifactId>
       <version>1.5.4</version>
     </dependency>
   <dependency>
     <groupId>org.postgresql</groupId>
     <artifactId>postgresql</artifactId>
     <version>9.4-1201-jdbc41</version>
   </dependency>
   <dependency>
     <groupId>com.google.guava</groupId>
     <artifactId>guava</artifactId>
     <version>18.0</version>
   </dependency>
   <dependency>
     <groupId>org.easymock</groupId>
     <artifactId>easymock</artifactId>
     <version>3.3.1</version>
     <scope>test</scope>
   </dependency>
   <dependency>
     <groupId>com.google.code.gson</groupId>
     <artifactId>gson</artifactId>
     <version>2.3.1</version>
     <scope>test</scope>
   </dependency>
 </dependencies>
  
 <build>
   <plugins>
     <plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-compiler-plugin</artifactId>
       <version>3.2</version>
       <configuration>
         <source>1.8</source>
         <target>1.8</target>
       </configuration>
     </plugin>
     <plugin>
       <groupId>org.codehaus.mojo</groupId>
       <artifactId>exec-maven-plugin</artifactId>
       <version>1.2.1</version>
       <configuration>
         <mainClass>me.tomassetti.BlogService</mainClass>
         <arguments>
         </arguments>
       </configuration>
     </plugin>
   </plugins>
 </build>

Искра Привет, мир

У вас есть все это? Круто, давайте напишем немного кода.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package me.tomassetti;
  
import static spark.Spark.get;
import static spark.Spark.post;
import spark.Request;
import spark.Response;
import spark.Route;
 
public class BlogService
{
    public static void main( String[] args) {
        get("/posts", (req, res) -> {
            return "Hello Sparkingly World!";
        });
    }
}

И теперь мы можем запустить его с чем-то вроде:

1
mvn compile && mvn exec:java

Давайте откроем браузер и зайдем на localhost http: // localhost: 4567 / posts . Здесь мы хотим сделать простое получение. Для выполнения постов вы можете использовать плагин Postman для вашего браузера или просто запустить curl . Все, что работает для вас.

Использование Джексона и Ломбока для потрясающих описательных объектов обмена

В типичном приложении RESTful мы ожидаем получать запросы POST с объектами json как часть полезной нагрузки. Наша работа будет состоять в том, чтобы проверить, правильно ли сформирован код JSON, соответствует ли он ожидаемой структуре, что значения находятся в допустимых диапазонах и т. Д. Вид скучен и повторен. Мы могли бы сделать это по-разному. Самый простой из них — использовать gson :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
JsonParser parser = new JsonParser();
JsonElement responseData = parser.parse(response);
if (!responseData.isJsonObject()){
  // send an error like: "Hey, you did not pass an Object!
}
JsonObject obj = responseData.getAsJsonObject();
if (!obj.hasField("title")){
  // send an error like: "Hey, we were expecting a field name title!
}
JsonElement titleAsElem = obj.get("title");
if (!titleAsElem.isString()){
  // send an error like: "Hey, title is not an string!
}
// etc, etc, etc

Мы, вероятно, не хотим этого делать.

Более декларативным способом указать, какую структуру мы ожидаем, является создание определенного класса.

01
02
03
04
05
06
07
08
09
10
11
12
class NewPostPayload {
   private String title;
   private List<String> categories;
   private String content;
    
   public String getTitle() { ... }
   public void setTitle(String title) { ... }
   public List<String> getCategories() { ... }
   public void setCategories(List<String> categories){ ... }
   public String getContent() { ... }
   public void setContent(String content) { ... }
}

И тогда мы могли бы использовать Джексона:

1
2
3
4
5
6
try {
   ObjectMapper mapper = new ObjectMapper();
   NewPostPayload newPost = mapper.readValue(request.body(), NewPostPayload.class);
} catch (JsonParseException e){
   // Hey, you did not send a valid request!
}

Таким образом, Джексон автоматически проверяет, имеет ли полезная нагрузка ожидаемую структуру. Мы могли бы проверить, соблюдаются ли дополнительные ограничения. Например, мы можем проверить, не является ли заголовок пустым и указана ли хотя бы одна категория. Мы могли бы создать интерфейс только для проверки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
interface Validable {
   boolean isValid();
}
 
class NewPostPayload implements Validable {
   private String title;
   private List<String> categories;
   private String content;
  
   public String getTitle() { ... }
   public void setTitle(String title) { ... }
   public List<String> getCategories() { ... }
   public void setCategories(List<String> categories){ ... }
   public String getContent() { ... }
   public void setContent(String content) { ... }
 
   public boolean isValid() {
      return title != null && !title.isEmpty() && !categories.isEmpty();
   }
}

Тем не менее у нас есть куча скучных добытчиков и сеттеров. Они не очень информативны и просто загрязняют код. Мы можем избавиться от них, используя Ломбок . Lombok — это процессор аннотаций, который добавляет вам повторяющиеся методы (методы получения, установки, равенства, hashCode и т. Д.). Вы можете думать об этом как о плагине для вашего компилятора, который ищет аннотации (например, @Data ) и генерирует методы на их основе. Если вы добавите его в свои зависимости, maven будет в порядке, но ваша IDE не сможет дать вам автозаполнение для методов, которые добавляет Lombok. Вы можете установить плагин. Для Intellij Idea я использую Lombok Plugin версии 0.9.1, и она прекрасно работает.

Теперь вы можете изменить класс NewPostPayload следующим образом:

01
02
03
04
05
06
07
08
09
10
@Data
class NewPostPayload {
   private String title;
   private List<String> categories;
   private String content;
    
   public boolean isValid() {
       return title != null && !title.isEmpty() && !categories.isEmpty();
   }
}

Намного лучше, а?

Полный пример

Нам нужно сделать в основном две вещи:

  1. вставить новый пост
  2. получить весь список сообщений

Первая операция должна быть реализована как POST (имеет побочные эффекты), а вторая — как GET. Они оба работают с коллекцией сообщений, поэтому мы будем использовать конечную точку / сообщения .

Начнем со вставки поста. Первым делом разберемся

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
// insert a post (using HTTP post method)
post("/posts", (request, response) -> {
    try {
        ObjectMapper mapper = new ObjectMapper();
        NewPostPayload creation = mapper.readValue(request.body(), NewPostPayload.class);
        if (!creation.isValid()) {
            response.status(HTTP_BAD_REQUEST);
            return "";
        }
        int id = model.createPost(creation.getTitle(), creation.getContent(), creation.getCategories());
        response.status(200);
        response.type("application/json");
        return id;
    } catch (JsonParseException jpe) {
        response.status(HTTP_BAD_REQUEST);
        return "";
    }
});

А затем посмотрим, как получить все сообщения:

1
2
3
4
5
6
// get all post (using HTTP get method)
get("/posts", (request, response) -> {
    response.status(200);
    response.type("application/json");
    return dataToJson(model.getAllPosts());
});

И окончательный код:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package me.tomassetti;
  
import static spark.Spark.get;
import static spark.Spark.post;
 
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import lombok.Data;
import spark.Request;
import spark.Response;
import spark.Route;
 
import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collector;
import java.util.stream.Collectors;
 
public class BlogService
{
 
    private static final int HTTP_BAD_REQUEST = 400;
     
    interface Validable {
        boolean isValid();
    }
 
    @Data
    static class NewPostPayload {
        private String title;
        private List<String> categories = new LinkedList<>();
        private String content;
 
        public boolean isValid() {
            return title != null && !title.isEmpty() && !categories.isEmpty();
        }
    }
     
    // In a real application you may want to use a DB, for this example we just store the posts in memory
    public static class Model {
        private int nextId = 1;
        private Map<Integer, Post> posts = new HashMap<>();
         
        @Data
        class Post {
            private int id;
            private String title;
            private List<String> categories;
            private String content;
        }
         
        public int createPost(String title, String content, List<String> categories){
            int id = nextId++;
            Post post = new Post();
            post.setId(id);
            post.setTitle(title);
            post.setContent(content);
            post.setCategories(categories);
            posts.put(id, post);
            return id;
        }
         
        public List<Post> getAllPosts(){
            return posts.keySet().stream().sorted().map((id) -> posts.get(id)).collect(Collectors.toList());
        }
    }
 
    public static String dataToJson(Object data) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            mapper.enable(SerializationFeature.INDENT_OUTPUT);
            StringWriter sw = new StringWriter();
            mapper.writeValue(sw, data);
            return sw.toString();
        } catch (IOException e){
            throw new RuntimeException("IOException from a StringWriter?");
        }
    }
     
    public static void main( String[] args) {
        Model model = new Model();
         
        // insert a post (using HTTP post method)
        post("/posts", (request, response) -> {
            try {
                ObjectMapper mapper = new ObjectMapper();
                NewPostPayload creation = mapper.readValue(request.body(), NewPostPayload.class);
                if (!creation.isValid()) {
                    response.status(HTTP_BAD_REQUEST);
                    return "";
                }
                int id = model.createPost(creation.getTitle(), creation.getContent(), creation.getCategories());
                response.status(200);
                response.type("application/json");
                return id;
            } catch (JsonParseException jpe) {
                response.status(HTTP_BAD_REQUEST);
                return "";
            }
        });
         
        // get all post (using HTTP get method)
        get("/posts", (request, response) -> {
            response.status(200);
            response.type("application/json");
            return dataToJson(model.getAllPosts());
        });
    }
}

Использование PostMan, чтобы попробовать приложение

Вы можете вместо этого использовать curl, если предпочитаете командную строку. Мне нравится не выходить из JSON и иметь базовый редактор, поэтому я использую PostMan (плагин Chrome).

Давайте вставим пост. Мы указываем все поля как часть объекта Json, вставленного в тело запроса. Мы возвращаем идентификатор созданного поста.

Screen-Shot-2015-03-30-в-17.25.22

Тогда мы можем получить список сообщений. В этом случае мы используем GET (без тела в запросе) и получаем данные всех постов (только тот, который мы вставили выше).

Screen-Shot-2015-03-30-в-17.30.33

Выводы

Я должен сказать, что я был положительно удивлен этим проектом. Я был готов к худшему: это такое приложение, которое требует элементарной логики и большого количества сантехники. Я обнаружил, что Python, Clojure и Ruby отлично справляются с подобными задачами, а во времена, когда я писал простые веб-приложения на Java, логика была утоплена в шаблонном коде. Ну, все может быть по-другому. Сочетание Spark, Lombok, Jackson и Java 8 действительно заманчиво. Я очень благодарен авторам этих частей программного обеспечения, они действительно улучшают жизнь разработчиков Java. Я считаю это также уроком: отличные фреймворки часто могут улучшить ситуацию гораздо больше, чем мы думаем.

Изменить: я получил предложение улучшить один из примеров от хороших людей на Reddit. Благодаря! Пожалуйста, продолжайте делать хорошие предложения!