Бизнес-логика

При взаимодействии с REST API часто требуется наличие бизнес-логики на уровне приложения, которая представляла бы собой точку вызова для API. Ее можно использовать для оркестрации, валидации и других задач, выполняющихся когда клиент API взаимодействует с системой. Entities API не допускает использования дополнительной бизнес-логики оркестрации при взаимодействии с API. Вместо этого клиент взаимодействует напрямую с уровнем доступа к данным Jmix.

В Jmix есть два способа предоставления бизнес-логики клиенту API:

В следующем разделе мы рассмотрим оба варианта и разницу между этими подходами.

Services API

Данный API позволяет предоставлять произвольный бин Spring в качестве конечной точки HTTP. В этом случае Jmix позаботится о таких взаимодействиях HTTP, как предоставление кодов ответов HTTP, обработка ошибок и т. д.

Ниже представлен общий вид взаимодействий между клиентом API и приложением Jmix при использовании Services API:

business logic diagram

Предоставление сервиса

Чтобы использовать бин Spring в Jmix Services API, он должен соответствовать следующим критериям:

  1. Бин должен иметь аннотацию Spring @Service (специализированная версия аннотации @Component).

  2. Бин необходимо зарегистрировать в файле конфигурации rest-services.xml.

Рассмотрим первый критерий на следующем примере:

CalculationServiceBean.java
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 вашего приложения. В нем перечислены все методы сервиса с информацией о параметрах, которые вы хотите предоставить.

rest-services.xml
<?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:

application.properties
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:

Calculate Total Order Amount via HTTP GET
GET http://localhost:8080/rest
            /services
            /sample_OrderService
            /calculateTotalAmount?orderId=123
Authorization: Bearer {{access_token}}
Response: 200 - OK
450.0
При использовании GET для вызова службы через Services API токен доступа OAuth по-прежнему должен быть предоставлен заголовком авторизации HTTP. Невозможно добавить токен доступа в качестве параметра запроса URL.

Метод сервиса может вернуть результат простого типа данных, сущность, коллекцию сущностей или сериализуемый POJO. В нашем случае метод сервиса возвращает int, поэтому тело ответа содержит только число.

Вызов сервиса через POST

В качестве альтернативы сервис можно также вызвать через HTTP POST. Это особенно полезно, когда метод сервиса имеет один из следующих типов параметров:

  • Сущности

  • Коллекции сущностей

  • Сериализуемые POJO

Предположим, мы добавили новый метод в OrderService, созданный в предыдущей части:

OrderServiceBean.java
@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 в качестве результирующего объекта:

OrderValidationResult.java
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:

Invoke Order Validation via HTTP POST
POST http://localhost:8080/rest/services/sales_OrderService/validateOrder

{
  "order" : {
    "number": "00050",
    "date" : "2016-01-01"
  },
  "validationDate": "2016-10-01"
}

Метод REST API возвращает сериализованный POJO OrderValidationResult:

Response: 200 - OK
{
  "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. Рассмотрим пример:

OrderController.java
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:

OrderController.java
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. Пример взаимодействия с контроллером:

Invoke Custom Orders Controller
GET http://localhost:8080/orders/calculateTotalAmount?orderId=123

Ответ содержит результат вычисления, представленный в виде JSON, а также определенные заголовки HTTP:

Response: 200 - OK
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:

application.properties
jmix.rest.authenticated-url-patterns = /orders/**

Здесь подстановочный знак /orders/** указывает Jmix, что все операции, начинающиеся с /orders/ также должны использовать механизм OAuth2.

Значение может содержать список шаблонов URL-адресов в стиле Apache Ant, разделенных запятыми.

Теперь попытка вызвать Order Controller без действительного токена OAuth2 приводит к результату HTTP 401 - Unauthorized:

Response: 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
        );
    }