Русский
Русский
English
Статистика
Реклама

Java

Маленькое удобство, способное прекратить вселенские споры

22.06.2020 00:17:16 | Автор: admin
Все те, кто пишет на Си-подобных языках, знакомы с двумя немного отличающимися стилями кода. Выглядят они вот так:

for (int i=0;i<10;i++) {    printf("Hello world!");    printf("Hello world again!");}


for (int i=0;i<10;i++){    printf("Hello world!");    printf("Hello world again!");}


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

Компромиссное решение могло бы выглядеть вот так:

image

Что, собственно, здесь произошло? Всего лишь небольшая магия или фокус. Потому что настоящие фигурные скобки никуда не делись. Они были заменены редактором на символы верхнего и нижнего подчёркивания. В исходном файле все скобки на месте, но при чтении исходного файла ваша любимая IDE заменяет скобки на подчёркивания по предлагаемому алгоритму.

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

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

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

И с другой стороны красота остаётся нетронутой! Визуально блок кода выделяется самой симметричной из всех симметричных конструкцией. Глаза всё так же легко найдут начало и конец. Ну и возражать против такого стиля будет просто некому, ведь оппоненты наверняка все перейдут в лагерь сторонников подхода.

Таким образом нам остаётся лишь рассмотреть алгоритм замены фигурных скобок на знаки подчёркивания.

Алгоритм замены скобок.



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

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

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

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

Но остаётся небольшая сложность в виде бесшовного совмещения процесса редактирования с отображением замен в виде подчёркиваний.

Сначала крупные изменения. Если блок удаляется вместе с обоими скобками, то делать ничего не надо, редактор сам отобразит всё правильно. Если блок удаляется без скобок, но целиком, или же если удаляется часть блока с одной из скобок, то мы восстанавливаем неудалённую скобку (скобки) и отдаём дальнейшее творчество в руки автора кода.

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

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

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

Пока мы в блоке текста, включая крайние строки, делать ничего не нужно. Когда мы нажимаем enter в любом месте последней строки кроме конца нужно перенести подчёркивание. Если enter нажимается в конце строки ждём ввода символа. Когда мы нажимаем backspace в последней строке в позиции подчёркивания нужно переместить текущее подчёркивание на одну линию выше. И разумеется, при всех операциях нужно не забывать вставлять и удалять скобки в тот текст, который будет сохраняться на диск.

Вот и весь алгоритм. Остаётся дождаться, когда знатоки плагинов сделают нам всем такой приятный подарок. Ну а за ними подтянется и спецификация языка, ведь как ей не соответствовать тому, что удобно, да ещё и используется очень многими?

И напоследок о символе overscore, который я здесь условно назвал верхним подчёркиванием. Его, к сожалению, нет на стандартной клавиатуре, а потому нет возможности вводить его напрямую без помощи редактора. Именно поэтому важна помощь IDE. Хотя теоретически можно вводить последовательность юникода (\u00af = ), которую некоторые редакторы автоматически преобразуют в символ overscore, но всё же делать так каждый раз, когда нам нужна скобка, было бы просто издевательством над разработчиками. Поэтому и нужны плагины, ну и изменения в спецификациях языков.

Всё, ждём срочных обновлений спецификаций языков и массу удобных плагинов для всех возможных IDE :)
Подробнее..
Категории: Javascript , C++ , C , Java , Код , Си , Стиль кода

Android Camera2 API от чайника, часть 6. Стрим видео сначала кодировали, теперь декодируем

20.06.2020 18:10:45 | Автор: admin

Итак, в предыдущем посте мы занимались кодированием живого видео формата H.264 на Android устройстве, которое затем отправляли для просмотра на персональный компьютер под виндой. Там наш видеопоток успешно раскодировывался и лицезрелся с помощью VLC плеера. А так же с помощью библиотеки VLCJ CAPRICA благополучно впихивался и в окошки JAVA приложения. Правда, каким именно образом он (VLC плеер) всё это проделывал, так и осталось загадкой. Но с другой стороны работает, да и ладно.

Подстольный настольный компьютер, ноутбук, лэптоп всё это прекрасно, но тем не менее, всё больше народа смотрит видео и управляет разными девайсами не из-за стола, а чаще валяясь на диване, со смартфоном в руках. И соответственно, к примеру, даже нашей роботележкой ныне удобней управлять именно оттуда. Поэтому настало время выяснить, как наш закодированный видео поток принять и отобразить на экране такого же Android устройства. Естественно, как и раньше мы проделаем всё через Camera2 API, концепцию Surface, да ещё и асинхронно!

Кому интересно вперёд.

Собственно, для чего мне это понадобилось, кроме как обычно стремления, по выражению ослика Иа, к обалдеванию новыми знаниями? Откровенно говоря, про роботележку я упомянул только для примера (я думаю за такое количество статей она уже всем успела изрядно надоесть). На самом деле, кодируя-декодируя мы соберём незамысловатый видеотелефон для домашней сети.

Но это будет:
в следующей статье. Поскольку надо ещё и аудио канал прикрутить.

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

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

image

Второй телефон можно отобрать у своей девицы и использовать её (девицу) в качестве подопытного кролика второго участника эксперимента.

Если по существу, то изображение с камеры первого Android устройства передается на экран второго и раздваивается там (VR очки же). И наоборот, со второго девайса видео поток подается на первый в таком же порядке. То есть, вы видите то же, что должен видеть ваш партнер, а он (она), видит то, что должны видеть вы. Поскольку мы, в буквальном смысле, есть там, где есть наши глаза, ощущения будут у вас непередаваемые, особенно, если вы будете стараться двигаться синхронно и смотреть на свое (её) тело. Для неврологических экспериментов просто поле непаханное, ну или для камасутры всякой.

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

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

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

А находится он, как известно, в классе:

MEDIA CODEC


В пред пред предыдущем посте с его помощью мы кодировали в формате H.264 видео поток полученный с камеры и отправляли его по UDP каналу на нужный адрес. Поэтому, чтобы начать работу, нам весьма пригодится код из того самого поста. Чтобы не парить голову с отладкой программы на двух телефонах сразу, для начала мы сделаем всё на одном. Просто для понимания принципа работы. В одном окошке мы будем снимать видео, кодировать и отправлять его по домашней сетке самим себе во второе окошко. Всё по-честному, никаких localhost и адресов 127.0.0.1. Только настоящий сетевой адрес, только хардкор!

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

  1. Изменение UI, то есть добавление второго окна и ассоциированного с ним Surface
  2. Блок кода для получения видео потока по UDP каналу
  3. Процедуру самого декодирования.

Всё это делается легко и просто, но об несколько подводных камней удариться всё равно придется.

Итак, сначала модифицируем макет добавляем второе окно.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context=".MainActivity">    <TextureView        android:id="@+id/textureView"        android:layout_width="320dp"        android:layout_height="240dp"        android:layout_marginTop="88dp"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintHorizontal_bias="0.494"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent" />    <TextureView        android:id="@+id/textureView3"        android:layout_width="320dp"        android:layout_height="240dp"        android:layout_marginTop="24dp"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintHorizontal_bias="0.494"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toBottomOf="@+id/textureView" />    <LinearLayout        android:layout_width="165dp"        android:layout_height="40dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toBottomOf="@+id/textureView3"        app:layout_constraintVertical_bias="0.838">        <Button            android:id="@+id/button1"            android:layout_width="wrap_content"            android:layout_height="36dp"            android:text="вкл" />        <Button            android:id="@+id/button3"            android:layout_width="wrap_content"            android:layout_height="37dp"            android:text="выкл" />    </LinearLayout></androidx.constraintlayout.widget.ConstraintLayout>


Эта часть вообще объяснений не требует. Идем дальше.

Пишем код для получения дэйтаграмм


Тут есть пара тонкостей. Когда мы дэйтаграммы отправляли, то действовали совсем незамысловато. Мы дожидались, когда сработает коллбэк буфера выходных данных onOutputBufferAvailable, а затем легко и просто пихали полученный от него байтовый массив в UDP пакет. Дальше он уже без всякой помощи с нашей стороны уезжал по указанному адресу. Единственное, мы его ещё рубили на килобайтовые блоки (чтобы гарантированно влез в размер MTU), но сейчас это совершенно излишне (и даже, как мы увидим потом, вредно).

В случае же приёма, надо во-первых, самим для этого организовать отдельный поток ожидания поскольку мы не в курсе, когда придёт следующая дэйтаграмма. Во-вторых, надо знать, скурил ли декодер уже пришедший пакет, чтобы в следующий раз предоставить ему новый, а не бывший в употреблении. В-третьих мы, (вроде как) в Java не знаем размер прибывшего пакета. А чтобы положить его в байтовый массив, размер его знать как-бы хочется.

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

В итоге, код оказался несложным:

        Udp_recipient()        {            start();            mNewFrame = false;        }        public void run()        {                         try {                    byte buffer[] = new byte[50000];                    DatagramPacket p = new DatagramPacket(buffer, buffer.length);                    udpSocketIn.receive(p);                    byte bBuffer[] = p.getData();                    outDataForEncoder = new byte[p.getLength()];                     synchronized (outDataForEncoder)                     {                         for (int i = 0; i < outDataForEncoder.length; i++)                         {                             outDataForEncoder[i] = bBuffer[i];                         }                     }                    mNewFrame = true;                } catch (Exception e) {                    Log.i(LOG_TAG, e + "  ");                }             }        }

Итак, поток в наличии. После получения дэйтаграммы устанавливаем флаг NewFrame, чтобы декодер не путался и сбрасываем флаг перед прибытием нового UDP пакета. Здесь неявно предполагается, что декодер скуривает пакеты быстрее, чем они приходят. А это действительно, так и оказалось.

Переходим теперь к самому декодеру


Как и кодер он может работать в двух режимах синхронном и асинхронном. В первом случае вы учреждаете немало потоков, флагов и тщательно следите за тем, чтобы не впасть в состояние гонок (всё ж параллельное). Этот процесс сопровождается кучей стереотипного кода, в просторечии Boiler Plate, и в общем многократно где описан.

Ну, а мы, поскольку кодировали видео асинхронно через коллбэки, то делать шаг назад к синхронности, вроде как, нам не к лицу. Но проблема в том, что примеров асинхронного кода, почему-то намного меньше и основная мысль там такая: я написал, а оно не работает, хелп!.

То ли считается, что там всё тривиально (а оно по сравнению с синхронным вариантом действительно так), то ли правда, оно у всех не работает. Разбираться опять придётся самим.

Итак, сначала учреждаем и инициализируем экземпляр декодера. Это вообще не сложно, поскольку он будет использовать формат кодера и нам только надо установить разрешение выходного окна в пихелях (это мое личное изобретение пихель, так сказать, родним русский и английский). Пока ограничимся скромным 640 на 480. Также надо будет присобачить декодер к Surface этого самого окна, чтобы было куда показывать.

       try {            decoder = MediaCodec.createDecoderByType("video/avc");// H264 декодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету декодека");        }        int width = 480; // ширина видео        int height = 640; // высота видео        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);        format.setInteger(MediaFormat.KEY_ROTATION,90);        SurfaceTexture texture = mImageViewDown.getSurfaceTexture();        texture.setDefaultBufferSize(480, 640);        mDecoderSurface = new Surface(texture);        decoder.configure(format, mDecoderSurface, null,0);        decoder.setOutputSurface(mDecoderSurface);        decoder.setCallback(new DecoderCallback());        decoder.start();        Log.i(LOG_TAG, "запустили декодер");

Далее обращаемся к коллбэкам. Нужных, как известно, два:

void onInputBufferAvailable(MediaCodec mc, int inputBufferId)void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, )

То есть, при срабатывании первого коллбэка мы в него кладём данные, а при вызове второго вынимаем готовенькое и отправляем на Surface. Может показаться странным, что когда мы делали кодирование (то есть, наоборот из Surface в кодек), то почему-то InputBuffer мы не использовали, а сразу волшебным образом доставали байтовые данные из OutputBuffer. Это мне тоже казалось странным, пока я не прочитал:
When using an input Surface, there are no accessible input buffers, as buffers are automatically passed from the input surface to the codec. Calling dequeueInputBuffer will throw an IllegalStateException, and getInputBuffers() returns a bogus ByteBuffer[] array that MUST NOT be written into.
Короче, автоматически это делается. Ну, и сделали бы при декодировании также. Но нет, придётся самим.

Итак, в метод void (MediaCodec mc, int inputBufferId) я ничтоже сумняшеся прописал:

 private class DecoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {                  decoderInputBuffer = codec.getInputBuffer(index);                  decoderInputBuffer.clear();                             if(mNewFrame)                             {                               synchronized (outDataForEncoder)                               { b=outDataForEncoder;  }                             }                             decoderInputBuffer.put(b);                 codec.queueInputBuffer(index, 0, b.length,0, 0);              if(mNewFrame)              new Udp_recipient();        }

Ну, по аналогии, как мы делали в прошлый раз в кодере.

То есть, когда декодер вдруг ощущает, что ему срочно нужны данные, он вызывает этот коллбэк и кладёт наш прибывший udp-пакет (который уже доступен в виде байтового массива) в один из своих буферов под номером index. Там их вроде четыре. Естественно, ничего не заработало. Я ж забыл про onOutputBufferAvailable.

Туда тоже необходимо вставить две строчки:

    @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            {                  {                      decoderOutputBuffer = codec.getOutputBuffer(index);                      codec.releaseOutputBuffer(index, true);                  }            }        }

Причем значение True/False отвечает за то, будет ли содержимое буфера рендерится на Surface или выкинется на помойку.

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



А в логах обнаружились какие-то таинственные:

buffer descriptor with invalid usage bits 0x2000
A resource failed to call release.


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

Оказалось, что имеет место удивительная вещь. Если коллбэк onInputBufferAvailable, сработал, то вынь да положь ему входные данные. Неважно какие, но положь. Иначе он дохнет. Поэтому пришлось предлагать ему байтовый массив нулевой длины в тех случаях, когда пакет актуальных данных уже был обработан, а новый ещё не пришёл. Как говорится, кому расскажешь не поверят.

Как только поправки были произведены, на Output Surface наконец-то появилось полученное через сеть видео.



Правда, как видно, на нём присутствуют некоторые недостатки. Посмотрев на них какое-то время, я стал в душе догадываться, что это все опять из-за onInputBufferAvailable. Ему снова что-то не нравилось. Оказалось, был не по вкусу байтовый массив. Я тогда про это не догадывался, мне лично не нравилась концепция флага NewFrame. Как-то она не сочеталась с реактивным программированием. Поэтому я решил полученные пакеты не просто класть в массив, а заворачивать этот массив в итоге в байтовый поток ByteArrayOutputStream. И пускай коллбэк, что ему надо, сам оттуда забирает, а конкретно каким образом, это его проблема.

Идея сработала блестяще и на экране я увидел это:

image

Согласитесь, прогресс существенный. Но опять чего-то не хватает. Или чего лишнее?

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

Решение было простым:

поток.reset();

И всё заработало как надо.



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

