1. Введение

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

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

2. Настройка

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

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

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

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

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

Опять же, последнюю версию зависимости mysql-connector-java можно найти на Maven Central.

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

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

Последний подключаемый модуль activejdbc-instrumentation также можно найти в Maven Central.

<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>

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

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

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

3.1. Отображение и инструментирование

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

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

public class Product {}

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

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

Нам нужно только расширить класс org.javalite.activejdbc.Model. ActiveJDBC выводит параметры схемы БД из базы данных. Благодаря этой возможности нет необходимости добавлять геттеры и сеттеры или какие-либо аннотации.

public class Product extends Model {}

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

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

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

или:

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

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

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

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

...
[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:

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

@RESTful
public class ProductsController extends AppController {

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

}

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

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

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

Controller method HTTP method URI
CREATE create() POST http://host:port/products
READ ONE show() GET http://host:port/products/{id}
READ ALL index() GET http://host:port/products
UPDATE update() PUT http://host:port/products/{id}
DELETE destroy() DELETE http://host:port/products/{id}

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

@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 должны следовать предопределенной структуре пакетов:

Есть один конкретный пакет, на который нам нужно обратить внимание — app.config.

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

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

«

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

Благодаря возможностям, подобным 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 использует FreeMarker в качестве механизма шаблонов, и все его шаблоны должны располагаться в папке src/main/webapp/WEB-INF/views.

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

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

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

[<@render partial="product" collection=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.

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

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

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

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

А затем вызвать его из любого метода нашего ProductController:

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-plugin в 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
}

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

.. прочитаем их:

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

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

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

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

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

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

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

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

Это было только введение в ActiveWeb и ActiveJDBC, дополнительную документацию можно найти на их веб-сайте, а приложение для наших продуктов — в проекте Github.