1. 程式人生 > 實用技巧 >老生再談 IoC

老生再談 IoC

IoC,Spring的核心理念之一,確實這是一個老生常談的東西。但是今天呢!又重新溫習之後,想再說說自己對IOC的一些想法。

IoC——Inversion of Control,控制反轉。要想理解IoC還是要從其本身出發,首先就控制而言,控制是對誰的控制——是物件的控制。其次,反轉是什麼的反轉或者說為什麼要稱做反轉——是物件控制權反轉。

物件控制,傳統的方式就是程式設計師通過new關鍵字的方式來生成一個物件,然後由程式設計師根據程式邏輯人為地控制物件的使用。從這裡出發,就可以很好地理解什麼是控制反轉了。

所謂控制反轉,就是將原本在程式設計師手中的物件建立和管理的許可權交給了Spring IoC容器。也就是說,控制反轉就是要轉移程式設計師對物件的控制權,而在Spring當中的實現就是Spring IoC容器通過Xml或註解的描述生成或者獲取物件,再由IoC容器對這些Bean進行管理。

所以,理解IoC(控制反轉),就只需要記住,控制權由誰反轉給了誰。

IoC容器
頂級IoC容器介面—BeanFactory
對於BeanFactory,它的重要性源自於所有IoC容器都是直接或者間接派生自它。雖然,它的功能不是很強大,但是從其原始碼當中卻可以看出很多端倪。

public interface BeanFactory {
/**
工廠Bean的字首,
用於判斷獲取的是FactoryBean還是FactoryBean所產生的例項 下面會有詳細解釋
**/
String FACTORY_BEAN_PREFIX = "&";

/**通過name 獲取Bean**/
Object getBean(String name) throws BeansException;
/**通過name和Class型別 獲取Bean**/
<T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException;
/**通過name和構造引數,也就是可以指定呼叫某個構造方法 獲取Bean**/
Object getBean(String name, Object... args) throws BeansException;
/**通過Class型別 獲取Bean**/
<T> T getBean(Class<T> requiredType) throws BeansException;
/**通過Class型別和構造引數,同樣可以指定呼叫某個構造方法 獲取Bean**/
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

/**返回一個被ObjectProvider包裝的Bean**/
<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

/**通過name判斷是否在容器中有這個Bean**/
boolean containsBean(String name);

/**是否為單例**/
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
/**是否為原型**/
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

/**型別匹配否**/
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;

boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

/**根據name找到Bean的Class型別**/
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;

/**獲取此Bean之外的別名**/
String[] getAliases(String name);

}
FACTORY_BEAN_PREFIX 在Spring當中,有一個叫做FactoryBean的介面,這個類有一個T getObject() throws Exception;這樣的方法,這個方法會返回一個物件例項。對於這個介面的實現類而言,通過BeanFactory的getBean()返回的Bean是實現類本身的例項,還是getObject()的返回例項就在於有沒有字首。有,返回FactoryBean;沒有,返回getObject()的返回例項。
/**舉個簡單的例子
實現這樣一個FactoryBean
用這樣一個FactoryBean來建立一個我們需要的User
**/
@Component("user")
public class UserFactoryBean implements FactoryBean {

@Autowired
private User user;

@Override
public User getObject() throws Exception {
    return user;
}

@Override
public Class<?> getObjectType() {
    return user.getClass();
}

}
//測試方法
public static void test1(){
ApplicationContext ctx = new AnnotationConfigApplicationContext(UserConfig.class);
//沒有字首
//得到的是User getObject() throws Exception的返回值
User user = (User) ctx.getBean("user");
System.out.println(user);

//有字首
//得到的是UserFactoryBean的例項
UserFactoryBean userFactoryBean =
    (UserFactoryBean) ctx.getBean("&user");
System.out.println(userFactoryBean);

}
這裡只是簡單的例子,用來說明FACTORY_Bean_PREFIX的作用,FactoryBean更具體的用法,可以參考工廠模式當中工廠的作用。

ObjectProvider 這是在spring4.3之後才出現的一個介面,它主要作用是解決注入時Bean不存在或者Bean存在多個時出現的異常情況。
//getIfAvailable()可以解決容器中沒有userDao時的異常
public class UserService{
private UserDao userDao;
public UserService(ObjectProvider dao){
userDao = dao.getIfAvailable();
}
}

//5.1之後可以通過流式處理來解決容器中存在多個userDao情況
public class UserService{
private UserDao userDao;
public UserService(ObjectProvider dao){
userDao = dao.orderedStream()
.findFirst()
.orElse(null)
}
}
核心容器—ApplicationContext
學習過Spring的人,對ApplicationContext都不會陌生。它是BeanFactory的子(準確的說應該是孫子)介面之一,而我們所使用到的大部分Spring IoC容器都是ApplicationContext的實現類。

​IoC Container.png

Spring的原始碼很龐大,也很複雜,所以建議學習的時候,從某幾個重點類開始,分析其繼承、擴充套件關係,以此橫向展開對Spring的認識。

這裡也就不再對ApplicationContext的各個繼承介面一一解釋了,API文件裡面都有: ApplicationContext。對於ApplicationContext這個容器更多的是側重於對它的應用介紹,就是如何通過這個容器來獲取Bean。

通過一個簡單的例子來了解一下:

//普通的JavaBean
public class User {
private Long id;
private String name;
private int age;
/getter,setter,toString/
}
//配置類,採用註解的形式來配置Bean
@Configuration
public class UserConfig {
@Bean(name="user")
public User getBeanUser(){
User user = new User();
user.setId(1L);
user.setName("klasdq1");
user.setAge(18);
return user;
}
}
//測試類
public class IocTest {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(UserConfig.class);
User user = ctx.getBean("user");//
System.out.println(user);
}
}
@Configuration @Configuration這個註解的作用就在於它標示的類擁有一個或多個@Bean修飾的方法,這些方法會被Spring容器處理,然後用於生成Bean或者服務請求。
//@Configuration的原始碼
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(
annotation = Component.class
)
String value() default "";