Листинг main_activity
package com.example.encoderdecoder;import androidx.annotation.RequiresApi;import androidx.appcompat.app.AppCompatActivity;import androidx.core.content.ContextCompat;import android.Manifest;import android.content.Context;import android.content.pm.ActivityInfo;import android.content.pm.PackageManager;import android.graphics.SurfaceTexture;import android.hardware.camera2.CameraAccessException;import android.hardware.camera2.CameraCaptureSession;import android.hardware.camera2.CameraDevice;import android.hardware.camera2.CameraManager;import android.hardware.camera2.CaptureRequest;import android.media.MediaCodec;import android.media.MediaCodecInfo;import android.media.MediaFormat;import android.os.Build;import android.os.Bundle;import android.os.Handler;import android.os.HandlerThread;import android.os.StrictMode;import android.util.Log;import android.view.Surface;import android.view.TextureView;import android.view.View;import android.widget.Button;import android.widget.Toast;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;import java.net.SocketException;import java.nio.ByteBuffer;import java.util.Arrays;public class MainActivity extends AppCompatActivity {    public static final String LOG_TAG = "myLogs";    public static Surface surface = null;    CameraService[] myCameras = null;    private CameraManager mCameraManager = null;    private final int CAMERA1 = 0;    private Button mButtonOpenCamera1 = null;    private Button mButtonTStopStreamVideo = null;    public static TextureView mImageViewUp = null;    public static TextureView mImageViewDown = null;    private HandlerThread mBackgroundThread;    private Handler mBackgroundHandler = null;    private MediaCodec encoder = null; // кодер    private MediaCodec decoder = null;    byte [] b;    Surface mEncoderSurface; // Surface как вход данных для кодера    Surface mDecoderSurface; // Surface как прием данных от кодера    ByteBuffer outPutByteBuffer;    ByteBuffer decoderInputBuffer;    ByteBuffer decoderOutputBuffer;    byte outDataForEncoder [];    DatagramSocket udpSocket;    DatagramSocket udpSocketIn;    String ip_address = "your target address";// сюда пишете IP адрес телефона куда шлете //видео, но можно и себе    InetAddress address;    int port = 40002;    ByteArrayOutputStream  out;    private void startBackgroundThread() {        mBackgroundThread = new HandlerThread("CameraBackground");        mBackgroundThread.start();        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());    }    private void stopBackgroundThread() {        mBackgroundThread.quitSafely();        try {            mBackgroundThread.join();            mBackgroundThread = null;            mBackgroundHandler = null;        } catch (InterruptedException e) {            e.printStackTrace();        }    }    @RequiresApi(api = Build.VERSION_CODES.M)    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();        StrictMode.setThreadPolicy(policy);        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);        setContentView(R.layout.activity_main);        Log.d(LOG_TAG, "Запрашиваем разрешение");        if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED                ||                (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)        ) {            requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);        }        mButtonOpenCamera1 = findViewById(R.id.button1);        mButtonTStopStreamVideo = findViewById(R.id.button3);        mImageViewUp = findViewById(R.id.textureView);        mImageViewDown = findViewById(R.id.textureView3);        mButtonOpenCamera1.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                setUpMediaCodec();// инициализируем Медиа Кодек                if (myCameras[CAMERA1] != null) {// открываем камеру                    if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera();                }            }        });        mButtonTStopStreamVideo.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if (encoder != null) {                    Toast.makeText(MainActivity.this, " остановили стрим", Toast.LENGTH_SHORT).show();                    myCameras[CAMERA1].stopStreamingVideo();                }            }        });        try {            udpSocket = new DatagramSocket();            udpSocketIn = new DatagramSocket(port);// we changed it to DatagramChannell becouse UDP packets may be different in size            try {            }            catch (Exception e){                Log.i(LOG_TAG, "  создали udp канал");            }            new Udp_recipient();            Log.i(LOG_TAG, "  создали udp сокет");        } catch (                SocketException e) {            Log.i(LOG_TAG, " не создали udp сокет");        }        try {            address = InetAddress.getByName(ip_address);            Log.i(LOG_TAG, "  есть адрес");        } catch (Exception e) {        }        mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);        try {            // Получение списка камер с устройства            myCameras = new CameraService[mCameraManager.getCameraIdList().length];            for (String cameraID : mCameraManager.getCameraIdList()) {                Log.i(LOG_TAG, "cameraID: " + cameraID);                int id = Integer.parseInt(cameraID);                // создаем обработчик для камеры                myCameras[id] = new CameraService(mCameraManager, cameraID);            }        } catch (CameraAccessException e) {            Log.e(LOG_TAG, e.getMessage());            e.printStackTrace();        }    }    public class CameraService {        private String mCameraID;        private CameraDevice mCameraDevice = null;        private CameraCaptureSession mSession;        private CaptureRequest.Builder mPreviewBuilder;        public CameraService(CameraManager cameraManager, String cameraID) {            mCameraManager = cameraManager;            mCameraID = cameraID;        }        private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() {            @Override            public void onOpened(CameraDevice camera) {                mCameraDevice = camera;                Log.i(LOG_TAG, "Open camera  with id:" + mCameraDevice.getId());                startCameraPreviewSession();            }            @Override            public void onDisconnected(CameraDevice camera) {                mCameraDevice.close();                Log.i(LOG_TAG, "disconnect camera  with id:" + mCameraDevice.getId());                mCameraDevice = null;            }            @Override            public void onError(CameraDevice camera, int error) {                Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error);            }        };        private void startCameraPreviewSession() {            SurfaceTexture texture = mImageViewUp.getSurfaceTexture();            texture.setDefaultBufferSize(480, 640);            surface = new Surface(texture);            try {                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);                mPreviewBuilder.addTarget(surface);                mPreviewBuilder.addTarget(mEncoderSurface);                mCameraDevice.createCaptureSession(Arrays.asList(surface, mEncoderSurface),                        new CameraCaptureSession.StateCallback() {                            @Override                            public void onConfigured(CameraCaptureSession session) {                                mSession = session;                                try {                                    mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);                                } catch (CameraAccessException e) {                                    e.printStackTrace();                                }                            }                            @Override                            public void onConfigureFailed(CameraCaptureSession session) {                            }                        }, mBackgroundHandler);            } catch (CameraAccessException e) {                e.printStackTrace();            }        }        public boolean isOpen() {            if (mCameraDevice == null) {                return false;            } else {                return true;            }        }        public void openCamera() {            try {                if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {                    mCameraManager.openCamera(mCameraID, mCameraCallback, mBackgroundHandler);                }            } catch (CameraAccessException e) {                Log.i(LOG_TAG, e.getMessage());            }        }        public void closeCamera() {            if (mCameraDevice != null) {                mCameraDevice.close();                mCameraDevice = null;            }        }        public void stopStreamingVideo() {            if (mCameraDevice != null & encoder != null) {                try {                    mSession.stopRepeating();                    mSession.abortCaptures();                } catch (CameraAccessException e) {                    e.printStackTrace();                }                encoder.stop();                encoder.release();                mEncoderSurface.release();                decoder.stop();                decoder.release();                closeCamera();            }        }    }    private void setUpMediaCodec() {        try {            encoder = MediaCodec.createEncoderByType("video/avc"); // H264 кодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету кодека");        }        {            int width = 480; // ширина видео            int height = 640; // высота видео            int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; // формат ввода цвета            int videoBitrate = 2000000; // битрейт видео в bps (бит в секунду)            int videoFramePerSecond = 30; // FPS            int iframeInterval = 1; // I-Frame интервал в секундах            MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);            format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate);            format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond);            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);            encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // конфигурируем кодек как кодер            mEncoderSurface = encoder.createInputSurface(); // получаем Surface кодера        }        encoder.setCallback(new EncoderCallback());        encoder.start(); // запускаем кодер        Log.i(LOG_TAG, "запустили кодек");        try {            decoder = MediaCodec.createDecoderByType("video/avc");// H264 декодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету декодека");        }        int width = 480; // ширина видео        int height = 640; // высота видео        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);        format.setInteger(MediaFormat.KEY_ROTATION,90);        SurfaceTexture texture = mImageViewDown.getSurfaceTexture();        texture.setDefaultBufferSize(480, 640);        mDecoderSurface = new Surface(texture);        decoder.configure(format, mDecoderSurface, null,0);        decoder.setOutputSurface(mDecoderSurface);        decoder.setCallback(new DecoderCallback());        decoder.start();        Log.i(LOG_TAG, "запустили декодер");    }    //CALLBACK FOR DECODER    private class DecoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {                  decoderInputBuffer = codec.getInputBuffer(index);                  decoderInputBuffer.clear();                                    synchronized (out)                    {                            b =  out.toByteArray();                        out.reset();                          }                            decoderInputBuffer.put(b);                   codec.queueInputBuffer(index, 0, b.length,0, 0);        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            {                  {                      decoderOutputBuffer = codec.getOutputBuffer(index);                      codec.releaseOutputBuffer(index, true);                  }            }        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {            Log.i(LOG_TAG, "decoder output format changed: " + format);        }    }    private class EncoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {            Log.i(LOG_TAG, " входные буфера готовы" );            //        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            outPutByteBuffer = encoder.getOutputBuffer(index);            byte[] outDate = new byte[info.size];            outPutByteBuffer.get(outDate);            try {                DatagramPacket packet = new DatagramPacket(outDate, outDate.length, address, port);                udpSocket.send(packet);            } catch (IOException e) {                Log.i(LOG_TAG, " не отправился UDP пакет");            }            encoder.releaseOutputBuffer(index, false);        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {          //  Log.i(LOG_TAG, "encoder output format changed: " + format);        }    }    @Override    public void onPause() {        if (myCameras[CAMERA1].isOpen()) {            myCameras[CAMERA1].closeCamera();        }        stopBackgroundThread();        super.onPause();    }    @Override    public void onResume() {        super.onResume();        startBackgroundThread();    }    public class Udp_recipient extends Thread {        Udp_recipient()        {            out = new ByteArrayOutputStream(50000);            start();        }        public void run()        {            while (true)            {                try {                    byte buffer[] = new byte[50000];                    DatagramPacket p = new DatagramPacket(buffer, buffer.length);                    udpSocketIn.receive(p);                    byte bBuffer[] = p.getData();                    outDataForEncoder = new byte[p.getLength()];                     synchronized (outDataForEncoder)                     {                         for (int i = 0; i < outDataForEncoder.length; i++)                         {                             outDataForEncoder[i] = bBuffer[i];                         }                     }                     synchronized (out)                            {out.write(outDataForEncoder);}                } catch (Exception e) {                    Log.i(LOG_TAG, e + "  ");                }            }        }        }    }


Как видно из листинга, мы выкинули из кодера фрагмент, где готовый байтовый массив рубился на кусочки длиной не более килобайта из-за опасения, что они могут не влезть в MTU. Опасения, как уже говорилось, оказались напрасными и даже вредными. Дело в том, (как мне показалось ) что кодер лепит эти массивы уже как некие смысловые единицы и соответственно декодер таким же порядком кладёт их в свои входные буферы. А если у нас килобайт попадает в один буфер, а хвостик в другой? Теперь-то скрывать уже нечего, но на самом деле, видео у меня красиво не показывало даже тогда, когда я учредил ByteArrayOutputStream. Не показывало до тех пор, пока я не выкинул этот злосчастный фрагмент кода.

А вот, когда мы гнали видео поток на компьютер и декодировали его VLC плеером, такой проблемы не было. Скорее всего, не было потому, что плеер предварительно формировал кэш (он же плеер, он больше для просмотра кино, а не живого видео). А в кэше, все эти порубленные килобайты вновь красиво срастались.

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



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

Для полноты счастья оставалось только провести эксперименты с доступными разрешением и битрейтом. Я сначала хотел было поставить вполне приличные 1280 х 960, но для этого в кодеке H.264 нужен битрейт 5000-6000 Кбит / с. А при установке такой скорости мой декодер, к сожалению, со своей работой уже не справлялся. Пришлось ограничиться разрешением 640 х 480 (тем, что и так уже было) и подходящим для этого битрейтом 2000 Кбит / с. Потому что при приближении к скорости света к 3 000 000 бит/c, декодер начинает иметь бесконечную массу, валять дурака и отваливаться.

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

Сама программа особо не поменяется. Макет вообще трогать не будем. Нам всего лишь надо:

  1. Отключить первую Surface от камеры
  2. Подключить к ней декодер. Грубо говоря, скопировать окно.

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

TextView1.setText(Int +  );TextView2.setText(Int +  );

Но видимо, поток данных в Surface не так прост как просто вывод текста. Если пробовать решить вопрос таким путем, то у вас будет выводится окно, которое было инициализировано последним. А первая инициализация исчезнет.

Чего я только не делал, даже textureView окна в макете пытался обозвать одинаково, ничего не помогало. На stockoverflow было на этот счёт мало чего (действительно, кому нафиг надо дублировать видео поток в два одинаковых окна на смартфоне), а в единственном обсуждении, что я нашёл, высказывалось туманное предположение, что один экземпляр Mediac Codec может связываться только с одной Surface.

Выход оказался совсем не элегантным. Надо скопировать видео поток во второе окно? Создавай второй экземпляр декодера, не больше ни меньше. Хорошо, что ещё не заставили второй раз данные по UDP каналу пересылать.

Но, тем тем не менее, при всей своей дубовости метод работает.

Листинг кода main_activity
package com.example.twovideosurfaces;import androidx.annotation.RequiresApi;import androidx.appcompat.app.AppCompatActivity;import androidx.core.content.ContextCompat;import android.Manifest;import android.content.Context;import android.content.pm.ActivityInfo;import android.content.pm.PackageManager;import android.graphics.SurfaceTexture;import android.hardware.camera2.CameraAccessException;import android.hardware.camera2.CameraCaptureSession;import android.hardware.camera2.CameraDevice;import android.hardware.camera2.CameraManager;import android.hardware.camera2.CaptureRequest;import android.media.MediaCodec;import android.media.MediaCodecInfo;import android.media.MediaFormat;import android.os.Build;import android.os.Bundle;import android.os.Handler;import android.os.HandlerThread;import android.os.StrictMode;import android.util.Log;import android.view.Surface;import android.view.TextureView;import android.view.View;import android.widget.Button;import android.widget.Toast;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;import java.net.SocketException;import java.nio.ByteBuffer;import java.util.Arrays;public class MainActivity extends AppCompatActivity  {    public static final String LOG_TAG = "myLogs";    CameraService[] myCameras = null;    private CameraManager mCameraManager = null;    private final int CAMERA1 = 0;    private Button mOn = null;    private Button mOff = null;    public static TextureView mImageViewUp = null;    public static TextureView mImageViewDown = null;    private HandlerThread mBackgroundThread;    private Handler mBackgroundHandler = null;    private MediaCodec encoder = null; // кодер    private MediaCodec decoder = null;    private MediaCodec decoder2 = null;    byte [] b;    byte [] b2;    Surface mEncoderSurface; // Surface как вход данных для кодера    Surface mDecoderSurface; // Surface как прием данных от кодера    Surface mDecoderSurface2; // Surface как прием данных от кодера    ByteBuffer outPutByteBuffer;    ByteBuffer decoderInputBuffer;    ByteBuffer decoderOutputBuffer;    ByteBuffer decoderInputBuffer2;    ByteBuffer decoderOutputBuffer2;    byte outDataForEncoder [];    static  boolean  mNewFrame=false;    DatagramSocket udpSocket;    DatagramSocket udpSocketIn;    String ip_address = "192.168.50.131";    InetAddress address;    int port = 40002;    ByteArrayOutputStream out =new ByteArrayOutputStream(50000);    ByteArrayOutputStream out2 = new ByteArrayOutputStream(50000);    private void startBackgroundThread() {        mBackgroundThread = new HandlerThread("CameraBackground");        mBackgroundThread.start();        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());    }    private void stopBackgroundThread() {        mBackgroundThread.quitSafely();        try {            mBackgroundThread.join();            mBackgroundThread = null;            mBackgroundHandler = null;        } catch (InterruptedException e) {            e.printStackTrace();        }    }    @RequiresApi(api = Build.VERSION_CODES.M)    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();        StrictMode.setThreadPolicy(policy);        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);        setContentView(R.layout.activity_main);        Log.d(LOG_TAG, "Запрашиваем разрешение");        if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED                ||                (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)        ) {            requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);        }        mOn = findViewById(R.id.button1);        mOff = findViewById(R.id.button3);        mImageViewUp = findViewById(R.id.textureView);        mImageViewDown = findViewById(R.id.textureView3);        mOn.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                setUpMediaCodec();// инициализируем Медиа Кодек                if (myCameras[CAMERA1] != null) {// открываем камеру                    if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera();                }            }        });        mOff.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if (encoder != null) {                    Toast.makeText(MainActivity.this, " остановили стрим", Toast.LENGTH_SHORT).show();                    myCameras[CAMERA1].stopStreamingVideo();                }            }        });        try {            udpSocket = new DatagramSocket();            udpSocketIn = new DatagramSocket(port);// we changed it to DatagramChannell becouse UDP packets may be different in size            try {            }            catch (Exception e){                Log.i(LOG_TAG, "  создали udp канал");            }            new Udp_recipient();            Log.i(LOG_TAG, "  создали udp сокет");        } catch (                SocketException e) {            Log.i(LOG_TAG, " не создали udp сокет");        }        try {            address = InetAddress.getByName(ip_address);            Log.i(LOG_TAG, "  есть адрес");        } catch (Exception e) {        }        mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);        try {            // Получение списка камер с устройства            myCameras = new CameraService[mCameraManager.getCameraIdList().length];            for (String cameraID : mCameraManager.getCameraIdList()) {                Log.i(LOG_TAG, "cameraID: " + cameraID);                int id = Integer.parseInt(cameraID);                // создаем обработчик для камеры                myCameras[id] = new CameraService(mCameraManager, cameraID);            }        } catch (CameraAccessException e) {            Log.e(LOG_TAG, e.getMessage());            e.printStackTrace();        }    }    public class CameraService {        private String mCameraID;        private CameraDevice mCameraDevice = null;        private CameraCaptureSession mSession;        private CaptureRequest.Builder mPreviewBuilder;        public CameraService(CameraManager cameraManager, String cameraID) {            mCameraManager = cameraManager;            mCameraID = cameraID;        }        private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() {            @Override            public void onOpened(CameraDevice camera) {                mCameraDevice = camera;                Log.i(LOG_TAG, "Open camera  with id:" + mCameraDevice.getId());                startCameraPreviewSession();            }            @Override            public void onDisconnected(CameraDevice camera) {                mCameraDevice.close();                Log.i(LOG_TAG, "disconnect camera  with id:" + mCameraDevice.getId());                mCameraDevice = null;            }            @Override            public void onError(CameraDevice camera, int error) {                Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error);            }        };        private void startCameraPreviewSession() {            try {                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);                mPreviewBuilder.addTarget(mEncoderSurface);                mCameraDevice.createCaptureSession(Arrays.asList(mEncoderSurface),                        new CameraCaptureSession.StateCallback() {                            @Override                            public void onConfigured(CameraCaptureSession session) {                                mSession = session;                                try {                                    mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);                                } catch (CameraAccessException e) {                                    e.printStackTrace();                                }                            }                            @Override                            public void onConfigureFailed(CameraCaptureSession session) {                            }                        }, mBackgroundHandler);            } catch (CameraAccessException e) {                e.printStackTrace();            }        }        public boolean isOpen() {            if (mCameraDevice == null) {                return false;            } else {                return true;            }        }        public void openCamera() {            try {                if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {                    mCameraManager.openCamera(mCameraID, mCameraCallback, mBackgroundHandler);                }            } catch (CameraAccessException e) {                Log.i(LOG_TAG, e.getMessage());            }        }        public void closeCamera() {            if (mCameraDevice != null) {                mCameraDevice.close();                mCameraDevice = null;            }        }        public void stopStreamingVideo() {            if (mCameraDevice != null & encoder != null) {                try {                    mSession.stopRepeating();                    mSession.abortCaptures();                } catch (CameraAccessException e) {                    e.printStackTrace();                }                encoder.stop();                encoder.release();                mEncoderSurface.release();                decoder.stop();                decoder.release();                decoder2.stop();                decoder2.release();                closeCamera();            }        }    }    private void setUpMediaCodec() {        try {            encoder = MediaCodec.createEncoderByType("video/avc"); // H264 кодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету кодека");        }        {            int width = 640; // ширина видео            int height = 480; // высота видео            int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; // формат ввода цвета            int videoBitrate = 2000000; // битрейт видео в bps (бит в секунду)            int videoFramePerSecond = 30; // FPS            int iframeInterval = 1; // I-Frame интервал в секундах            MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);            format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate);            format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond);            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);            encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // конфигурируем кодек как кодер           mEncoderSurface = encoder.createInputSurface(); // получаем Surface кодера        }        encoder.setCallback(new EncoderCallback());        encoder.start(); // запускаем кодер        Log.i(LOG_TAG, "запустили кодек");        try {            decoder = MediaCodec.createDecoderByType("video/avc");// H264 декодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету декодека");        }        int width = 480; // ширина видео        int height = 640; // высота видео        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);        format.setInteger(MediaFormat.KEY_ROTATION,90);        SurfaceTexture texture= mImageViewUp.getSurfaceTexture();        mDecoderSurface = new Surface(texture);        decoder.configure(format, mDecoderSurface, null,0);        decoder.setOutputSurface(mDecoderSurface);        decoder.setCallback(new DecoderCallback());        decoder.start();        Log.i(LOG_TAG, "запустили декодер");        try {            decoder2 = MediaCodec.createDecoderByType("video/avc");// H264 декодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету декодека");        }        int width2 = 480; // ширина видео        int height2 = 640; // высота видео        MediaFormat format2 = MediaFormat.createVideoFormat("video/avc", width2, height2);        format2.setInteger(MediaFormat.KEY_ROTATION,90);        SurfaceTexture texture2= mImageViewDown.getSurfaceTexture();        mDecoderSurface2 = new Surface(texture2);        decoder2.configure(format2, mDecoderSurface2, null,0);        decoder2.setOutputSurface(mDecoderSurface2);        decoder2.setCallback(new DecoderCallback2());        decoder2.start();        Log.i(LOG_TAG, "запустили декодер");    }    private class DecoderCallback2 extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {            decoderInputBuffer2 = codec.getInputBuffer(index);            decoderInputBuffer2.clear();            synchronized (out2)            {                b2 =  out2.toByteArray();                out2.reset();            }            decoderInputBuffer2.put(b2);            codec.queueInputBuffer(index, 0, b2.length,0, 0);            if (b2.length!=0)            {                //   Log.i(LOG_TAG, b.length + " декодер вход  "+index );            }        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            {                {                    decoderOutputBuffer2 = codec.getOutputBuffer(index);                    codec.releaseOutputBuffer(index, true);                }            }        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {            Log.i(LOG_TAG, "decoder output format changed: " + format);        }    }    //CALLBACK FOR DECODER    private class DecoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {            decoderInputBuffer = codec.getInputBuffer(index);            decoderInputBuffer.clear();            synchronized (out)            {                b =  out.toByteArray();                out.reset();            }            decoderInputBuffer.put(b);            codec.queueInputBuffer(index, 0, b.length,0, 0);                 if (b.length!=0)            {               //  Log.i(LOG_TAG, b.length + " декодер вход  "+index );            }        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            {                {                    decoderOutputBuffer = codec.getOutputBuffer(index);                    codec.releaseOutputBuffer(index, true);                }            }        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {            Log.i(LOG_TAG, "decoder output format changed: " + format);        }    }    private class EncoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {            Log.i(LOG_TAG, " входные буфера готовы" );        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            outPutByteBuffer = encoder.getOutputBuffer(index);            byte[] outDate = new byte[info.size];            outPutByteBuffer.get(outDate);            try {                //  Log.i(LOG_TAG, " outDate.length : " + outDate.length);                DatagramPacket packet = new DatagramPacket(outDate, outDate.length, address, port);                udpSocket.send(packet);            } catch (IOException e) {                Log.i(LOG_TAG, " не отправился UDP пакет");            }            encoder.releaseOutputBuffer(index, false);        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {            //  Log.i(LOG_TAG, "encoder output format changed: " + format);        }    }    @Override    public void onPause() {        if (myCameras[CAMERA1].isOpen()) {            myCameras[CAMERA1].closeCamera();        }        stopBackgroundThread();        super.onPause();    }    @Override    public void onResume() {        super.onResume();        startBackgroundThread();    }    public class Udp_recipient extends Thread {        Udp_recipient() {            start();            //    Log.i(LOG_TAG, "запустили прием данных по udp");        }        public void run() {            while (true) {                try {                    byte buffer[] = new byte[50000];                    DatagramPacket p = new DatagramPacket(buffer, buffer.length);                    udpSocketIn.receive(p);                    byte bBuffer[] = p.getData();                    outDataForEncoder = new byte[p.getLength()];                    synchronized (outDataForEncoder) {                        for (int i = 0; i < outDataForEncoder.length; i++) {                            outDataForEncoder[i] = bBuffer[i];                        }                    }                    mNewFrame = true;                    synchronized (out)                    {out.write(outDataForEncoder);}                    synchronized (out2)                    {out2.write(outDataForEncoder);}                } catch (Exception e) {                    Log.i(LOG_TAG, e + "hggh ");                }            }        }    }}       



Теперь в таргет адресе первого смартофона:

String ip_address = " допустим 192.168.50.131";

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

Гены Ардуинщика

21.06.2020 16:16:20 | Автор: admin


В очередной раз, при обдумывании самоделки на Atmega, встал вопрос проектирования соединений. В моем случае 12 внешних коннекторов и 21 связь. Расположение, соединение, пересечения, программирование, ошибки, ошибки, ошибки.

При кажущейся простоте задачи поломать мозг придется день, а то и два. Без опыта месяц.
И лень взяла свое.

Как это делается


Обычный алгоритм в таком случае:

  • используем макетную плату (самодельную или Arduino) не меняя или добавляя разъемы и разводку
  • подгоняем под макетку внешние коннекторы, чтобы не было пересечений
  • пишем программный код для настройки контроллера и макросы-переменные.

А что если последовательность пинов в коннекторах уже жестко заданы?

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

Попробуем решить задачу с помощью генетического алгоритма. И для начала определимся с моделью.

Список разъемов


  • Шина i2c для компаса I2C(SDA, SCL)
  • UART для связи с внешним миром UART(RXD, TXD)
  • Ультразвуковой сенсор SONAR1(Echo, Trig)
  • Управление маршевыми двигателями DRIVE(ENA, IN1, IN2, IN3, IN4, ENB) с помощью L298N длинный коннектор как раз для шлейфа
  • Энкодер на колесе:
  • левый ENCODER_L(IN)
  • правый ENCODER_R(IN)
  • Сенсор-выключатель впереди робота:
  • левый SENS_L(IN)
  • правый SENS_R(IN)
  • Включение питания Мозга (OPI PC) CPU(EN)
  • Включение турбины VAC_CLEAN(EN)
  • Включение веника BROOM(EN)
  • Напряжение батареи и потребляемый ток PWR(LVL, CUR).

Модель


Получается ген длиной 21 хромосом, которые отображают пины микроконтроллера, а точнее их соединения с коннекторами.

VCC и GND пины коннекторов игнорируем, так как шины мы можем вынести за коннекторы.
Каждая ячейка может иметь значение от 1 до 32 (количество лап у микросхемы). Значения в гене не могут повторяться.

Не допускается пересечений проводников (соединения делаются последовательно, если следующий пин занят проскакиваем далее)

Количество вариантов соединений:

$32^{21}=$ 40564819207303340847894502572032.

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

Ускоряемся


Для уменьшения пространства поиска используем функции пина коннектора (ADC, INT, PWM, PCINT). Например, если пин может быть только ADC, то вести к нему линию PWM или дискретного входа бессмысленно.

Данный фильтр уменьшает количество вариантов до 8 748 869 014 201 881 088. Разница ощутима. Но миллиарды миллиардов вариантов это тоже много.

Так же ранее использовались ручные эмпирические правила:

  • начинаем процесс соединения с уникальных пинов (SDA, SCL, RXD, TXD)
  • после соединяем разъемы с большим количеством пинов
  • последними соединяем пины с более общим спектром функций.

Но волшебный Collections.shuffle задающий последовательность коннекторов для обработки, решает эту задачу успешно на автомате.

Решение


Запускаем алгоритм и получаем решение для Atmega328p TQFP32. У меня на бюджетном ноутбуке находит менее чем за минуту.

1 SONAR1.Echo
2 SONAR1.Trig
9 DRIVE.ENB
10 DRIVE.IN4
11 DRIVE.IN3
12 DRIVE.IN2
13 DRIVE.IN1
14 DRIVE.ENA
15 ENCODER_L.IN
16 VAC_CLEAN.EN
17 CPU.EN
22 PWR.LVL
23 PWR.CUR
24 SENS_R.IN
25 SENS_L.IN
26 ENCODER_R.IN
27 I2C.SDA
28 I2C.SCL
30 UART.RXD
31 UART.TXD
32 BROOM.EN

Алгоритм находит решение за пару тысяч эпох. Иногда не находит и за миллион. В таком случаем просто надо перезапустить программу, потому что инициализация происходит случайно.
Без фильтра по функциям пинов алгоритм так же находит решение. Правда за 74 минуты и 13 перезапусков алгоритма по миллиону эпох на каждый. Перед каждым запуском делаем shuffle последовательности соединений.

Остается только соединить проводами макетку или нарисовать дорожки на плате с микроконтроллером.

Детали


Чтобы описать всё, надо будет написать не одну статью. Я постарался комментировать непонятные и самые интересные моменты в java-коде.

Желающие углубиться в тему могут заглянуть в git-проекта.
Подробнее..

Recovery mode Понятнее о S.O.L.I.D

29.06.2020 08:16:31 | Автор: admin
Большинство разработчиков с разговорами о принципах архитектурного дизайна, да и принципах чистой архитектуры вообще, обычно сталкивается разве что на очередном собеседовании. А зря. Мне приходилось работать с командами, которые ничего не слышали о S.O.L.I.D, и команды эти пролетали по срокам разработки на многие месяцы. Другие же команды, которые следовали принципам дизайна и тратили очень много времени на буквоедство, соблюдение принципов чистой архитектуры, код-ревью и написание тестов, в результате значительно экономили время Заказчика, писали лёгкий, чистый, удобочитаемый код, и, самое главное, получали от этого кайф.

Сегодня мы поговорим о том, как следовать принципам S.O.L.I.D и получать от этого удовольствие.



Что такое S.O.L.I.D? Погуглите и получите 5 принципов, которые в 90% случаев описываются очень скупо. Скупость эта потом выливается в непонимание и долгие споры. Я же предлагаю вернуться к одному из признанных источников и хотя бы на время закрыть этот вопрос.

Источником принципов S.O.L.I.D принято считать книгу Роберта Мартина Чистая архитектура. Если у Вас есть время прочесть книгу, лучше отложите эту статью и почитайте книгу. Если времени у Вас нет, а завтра собес велком.

Итак, 5 принципов:

Single Responsibility Principle принцип единственной ответственности.
Open-Closed Principle принцип открытости/закрытости.
Liskov Substitution Principle принцип подстановки Барбары Лисков.
Interface Segregation Principle принцип разделения интерфейсов.
Dependency Inversion Principle принцип инверсии зависимости.


Разберём каждый из принципов. В примерах я буду использовать Java и местами Kotlin.

Single Responsibility Principle принцип единственной ответственности.


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

Сформулировать его можно так:

Модуль должен иметь только одну причину для изменения. Или: модуль должен отвечать только за одну заинтересованную группу.

Вот тут становится непонятно, поэтому мы обратимся к признанному первоисточнику книге Роберта Мартина, у которого есть отличный пример на этот счёт. Воспользуемся им.

Предположим, что существует какая-то система, в которой ведётся учёт сотрудников. Сотрудники интересны как отделу кадров, так и бухгалтерии. Для их нужд, в числе прочих, в сервисе EmployeeService есть 2 метода:

fun calculateSalary(employeeId: Long): Double //рассчитывает зарплату сотрудникаfun calculateOvertimeHours(employeeId: Long): Double //рассчитывает сверхурочные часы

Методом расчёта зарплаты пользуется бухгалтерия. Методом расчёта сверхурочных часов пользуется отдел кадров.

Логично, что для расчёта зарплаты бухгалтерии может потребоваться учесть сверхурочные часы сотрудника. В таком случае, метод calculateSalary() может вызвать calculateOvertimeHours() и применить его результаты в своих формулах.

Окей, прошло полгода, и в отделе кадров решили поменять алгоритм расчёта сверхурочных. Предположим, раньше один сверхурочный час рассчитывается не по коэффициенту * 2, а стал по коэффициенту * 2,5. Разработчик получил задание, изменил формулу, проверил, что всё работает, и успокоился. Город засыпает, просыпается бухгалтерия. А у бухгалтерии ничего не поменялось, они считают зарплаты по тем же формулам, вот только в этой формуле теперь будут другие цифры, потому что calculateSalary() ходит в calculateOvertimeHours(), а там теперь по просьбе отдела кадров сверхурочные не * 2, а * 2,5. Упс

Теперь, если мы вернёмся к описанию принципа, всё станет намного понятнее.

Модуль должен иметь только одну причину для изменения. Эта причина, в нашем случае потребности только отдела кадров, но не бухгалтерии. Модуль должен отвечать только за одну заинтересованную группу. calculateOvertimeHours() должен обрабатывать только одну группу отдел кадров, несмотря на то, что он может показаться универсальным. Работа этого метода никаким образом не должна касаться бухгалтерии.

Теперь, я надеюсь, стало понятнее.

Каким может быть решение проблемы выше?

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

Open-Closed Principle принцип открытости/закрытости.


Принцип гласит:

Программные сущности должны быть открыты для расширения и закрыты для изменения.

Возьмём пример из суровой реальности.

У нас есть приложение, и, предположим, довольно неплохое. В нём есть сервис, который пользуется внешним сервисом. Предположим, это сервис котировок ЦБ РФ. В простой реализации архитектура этого сервиса будет выглядеть следующим образом:



Вроде всё неплохо, но проект пора отдавать на интеграционное тестирование, и нам нужно отключить запросы в ЦБ. Как бы мы ни попытались решить эту проблему, нам будет необходимо изменить текущую реализацию FinancialQuotesRestService так, чтобы она не отправляла больше запросы. Но в чистой архитектуре такой подход недопустим, поскольку нарушается Принцип открытости-закрытости старый код не должен меняться.

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

Конечно же, через интерфейсы. Стоит нам разделить объявление функциональности и её реализацию, как эта проблема будет решена. Теперь мы можем реализовать интерфейс FinancialQuotesRestService сколько угодно раз; в нашем случае, это будет FinancialQuotesRestServiceImpl и FinancialQuotesRestServiceMock. На тестовом стенде приложение будет запускаться по Mock-профилю, на продакшене оно будет запускаться в обычном режиме.



Далее, если мы захотим добавить ещё одну реализацию, нам не придётся менять существующий код. Например, мы хотим добавить получение котировок не ЦБ, а в некоторых случаях Сбербанка. Нет ничего проще:



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

Liskov Substitution Principle принцип подстановки Барбары Лисков.


Принцип подстановки Барбары Лисков можно сформулировать так:

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

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

public interface FinancialQuotesRestService {    List<Quote> getQuotes();}

В данном контракте прописано, что в используемом интерфейсе используется метод getCuotes, который не принимает аргументы, возвращая при этом список котировок. И это всё, что нужно знать классам, которые будут его использовать.

В нашем случае, контроллер будет выглядеть так:

@RestController("/quotes")@RequiredArgsConstructorpublic class FinancialQuotesController {        private final FinancialQuotesRestService service;        @GetMapping    public ResponseEntity<List<Quote>> getQuotes() {        return ResponseEntity.ok(service.getQuotes());    }}

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

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

Interface Segregation Principle принцип разделения интерфейсов.


Принцип можно сформулировать так:

Необходимо избегать зависимости от того, что не используется.

Например, мы решили реализовать CRUD-интерфейс, который будет поддерживать операции CRUD. И реализовать его во всех сервисах. Выглядеть интерфейс может так:

public interface CrudService<T> {        T create(T t);        T update(T t);        T get(Long id);        void delete(Long id);}

Окей, мы реализовали его в сервисе UserService:

@Service@RequiredArgsConstructorpublic class UserService implements CrudService<User> {    private UserRepository repository;        @Override    public User create(User user) {        return repository.save(user);    }    @Override    public User update(User user) {        return repository.update(user);    }    @Override    public User get(Long id) {        return repository.find(id);    }    @Override    public void delete(Long id) {        repository.delete(id);    }}

Далее, нам потребовалось реализовать класс PersonService, который мы тоже наследуем от CrudService. Но проблема в том, что сущности Person не удаляемые, и нам не нужна реализация метода delete(). Как быть? Можно сделать так, например:

    @Override    public void delete(Long id) {        //не реализуется    }

И это будет ужасно. Мало того, что мы имеем в классе мёртвый код, который выглядит, как живой, так его кто-то ещё может и вызвать, не зная, что удаление не произойдёт.

Что делать в такой ситуации?

Выделить реализацию CRUD без удаления в отдельный интерфейс. Например, так:

public interface CruService<T> {    T create(T t);    T update(T t);    T get(Long id);}

Да простит меня читатель за такое именование интерфейса, но для наглядности самое то.

@Service@RequiredArgsConstructorpublic class PersonService implements CruService<Person> {    private final PersonRepository repository;    @Override    public Person create(Person person) {        return repository.save(person);    }    @Override    public Person update(Person person) {        return repository.update(person);    }    @Override    public Person get(Long id) {        return repository.find(id);    }}

Теперь сущность Person нельзя удалить! PersonService реализует интерфейс, в котором не объявлено ничего лишнего. В этом и есть соблюдение Принципа разделения интерфейсов.

Dependency Inversion Principle принцип инверсии зависимости.


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

Связи должны строиться на основе абстракций. Если вы вызываете какой-то сервис (например, автовайрите его), Вы должны вызывать его интерфейс, а не реализацию.

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

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

В общем, Принцип инверсии зависимости можно постулировать так:

Абстракции стабильны. Реализации нестабильны. Строить зависимости необходимо на основе стабильных компонентов. Стройте зависимости от абстракций. Не стройте их от реализаций.

Заключение.


Мы, разработчики, не только пишем код (и это лучшие моменты в нашей работе). Мы вынуждены его поддерживать. Чтобы эти моменты нашей работы не стали худшими, используйте принципы S.O.L.I.D. Использование принципов S.O.L.I.D, по моему опыту, окупается в течение одного спринта. Потом сами себе скажете спасибо.
Подробнее..

Подписка на JetBrains Academy готовимся к запуску и отвечаем на вопросы

24.06.2020 18:08:11 | Автор: admin

Привет, Хабр!


Недавно мы объявили, что ранний доступ к JetBrains Academy завершается и платформа переходит на модель подписки. Это важный шаг для продукта. Поэтому мы постарались получить как можно больше обратной связи и собрать как можно больше вопросов. Здесь мы постараемся ответить на самые популярные из них.


Почему платформа становится платной?


Мы хотим, чтобы платформа JetBrains Academy продолжала развиваться. Платный продукт сам обеспечивает свое устойчивое развитие. Бесплатные продукты тоже имеют цену, просто оплачиваются по-другому. Мы не скрываем своих намерений, и наша ценовая политика максимально прозрачна. Создавая платный продукт, мы, как разработчики, можем позволить себе установить более высокие требования к качеству учебного контента и функциональности продукта, а также получать более прямую и актуальную обратную связь.


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



$50 в месяц, пожалуй, слишком высокая цена для тех, кто хочет научиться программировать и не имеет постоянной работы. Есть ли какие-то скидки или программы бесплатного доступа?


Сейчас мы обсуждаем варианты специальных программ для студентов вузов и пользователей с ограниченными финансовыми возможностями. Мы также планируем награждать самых активных пользователей. Мы обязательно расскажем о специальных программах до конца года. А пока приглашаем вас зарегистрироваться до 1 июля 2020 года: вы получите бесплатный доступ до 1 января 2021 года.


Планируется ли годовая подписка?


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


Будут ли скидки для тех, у кого есть платная лицензия на продукты JetBrains? Планируете ли вы включить в подписку лицензии на инструменты JetBrains?


JetBrains Academy это самостоятельный продукт со своей аудиторией и собственным планом подписки. Однако JetBrains Academy предназначена как для начинающих, так и для опытных разработчиков, а также интегрируется с инструментами JetBrains, поэтому мы рассматриваем возможность выпуска комплектов лицензий, объединяющих в себе JetBrains Academy и наши платные IDE. Сейчас для изучения Java и Python в JetBrains Academy можно бесплатно пользоваться IntelliJ IDEA Edu и PyCharm Edu.


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


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


Не планируете ли вы заменить модель подписки оплатой отдельных курсов или проектов либо добавить встроенные покупки гемов, решений и т. п.?


Концепция JetBrains Academy состоит не в том, чтобы изучать отдельные темы и выборочно выполнять задания. Ценность платформы во всестороннем учебном опыте, который основывается на решении практических задач. Одна из главных идей заключается в том, что отдельные элементы платформы формируют широкое образовательное пространство для получения новых знаний и навыков необходимы все элементы. Поэтому в ближайшее время мы не планируем отказываться от модели подписки, но рассматриваем возможность бесплатного доступа к учебным направлениям, находящимся в разработке (бета-трекам). Мы также обсуждаем различные варианты подписки, которые откроют доступ к дополнительной функциональности платформы.


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


К нашей целевой аудитории относятся как начинающие, так и опытные разработчики. Мы начали с курсов по Java и Python, которые подходят для тех, кто хочет попробовать себя в программировании и сменить профессию. Однако мы планируем добавить и более продвинутый контент, который поможет профессиональным разработчикам заполнить пробелы в знаниях.


Сколько времени занимает прохождение курса?


В среднем курс по Java занимает 128 часов, Python 70 часов. Так что если вы планируете заниматься по 4 часа в неделю, вам потребуется примерно 7 месяцев для прохождения курса по Java и около 4 месяцев для освоения Python. Эти оценки основаны на средней скорости прохождения курсов: имейте в виду, что опытные разработчики быстро проходят базовые разделы. Если вы только начинаете программировать, вам может потребоваться больше времени. Мы работаем над тем, чтобы более точно оценивать время прохождения курсов людьми с разным уровнем подготовки.


Обучение в JetBrains Academy поможет мне устроиться на работу?


Обучение в JetBrains Academy определенно поможет вам подготовиться к первому собеседованию. Более сложные проекты, над которыми вам предстоит работать во время учебы, основаны на тестовых заданиях для младших разработчиков. Но у нас нет никаких договоренностей с IT-компаниями, поэтому мы не можем гарантировать ваше трудоустройство.


По окончании обучения я получу сертификат?


На данный момент сертификатов мы не выдаем. Оставляйте комментарии и голосуйте за эту возможность в нашем трекере.


Планируете ли вы вернуть код-ревью?


Это была экспериментальная функциональность. Теперь нам нужно оценить ее результаты. Мы обдумываем введение программы менторства код-ревью может стать ее частью.


Вы проводили опрос для потенциальных менторов. Какие планы в этом направлении?


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


Когда ждать курсов по Android, Data Science и Frontend-разработке? Планируете ли вы расширять список языков?


Сейчас мы сосредоточены на качестве треков по Java, Python и Kotlin. За время EAP мы получили много отзывов, и нам есть что улучшать. Мы продолжим выпускать новые бета-треки, но сейчас они не являются приоритетной задачей. Мы открыты для ваших идей о том, какие языки программирования и технологии нам стоит поддержать.


В JetBrains Academy планируется использование контента, создаваемого сообществом? Было бы здорово, если бы участники сообщества могли сами создавать учебные материалы и получать за это вознаграждение.


Мы видим будущее JetBrains Academy именно таким, но понимаем, что до этого пока далеко и стоит начинать с малого. Первый шаг уже сделан: учащиеся теперь могут делиться своими решениями и учиться друг у друга. На следующем этапе учащиеся смогут добавлять свои задачи эта возможность уже включена в план разработки. Вы также можете стать контрибьютором.




Если вы не нашли ответа на свой вопрос, задайте его в комментариях или напишите нам в Twitter или на Reddit.


Материалы JetBrains Academy, включая теоретические разделы и описания проектов, сейчас доступны только на английском языке.


Ваша команда JetBrains Academy

Подробнее..

Перевод Kotlin vs Java

24.06.2020 18:08:11 | Автор: admin
И снова здравствуйте. В преддверии старта нового курса Backend-разработка на Kotlin, мы подготовили для вас перевод статьи, в которой рассказывается о том, чем же Kotlin отличается от Java.




Kotlin новый язык программирования, который заставит вас отказаться от Java. На европейской конференции Zebra APPFORUM 2017 в Праге наш Android-разработчик Питер Оттен вдохновлял других начать писать на Kotlin. Расстроены, что пропустили? Не переживайте! Питер расскажет вам, почему он стал большим поклонником этого языка.

Подъем


Kotlin это язык программирования, разработанный компанией Jetbrains, которая знаменита IntelliJ, WebStorm, PhpStorm и плагином ReSharper для Visual Studio. Они занялись поисками нового языка программирования, который был бы лучше, чем Java, но все еще функционально совместим с ним. Вдохновляясь языками Java, C#, Scala и Groovy, ребята из Jetbrains собрали команду для разработки нового языка программирования. Kotlin проектировался людьми, которые испытали всю боль Java.

Так что же такое Kotlin?


Kotlin был впервые представлен в 2011 году, а в феврале 2016 года появилась его версия 1.0 stable release, затем 1.1 в марте. Язык программирования с открытым исходным кодом компилируется в JVM (Java Virtual Machine), Android и JavaScript. Таким образом, Kotlin может использоваться одновременно на JVM и Android-устройствах (интероперабельность). Также он может запускаться на фронтенде с помощью JavaScript. Google официально объявила на своей конференции I/O в мае, что Kotlin стал официально поддерживаемым языком для Android-разработки. С тех пор интерес к языку, его применение и сообщество выросли в разы.



По сравнению с Java


Для сравнения Java и Kotlin на презентации был приведен в пример класс POJO и то, как его можно использовать (рисунок выше). Здесь можно увидеть всю силу и лаконичность Kotlin, когда простой класс Person (с именем, геттером/сеттером и стандартными методами POJO) заменяется ключевым словом data. Также, глядя на использование класса Person можно заметить следующие различия:

  • Методы в Kotlin называются fun (это сразу же делает программирование веселее);
  • Если метод ничего не возвращает, то тип возвращаемого значения указывать не надо (в Java нужно писать void);
  • В Kotlin есть интерференция типов, поэтому указывать, что name имеет тип String не нужно. Простой используйте val.
  • Также эта особенность делает переменную неизменяемой. Если вы захотите изменить переменную name, нужно использовать var. Так Kotlin заставляет вас при написании кода заранее думать о переменных/полях и т.д.
  • Для создания нового экземпляра класса ключевое слово new не нужно;
  • Kotlin и Java совместимы. Kotlin без проблем может использовать класс Person, написанный на Java.
  • Геттеры и сеттеры из кода на Java автоматически станут свойствами, то есть getName() в Kotlin будет недоступно, но можно использовать свойство name из Person.



Выводы Mediaan об использовании Kotlin


После посещения других докладов о Kotlin на Droidcon в 2015 и 2016 в Лондоне и GDG DevFest 2016 в Амстердаме наша команда мобильных разработчиков решила, что пришло время взглянуть на новый язык. Мы начали использовать его в октябре 2016 года и просто влюбились в него. Первый новый проект под Android уже был на 100% написан на Kotlin. С тех пор мы не возвращались к Android-разработке на Java.

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

  • Код более лаконичен, то есть вы сможете писать и поддерживать меньшее количество кода;
  • Код более безопасный, поскольку язык заставляет вас думать о (не)изменяемости, nullability и т.д. во время написания кода;
  • Вышесказанное также ускоряет развитие. Вам становится проще делать сложны вещи.
  • Отличная поддержка IDE (мы пользуемся Android Studio);
  • Большое и растущее сообщество. Существующие библиотеки поддерживаются или мигрируют в Kotlin, интернет переполнен постами в блогах, видео, презентациями и, конечно же, вопросами/ответами на
    StackOverflow.
  • Kotlin уже готов к продакшену. Мы используем его на продакшене также, как и многие компании, такие как Square, Pinterest, Trello и Google.

Теперь Google объявила, что Kotlin официальный язык для разработки под Android, и у вас практически нет причин, чтобы не взглянуть на этот язык и не начать его использовать.

Итак, начнете ли вы с Kotlin или перейдете на него?


Есть множество ссылок, которые помогут вам в его освоении:


Кроме того, в плагине для Android Studio есть возможность быстро конвертировать ваш текущий открытый файл на Java в Kotlin. Работает достаточно хорошо. Возможно, вам потребуется подправить кое-какие мелочи в коде, но вы мгновенно получите Kotlin-версию вашего кода на Java для сравнения и изучения. Есть еще одна удивительная возможность: когда вы работаете в файле на Kotlin и вставляете в него фрагмент кода на Java, он автоматически преобразуется в код на Kotlin при вставке.

Чтобы использовать Kotlin в уже существующем проекте или чтобы полностью перенести существующий проект на него, мы рекомендуем следующий подход:

  • Используйте интероперабельность (Kotlin и Java могут работать бок о бок);
  • Используйте плагин для конвертации файлов и фрагментов кода.
  • Все новые функции пишите на Kotlin;
  • Все, что вы редактируете, конвертируйте из Java в Kotlin.

О будущем


Помимо поддержки JVM, Android и JavaScript, Kotlin работает над поддержкой большего числа платформ. Поддержка машинного кода это следующий большой шаг. Например, запустить код на RaspBerry Pi уже можно (в бета версии). Jetbrains упорно работает над добавлением поддержки для iOS, MacOS и Windows. Это значит, что Kotlin может однажды стать основной нового кроссплатформенного решения для приложений. Больше информации о дальнейшем развитии вы сможете узнать на KotlinConf, их собственной конференции в конце этого года в Сан-Франциско.



Узнать подробнее о курсе.


Подробнее..

JPoint 2020 новый формат, новые возможности

04.07.2020 20:20:46 | Автор: admin
С 29 июня по 3 июля 2020 года в онлайн-формате прошла Java-конференция JPoint 2020. Информация о докладах, спикерах, особенностях проведения, впечатления от конференции всё это можно прочитать далее.



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

В предверии летнего блока конференций участники команды JUG Ru Group проделали титанический объём работы как административного, так и технического характера. Была создана онлайн-платформа для трансляции митапов и конференций. Также было проведено множество онлайн-встреч, в том числе Java-серия Первая чашка кофе с JPoint с интервью с участниками программного комитета и спикерами: Владимиром Ситниковым, Маргаритой Недзельской, Тагиром Валеевым, Олегом Докукой, Иваном Углянским и Алексеем Шипилёвым.

В блоге компании JUG Ru Group до летних конференций появилось множество интересных статей и интервью:

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

Открытие


Непосредственно перед открытием Алексей Фёдоров предоставил возможность увидеть пультовую Всевластья, помогающую организовать онлайн-конференции. После вводных слов и приветствия спонсоров конфереция была объявлена открытой.



Первый день


Прекрасным предисловием к интервью с James Gosling, отцом языка Java, стала статья, написанная phillennium. Беседу вели и задавали вопросы Андрей Дмитриев и Volker Simonis. Интервью получилось живое и эмоциональное, вызвавшее большой интерес у самого Джеймса. Было задано множество вопросов, от касающихся подробностей его прошлых проектов до отношения к популярному в настоящее время JVM-языку Kotlin. Безусловно, Джеймс является личностью, колоссальным образом повлиявшей на индустрию и внёсшей огромный вклад. Его присутствие в числе спикеров большая удача для конференции.



В перерыве между большими докладами можно было посмотреть познавательные интервью, одним из которых стало ML и AI: Как сейчас выглядит разработка решений в крупных компаниях Андрея Дмитриева с Дмитрием Бугайченко про машинное обучение и искусственный интеллект. Достаточно интересно было послушать мнение Дмитрия, являющегося экспертом в этой области и докладчиком этой и других конференций JUG Ru Group.



Доклад Precomputed data access with Micronaut Data от Graeme Rocher, автора Micronaut Framework. У данного спикера на конференции два доклада (доклад Micronaut deep dive был в этот же день чуть раньше, его я ещё планирую посмотреть). Очень полезным оказалось предварительное ознакомление с интервью, взятым недавно. В данном докладе было рассказано про Micronaut Data, легковесное решение для доступа к базам данных, выглядящее чрезвычайно привлекательно. После доклада Грэму вопросы слушателей и свои задавал Антон Архипов. На интересующий многих заданный Антоном вопрос, возможно ли использование Micronaut Data без всего остального из Micronaut Framework, был дан положительный ответ.



Второй день


В нативный код из уютного мира Java: Путешествие туда и обратно блестящий доклад Ивана Углянского на тему возможностей вызова из Java-кода процедур и функций нативных (native) библиотек. Всеобъемлющая ретроспектива существовавших до JNI альтернатив (JDK 1.0 NMI, RNI, JRI), популярных существующих сейчас (JNA, JNR, JavaCPP) и перспективных пока что экспериментальных (Panama, Sulong). Подробное сравнение всего современного вышеперечисленного (начиная с JNI) с большим количеством слайдов говорит об огромной проделанной работе. Очень удачные выбранные аналогии на тему произведений Толкиена: левый слайд (Шир) иллюстрирует милый и безопасный Java-код, правый слайд опасный нативный код (Мордор).



How to develop a successful Kubernetes native application using Quarkus небольшой пятнадцатиминутный доклад Alex Soto Bueno от компании RedHat, спонсора конференции. Доклад о разработке микросервисов с использованием Kubernetes и фреймворка Quarkus, детища RedHat.



Олег Шелаев является одним из тех спикеров, доклады которых всегда можно смело выбирать, зная, что совершенно точно будет интересно, увлекательно и полезно. Обладает редкой способностью просто объяснять очень сложные с технической точки зрения вещи. Доклад под названием Polyglot done right with GraalVM не стал исключением в этом смысле. В нём Олег продолжил раскрывать тему GraalVM, являясь developer advocate проекта GraalVM в OracleLabs. В данном докладе более полно была раскрыта направленность продукта на возможность одновременного применения различных языков программирования: API, шаблоны взаимодействия и прочие детали GraalVM. Ожидания от прослушанного полностью оправдались, отличный доклад.



Третий день


Всеволод Брекелов входит в команду JUG Ru Group, активно участвуя в проведении летнего блока конференций, к которому относится и конференция JPoint. Тем интереснее, регулярно видя его в роли ведущего конференций, было посмотреть доклад в его исполнении под названием Contract testing: Should or shouldn't? Ему очень удачно помогали Андрей Дмитриев, Владимир Плизга и Алексей Виноградов например, представление Владимиром докладчика в самом начале просто восхищает оригинальностью. Обсуждение было посвящено контрактным тестам, были последовательно продемонстрированы несколько подходов с использованием Spring Cloud Contract, Pact и Protocol Buffers. Получилось зажигательно и интересно.



Доклад Страх и ненависть в Scala и Kotlin interop от Маргариты Недзельской был посвящён проблемам взаимодействия кода, написанного на двух JVM-языках Kotlin и Scala. Название доклада является аллюзией на фильм Fear and Loathing in Las Vegas, им же достаточно оригинально был проиллюстрирован весь рассказ. Проблемы вызвали искреннее сочувствие, технические подробности были приведены весьма убедительные. Маргарите помогали Паша Финкельштейн и Евгений Мандриков, ведя беседу, озвучивая результаты голосований и задавая вопросы слушателей.



Четвёртый день


Ещё немного маленьких оптимизаций стал своеобразным продолжением доклада, сделанным на конференции Joker 2019 тем же автором, Тагиром Валеевым. Доклад первой части был посвящён улучшениям в строках, коллекциях и операциям с числами, в этот раз уже другим оптимизациям тоже в строках, коллекциях и теперь ещё и в reflection. Изменения, о которых было рассказано, произошли в версиях Java с 9 по 16. Традиционное глубокое понимание темы, множество результатов сравнений, характерные для докладов Тагира всё это было и в этот раз.



На Интервью и Q&A с Алексеем Шипилёвым интервьюеры Алексей Фёдоров и Иван Крылов поговорили и задали вопросы Алексею Шипилёву об особенностях работы в Red Hat, про используемые инструменты performance-инженера, про различия сборщиков мусора в Java, историю создания Shenandoah GC, об отношении к статьям с замерами производительности, мнении о GraalVM, про совместное использование jmh и async-profiler, о советах для молодых разработчиков и инженеров.



Пятый день


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



Внедрение open source-решений на примере Одноклассников: интервью Дмитрия Чуйко с Андреем Паньгиным. Одной из тем разговора стал переход компанией Одноклассники на использование дистрибутива Liberica JDK компании BellSoft, поэтому представляющий BellSoft Дмитрий Чуйко в качестве берущего интервью был весьма уместен. Также были упомянуты популярные проекты Андрея one-nio и async-profile, тоже являющиеся open source-решениями и вызывающие интерес и уважение.



Доклад Valhalla is coming от Сергея Куксенко был продолжение его же предыдущего доклада, сделанного им на Joker 2019. С конца октября 2019 года в разработке инлайн-типов произошли значительные изменения, подробно о которых было рассказано примерно с середины данного доклада. Сергей харизматичный спикер и высококвалифицированный инженер, доклады которого безошибочно всегда можно выбирать. Отлично дополнил доклад Иван Углянский, задававший вопросы и помогавший Сергею во взаимодействии со слушателями.



Прочие события


Кроме впечатляющей онлайн-платформы для стриминга конференций, всевозможных активностей во время их проведения к летним конференциям была выпущена новая версия веб-приложения, о котором ранее уже писалось в обзорах про конференции TechTrain 2019 и Joker 2019. Приложение доступно по ссылке, в репозитории на GitHub (ставьте звёздочки) имеется описание с информацией, включающей актуальную ссылку на веб-сайт.

Приложение, ранее бывшее только игрой по угадыванию спикера, теперь разделено на две части. В первой из них можно произвести поиск и просмотр информации обо всех конференциях JUG Ru Group, а также митапах Java-сообществ JUG.ru, JUG.MSK, JUGNsk. Содержится абсолютно та же информация, что и представленная на сайтах конференций и митапов. Доступны для удобного просмотра уже опубликованные видео и презентации докладов (ниже для примера показано отображение сведений об Антоне Архипове и об одном из его докладов).



В разделе со статистикой приведены сведения, которые могут заинтересовать как организаторов конференций, так и их участников: с какого времени проводится каждая из конференций или каждый из митапов, общая их длительность, количество конференций, докладов и спикеров, сколько из спикеров удостоено звания Java Champion или Most Valuable Professional (MVP). Можно щёлкнуть по картинкам для их увеличения (или посмотреть то же самостоятельно в веб-приложении по ссылке, приведённой выше).

Второй и третий скриншоты ниже показывают топ спикеров по количеству сделанных ими докладов (скриншот слева без учёта митапов, справа конференции вместе с митапами). Уверенную победу в обоих случаях (только конференции и конференции с митапами) одерживает Барух Садогурский, на втором месте Евгений Борисов. Третье месте в случае только конференций Кирилл Толкачёв, конференции с митапами Алексей Шипилёв.



В игре Угадай спикера, второй части веб-приложения, после загрузки данных обо всех конференциях и митапах стало возможным использовать все ранее доступные режимы угадывания для конкретной конференции (например, JPoint 2020). По умолчанию для угадывания предлагается в данный момент идущая либо ближайшая конференция. Дополнительно были реализованы возможности попытаться угадать Twitter, GitHub спикеров и, наоборот, спикера по представленному их Twitter, GitHub.



Закрытие


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



Совершенно точно, что в сложившейся сегодня ситуации команда JUG Ru Group сделала абсолютно всё возможное (мне кажется, что даже чуточку больше), чтобы праздник, которым конференция всегда бывает и для спикеров, и для обычных участников, состоялся. В какой-то степени, праздник можно продлить у каждого осталось много замечательных докладов, оставшихся пока непросмотренными, и имеется возможность поделиться своими впечатлениями от конференции в комментариях к статье.

Сезон летних конференций JUG Ru Group продолжается по-прежнему можно успеть присоединиться к оставшимся двум онлайн-конференциям DevOops (6-10 июля 2020 года) и Hydra (6-9 июля 2020 года). Есть возможность купить единый билет на все восемь конференций, видео докладов в этом случае становятся доступны сразу же после завершения конференций.
Подробнее..

Перевод Управление несколькими JDK в Mac OS, Linux и Windows WSL2

23.06.2020 16:19:11 | Автор: admin
И снова здравствуйте. В преддверии старта курса Разработчик Java подготовили для вас перевод интересной статьи.



Если вам, как и мне, приходится одновременно работать с несколькими проектами (как с легаси, так и с современными), а возможно, вы разрабатываете какую-то библиотеку или утилиту для пользователей с разными версиями Java, то вы поймете проблему с переключением между разными JDK.



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

Ручной способ


Вы можете скачать дистрибутивы с сайта вендора JDK и установить их все вручную (или просто распаковать их в какую-нибудь папку вроде $user/jdks) и все. Но тогда вам придется каждый раз проверять и обновлять JAVA_HOME, чтобы там была нужная вам JDK.

Это можно сделать с помощью bash-скриптов, bash-функций и т. д. В чем же тогда проблема? Вы просто можете забыть вызвать скрипт / функцию и поймете, что что-то пошло не так, только когда увидите UnsupportedClassVersionError.

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

Лучший способ


На мой взгляд, идеальным решением будет объединение двух open source инструментов, которые существуют уже довольно давно, и возможно, вы знакомы, по крайней мере, с одним из них. Но что вы не знаете, так это то, что их можно использовать вместе.



Давайте посмотрим на их совместное использование.

Установка нескольких JDK



После установки SDKMAN! наберите:

$ sdk list java


Найдите нужного вендора, версию и введите:

$ sdk install java <id>


Давайте установим четыре JDK: две последние версии Oracle OpenJDK и две LTS-версии AdoptOpenJDK.

При установке не выбирайте JDK по умолчанию.

  1. AdoptOpenJDK сборка OpenJDK 8u252 LTS
  2. AdoptOpenJDK сборка OpenJDK 11.0.7 LTS
  3. Oracle OpenJDK 14 Последний GA
  4. Oracle OpenJDK 15 Early Access сборка


Эти четыре версии должны отвечать вашим потребностям для легаси-проектов на Java 8, для современных проектов на Java 11, а также для экспериментов с нововведениями в Java 14 и Java 15.

Отлично. У вас есть четыре JDK, установленные локально с помощью SDKMAN! Я думаю, что эта утилита действительно удобна для установки JDK и, я надеюсь, она вам тоже понравилась.

Все установленные JDK можно найти в следующем каталоге:

$ cd /Users/bruno/.sdkman/candidates/java$ ls11.0.7.hs-adpt  14.0.1-open     15.ea.20-open   8.0.252.hs-adpt


Если у вас нет необходимости часто переключаться между разными JDK, то этого будет достаточно. Вы можете использовать SDKMAN! для выбора JDK:

$ sdk current javaUsing java version 15.ea.19-open$ sdk default java 15.ea.20-openDefault java version set to 15.ea.20-open


На этом можно остановиться и использовать только SDKMAN!, но рано или поздно, вы забудете переключить JDK точно так же как при ручном подходе. Поэтому я предпочитаю автоматическое переключение JDK.

Управление несколькими JDK с помощью jEnv




Несмотря на то что SDKMAN! удобен для установки JDK, но он не помогает при автоматическом переключении между разными JDK при переходе от одного проекта к другому. Здесь нам поможет jEnv.

После того как вы установите несколько JDK, с помощью SDKMAN! или любыми другими способами, вам нужно будет добавить их в jEnv:

$ cd ~/.sdkman/candidates/java$ ls -111.0.7.hs-adpt 14.0.1-open    15.ea.20-open8.0.252.hs-adpt$ jenv add 15.ea.20-openopenjdk64-15-ea added15-ea added15-ea added...$ jenv versions system 1.8 1.8.0.252 11.0 11.0.7 14.0 14.0.1* 15-ea (set by /Users/bruno/.jenv/version) openjdk64-1.8.0.252 openjdk64-11.0.7 openjdk64-14.0.1 openjdk64-15-ea


Повторите команду jenv add для оставшихся трех версий JDK.

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

  1. Установить версию Java глобально для вашей системы.
  2. Установить версию Java для текущего каталога / проекта, в котором вы находитесь.
  3. Установить версию Java для текущего shellа.


jEnv использует shim и автоматически управляет переменной окружения JAVA_HOME. Таким образом, после того как вы настроите вашу систему, jEnv будет переключать версию JDK на наиболее подходящую, на основе вышеуказанных приоритетов и текущей директории. Круто! У него есть и другие полезные функции дополнительную информацию смотрите на сайте.

Автоматическое переключение между JDK


Теперь у вас установлены Java 8, 11, 14 и 15-EA. Итак, как же переключаться между ними? Давайте настроим версии и протестируем переключение.

Сначала нужно указать дефолтную глобальную JDK в системе. Для этого я обычно использую ранние сборки (Early Access). Всякий раз, когда я начинаю работать с новым проектом, я автоматически использую предстоящий релиз, что помогает мне выявить потенциальные проблемы, которые я могу сообщить в проект OpenJDK. Вы же тоже так делаете

$ jenv global 15


Теперь для любой папки, в которой вы находитесь, при выполнении java -version вы получите OpenJDK 15 EA.
Если у вас есть проект, который должен использовать Java 8, то перейдите в папку и запустите:

$ jenv local 1.8


В результате будет создан файл под названием .java-version со следующим содержимым:

$ cat .java-version1.8


Этот файл сообщает jEnv, какую JDK использовать, когда вы находитесь в этой папке.

Наконец, если вы и хотите временно переключиться на другую версию JDK, то используйте jenv shell.

Здесь есть демо, которое может пояснить все вышеописанное и как работает автопереключение.

После публикации статьи на Reddit были несколько интересных комментариев, о которых стоит здесь сказать:

  • SDKMAN! с версии 5.8.1+ поддерживает sdk env и там тоже доступно автоматическое переключение.
  • direnv очень хорош и не привязан к java. Делает то же самое, что и jEnv, за исключением того, что не помогает вам легко переключать JDK. Вы должны редактировать файлы вручную.
  • autoenv еще одна альтернатива direnv с теми же ограничениями.
  • jabba наиболее перспективная альтернатива сочетанию SDKMAN + jEnv, поскольку поддерживает весь их функционал и работает в Windows (PowerShell), что не поддерживается другими, упомянутыми выше, инструментами.

Надеюсь, вам понравилось прочитанное. Если у вас есть вопросы, просто свяжитесь со мной в Твиттере.



УПРАВЛЯЕМ ВЕРСИЯМИ БАЗ ДАННХ ЧЕРЕЗ FLYWAY




Еще по теме


Подробнее..

Интуиция в машине

25.06.2020 20:11:16 | Автор: admin
На самом деле всё просто.

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

Как же добавить компьютеру Интуицию?

image

Кто нибудь задумывался откуда взялся этот список?
Заметьте это уже много лет как работает и экономит человечеству 600 000 человеко-часов в день (это только Яндекс).
Но это подсказки. Последовательность слов.
А что такое слово? Фраза? Предложение?

Это образ


Некий набор смыслов, параметров, многомерный вектор или тензор, говоря языком математики и современного программирования.

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

Улыбнулись? Ощутили? Теперь понятно? Интуиция (память) подсказала, что будет дальше? Перед глазами увидели картинки?

Да не из детства. Природу!
Образы могут быть очень абстрактны. На уровне ощущений.
Вот чувствую, что надо так, но объяснить не могу почему

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

Соображать значит сопоставлять образы


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

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

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

Кодер


1 лес
2 полянка
3 холмик
4 ямка

В итоге будет последовательность чисел: 1, 2, 3, 4. Когда образов будет больше цифры будут больше, да и последовательности другие.
Это мы тоже подсмотрели у природы. В нашем мозге есть область, выполняющая данную функцию гиппокамп.
Если подавать в наш е-мозг такие последовательности (паттерны), то мы получаем статистику по каждому переходу от образа к образу.

Закодировать одним числом можно и сложный многомерный вектор:
{температура: 20, влажность: 60, ветер: 14, давление: 120/60}
{после дождика, четверг} {после града, вторник}
{грязный, бесстрашный, танк}
{тепло, светло, мухи не кусают}
{я, на солнышке, лежу, ушами шевелю}.

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

Алгоритм


Допустим мы услышали:
мама мыла раму 100 раз
мама мыла милу мылом 20 раз
мама мыла калькулятор 5 раз
мама мыла магазин 1 раз.
Суммарно 100 + 20 + 5 + 1 = 126 фраз.

В итоге получается такая картина:
image
И если мы будем слышать мама мыла, то на ум придет вариант с более высоким рейтингом/частотой. В данном случае раму.
Остальные варианты так же всплывают в памяти, но так как их повторяемость была ниже, то в списке на выдачу они ниже.

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

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

Ближе к делу


Какие могут быть паттерны?
Бизнес-процесс: прием товара, проверка комплектации, ввод накладной в 1С, перенос на склад.
Механика: болт, шайба, гровер, гайка.
Вождение авто: сцепление, передача, газ.
Время суток: утро, день, вечер, ночь.
Время года: зима, весна, лето, осень.
Продажи: удочка, комплект блесен, палатка, джип.

Как вы видите всё предсказуемо, когда видишь все много раз. После лета зима не начнется. Маловероятно.
Одного распознавания и предсказания мало.
Важно то, что мы можем сделать с данной информацией.
И сможем ли.

Всё очень просто


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

Развею ваши страхи и сомнения:
Интуиция не есть Озарение.
Подробнее..

Используем Xtend для прикладной кодогенерации сеанс чёрной магии с разоблачением

25.06.2020 10:15:49 | Автор: admin

Привет Хабр! Меня зовут Когунь Андрей. В КРОК я руковожу группой разработчиков Java (у нас большая распределённая по всей стране команда). Ещё я провожу встречи московского сообщества Java разработчиков JUG.MSK. Делаю это исключительно в корыстных целях: фотографируюсь там со всеми докладчиками, и однажды открою галерею с самыми интересными людьми в мире Java-разработки. Также помогаю делать конференции для разработчиков: JPoint, Joker и DevOops в качестве члена программного комитета. Ну и для души, так сказать, преподаю Java-технологии студентам.


В КРОК мы с коллегами в основном занимаемся заказной разработкой. Одно из наших направлений так называемые учётные системы. Их надо делать по возможности быстро. Они типовые, различия обычно наблюдаются только в доменной модели. Поэтому мы постоянно боремся за то, чтобы писать меньше бойлерплейт-кода, будь то тривиальные геттеры-сеттеры, конструкторы и т.п. или CRUD-репозитории и контроллеры. Мы для этого активно пользуем кодогенерацию.


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



О чём статья и чего в статье не будет


За много лет работы с Java мы перепробовали много чего интересного:


  • поддержка генерации в IDE,
  • генерация байт-кода при помощи Lombok,
  • процессоры аннотаций, порождающие новый код,
  • фреймворки, позволяющие по описанию модели получить готовое (почти) приложение,
  • и много чего ещё, в том числе новые и не очень JVM-языки, которые позволяют писать более лаконичный код и реализовывать DSL для решения прикладных задач.

В какой-то момент, проанализировав сильные и слабые стороны всех этих подходов, их ограничения и практическую применимость, мы пришли к тому, что в нашем собственном фреймворке для быстрой разработки (jXFW) будем использовать Xtend. Использовать для кодогенерации исходного Java-кода по доменной модели и для аккумулирования того опыта, который мы накопили в работе с различными технологиями. Сейчас расскажу, как в jXFW это всё работает и покажу, как вы можете сделать то же самое для своих нужд. Причём первую версию вы сможете изготовить буквально за пару дней и дальше начать применять подход know-how как код.


Рассказывать буду на примере упрощённого демо-проекта, который был реализован в рамках доклада на JPoint.


Ремарка: чего в статье не будет:


  1. Я не хочу, чтобы мы в итоге делали выводы про то, что технология А лучше технологи Б. Или что там Eclipse лучше IDEA или наоборот. Поэтому я не буду напрямую сравнивать какие-то языки, технологии. Всё что упоминаю, это лишь для того, чтобы какую-то аналогию объяснить на понятных примерах.
  2. Я не буду делать введение в Spring и Spring Boot. Исхожу из того, что вы имеете хотя бы какой-то опыт работы с этими технологиями. Мне кажется, сейчас сложно найти джависта, который не работал с ними. Но если вы вдруг слышите о Spring и Spring Boot впервые, вам срочно надо посмотреть доклады и тренинги Евгения Борисова и Кирилла Толкачева, там мои коллеги рассказали об этих технологиях очень подробно.
  3. Не буду очень сильно погружаться в Xtend. Но поскольку, как показывает мой опыт выступления на Java-конференциях, эта технология мало кем используется, сделаю небольшой ликбез. Чтобы вы уже дальше могли для себя решить, нужен вам Xtend или нет.

Короткий ликбез по Xtend


Xtend это статически типизированный язык программирования, приемник Xpand, построенный с использованием Xtext и компилирующийся в исходный код Java. Технология Xtext нужна для того, чтобы реализовывать всевозможные DSL. По сути, Xtend это такой своеобразный DSL.


Xtend совсем не новый язык программирования. Его создали ещё в 2011, примерно тогда же, когда появлялось большинство JVM-языков. Интересно, что у Xtend был слоган: Java 10 сегодня! Да, сегодня Java 10 у нас уже есть, слоган морально устарел. Но, похоже, люди что-то знали про Java, когда создавали Xtend, и некоторые фичи, заложенные в Xtend, они вот как раз прямо в Java 10 и появились. В частности, вывод типа локальной переменной (var). Но есть в Xtend и такие фичи, которых у Java пока ещё нет:


  • активные аннотации,
  • шаблонные выражения,
  • Switch Expressions.

Как работает кодогенератор в jXFW


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


Запускаю Eclipse.



Как видите, здесь практически ничего нет. Только application.java (конфигурация для Spring Boot) и собственно исходник на Xtend, в нём реализована доменная модель.



Как видите, Xtend-исходник очень похож на Java. Здесь нет ничего особенного. Просто класс с полями и несколько аннотаций. А что в итоге? jXFW генерирует два приложения (см. рисунок ниже): одно выполняется на сервере (тот самый Spring Boot) и даёт нам апишечку, а другое на клиенте.



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



то получим соответствующую запись и на клиенте, и на сервере.



То есть всё по-честному.


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


Что за магия здесь под капотом? И как в ней замешан Xtend? Рассказываю. У нас есть класс, на нём проставлены аннотации, вернее активные аннотации. Вся магия скрывается в них. Аннотации в Xtend очень похожи на аннотации в Java. Просто в Xtend для них есть отдельное ключевое слово:annotation.



Активной аннотация становятся, если её, в свою очередь, пометить другой аннотацией: @Active, а в ней указать класс процессора, который активируется, когда эта аннотация поставлена над каким-то элементом.



Дальше всё как обычно.


Xtend из коробки имеет некоторое количество таких аннотаций.



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


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


Как активные аннотации помогают писать меньше кода


Открываем проект jp-boot-xtend-demo. Я его получил при помощи Spring Initializr.



Дополнительных зависимостей здесь практически нет (см. файл pom.xml). Есть только spring-boot-starter-data-jpa и spring-boot-starter-data-rest. Плюс, подключен модуль jp-boot-xtend-demo-compile, в котором реализована наша активная аннотация. Если вам доводилось работать с процессорами аннотаций, вы наверно в курсе, что сам процессор определяется в отдельном модуле. Xtend в этом смысле не исключение.


И уже здесь, в jp-boot-xtend-demo-compile (см. файл pom.xml), мы подключаем все Xtend-зависимости, которые нам нужны: org.eclipse.xtend.lib, org.eclipse.xtend.lib.macro. Плюс, подключаем плагин xtend-maven-plugin. На случай если захотим тестировать наш Xtend-код, нам понадобится ещё несколько зависимостей: org.eclipse.xtend.core, org.eclipse.xtext.testing, org.eclipse.xtext.xbase.testing.


Кроме того, в Eclipse, я соответственно подключил плагин, который называется Xtend IDE. Актуальная инструкция как установить плагин тут. Ещё один вариант: сразу взять дистрибутив, в котором этот плагин предустановлен Eclipse for Java and DSL Developers.


Давайте смотреть как тут всё работает. Как и в случае с jXFW здесь есть приложение (см. файл DemoApplication.java), а также Java-класс, который будет нашей Entity, на базе которой мы будем всё строить (см. файл Country.xtend).



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



Например, в нашем DemoApplication есть кусок кода, который пытается вызывать метод setName. Но пока он красненький.



Я добавляю в Xtend-исходник активную аннотацию @Accessors, и у меня в сгенерированном Java-коде автоматически появляются геттеры и сеттеры, в том числе setName.



Возможностей управлять активной аннотацией у меня конечно не много, но по крайней мере, я могу сказать что мне нужны только геттеры.



Тут я ещё вписал в Xtend-файл аннотации @ToString и @EqualsHashCode, и в итоге получил Java-исходник прямо такой, как и хотел.


Небольшой лайфхак, который избавит вас от необходимости после каждой правки Xtend-исходника отыскивать в target сгенерированный Java-файл. В Eclipse есть специальная оснастка: Generated Code. Что она делает? Встаньте на любую строчку в Xtend-исходнике, и увидите в окне Generated Code Java-код, который для неё сгенерирован. А оттуда при необходимости уже можете пойти непосредственно в Java-исходник. Вот такая удобная штука.


Самый маленький кодогенератор на основе аннотаций


В принципе, всё хорошо работает. Но как только мы начинаем работать с кодогенерацией, тут же возникает вопрос: А можно такой же, но только с перламутровыми пуговицами? Так Что бы я ещё хотел? Я бы хотел наверно, чтобы у меня сеттеры мои вызывались в цепочке т.е. не просто устанавливалось значение, но ещё, чтобы и сам объект возвращался из этого сеттера, и я мог на нём следующий позвать.


Из коробки в Xtend такой аннотации нет. Поэтому нам придётся её делать ручками. И какие тут есть варианты?


В принципе, мы знаем, что существует аннотация @Accessors мы посмотрим на её исходный код, увидим, что там есть Accessors Processor, специально написанный. И вот мы уже смотрим на Xtend-код и пытаемся понять, а в каком месте мы могли бы здесь что-то подкрутить, чтобы у нас работало так, как надо. Но это не очень продуктивный путь. Мы по нему не пойдём.


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


Соответственно, вот эта наша аннотация (это я уже зашёл в проект jp-boot-xtend-demo-compile; см. файл EntityProcessor.xtend) @Active она нам говорит про те самые четыре фазы, к которым мы можем привязываться. На каждой фазе работает свой собственный Participant-вариант, и мы можем реализовать тот, который нам нужен.


В принципе, есть базовый специальный класс AbstractClassProcessor, в котором мы оверрайдим метод doTransform. В него нам будут приходить соответственно описания наших классов, помеченных этой аннотацией. И дальше мы, соответственно, что-то делаем.


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



Мне кажется, это самый маленький по объёму код для генерации при помощи аннотаций который я видел в жизни.


Что здесь делает Xtend? У него есть вот эти самые шаблонные выражения. Мы ставим три одинарные кавычки, и дальше пишем то, что хотим получить на выходе. И при этом форматируем так, как нам удобно.



А когда ставим фигурные кавычки, и пользуем simpleName от нашего филда в данном случае, который по совместительству является аргументом, то сюда подставляется нужное значение.


Код написан на Xtend. Мне кажется читать его, с одной стороны легко, потому что его мало. С другой стороны, он может мне быть понятен сходу. Например, почему мы позвали метод modifySetter, который я определил чуть ниже, и передали в него всего один аргумент?


Дело в том, что в Xtend есть такая вещь как Extension-методы. И у объекта того типа, которым является первый аргумент, можно этот Extension-метод позвать. Хорошо, а почему мы тогда его здесь не указали? Да потому что мы внутри лямбды, а в ней есть переменная it. Когда у нас есть переменная it, к лямбде можно обращаться, не указывая её. То же самое вот с it, который мы указали в качестве аргумента. Поэтому declaredFields-property у MutableClassDeclaration мы зовём напрямую, безо всяких префиксов.


Это вот всё, что в принципе придётся знать про Xtend.


А можно такой же, но только с перламутровыми пуговицами?


Давайте теперь посмотрим как это работает. Я определяю аннотацию @Entity. Затем иду вот в этот наш класс.



Заменяю текущую @Entity с javax.persistence на свою на активную аннотацию.



И вот теперь сеттер у нас такой как надо. Т.е. из Country возвращается this мы возвращаемое значение поменяли с void на тип объекта, над которым стоит аннотация: @Id Long id.


Но, допустим, я хочу, чтобы айдишник сеттился немножко по-другому (всё к той же идее хочу такое же, но с перламутровыми пуговицами). Тогда я вписываю в свой класс setID. И оно даже отчасти работает. Ведь сеттер появился в нужном месте сразу после id.



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



И хотя в том коде джавовом, который получился, ошибки нет, он скомпилируется и всё будет работать в коде есть проблема. Она заключается в том, что мы подменяем тело метода, который определили в Xtend-исходнике.


Поэтому нам надо внимательно следить за тем, чтобы таких казусов не возникало, когда пишем что-то на Xtend. Как такое отследить? Например, можно у того Transformation-контекста, который сюда приходит, прописать метод isThePrimaryGeneratedJavaElement, и соответственно передать туда сеттер. Получается прямо в таком же стиле, как мы обычно пишем на Java.



То же самое можно написать и по-другому, если вам так привычней.



Теперь всё работает как надо. Ошибки компиляции больше нет, а сеттерный айдишник стал такой как я и хотел.



Насколько это сложно прикручивать новые улучшения? Не увеличивают ли они сложность кода?


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



Не знаю, для чего в жизни это может пригодиться вам, но лично мне в работе такая штука нужна. Что мы тут указываем? Мы здесь указали имя филда. И дальше опять вот эта наша квадратная скобочка открываем лямбду; здесь дальше соответственно указываем, что нас интересует. Причём, нам важно, чтобы поле было транзиентное.


И теперь, вот здесь в сеттере тоже важно, чтобы такое поведение было на все поля, кроме транзиентных. Потому что когда мы меняем транзиентное поле, мы в общем-то не ожидаем, что наш объект будет dirty ведь эти значения не попадают в хранилище.



Давайте посмотрим, насколько это нам помогло.



Да, всё хорошо! Причём dirty написано ровно в том месте, где и должно. Нет никаких выкрутасов с отступами и т.д. Код выглядит хорошо и там, и там. Несмотря на то, что получился в результате кодогенерации. Плюс, как видите, код всё ещё остался простым для понимания.


Пишем процессор на смешанном диалекте Xtend и Java


@Entity больше мучить не будем. Убираю её в комментарии. И объявляю ту же самую аннотацию, но на Java (см. файл Entity.java). Здесь, как и на Xtend, всё просто, только чуть больше букв.



Процессор тоже можно писать на Java (см. файл JavaEntityProcessor.java).



Что я тут сделал? Я добавил обработчик для ещё одной фазы: doRegisterGlobals и докинул в контекст классы, которые мне понадобятся: Service и Repository. Плюс, заоверрайдил метод doTransform тот самый doTransform, который написал чуть раньше на Xtend. Причём я тут нормально навигируюсь по коду. Могу попадать в Xtend-код



и обратно в Java-код.



Дальше (см. метод doTransform) я добавляю к нашей entity аннотацию. Обратите внимание, здесь, в отличие от Xtend все методы надо вызывать явно через context.


Затем идёт метод, который создаёт репозиторий: createRepository. Важный момент: для всего того что мы генерируем, важно указывать PrimarySource: context.setPrimarySourceElement(repositoryType, entity);. Зачем? Чтобы при кодогенерации, когда у нас появляется Java-файл, он был связан со своим Xtend-источником.


Дальше немного скучного кода: пользую типы из Spring Data, чтобы указать какой у репозитория должен быть интерфейс.



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



И смотрите, здесь видно, что лямбды в Java очень хорошо дружат с лямбдами в Xtend. Одно на другое взаимозаменяется. Т.е. функциональные интерфейсы все здесь работают. И API был задизайнен так, что сюда джавововые лямбды нормально встают.


Дальше добавляем к нашим филдам всякие разные findBy-методы. Причём смотрим на аннотацию @Column, которая стоит над филдом. Если она имеет установленный атрибут признака уникальности значения (isUnique), просто возвращаем entityType. Если нет, возвращаем List. В конце ставим аннотацию @Param, которая нужна для того чтобы работал Spring Data Rest.



Всё! Для Repository генератор готов. Теперь если откроем Xtend-исходник, на основе которого будет генерироваться Java-код, и посмотрим на Gentrated Code, то здесь у нас добавился ещё и репозиторий. Мы можем смотреть на него, вот он такой.



Дальше пишем генератор для Service. Там всё почти всё так же как и с Repository.



Вот и всё. Процессор готов. Можно запускать сгенерированное приложение.


Ещё несколько улучшений, и запускаем сгенерированное приложение


Хорошо, сервис и репозиторий есть. Но как нам узнать, что у нас с моделью нашей всё хорошо? Добавим ещё одну фазу фазу валидации. Я добавляю два валидатора.



Теперь, если разработчик, который пишет Extend-код, вдруг забудет поставить перед своим классом аннотацию @ToString, валидатор выведет на экран Warning.



Или если разработчик поставит аннотацию @ManyToOne, а под ней ещё и @Column, то это уже ошибка. А ошибиться-то очень легко. Мы же программируем очень часто на копи-пасте, особенно когда есть возможность всё в один и тот же файл писать, как в Xtend. Скопировали, вроде работает успокоились. Но можно нарваться на коварную ошибку.


Допустим, у меня в Country.xtend у филда lastName прописано nullable = false, и я хочу, чтобы у Country тоже было nullable = false. Так неправильно. Поэтому Eclipse предупреждает меня. Но при этом генерируется Java код, в котором вроде как нет проблем.



Я меняю на @JoinColumn(nullable = false), и теперь всё хорошо. Можно запускать приложение.



Давайте наберём в браузере localhost:8080



затем localhost:8080/users/search.



Все наши findBy на месте. Приложение работает!


Пишите меньше кода, делайте меньше ошибок, применяйте технологии правильно


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


Вы теперь умеете создавать собственные активные аннотации, писать и отлаживать код процессора. Причём делать всё это на смешанном диалекте Java и Xtend, без необходимости переносить всю свою кодовую базу на Xtend.


Демо-проект, который мы с вами прямо в этой статье сейчас разработали, я заопенсорсил на гитхабе. Скачивайте, изучайте, пользуйте. А если информацию легче воспринимаете на слух и с видео, вот мой доклад с конференции JPoint, где рассказываю всё то же самое, что и здесь в статье.
У меня всё. Пишите меньше скучного кода, делайте меньше ошибок, применяйте технологии осознанно. Буду рад ответить на ваши вопросы. Можете писать мне на akogun@croc.ru. Кстати, помните, я в начале статьи говорил, что участвую в подготовке конференций для джавистов? JPoint 2020 из-за известных причин будет проходить онлайн, но это даже совсем неплохо, у нас много отличных спикеров, которые не смогли бы приехать и выступить очно, а сама конференция будет идти целых 5 дней! С 29 июня по 3 июля jpoint.ru. Приходите!

Подробнее..

Онлайн-стримы с Алексеем Шипилёвым и с Артемом Ерошенко

22.06.2020 18:15:30 | Автор: admin


Конференции продолжаются, и на этой неделе мы приглашаем вас окунуться в мир Java и тестирования вместе с нашими шоу.

Расписание на эту неделю:





Среда: Java


Первая чашка кофе с JPoint / Алексей Шипилёв
Начало: 24 июня в 12:00 (московское время)

24 июня в 12:00 в Первую чашку кофе с JPoint придет долгожданный гость Алексей Шипилёв. Сейчас он живет в Германии и работает в Red Hat, где занимается разработкой OpenJDK и его производительностью. Также Алексей разрабатывает и поддерживает несколько подпроектов в OpenJDK, включая JMH, JOL и JCStress. Он активно участвует в экспертных группах и сообществах, работающих над вопросами производительности и многопоточности.

Ведущие эфира Владимир Ситников и Олег Шелаев. Владимир автор более десятка улучшений производительности в официальном PostgreSQL JDBC-драйвере, уже десять лет работает над производительностью и масштабируемостью NetCracker OSS. Олег Developer Advocate проекта GraalVM в OracleLabs.

Ведущие обсудят с Алексеем Metropolis, GraalVM, новый проект в OpenJDK, Shenandoah 3.0, мета-информацию в OpenJDK JIRA и многое другое. А также поговорят о судьбе Java 8: умрет ли она и кто ее будет поддерживать?

Напомним, что на конференции JPoint 2020 будет Q&A-сессия с Алексеем, где вы сможете задать ему вопросы в прямом эфире. Присоединяйтесь к нам!



Пятница: Тестирование


Ошибка выжившего Episode 9
Начало: 26 июня в 18:00 (московское время)

26 июня в 18:00 вас ждет новый выпуск шоу Ошибка выжившего. Эксперт по автоматизации тестирования, автор Allure/Allure 2 Артем Ерошенко и участник программного комитета конференции Heisenbug, бывалый QA и разработчик, ведущий подкаста Битовая Каска Всеволод Брекелов встретятся в студии, чтобы обсудить, что произошло в мире тестирования за неделю, и разобрать полезные инструменты.

Прошлый выпуск шоу уже доступен в записи. В нем Артем с Севой проверяли, как завести QA-инфраструктуру крупной компании на локальной машине.

Сезон конференций в самом разгаре! На этой неделе идут Mobius и HolyJS, а уже на следующей неделе можно будет задать все свои вопросы Алексею Шипилёву на JPoint 2020.
Подробнее..

27 июня, стрим-конференция Кодинг будущего

25.06.2020 14:15:14 | Автор: admin
Привет!

Если вы читали наши предыдущие посты, то уже знаете про Alfa Battle для Java-разработчиков. Послезавтра в прямом эфире можно будет посмотреть финал чемпионата, с 12.00 до 18.00.

Параллельно стриму с финалом мы запустим стрим-конференцию под названием Кодинг будущего, где с партнерами чемпионата (Билайн и X5 Retail Group) поговорим о футуристических прогнозах в IT. От компаний будут ведущие разработчики и топ-менеджеры.



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

Основных модулей стрим-конференции у нас получилось 6 штук:

  1. AI
  2. Web-банкинг
  3. Growth hacking
  4. Mobile banking
  5. Продуктовый дизайн
  6. Архитектуры Java-проектов

А вот сами доклады и спикеры.

Начнётся стрим в 12:00, вместе со стартом чемпионата Alfa Battle

12:05
Футуристический прогноз: российская IT-индустрия завтра

Михаил Тюрганов, Руководитель дирекции развития цифровых сервисов, Альфа-Банк
Павел Дерендяев, Руководитель центра компетенций Java, Альфа-Банк


Модуль #1. Java Alfa Digital


12:15
Технологии и инфраструктура Альфа-Мобайл
Максим Шатунов / Java Tech Lead, Альфа-Мобайл, Альфа-Банк

12:35
Эволюция корпоративного банка
Никита Хренов / Ведущий разработчик, Альфа-Банк

12:55
Внутреннее устройство сайта alfabank.ru
Максим Чернухин / Архитектор направления, Альфа-Банк

Модуль #2. AI Билайн


13:15
Интеллектуальная обработка звонков в реальном времени
Донат Фетисов / Head of Architecture and Infrastructure department, Билайн

Модуль #3. X5 Retail Group


13:40
Цифровизация помидора

Руслан Каймаков / Head of Web&Mobile Development, X5 Retail Group

14:05
От Java до Scala на грузовике Х5

Вадим Ануфриев, Senior Scala Developer, X5 Retail Group

Модуль #4. Кодинг будущего


14:30
Дискусcионная панель Кодинг будущего

Иван Пятков / Директор по цифровому бизнесу, член Правления Альфа-Банка
Донат Фетисов / Руководитель департамента по архитектуре и инфраструктуре данных, Билайн
Антон Вальков / Директор по IT, член правления X5 Retail Group


Модуль #5. Alfa Digital Products


15:15
Alfa Digital: Новый взгляд на банкинг

Дамир Баттулин / Head of Digital Channels, Альфа-Банк

15:30
Web-банкинг в эпоху Mobile 1st

Нина Красавина / Digital product owner, Альфа-Банк

15:40
Voice UX. Практическая магия

Владимир Китляр / Digital CPO, Альфа-Банк
Елена Грунтова / Руководитель продукта в ML сервисах Яндекс.Облака


16:00
Как мы делаем из Альфа-Мобайл лучший мобильный банк в стране

Евгений Тонкошкуров /СРО Альфа-Мобайла, Альфа-Банк

16:15
Роль лида дизайнеров цифровых продуктов

Анастасия Попова / Руководитель направления продуктового дизайна, Альфа-Банк

16:30
Дизайн нового Альфа-Мобайла

Вячеслав Киржаев / Lead Product Designer, Альфа-Банк

16:45
Про страшные ругательства: Growth Hacking и Dual-track Agile

Илья Кузнецов / CPO Digital Innovations, Альфа-Банк

Модуль #6. Alfa Digital Products


17:00
Финиш чемпионата Alfa Battle


17:15
Интервью с Владимиром Верхошинским

Главный управляющий директор, член Наблюдательного совета Альфа-Групп

17:45
Подведение итогов и награждение победителей

Владимир Верхошинский

Подробнее..

JetBrains Technology Day for Java

02.07.2020 20:17:59 | Автор: admin
В мае языку Java исполнилось 25 лет. Такое событие нельзя пропустить! Давайте праздновать вместе с JetBrains 10 июля на первом в мире виртуальном митапе для Java энтузиастов JetBrains Technology Day for Java.

image

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

Здесь вся программа.

Java и JetBrains


Те, кто давно работают с Java, знают, что этот язык вдохновение для команды JetBrains. Наша флагманская IDE IntelliJ IDEA была написана для и на Java. Она быстро заняла нишу среди ведущих сред разработки, завоевала множество наград и преданных фанатов по всему миру.

С 2000 года JetBrains выпустила несколько IDE на базе платформы IntelliJ для различных языков программирования. Многие наши инструменты, такие как Space (пространство для командной работы), YouTrack (инструмент для управления проектами и отслеживания ошибок), TeamCity (наш CI/CD-сервер) и другие, написаны на Java. Kotlin, популярный язык программирования, разработанный JetBrains, совместим с Java и также работает поверх JVM.

Чем займемся?


На протяжении всего дня с 11:00 до 21:00 по Москве лучшие Java-спикеры проведут 10 часовых инфосессий. Выбранные темы будут интересны и тем, кто давно работает с Java, и тем, кто только собирается с духом, чтобы шагнуть в этот увлекательный мир. Например, вы узнаете:

  • Как разрабатывать через тестирование (TDD)
  • Что такое ленивые вычисления (lazy evaluation)
  • Можно ли бесплатно выучить Java и написать код на первом уроке
  • Как создать новые и расширить существующие Java collections
  • Зачем нужны ZGC и Shenandoah GC
  • Почему визуальная валидация (visual validation testing) улучшает UX, локализацию и адаптивный дизайн

А вот и полный список тем:



Как участвовать?


Все просто: заходите на сайт и регистрируйтесь. Выбирайте интересные вам сессии или подтверждайте участие сразу во всех. После регистрации мы вышлем вам подробные инструкции.

Во время прямого эфира задавайте вопросы в чате. Спикеры ответят на них в конце сессий.

Сразу же после ивента запись видео появится на сайте, и чуть позже вы сможете посмотреть сессии на канале IntelliJ IDEA на YouTube.

Если JetBrains Technology Day for Java интересен вашей юзер-группе, посвященной Java, делитесь ссылкой и регистрируйте свой JUG. Присоединяйтесь к десяткам JUG, которые поддерживают наше мероприятие!

Регистрируйтесь сейчас, ведь количество мест ограничено, а любителей Java становится больше с каждой минутой.

Хочу на JetBrains Technology Day for Java

P.S. Не забывайте хэштеги #JBTechDayforJava и #JetBrainsLovesJava в Twitter, Facebook и LinkedIn. Мы тоже там будем, давайте общаться!

Ваша команда JetBrains
The Drive to Develop
Подробнее..

Подключение к session в Java и Python. HttpURLConnection и CookieManager (Java). Requests(Python)

29.06.2020 22:07:32 | Автор: admin
Допустим, что нам надо подключиться к серверу, авторизоваться и поддерживать сессию. В браузере это выглядит следующим образом:
  1. На адрес http://localhost:8080/login отправляется пустой GET запрос.
  2. Сервер присылает формочку для заполнения логина и пароля, а также присылает Cookie вида JSESSIONID=094BC0A489335CF8EE58C8E7846FE49B.
  3. Заполнив логин и пароль, на сервер отправляется POST запрос с полученной ранее Cookie, со строкой в выходном потоке username=Fox&password=123. В Headers дополнительно указывается Content-Type: application/x-www-form-urlencoded.
  4. В ответ сервер нам присылает новую cookie c новым JSESSIONID=. Сразу же происходит переадресация на http://localhost:8080/ путём GET запроса с новой Cookie.
  5. Далее можно спокойно использовать остальное API сервера, передавая последнее Cookie в каждом запросе.


Рассмотрим, как это можно реализовать на Java и на Python.



Содержание:




Реализация на Python. Requests.



При выборе библиотеки для работы с сетью на Python большинство сайтов будет вам рекомендовать библиотеку requests , которая полностью оправдывает свой лозунг:
HTTP for Humans

Вся задача решается следующим скриптом:
import requestssession = requests.session()  #создаём сессиюurl = "http://localhost:8080/login"session.get(url)   #получаем cookiedata = {"username": "Fox", "password": "123"} response = session.post(url, data=data) #логинимся


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

Реализация на Java, HttpURLConnection и CookieManager.



Поиски библиотеки для работы с сетью на Java приводят сразу к нескольким библиотекам. Например, java.net, Apache HttpClient и OkHttp3.

Я остановился на HttpURLConnection (java.net). Плюсами данной библиотеки является то, что это библиотека "из-под коробки", а так же, если надо написать приложение под android, на официальном сайте есть документация. Минусом является очень большой объём кода. (После Python это просто боль).

Итак, начнём. По документации для работы с сессиями можно использовать CookieManager:

CookieManager cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL);CookieHandler.setDefault(cookieManager);


Что нужно отметить, используя такой подход:
  • CookiePolicy.ACCEPT_ALL указывает, что надо работать со всеми cookie.
  • Переменная cookieManager далее нигде не будет использоваться. Она контролирует все подключения, и, если необходимо поддерживать несколько активных сессий, необходимо будет в этой одной переменной руками менять Cookie


Учтя это, можно записать и в одну строчку:
 CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));


