.transform/.compose дублирует выполнение Mono с помощью Spring Security

При реализации решения для аутентификации на основе Spring Security Reactive я столкнулся с проблемой, когда операции в цепочке в какой-то момент дублируются. От того все вызывалось дважды.

Виновником оказался оператор .transform в одной точке цепочки. После редактирования вызываемого метода и замены оператора на .flatMap проблема решилась и все вызывалось только один раз.

Вопрос

Согласно документация оператора,

function is applied to an original operator chain at assembly time to augment it with the encapsulated operators

и

is basically equivalent to chaining the operators directly.

Тогда почему оператор .transform инициировал вторую подписку на цепочку?

Контекст

Этот поток проверки подлинности принимает имя доверенного пользователя и извлекает его данные из веб-службы.

Метод аутентификации для реализации ReactiveAuthenticationManager:

@Override
public Mono<Authentication> authenticate(Authentication providedAuthentication) {
    String username = (String) providedAuthentication.getPrincipal();
    String token = (String) providedAuthentication.getCredentials();

    return Mono.just(providedAuthentication)
            .doOnNext(x -> LOGGER.debug("Starting authentication of user {}", x))
            .doOnNext(AuthenticationValidator.validateProvided)
            .then(ReactiveSecurityContextHolder.getContext())
            .map(SecurityContext::getAuthentication)
            .flatMap(auth -> AuthenticationValidator.validateCoherence(auth, providedAuthentication))
            .switchIfEmpty(Mono.defer(() -> {
                LOGGER.trace("Switch if empty before retrieving user");
                return retrieveUser(username, token);
            }))
            .doOnNext(logAccess);
}

Дублирование звонков началось от поставщика .switchIfEmpty до конца цепочки.


Метод создания Mono, используемый .switchIfEmpty:

private Mono<PreAuthenticatedAuthenticationToken> retrieveUser(String username, String token) {
    return  Mono.just(username)
            .doOnNext(x -> LOGGER.trace("Before find by username"))
            .then(habileUserDetails.findByUsername(username, token))
            .cast(XXXUserDetails.class)
            .transform(rolesProvider::provideFor)
            .map(user -> new PreAuthenticatedAuthenticationToken(user, GlobalConfiguration.NO_CREDENTIAL, user.getAuthorities()))
            .doOnNext(s -> LOGGER.debug("User data retrieved from XXX"));
}

Оператор .transform в строке 4 был заменен на .flatMap для решения проблемы.


Оригинальный метод, вызываемый оператором .transform:

public Mono<CompleteXXXUserDetails> provideFor(Mono<XXXUserDetails> user) {
    return user
        .map(XXXUserDetails::getAuthorities)
        .map(l -> StreamHelper.transform(l, GrantedAuthority::getAuthority))
        .map(matcher::match)
        .map(enricher::enrich)
        .map(l -> StreamHelper.transform(l, SimpleGrantedAuthority::new))
        .zipWith(user, (authorities, userDetails)
            -> CompleteXXXUserDetails.from(userDetails).withAllAuthorities(authorities));
}

Вот след казни:

DEBUG 20732 --- [ctor-http-nio-3] c.a.s.s.h.a.XXXAuthenticationManager  : Starting authentication of user [REDACTED]
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.a.XXXAuthenticationManager  : Switch if empty before retrieving user
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.a.XXXAuthenticationManager  : Before find by username
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.xxx.user.UserRetriever        : Between request and call
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.u.retriever.UserRetrieverV01: Calling webservice v01
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.a.XXXAuthenticationManager  : Before find by username
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.xxx.user.UserRetriever        : Between request and call
TRACE 20732 --- [ctor-http-nio-3] c.a.s.s.h.u.retriever.UserRetrieverV01: Calling webservice v01

Для информации, я использую Spring Boot 2.1.2.RELEASE.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Версия Java на основе версии загрузки
Версия Java на основе версии загрузки
Если вы зайдете на официальный сайт Spring Boot , там представлен start.spring.io , который упрощает создание проектов Spring Boot, как показано ниже.
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
0
0
541
2

Ответы 2

Этот ответ не устраняет основную причину, а скорее объясняет, как transform может применяться несколько раз при подписке на несколько раз, что не относится к проблеме OP. Изменил исходный текст в цитату.

That statement is only valid when the transform is applied as a top-level operator in the chain you subscribe to. Here you are applying it within retrieveUser, which is invoked inside a Mono.defer (which goal is to execute that code for each different subscription). (edit:) so if that defer is subscribed to x times, the transform Function will be applied x times as well.

compose is basically transform-inside-a-defer by the way.

Спасибо Саймон. Имея это в виду, есть ли способ добиться ожидаемого поведения .transform на других уровнях? Я понимаю, что инструкции .defer выполняются при каждой подписке, но почему вообще существует дублированная подписка? Из-за поведения .transformнепредвиденный?

Adrien 06.02.2019 11:54
transform не вызывает подписки, поэтому он должен исходить откуда-то еще... если вы посмотрите на источник, flux.transform(function) - это просто function.apply(flux). возможно, в этом свете вы сможете понять, почему применение функции provideFor приведет к «дополнительным подпискам». Кстати, вы можете показать, как вы узнали об этих дополнительных подписках?
Simon Baslé 06.02.2019 12:08

Если вы посмотрите на трассировку выполнения, которую я предоставил, вы увидите, что последние 6 строк дублируются. В первом фрагменте кода вы можете увидеть строку журнала, которая соответствует второй строке трассировки выполнения. Что меня действительно беспокоит, так это то, что инструкция журнала, которая также есть в .defer, выполняется только один раз, а то, что после, выполняется дважды.

Adrien 06.02.2019 13:38

Вы также можете поделиться тем, как выглядел оригинальный provideFor?

Simon Baslé 06.02.2019 13:43

Это оригинал, до того, как я исправил проблему. Я редактирую вопрос, чтобы быть более ясным.

Adrien 06.02.2019 13:45

настоящая проблема заключается в другом, добавил ответ

Simon Baslé 06.02.2019 16:44

Проблема в том, что вы делаете user.whatever(...).zipWith(user, ...).

С преобразованием это переводится как:

Mono<XXXUserDetails> user = Mono.just(username)
    .doOnNext(x -> LOGGER.trace("Before find by username"))
    .then(habileUserDetails.findByUsername(username, token))
    .cast(XXXUserDetails.class);

return user.wathewer(...)
    .zipWith(user, ...);

Принимая во внимание, что с flatMap я предполагаю, что вы сделали что-то с эффектом flatMap(u -> provideFor(Mono.just(u))? Если это так, это будет означать:

Mono<XXXUserDetails> user = Mono.just(username)
    .doOnNext(x -> LOGGER.trace("Before find by username"))
    .then(habileUserDetails.findByUsername(username, token))
    .cast(XXXUserDetails.class);

return user.flatMap(u -> {
    Mono<XXXUserDetails> capture = Mono.just(u);
    return capture.whatever(...)
        .zipWith(capture, ...);
}

Вы можете видеть, как оба дважды подписываются на аMono<XXXUserDetails из-за zipWith.

Причина того, что кажется подписывается один раз с помощью flatMap, заключается в том, что он захватывает выходные данные восходящего конвейера и применяет функцию provideFor к этому захвату. Захват (Mono.just(u)) подписывается дважды, но действует как кеш и не несет никакой логики/логов/и т.д...

С transform захвата нет. Функция provideFor применяется непосредственно к восходящему конвейеру, что делает очевидным тот факт, что он подписывается дважды.

Другие вопросы по теме