воскресенье, 23 февраля 2020 г.

Про "интересные" задачи на собеседованиях

Одно время очень популярными на собеседованиях были так называемые задачи "с подвохом".
Например:
Как изменить код программы, чтобы она компилировалась и выводила в консоль "Right", при этом не изменяя код первого метода.
public class Main {
    public static void main(String[] args) {
        System.out.println("Wrong");
    }
    public static void main(String[] args) {
        System.out.println("Right");
    }
}

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

(с) Вячеслав
Я уже прочитал ответ. Эта задачка попала бы в доклад на джокере упоротые вопросы для собеседования)
(с) Сергей
 Задача, чтобы посмотреть:
1) насколько адекватна реакция кандидата на тупой вопрос,
2) читал ли кандидат статью "Как пройти собеседование в ${company-name}"
(с) Михаил
Мне такие задачи напоминают модные в школах в начале 1990-х "задачи на логику". Вот одна из них, правда, из анекдота:
"Летят 3 гуся: 1 серый и 2 белых. Сколько мне лет?"
Когда задают такой вопрос, в голове крутится только один вопрос: "Что?"
Чтобы на интервью решить такую задачу "правильно", нужно предварительно прочитать ту самую статью про "Как пройти собеседование...".
Кстати, у задачи про гусей есть правильный ответ: 22. Чтобы узнать почему 22, нужно найти и прочитать анекдот.

Работа на "серую" компанию

В интернетах часто возникают вопросы про работу на компанию, в которой зарплата выплачивается по "серой" схеме.

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

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

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

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

Карьерный рост в такой компании - штука относительная. У нас в должности Java Architect работал системный администратор без высшего образования, с опытом работы пару лет и самостоятельно нахватавшийся основ Java. После банкротства он никуда архитектором, естественно, не устроился. Нашёл работу то ли системным администратором, то ли инженером по DevOps. 

Отношения между мной и работой

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

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

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

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

Зачем мне нужна работа

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

Зачем мне нужна работа?
Можно жить, не работая. Я проверял. Раходы снижаются до минимума. Но они всё равно есть, даже если жить своим хозяйством или за счёт родственников. Т.е. очевидный и правильный ответ на этот вопрос: чтобы были деньги, на которые можно... и т.д.

Так, для чего мне нужна работа?
В книге "Проект Феникс" Д.Ким, К.Бер и Д.Спаффорд пишут интересную мысль о том, что приритет ролей человека должен определяться следующим порядком: кормилец, родитель, супруг, профессиональная деятельность.
Когда-то давно я задавал себя вопрос: что значит быть мужчиной. Пришёл к выводу, что мужчина должен уметь прокормить и защитить (называю это одним словом - обеспечить) себя и свою семью.
Есть ещё пирамида потребностей Маслоу и других исследователей этого вопроса.
Что объединяет все эти утверждения? То, что для их реализации человеку может быть недостаточно собственных сил и ему необходима будет помощь других людей. Как получить такую помощь? Только за деньги или ответную помощь.

И всё-таки, почему мне нужна именно эта работа?
В сутках 24 часа, из которых:
- 8 часов - это установленная медициной средняя оптимальная продолжительность сна,
- 9 часов - установленное законом время работы (включая 1 час на обед),
- 7 часов - время, которое остаётся на семью, на дорогу к месту работы и обратно, на подготовку к работе, отдых после работы, учёбу, хобби и т.д.
Выходит, что на работу человек выделяет больше времени, чем на любое другое занятие. И так происходит 5 (или 6) дней в неделю и около 250 дней в году.

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

Неуловимый баг

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

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

Баг был плавающим, т.к. smoke тесты (интеграционные тесты, запускавшиеся как отдельный сервис и проверявшие совместную работу нескольких сервисов) падали примерно 1 раз в неделю. Как водится, тесты падали на выходных и ночью, когда рядом не было посторонних наблюдателей, которые могли посмотреть, что в тот момент происходило с сервисом.

Понедельник. Видим, что тест упал в воскресенье.
По результатам исследования логов вижу, что один из сервисов выбрасывает исключение OptimisticLockingException. Такое исключение выбрасывает Hibernate, когда разные потоки пытаются одновременно внести изменения в какую-нибудь сущность и сохранить их в базе данных.
В сервисе, выбросившем исключение, вперемежку используются синхронные (REST) и асинхронные (Apache Kafka) вызовы.
Предполагаю, что асинхронно вызванный метод конфликтует с синхронно вызванным методом за изменение одной из сущностей и сохранение её в базу данных.
Добавляю между двумя тестами паузу, прогоняю несолько раз "зелёные" тесты, рапортую об исправлении бага.

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

