Статьи

Расширение и переопределение потоков из внешних приложений CorDapps

Corda 4 была выпущена на прошлой неделе (21 февраля) и принесла массу новых функций, с которыми Corda стало приятнее работать. Честно говоря, я предполагаю, что есть много новых функций. Я быстро просмотрел список изменений, главным образом, чтобы увидеть ссылки на мои материалы, но я помню, что видел много строк текста. Это должно быть хорошо, верно?

В любом случае, одной из этих функций является возможность расширять и переопределять потоки. На самом деле, это не кажется очень причудливым, когда вы понимаете, что Corda написана на Kotlin и в ней полностью заложено наследство (верно и для Java). Но это еще не все. Корда должна сопоставить инициирующий поток с потоком контрагента, который на него реагирует.

Это нормально, когда две стороны используют один и тот же CorDapp. В этой ситуации нет дополнительной сложности. Если, с другой стороны, контрагент хотел отправить некоторые данные во внешнюю систему после получения транзакции, как он мог это сделать? Оригинальный CorDapp не знает и не заботится об этой системе, и поэтому не может удовлетворить эти потребности. Возможность решения такого рода проблем позволяет разработчикам опираться на существующие CorDapps и адаптировать их, чтобы они были более подходящими для их варианта использования. Кроме того, после того, как выложены передовые практики, расширение сторонних приложений CorDapps станет проще и избавит группы разработчиков от необходимости постоянно изобретать велосипед, когда другие уже решили часть проблемы. Очевидно, это предполагает, что есть доступ к этим внешним CorDapps, но это полностью в сфере возможностей. Особенно с R3 Marketplace, уже демонстрирующей коллекцию.

В этом посте мы сосредоточимся на расширении и переопределении потоков. Кроме того, мы возьмем точку зрения двух разных точек зрения.

  • Разработчик / сопровождающий CorDapp
  • Разработчик, желающий использовать и адаптировать существующий CorDapp

Чтобы процесс работал, обе стороны должны приложить усилия для написания своих приложений надлежащим образом, чтобы можно было использовать преимущества.

Мы начнем с рассмотрения того, что должен содержать оригинальный CorDapp, и того, что должен сделать разработчик, чтобы расширить его.

Прежде чем мы продолжим, вот ссылка на официальную документацию по расширению и переопределению потоков .

Написание базового потока для расширения

Написание CorDapp таким образом, чтобы его можно было легко расширять, вероятно, потребует разумного количества размышлений. Это во многом зависит от того, чего пытается достичь сопровождающий CorDapp. Предоставление разработчикам возможности расширить CorDapp, чтобы они могли отправлять данные во внешние системы или добавлять свои собственные журналы, не должно создавать проблем. С другой стороны, для того, чтобы разрешить изменение содержимого транзакции или кому она отправляется, потребуется больше усилий, чтобы убедиться, что CorDapp не используется не по назначению. Это тема, которую я надеюсь изучить в следующих статьях.

Для целей этого поста мы рассмотрим более простой вариант. Давайте сразу перейдем к тому, что текста было много, а кода нет. Ниже приведен SendMessageFlow который будет действовать как «базовый» поток, который будет расширен в следующем разделе:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@InitiatingFlow
open class SendMessageFlow(private val message: MessageState) :
  FlowLogic<SignedTransaction>() {
 
  open fun preTransactionBuild() {
    // to be implemented by sub type flows - otherwise do nothing
  }
 
  open fun preSignaturesCollected(transaction: SignedTransaction) {
    // to be implemented by sub type flows - otherwise do nothing
  }
 
  open fun postSignaturesCollected(transaction: SignedTransaction) {
    // to be implemented by sub type flows - otherwise do nothing
  }
 
  open fun postTransactionCommitted(transaction: SignedTransaction) {
    // to be implemented by sub type flows - otherwise do nothing
  }
 
  @Suspendable
  final override fun call(): SignedTransaction {
    logger.info("Started sending message ${message.contents}")
    preTransactionBuild()
    val tx = verifyAndSign(transaction())
    preSignaturesCollected(tx)
    val sessions = listOf(initiateFlow(message.recipient))
    val stx = collectSignature(tx, sessions)
    postSignaturesCollected(stx)
    return subFlow(FinalityFlow(stx, sessions)).also {
      logger.info("Finished sending message ${message.contents}")
      postTransactionCommitted(it)
    }
  }
 
  // collectSignature
 
  // verifyAndSign
 
  // transaction
}

Я удалил несколько функций, чтобы мы могли сосредоточиться на том, что важно.

Первый и иногда важный шаг, позволяющий расширить этот класс, — это тот факт, что он open . Это скорее вещь Kotlin, чем Java, так как все классы в Kotlin являются final по умолчанию. Если вы пишете это на Java, просто игнорируйте последние несколько предложений!