Пункт 1 и 2. Выполним GET запрос для получения первой Cookie
URL url = new URL("http://localhost:8080/login");HttpURLConnection con = (HttpURLConnection) url.openConnection();con.setRequestMethod("GET");BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));String inputLine;final StringBuilder content = new StringBuilder();while ((inputLine = in.readLine()) != null) {    content.append(inputLine);}


После этого наш cookieManager будет содержать Cookie с сервера и автоматически подставит её в следующий запрос.

Веселье начинается с POST запросом.
url = new URL("http://localhost:8080/login");con = (HttpURLConnection) url.openConnection();con.setRequestMethod("POST");


Нужно записать в Headers Content-Type: application/x-www-form-urlencoded.
Почему метод называется setRequestProperty, а не setHeaders (или addHeaders) при наличии метода getHeaderField, остаётся загадкой.
con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");


Далее идёт код, который непонятно по каким причинам не засунут под капот библиотеки.
con.setDoOutput(true);

Нужна эта строчка кода для открытия исходящего потока. Забавно, что без этой строки мы получим следующее сообщение:
Exception in thread main java.net.ProtocolException: cannot write to a URLConnection if doOutput=false call setDoOutput(true)

Открываем исходящий поток и записываем туда логин и пароль:
final DataOutputStream out = new DataOutputStream(con.getOutputStream());out.writeBytes("username=Fox&password=123");out.flush();out.close();


