Разрабатывая приложения используя IoC-контейнер Spring думаю каждый задумывался, а как же «правильнее и красивее» создать логгер. В данной публикации хочу привести несколько примеров решения данной задачи.

Решение 1


Получаем логгер напрямую через LoggerFactory:
@Component
public class MyBean {
    private static final Logger log = LoggerFactory.getLogger("application");
    ...
}

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

Решение 2


Получаем логгер из контейнера при помощи Autowired:
@Component
public class MyBean {
    @Autowired
    private Logger log;
    ...
}

Для этого в конфигурации Spring объявляем Bean:
@EnableAutoConfiguration
@ComponentScan
public class ApplicationConfiguration {
    @Bean
    public Logger logger(){
        return LoggerFactory.getLogger("application");
    }
...
}

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

Решение 3


Объявляем каждый логгер в виде отдельного Bean:
@EnableAutoConfiguration
@ComponentScan
public class ApplicationConfiguration {
    @Bean
    @Primary
    public Logger logger(){
        return LoggerFactory.getLogger("application");
    }

    @Bean(name = "loggerBean")
    public Logger loggerBean(){
        return LoggerFactory.getLogger("loggerBean");
    }
...
}

Получаем нужный логгер используя соответствующий Qualifier:
@Component
public class MyBean {
    @Autowired
    private Logger log;
    @Autowired
    @Qualifier("loggerBean")
    private Logger log2;
    ...
}

Данное решение является достаточным в большинстве случаев, и использует только готовые средства контейнера. Одним из минусов данного решения является то, что при добавлении нового логгера всегда придется объявлять новый Bean. Есть ли более универсальный способ?

Решение 4


Получаем логгер из контейнера при помощи специальной аннотации, назовем ее Logging:
@Component
public class MyBean {
    @Logging
    private Logger log;
    @Logging("loggerBean")
    private Logger log2;

    ...
}

Для это собственно необходимо объявить аннотацию:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {
    String value();
}

Данная аннотация будет указывать контейнеру на то, что необходим логгер с именем переданным в параметр value. Если данный параметр не указан, то логгер будет получен по классу компонента, в нашем случае MyBean.
Отлично, но контейнер не умеет обрабатывать нашу аннотацию. Давайте его научим, для этого создадим процессор:
public class LoggingAnnotationProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        Class clazz = bean.getClass();
        do {
            for (Field field : clazz.getDeclaredFields()) {
                Logging annotation = field.getAnnotation(Logging.class); 
                if (annotation!= null) {
                    boolean accessible = field.isAccessible();
                    field.setAccessible(true);
                    try {
                        if(!annotation.value().isEmpty()){
                            field.set(bean, LoggerFactory.getLogger(annotation.value()));
                        } else {
                            field.set(bean, LoggerFactory.getLogger(clazz));
                        }
                    } catch (IllegalAccessException e) {
                        LoggerFactory.getLogger(this.getClass()).error(e.getMessage(), e);
                    }
                    field.setAccessible(accessible);
                }
            }
            clazz = clazz.getSuperclass();
        } while (clazz != null);
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        return bean;
    }

}

И объявим процессор в конфигурации Spring:
@EnableAutoConfiguration
@ComponentScan
public class ApplicationConfiguration {
    @Bean
    public LoggingAnnotationProcessor loggingAnnotationProcessor(){
        return new LoggingAnnotationProcessor();
    }
...
}

Данное решение является более универсальным, но необходимо дополнительно объявить аннотацию и написать для нее процессор.

Заключение


Друзья, предлагай в комментариях ваши варианты решения данной задачи, буду очень рад!

Исходный код примеров доступен на GitHub