Исходя из этого, есть ряд функций, которые доступны для переопределения. Каждая функция была размещена в соответствующем месте внутри основного исполнения потока. Затем они будут вызваны при запуске потока. На данный момент им даны пустые реализации, так как они не используют разработчика CorDapp.

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

Раскопав немного подробнее. Функция call сделана final (такой же, как в Java), чтобы предотвратить переопределение всего содержимого потока. Если кто-то хочет взять ваш Flow и полностью заменить его «основные» функции, то какой в ​​этом смысл? Мне кажется, это выглядит довольно хитро. Устранить эту возможность, сделав ее final — разумный шаг.

Позже мы рассмотрим, как этот поток может быть разделен на подклассы.

Ниже приведен SendMessageResponder который взаимодействует с SendMessageFlow . Он следует тем же понятиям, что и выше, и поэтому я покажу его только в качестве ссылки для дальнейшего:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@InitiatedBy(SendMessageFlow::class)
open class SendMessageResponder(private val session: FlowSession) : FlowLogic<Unit>() {
 
  open fun postTransactionSigned(transaction: SignedTransaction) {
    // to be implemented by sub type flows - otherwise do nothing
  }
 
  open fun postTransactionCommitted(transaction: SignedTransaction) {
    // to be implemented by sub type flows - otherwise do nothing
  }
 
  @Suspendable
  final override fun call() {
    val stx = subFlow(object : SignTransactionFlow(session) {
      override fun checkTransaction(stx: SignedTransaction) {}
    })
    postTransactionSigned(stx)
    val committed = subFlow(
      ReceiveFinalityFlow(
        otherSideSession = session,
        expectedTxId = stx.id
      )
    )
    postTransactionCommitted(committed)
  }
}

Расширение существующего инициирующего потока

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

Давайте начнем с расширения инициирующего потока. Требования для этого следующие:

  • Расширить базу @InitiatingFlow
  • Не добавляйте @InitiatingFlow в новый поток (в противном случае будут возникать ошибки)
  • Ссылка на конструктор базового потока ( super в Java)
  • Переопределите любые желаемые функции
  • Вызовите новый поток вместо базового потока

Прочитав этот список, вы, возможно, поняли, что это в значительной степени описание наследования в объектно-ориентированных языках (таких как Kotlin и Java). Возможно, внутри Corda будет происходить больше, чтобы это работало, но с вашей точки зрения вы просто пишете нормальный объектно-ориентированный код, как обычно.

Принимая эти требования, мы видим, как может выглядеть расширенный поток:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@StartableByRPC
class CassandraSendMessageFlow(private val message: MessageState) :
  SendMessageFlow(message) {
 
  override fun preTransactionBuild() {
    serviceHub.cordaService(MessageRepository::class.java).save(
      message,
      sender = true,
      committed = false
    )
    logger.info("Starting transaction for message: $message")
  }
 
  override fun preSignaturesCollected(transaction: SignedTransaction) {
    val keys = transaction.requiredSigningKeys - ourIdentity.owningKey
    logger.info("Collecting signatures from $keys for transaction for message: $message")
  }
 
  override fun postSignaturesCollected(transaction: SignedTransaction) {
    logger.info("Collected signatures for transaction for message: $message")
  }
 
  override fun postTransactionCommitted(transaction: SignedTransaction) {
    serviceHub.cordaService(MessageRepository::class.java).save(
      message,
      sender = true,
      committed = true
    )
    logger.info("Committed transaction for message: $message")
  }
}

Я оставил во всех шумных функциях, которые реализуют дополнительную отслеживаемость, о которой я говорил, но это связано с тем, насколько пустым был бы класс без них. Поскольку call не должен быть реализован. Этот поток нужен только для переопределения open функций. Если честно, их вообще не нужно переопределять, они необязательны. При желании этот поток может переопределить одну функцию, а затем оставить ее пустой.

Были ли выполнены все перечисленные выше требования?

  • CassandraSendMessageFlow расширяет SendMessageFlow
  • Там нет @InitiatingFlow в поле зрения
  • В Kotlin вы все равно должны вызывать super конструктор, так что все готово
  • В этом сценарии все функции переопределены
  • У нас не так далеко

Хорошо, так что пока 4/5. Это довольно хорошее начало. Чтобы вычеркнуть последний элемент в списке, нам нужно посмотреть, как он называется. Ниже приведены фрагменты, которые вызывают базовый SendMessageFlow и расширяющий поток CassandraSendMessageFlow .

Начиная с SendMessageFlow :

1
proxy.startFlow(::SendMessageFlow, messageState)

Далее следует CassandraSendMessageFlow :

1
proxy.startFlow(::CassandraSendMessageFlow, messageState)

Заметили разницу? В этом сценарии изменилось только имя потока. Ничего больше.