Остаётся считать ответ с уже перенаправленного запроса.

Реализация на Java, HttpURLConnection без CookieManager.



Можно реализовать и без CookieManager и самому контролировать перемещение cookie.
Пункт 1 и 2. Вынимаем cookie.
URL url = new URL("http://localhost:8080/login");HttpURLConnection con = (HttpURLConnection) url.openConnection();con.setRequestMethod("GET");BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));String inputLine;final StringBuilder content = new StringBuilder();while ((inputLine = in.readLine()) != null) {    content.append(inputLine);String cookie = con.getHeaderField("Set-Cookie").split(";")[0];}


Далее отправляем POST запрос, только на этот раз вставив cookie и отключив автоматическое перенаправление, т.к. перед ним надо успеть вытащить новое cookie:

// создаём запросurl = new URL("http://localhost:8080/login");con = (HttpURLConnection) url.openConnection();con.setRequestMethod("POST");//указываем headers и cookiecon.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");con.setRequestProperty("Cookie", cookie);//отключаем переадресациюcon.setInstanceFollowRedirects(false);//отправляем логин и парольcon.setDoOutput(true);final DataOutputStream out = new DataOutputStream(con.getOutputStream());out.writeBytes("username=Fox&password=123");out.flush();out.close();//считываем и получаем второе cookieBufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));String inputLine;final StringBuilder content = new StringBuilder();while ((inputLine = in.readLine()) != null) {    content.append(inputLine);String cookie2 = con.getHeaderField("Set-Cookie").split(";")[0];


