业务逻辑
服务 API
第一种开放业务逻辑 API 的方法是:服务 API。
服务 API 可以将任意 Spring bean 作为 HTTP 接口开放。Jmix 负责 HTTP 交互,例如,提供 HTTP 响应编码、进行错误处理等。
在下面的流程图中,可以看到使用服务 API 时,API 客户端和 Jmix 应用程序之间的交互过程:
服务 API 开放
如需将一个 Spring bean 作为 Jmix 服务 API 的一部分,需要满足下列条件之一:
-
新的基于注解的试验方式:Spring bean 中必须使用新的基于注解的方法。
-
传统方法:Spring bean 必须满足下列条件:
-
Spring bean 需要使用 Spring 的
@Service
注解(@Component
注解的特殊版本)。 -
Spring bean 需要在
rest-services.xml
配置文件中注册。
-
Let’s examine these two methods in more detail.
使用注解
这是试验性的 API,将来有可能更改或删除。使用风险自担。 |
创建 Spring bean,且使用 @RestService
注解。
import io.jmix.rest.annotation.RestMethod;
import io.jmix.rest.annotation.RestService;
import java.math.BigDecimal;
@RestService("sample_OrderService") (1)
public class OrderService {
@RestMethod (2)
public BigDecimal calculateTotalAmount(int orderId) {
// ...
}
}
1 | @RestService 注解用于标记服务类,表示可以通过 REST API 访问。 |
2 | @RestMethod 注解用于配置服务方法和特定的 REST 端点。可以使用 httpMethods 参数指定通过通用 REST API 可以使用的 HTTP 方法。默认支持 GET 和 POST 。 |
使用 rest-services.xml
先看第一个条件,示例:
import org.springframework.stereotype.Service;
@Service("sample_OrderService") (1)
public class OrderService {
public BigDecimal calculateTotalAmount(int orderId) {
// ...
}
}
1 | OrderServiceBean 使用 sample_OrderService 名称注册为 Spring @Service 。 |
如果未在注解中显式指定服务名称,则服务名为第一个字母小写的类名称。例如,上面示例中,服务名是 orderService 。
|
第二部分是需要将服务网的方法作为 API 接口描述。这通过 XML 配置文件完成,一般命名为 rest-services.xml
。需要在 Jmix 项目的 src/main/resources
目录手动创建。文件中列举所有需要开放的服务方法,并提供方法的参数信息。
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://jmix.io/schema/rest/services">
<service name="sample_OrderService"> (1)
<method name="calculateTotalAmount"> (2)
<param name="orderId"/> (3)
</method>
</service>
</services>
1 | 用服务的 Spring 组件名注册 |
2 | 每个需要开放的方法都在这里描述 |
3 | 方法参数需要提供名称和类型(类型可选) |
服务类完成且该配置文件也创建好后,最后一步是在 Jmix 项目的 application.properties
文件中注册 rest-services.xml
:
jmix.rest.services-config = rest-services.xml
services-config 的值是 classpath 中文件的引用。我们的例子中,文件位于 classpath 的根目录 src/main/resources 。如果你的文件放置于 src/resources/com/example/rest-services.xml ,则配置值为:com/example/rest-services.xml 。
|
使用服务 API
我们通过服务 API 开放服务方法之后,可以用 API 客户端进行调用。可以用 HTTP GET
或 POST
。
用 GET 调用服务
使用 HTTP GET 时,方法参数通过 URL 查询参数提供:
GET http://localhost:8080/rest
/services
/sample_OrderService
/calculateTotalAmount?orderId=123
Authorization: Bearer {{access_token}}
450.0
当使用 GET 方法调用通过服务 API 调用服务时,仍需要在 HTTP 的 Authorization 请求头提供 OAuth 访问 token。不支持使用 URL 查询参数提供访问 token。 |
一个服务方法可以返回一个简单类型数据、实体、实体集合或者可序列化的 POJO。上面的例子中,服务方法返回一个 int
,因此响应体中仅包含一个数字。
用 POST 调用服务
或者,可以通过 HTTP POST 调用服务。当服务方法有下列类型的参数时,推荐使用 POST 方法:
-
实体
-
实体集合
-
可序列化的 POJO
假设我们为 OrderService 添加了一个新的方法:
@Service("sales_OrderService")
public class OrderService {
public OrderValidationResult validateOrder(Order order, Date validationDate){
OrderValidationResult result = new OrderValidationResult();
result.setSuccess(false);
result.setErrorMessage("Validation of order " + order.getNumber() + " failed. validationDate parameter is: " + validationDate);
return result;
}
}
使用如下结构的 OrderValidationResult
POJO 作为结果对象:
import java.io.Serializable;
public class OrderValidationResult implements Serializable {
private boolean success;
private String errorMessage;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
新方法接收 Order 实体作为参数,并返回一个 POJO。在调用 REST API 之前,新方法也需要在 rest-services.xml
中注册。完成接口开放之后,可以执行 API 调用:
POST http://localhost:8080/rest/services/sales_OrderService/validateOrder
{
"order" : {
"number": "00050",
"date" : "2016-01-01"
},
"validationDate": "2016-10-01"
}
REST API 方法返回一个序列化的 OrderValidationResult
POJO:
{
"success": false,
"errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01"
}
参数传递
参数值格式必须符合对应 数据类型 的要求。
-
如果参数类型是
java.util.Date
,值的格式由DateTimeDatatype
处理。此数据类型的实现使用 ISO_DATE_TIME 格式进行解析,其中日期和时间部分以T
分隔,例如,2011-12-03T10:15:30
。 -
对于
java.sql.Date
参数类型,值的格式DateDatatype
处理。使用 ISO_DATE 格式,例如,2011-12-03
。 -
对于
java.sql.Time
参数类型,值的格式TimeDatatype
处理。使用 ISO_TIME 格式,例如,10:15:30
。
自定义控制器
开放业务逻辑 API 的第二个方法是使用自定义 HTTP 控制器。主要的不同点是,在这种情况下,可以自己干预 HTTP 的交互(比如状态码、安全等)。Jmix 使用 Spring MVC 的默认机制创建 HTTP 接口。
自定义控制器的使用场景可以是:
-
需要显式定义 HTTP 状态码
-
使用除 JSON 外的其他请求和响应类型
-
设置自定义的响应头(例如,支持缓存)
-
为异常创建自定义的错误消息
这些场景中,通用服务 API 可能不够灵活,难以满足要求。因此,Jmix 支持原生集成 Spring MVC 控制器。
创建自定义控制器
如需创建自定义控制器,只需要在 Jmix 应用程序中创建 Spring MVC 控制器的 bean 即可。Jmix 本身没有其他的额外要求。看一个控制器示例:
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController (1)
@RequestMapping("/orders") (2)
public class OrderController {
// ...
}
1 | 自定义控制器带 @RestController 注解 |
2 | RequestMapping 定义此控制器的基础路径 |
现在 Spring 控制器注册好了,我们可以创建一个方法,用于开放特定的 HTTP 接口:
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/calculateTotalAmount") (1)
public ResponseEntity<OrderTotalAmount> calculateTotalAmount(
@RequestParam int orderId (2)
) {
BigDecimal totalAmount = orderService.calculateTotalAmount(orderId);
return ResponseEntity (3)
.status(HttpStatus.OK)
.header(HttpHeaders.CACHE_CONTROL, "max-age=31536000")
.body(new OrderTotalAmount(totalAmount, orderId));
}
}
1 | calculateTotalAmount 方法使用了 @GetMapping 注解,表示可以通过 /calculateTotalAmount 子路径使用 HTTP GET 访问。 |
2 | 参数 orderId 需要从 URL 查询参数获取。 |
3 | 我们可以使用 Spring 的 ResponseEntity 类封装 JSON 响应体,并使用 HTTP 的其他特性。 |
关于如何创建 Spring MVC 控制器的更多内容可以参阅 Spring 的指南: 构建 RESTful Web 服务,以及 Spring MVC 的 参考文档。
有了上面的控制器后,Jmix 可以提供此 HTTP 服务了。我们看看如何与控制器交互:
GET http://localhost:8080/orders/calculateTotalAmount?orderId=123
响应包含计算结果的 JSON 对象以及定义的 HTTP 响应头:
HTTP/1.1 200
Cache-Control: max-age=31536000
Content-Type: application/json
{
"orderId": 123,
"totalAmount": 450.0
}
保护自定义控制器
如需为自定义控制器提供与 Jmix REST API 一样的 OAuth2 保护机制,在 jmix.rest.authenticated-url-patterns
应用程序属性中注册控制器的 URL pattern:
jmix.rest.authenticated-url-patterns = /orders/**
这里的 /orders/**
通配符表示任何以 /orders/
开头的接口都使用 OAuth2 保护机制。
此配置的值支持以逗号分隔的 Apache Ant style URL patterns 列表。 |
如果现在尝试不使用有效的 OAuth2 token 调用 Order 控制器的方法,结果会是 HTTP 401 - Unauthorized
:
HTTP/1.1 401
WWW-Authenticate: Bearer realm="oauth2-resource", error="unauthorized", error_description="Full authentication is required to access this resource"
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
认证后的接口可以使用 Jmix 安全 子系统提供的数据访问控制。如果你的控制器使用 DataManager 读写数据,会自动检查认证用户的 实体操作 权限。下面示例中,如果用户没有对 Order
实体的读权限,会抛出 "Access denied(拒绝访问)" 异常:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private DataManager dataManager;
@GetMapping("/all")
public List<Order> loadAll() {
return dataManager.load(Order.class).all().list();
}
如需限制为实体属性的访问,需要使用 EntitySerialization
bean 将接口返回的实体进行序列化。下面示例中,返回客户端的 JSON 中只带有经过 实体属性策略 许可的实体属性。
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private DataManager dataManager;
@Autowired
private EntitySerialization entitySerialization;
@GetMapping("/all")
public String loadAll() {
List<Order> orders = dataManager.load(Order.class).all().list();
return entitySerialization.toJson(
orders,
null,
EntitySerializationOption.DO_NOT_SERIALIZE_DENIED_PROPERTY
);
}