Бизнес-логика
При взаимодействии с REST API часто требуется наличие бизнес-логики на уровне приложения, которая представляла бы собой точку вызова для API. Ее можно использовать для оркестрации, валидации и других задач, выполняющихся когда клиент API взаимодействует с системой. Entities API не допускает использования дополнительной бизнес-логики оркестрации при взаимодействии с API. Вместо этого клиент взаимодействует напрямую с уровнем доступа к данным Jmix.
В Jmix есть два способа предоставления бизнес-логики клиенту API:
В следующем разделе мы рассмотрим оба варианта и разницу между этими подходами.
Services API
Данный API позволяет предоставлять произвольный бин Spring в качестве конечной точки HTTP. В этом случае Jmix позаботится о таких взаимодействиях HTTP, как предоставление кодов ответов HTTP, обработка ошибок и т. д.
Ниже представлен общий вид взаимодействий между клиентом API и приложением Jmix при использовании Services API:
Предоставление сервиса
Чтобы использовать бин Spring в Jmix Services API, он должен соответствовать следующим критериям:
-
Бин должен иметь аннотацию Spring
@Service
(специализированная версия аннотации@Component
). -
Бин необходимо зарегистрировать в файле конфигурации
rest-services.xml
.
Рассмотрим первый критерий на следующем примере:
import org.springframework.stereotype.Service;
@Service("sample_OrderService") (1)
public class OrderService {
public BigDecimal calculateTotalAmount(int orderId) {
// ...
}
}
1 | OrderServiceBean зарегистрирован как @Service Spring с именем sample_OrderService . |
Если имя сервиса не указано в аннотации явно, предполагается, что оно равно простому имени класса с первой буквой в нижнем регистре. В приведенном выше примере это было бы orderService .
|
Для выполнения второго критерия нужно упомянуть все методы сервиса как конечные точки API. Это делается через файл конфигурации XML, обычно называемый rest-services.xml
. Вам нужно создать такой файл в 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 | Параметр должен быть описан своим именем и, что необязательно, своим типом. |
После создания файла и определения сервисов последним шагом является регистрация конфигурации rest-services.xml
в application.properties
вашго приложения Jmix:
jmix.rest.services-config = rest-services.xml
Значение serviceConfig является ссылкой на файл в пути к классу. В нашем случае файл находится непосредственно в корне пути к классу в разделе src/main/resources . Если поместить файл в, например, пакет src/resources/com/example/rest-services.xml , то значение будет таким: com/example/rest-services.xml .
|
Использование Services API
Как только вы предоставили сервис через Services API, вы сможете вызывать ее из клиента API с помощью HTTP GET или HTTP POST.
Вызов сервиса через GET
В случае использования HTTP GET необходимо предоставить значения параметров метода в качестве параметров запроса URL:
GET http://localhost:8080/rest
/services
/sample_OrderService
/calculateTotalAmount?orderId=123
Authorization: Bearer {{access_token}}
450.0
При использовании GET для вызова службы через Services API токен доступа OAuth по-прежнему должен быть предоставлен заголовком авторизации HTTP. Невозможно добавить токен доступа в качестве параметра запроса URL. |
Метод сервиса может вернуть результат простого типа данных, сущность, коллекцию сущностей или сериализуемый POJO. В нашем случае метод сервиса возвращает int
, поэтому тело ответа содержит только число.
Вызов сервиса через POST
В качестве альтернативы сервис можно также вызвать через HTTP 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;
}
}
Метод обладает следующей структурой для POJO OrderValidationResult
в качестве результирующего объекта:
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 возвращает сериализованный POJO OrderValidationResult
:
{
"success": false,
"errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01"
}
Передача параметров
Значения параметров должны передаваться в формате, определенном для соответствующего datatype.
-
Если тип параметра –
java.util.Date
, то значение обрабатываетDateTimeDatatype
. Эта реализация datatype использует формат 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;
-
Установка пользовательских заголовков ответов (например, для кэширования);
-
Создание собственных сообщений об ошибках исключений.
В таких ситуациях обычный Services API может оказаться недостаточно гибким. Поэтому Jmix позволяет интегрировать контроллеры Spring MVC в собственном коде в приложение Jmix.
Создание пользовательских контроллеров
Для создания контроллера требуется только создать в приложении Jmix бин Spring, аннотированный как контроллер Spring MVC. Сам Jmix не предъявляет никаких дополнительных требований к Spring MVC. Рассмотрим пример:
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController (1)
@RequestMapping("/orders") (2)
public class OrderController {
// ...
}
1 | Пользовательский контроллер помечен как @RestController чтобы указать Spring, что этот бин содержит операции HTTP. |
2 | Сопоставление запроса определяет базовый путь для этого контроллера. |
Теперь, когда контроллер 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 , указывающей, что он доступен через HTTP GET в подпути /calculateTotalAmount . |
2 | Параметр orderId извлекается через параметры запроса URL. |
3 | Класс Spring ResponseEntity можно использовать для указания ответа JSON вместе с различными аспектами HTTP. |
Более подробную информацию о различных аспектах создания контроллеров Spring MVC можно найти в руководстве Spring: Building a RESTful Web Service, а также в справочной документации по 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
}
Защита пользовательских контроллеров
Чтобы защитить пользовательский контроллер с помощью того же механизма OAuth2, который используют другие части Jmix REST API, зарегистрируйте шаблон URL-адреса контроллера в свойстве приложения jmix.rest.authenticated-url-patterns
:
jmix.rest.authenticated-url-patterns = /orders/**
Здесь подстановочный знак /orders/**
указывает Jmix, что все операции, начинающиеся с /orders/
также должны использовать механизм OAuth2.
Значение может содержать список шаблонов URL-адресов в стиле Apache Ant, разделенных запятыми. |
Теперь попытка вызвать Order Controller без действительного токена OAuth2 приводит к результату 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
:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private DataManager dataManager;
@GetMapping("/all")
public List<Order> loadAll() {
return dataManager.load(Order.class).all().list();
}
Если вы также хотите ограничить доступ к атрибутам сущностей, используйте бин EntitySerialization
для сериализации сущностей, возвращаемых из операции. В следующем примере только атрибуты, разрешенные политикой атрибутов сущности, будут возвращены в формате 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
);
}