Далее во все запросы просто добавляем следующую строку:
con.setRequestProperty("Cookie", cookie2);


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

Telegram как публичный NASFTP

26.06.2020 18:17:05 | Автор: admin

image
Привет, Хабр!


Эта статья продолжение первой статьи Telegram как NAS/FTP


Речь всё о том же боте TeleFS, он приобрёл важную составляющую публичность. Точнее, пользователи бота теперь могут делиться своими файлами и папками с любыми другими пользователями Telegram.


И в этот раз расскажем о том как и с помощью чего создавался бот.


Стек


Язык разработки Java 8
Движок веб-сервис, на основе фреймворка Play! Framework версии 2.7.
Хранилище Postgres v10
Маппер MyBatis (плагин mybatis-guice)
Фронт-сервер nginx (его задача сводится только к обеспечению tls)


В общем смысле указанный стек абсолютно не критичен. Язык и технологии могут быть использованы любые, жёсткое ограничение из себя представляют только два момента:
1) движок должен уметь в http, так как Telegram Bot API это http-endpoints и получение обновлений всё-таки приятнее без long-poll.
2) база должна быть реляционной, так как бот эксплуатирует иерархию на основе первичных ключей с подзапросами.


Архитектура хранения


Задача бота хранить структуры "файловой системы" (ФС) для каждого пользователя.
Идея бота изначально рассматривалась как некое "общее" хранилище для всех пользователей, но после нескольких напряжённых экспериментов была отринута как несостоятельная. Взамен каждый пользователь получил собственную ФС, которая хранится в отдельной плоской таблице, с собственной иерархией. Каждый файл, директория и заметка это одна физическая запись в таблице владельца, а все представления записи (дочерний узел в ФС, открытый доступ для других пользователей, результат поиска) это всё view (представления) в БД.


