Посібник з JavaLite - Створення додатку RESTful CRUD

1. Вступ

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

У цьому підручнику ми розглянемо функції JavaLite, орієнтовані на побудову простого API.

2. Налаштування

У цьому посібнику ми створимо просту програму RESTful CRUD. Для цього ми використаємо ActiveWeb та ActiveJDBC - два фреймворки, з якими JavaLite інтегрується.

Отже, давайте почнемо і додамо першу залежність, яка нам потрібна:

 org.javalite activeweb 1.15 

Артефакт ActiveWeb включає ActiveJDBC, тому немає необхідності додавати його окремо. Зверніть увагу, що останню активну веб-версію можна знайти в Maven Central.

Другою залежністю, яка нам потрібна, є з'єднувач бази даних . У цьому прикладі ми будемо використовувати MySQL, тому нам потрібно додати:

 mysql mysql-connector-java 5.1.45 

Знову ж таки, останню залежність mysql-connector-java можна знайти на Maven Central.

Остання залежність, яку ми повинні додати, - це щось специфічне для JavaLite:

 org.javalite activejdbc-instrumentation 1.4.13   process-classes  instrument    

Останній плагін 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 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:

//:/products

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

Метод контролера HTTP-метод URI
СТВОРИТИ create () Опублікувати // хост: порт / продукти
ПРОЧИТАЙТЕ ОДИН показати () ОТРИМАТИ // хост: порт / продукти / {id}
ПРОЧИТАЙТЕ ВСЕ індекс () ОТРИМАТИ // хост: порт / продукти
ОНОВЛЕННЯ оновити () ВСТАНОВИТИ // хост: порт / продукти / {id}
ВИДАЛИТИ знищити () ВИДАЛИТИ // хост: порт / продукти / {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 is based mostly on conventions, project structure is an example of that. ActiveWeb projects need to follow a predefined package layout:

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

There's one specific package that we need to take a look at – app.config.

Inside that package we're going to create three classes:

public class DbConfig extends AbstractDBConfig { @Override public void init(AppContext appContext) { this.configFile("/database.properties"); } }

This class configures database connections using a properties file in the project's root directory containing the required parameters:

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

This will create the connection automatically replacing what we did in the first line of our mapping test.

The second class that we need to include inside app.config package is:

public class AppControllerConfig extends AbstractControllerConfig { @Override public void init(AppContext appContext) { add(new DBConnectionFilter()).to(ProductsController.class); } }

This codewill bind the connection that we just configured to our controller.

The third class willconfigure our app's context:

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

After creating the three classes, the last thing regarding configuration is creating our web.xml file under webapp/WEB-INF directory:

   dispatcher org.javalite.activeweb.RequestDispatcher  exclusions css,images,js,ico   encoding UTF-8    dispatcher /*  

Now that configuration is done, we can go ahead and add our logic.

6. Implementing CRUD Logic

With the DAO-like capabilities provided by our Product class, it's super simple to add basic CRUD functionality:

@RESTful public class ProductsController extends AppController { private ObjectMapper mapper = new ObjectMapper(); public void index() { List 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(); // ... } }

Easy, right? However, this isn't returning anything yet. In order to do that, we have to create some views.

7. Views

ActiveWeb uses FreeMarker as a templating engine, and all its templates should be located under src/main/webapp/WEB-INF/views.

Inside that directory, we will place our views in a folder called products (same as our controller). Let's create our first template called _product.ftl:

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

It's pretty clear at this point that this is a JSON response. Of course, this will only work for one product, so let's go ahead and create another template called index.ftl:

[]

This will basically render a collection named products, with each one formatted by _product.ftl.

Finally, we need to bind the result from our controller to the corresponding view:

@RESTful public class ProductsController extends AppController { public void index() { List products = Product.findAll(); view("products", products); render(); } public void show() { String id = getId(); Product p = Product.findById(id); view("product", p); render("_product"); } }

In the first case, we're assigning products list to our template collection named also products.

Then, as we're not specifying any view, index.ftl will be used.

In the second method, we're assigning product p to element product in the view and we're explicitly saying which view to render.

We could also create a view message.ftl:

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

And then call it form any of our ProductsController‘s method:

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

Let's now see our final 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; } }

At this point, our application is done and we're ready to run it.

8. Running the Application

We'll use Jetty plugin:

 org.eclipse.jetty jetty-maven-plugin 9.4.8.v20171121 

Find latest jetty-maven-plugin in Maven Central.

And we're ready, we can run our application:

mvn jetty:run

Let's create a couple of products:

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

.. read them:

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

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

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

... прочитайте ту, яку ми щойно оновили:

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

Нарешті, ми можемо видалити один:

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

9. Висновок

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

Це було лише вступом до ActiveWeb та ActiveJDBC, знайти більше документації на їх веб-сайті та шукати застосування наших продуктів у проекті Github.