java mono что это

Java и Project Reactor

Всем привет! Меня зовут Лёха, и я работаю бэкенд-разработчиком в FunCorp. Сегодня мы поговорим про реактивное программирование, библиотеку Reactor и немного про веб.

Реактивное программирование часто «подвергается упоминанию», но если вы (как и автор статьи) всё ещё не знаете, что это такое — устраивайтесь поудобнее, попробуем разобраться вместе.

Что же такое реактивное программирование?

Реактивное программирование — это управление асинхронными потоками данных. Вот так просто. Мы люди нетерпеливые и не вникаем во все эти ваши манифесты с подробностями, а стоило бы.

Причём тут веб?

Ходят слухи, что если выстраивать свою систему реактивно, согласно всем канонам Reactive Manifesto, начиная с HTTP-сервера и заканчивая драйвером БД, можно вызвать второе пришествие. Ну, или хотя бы построить действительно качественный бэкенд.

Это, конечно, лёгкое лукавство. Но если ваш юзкейс — обработка множественных и не всегда быстрых запросов, а контейнер сервлетов перестаёт справляться — добро пожаловать в прекрасный мир реактивного!

If you have 128 continuous parallel requests, a servlet container is probably not the right tool for the job.

А на чём писать реактивно, если не на Netty? Стоит отметить, что написание бэкенда на голом Netty утомительно, и приятно иметь абстракции для работы.

Годных серверных абстракций для Netty много не бывает, поэтому ребята из Pivotal добавили в Spring Boot 2 его поддержку. 1 марта 2018 года всё это даже зарелизилось. Чтобы сделать нам совсем приятно, они создали модуль WebFlux, который является альтернативой Spring MVC и представляет собой реактивный подход для написания веб-сервисов.

WebFlux позиционирует себя как микрофреймворк (микрофреймворк и Spring, ха-ха), обещает вписаться в эти ваши (наши) модные микросервисы, представляет API в функциональном стиле и уже упоминался на Хабре. Более подробно (в т.ч. об отличиях от Spring MVC) можно почитать здесь. Но сегодня о другом. В основе WebFlux лежит библиотека Reactor. О ней и поговорим.

Reactor — это реактивная (внезапно!) open-source-платформа, разрабатываемая Pivotal. Я решился на вольный пересказ (с комментариями) введения в эту замечательную библиотеку.

Blocking code (для самых маленьких)

Программный код на языке Java обычно блокирующий. Например, вызовы по HTTP или запросы к БД вешают наш текущий поток до момента, пока нам не ответит сторонний сервис. Это нормальная практика, если сервис отвечает за приемлемое время. В противном случае это дело превращается в bottleneck. Мы вынуждены его распараллелить, запускать больше потоков, которые будут выполнять один и тот же блокирующий код. Попутно приходится решать возникающие проблемы с contention и конкурентностью.

Частое блокирование, особенно из-за I/O (а уж если у вас много мобильных клиентов, то совсем не быстрого I/O), заставляет наши многочисленные потоки просиживать штаны в ожидании данных, тратя драгоценные ресурсы на переключение контекста и всякое такое.

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

Async && non-blocking

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

Из текста выше можно сделать вывод, что во всём виноват блокирующий код. Окей, давайте начнём писать неблокирующий. Что под этим подразумевается? Если мы ещё не готовы отдать результат, то вместо его ожидания мы отдаём какую-то ошибку, например, с просьбой повторить запрос позднее. Круто, конечно, но что нам с этой ошибкой делать? Так у нас появляется асинхронная обработка, чтобы позднее отреагировать на ответ: всё готово!

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

Так вот, Reactor. Если очень коротко

По факту, Reactor (по-крайней мере его core-часть) — это имплементация спецификации Reactive Streams и части ReactiveX-операторов. Но об этом чуть позже.

Если вы знакомы или наслышаны о RxJava, то Reactor разделяет подход и философию RxJavа, но имеет ряд семантических отличий (которые растут из-за backward compatibility со стороны RxJava и особенностей Android-разработки).