boolean proxyBeanMethods() default true;

}
從註解的原始碼當中可以看出,它有兩個;一是value,用於為配置類宣告一個具體的Bean name。二是proxyBeanMethods,用於指定@Bean修飾的方法能否被代理。

@Bean 這個註解只用在方法上面,用於Spring容器管理生成Bean。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String[] value() default {};

@AliasFor("value")
String[] name() default {};

/** @deprecated */
@Deprecated
Autowire autowire() default Autowire.NO;

boolean autowireCandidate() default true;

String initMethod() default "";

String destroyMethod() default "(inferred)";

}
@Bean的引數中重要的就是name(value),其含義在於為Bean宣告具體的名稱,一個Bean可以有多個名稱,這也是為什麼BeanFactory中有一個getAliases()方法。其他引數,看名字就知道什麼意圖,就不再多解釋了。

AnnotationConfigApplicationContext 這是ApplicationContext類的具體實現類之一,用於註解形式的Bean的生成。與之相對應的還有ClassPathXmlApplicationContext從XML檔案中獲取Bean。
Bean的裝配
在Spring當中對於Bean的裝配允許我們通過XML或者配置檔案裝配Bean,但在Spring Boot中常用註解的形式,為了方便Spring Boot開發的需要,就不再使用XML的形式了。

直接看例子:

//配置JavaBean
@Component("klasdq2")
public class User {
@Value("2")
private Long id;
@Value("klasdq2")
private String name;
@Value("19")
private int age;
/getter,setter,toString/
}
//配置類掃描裝配Bean
@Configuration
@ComponentScan
public class UserConfig {
}
//測試類
public class IocTest {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(UserConfig.class);
User user = (User) ctx.getBean("klasdq2");
System.out.println(user);
}
@Component

@Component的原始碼很簡單:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";
}
引數當中只有一個value,用於宣告Bean的名字(標識)。這裡又出現一個新的註解@Indexed,顧名思義這個註解就是增加一個索引,這是因為Spring Boot當中大量採用掃描的形式來裝配Bean之後,掃描的Bean越多,解析時間就越長,為了提高效能,在5.0版本的時候就引入了這樣一個註解。

@Value

@Target({ElementType.FIELD,
ElementType.METHOD,
ElementType.PARAMETER,
ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {
String value();
}
這個註解可以用在欄位、方法、方法引數、註解上,通過一個表示式或者具體字串為其傳入相應的值。@Value是一個功能非常強大的註解,建議對其多做了解。

其功能主要包括以下幾種:

注入普通字串
書寫SpEL表示式,如:@Value("#{person.name}"),可以從配置檔案、Bean屬性、呼叫方法等等得到資料。
注入Resource,如:@Value("classpath:com/demo/config.txt") 使用Resource型別接收
注入URL資源,如:@Value("http://www.baidu.com") 使用Resource型別接收

@ComponentScan

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
/**
* 這個引數是ComponetScan註解最常用的,其作用就是宣告掃描哪些包,
* 通過掃描,將含有@Componet註解的Bean裝入Spring容器中。
* value和basePackages效果一樣,其預設值為配置類所在包及其子包。
**/
@AliasFor("basePackages")
String[] value() default {};

@AliasFor("value")
String[] basePackages() default {};

/**掃描哪些類**/
Class<?>[] basePackageClasses() default {};

/**Bean Name生成器:自定義bean的命名生成規則**/
Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

/**作用域解析器**/
Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

/**作用域代理**/
ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

/**資源的匹配模式,預設就是.Class**/
String resourcePattern() default "**/*.class";

/**是否啟用預設過濾器(原始碼下面自定義的過濾器)**/
boolean useDefaultFilters() default true;

/**符合過濾器條件的元件 才會掃描**/
ComponentScan.Filter[] includeFilters() default {};
/**符合過濾器條件的元件 不會掃描**/
ComponentScan.Filter[] excludeFilters() default {};

/**是否啟用懶載入**/
boolean lazyInit() default false;

/**過濾器**/
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Filter {
    /**可以按照註解型別或者正則式過濾**/
    FilterType type() default FilterType.ANNOTATION;

    /**過濾哪些類**/
    @AliasFor("classes")
    Class<?>[] value() default {};

    @AliasFor("value")
    Class<?>[] classes() default {};

    /**匹配方式**/
    String[] pattern() default {};
}

}
例如:

@ComponetScan(basePackages="com.klasdq.sb.service.*"
,excludeFilters=(@Filter(classes="UtilService.Class")))
這樣的一個例子中,basePcakages指定了掃描service包下所有具體@Component註解的Service Bean(@Service包含了@Component)。而excludeFilters定義使用@Filter過濾掉UtilService.Class。其他的引數使用,可以引數API文件中的介紹,大同小異。

@ComponetScans 這個註解也可以用於掃描元件,可以定義@ComponetScan,如:
@ComponentScans(value = { @ComponentScan(value = "com.klasdq.sb.service."),
@ComponentScan(value = "com.klasdq.sb.dao.
", excludeFilters=(@Filter(classes="UtilDao.Class")) })
通過這樣一種方式來定義多個掃描元件,使得掃描更加精確。因為@ComponentScan(value="com.klasdq.sb.*")全包掃描的方式雖然寫起來簡單,但是耗費的時間代價卻是極大的。