Бизнес-логика
При взаимодействии с 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-бин должен быть создан с использованием специальных аннотаций. 
- 
Традиционный подход: Spring-бин должен соответствовать следующим критериям: - 
Бин должен иметь аннотацию Spring @Service(специализированная версия аннотации@Component).
- 
Бин необходимо зарегистрировать в файле конфигурации rest-services.xml.
 
- 
Давайте подробнее рассмотрим эти два метода.
С использованием аннотаций
Создайте класс сервиса и добавьте ему аннотацию @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, который принимает список возможных HTTP-методов для вызова сервисов через Generic REST API. По умолчанию он включаетGETиPOST. | 
С использованием rest-services.xml
Рассмотрим первый критерий на следующем примере:
import org.springframework.stereotype.Service;
@Service("sample_OrderService") (1)
public class OrderService {
    public BigDecimal calculateTotalAmount(int orderId) {
        // ...
    }
}| 1 | OrderServiceBeanзарегистрирован как@ServiceSpring с именем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.
Использование DTO в качестве параметров запроса
При использовании структурированного объекта в качестве параметра метода тело JSON-запроса должно включать имя параметра в качестве ключа верхнего уровня.
Например:
public Owner registerOwner(OwnerRegistration registration) {
    return dataManager.create(Owner.class);
}При следующем определении DTO:
public record OwnerRegistration(String firstName, String lastName) {}Запрос должен иметь следующую структуру:
{
  "registration": {
    "firstName": "John",
    "lastName": "Doe"
  }
}| Вместо POJO рекомендуется использовать DTO-сущности. Это предоставляет такие преимущества, как правильная генерация схемы OpenAPI и поддержка Bean Validation для валидации параметров запроса. | 
Пользовательские контроллеры
Второй способ представления бизнес-логики в виде 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.resource-server.authenticated-url-patterns:
jmix.resource-server.authenticated-url-patterns = /rest/**,/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
        );
    }