Ръководство за 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 извежда параметри на DB схемата от базата данни . Благодарение на тази възможност няма нужда да добавяте гетери и сетери или каквато и да е анотация .

Освен това ActiveJDBC автоматично разпознава, че класът Продукт трябва да бъде съпоставен с таблицата ПРОДУКТИ . Използва английски флексии, за да преобразува единична форма на модел в множествена форма на таблица. И да, работи и с изключения.

Има едно последно нещо, което ще ни трябва, за да накараме нашето картографиране да работи: инструментариум. Инструментариумът е допълнителна стъпка, изисквана от ActiveJDBC, която ще ни позволи да играем с нашия продуктов клас, сякаш има гетери, сетери и 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
СЪЗДАЙТЕ създаване () ПОСТ // хост: порт / продукти
ПРОЧЕТЕТЕ ЕДИН покажи () ВЗЕМЕТЕ // хост: порт / продукти / {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.