Подтвердить что ты не робот

Что такое исключение NoSuchBeanDefinitionException и как его исправить?

Пожалуйста, объясните следующее об исключении NoSuchBeanDefinitionException в Spring:

  • Что это значит?
  • При каких условиях он будет брошен?
  • Как я могу предотвратить это?

Этот пост призван стать исчерпывающим NoSuchBeanDefinitionException на NoSuchBeanDefinitionException о возникновении NoSuchBeanDefinitionException в приложениях, использующих Spring.

4b9b3361

Ответ 1

NoSuchBeanDefinitionException объясняет

Исключение BeanFactory когда у BeanFactory запрашивается экземпляр компонента, для которого он не может найти определение. Это может указывать на несуществующий компонент, неуникальный компонент или зарегистрированный вручную экземпляр синглтона без соответствующего определения компонента.

BeanFactory - это абстракция, представляющая Spring Inversion of Control. Он предоставляет бобам внутреннюю и внешнюю поддержку вашему приложению. Когда он не может найти или получить эти компоненты, он NoSuchBeanDefinitionException.

Ниже приведены простые причины, по которым BeanFactory (или связанные классы) не могут найти компонент, и как вы можете убедиться в этом.


Боб не существует, он не был зарегистрирован

В приведенном ниже примере

@Configuration
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
        ctx.getBean(Foo.class);
    }
}

class Foo {}   

мы не зарегистрировали определение bean-компонента для типа Foo ни с @Bean метода @Component, @Component сканирования @Component, ни определения XML, ни каким-либо другим способом. Следовательно, BeanFactory управляемый AnnotationConfigApplicationContext не имеет указания, где получить бин, запрошенный getBean(Foo.class). Фрагмент кода выше

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException:
    No qualifying bean of type [com.example.Foo] is defined

Точно так же исключение могло быть @Autowired при попытке удовлетворить зависимость @Autowired. Например,

@Configuration
@ComponentScan
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
    }
}

@Component
class Foo { @Autowired Bar bar; }
class Bar { }

Здесь определение bean-компонента зарегистрировано для Foo через @ComponentScan. Но Spring ничего не знает о Bar. Поэтому он не может найти соответствующий компонент при попытке автоматически связать поле bar экземпляра компонента Foo. Он выбрасывает (вложенный в UnsatisfiedDependencyException)

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 
    No qualifying bean of type [com.example.Bar] found for dependency [com.example.Bar]: 
        expected at least 1 bean which qualifies as autowire candidate for this dependency. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

Есть несколько способов зарегистрировать определения бинов.

  • @Bean метод в @Configuration классе или <bean> в конфигурации XML
  • @Component (и его @Component, например, @Repository) через @ComponentScan или <context:component-scan.../> в XML
  • Вручную через GenericApplicationContext#registerBeanDefinition
  • Вручную через BeanDefinitionRegistryPostProcessor

...и больше.

Убедитесь, что ожидаемые бобы правильно зарегистрированы.

Распространенной ошибкой является регистрация бинов несколько раз, т.е. смешивая варианты выше для того же типа. Например, я мог бы иметь

@Component
public class Foo {}

и конфигурация XML с

<context:component-scan base-packages="com.example" />
<bean name="eg-different-name" class="com.example.Foo />

Такая конфигурация будет регистрировать два компонента типа Foo, один с именем foo а другой с именем eg-different-name. Убедитесь, что вы случайно не регистрируете больше бобов, чем хотели. Что приводит нас к...

Если вы используете конфигурации как на основе XML, так и на основе аннотаций, убедитесь, что вы импортируете одну из другой. XML обеспечивает

<import resource=""/>

в то время как Java предоставляет аннотацию @ImportResource.

Ожидается один соответствующий боб, но найдено 2 (или больше)

Есть моменты, когда вам нужно несколько бинов для одного типа (или интерфейса). Например, ваше приложение может использовать две базы данных, экземпляр MySQL и Oracle. В таком случае у вас будет два компонента DataSource для управления подключениями к каждому из них. Для (упрощенного) примера, следующее

@Configuration
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
        System.out.println(ctx.getBean(DataSource.class));
    }
    @Bean(name = "mysql")
    public DataSource mysql() { return new MySQL(); }
    @Bean(name = "oracle")
    public DataSource oracle() { return new Oracle(); }
}
interface DataSource{}
class MySQL implements DataSource {}
class Oracle implements DataSource {}

бросает

Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: 
    No qualifying bean of type [com.example.DataSource] is defined:
        expected single matching bean but found 2: oracle,mysql

потому что оба @Bean зарегистрированные через методы @Bean удовлетворяли требованию BeanFactory#getBean(Class), т.е. они оба реализуют DataSource. В этом примере Spring не имеет механизма, чтобы различать или расставлять приоритеты между ними. Но такие механизмы существуют.

Вы можете использовать @Primary (и его эквивалент в XML), как описано в документации и в этом посте. С этим изменением

@Bean(name = "mysql")
@Primary
public DataSource mysql() { return new MySQL(); } 

предыдущий фрагмент не генерировал бы исключение и вместо этого возвращал бин mysql.

Вы также можете использовать @Qualifier (и его эквивалент в XML), чтобы лучше контролировать процесс выбора bean-компонента, как описано в документации. В то время как @Autowired в основном используется для автоматической передачи по типу, @Qualifier позволяет выполнять автоматическую передачу по имени. Например,

@Bean(name = "mysql")
@Qualifier(value = "main")
public DataSource mysql() { return new MySQL(); }

теперь можно вводить как

@Qualifier("main") // or @Qualifier("mysql"), to use the bean name
private DataSource dataSource;

без проблем. @Resource также вариант.

Используя неправильное имя бина

Так же, как существует несколько способов регистрации bean-компонентов, существует также несколько способов их именования.

@Bean имеет name

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

<bean> имеет атрибут id для представления уникального идентификатора для bean-компонента, а name можно использовать для создания одного или нескольких псевдонимов, недопустимых в (XML) id.

@Component и его @Component имеют value

Значение может указывать на предложение для имени логического компонента, который должен быть превращен в bean-компонент Spring в случае автоопределенного компонента.

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

@Qualifier, как уже упоминалось ранее, позволяет вам добавлять дополнительные псевдонимы для bean-компонента.

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


Более сложные случаи

профили

Профили определения бинов позволяют регистрировать бины условно. @Profile, в частности,

Указывает, что компонент имеет право на регистрацию, когда один или несколько указанных профилей активны.

Профиль - это именованная логическая группа, которая может быть активирована программно через ConfigurableEnvironment.setActiveProfiles(java.lang.String...) или декларативно, установив свойство spring.profiles.active в качестве системного свойства JVM, в качестве переменной среды или как параметр контекста сервлета в web.xml для веб-приложений. Профили также можно активировать декларативно в интеграционных тестах с помощью аннотации @ActiveProfiles.

Рассмотрим примеры, в spring.profiles.active свойство spring.profiles.active не установлено.

@Configuration
@ComponentScan
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
        System.out.println(Arrays.toString(ctx.getEnvironment().getActiveProfiles()));
        System.out.println(ctx.getBean(Foo.class));
    }
}

@Profile(value = "StackOverflow")
@Component
class Foo {
}

Это не покажет активных профилей и NoSuchBeanDefinitionException для бина Foo. Поскольку профиль StackOverflow не был активен, бин не был зарегистрирован.

Вместо этого, если я инициализирую ApplicationContext при регистрации соответствующего профиля

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("StackOverflow");
ctx.register(Example.class);
ctx.refresh();

боб зарегистрирован и может быть возвращен/введен.

АОП Прокси

Spring использует AOP-прокси для реализации расширенного поведения. Вот некоторые примеры:

Для этого у Spring есть два варианта:

  1. Используйте прокси- класс JDK для создания экземпляра динамического класса во время выполнения, который реализует только интерфейсы вашего компонента и делегирует все вызовы методов фактическому экземпляру компонента.
  2. Используйте прокси-серверы CGLIB для создания экземпляра динамического класса во время выполнения, который реализует как интерфейсы, так и конкретные типы вашего целевого компонента и делегирует все вызовы методов фактическому экземпляру компонента.

Возьмите этот пример JDK-прокси (достигается с помощью @EnableAsync умолчанию proxyTargetClass из false)

@Configuration
@EnableAsync
public class Example {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(Example.class);
        System.out.println(ctx.getBean(HttpClientImpl.class).getClass());
    }
}

interface HttpClient {
    void doGetAsync();
}

@Component
class HttpClientImpl implements HttpClient {
    @Async
    public void doGetAsync() {
        System.out.println(Thread.currentThread());
    }
}

Здесь Spring пытается найти bean-компонент типа HttpClientImpl который мы ожидаем найти, потому что этот тип явно аннотирован @Component. Однако вместо этого мы получаем исключение

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: 
    No qualifying bean of type [com.example.HttpClientImpl] is defined

Spring обернул бин HttpClientImpl и показал его через объект Proxy который реализует только HttpClient. Таким образом, вы можете получить его с

ctx.getBean(HttpClient.class) // returns a dynamic class: com.example.$Proxy33
// or
@Autowired private HttpClient httpClient;

Всегда рекомендуется программировать на интерфейсы. Когда вы не можете, вы можете сказать Spring использовать прокси CGLIB. Например, с @EnableAsync вы можете установить для proxyTargetClass значение true. Аналогичные аннотации (EnableTransactionManagement и т.д.) Имеют похожие атрибуты. XML также будет иметь эквивалентные параметры конфигурации.

ApplicationContext Иерархии - Spring MVC

Spring позволяет создавать экземпляры ApplicationContext с другими экземплярами ApplicationContext как родительские, используя ConfigurableApplicationContext#setParent(ApplicationContext). Дочерний контекст будет иметь доступ к bean-компонентам в родительском контексте, но обратное неверно. Этот пост подробно описывает, когда это полезно, особенно в Spring MVC.

В типичном приложении Spring MVC вы определяете два контекста: один для всего приложения (корень) и один специально для DispatcherServlet (маршрутизация, методы-обработчики, контроллеры). Вы можете получить более подробную информацию здесь:

Это также очень хорошо объяснено в официальной документации, здесь.

Распространенной ошибкой в конфигурациях Spring MVC является объявление конфигурации WebMVC в корневом контексте с помощью @EnableWebMvc аннотированных классов @Configuration или <mvc:annotation-driven/> в XML, но компоненты @Controller в контексте сервлета. Поскольку корневой контекст не может попасть в контекст сервлета для поиска каких-либо bean-компонентов, обработчики не регистрируются, и все запросы завершаются с ошибкой 404. Вы не увидите NoSuchBeanDefinitionException, но эффект тот же.

Убедитесь, что ваши бобы зарегистрированы в соответствующем контексте, т.е. где они могут быть найдены бобами зарегистрированных WebMVC (HandlerMapping, HandlerAdapter, ViewResolver, ExceptionResolver и т.д.). Лучшее решение - правильно изолировать бобы. DispatcherServlet отвечает за маршрутизацию и обработку запросов, поэтому все связанные bean-компоненты должны входить в его контекст. ContextLoaderListener, который загружает корневой контекст, должен инициализировать любые компоненты, необходимые для вашего приложения: службы, репозитории и т.д.

Массивы, коллекции и карты

Бобы некоторых известных типов обрабатываются специальным образом Spring. Например, если вы попытались MovieCatalog массив MovieCatalog в поле

@Autowired
private MovieCatalog[] movieCatalogs;

Spring найдет все компоненты типа MovieCatalog, MovieCatalog их в массив и вставит этот массив. Это описано в документации Spring, обсуждающей @Autowired. Аналогичное поведение применяется к целям внедрения Set, List и Collection.

Для цели внедрения Map Spring также будет вести себя так, если тип ключа - String. Например, если у вас есть

@Autowired
private Map<String, MovieCatalog> movies;

Spring найдет все компоненты типа MovieCatalog и добавит их в качестве значений на Map, где соответствующим ключом будет их имя компонента.

Как описано ранее, если NoSuchBeanDefinitionException запрашиваемого типа недоступны, Spring выдаст NoSuchBeanDefinitionException. Иногда, однако, вы просто хотите объявить bean-компонент этих типов коллекций, таких как

@Bean
public List<Foo> fooList() {
    return Arrays.asList(new Foo());
}

и вводить их

@Autowired
private List<Foo> foos;

В этом примере Spring потерпит неудачу с NoSuchBeanDefinitionException потому что в вашем контексте нет NoSuchBeanDefinitionException Foo. Но вы не хотели боб Foo, вы хотели бин List<Foo>. До весны 4.3 вы должны были использовать @Resource

Для bean-компонентов, которые сами определены как тип коллекции/карты или типа массива, @Resource является хорошим решением, ссылаясь на конкретную коллекцию или bean-компонент массива по уникальному имени. Тем не менее, @Autowired с 4.3, типы коллекции/карты и массива можно сопоставлять также с помощью алгоритма соответствия типов Springs @Autowired, если информация о типе элемента сохраняется в @Bean возвращаемого типа @Bean или в иерархиях наследования коллекции. В этом случае значения квалификатора могут использоваться для выбора среди однотипных коллекций, как описано в предыдущем абзаце.

Это работает для конструктора, сеттера и инжекции поля.

@Resource
private List<Foo> foos;
// or since 4.3
public Example(@Autowired List<Foo> foos) {}

Однако это не удастся для методов @Bean, т.е.

@Bean
public Bar other(List<Foo> foos) {
    new Bar(foos);
}

Здесь Spring игнорирует любые аннотации метода @Resource или @Autowired, потому что это метод @Bean, и поэтому не может применять поведение, описанное в документации. Однако вы можете использовать Spring Expression Language (SpEL) для ссылки на bean-компоненты по их именам. В приведенном выше примере вы можете использовать

@Bean
public Bar other(@Value("#{fooList}") List<Foo> foos) {
    new Bar(foos);
}

сослаться на компонент с именем fooList и внедрить его.