Среда. Ночное падение тестов.
Читаю логи. Вижу проблему: один из сервисов получает от другого сервиса 1 запрос и возвращает на него 2 идентичных ответа. Через Кафку возвращает. Асинхронно.
Как говорил Шерлок Холмс: отбросьте невозможное и оставшееся, каким бы невероятным оно ни было, и будет ответом на ваш вопрос.
Сыщик из меня неочень хороший. Поэтому первая моя рабочая гипотеза была такой: где-то в сервисе завёлся поток-призрак (или процесс), который возвращает второй ответ. Невероятно? Ещё бы! Следовательно, по Конан Дойлю, это и есть правильный ответ.
Задвигаю сомнения подальше, начинаю с энтузиазмом прорабатывать свою невероятную версию.
Не нахожу ответа. Внимательно смотрю в логи. Вижу то, что меня смущает: в ответ возвращается сущность, сохранённая в базу данных под уникальным ID и в ответах-близнецах ID возвращаемой сущности всегда одинаковый. Да, быть такого не может, чтобы 2 раза отработавший метод сохранил одну и ту же сущность в базу данных под одним и тем же ID!
Лезу ещё раз в код метода...

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

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

среда, 21 августа 2019 г.

Как добавить в Spring Boot приложение шрифты для библиотеки jasperreports. How to add fonts into Spring Boot application for the jasperreports library

Один из непростых квестов, с которыми мне пришлось столкнуться в последнее время - добавление китайских шрифтов для создания документов с помощью библиотеки jasperreports.
Решил так:

1. Скачал китайский true type шрифт ARIALUNI.TTF вот отсюда: https://community.jaspersoft.com/sites/default/files/files/Report24775-eastAsian.zip

2. Добавил в каталог resources проекта файл шрифтов, а также следующие файлы:


Место размещения файлов должно быть следующим:


3. Проверил доступность шрифтов для библиотеки jasperreports:

Как добавить шрифты в docker-контейнер. How to add fonts into docker container


Вариант 1: добавляем пакет MS Core Fonts:
Вариант 2: добавляем отдельно ttf-файл шрифтов:
Проверяем доступность шрифтов в системе:

четверг, 15 августа 2019 г.

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

Hibernate: How to map ManyToMany association with external table and composite key?
Этот интересный вопрос возник передо мной, когда на новом проекте разработка началась по принципу DDD - т.е. сначала была создана база данных со всеми таблицами и сущностями, а потом всё это нужно было реализовать в java-коде.
Честно скажу, повозиться пришлось с этим кодом немало. Зато теперь всё работает как часы и опыта в копилку прибавилось.
Итак, имеем следующую базу данных:

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

Параметр orphanRemoval=true сообщает фреймворку Hibernate, что тот должен навсегда удалять объекты при удалении их из коллекции.
Параметр fetch = FetchType.LAZY говорит о том, что коллекция будет загружаться только при прямом обращении к ней (по требованию) - т.н. стратегия отложенной загрузки. Для реализации такой отложенной загрузки Hibernate использует вместо объектов-коллекций сгенерированные во время выполнения заглушки, называемые прокси-объектами.
Создаём класс, который будет отображать связь многие-ко-многим между классами Tag и Post:
Что в этой промежуточной сущности интересно:
- аннотация @Immutable объявляет класс неизменяемым. Это позволяет Hibernate не проверять состояние объекта во время выталкивания контекста;
- встроенный класс PostTag.Id инкапсулирует составной ключ, состоящий из полей post_id и tag_id;
- @Embedable объявляет класс PostTag.Id встроенным в таблицу класса PostTag;
- @EmbeddedId - делает почти то же самое, что @Embedable, только для столбцов первичного ключа;
- конструктор класса PostTag, в котором производится формирование первичного ключа и двунаправленной связи многие-ко-многим между объектами Post и Tag. Эту связь очень просто сделать однонаправленной. Для этого в сущности, в которой отображение этой связи не нужно, необходимо убрать поле Set<PostTag> postTags и из конструктора класса PostTag убрать добавление созданного объекта PostTag в это поле.

Как упрощённо выглядит в java-коде создание и удаление связи между объектами Post и Tag на примере класса PostService:
Всё доволно просто:
- для создания связи нужно создать объект PostTag,
- для разрушения связи - удалить соответствующий объект PostTag из коллекции объекта Post.
Всё остальное за нас делает Hibernate.