Рассмотрим конкретику. Вот таблица одного пользователя (с id 990823086):
imagehttps://habrastorage.org/webt/ay/1w/ja/ay1wjaycn6qf--zgrg0idur2oeq.png
Так хранятся физические записи обо всех узлах его файловой системы.


Поверх этой таблицы организовано представление для отображения иерархии:


create view fs_paths_990823086(id, parent_id, owner, path) asWITH RECURSIVE tree AS (    SELECT fs_user_990823086.id,           fs_user_990823086.name,           fs_user_990823086.parent_id,           fs_user_990823086.owner,           ARRAY [fs_user_990823086.name] AS fpath    FROM fs_user_990823086    WHERE fs_user_990823086.parent_id IS NULL    UNION ALL    SELECT si.id,           si.name,           si.parent_id,           si.owner,           sp.fpath || si.name AS fpath    FROM fs_user_990823086 si             JOIN tree sp ON si.parent_id = sp.id)SELECT tree.id,       tree.parent_id,       tree.owner,       array_to_string(tree.fpath, '/'::text) AS pathFROM tree;

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


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


create view fs_user_990823086(id, parent_id, name, type, ref_id, options, owner, rw) asSELECT fs_data_990823086.id,       fs_data_990823086.parent_id,       fs_data_990823086.name,       fs_data_990823086.type,       fs_data_990823086.ref_id,       fs_data_990823086.options,       990823086::bigint AS owner,       true              AS rwFROM fs_data_990823086;

В этом слое добавим то, что необходимо для организации именно разделения доступа: понятие "владельца" и примитивных "прав доступа" в виде rw (read/write). И все элементы ФС теперь будем получать именно из этого слоя.


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


create view fs_share_990823086_990823086_5786da(id, name, type, parent_id, ref_id, options, owner, rw) asWITH RECURSIVE tree AS (    SELECT fs_data_990823086.id,           fs_data_990823086.name,           fs_data_990823086.type,           '8b990db3-e41a-4130-990d-b3e41a71305a'::uuid AS parent_id,           fs_data_990823086.options,           fs_data_990823086.ref_id    FROM fs_data_990823086    WHERE fs_data_990823086.id = 'c07b37a1-4abf-4a0c-bb37-a14abf6a0c0b'::uuid    UNION ALL    SELECT si.id,           si.name,           si.type,           si.parent_id,           si.options,           si.ref_id    FROM fs_data_990823086 si             JOIN tree sp ON si.parent_id = sp.id)SELECT tree.id,       tree.name,       tree.type,       tree.parent_id,       tree.ref_id,       tree.options,       share.owner,       share.rwFROM tree         LEFT JOIN shares share ON share.id = '5786da'::text;

Затем нам необходимо где-то регулировать доступ к этому кусочку, для этого служит отдельная таблица shares, в которой хранятся именно такие настройки:
image


Далее, как вы уже наверное догадались, получателю доступа достаточно всего лишь включить это представление в свой слой представления, для того, чтобы чужой "кусочек" встроился в его ФС:


create or replace view fs_user_990823086(id, parent_id, name, type, ref_id, options, owner, rw) asSELECT fs_data_990823086.id,       fs_data_990823086.parent_id,       fs_data_990823086.name,       fs_data_990823086.type,       fs_data_990823086.ref_id,       fs_data_990823086.options,       990823086::bigint AS owner,       true              AS rwFROM fs_data_990823086UNION ALLSELECT fs_share_990823086_990823086_5786da.id,       fs_share_990823086_990823086_5786da.parent_id,       fs_share_990823086_990823086_5786da.name,       fs_share_990823086_990823086_5786da.type,       fs_share_990823086_990823086_5786da.ref_id,       fs_share_990823086_990823086_5786da.options,       fs_share_990823086_990823086_5786da.owner,       fs_share_990823086_990823086_5786da.rwFROM fs_share_990823086_990823086_5786da;

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


Движок сервиса


Сервис реализует конечный автомат. В любой момент времени каждый пользователь, условно, находится в одном из 5 основных состояний: "зритель", "редактор", "сторож", "раздающий" и "создатель". Каждый запрос пользователя это либо "новый файл", либо "ввод текста", либо "нажатие на кнопку".


Модель пользователя содержит набор неизменяемых данных (идентификатор telegram-пользователя и идентификатор корневой папки) и аморфное представление атрибутов текущего состояния (роли) в виде json. Каждый запрос извлекает из базы данные пользователя, интерпретирует их в конкретное состояние, согласно маркеру роли, совершает необходимые действия с записями ФС, фиксирует новое состояние и сохраняет обратно в базу. Никаких сессий, никаких авторизаций/токенов, ничего подобного.
Как видим, в самом движке ничего "интеллектуального" нет, он простой.


Несколько слов о Telegram Bot Api.


Хотелось бы заострить внимание на некоторых неочевидных моментах Bot API. Не смотря на то, что эти моменты так или иначе описаны в документации, это было упущено при разработке.


  1. Каждый входящий запрос от Telegram к боту это уведомление. В документации сам документ и называется Update, но почему-то так не воспринимался. Уведомление, которое не требует какого-то структурированного ответа, только положительного статуса о его, уведомления, приёмке, т.е. пустой OK 200. Не нужно обрабатывать запрос и отдавать ему ответ. Нужно как можно быстрее отдать ему пустой ответ об успешном приёме и дальше заняться своими делами, в том числе обработкой этого самого уведомления.


  2. Каждый входящий запрос типа answerCallbackQuery фактически требует асинхронного "ответа" отдельного вызова Bot API, независимо от всех остальных действий, чтобы пользователь не смотрел на индикатор загрузки.


  3. Рекомендованный лимит исходящих запросов от бота к API 1 сообщение в секунду в один чат. Возможно, наука когда-нибудь объяснит избирательность восприятия, но слова про один чат были начисто позабыты во время разработки, осталось только ограничение "1 сообщение в секунду". Впоследствии, при разборе полётов это было исправлено, но это впоследствии, не сразу :)


  4. Любое сообщение в личных диалогах "пользователь/бот" может быть отредактировано в любой момент времени. Хоть через год. А вот удалить можно только в течение 2 суток с момента отправки.
    Тут сделаем небольшое отступление, следует пояснить, почему этот момент важен:
    бот исповедует подход "одного окна". То есть, по возможности, бот не отсылает пользователю каждый раз новое сообщение, а пытается редактировать содержимое того, что было отослано ранее. Так как, как показала практика полевых испытаний, пользователи путаются в сообщениях, жмут не туда, попадают не туда куда хотели и т.д. Этот подход всем хорош, но иногда бот отсылает т.н. "диалоговые" сообщения, которые не предполагают интерактива и их следует удалять спустя непродолжительное время. Но если пользователь остановился на пол-шаге: например, захотел создать новую папку, получил приглашение на ввод названия и дальше не пошёл, то через 2 суток такое сообщение будет уже неудаляемым. Обработка таких ситуаций пока ещё в планах.


  5. Всего боту доступно два вида сообщений: текстовые и медийные. Текстовые, это которые могут содержать текст и кнопки. Медийные это которые могут содержать один вид медиа-контента, текст (в 4 раза меньше, чем текстовые сообщения) и кнопки. Типы между собой несовместимы. Это означает, что нельзя сделать ранее отосланное медийное сообщение текстовым. Как понятно из предыдущего пункта, если пользователь только что смотрел файл, а затем перешёл в режим просмотра директории т.е. было медийное сообщение, а должно стать текстовое в этом случае бот должен удалить предыдущее и отослать новое, что расточительно.



Приватность/безопасность


Данный абзац написан потому, что самый частый вопрос про бота в различнейших вариациях сводится к усреднённой форме: "Где лежат мои файлы и кому они доступны".


Сначала немного общей теории про Telegram и файлы в нём.
Когда вы посылаете файл в Telegram (неважно кому именно), если это не "секретный чат", то файл физически закачивается в облако Telegram, где он лежит в зашифрованном виде. Ваш собеседник (например, бот), получает не сам файл, а его уникальный идентификатор и некие атрибуты контента (название, размер и т.д., в зависимости от типа файла). Далее, если этот идентификатор передать в Telegram, то файл станет доступен для скачивания тому, кто его запросил. Но, для того, чтобы можно было скачать файл этот идентификатор должен быть из вашего диалога. То есть, нельзя взять любой идентификатор из любого диалога и скачать файл. Говоря простыми словами, чтобы кто-то мог скачать ваш файл из облака Telegram он должен получить его идентификатор от вас через Telegram. Лично, либо через группу/канал и никак иначе.


Теперь к практике, т.е. к боту TeleFS.
То, что вы отсылаете файлы боту означает, что бот в состоянии создать действующую ссылку для скачивания вашего файла. И в состоянии переслать действующий идентификатор кому-либо из своих собеседников. Бот этого не делает, но он в состоянии это сделать. Что важно сделать он это может только для своих собеседников. Если кто-то неожиданно получит данные из базы бота, то использовать эти идентификаторы за пределами диалога с этим конкретным ботом он не сможет. То есть, если вы подняли свой экземпляр бота и ваша БД утекла к злоумышленникам они ничего не смогут получить, даже если поднимут у себя копию такого же бота TFS.


Из чего следует: не храните в публичной структуре TFS ничего личного, секретного и/или очень важного. Поднимите своего персонального бота TFS и храните всё там.




Это всё, о чём хотелось бы рассказать в этот раз.
Всем спасибо за внимание.


ссылки

Бот в Telegram
Бот на GitHub
Инструкция по сборке и установке на свой сервер
Группа для обсуждений/предложений


Первая статья про бота

Подробнее..

FlexCube внедрение революционной бэк-офисной платформы в Росбанке

22.06.2020 20:08:04 | Автор: admin
Друзья, привет!

Я Никита Климов, Platform Owner Oracle FlexCube (FCUBS) для процессинга операций корпоративных депозитов, межбанковских кредитов, валютных операций и деривативов в Росбанке. Сегодня я расскажу, как мы внедряли платформу FCUBS и в чем уникальность этого проекта для российского рынка. Все подробности под катом.

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

Архитектура

