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从数据库推断模式参数,因此无需添加getter/setter或任何注解

此外,ActiveJDBC自动将Product类映射到PRODUCTS表,通过英语复数转换规则实现。✅

还有一个关键步骤:增强(Instrumentation)。这是ActiveJDBC的特殊要求,让我们能像操作普通POJO一样使用Product类。

增强后可执行如下操作:

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

或:

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

这就是activejdbc-instrumentation插件的作用。添加依赖后,构建时会自动增强类:

...
[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
READ ONE show() GET
READ ALL index() GET
UPDATE update() PUT
DELETE destroy() DELETE

在ProductsController中添加这些方法:

@RESTful
public class ProductsController extends AppController {

    public void index() {
        // 获取所有产品
    }

    public void create() {
        // 创建新产品
    }

    public void update() {
        // 更新现有产品
    }

    public void show() {
        // 查询单个产品
    }

    public void destroy() {
        // 删除现有产品 
    }
}

实现逻辑前,先看几个需要配置的关键点。

5. 配置

ActiveWeb主要基于约定,项目结构就是典型例子。ActiveWeb项目必须遵循预定义的包结构

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

需要特别关注app.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

这将自动创建连接,替代我们在映射测试中的手动操作。

第二个类:

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) {}
}

创建这三个类后,最后一步是在webapp/WEB-INF目录下创建web.xml

<?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逻辑

利用Product类提供的DAO功能,添加基础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

第二种方法将产品p赋给视图的product元素,并显式指定渲染视图。

还可创建通用消息模板message.ftl

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

在控制器中调用:

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


原始标题:RESTFul CRUD application with JavaLite