Руководство по JavaLite - Создание приложения RESTful CRUD

1. Вступление

  • JavaLite - это набор платформ для упрощения общих задач ** , с которыми приходится сталкиваться каждому разработчику при создании приложений.

В этом руководстве мы рассмотрим функции JavaLite, направленные на создание простого API.

2. Настроить

В этом руководстве мы создадим простое приложение RESTful CRUD. Для этого мы будем использовать ActiveWeb и ActiveJDBC - две платформы, с которыми интегрируется JavaLite.

Итак, давайте начнем и добавим первую зависимость, которая нам нужна:

<dependency>
    <groupId>org.javalite</groupId>
    <artifactId>activeweb</artifactId>
    <version>1.15</version>
</dependency>

ActiveWeb артефакт включает в себя ActiveJDBC, поэтому нет необходимости добавлять его отдельно. Обратите внимание, что можно найти самую последнюю версию activeweb версию. в Maven Central.

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.45</version>
</dependency>

И снова последний mysql-connector-java можно найти зависимость от Maven Central.

Последняя зависимость, которую мы должны добавить, - это что-то особенное для JavaLite:

<plugin>
    <groupId>org.javalite</groupId>
    <artifactId>activejdbc-instrumentation</artifactId>
    <version>1.4.13</version>
    <executions>
        <execution>
            <phase>process-classes</phase>
            <goals>
                <goal>instrument</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Последняя версия activejdbc-instrumentation также быть найденным в Maven Central.

Имея все это на месте, и прежде чем начинать с сущностей, таблиц и отображений, мы позаботимся о том, чтобы одна из supported database работала . Как мы уже говорили, мы будем использовать MySQL.

Теперь мы готовы начать с объектно-реляционного отображения.

3. Объектно-реляционное отображение

3.1. Картографирование и приборостроение

Давайте начнем с создания класса Product , который будет нашей основной сущностью ** :

public class Product {}

И давайте также создадим для него соответствующую таблицу :

CREATE TABLE PRODUCTS (
    id int(11) DEFAULT NULL auto__increment PRIMARY KEY,
    name VARCHAR(128)
);

Наконец, мы можем изменить наш класс Product для отображения :

public class Product extends Model {}

Нам нужно только расширить класс org.javalite.activejdbc.Model .

  • ActiveJDBC выводит параметры схемы БД из базы данных . Благодаря этой возможности нет необходимости добавлять геттеры и сеттеры или какие-либо аннотации ** .

Кроме того, ActiveJDBC автоматически распознает, что класс Product необходимо сопоставить с таблицей PRODUCTS . Это использует английские склонения, чтобы преобразовать единственную форму модели во множественную форму таблицы. И да, это работает с исключениями.

Есть одна последняя вещь, которая нам понадобится, чтобы наш картограф работал:

измерительные приборы. Инструментирование - это дополнительный шаг, требуемый ActiveJDBC , который позволит нам играть с нашим классом Product , как если бы он имел методы получения, установки и DAO-подобные методы.

После запуска инструментовки мы сможем сделать что-то вроде:

Product p = new Product();
p.set("name","Bread");
p.saveIt();

или же:

List<Product> products = Product.findAll();

Вот тут и появляется плагин activejdbc-instrumentation . Поскольку у нас уже есть зависимость в нашем pom, мы должны увидеть инструментирование классов во время сборки:

...[INFO]--- activejdbc-instrumentation:1.4.11:instrument (default) @ javalite ---
** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **  START INSTRUMENTATION ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
Directory: ...\tutorials\java-lite\target\classes
Instrumented class: .../tutorials/java-lite/target/classes/app/models/Product.class
** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **  END INSTRUMENTATION ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** ** **
...

Далее мы создадим простой тест, чтобы убедиться, что он работает.

3.2. Тестирование

Наконец, чтобы проверить наше сопоставление, мы выполним три простых шага: откройте соединение с базой данных, сохраните новый продукт и получите его:

@Test
public void givenSavedProduct__WhenFindFirst__ThenSavedProductIsReturned() {

    Base.open(
      "com.mysql.jdbc.Driver",
      "jdbc:mysql://localhost/dbname",
      "user",
      "password");

    Product toSaveProduct = new Product();
    toSaveProduct.set("name", "Bread");
    toSaveProduct.saveIt();

    Product savedProduct = Product.findFirst("name = ?", "Bread");

    assertEquals(
      toSaveProduct.get("name"),
      savedProduct.get("name"));
}

Обратите внимание, что все это (и даже больше) возможно только при наличии пустой модели и инструментов.

4. Контроллеры

Теперь, когда наше отображение готово, мы можем начать думать о нашем приложении и его методах CRUD.

Для этого мы собираемся использовать контроллеры, которые обрабатывают HTTP-запросы.

Давайте создадим наш ProductsController :

@RESTful
public class ProductsController extends AppController {

    public void index() {
       //...
    }

}