Архитектура системы это классическая трехзвенка. В нашем случае backend это Oracle 12c (правда, он на текущий момент уже снят с поддержки, и мы переходим на 19сно это уже совсем другая история) и frontend IBM WebSphere. Искушенный читатель сразу задаст вопрос а почему не использовать нативный для Oracle Weblogic? И, безусловно, будет прав, потому что это первое, что рекомендовал нам сам Oracle. Но, так вышло, что для банка стандартом является именно сервер приложений IBM WebSphere, к тому же у нас ULA на этот продукт, и было принято решение адаптировать слой приложения под особенности WebSphere. Не сказать, что это было очень трудной задачей, однако ряд особенностей организации внутренних очередей все же имелся, и нашей команде пришлось провести немало часов на трехсторонних конф-коллах с поддержками Oracle и IBM.
image

В то время как мы пытались настроить тестовое окружение и показать проектной команде интерфейс, наши бизнес-аналитики проводили GAP анализ, описывали требования к функциональности и продумывали миграцию данных из старой системы. Не буду фокусироваться на миграции данных, т.к. по сути это процесс заполнения промежуточных, транспортных таблиц внутри FlexCube. Все это действо сводилось к итерационному наполнению и выверке данных до успешного выполнения миграции- ведь главное передать нужное значение в нужное поле таблицы.
Отличительная особенность нашего внедрения заключалась в том, что на замену системы с полным ручным приводом и постоянным пользовательским участием, мы создавали событийную систему на STP процессах, где предусматривалось минимальное вовлечение бизнес-пользователей. Предусмотрены лишь checkpoint для контроля процессинга. Для этого нам пришлось ломать старые бизнес-процессы и выстраивать новые.

Функциональность

Как я уже отметил ранее, система из коробки была совершенно не готова к российским реалиям, начиная с отсутствия плана счетов и заканчивая налоговым учетом. По сути это было просто ядро с набором событийных моделей, из которого надо строить космический корабль. Следовательно, вооружившись функциональными требованиями от бизнеса, мы приступили к разработке своего custom слоя на основе ядра системы. Мы разработали свой Accounting engine для генерации двадцатизначных счетов и приступили к реализации STP процессов. Исключить ручное вмешательство пользователей оказалось нетривиальной задачей, и не решалось лишь с помощью триггеров на уровне СУБД. Пришлось строить событийную логику на JOB и вводить расписание заданий. Этого тоже оказалось недостаточно, и мы вынуждены были использовать Quartz, на основе которого мы и автоматизировали наш workflow. В результате у нас в полностью автоматическом процессе происходит следующее:

  • Контракт попадает к нам из фронтовой системы Kondor+, и в зависимости от его суммы он либо автоматически авторизовывается, либо уходит на авторизацию к бизнесу;
  • После успешной авторизации система анализирует клиента является ли он клиентом головного офиса, а значит его счета лежат в GL1, либо это клиент регионов, а значит его счета лежат в GL2. Есть еще случай, когда это совершенно новый клиент, и тогда мы должны запросить его в нашей CRMсистеме и на основании полученной информации инициировать ему открытие необходимых счетов в соответствующей GL;
  • В результате процессинга система в режиме онлайн запрашивает остатки по счетам и при наличии таковых генерирует и передает в соответствующую GL необходимые проводки, формирует и отправляет SWIFT сообщения и платежки в ЕРЦ;
  • Внутри дня в системе происходят стандартные операции погашения, начисления процентов, досрочное закрытие и т.д;
  • Различную информацию о движениях по счету, контрагентах и контрактах мы автоматически передаем в ФНС, AML, Nostro. Также не забыли и об Интернет-Клиент Банке, через который клиенты видят, что происходит с их счетами после открытий и погашений депозитов;
  • Подготавливаем различную информацию для обязательной и управленческой отчетности и отдаем ее в DWH тут стоить отметить, что мы как делаем классические view для забора информации, так и генерируем транзакционные логи для IBM CDC, который в режиме онлайн забирает и агрегирует эту информацию.


Интеграция
image
Тут я для наглядности приложу нашу архитектуру и скажу лишь, что в связи с выбором frontend IBM WebSphere, было принято решение отказаться от стандартного для FCUBS Gateway, который разворачивается как дополнительное приложение и работает по старинке с листнерами и очередями, и перейти на работу c MDB Activation Specification. В результате чего мы разработали дополнительные интеграционные приложения, опубликовали их на нашем сервере и подключили к банковской интеграционной шине для взаимодействия с другими системами.
Кроме этого, у нас так же используется интеграция по средствам Systematica Modullar на основе TIBCO Rendezvous, общающийся с нашим фронтом и являющейся входной точкой для всех контрактов и ETL средство IBM DataStage. При этом функциональность на DataStage используется для интеграций, не связанных с DWH. Для одной из GL cпециально разработана логика батчевой загрузки\выгрузки данных, с проверкой статусов и breakpoints для ожидания вычислений.
image
ИТОГИ

  1. Заменили морально и технически устаревшую систему
  2. На основе ядра FlexCube создали свою платформу с неограниченными возможностями по параметризации и вариациям учета
  3. Минимизировали участие пользователей в процессе обработки дня
  4. Оптимизировали время выполнения EOD 15 минут вместо 3 часов ранее.
  5. Создали внутри банка центр компетенций и можем поддерживать и развивать платформу независимо от поставщика
  6. Практически неограниченно можем изменять usability пользовательского интерфейса и создавать любые проверочные экраны консолидированной информации для удобства контроля
  7. Внедрили систему мониторинга контрольных точек для беспрерывного процесса обработки
  8. Создали платформу, на которой готовы реализовать любой банковский продукт

image
Подробнее..

Weapon wheel в Doom 1993

29.06.2020 18:10:05 | Автор: admin
Приветствую.
Многие из нас с теплотой относятся к олдскульным видеоиграм, вышедшим на стыке веков. У них превосходная атмосфера, бешеная динамика и множество оригинальных решений, которые не устарели спустя десятилетия. Однако в наши дни видение интерфейса игр несколько изменилось на смену запутанным уровням пришли линейные коридоры, на смену аптечкам регенерация, а вместо длинного ряда клавиш 0-9 для выбора арсенала пришли сначала колесико мыши, а затем виртуальное колесо. Именно о нем сегодня и пойдет речь.
image


Историческая сводка
Раньше, во время появления жанра шутеров как таковых, вопрос об управлении мышкой не стоял для управления протагонистом использовалась только клавиатура. Причем единого формата управления тоже не было WASD стал стандартом чуть позднее. Более подробно о старых игровых раскладках клавиатуры можно почитать вот тут
Соответственно, в тех играх, где была реализована возможность выбора снаряжения (Doom, Wolfenstein, Quake etc) был реализован единственным интуитивным на тот момент способом с помощью цифровых клавиш на клавиатуре. И на многие годы этот способ был единственным.
Потом, в конце 90х годов, появилась возможность смены вооружения колесиком мышки. Однозначной информации на эту тему найти не удалось, однако в CS 1.6 такая возможность включалась через консоль. Впрочем, возможно такие прецеденты были и ранее в таком случае, просьба указать на это в комментариях или в ЛС. А вот в привычном в наше время виде Weapon Wheel вошло в использование лишь с Crysis'ом и его Suit menu, Хотя попытки сделать нечто похожее были начиная с HL2, в массы колесо пошло лишь в конце 00х годов, а сейчас является мейнстримом.

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

Постановка задач
Для того, что бы реализовать WW, нужно каким-либо образом перехватывать движения мышки, отслеживать ее перемещение, пока зажата клавиша селектора, и, по отпусканию, эмулировать нажатие на кнопку, соответствующую выбранному сектору.
Для этого мной был использован язык Java, в частности, перехват клавиш осуществляется за счет библиотеки jnativehook, а нажатие за счет awt.Robot. Обработка полученных хуков не представляет сложностей, поэтому производится вручную.

Реализация
Предварительно были разработаны классы, задающие пары координат, для определния вектора смещения.
В частности, класс Shift позволяет хранить двумерный вектор, а также определять его длину, а класс NormalisedShift, разработанный для хранения нормализованного вектора, помимо прочего, позволяет определить угол между перехваченным вектором и вектором (1,0)
Заголовок спойлера
class Shift{    int xShift;    int yShift;    public int getxShift() {        return xShift;    }    public int getyShift() {        return yShift;    }    public void setxShift(int xShift) {        this.xShift = xShift;    }    public void setyShift(int yShift) {        this.yShift = yShift;    }    double getLenght(){        return Math.sqrt(xShift*xShift+yShift*yShift);    }}class NormalisedShift{  double normalizedXShift;  double normalizedYShift;  double angle;  NormalisedShift (Shift shift){      if (shift.getLenght()>0)      {          normalizedXShift = -shift.getxShift()/shift.getLenght();        normalizedYShift = -shift.getyShift()/shift.getLenght();      }      else      {          normalizedXShift = 0;          normalizedYShift = 0;      }  }  void calcAngle(){      angle = Math.acos(normalizedXShift);  }  double getAngle(){      calcAngle();      return (normalizedYShift<0?angle*360/2/Math.PI:360-angle*360/2/Math.PI);    };};


Особого интереса они не представляют, и комментарий требуют только строки 73-74, нормализующие вектор. Помимо всего прочего, вектор переворачивается. у нег меняется система отсчета дело в том, что с точки зрения программного обеспечения и с точки зрения привычной математики вектора традиционно направляют по разному. Именно поэтому вектора класса Shift имеют начало координат слева сверху, а класса NormalizedShift слева снизу.

Для реализации работы программы был реализован класс Wheel, реализующий интерфейсы NativeMouseMotionListener и NativeKeyListener. Код под спойлером
Заголовок спойлера
public class Wheel  implements NativeMouseMotionListener, NativeKeyListener {    final int KEYCODE = 15;    Shift prev = new Shift();    Shift current = new Shift();    ButtomMatcher mathcer = new ButtomMatcher();    boolean wasPressed = false;    @Override    public void nativeMouseMoved(NativeMouseEvent nativeMouseEvent) {        current.setxShift(nativeMouseEvent.getX());        current.setyShift(nativeMouseEvent.getY());    }    @Override    public void nativeMouseDragged(NativeMouseEvent nativeMouseEvent) {    }    @Override    public void nativeKeyTyped(NativeKeyEvent nativeKeyEvent) {    }    @Override    public void nativeKeyPressed(NativeKeyEvent nativeKeyEvent) {        if (nativeKeyEvent.getKeyCode()==KEYCODE){            if (!wasPressed)            {                prev.setxShift(current.getxShift());                prev.setyShift(current.getyShift());            }            wasPressed = true;        }    }    @Override    public void nativeKeyReleased(NativeKeyEvent nativeKeyEvent) {        if (nativeKeyEvent.getKeyCode() == KEYCODE){            Shift shift = new Shift();            shift.setxShift(prev.getxShift() - current.getxShift());            shift.setyShift(prev.getyShift() - current.getyShift());            NormalisedShift normalisedShift = new NormalisedShift(shift);            mathcer.pressKey(mathcer.getCodeByAngle(normalisedShift.getAngle()));            wasPressed = false;        }    }


Разберемся, что тут происходит.
В переменной KEYCODE хранится код клавиши, служащей для вызова селектора. Обычно это TAB, но при необходимости, его можно изменить в коде или в идеале подтянуть из файла конфига.
prev хранит положение курсора мыши, которое было на момент вызова селектора. В сurrent поддерживается актуальное положение курсора в настоящий момент времени. Соответственно, при отпускании клавиши селектора происходит вычитание векторов и в переменную shift записывается смещение курсора за время удержания клавиши селектора.
Затем, в строке 140, вектор нормализуется, т.е. приводится к виду, когда его длина близка к единице. После чего, нормализованный вектор передается в матчер, который устанавливает соответствие между кодом клавиши, которую нужно нажать и углом проворота вектора. Из соображений читаемости, угол переводится в градусы, а так же ориентируется по полному единичному кругу (acos работает только с углами до 180 градусов).
В классе ButtonMatcher определяется соответствие между углом и выбранным кодом клавиши.
Заголовок спойлера
class ButtomMatcher{    Robot robot;    final int numberOfButtons = 6;    int buttonSection = 360/numberOfButtons;    int baseShift = 90-buttonSection/2;    ArrayList<Integer> codes = new ArrayList<>();    void matchButtons(){        for (int i =49; i<55; i++)            codes.add(i);    }    int getCodeByAngle(double angle){        angle= (angle+360-baseShift)%360;        int section = (int) angle/buttonSection;        System.out.println(codes.get(section));        return codes.get(section);    }    ButtomMatcher() {        matchButtons();        try        {            robot = new Robot();        }        catch (AWTException e) {            e.printStackTrace();        }    }    void pressKey(int keyPress)    {        robot.keyPress(keyPress);        robot.keyRelease(keyPress);    }}


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

Заключение
В рамках данной статьи была описана возможность кастомизации интерфейса классических шутеров для современных стандартов. Конечно, ни аптечек, ни линейности мы тут не добавляем для этого есть множество модов, но зачастую именно в подобных деталях и кроется дружелюбный и удобный интерфейс. Автор осознает, что, вероятно, описал не самый оптимальный способ достижения требуемого результата, а так же ждет в комментариях картинку с буханкой и троллейбусом, но тем не менее это был интересный опыт, который, возможно, сподвигнет какого-нибудь геймера открыть для себя удивительный мир Java.
Конструктивная критика приветствуется.
Исходники
Подробнее..

Spring Integration динамические потоки данных

05.07.2020 20:11:11 | Автор: admin
Салют Хабр! Сегодня мы разберем достаточно специфичную область потоковая обработка данных, с помощью Spring Integration фреймворка и как сделать эти потоки в runtime без предварительной инициализации в контексте приложения. Полный пример приложения лежит в Гите.

Введение


Spring Integration фреймворк корпоративной интеграции (EIP), использующий под капотом механизмы обмена сообщениями между адаптерами различных протоколов/систем интеграции на основе каналов сообщений (условные очереди). Известными аналогами являются Camel, Mule, Nifi.

Из тестового кейса у нас будет сделать REST сервис, который умеет считывать полученные параметры запроса, ходить в нашу базу, к примеру, postgres, делать обновление и выборку из данных таблиц по параметрам, полученных от источника, и отдавать результат в очередь обратно (request/response), а также сделать несколько экземпляров с разными путями запроса.

Условно диаграмма data flow (потока) будет выглядеть так:

image

Далее я покажу, как это можно просто сделать без особых танцев с бубном, с помощью IntegrationFlowContext, с REST-управляющими эндепоинтами компонентов/потоков. Весь основной код проекта будет расположен в репозитории, здесь укажу лишь некоторые вырезки. Что ж, кто заинтересован, прошу под кат.


Инструментарий


По стандарту начнем с блока зависимостей. В основном нам понадобятся проекты spring-boot для REST идеологии управления потоками/компонентами, spring-integration для создания нашего кейса на основе каналов и адаптеров.

И сразу думаем, что же нам еще понадобиться для воспроизведения кейса. Кроме core зависимостей, нам понадобятся подпроекты integration-http, integration-jdbc, integration-groovy (обеспечивает динамически-настраиваемые преобразователи данных на основе Goovy скриптов). Отдельно скажу, что в данном примере мы не будем использовать groovy преобразователь за ненадобностью, но предоставим возможность его настройки из вне.

Dependency list
 <!-- Spring block -->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.data</groupId>            <artifactId>spring-data-commons</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-integration</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.integration</groupId>            <artifactId>spring-integration-groovy</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.integration</groupId>            <artifactId>spring-integration-http</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.integration</groupId>            <artifactId>spring-integration-jdbc</artifactId>        </dependency>        <!-- Db block -->        <dependency>            <groupId>org.postgresql</groupId>            <artifactId>postgresql</artifactId>        </dependency>        <dependency>            <groupId>com.zaxxer</groupId>            <artifactId>HikariCP</artifactId>        </dependency>        <!-- Utility block -->        <dependency>            <groupId>org.apache.commons</groupId>            <artifactId>commons-lang3</artifactId>        </dependency>        <dependency>            <groupId>org.reflections</groupId>            <artifactId>reflections</artifactId>            <version>0.9.12</version>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <version>1.18.4</version>            <scope>provided</scope>        </dependency>



Внутренняя кухня


Перейдем к созданию необходимых компонентов системы (оберток/моделей). Нам понадобятся модели channel, bean, httpInboundGateway, handler, jdbcOutboundGateway и result.

bean вспомогательный объект, необходимый для работы адаптеров, потока
channel канал поставки сообщений в/из компонентов потока
httpInboundGateway http точка доступа, которой мы дальше будем отправлять запрос с данными для последующей обработки
handler обобщенный тип обработчика (груви трансформеры, различные адаптеры, etc.)
jdbcOutboundGateway адаптер jdbc
result обработчик отдачи информации в определенный канал

Обертки нам понадобятся для хранения параметров и корректной инициализации компонентов цельного потока, поэтому сразу делаем хранилище компонентов, доп. функционал конверторов JSON -> Definition Model. Прямое сопоставление полей с помощью jackson и объектов в моем кейсе не было применимо имеем еще один велосипед под специфический протокол общения.

Сразу сделаем красиво, на аннотациях:
StreamComponent отвечает за идентификацию классов, как настроечную модель компонента потока и имеет в себе служебную информацию имя компонента, тип компонента, вложенный ли компонент и описание;
SettingClass отвечает за дополнительные опции сканирования модели, такие как сканирования полей супер класса и игнорировании полей при инициализации значений;
SettingValue отвечает за идентификацию поля класса, как настраиваемого из вне, с настройками именования в JSON, описанием, преобразователем типов, флагом обязательного поля и флагом внутреннего объекта для информативности;

Менеджер хранения компонентов

Вспомогательные методы работы с моделями для REST контроллеров

Базовая модель абстракция с набором вспомогательных полей/методов модели

Текущие модели настройки потока

Маппер JSON -> Definition Model

Основную почву для работы подготовили. Теперь приступим к реализации, непосредственно, сервисов, которые будут отвечать за жизненный цикл, хранение и инициализацию потоков и сразу будем закладывать идею того, что 1 поток с тем же именованием мы можем расспараллелить на несколько экземпляров, т.е. нам нужно будет делать уникальные идентификаторы (гуиды) для всех компонентов потока иначе в контексе приложения могу возникнуть коллизии с другими singleton компонентами (бинами, каналами и пр.). Но предварительно сделаем мапперы двух компонентов это http и jdbc, т.е. приращение моделей, сделанных ранее к компонентам самого потока (HttpRequestHandlerEndpointSpec и JdbcOutboundGateway).

HttpRegistry

JdbcRegistry

Центральный управляющий сервис (StreamDeployingService) выполняет функции хранения рабочих/неактивных, регистрирует новые, запускает, останавливает и удаляет потоки полностью из контекста приложения. Важной особенностью сервиса является внедрение зависимости IntegrationFlowBuilderRegistry, который нам и помогает делать динамику приложения (возможно вспомните эти конфигурационные xml файлы или DSL классы на километры). По спецификации потока он должен всегда начинаться с inbound компонента или канала, поэтому учитываем это в реализации метода registerStreamContext.

И вспомогательный менеджер (IntegrationFlowBuilderRegistry), выполняющий функцию как маппера моделей на компоненты потока, так и инициализацию самого потока средствами IntegrationFlowBuilder. Так же я внедрил обработчик логов в пайплайн потока, сервис сбора метрик каналов потока (выключаемая опция) и возможную реализацию преобразователей сообщений потока, основанных на Groovy реализации (если вдруг этот пример станет основой прода, то предкомпиляцию groovy скриптов обязательно нужно делать на этапе инициализации потока, ибо упретесь на нагрузочных тестах в ОЗУ и без разницы сколько у Вас ядер и мощности). В зависимости от конфигурации модели параметров log-stages и log-level, он будет активен после каждой передачи сообщения от компонента к компоненту. Мониторинг включается и отключается параметром в application.yml:
monitoring:  injectction:    default: true


Теперь у нас есть вся механика для инициализации динамических потоков обработки данных, можно дополнительно написать мапперы для различных протоколов и адаптеров типа RabbitMQ, Kafka, Tcp, Ftp, etc. тем более собственноручно в большинстве случаев ничего самому (кроме, естественно, моделей настройки и вспомогательных методов) ничего писать уже не нужно достаточно большое количество компонентов уже присутствуют в репозитории.

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

ComponentsController обеспечивает отдачу информации как о всех компонентах в удобочитаемой модели, так и одного компонента по имени и типу.

StreamController обеспечивает полноценное управление потоками, а именно инициализацию новых по JSON модели, запуск, остановку, удаление и выдачу метрик по идентификатору.

Конечный продукт


Поднимаем получившееся приложение и описываем тестовый кейс в JSON формате.

Sample Data Stream
Скрипт инициализации базы данных:
CREATE TABLE IF NOT EXISTS account_data(    id          INT                      NOT NULL,    accountname VARCHAR(45)              NOT NULL,    password    VARCHAR(128),    email       VARCHAR(255),    last_ip     VARCHAR(15) DEFAULT NULL NOT NULL);CREATE UNIQUE INDEX account_data_username_uindex    ON account_data (accountname);ALTER TABLE account_data    ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (        SEQUENCE NAME account_data_id_seq            START WITH 1            INCREMENT BY 1            NO MINVALUE            NO MAXVALUE            CACHE 1        );ALTER TABLE account_data    ADD CONSTRAINT account_data_pk        PRIMARY KEY (id);CREATE TABLE IF NOT EXISTS account_info(    id             INT NOT NULL,    banned         BOOLEAN  DEFAULT FALSE,    premium_points INT      DEFAULT 0,    premium_type   SMALLINT DEFAULT -1);ALTER TABLE account_info    ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (        SEQUENCE NAME account_info_id_seq            START WITH 1            INCREMENT BY 1            NO MINVALUE            NO MAXVALUE            CACHE 1        );ALTER TABLE account_info    ADD CONSTRAINT account_info_account_data_id_fk FOREIGN KEY (id) REFERENCES account_data (id)        ON UPDATE CASCADE ON DELETE CASCADE;ALTER TABLE account_info    ADD CONSTRAINT account_info_pk        PRIMARY KEY (id);INSERT INTO account_data (accountname, password, email, last_ip)VALUES ('test', 'test', 'test@test', '127.0.0.1');INSERT INTO account_info (banned, premium_points, premium_type)VALUES (false, 1000, 1);


Важно: параметр order служит для последовательной инициализации компонентов в контексте потока, т.е. как выстроены компоненты по этому параметру, так и будет воспроизведена обработка входящего сообщения. (каналы и бины всегда выставляются первыми в списке). А по хорошему нужно сделать обработку графа и надобность в этом параметре отпадет сам собой.
{  "flowName": "Rest Postgres stream",  "components": [    {      "componentName": "bean",      "componentType": "other",      "componentParameters": {        "id": "pgDataSource",        "bean-type": "com.zaxxer.hikari.HikariDataSource",        "property-args": [          {            "property-name": "username",            "property-value": "postgres"          },          {            "property-name": "password",            "property-value": "postgres"          },          {            "property-name": "jdbcUrl",            "property-value": "jdbc:postgresql://localhost:5432/test"          },          {            "property-name": "driverClassName",            "property-value": "org.postgresql.Driver"          }        ]      }    },    {      "componentName": "message-channel",      "componentType": "source",      "componentParameters": {        "id": "jdbcReqChannel",        "order": 1,        "channel-type": "direct",        "max-subscribers": 1000      }    },    {      "componentName": "message-channel",      "componentType": "source",      "componentParameters": {        "id": "jdbcRepChannel",        "order": 1,        "channel-type": "direct"      }    },    {      "componentName": "http-inbound-gateway",      "componentType": "source",      "componentParameters": {        "order": 2,        "http-inbound-supported-methods": [          "POST"        ],        "payload-type": "org.genfork.integration.model.request.http.SimpleJdbcPayload",        "log-stages": true,        "log-level": "INFO",        "request-channel": "jdbcReqChannel",        "reply-channel": "jdbcRepChannel"      }    },    {      "componentName": "handler",      "componentType": "processor",      "componentParameters": {        "order": 3,        "handler-definition": {          "componentName": "jdbc-outbound-adapter",          "componentType": "app",          "componentParameters": {            "data-source": "pgDataSource",            "query": "SELECT accountname, password, email, last_ip, banned, premium_points, premium_type FROM account_data d INNER JOIN account_info i ON d.id = i.id WHERE d.id = :payload.accountId",            "update-query": "UPDATE account_info SET banned = true WHERE id = :payload.accountId",            "jdbc-reply-channel": "jdbcRepChannel",            "log-stages": true,            "log-level": "INFO"          }        }      }    },    {      "componentName": "result",      "componentType": "app",      "componentParameters": {        "order": 4,        "cancel": false,        "result-channel": "jdbcRepChannel"      }    }  ]}



Тестируем:
1) Инициализируем новый поток методом:
POST /stream/deploy, где в теле запроса будет наш JSON.

