Spring編程常見錯誤-Spring Core篇(1)

01|Spring Bean 定義常見錯誤

案例 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例》

本文版權歸做者和掘金共有,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文連接。

相關文章
相關標籤/搜索