Комментарии (38)


  1. gurinderu
    07.02.2016 16:10
    +7

    Мне кажется логгер нужно получать по месту ИМХО.
    Ибо уровень логирования в настройках указать на package проще будет.


    1. xonix
      07.02.2016 16:29
      +2

      Плюсану, всегда использовал что-то типа

      class SomeCls {
        private final Logger log = Logger.getLogger(SomeCls.class);
      }
      

      Также подход автора не позволит логировать из static методов.


      1. MzMz
        07.02.2016 21:03
        +9

        Зато позволит весело словить NPE — если вызов логера произойдет до связывания.


        1. gurinderu
          07.02.2016 23:31

          Не уверен, что это возможно. Спринг бины иницилизирует при старте контексте(кроме lazy).


          1. MzMz
            08.02.2016 11:44
            +3

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


            1. gurinderu
              08.02.2016 13:49
              +1

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


  1. Borz
    07.02.2016 18:50
    +2

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

    В IDEA у себя держу следующий шаблон, который генерит код для Slf4J:

    @SuppressWarnings("unused")
    private static final transient Logger LOG = LoggerFactory.getLogger(CURRENT_CLASS.class.getName());


    но в последнее перебрался на аннотацию @Slf4j из Lombok


    1. gurinderu
      07.02.2016 20:04
      +3

      Зачем static field у вас transient? Они же вроде несереализуемые.


      1. Borz
        07.02.2016 20:13

        1. omickron
          08.02.2016 09:21
          +1

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


          1. Borz
            08.02.2016 11:00

            это не для защиты против обхода, а скорее для тех, кто смотрит только на transient и не смотрит на static. А в целом не понятно — что плохого в такой избыточности?


            1. gurinderu
              08.02.2016 11:20
              +1

              Не знаю насколько это плохо, но если ваш сериализатор сериализует static, то скорее всего нужно дать по шее разработчику этого сериализатора. ИМХО static field никакого отношения к вашим объектам иметь не должны. В этом я даже одобряю scala подход к разделение на class и object.


            1. omickron
              08.02.2016 11:27
              +1

              Ответ в самом вопросе — избыточность. Избыточность ведёт к неоднозначности.

              Если в спецификации сказано, что static поля не должны быть сериализованы, значит нужно бить по рукам тех, кто это делает, во время code review.

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


  1. ruslanys
    08.02.2016 12:13

    Всем рекомендую Lombok и его аннотацию @Sl4j.


    1. uchonyy
      08.02.2016 16:33

      @Slf4j на сколько я помню создает логгер по названию класса, а если необходимо создавать по имени — то не подойдет. Другой вопрос кому как удобнее получать логгер — по классу или по имени, лично у меня есть примеры когда в одних случаях удобнее первый подход, есть когда второй — тут вопрос как личных предпочтений так и зависимости от контекста конкретной задачи =)


      1. Borz
        08.02.2016 16:36

        она имеет атрибут "topic", где можно переопределить с класса на имя


        1. uchonyy
          08.02.2016 16:41

          отлично, перейду пожалуй на Lombok :)


      1. omickron
        08.02.2016 16:37

        Если не ошибаюсь, логгер всегда получается по имени. В частном случае имя логгера берётся из полного названия класса.

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


        1. Borz
          08.02.2016 16:43

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


  1. Lure_of_Chaos
    08.02.2016 15:21
    +2

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


    1. omickron
      08.02.2016 15:25

      Профит этого подхода — вместо `@Autowired` + `@Qualifier` использовать одну аннотацию.
      Но для каждого класса придётся объявлять свою аннотацию + процессор. Разве это удобно?


      1. Lure_of_Chaos
        08.02.2016 15:42

        Но для каждого класса придётся объявлять свою аннотацию + процессор

        Разве?

        И, кстати, хорошо бы вместо @Autowired и Qualifier использовать Inject и Named, а еще бы объединить эти аннотации в одну.


        1. omickron
          08.02.2016 15:59

          Разве?

          Конечно.
          LoggingAnnotationProcessor работает только с `@Logging` аннотацией — создаёт логгер и присваивает его полю с этой аннотацией.

          На мой взгляд, этот подход — велосипед. Я бы не стал его использовать.

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


          1. Lure_of_Chaos
            08.02.2016 16:17

            LoggingAnnotationProcessor работает только с `@Logging` аннотацией

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

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


            1. omickron
              08.02.2016 16:20

              написать аннотацию+процессор, использующую некоторую фабрику

              Добро пожаловать в клуб любителей шаблона ServiceLocator :)

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

              Это же делает AOP. Нужно ли писать велосипед?


              1. Lure_of_Chaos
                08.02.2016 16:29

                Добро пожаловать в клуб любителей шаблона ServiceLocator :)

                А и верно :)
                Это же делает AOP.

                Точно.
                Но и AOP не предел.

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


                1. omickron
                  08.02.2016 16:34

                  все когда-то было велосипедом, пока не стало достаточно популярным :)

                  Это и заставляет писать новые велосипеды :)


        1. Borz
          08.02.2016 16:19

          а разве @Autowired и Inject не эквивалентны?


          1. Lure_of_Chaos
            08.02.2016 16:22

            Верно, в последних версиях Спринга это так.


          1. omickron
            08.02.2016 16:23

            @Autowired — личная аннотация Spring.
            Inject — спецификация JavaEE.

            Spring поддерживает оба варианта. Но если переезжать со Spring на другой контейнер, который не поддерживает @Autowired, код придётся редактировать.

            P. S. Парень, зарегистрировавшийся с логином Inject, наверно икает :)


            1. Borz
              08.02.2016 16:27
              +1

              Не сталкивался ещё со случаями, когда понадобилось избавиться от Spring и целиком переехать на JavaEE. Можете привести?

              P. S. Парень, зарегистрировавшийся с логином Inject, наверно икает :)
              а нечего служебные слова использовать ;)


              1. omickron
                08.02.2016 16:28

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


                1. Lure_of_Chaos
                  08.02.2016 16:35

                  Видимо, у Спринга нет (а, похоже, и не будет) достойных конкурентов.

                  Например, мне очень когда-то понравился фреймворк Tapestry в его 5ой версии. Но пока парни пилили версию 5.4, он безнадежно устарел, увы…


                  1. gurinderu
                    08.02.2016 18:20

                    А как же play?


                    1. Lure_of_Chaos
                      08.02.2016 18:34

                      Play я ковырял достаточно давно, тогда он меня не впечатлил, особенно тем, что он stateless, а также для раскрытия потенциала нужна scala.


  1. zencd
    08.02.2016 20:49

    Автор сам своё творение не использовал что ли…
    Class.getDeclaredFields() может запросто вернуть (и таки возвращает) кучу автогенерируемых полей и только их, т.к. спринг наследует наши классы, а getDeclaredFields() работает только на один уровень; так что до наших собственных полей дело просто не дойдёт (по крайней мере, не сделает это гарантированно).


    1. uchonyy
      08.02.2016 23:04
      +1

      «clazz = clazz.getSuperclass();» дойдет