Оба фрагмента полностью действительны. Вызов оригинального SendMessageFlow все еще разрешен. Помните, с нашей точки зрения, это просто обычный объектно-ориентированный код. У него не будет сложного дополнительного кода, добавляемого в расширяющийся поток, но он все равно будет выполняться без проблем. Завершение этого шага соответствует последнему требованию по расширению @InitiatingFlow .

Прежде чем мы закончим этот раздел, вот важная информация, которую нужно запомнить из документации Corda :

«Вы должны убедиться, что последовательность посылок / приёмов / вложенных потоков в подклассе совместима с родителем».

Я расскажу об этом во всех следующих разделах, поскольку несоблюдение этого приведет к сбою ваших потоков.

Расширение потока ответчика

Расширение потока ответчика работает очень похоже на расширение потока @InitiatingFlow . Разница лишь в том, как это называется. Как указано в документации :

«Corda обнаружит, что и BaseResponder и SubResponder настроены для ответа инициатору. Затем Corda рассчитает переходы к FlowLogic и выберет реализацию, которая является самым дальним расстоянием, то есть: реализацию с наибольшим подклассом ».

Утверждение «самый подкласс» является важным выводом из этого текста. Поэтому, с точки зрения разработчика, все, что им нужно сделать, — это расширить внешний базовый поток ответчика и все. Мне очень понравился предыдущий список требований, поэтому давайте рассмотрим еще один для расширения потоков ответчиков:

  • Расширить базовый @InitiatedBy / Responder
  • Добавьте @InitiatedBy в новый поток
  • Ссылка на конструктор базового потока ( super в Java)
  • Переопределите любые желаемые функции

Если вы бдительны, вы могли заметить, что нет упоминания о том, как это назвать. Расширяющий поток ответчика не нужно ни вызывать, ни ссылаться на него где-либо еще. Корда сделает все, чтобы направить все в нужное место.

Просто чтобы быть уверенным, давайте кратко рассмотрим пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@InitiatedBy(SendMessageFlow::class)
class CassandraSendMessageResponder(session: FlowSession) :
  SendMessageResponder(session) {
 
  override fun postTransactionSigned(transaction: SignedTransaction) {
    val message = transaction.coreTransaction.outputsOfType<MessageState>().single()
    logger.info("Signed transaction for message: $message")
  }
 
  override fun postTransactionCommitted(transaction: SignedTransaction) {
    val message = transaction.coreTransaction.outputsOfType<MessageState>().single()
    serviceHub.cordaService(MessageRepository::class.java).save(
      message,
      sender = false,
      committed = true
    )
    logger.info("Committed transaction for message: $message")
  }
}

Кроме того, давайте еще раз вернемся к утверждению «самый подкласс». CassandraSendMessageResponder является подклассом SendMessageResponder и поэтому выбирается Corda для обработки запросов из Инициирующего потока. Но это может быть сделано еще дальше. Если был другой класс, скажем, SuperSpecialCassandraSendMessageResponder , то этот поток — это то, что Corda начнет использовать. Хотя в настоящий момент я считаю такой сценарий несколько маловероятным, об этом определенно стоит знать.

Скопируйте и вставьте это утверждение снова, чтобы не забыть:

«Вы должны убедиться, что последовательность посылок / приёмов / вложенных потоков в подклассе совместима с родителем».

Переопределение потока ответчика

Это нарочно отдельный раздел. Здесь мы поговорим конкретно о переопределении потока ответчика, а не его расширении. Зачем ты это делаешь и в чем разница? Отвечая на первый вопрос, разработчик может захотеть написать поток ответчика, который сильно отличается от исходного базового потока, но все же должен взаимодействовать с конкретным инициирующим потоком, предоставляемым внешним CorDapp. Чтобы достичь этого, они могут переопределить Поток. Другое слово для описания этого может быть «заменить». Исходный базовый поток полностью заменен перекрывающим потоком. В этой ситуации расширение участия не предусмотрено.

Я думаю, что документация Corda по этому вопросу довольно хороша:

«Хотя подход с использованием подклассов, вероятно, будет полезен для большинства приложений, существует другой механизм, позволяющий переопределить это поведение. Это было бы полезно, если, например, конкретный пользователь CordApp требует такого другого респондента, что создание подклассов существующего потока не будет хорошим решением ».

Надеюсь, этот фрагмент вместе с моим более ранним описанием прояснит разницу между расширением и переопределением потоков ответчика.

Итак, как может выглядеть перекрывающий поток? Ну, все, что вы хотите на самом деле, в пределах разумного. Возможно, это может выглядеть так, как показано ниже, хотя я сомневаюсь в этом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@InitiatedBy(SendMessageFlow::class)
class OverridingResponder(private val session: FlowSession) :
  FlowLogic<Unit>() {
 
  @Suspendable
  override fun call() {
    val stx = subFlow(object : SignTransactionFlow(session) {
      override fun checkTransaction(stx: SignedTransaction) {}
    })
    logger.info("Screw the original responder. I'll build my own responder... with blackjack and hookers!")
    subFlow(
      ReceiveFinalityFlow(
        otherSideSession = session,
        expectedTxId = stx.id
      )
    )
  }
}