В этой реализации ActiveWeb автоматически сопоставит метод index () со следующим URI:

http://<host>:<port>/products

Контроллеры, помеченные @ RESTful , предоставляют фиксированный набор методов, автоматически сопоставляемых с различными URI. Давайте посмотрим, какие из них будут полезны для нашего примера CRUD:

| ================================================= ============= | Метод контроллера | Метод HTTP | URI | | CREATE | create () | POST | http://хост : порт/продукты | ЧИТАТЬ ОДИН | show () | GET | http://хост : порт/продукты/\ {id} | ЧИТАТЬ ВСЕ | index () | GET | http://хост : порт/products | UPDATE | update () | PUT | http://хост : порт/products/\ {id} | УДАЛИТЬ | destroy () | УДАЛИТЬ | http://хост : порт/продукты/\ {id} | ======================================= ============================

И если мы добавим этот набор методов в наш ProductsController :

@RESTful
public class ProductsController extends AppController {

    public void index() {
       //code to get all products
    }

    public void create() {
       //code to create a new product
    }

    public void update() {
       //code to update an existing product
    }

    public void show() {
       //code to find one product
    }

    public void destroy() {
       //code to remove an existing product
    }
}

Прежде чем перейти к нашей реализации логики, мы кратко рассмотрим несколько вещей, которые нам нужно настроить.

5. Конфигурация

ActiveWeb основан в основном на соглашениях, примером тому является структура проекта. Проекты ActiveWeb должны следовать предопределенному макету пакета :

src
 |----main
       |----java.app
       |     |----config
       |     |----controllers
       |     |----models
       |----resources
       |----webapp
             |----WEB-INF
             |----views

Есть один конкретный пакет, на который нам нужно взглянуть - a _pp.config _ .

Внутри этого пакета мы собираемся создать три класса:

public class DbConfig extends AbstractDBConfig {
    @Override
    public void init(AppContext appContext) {
        this.configFile("/database.properties");
    }
}
  • Этот класс настраивает соединения с базой данных ** , используя файл свойств в корневом каталоге проекта, содержащий обязательные параметры:

development.driver=com.mysql.jdbc.Driver
development.username=user
development.password=password
development.url=jdbc:mysql://localhost/dbname

Это создаст соединение, автоматически заменяющее то, что мы сделали в первой строке нашего теста отображения.

Второй класс, который нам нужно включить в пакет app.config :

public class AppControllerConfig extends AbstractControllerConfig {

    @Override
    public void init(AppContext appContext) {
        add(new DBConnectionFilter()).to(ProductsController.class);
    }
}
  • Этот код свяжет только что настроенное нами соединение с нашим контроллером. **

Третий класс будет настраивать контекст нашего приложения :

public class AppBootstrap extends Bootstrap {
    public void init(AppContext context) {}
}

После создания трех классов последнее, что касается конфигурации, это создание нашего файла web.xml в каталоге webapp/WEB-INF :

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns=...>

    <filter>
        <filter-name>dispatcher</filter-name>
        <filter-class>org.javalite.activeweb.RequestDispatcher</filter-class>
        <init-param>
            <param-name>exclusions</param-name>
            <param-value>css,images,js,ico</param-value>
        </init-param>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>dispatcher</filter-name>
        <url-pattern>/** </url-pattern>
    </filter-mapping>

</web-app>

Теперь, когда настройка завершена, мы можем продолжить и добавить нашу логику.

6. Внедрение CRUD Logic

Благодаря DAO-подобным возможностям, предоставляемым нашим классом Product , очень просто добавить базовые функции CRUD :

@RESTful
public class ProductsController extends AppController {

    private ObjectMapper mapper = new ObjectMapper();

    public void index() {
        List<Product> products = Product.findAll();
       //...
    }

    public void create() {
        Map payload = mapper.readValue(getRequestString(), Map.class);
        Product p = new Product();
        p.fromMap(payload);
        p.saveIt();
       //...
    }

    public void update() {
        Map payload = mapper.readValue(getRequestString(), Map.class);
        String id = getId();
        Product p = Product.findById(id);
        p.fromMap(payload);
        p.saveIt();
       //...
    }

    public void show() {
        String id = getId();
        Product p = Product.findById(id);
       //...
    }

    public void destroy() {
        String id = getId();
        Product p = Product.findById(id);
        p.delete();
       //...
    }
}

Легко, правда? Тем не менее, это еще ничего не возвращает. Для этого нам нужно создать несколько представлений.

7. Просмотры

  • ActiveWeb использует в качестве движка шаблонов, и все его шаблоны должны находиться в src/main/webapp/WEB-INF/views . **

Внутри этого каталога мы поместим наши представления в папку с именем products (так же, как наш контроллер). Давайте создадим наш первый шаблон с именем __product.ftl :

{
    "id" : ${product.id},
    "name" : "${product.name}"
}

На данный момент довольно ясно, что это ответ JSON. Конечно, это будет работать только для одного продукта, поэтому давайте продолжим и создадим еще один шаблон с именем index.ftl :

----[<@render partial="product" collection=products/>]----
  • Это в основном отобразит коллекцию с именем products , каждый из которых отформатирован с помощью __product.ftl . **

Наконец, нам нужно привязать результат от нашего контроллера к соответствующему представлению :

@RESTful
public class ProductsController extends AppController {

    public void index() {
        List<Product> products = Product.findAll();
        view("products", products);
        render();
    }

    public void show() {
        String id = getId();
        Product p = Product.findById(id);
        view("product", p);
        render("__product");
    }
}

В первом случае мы назначаем список products нашей коллекции шаблонов с именем products .

Тогда, поскольку мы не указываем никакого представления, будет использоваться index.ftl .

Во втором методе мы назначаем продукт p элементу product в представлении, и мы явно указываем, какое представление визуализировать.

Мы также можем создать представление message.ftl :

{
    "message" : "${message}",
    "code" : ${code}
}

И затем вызвать его из любого нашего метода ProductsController ‘s:

view("message", "There was an error.", "code", 200);
render("message");

Давайте теперь посмотрим наш последний ProductsController :

@RESTful
public class ProductsController extends AppController {

    private ObjectMapper mapper = new ObjectMapper();

    public void index() {
        view("products", Product.findAll());
        render().contentType("application/json");
    }

    public void create() {
        Map payload = mapper.readValue(getRequestString(), Map.class);
        Product p = new Product();
        p.fromMap(payload);
        p.saveIt();
        view("message", "Successfully saved product id " + p.get("id"), "code", 200);
        render("message");
    }

    public void update() {
        Map payload = mapper.readValue(getRequestString(), Map.class);
        String id = getId();
        Product p = Product.findById(id);
        if (p == null) {
            view("message", "Product id " + id + " not found.", "code", 200);
            render("message");
            return;
        }
        p.fromMap(payload);
        p.saveIt();
        view("message", "Successfully updated product id " + id, "code", 200);
        render("message");
    }

    public void show() {
        String id = getId();
        Product p = Product.findById(id);
        if (p == null) {
            view("message", "Product id " + id + " not found.", "code", 200);
            render("message");
            return;
        }
        view("product", p);
        render("__product");
    }

    public void destroy() {
        String id = getId();
        Product p = Product.findById(id);
        if (p == null) {
            view("message", "Product id " + id + " not found.", "code", 200);
            render("message");
            return;
        }
        p.delete();
        view("message", "Successfully deleted product id " + id, "code", 200);
        render("message");
    }

    @Override
    protected String getContentType() {
        return "application/json";
    }

    @Override
    protected String getLayout() {
        return null;
    }
}

На данный момент наше приложение готово, и мы готовы его запустить.

8. Запуск приложения

Мы будем использовать плагин Jetty:

<plugin>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>9.4.8.v20171121</version>
</plugin>

Найти последний jetty-maven -плагин в Maven Central.

И мы готовы, мы можем запустить наше приложение :

mvn jetty:run

Давайте создадим пару продуктов:

$ curl -X POST http://localhost:8080/products
  -H 'content-type: application/json'
  -d '{"name":"Water"}'
{
    "message" : "Successfully saved product id 1",
    "code" : 200
}
$ curl -X POST http://localhost:8080/products
  -H 'content-type: application/json'
  -d '{"name":"Bread"}'
{
    "message" : "Successfully saved product id 2",
    "code" : 200
}
  1. читать их:

$ curl -X GET http://localhost:8080/products[    {
        "id" : 1,
        "name" : "Water"
    },
    {
        "id" : 2,
        "name" : "Bread"
    }]----

.. обновить один из них:

[source,bash,gutter:,true]

$ curl -X PUT http://localhost:8080/products/1 -H 'content-type: application/json' -d '{"name":"Juice"}' { "message" : "Successfully updated product id 1", "code" : 200 }

... читать тот, который мы только что обновили:

[source,bash,gutter:,true]

$ curl -X GET http://localhost:8080/products/1 { "id" : 1, "name" : "Juice" }

Наконец, мы можем удалить один:

[source,bash,gutter:,true]

$ curl -X DELETE http://localhost:8080/products/2 { "message" : "Successfully deleted product id 2", "code" : 200 }

[[conclusions]]

===  **  9. Заключение**

JavaLite имеет множество инструментов, которые помогут разработчикам **  запустить приложение в считанные минуты ** . Однако, в то время как основанные на соглашениях результаты приводят к более чистому и простому коду, требуется некоторое время, чтобы понять наименование и расположение классов, пакетов и файлов.

Это было только знакомство с ActiveWeb и ActiveJDBC, вы можете найти дополнительную документацию на их http://javalite.io[веб-сайте]и найти приложение для наших продуктов в https://github.com/eugenp/tutorials/tree/master/java- lite[проект Github].