Что же такое Reactive Streams в Java?

Если грубо, то это 4 интерфейса, которые представлены в библиотеке reactive-streams-jvm:

Их точные копии присутствуют в классе Flow из девятки.

Если ещё более грубо, то к ним всем выдвигаются примерно следующие требования:

Давайте взглянем на код класса Flow из JDK 9 (Javadoc-комментарии убраны для лаконичности):

Пока что это вся поддержка реактивности на уровне JDK. Где-то в модуле инкубатора зреет HTTP/2-клиент, в котором Flow активно используется. Других использований внутри JDK 9 я не обнаружил.

Интеграции

Reactor интегрирован в наши любимые приблуды Java 8, среди которых CompletableFuture, Stream, Duration. Поддерживает IPC-модули. У него есть адаптеры для Akka и RxJava, модули test (очевидно, для написания тестов) и extra (utility классы).

Для любителей Redis у клиентов lettuce/redisson есть реактивный API с поддержкой Reactor.
Для любителей MongoDB есть официальный реактивный драйвер, который имплементирует Reactive Streams, в связи с чем его легко подхватывает Reactor.

Отлично, а как всё это запустить?

Всё это можно запустить на JDK8 и выше. Однако, если вы используете Android и ваш (minSdk Если у вас Maven

BOM служит для обеспечения лучшей совместимости различных кусков Reactor. В Gradle нет нативной поддержки BOM, поэтому нужен плагин.

Примерчики

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

На солнечном острове под названием Java для это есть два основных способа:

Callbacks. В случае с колбэками у метода нет возвращаемого значения (void), но он принимает дополнительный параметр (лямбду, анонимный класс и т.п.), который вызывается после определённого события. В качестве примера можно привести EventListener из библиотеки Swing.

Это хорошо известные инструменты, но в какой-то момент их становится недостаточно.

Проблемы с колбэками

Колбэки плохо поддаются композиции и быстро превращаются в мешанину под названием «callback hell».

Разберём на примере

Нужно показать пользователю 5 топовых мемов, а если их нет, то сходить в сервис предложений и взять 5 мемов оттуда.

Итого задействовано 3 сервиса: первый отдаёт ID любимых мемов пользователя, второй фетчит сами мемы, а третий отдаёт предложения, если любимых мемов не имеется.

Выглядит как-то не круто.

Теперь посмотрим, как мы бы сделали это с Reactor

А что если мы вдруг захотели отваливаться по тайм-ауту в 800 мс и загружать кэшированные данные?

В Reactor мы просто добавляем в цепочку вызовов оператор timeout. Timeout выбрасывает исключение. Оператором onErrorResume мы указываем альтернативный (fallback) источник, из которого нужно взять данные в случае ошибки.

Колбэки в 20!8, у нас же есть CompletableFuture

У нас есть список ID, по которым мы хотим запросить имя и статистику, а затем скомбинировать в виде пар «ключ-значение», и всё это асинхронно.

Как мы можем сделать это с Reactor?

В итоге нам предоставляется высокоуровневый API, композируемый и читабельный (на самом деле изначально мы использовали Reactor именно для этого, т.к. нужен был способ писать асинхронный код в едином стиле), и прочие вкусности: ленивое выполнение, управление BackPressure, различные планировщики (Schedulers) и интеграции.

Окей, какие ещё Flux и Mono?

Flux и Mono — это две основные структуры данных Reactor.

Flux — это имплементация интерфейса Publisher, представляет из себя последовательность из 0..N элементов, которая может (но не обязательно) завершаться (в т.ч. и с ошибкой).

У последовательности Flux есть 3 допустимых значения: объект последовательности, сигнал завершения или сигнал ошибки (вызовы методов onNext, onComplete и onError соответственно).

Каждое из 3 значений опционально. К примеру, Flux может представлять из себя бесконечную пустую последовательность (ни один метод не вызывается). Или конечную пустую последовательность (вызывается только onComplete). Или бесконечную последовательность значений (вызывается только onNext). И т.д.

Читайте также:  iastorv в реестре что это

Выведет следующий текст:

Метод doOnEach(Consumer ) применяет сайд-эффект к каждому элементу в последовательности, что удобно для логирования.

Обратите внимание на blockLast(): т.к. последовательность бесконечная, поток, в котором происходит вызов, будет бесконечно ждать окончания.

Если вы знакомы с RxJava, то Flux очень похож на Observable

Mono — это имплементация интерфейса Publisher, представляет из себя какой-то асинхронный элемент или его отсутствие Mono.empty().

В отличии от Flux, Mono может вернуть не более 1 элемента. Вызовы onComplete() и onError(), как и в случае с Flux, опциональны.

Mono также может использоваться как какая-то асинхронная задача в стиле «выполнил и забыл», без возвращаемого результата (похоже на Runnable). Для этого можно объявить его как Mono и использовать оператор empty.

Если вы знакомы с RxJava, воспринимайте Mono как коктейль из Single + Maybe

К чему это разделение?

Разделение на Flux и Mono помогает улучшить семантику реактивного API, делая его достаточно выразительным, но не избыточным.

В идеале, просто посмотрев на возвращаемое значение, мы можем понять, что делает метод: какой-то вызов (Mono ), запрос-ответ (Mono ) или возвращает нам поток данных (Flux ).

Также у них есть уникальные операторы. Некоторые имеют смысл только при N элементах в последовательности (Flux) или, наоборот, актуальны только для одного значения. Например, у Mono есть or(Mono ), а у Flux есть операторы limit/take.

Ещё примерчики

Самый простой способ создать Flux/Mono — воспользоваться одним из массы фабричных методов, которые представлены в этих классах.

Источник

Реактивное программирование со Spring, часть 2 Project Reactor

Это вторая часть серии заметок о реактивном программировании, в которой представлен обзор Project Reactor, реактивной библиотеки, основанной на спецификации Reactive Streams.

1. Введение в Project Reactor

Реактивное программирование поддерживается Spring Framework, начиная с версии 5. Эта поддержка построена на основе Project Reactor.

1.1 Модули Reactor

Проект Reactor состоит из набора модулей, перечисленных в документации Reactor. Модули встраиваемы и совместимы. Основным артефактом является Reactor Core, который содержит реактивные типы Flux и Mono, которые реализуют интерфейс Publisher Reactive Stream (подробности см. в первом сообщении этой серии) и набор операторов, которые могут применяться к ним.

Некоторые другие модули:

1.2 Настройка проекта

Прежде чем мы продолжим, если вы хотите настроить проект и запустить некоторые из приведенных ниже примеров кода, сгенерируйте новое приложение Spring Boot с помощью Spring Initializr. В качестве зависимости выберите Spring Reactive Web. После импорта проекта в вашу среду IDE взгляните на файл POM, и вы увидите, что добавлена ​​зависимость spring-boot-starter-webflux, которая также внесет зависимость ядра-реактора. Также в качестве зависимости добавлен тест-реактор. Теперь вы готовы к запуску следующих примеров кода.

2. Возможности Reactor Core

Reactor Core определяет реактивные типы Flux и Mono.

2.1 FLUX и MONO

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

Метод just создает поток, который испускает предоставленные элементы, а затем завершается. Ничего не передается, пока кто-нибудь на это не подпишется. Чтобы подписаться на него, мы вызываем метод subscribe и в этом случае просто распечатываем отправленные элементы. Создание Mono также может быть выполнено с помощью метода just, с той лишь разницей, что разрешен только один параметр.

2.2 Объединение операторов

Взгляните на Flux API, и вы увидите, что почти все методы возвращают Flux или Mono, что означает, что операторы могут быть связаны. Каждый оператор добавляет поведение к Publisher (Flux или Mono) и переносит Publisher предыдущего шага в новый экземпляр. Данные поступают от первого издателя и перемещаются по цепочке, трансформируясь каждым оператором. В конце концов, подписчик завершает процесс. Обратите внимание, что ничего не происходит, пока подписчик не подпишется на издателя.

Существует оператор log(), который обеспечивает регистрацию всех сигналов Reactive Streams, происходящих за кулисами. Просто измените последнюю строку приведенного выше примера на

и перезапустите тест. Теперь вы увидите, что к выходным данным добавляется следующее:

Теперь, чтобы увидеть, что произойдет, если вы исключите вызов subscribe(), снова измените последнюю строку кода на следующую и повторно запустите тест:

2.3 Поиск подходящего оператора

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

КАТЕГОРИЯ ОПЕРАТОРА

ПРИМЕРЫ

Создание новой последовательности

just, fromArray, fromIterable, fromStream

Преобразование существующей последовательности

map, flatMap, startWith, concatWith

Заглядывать в последовательность

doOnNext, doOnComplete, doOnError, doOnCancel

filter, ignoreElements, distinct, elementAt, takeLast

onErrorReturn, onErrorResume, retry

Работаем со временем

elapsed, interval, timestamp, timeout

buffer, groupBy, window

Возвращаясь к синхронному миру

block, blockFirst, blockLast, toIterable, toStream

Многоадресная рассылка потока нескольким подписчикам

publish, cache, replay

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

Или оператор zip, который объединяет несколько источников вместе (ожидая, пока все источники испускают один элемент, и объединяет их в кортеж):

3. Обработка ошибок

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

Результат будет выглядеть так:

Также можно обрабатывать ошибки в середине реактивной цепочки, используя операторы обработки ошибок:

Метод onErrorReturn будет выдавать резервное значение, когда наблюдается ошибка указанного типа. Это можно сравнить с перехватом исключения и возвратом статического запасного значения в императивном программировании. См. Пример ниже:

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

4. Тестирование

Модуль Reactor Test предоставляет служебные программы, которые могут помочь в тестировании поведения вашего Flux или Mono. В этом помогает API StepVerifier. Вы создаете StepVerifier и передаете его издателю для тестирования. StepVerifier подписывается на Publisher при вызове метода verify, а затем сравнивает выданные значения с вашими определенными ожиданиями.

См. следующий пример:

Для объекта создается StepVerifier, fluxCalc и определяются два ожидания: сначала ожидается, что будет выдана одна String, а затем должна быть выдана ошибка с типом ArithmeticException. С помощью вызова verify StepVerifier начинает подписываться на Flux, и инициируется поток.

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

Модуль Reactor Test также предоставляет другой API, TestPublisher который представляет собой Publisher, которым вы можете напрямую управлять, инициируя события onNext, onComplete и onError для целей тестирования.

5. Модель параллелизма

Есть два способа переключения контекста выполнения в реактивной цепочке: publishOn и subscribeOn. Отличается следующее:

publishOn(Scheduler scheduler) влияет на выполнение всех последующих операторов (если не указано иное)

subscribeOn(Scheduler scheduler) изменяет поток, из которого подписывается вся цепочка операторов, на основе самого раннего вызова subscribeOn в цепочке. Это не влияет на поведение последующих вызовов publishOn

Класс Schedulers содержит статические методы, чтобы обеспечить контекст выполнения, например:

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

Взглянув на вывод (показан ниже), вы можете увидеть, что первая и вторая операции map выполняются в потоке из планировщика A, поскольку первый subscribeOn в цепочке переключается на этот планировщик, и это влияет на всю цепочку. Перед третьей операцией map выполняется publishOn, переключающий контекст выполнения на Scheduler B, в результате чего третья и четвертая операции map выполняются в этом контексте (поскольку вторая subscribeOn не будет иметь никакого эффекта). И, наконец, есть новый метод publishOn, который переключает обратно на Планировщик A перед последней операцией map.

Читайте также:  что делать если в фотоаппарат попала вода

6. BACKPRESSURE (противодавление)

В приведенном ниже примере показано, как подписчик может контролировать скорость передачи, вызывая request(n) метод в Subscription.

Запустите его, и вы увидите, что по запросу одновременно генерируются два значения:

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

7. Холодные и горячие Publisher

Холодный Publisher генерирует новые данные для каждой подписки. Если подписки нет, данные никогда не генерируются. Напротив, hot издатель не зависит от подписчиков. Он может начать публикацию данных без подписчиков. Если подписчик подписывается после того, как издатель начал передавать значения, он получит только значения, выпущенные после его подписки.

Давайте посмотрим на простой холодный и горячий Publisher, чтобы увидеть различное поведение. В приведенном ниже примере coldPublisherExample оператор interval используется для создания потока, который генерирует значения long, начинающиеся с 0.

При запуске будет получен следующий результат:

Теперь вы можете задаться вопросом, почему что-то происходит, когда основной поток спит, но это потому, что оператор интервала по умолчанию выполняется в планировщике Schedulers.parallel(). Как видите, оба подписчика получат значения, начинающиеся с 0.

Теперь давайте посмотрим, что происходит, когда мы используем ConnectableFlux:

На этот раз мы получаем следующий результат:

Как мы видим, на этот раз ни один из подписчиков не получает исходные значения 0 и 1. Они получают значения, которые отправляются после подписки. Вместо того, чтобы вручную запускать публикацию, с помощью этого autoConnect(n) метода также можно настроить ConnectableFlux так, чтобы он запускался после n подписок.

8. Прочие возможности

8.1 Завершение синхронного, блокирующего вызова

Когда необходимо использовать источник информации, который является синхронным и блокирующим, в Reactor рекомендуется использовать следующий шаблон:

Метод fromCallable создает Mono, который производит его значение с помощью прилагаемого Callable. Используя Schedulers.boundedElastic(), мы гарантируем, что каждая подписка выполняется на выделенном однопоточном работнике, не влияя на другую неблокирующую обработку.

8.2 Контекст

Иногда возникает необходимость передать некоторые дополнительные, обычно технические данные, через реактивный конвейер. Сравните это с привязкой некоторого состояния к потоку с помощью ThreadLocal в императивном мире.

Контекст может быть заполнен во время подписки путем добавления вызова метода subscriberContext(Function) или subscriberContext(Context) метода в конце вашего реактивного конвейера, как показано в методе тестирования ниже..

8.3 SINKS

Rector также предлагает возможность создавать Flux или Mono, программно определяя события onNext, onError и onComplete. Для этого предоставляется так называемый API-интерфейс приемника, запускающий события. Существуют несколько различных вариантов раковин, чтобы узнать больше об этом, читайте далее в справочной документации: Программное создание последовательности

8.4 Отладка

Отладка реактивного кода может стать проблемой из-за его функционального декларативного стиля, в котором фактическое объявление (или «assembly ») и обработка сигнала («execution») не происходят одновременно. Обычная трассировка стека Java, генерируемая приложением Reactor, не будет включать никаких ссылок на ассемблерный код, что затрудняет определение фактической основной причины распространенной ошибки.

Чтобы получить более значимую трассировку стека, которая включает информацию о сборке (также называемую трассировкой), вы можете добавить вызов Hooks.onOperatorDebug() в свое приложение. Однако это нельзя использовать в производственной среде, потому что это связано с перемещением тяжелого стека и может отрицательно повлиять на производительность.

Для использования в производственной среде Project Reactor предоставляет отдельный Java-агент, который инструментирует ваш код и добавляет отладочную информацию, не требуя больших ресурсов для захвата трассировки стека при каждом вызове оператора. Чтобы использовать его, вам нужно добавить reactor-tools артефакт в свои зависимости и инициализировать его при запуске приложения Spring Boot:

8.5 Метрики

Reactor предоставляет встроенную поддержку для включения и отображения показателей как для планировщиков (Schedulers), так и для издателей (Publishers). Дополнительные сведения см. в разделе «Метрики» Справочного руководства.

9. Подводя итог…

Источник

.NET/Mono в Java? Легко!

Пока есть только альфа версия, и поэтому для реального использования компилятор пока не годится, однако уже частично работоспособен, генерирует валидный код Java и поддерживает часть стандарта ECMA-335.

Компилятор представляет собой консольное приложение, которое при запуске без параметров выводит справку. Так что с использованием проблем возникнуть не должно.

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

Что не поддерживается

Как это работает

Компиляция разделена на три больших этапа. Первым этапом компилятор загружает и преобразует во внутреннее представление все используемые типы. Вторым производит подготовку к третьему этапу. Третьим этапом преобразует метаинформацию и компилирует код.

Преобразование типов во внутреннее представление происходит на лету во время их загрузки. То есть берётся тип, преобразуется во внутреннее представление, затем добавляется в список компиляции. Затем из оригинального типа берутся все поля и методы и так же преобразуются во внутреннее представление. Таким образом, в список компиляции добавляются все типы, их поля и методы из явно указанных в качестве входных, сборок.

Но так же, во время преобразования любого типа, поля или метода (далее члена) так же подгружаются, преобразуются во внутреннее представление и добавляются в список компиляции, все члены от которых исходный член зависит. При этом добавляются только действительно используемые члены. Таким образом, после первого этапа в списке компиляции будут лишь те типы, которые есть в исходных сборках, плюс те типы и их члены, которые необходимы. Благодаря этому на выходе мы получаем откомпилированными исходные сборки и необходимые куски остальных. Для примера возьмём код:

После первого этапа, список компиляции будет выглядеть вот так:

Здесь так же необходимо отметить о механизме подмены сборок. При обнаружении ссылки на тип, который находится во внешней сборке (не указанной в качестве входной), эта сборка будет автоматически подгружена. Однако что делать, если подгружаемая сборка имеет реализацию не совместимую с Java? Например, стандартный mscorlib? Для этого и нужен механизм подмены сборок. По умолчанию, на данный момент, так подменяется mscorlib на специальную реализацию, которая использует для работы механизмы Java. Можно так же указать и другие сборки, для подмены используя ключ компиляции –r. Вкратце работает это следующим образом: когда Mono.Cecil начинает искать откуда загрузить сборку, он обращается с этим вопросом к AssemblyResolver, который ему был передан в качестве параметра при чтении изначальной сборки. AssemblyResolver компилятора сначала ищет сборку с таким именем в ранее загруженных, если там не находит, то смотрит нет ли её в списках на подмену. Если есть, то загружает и возвращает сборку, указанную в списке на подмену. Если же и в списках на подмену её нет, то загружается стандартная сборка, стандартными средствами.

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

И собственно третий этап — самый основной. Преобразование метаинформации процесс достаточно простой и понятный. Единственное что хотелось бы отметить – это то, что все типы объявляются в результате с публичным доступом из-за несовместимости уровней видимости Java и CIL. Глобальные типы в Java могут иметь либо публичный доступ, либо доступ только из пакета (пространства имён). А вложенные типы в Java, имеющие например закрытый уровень видимости вообще не могут использоваться за пределами типа, в котором объявлены. Если обратится к любому члену такого типа из внешнего класса будет сгенерировано исключение. Так что все типы автоматически становятся открытыми.

А вот компиляция кода более сложный и объёмный процесс, который так же делится на несколько этапов. Первым этапом строится граф кода. Этим занимается библиотека ICSharpCode.Decompiler из состава ILSpy. В целом получается почти готовый для компиляции граф, но все же несколько дополнительных преобразований выполняется. Для примера обратно преобразуются псевдо инструкции CompoundAssignment, которые генерирует ICSharpCode.Decompiler. Ну и после этого собственно происходит компиляция в байт-код Java.

Читайте также:  Что значит фотографическая память

Вот так работает сам компилятор. Теперь я более подробно расскажу о некоторых моментах работы и как реализована поддержка тех или иных вещей.

Дженерики

С точки зрения JVM – дженериков не существует. Дженерики в Java – это лишь расширение компилятора и дженерик с точки зрения JVM – это обычный объект типа java.lang.Object. Дженерики в CLI являются компилируемыми во время исполнения. Это значит, что когда компилятор встречает дженерик, он подставляет вместо него реальный тип и, по сути, создаёт новый тип или метод на основе исходного. CIL2Java действует так же, пропуская методы и типы имеющие джерик параметры и создаёт их только когда встречает ссылку с указанием какие на какие типы заменять эти параметры.

Значимые типы

Используя эти три метода поведение значимых типов полностью эмулируется. Для примера код «Foo(valType);» будет преобразован в «Foo(valType.c2j__$__GetCopy());» и в метод Foo будет передана копия valType.

Так же для корректной работы все значимые типы автоматически инициализируются конструктором по умолчанию в конструкторах и в самом начале методов (прологе).

Таким образом, главное преимущество таких типов – при правильном использовании они увеличивают скорость работы приложения, не просто теряется, а наоборот их использование будет замедлять работу приложения.

Перечисления

При компиляции вместо типа перечисления, подставляется его базовый тип. То есть метод «void Foo(EnumType val);» после компиляции станет «void Foo(int val);».

Упаковка

Упаковка значимых типов делится на три категории: упаковка примитивных типов, упаковка значимых типов и упаковка перечислений.

Упаковка примитивных типов реализована двумя способами: упаковка в типы CIL или упаковка в типы Java. В первом случае в качестве типов для упаковки используются стандартные для CIL типы из пространства имён System (System.Int32, System.Single и т.д.). Во втором – стандартные типы для Java (java.lang.Integer, java.lang.Float и т.д.)

В случае упаковки в типы CIL мы сохраняем информацию о беззнаковых типах и код вида «uintType.ToString()» будет иметь правильный результат. Однако при передаче таких параметров в Java, в методы где требуется передать упакованный примитивный тип (например java.lang.reflect.Method.invoke) компилятору будет необходимо генерировать код переупаковки (правда пока этой функции в компиляторе нет), и, таким образом, будет падать производительность.

В случае упаковки в типы Java все соответственно наоборот. Код «uintType.ToString()» даст неправильный результат, если значение uintType будет больше 2 147 483 647, но не будет лишних переупаковок из CIL в Java и обратно. Какой метод применять – решать вам. За это отвечает параметр компиляции – box. По умолчанию происходит упаковка в типы CIL.

С упаковкой значимых типов все проще. Берём копию типа и просто передаём её. Он и так по факту, после компиляции, становится указательным типом.

А вот перечисления упаковываются в их настоящий тип. То есть, если имеется перечисление типа EnumType, имеющее базовый тип int, то, как было сказано выше, при компиляции вместо EnumType будет подставляться тип int. А вот в случае упаковки, будет создан объект именно типа EnumType, а в его поле value__ будет положено значение этого перечисления. Таким образом, будет сохранена информация о типе.

Указатели

Как уже было сказано, компилятор не поддерживает небезопасные указатели. А вот передача по ссылке вполне успешно работает. Если в метод передаётся значение по ссылке, то типом этого параметра станет тип CIL2Java.VES.ByRef[type], где [type] – это тип на который создаётся ссылка (возможные значения: Byte, Short, Int, Long, Float, Double, Bool, Char, Ref). Отдельные типы для примитивных типов необходимы для того, что бы не упаковывать/распаковывать их при каждом обращении. Сам тип ссылки является абстрактным классом с двумя абстрактными методами: get_Value и set_Value для получения и установки значения по ссылке соответственно. Вот так это выглядит:

При создании ссылки на значение, создаётся объект который реализует соответственный абстрактный класс. И реализует в зависимости от того, где хранится значение на которое мы создаём ссылку:

LocalByRef[type] – ссылка на локальную переменную или параметр метода. Просто хранит в себе значение до выхода из вызываемого места, после чего происходит восстановление значения переменной или параметра.
Возьмём вот такой код:

После компиляции код будет выглядеть вот так:

FieldByRef[type] – ссылка на поле объекта. Реализуется силами рефлексии. Вот так выглядит этот тип после компиляции:

ArrayByRef[type] – ссылка на элемент массива. Тут всё просто – сохраняем сам массив (который является указательным типом) и индекс в этом массиве. Вот так это выглядит после компиляции:

Указатели на методы и делегаты

Это то, чего мне сильнее всего не хватает в Java. Одним из способов реализации указателей на методы является рефлексия. Но мне этот вариант не понравился тем, что требует упаковки параметров, что снижает производительность. Таким образом был использован второй способ.

В дальнейшем описании я буду использовать вот такой пример:

Способ заключается в том, что если мы встречаем инструкцию ldftn или ldvirtftn то сначала генерируется интерфейс в пространстве имён CIL2Java.VES.MethodPointers с именем, зависящим от сигнатуры метода и с единственным методом invoke, имеющим почти такую же сигнатуру что и метод, на который мы получаем указатель, добавив первым параметром ссылку на объект в котором необходимо вызвать метод. В нашем примере такой интерфейс будет выглядеть вот так:

Затем, каждая инструкция ldftn или ldvirtftn генерирует вложенный тип реализующий интерфейс указателя на метод. Метод invoke просто вызывает метод, на который инструкция получает указатель. В приведённом примере это выглядит так:

И уже в конструктор делегата, в качестве указателя на метод, передаётся экземпляр данного класса.

Сам делегат после компиляции принимает такой вид:

Таково поведение компилятора по умолчанию. Как вы можете заметить, сигнатура конструктора делегата изменена – последний параметр имеет тип интерфейса указателя метода, а не native int как это необходимо по стандарту. Сделано это опять же для оптимизации. Однако вы можете указать компилятору что необходимо компилировать указатели на метод согласно стандарту используя параметр «-method_pointers standart». В таком случае создание делегата в нашем примере принимает вид:

А сам делегат становится вот таким:

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

yield return/break

Здесь если честно рассказывать нечего. Просто работает.

Async/await

Здесь тоже особо рассказывать нечего. Код использующий async/await компилируется, но не работает. Не работает потому что нет реализации необходимых для работы типов (System.Threading.Tasks.Task, System.Runtime.CompilerServices.AsyncTaskMethodBuilder и так далее)

Беззнаковые числа

Поддержка беззнаковых чисел в компиляторе имеется, но включается отдельно параметром «-unsigned». В реализации очень помогла статья http://habrahabr.ru/post/225901/ за авторством elw00d. В целом в этой статье всё описано и все операции с беззнаковыми числами были сделаны по этой статье.

Исключения

В целом исключения в Java и в CIL очень похожи. Пока правда не поддерживаются фильтры исключений (их не поддерживает ICSharpCode.Decompiler).

Дополнительно, был добавлен механизм связки типов исключений Java и CIL. К примеру в CIL имеется исключение System.ArithmeticException. В Java имеется свой тип java.lang.ArithmeticException. Как сделать так, что бы перехватывая System.ArithmeticException перехватывался так же и java.lang.ArithmeticException? Для этого был введёт атрибут JavaExceptionMapAttribute который указывает компилятору аналогичное исключение в Java. И когда компилятор встречает перехват System.ArithmeticException, он так же добавляет перехват и аналогичного Java исключения. Единственное что добавляется условие что в System.ArithmeticException должен быть введён дополнительный конструктор, принимающий только один параметр типа java.lang.ArithmeticException для того, что бы перехватчику был передан экземпляр исключения одного типа.

Отладка

Компилятор поддерживает генерацию отладочной информации (если она есть в исходных сборках) при указании ключа компиляции «-debug». Вот пример того, как тестовое приложение отлаживается в Eclipse:

Источник

Сказочный портал