Внедрение зависимостей в класс, не являющийся частью контекста Spring

Или, другими словами, как быть, если внедрить зависимость нужно, а класс собирается где-то в недрах проекта через new, то есть контекст Spring о нём даже не догадывается (а значит и никаких зависимостей в него не добавляет)?

Картинки по запросу how to do funny pic

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

Впрочем, к чему нам эти долгие вступления? Имеется класс, который мы наследовали от другого класса. Тот (класс-родитель) довольно загруженный, реализует несколько интерфейсов, при этом сам наследует еще один класс, сложно конструируется и выглядит громоздко. Сделать наш новый класс частью контекста Spring кажется невыполнимой задачей – слишком уж сложные зависимости, большая часть которых неизвестна до момента выполнения запроса в БД. Да и не нужен он нам в качестве компонента Spring: у нас имеется абстракция более высокого уровня, которая создаёт все нужные зависимости через new и именно её мы используем в контексте Spring, используя setter injection.

А зачем вообще внедрять?

В одном из переопределённых методов “нашего” наследованного класса нужно вызвать скромный метод из соседнего Spring-бина, который выполнит всю необходимую работу и тим-лид будет доволен. Звучит неплохо!

Реализовать сие “в лоб” не получится – поставив @Autowired перед полем с типом нужного бина мы гарантированно получим null. А зачем Spring внедрять зависимости в классе, если тот создаётся где-то глубоко в дебрях клиента, черезnew, да еще и не является частью его контекста?

Картинки по запросу why funny

А что если… реализовать ApplicationContextAware?!

ApplicationContextAware – это интерфейс, и, если верить документации Spring (лучше верить) он должен реализовываться всеми объектами, которые хотят знать в каком ApplicationContext они запущены. Вроде бы звучит просто. Интерфейс определяет единственный метод setApplicationContext(AppicationContext applicationContext). Приступим к его реалиации!

 @Service("BeanUtil")
public class BeanUtil implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }

    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }

    public static <T> Object getBean(String className) {
        return context.getBean(className);
    }
}

Создали класс BeanUtil, в нём – статическая переменная для хранения ссылки на текущий контекст. Переопределили метод setApplicationContext и создали свой метод getBean, который по имени класса возвращает бин из текущего контекста. Все и вправду легко.

Само собой, чтобы дать Spring корректно внедрить все зависимости, те классы-компоненты контекста, в которых вызывается getBean, должны собираться после того, как собран сам BeanUtil, иначе на старте контекста можем поймать самый настоящий NullPointerException (это, кстати, самое популярное исключение в мире Java). Для этого на зависимых классах можем указать аннотацию @DependsOn:

@Bean
@DependsOn("BeanUtil")
public DbClient clusterClient() {
    return new DBClient();
}

Собираем все вместе

Используем статический метод getBean для получения нужного компонента Spring в классе, который о контексте Spring вообще ничего не знает (и наоборот):

neoClient.setSecondaryBean(BeanUtil.getBean(Bean.class));

Теперь можно быть уверенным (а может и нельзя), что в этом поле не окажется null (а может и окажется).