П.с.: В классах используются аннотации фреймворков Lombok, Spring и Hibernate.

четверг, 1 августа 2019 г.

Учёба

Пару недель назад начал проходить курс React + Redux - Профессиональная Разработка .
Прошёл уже 120 уроков из 149, выполнил 3 учебных проекта. Осталось немного и можно будет пилить какое-нибудь своё приложение на React, чтоб не потерять приобретённые навыки.
Прошёл курс по тестированию web UI: End to End Testing with Google's Puppeteer and Jest
Учебные проекты можно посмотреть здесь, здесь и здесь.
Скриншоты того, что получилось:





Строим очередь сообщений на Apache Kafka с помощью Spring Cloud Stream

Почему Spring Cloud Stream?
Потому что в этом случае для замены в дальнейшем Apache Kafka на другой менеджер очереди (например, RabbitMQ) понадобится всего лишь:
- добавить соответствующие зависимости в файл pom.xml;
- скорректировать настройки в файле application.properties для новой системы сообщений.

Итак, приступим:

1. Добавим в pom.xml зависимости для Apache Kafka.

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-stream-binder-kafka-streams</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>

Далее мы будем просто переключать профили Spring для использования RabbitMQ или Apache Kafka.

2. Добавим в файл application.properties настройки очереди Apache Kafka и свои заголовки.

# Apache Kafka properties
# general
spring.cloud.stream.kafka.binder.brokers=localhost:9092
spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000
spring.cloud.stream.kafka.streams.binder.configuration.default.key.serde=org.apache.kafka.common.serialization.Serdes$StringSerde
spring.cloud.stream.kafka.streams.binder.configuration.default.value.serde=org.apache.kafka.common.serialization.Serdes$StringSerde
#
# outputChannel (out)
spring.cloud.stream.bindings.paymentEventsChannel.producer.headerMode=headers
spring.cloud.stream.bindings.paymentEventsChannel.destination=output
#
# custom headers
spring.cloud.stream.kafka.binder.headers=SenderId,SenderName,Type,MessageId
#
# inputChannel (in)
spring.cloud.stream.bindings.inputChannel.consumer.headerMode=headers
spring.cloud.stream.bindings.inputChannel.destination=input
spring.cloud.stream.bindings.inputChannel.group=queue

3. Создадим свой обработчик каналов сообщений.
Можно использовать имеющиеся в Spring Cloud Stream интерфейсы Source (output channel), Sink (input channel) или Processor (input & output channels), но полезнее будет сделать всё "ручками". Тем более, что в свой обработчик каналов мы можем добавить любое количество каналов.

@Slf4j
@Component
@EnableBinding(ChannelProcessor.class)
public class MessageListener {

    @StreamListener(ChannelProcessor.INPUT)
    public void listen(String message) {
        log.info("MessageListener got message: {}.", message);
    }
}
5. Создадим производителя сообщений, который будет передавать в качестве сообщения наш объект Request.

@Component
public class MessagePublisher {

    private ChannelProcessor channelProcessor;

    @Autowired
    public MessagePublisher(ChannelProcessor channelProcessor) {
        this.channelProcessor = channelProcessor;
    }

    public void sendRequest(Request request) {
        channelProcessor.outputChannel().send(message(request));
    }

    private static final <T> Message<T> message(T val) {
        return MessageBuilder.withPayload(val).build();
    }
}

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

@Slf4j
@Component
@EnableBinding(ChannelProcessor.class)
public class MessageListener {

    @StreamListener(ChannelProcessor.INPUT)
    public void listen(Message<QueueMessage> msg) {

        QueueMessage message = msg.getPayload();
        MessageHeaders headers = msg.getHeaders();
        String messageType = (String) headers.get(MessageDefinitions.TYPE);
        final String token = extractToken(headers);
        final String clientIP = extractClientIP(headers);
        log.info("Got message: {}, client IP: {}, token: {}, message type: {}.",
                message.toString(), clientIP, token, messageType);
    }

    private String extractToken(MessageHeaders headers) {
        return (String) headers.get(MessageDefinitions.SENDER_ID);
    }

    private String extractClientIP(MessageHeaders headers) {
        return (String) headers.get(MessageDefinitions.SENDER_NAME);
    }
}

Аннотация @EnableBinding указывает на то, с каким обработчиком каналов будет связан наш слушатель. @StreamListener - сообщения из какого канала обрабатывает этот метод.

На этом всё.