案例 1:隱式掃描不到 Bean 的定義java
Application 類定義以下:spring
package com.spring.puzzle.class1.example1.application.application
//省略 import
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
HelloWorldController 代碼以下:編程
package com.spring.puzzle.class1.example1.controller.application
//省略 import
@RestController
public class HelloWorldController {
@RequestMapping(path = "hi", method = RequestMethod.GET)
public String hi(){
return "helloworld";
};
}
複製代碼
如圖所示包的結構,咱們會發現這個 Web 應用失效了,即不能識別出 HelloWorldController 了。也就是說,咱們找不到 HelloWorldController 這個 Bean 了。這是爲什麼?數組
案例解析:markdown
要了解 HelloWorldController 爲何會失效,就須要先了解以前是如何生效的。對於 Spring Boot 而言,關鍵點在於 Application.java 中使用了 SpringBootApplication 註解。而這個註解繼承了另一些註解,具體定義以下:app
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//省略非關鍵代碼
}
複製代碼
從定義能夠看出,SpringBootApplication 開啓了不少功能,其中一個關鍵功能就是 ComponentScan,參考其配置以下:ui
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class)
複製代碼
ComponentScan掃描的位置是由ComponentScan 註解的 basePackages 屬性指定的,具體可參考以下定義:this
public @interface ComponentScan {
/**
* Base packages to scan for annotated components.
* <p>{@link #value} is an alias for (and mutually exclusive with) this
* attribute.
* <p>Use {@link #basePackageClasses} for a type-safe alternative to
* String-based package names.
*/
@AliasFor("value")
String[] basePackages() default {};
//省略其餘非關鍵代碼
}
複製代碼
通過調試以後,若是直接使用 SpringBootApplication 註解定義的 ComponentScan,它的 basePackages 沒有指定,掃描的包會是 declaringClass 所在的包,在本案例中,declaringClass 就是 Application.class,因此掃描的包其實就是它所在的包,即 com.spring.puzzle.class1.example1.applicationspa
問題修正調試
在這裏,真正解決問題的方式是顯式配置 @ComponentScan。具體修改方式以下:
@SpringBootApplication
@ComponentScan("com.spring.puzzle.class1.example1.controller")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
也可使用@ComponentScans
@SpringBootApplication
@ComponentScans(value = { @ComponentScan(value = "com.spring.puzzle.class1.example1.controller") })
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
ComponentScans 相比較 ComponentScan 多了一個 s,支持多個包的掃描範圍指定。
案例 2:定義的 Bean 缺乏隱式依賴
如下代碼段,看似沒有問題,實則...
@Service
public class ServiceImpl {
private String serviceName;
public ServiceImpl(String serviceName){
this.serviceName = serviceName;
}
}
複製代碼
ServiceImpl 由於標記爲 @Service 而成爲一個 Bean。另外咱們 ServiceImpl 顯式定義了一個構造器。可是,上面的代碼不是永遠都能正確運行的,有時候會報下面這種錯誤:
Parameter 0 of constructor in com.spring.puzzle.class1.example2.ServiceImpl required a bean of type 'java.lang.String' that could not be found.
複製代碼
案例解析:
當建立一個 Bean 時,它主要包含兩大基本步驟:尋找構造器和經過反射調用構造器建立實例。核心的代碼執行,能夠參考如下代碼片斷:
// Candidate constructors for autowiring?
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
return autowireConstructor(beanName, mbd, ctors, args);
}
複製代碼
Spring 會先執行 determineConstructorsFromBeanPostProcessors 方法來獲取構造器,而後經過 autowireConstructor 方法帶着構造器去建立實例。
autowireConstructor 方法要建立實例,不只須要知道是哪一個構造器,還須要知道構造器對應的參數,這點從最後建立實例的方法名也能夠看出,(即ConstructorResolver#instantiate):
private Object instantiate(
String beanName, RootBeanDefinition mbd, Constructor<?> constructorToUse, Object[] argsToUse)
複製代碼
那麼上述方法中存儲構造參數的 argsToUse 如何獲取呢?換言之,當咱們已經知道構造器 ServiceImpl(String serviceName),要建立出 ServiceImpl 實例,如何肯定 serviceName 的值是多少?
在 Spring當中,咱們不能直接顯式使用 new 關鍵字來建立實例。Spring 只能是去尋找依賴來做爲構造器調用參數。
參數獲取,能夠參考下面的代碼片斷(即 ConstructorResolver#autowireConstructor):
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);
複製代碼
能夠調用 createArgumentArray 方法來構建調用構造器的參數數組,而這個方法的最終實現是從 BeanFactory 中獲取 Bean,能夠參考下述調用:
return this.beanFactory.resolveDependency(
new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);
複製代碼
若是用DeBug調試,則能夠看到更多的信息:
如圖所示,上述的調用便是根據參數來尋找對應的 Bean,在本案例中,若是找不到對應的 Bean 就會拋出異常,提示裝配失敗。
問題修正:
Spring隱式規則:定義一個類爲 Bean,若是再顯式定義了構造器,那麼這個 Bean 在構建時,會自動根據構造器參數定義尋找對應的 Bean,而後反射建立出這個 Bean。
咱們能夠直接定義一個能讓 Spring 裝配給 ServiceImpl 構造器參數的 Bean,例如定義以下:
//這個bean裝配給ServiceImpl的構造器參數「serviceName」
@Bean
public String serviceName(){
return "MyServiceName";
}
複製代碼
程序運行正常。
因此,咱們在使用 Spring 時,不要總想着定義的 Bean 也能夠在非 Spring 場合直接用 new 關鍵字顯式使用,這種思路是不可取的。
參考文獻:
傅健,《Spring編程常見錯誤50例》
本文版權歸做者和掘金共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接。