Custom Kafka Deserializer и Spring’овый контекст. Как инжектить в статические поля

от автора

Привет, Хабр! Меня зовут Николай Пискунов, я ведущий разработчик в подразделении Big Data. 

В блоге beeline cloud я рассказывал о Spring Data JPA, Hibernate, делился личными наблюдениями, как облегчить себе жизнь при написании тестов. Сегодня речь о другом: расскажу, как инжектить в статические поля. Как всегда — на примерах. Поехали.

На практике десериализаторов, представленных в ядре Spring, хватает в 99% случаев. Но бывают ситуации, когда всё же требуется описать свою логику предобработки входящего сообщения.

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

Задавать разделитель и индексы в файле application.properties мы планируем так:

kafka.message.deserializer.delimiter=, kafka.message.deserializer.indexes=0,2,3,5

Сразу скажу, что org.apache.kafka.common.serialization.Deserializer и все его реализации не знают о спринговом контексте ничего — от слова совсем. Даже если попытаться сделать из него бин, пометив соответствующей аннотацией, например @Component.

 Для этого напишем сам Deserializer и попробуем прямо в него заинжектить требуемые настройки, используя спринговую аннотацию @Value:

@Slf4j @Component public class SmartStringDeserializer implements Deserializer<String> {    @Value("${kafka.message.deserializer.delimiter}")    String delimiter;      @Value("${kafka.message.deserializer.indexes}")    List<Integer> indexes;    @Override    public void configure(Map<String, ?> configs, boolean isKey) {        Deserializer.super.configure(configs, isKey);    }      @Override    public String deserialize(String s, byte[] bytes) {        return setValueByIndex(bytes);    }      @Override    public void close() {        Deserializer.super.close();    }      private String setValueByIndex(byte[] bytes) {        String msg = new String(bytes, StandardCharsets.UTF_8);        LOGGER.debug("Income message: {}", msg);        if (indexes.getFirst() == -1) {        return msg;        }        String[] incomeMessage = msg.split(delimiter);        String formattedIncomeMessage = indexes.stream()            .map(index -> incomeMessage[index])            .collect(Collectors.joining(delimiter));        LOGGER.debug("Consumed message: {}", formattedIncomeMessage);          return formattedIncomeMessage;    } }

К сожалению, нам вернется null при попытке получить значения:

А если мы проставим стандартные значения в @Values, результат будет таким же:

 @Value(«${kafka.message.deserializer.delimiter:,}»)

 Но можно использовать статические поля, описав их в соседнем классе. Вот только надо придумать, как в них заинжектить данные из спрингового контекста. Конечно, это должен быть спринговый бин, например @Component, и в нем мы определим статические поля, включая поля, которые будут заполняться данными из properties:

@Component public class DeserializerUtils {      public static String DELIMITER;      public static List<Integer> INDEXES;      private String delimiter;      private List<Integer> indexes;    … }

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

@Component public class DeserializerUtils {      public static String DELIMITER;      public static List<Integer> INDEXES;      private String delimiter;      private List<Integer> indexes;      @Value("${kafka.message.deserializer.delimiter:,}")    private void setDelimiter(String delimiter){        DeserializerUtils.DELIMITER = delimiter;    }      @Value("${kafka.message.deserializer.indexes:-1}")    private void setIndexes(List<Integer> indexes){        DeserializerUtils.INDEXES = indexes;    } }

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

 Теперь перепишем сам десериалайзер:

@Slf4j public class SmartStringDeserializer implements Deserializer<String> {      @Override    public void configure(Map<String, ?> configs, boolean isKey) {        Deserializer.super.configure(configs, isKey);    }      @Override    public String deserialize(String s, byte[] bytes) {        return setValueByIndex(bytes);    }      @Override    public void close() {        Deserializer.super.close();    }      private String setValueByIndex(byte[] bytes) {        String delimiter = DeserializerUtils.DELIMITER;        List<Integer> indexes = DeserializerUtils.INDEXES;        String msg = new String(bytes, StandardCharsets.UTF_8);        LOGGER.debug("Income message: {}", msg);        if (indexes.getFirst() == -1) {        return msg;        }        String[] incomeMessage = msg.split(delimiter);        String formattedIncomeMessage = indexes.stream()            .map(index -> incomeMessage[index])            .collect(Collectors.joining(delimiter));        LOGGER.debug("Consumed message: {}", formattedIncomeMessage);          return formattedIncomeMessage;    } }

А вот теперь мы можем конфигурировать Deserializer, используя спринговый контекст:

У такого подхода есть свои минусы. По факту у нас дублируются данные в хипе, которые не удалит GC ввиду того, что первые хранятся в контексте спринга, вторые — в статических полях. Поэтому старайтесь инжектить конкретные бины и не делайте так:

public static ApplicationContext CONTEXT; private ApplicationContext context;   private void setIndexes(ApplicationContext context){    DeserializerUtils.CONTEXT = context; }

beeline cloud — secure cloud provider. Разрабатываем облачные решения, чтобы вы предоставляли клиентам лучшие сервисы.


ссылка на оригинал статьи https://habr.com/ru/articles/815829/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *