Перейти к основному содержимому

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

· 10 мин. чтения

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.

Имея все это на месте, и прежде чем приступить к сущностям, таблицам и сопоставлениям, мы удостоверимся, что одна из поддерживаемых баз данных запущена и работает . Как мы уже говорили, мы будем использовать 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**    |    | 
| СОЗДАЙТЕ | `Создайте()` | ПОЧТА | `http://хост:порт/продукты` |
| ПРОЧИТАЙТЕ ОДИН | `показывать()` | ПОЛУЧИТЬ | `http://хост:порт/продукты/{id}` |
| ПРОЧИТАТЬ ВСЕ | `индекс()` | ПОЛУЧИТЬ | `http://хост:порт/продукты` |
| ОБНОВИТЬ | `Обновить()` | ПОМЕЩАТЬ | `http://хост:порт/продукты/{id}` |
| УДАЛИТЬ | `разрушать()` | УДАЛИТЬ | `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

Нам нужно взглянуть на один конкретный пакет — 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-логики

Благодаря возможностям, подобным 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/>]

В основном это будет отображать коллекцию с именем 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}
}

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

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
}

.. читать их:

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