Поскольку этот поток полностью заменяет исходный базовый поток, он будет выглядеть как обычный поток ответчика. Так как, ну, это один. Это означает, что он имеет @InitiatedBy ссылающийся на инициирующий поток, расширяет FlowLogic и реализует функцию call .

Просто поместите это здесь в последний раз:

«Вы должны убедиться, что последовательность посылок / приёмов / вложенных потоков в подклассе совместима с родителем».

Это даже более распространено здесь, чем в предыдущих разделах. Поскольку вся функция call переопределяется, вы должны убедиться, что каждый send и receive находятся в нужном месте, чтобы взаимодействия с инициирующим потоком выполнялись без ошибок.

В настройке есть немного больше, чем расширение потока. В этой ситуации мы пытаемся полностью заменить ответчика другим. Для этого нам нужен способ указать узлу перенаправить взаимодействия из инициирующего потока в новый переопределяющий поток ответчика. Корда предоставляет способ сделать это.

Чтобы указать перенаправление, добавьте следующее в ваш node.conf :

1
2
3
4
5
6
7
8
flowOverrides {
  overrides=[
    {
      initiator="com.lankydanblog.tutorial.base.flows.SendMessageFlow"
      responder="com.lankydanblog.tutorial.cassandra.flows.OverridingResponder"
    }
  ]
}

Очевидно, измените классы, на которые ссылаются ваши собственные …

и так, что здесь происходит? Конфигурация говорит, что SendMessageFlow который обычно взаимодействует с SendMessageResponder , теперь будет вместо этого направлять к OverridingResponder .

Чтобы сделать все немного проще, плагин Cordform предоставляет метод flowOverride как часть deployNodes . Затем сгенерированный выше блок конфигурации для вас. В приведенном выше примере использовался следующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
node {
  name "O=PartyA,L=London,C=GB"
  p2pPort 10002
  rpcSettings {
    address("localhost:10006")
    adminAddress("localhost:10046")
  }
  rpcUsers = [[user: "user1", "password": "test", "permissions": ["ALL"]]]
  cordapp(project(':cordapp-contracts-states'))
  cordapp(project(':cordapp'))
  cordapp(project(':cordapp-extended-cassandra'))
  // the important part
  flowOverride("com.lankydanblog.tutorial.base.flows.SendMessageFlow",
    "com.lankydanblog.tutorial.cassandra.flows.OverridingResponder")
}

Теперь, после deployNodes и запуска вашего узла, любые запросы, поступающие от SendMessageFlow или любого из его подклассов, будут теперь направлять связь к OverridingResponder .

Вывод

Одной из удобных функций, которые предоставляет Corda 4, является возможность настраивать потоки из сторонних CorDapps (или ваших собственных). Это делается двумя способами: расширение или переопределение.

Расширение было бы моим первым выбором между двумя, но это требует немного больше усилий со стороны разработчика CorDapp. Они должны предоставить достаточно возможностей для настройки, не отказываясь от контроля первоначальной функциональности своих потоков. Предоставление небольшого количества настроек не может помешать другим разработчикам использовать их CorDapp. Но разработчики могут стать недовольными отсутствием контроля над собственным приложением. Это скользкий путь для контроля первоначального замысла с помощью маршрутов для настройки. С другой стороны, фактическое расширение потока не требует большой работы, что облегчает разработчикам принятие и адаптацию внешних потоков.

Переопределение, с другой стороны, не требует работы для разработчика CorDapp, и вместо этого все откладывается на разработчика, используя внешние потоки ответчика. Это связано с тем, что существующий поток в значительной степени отбрасывается, и единственной ссылкой на исходную реализацию является ссылка на инициирующий поток.

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

Код, используемый в этом посте, можно найти на моем GitHub . Он содержит код для CassandraSendMessageFlow который устанавливает соединение с внешней базой данных Cassandra для сохранения данных стиля трассировки. Он также содержит другой модуль, который отправляет HTTP-запросы как часть своего расширения базовых потоков. Если после прочтения этого поста вам все еще интересно, этот репозиторий может помочь.

Если вам понравился этот пост или вы нашли его полезным (или и тем, и другим), пожалуйста, не стесняйтесь, следите за мной в Твиттере на @LankyDanDev и не забудьте поделиться с кем-либо, кто может найти это полезным!

Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . Смотрите оригинальную статью здесь: Расширение и переопределение потоков из внешних CorDapps

Мнения, высказанные участниками Java Code Geeks, являются их собственными.