Все мы любим Spring. Или не любим. Но по крайней мере знаем. Если вы Java-программист, то вероятно используете этот фреймворк каждый день в своей работе. Spring — это огромная платформа, которая предоставляет большой функционал. Тем не менее во главе угла стоят две вещи — это DI (Dependency Injection) и IoC (Inversion of Control). Концепции, которые были призваны, чтобы сделать наш код более читаемым и поддерживаемым. Но к несчастью, все оказалось не так радужно. Именно это мы сегодня и обсудим.  Триггером к написанию этой статьи послужил вот этот вопрос на StackOverflow. Суть заключалась в том, чтобы понять порядок вызовов @PostConstruct и @Autowired, если один компонент зависит от другого. Исходя из этого нужно было определить, что и в каком порядке будет выведено на экран. Мы вернемся к этой задаче в конце статьи, а пока давайте посмотрим на код. Для наглядности я его немного видоизменил, но смысл вопроса сохранен. Люди оставляли довольно подробные ответы с пояснениями. Мол, сначала Spring сделает это, потом то и так далее. Однако я считаю, что здесь стоило несколько сместить акцент. На мой взгляд в этом коде присутствует ряд архитектурных ошибок, после исправления которых вопросы о порядке инстанцирования и вызовов @PostConstruct отпали бы сами собой. Что конкретно здесь плохо? Это мы разберем чуть далее. В начале было слово, и слово было @Autowired. Благодаря этой аннотации совершается магия внедрения зависимостей в наш код. Давайте взглянем на ее декларацию. @Target определяет возможные варианты размещения аннотации. Мы не будет брать в расчет ANNOTATION_TYPE и PARAMETER. Так как первый применяется для хитрого механизма наследования аннотаций в Spring, а второй – для тестовых сред (по крайней мере, так написано в JavaDoc, но лично я ни разу не видел реального примера).
Отсюда можно сделать вывод, что нас интересует три способа применения @Autowired: Рассмотрим каждый из них подробнее. Я думаю, что это худший из всех представленных вариантов. Он нарушает принципы ООП и логику управления потоком данных. Нет ни конструктора, ни сеттера, но зависимость каким-то образом просто появляется внутри объекта. Spring использует Reflection API, поэтому ему совершено безразличны область видимости и отсутствие публичного доступа. Однако нас, программистов, это еще как касается. Во-первых, на такой класс невозможно написать хороший unit-тест. Чтобы убедиться, давайте попробуем сделать это. Напишем тест на вышеприведенный Parent. Здесь мы ожидаемо получим NullPointerException, так как зависимость на Child отсутствует. Но мы не сдаемся. Воспользуемся упомянутым Reflection API. Теперь тест работает так, как ожидается. Насколько плохим является такой подход? Я бы сказал, что на 10 хрустальных ваз из 10. Если мы поменяем имя поля или, не дай бог, тип переменной, тест тут же упадет. Но мы не узнаем об этом до тех пор, пока не запустим его. Особенно неприятно, когда подобные ошибки дают о себе знать после пятнадцатиминутного ожидания сборки на Jenkins. Но тесты — это не единственная проблема (да и будем честны, не всегда мы их пишем). Предположим, что теперь необходимо производить инициализацию через BeanFactory. К сожалению, свойство child опять останется null, потому что нет законного способа передать для него значение. Конечно, мы могли бы вновь использовать всевидящее око. Но хуже рефлексии в тестах может быть только неоправданная рефлексия в коде продукта. Как видим, DI через поля порождает гору проблем, при этом не давая ничего взамен, кроме сомнительной пользы от уменьшения кода класса на несколько строчек. Давайте перепишем компоненты, используя внедрение зависимостей через сеттеры. Стало гораздо лучше. Более того, теперь и тесты можно писать без проблем, и при необходимости инстанцировать объект через BeanFactory. Казалось бы, живи и радуйся. Но и тут есть ложка дегтя. Наличие публичного сеттера дает нам возможность проворачивать нехорошие манипуляции над бизнес-логикой. К примеру, вот такие. Вскоре у нас возникнут проблемы. А может быть и не возникнут. Мы этого не знаем. Возможно эта мина подорвется через 5 минут, а возможно через 5 лет. Все зависит от частоты использования подпорченного нами функционала. Кто-то может заметить, что такого никогда не произойдет. Зачем так писать? В этом нет никакого смысла. Я думаю, что вы будете правы. Если бы такой код упал ко мне код-ревью, аппрувать его я бы точно не стал. Но вот пример, где это имело бы смысл. Предположим, что Parent был объявлен со @Scope(SCOPE_PROTOTYPE). Это означает, что при каждом запросе к ApplicationContext.getBean(Parent.class) будет возвращаться новый инстанс класса. В одном из участков программы потребовалось поменять стандартное поведение компонента. Поэтому через parent.setChild была передана другая имплементация (в данном случае наследник). Все работало как часы. Ведь мы всего лишь поменяли поле у только что созданного объекта. Но в какой-то момент аннотация @Scope была убрана, что означает, что теперь ApplicationContext возвращает синглтон. А тот самый сеттер поменял поведения объекта во всей программе. Кроме того, у DI через сеттеры и поля есть общая проблема – циклические зависимости. Я бы разделил их на два типа: явные и неявные.  Такое невозможно провернуть через конструктор, так как в процессе инициализации бинов мы получим исключение. В нашем случае имеет место быть явная циклическая зависимость.  Что в этом плохого? На мой взгляд они нарушают уровни взаимодействия абстракций. По заветам Дядюшки Боба мы знаем, что потоки данных должны распространятся от более высоких к более низким слоям абстракций. При этом нижние уровни ничего не знают о вышестоящих. Звучит разумно. В конце концов, было бы странно, если драйвер для принтера мог позвонить кому-нибудь в Skype. Однако при наличии возможности внедрить циклические зависимости этот принцип очень легко нарушить. Самым обидным является то, что далеко не всегда такая циклическая связь очевидна. Считаю ли я сеттеры абсолютным злом? Не совсем. Я думаю, что их нужно избегать, но в некоторых ситуациях без них не обойтись. Отличный пример привел Евгений Борисов в своем докладе «Spring-потрошитель, часть 1» (34:56). В рантайме с помощью JMX в Java-приложении переключался флаг профилирования функций. Очевидно, что без сеттера этого не сделать. Я написал довольно подробную статью на Medium, в которой высказываю свою позицию насчет сеттеров. Желающие могут ознакомиться по этой ссылке. Давайте заменим сеттеры конструкторами. Для того чтобы приложение корректно запустилось, необходимо было избавиться от циклической зависмости Parent → Child → Parent. Благо здесь это не составило труда. Я думаю, что этот вариант является лучшим из всех представленных. Во-первых, классы тестируемы. Во-вторых, при необходимости мы можем легко поменять тип инициализации бина. В-третьих, все зависимости инкапсулированы внутри компонентов и не могут быть изменены ни снаружи, ни внутри, так как поля final. Однако еще не все. Осталось последнее – @PostConstruct. Подробно свое мнение по поводу этой аннотации я высказал в этой статье, но вкратце скажу, что здесь в ней нет никакой необходимости и мы можем просто вызвать функцию init внутри конструктора. Приятным бонусом является то, что, начиная со Spring 4.3, @Autowired использовать необязательно, если в классе присутствует единственный конструктор, через который внедряются все зависимости. Исходя из всех вышеописанных выводов, перепишем Parent и Child. А теперь давайте вернемся к изначальному вопросу. В каком порядке будут инстанцированы объекты и выведены надписи на экран? Теперь решение прозрачно. Parent зависит от Child, поэтому он не может быть создан первым. Следовательно, порядок вызовов конструкторов – Child → Parent. Значит, сначала мы увидим "Child is called", потом "Parent is called", а потом 9. Это поведение детерменировано. Мы можем запускать код сколько угодно раз, но результат всегда будет один и тот же. Несмотря на то, что Spring позволяет нам внедрять зависимости как через сеттеры, так и через поля, следует этого избегать. Я думаю, многие со мной согласятся, что вариант классов Parent и Child с DI через конструкторы намного понятнее и чище. Скорее всего, это не вызовет у ваших коллег никаких вопросов, в отличии от запутанного flow с полями и @PostConstruct. Если вы у вас есть вопросы или замечания, прошу оставить комментарии. Спасибо за чтение! Java Team Lead, спикер, лектор/автор в МТС Тета