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提供了丰富工具,帮助开发者在几分钟内启动应用。虽然基于约定能带来更简洁的代码,但理解类、包和文件的命名与位置规则需要一定学习成本。