В ответ система должна будет прислать, если все корректно, в противном же случаи будет видно сообщение об ошибке:
{    "status": "SUCCESS", - статус инициализации    "streamId": "2bf65d9d-97c6-4199-86aa-0c808c25071b" - идентификатор потока}


2) Инициируем запуск методом:
GET /stream/2bf65d9d-97c6-4199-86aa-0c808c25071b/start, где указываем идентификатор проинициализированного потока ранее.

В ответ система должна будет прислать, если все корректно, в противном же случаи будет видно сообщение об ошибке:
{    "status": "SUCCESS", - статус инициализации}


3) Вызываем поток по идентификатору в системе? Как, что и где в маппере модели HttpRegistry я прописал условие
Http.inboundGateway(localPath != null ? localPath : String.format("/stream/%s/call", uuid))

где, учитывается параметр http-inbound-path, и, если он не указан явно в конфигурации компонента, то игнорируется и выставляется системный путь вызова. В нашем случаи это будет:

POST /stream/ece4d4ac-3b46-4952-b0a6-8cf334074b99/call где присутствует идентификатор потока, c телом запроса:
{    "accountId": 1}


В ответ получим, если этапы обработки запроса отработали корректно, получим плоскую структуру записей таблиц account_data и account_info.

{    "accountname": "test",    "password": "test",    "email": "test@test",    "last_ip": "127.0.0.1",    "banned": true,    "premium_points": 1000,    "premium_type": 1}


Специфика адаптера JdbcOutboundGateway такая, что если указывать параметр update-query, то регистрируется дополнительный обработчик, который выполняет сначала обновление данных, а только потом выборку по параметру query.

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

4) Посмотрим метрики методом GET /stream/2bf65d9d-97c6-4199-86aa-0c808c25071b/metrics
Содержимое ответа
Показана информация о отправленном количестве сообщений в физические каналы компонентов потока, рейте ошибочных/успешных сообщений, среднее/минимальное и максимальное время обработки на каждом канале/компоненте потока:
[    {        "streamId": "2bf65d9d-97c6-4199-86aa-0c808c25071b",        "channelName": "application.Rest Postgres stream_2bf65d9d-97c6-4199-86aa-0c808c25071b_jdbcReqChannel",        "sendDuration": {            "count": 1,            "min": 153.414,            "max": 153.414,            "mean": 153.414,            "standardDeviation": 0.0,            "countLong": 1        },        "maxSendDuration": 153.414,        "minSendDuration": 153.414,        "meanSendDuration": 153.414,        "meanSendRate": 0.001195117818082359,        "sendCount": 1,        "sendErrorCount": 0,        "errorRate": {            "count": 0,            "min": 0.0,            "max": 0.0,            "mean": 0.0,            "standardDeviation": 0.0,            "countLong": 0        },        "meanErrorRate": 0.0,        "meanErrorRatio": 1.1102230246251565E-16    },    {        "streamId": "2bf65d9d-97c6-4199-86aa-0c808c25071b",        "channelName": "application.2bf65d9d-97c6-4199-86aa-0c808c25071b.channel#2",        "sendDuration": {            "count": 1,            "min": 0.1431,            "max": 0.1431,            "mean": 0.1431,            "standardDeviation": 0.0,            "countLong": 1        },        "maxSendDuration": 0.1431,        "minSendDuration": 0.1431,        "meanSendDuration": 0.1431,        "meanSendRate": 0.005382436008121413,        "sendCount": 1,        "sendErrorCount": 0,        "errorRate": {            "count": 0,            "min": 0.0,            "max": 0.0,            "mean": 0.0,            "standardDeviation": 0.0,            "countLong": 0        },        "meanErrorRate": 0.0,        "meanErrorRatio": 0.0    },    {        "streamId": "2bf65d9d-97c6-4199-86aa-0c808c25071b",        "channelName": "application.Rest Postgres stream_2bf65d9d-97c6-4199-86aa-0c808c25071b_jdbcRepChannel",        "sendDuration": {            "count": 1,            "min": 0.0668,            "max": 0.0668,            "mean": 0.0668,            "standardDeviation": 0.0,            "countLong": 1        },        "maxSendDuration": 0.0668,        "minSendDuration": 0.0668,        "meanSendDuration": 0.0668,        "meanSendRate": 0.001195118373693797,        "sendCount": 1,        "sendErrorCount": 0,        "errorRate": {            "count": 0,            "min": 0.0,            "max": 0.0,            "mean": 0.0,            "standardDeviation": 0.0,            "countLong": 0        },        "meanErrorRate": 0.0,        "meanErrorRatio": 1.1102230246251565E-16    }]



Заключение


Таким образом было показано, как можно потратив чуть больше времени и сил, написать приложении интеграции с различными системами, чем каждый раз в своем же приложении писать дополнительные ручные обработчики (пайплайны) для интеграции с другими системами по 200-500 строк кода.
В текущем примере можно распараллеливать работу однотипных потоков на несколько экземпляров средством уникальных идентификаторов избегая коллизий в глобальном контексте приложения между зависимостями потока (бинами, каналами, etc.)
В дополнении можно развивать проект:
сделать сохранение потоков в бд;
сделать поддержку всех интеграционных компонентов, что предоставляет нам сообщество spring и spring-integration;
сделать воркеры, которые по расписанию бы выполняли работу с потоками;
сделать вменяемый UI по конфигурированию потоков условной мышкой и кубиками компонентов (кстати, пример частично затачивался под проект github.com/spring-cloud/spring-cloud-dataflow-ui);

И еще раз продублирую ссылку на репозиторий.
Подробнее..
Категории: Java , Spring , Integration , Data-flow

Я бросил свой бизнес и стал разработчиком в 43 года

06.07.2020 12:10:38 | Автор: admin
Интернет пестрит захватывающими историями о людях, которые бросили наёмную работу ради собственного бизнеса. У Сергея Парахина, разработчика из московского офиса EPAM, ситуация другая. Он больше 20 лет развивал свой собственный бизнес, который всегда был связан с IT. По иронии судьбы именно стремительное развитие информационных технологий в корне изменило его компанию, и бизнес всё дальше уходил от IT-сферы. Это побудило Сергея задуматься о смене профессии, и он решил стать разработчиком.

Сергей рассказал, почему в 43 года бросил бизнес и стал разработчиком в IT-компании, и описал, к каким сложностям стоит быть готовым.



Как я решился уйти из собственного бизнеса в разработку


Я закончил институт по специальности Информационные системы и сети и следующие 20 лет вместе с партнёром развивал собственный бизнес по поставке и обслуживанию справочно-правовых систем. Разработчиков как таковых у нас не было, но была своя техническая поддержка, которая устанавливала клиентам справочно-правовые базы и устраняла неполадки в их работе. Я занимался всем от набора новых сотрудников до взаимодействия с поставщиками и клиентами. За эти 20 лет я накопил очень большой технический кругозор: начинал работать ещё во времена MS-DOS, дискет и первых версий Windows. Многое понимал и знал из IT-области, но каких-то системных и глубоких знаний для того, чтобы зарабатывать именно программированием, тогда у меня ещё не было.

С относительно недавнего времени все справочно-правовые системы перешли в онлайн-формат, и мы перестали работать непосредственно с самими базами. Таким образом наш бизнес стал смещаться в сторону консультирования бухгалтеров и юристов, наших основных клиентов. Мне это не нравилось: консалтинг мне неинтересен, да и не считаю я себя профессионалом в этой области. В итоге мы с партнёром решили выйти из бизнеса. На тот момент мне было 43 года.

Нужно было думать, чем заниматься дальше. Мне хотелось что-то делать руками, работать самостоятельно и не зависеть от посторонних людей. У многих ошибочное представление, что бизнесмен ни от кого не зависит, но это не так. Ты зависим от своих сотрудников, клиентов, партнёров, поставщиков, государства и ещё десятка других факторов. Программирование так или иначе сопровождало меня всю жизнь, и я начал думать в сторону этой области. На рынке достаточно большой спрос на разработчиков, и я понял, что могу вскочить на IT-поезд и чего-то достичь, даже если мне за 40. У меня были перед глазами примеры: несколько моих 33-35-летних знакомых в своё время закончили в Иннополисе курсы по Java. Сейчас все они опытные разработчики и успешно работают в IT. Мне захотелось повторить их путь. Ведь раз они смогли сменить профессию, то же самое было под силу и мне. Меня ещё сильно мотивировали и подстёгивали истории успеха на JavaRush. Я мечтал, что когда-нибудь смогу написать про собственный удачный опыт, а теперь вот рассказываю вам о своём пути.

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

Конечно, было страшно заканчивать с бизнесом, который я вёл целых 20 лет, и совершенно непонятно, что будет дальше. Но буквально за несколько недель страхи ушли: я посмотрел вакансии, увидел популярность языков разработки и оценил свои перспективы через несколько лет. Я всю жизнь только и делал, что учился чему-то новому, поэтому быстро понял, что бояться нечего.

Бывший партнёр по бизнесу сомневался во всей этой затее и говорил, что молодые разработчики задавят меня и вообще уже поздно так кардинально менять жизнь. Но я его особо не слушал. Нужно думать не Мне уже 40, а Мне только 40. Впереди минимум 20-30 лет активной жизни, поэтому я не рассуждал, где нахожусь сейчас, а думал о том, где буду через несколько лет.

Для меня самым главным было то, что семья меня полностью поддержала. На тот момент мы жили в Орле, но моя старшая дочь заканчивала 11 класс и хотела поступать в московский вуз. Она активно готовилась к ЕГЭ, участвовала и побеждала на всероссийских олимпиадах и во всевозможных конкурсах. Меня мотивировали её стремление и старания, и я уже просто не мог отступить. К тому же оставшаяся от продажи бизнеса финансовая подушка позволила мне не работать несколько месяцев и посвятить всё время обучению.

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

Как я учился: самостоятельно, с ментором и на курсах


Самостоятельная учёба

Летом 2018 года я начал изучать Java. На тот момент работы как таковой у меня не было я уже передавал дела по бизнесу, и мне удавалось ежедневно посвящать учёбе по 4-8 часов. Начинал с ресурса JavaRush. Решал задачки, смотрел обучающие видео, читал. Самостоятельно дошёл до 20 уровня из 41. Проблем с материалами не было: всегда можно найти что-то полезное в интернете. Не зря же говорят, что главное умение программиста умение гуглить. Учиться самому можно, было бы желание и, самое главное, время.

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

Менторская программа и первые проекты

Вскоре я нашёл проект Java Mentor, и обучение пошло быстрее. Я общался уже не с ботом на сайте, а с живым человеком. Опытные менторы давали обратную связь по выполнению заданий, проводили код-ревью, объясняли ошибки. В феврале я даже побывал на двухнедельном хакатоне Java Bootcamp в Санкт-Петербурге, где наконец-то поработал в команде над небольшим проектом.

После хакатона я прошёл отбор на онлайн-курс по Java EE при Университете Иннополис. Здесь всё было по-серьезному: очень плотный график занятий, ментор из IT-компании, настоящий и большой командный проект (мы разрабатывали аналог виртуальной торговой площадки).

Таким образом, у меня за плечами были уже два проекта. Неважно, что это был не продакшн и за них не платили. В любом случае, это была моя реальная работа. Советую всем новичкам в IT: не пишите в резюме учебный проект, лучше укажите, чем именно вы занимались и каких результатов добились, например, реализовал систему сортировки данных, улучшил производительность системы с 50 до 100 запросов в секунду. Точно так же не стоит оценивать себя как специалиста и указывать в резюме, что вы джун или мидл. Пишите просто: Java-разработчик. Оценят вас уже на собеседовании, которых может быть десяток, и в одной компании дадут джуна, а в другой синьора. Поэтому всегда лучше сосредотачиваться на своих реальных достижениях и результатах.

Первые офферы, переезд в Москву и работа в EPAM


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

Получается, спустя год после того, как я всё бросил и начал изучать Java, я имел на руках три предложения о работе. Меня звали на позицию мидла, но в Иннополисе я работать не хотел это очень далеко от Орла. Но и в моём родном городе не было подходящих вакансий. Встал вопрос о переезде.

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

В офисе в основном работают ребята до 30 лет, и иногда на меня посматривали молодые коллеги, мол, что этот дядя тут делает. К тому же меня не миновал синдром самозванца: я не мог понять, как меня вообще сюда взяли, казалось, я не на своём месте. Потом понял, что всегда, когда приходишь в новую компанию, ещё и на большой проект, первое время чувствуешь себя неловко. И это нормально! Иногда проще спросить и сэкономить время, своё и своей команды, чем сидеть и копаться. Если ты, допустим, за час даже примерно не понял, как выполнить задачу, лучше спроси у коллег. Не бойся выглядеть глупцом, какие бы регалии у тебя не были.

Несмотря на хороший менеджерский опыт, сейчас мне интересно развиваться именно в техническую сторону. Сейчас я работаю на большом проекте по автострахованию, который скоро уже выходит в продакшн. Каких-то прямо фундаментальных трудов по Java я не читаю, но стараюсь систематизировать все новые знания, которые получаю в процессе работы.

Что изменилось после того, как я пришёл в разработку


Один из самых больших плюсов IT для меня возможность удалённой работы. У нас распределённая команда, и мы работали в таком режиме ещё задолго до кризиса. Я могу работать из Орла, Москвы и откуда угодно, лишь бы был интернет и оборудование.

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

Полезные ресурсы для начинающих Java-разработчиков


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

Книги
  • Изучаем Java, Кэти Сьерра и Берт Бейтс книга для совсем новичков не только в Java, но и в программировании в целом.
  • Философия Java, Брюс Эккель.
  • Java. Библиотека профессионала. Том 1 и 2, Кей Хорстманн и Гари Корнелл.
  • Java. Полное руководство, Герберт Шилдт.
  • Java. Руководство для начинающих, Герберт Шилдт.
  • Основы Java, Николай Прохоренок .
  • Грокаем алгоритмы. Иллюстрированное пособие для программистов и любопытствующих, Адитья Бхаргава очень хорошая книга для понимания основных алгоритмов.

Видеоресурсы

alishev YouTube-канал с видеуроками.
Бесплатный курс на Stepic по основам web-разработки на Java.
letsCode YouTube-канал.
Лекция Основы разработки на Java.

Автор: Элиза Ильязова
Подробнее..

Перевод Создаем Gatling скрипты с помощью VS Code

06.07.2020 20:09:26 | Автор: admin

Перевод статьи подготовлен в преддверии старта курса Нагрузочное тестирование.





Предисловие


Недавно, благодаря комментарию одного из студентов, изучающих мой курс Gatling Fundamentals, я узнал о том, что вы можете создавать Gatling скрипты с помощью Visual Studio Code. Я, честно говоря, понятия не имел, что это возможно, но был приятно удивлен, обнаружив, насколько хорошо это работает!


В этом посте мы рассмотрим, как настроить вашу среду разработки Gatling скриптов в VS Code. Мы рассмотрим инструменты сборки Maven и SBT.


Установка Metals


Первое, что нужно сделать, планируете ли вы работать с Maven или SBT, это установить плагин Scala Metals внутри VS Code. Этот плагин позволит языковому серверу Scala работать в VS Code и предоставит типичные функции, которые вы ожидаете от современного IDE.



Установите плагин из VS Code самым стандартным способом, перейдя на вкладку Extensions и выполнив поиск Scala (Metals):



Имея установленный Metals, давайте сначала посмотрим, как запустить Gatling в VS Code с помощью Maven.


Gatling VScode с Maven


Во-первых, вам нужно установить Maven вы можете скачать Maven с официального сайта и следовать инструкциям по установке на его странице.


Затем установите плагин Maven for Java внутри VS Code:



По-прежнему внутри VS Code, откройте Command Pallette (View > Command Pallette) и выберите Maven: Update Maven Archetype Catalog:



Как и следовало ожидать, это обновит каталог доступных архетипов Maven.


Теперь мы хотим создать новый проект Gatling из архетипа Gatling Maven. Для этого сначала откройте Command Pallette и выберите Maven: Create Maven Project. При выборе архетипа, нажмите more. Введите Gatling, после чего должен появиться архетип Gatling. Дальше смотрите видео ниже:



Сохраните проект в подходящем месте на вашем компьютере. Затем откройте проект как обычно в VS Code. Возможно, на этом этапе вам придется импортировать сборку. Для этого перейдите на вкладку Metals в VS Code и нажмите Import Build:



Это заставит Maven собрать ваш проект.


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


package computerdatabaseimport io.gatling.core.Predef._import io.gatling.http.Predef._import scala.concurrent.duration._class BasicSimulation extends Simulation {  val httpProtocol = http    .baseUrl("http://computer-database.gatling.io") // Здесь находится корень для всех относительных URL    .acceptHeader(      "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"// Вот общие заголовки    .acceptEncodingHeader("gzip, deflate")    .acceptLanguageHeader("en-US,en;q=0.5")    .userAgentHeader(      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0"    )  val scn =    scenario("Scenario Name") // A scenario is a chain of requests and pauses      .exec(        http("request_1")          .get("/")      )      .pause(7) // Note that Gatling has recorder real time pauses  setUp(scn.inject(atOnceUsers(1)).protocols(httpProtocol))}

Чтобы запустить скрипт, откройте терминал в VS Code, и введите mvn gatling:test. Если вы хотите запустить определенный тестовый сценарий, вы можете вместо этого выполнить


mvn gatling:test -Dgatling.simulationClass=computerdatabase.BasicSimulation

Советую вам узнать больше о плагине Gatling Maven.


Gatling VScode с SBT


Если вы предпочитаете запускать и создавать свои Gatling проекты с помощью Scala Build Tool (SBT), я считаю, что проще всего сначала клонировать проект Gatling SBT Plugin Demo.


Как только вы клонировали проект, откройте его как обычно в VS Code. Перейдите на вкладку Metals в VS Code и нажмите Import Build:



VS Code теперь должен собрать ваш проект Gatling с помощью SBT.


Чтобы запустить все тесты в вашем проекте, откройте терминал и введите sbt gatling:test. Или же чтобы запустить конкретный тестовый скрипт, вы можете выполнить команду sbt gatling:testOnly computerdatabase.BasicSimulation.


Вы можете узнать больше о плагине Gatling SBT в его документации.


Резюме


В этой статье мы узнали, как использовать Visual Studio Code для создания наших Gatling скриптов. Мы рассмотрели, как создать и запустить Gatling проект с инструментами сборки Maven и SBT.


Несмотря на то, что IntelliJ IDEA остается моей предпочтительной средой разработки для разработки кода Scala и Gatling, здорово иметь возможность использовать и более популярный VS Code!




Узнать подробнее о курсе Нагрузочное тестирование



Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru