Spring管理的組件和Classpath的掃描
在前文描述中使用到的Spring中的Bean的定義,都是通過(guò)指定的XML來(lái)配置的。而前文中描述的注解的解析則是在源代碼級(jí)別來(lái)提供配置元數(shù)據(jù)的。在那些例子中,Bean的定義還是在XML中明確的定義的,注解的作用只不過(guò)是提供依賴注入。本文將描述Spring通過(guò)掃描classpath來(lái)自動(dòng)注冊(cè)組件。候選的組建就是指一些類能夠匹配一些過(guò)濾的標(biāo)準(zhǔn),然后自動(dòng)注冊(cè)成為Spring容器之中的Bean。這種方式可以不用在XML中指定Bean的注冊(cè)配置,而是由開(kāi)發(fā)者使用一些注解(比如@Component),AspectJ類型表達(dá)式,或者開(kāi)發(fā)者自定義的過(guò)濾標(biāo)準(zhǔn)來(lái)將類注冊(cè)為容器的Bean。
從Spring 3.0開(kāi)始,Spring的基于
JavaConfig的配置提供的特性就屬于Spring框架了。開(kāi)發(fā)者可以通過(guò)這些特性來(lái)使用Java來(lái)配置Bean,而不再使用XML文件??梢詤⒖?code>@COnfiguration,@Bean,@Import以及@DependsOn這些注解來(lái)了解如何使用這些特性。
@Component和相關(guān)的注解
任何的類配置了@Repository注解,都表示這個(gè)類屬于倉(cāng)庫(kù)類(通常就是DAO對(duì)象)。
Spring提供了很多類似的注解,包括@Component, @Service以及@Controller等。@Component是一個(gè)泛型,代表任何由Spring所管理的組件。而@Repository,@Service以及@Controller都是組件的具體化的一種形式,比如,在持久化上,就會(huì)使用@Repository。因此,開(kāi)發(fā)者可以將任何的組件類都聲明為@Component,但是如果將其聲明為@Repository,@Service,@Controller可以攜帶更多的額外的含義。因此,如果開(kāi)發(fā)者選在在服務(wù)層可以使用@Service或者@Compponent的話,那么@Service則是更好的選擇。
元注解
Spring提供的很多注解都可以在開(kāi)發(fā)者的代碼中作為元注解。元注解,就是可以簡(jiǎn)單的用到另一個(gè)注解之上的注解。比如,@Service注解就是通過(guò)@Component構(gòu)成的元注解。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // Spring will see this and treat @Service in the same way as @Component
public @interface Service {
// ....
}
元注解可以組合起來(lái)創(chuàng)建復(fù)合注解。舉例來(lái)說(shuō),SpringMVC中的@RestController注解就是一個(gè)符合的注解,是通過(guò)@Controller和@ResponseBody組合而成的。
而且,復(fù)合注解可以選擇性的重新聲明注解的屬性,開(kāi)發(fā)者可以通過(guò)這一特性來(lái)進(jìn)行自定義。這一點(diǎn)在開(kāi)發(fā)者想使用一個(gè)元注解某一個(gè)屬性的子集的時(shí)候,尤其高效。比如,Spring的@SessionScope注解就將作用域硬編碼為session了,但是還允許自定義proxyMode屬性。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
SessionScope完全可以通過(guò)如下代碼來(lái)使用:
@Service
@SessionScope
public class SessionScopedService {
// ...
}
或者配合使用proxyMode,如下:
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}
自動(dòng)注冊(cè)
Spring可以自動(dòng)檢測(cè)模板類,然后將其注冊(cè)成為Bean,放到ApplicationContext之中,由其管理。比如,如下的代碼就符合自動(dòng)注冊(cè)的條件。
@Service
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}
為了令諸如上面的類能夠自動(dòng)注冊(cè)成為Bean,你需要在配置有@Configuration的類中增加@ComponentScan注解,在其中配置basePackages屬性,也代表將要掃描的包。(當(dāng)然,開(kāi)發(fā)者也可以根據(jù)逗號(hào)/分號(hào)/空格之類的分隔符來(lái)配置多個(gè)包)。
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}
如果簡(jiǎn)單的配置的話,可以使用注解的
value屬性,比如:ComponentScan("org.example")。
下面是XML的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="org.example"/>
</beans>
明確的使用
<context:component-scan>會(huì)使能<context:annotation-config>。所以在使用<context:component-scan>的時(shí)候一般就不需要包含<context:annotation-config>標(biāo)簽了。
classpath的包路徑的掃描是于目錄結(jié)構(gòu)相關(guān)的。當(dāng)開(kāi)發(fā)者通過(guò)Ant來(lái)構(gòu)建Jar包的時(shí)候,請(qǐng)確保沒(méi)有激活JAR任務(wù)中的files-only開(kāi)關(guān)。而且,classpath的目錄有可能因?yàn)榘踩牟呗裕谝恍┉h(huán)境無(wú)法獲取。相關(guān)問(wèn)題可以參考:stackoverflow
此外,AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor在使用組件掃描元素的時(shí)候,都會(huì)被明確包含的。也就是說(shuō),這兩個(gè)組件也會(huì)自動(dòng)檢測(cè)和裝載,而不需要在XML中進(jìn)行配置。
開(kāi)發(fā)者也可以手動(dòng)取消上面提到的Bean的注冊(cè),在包含注解的屬性中配置為false即可。
使用過(guò)濾器來(lái)自定義掃描
默認(rèn)情況下,所有的注解了諸如@Component,@Repository,@Service,@Controller以及一些自定義的使用@Component元注解的組件,都會(huì)被注冊(cè)到Spring的ApplicationContext之中。然而,開(kāi)發(fā)者可以通過(guò)使用過(guò)濾器來(lái)擴(kuò)展和修改這個(gè)行為。在@ComponentScan注解中增加includeFilters或者excludeFilters(或者是在XML的配置中寫入include-filter或者exclude-filter的子標(biāo)簽)。每一個(gè)過(guò)濾器的元素都要求type和expression兩個(gè)屬性。下表中描述了這些過(guò)濾的選項(xiàng):
| 過(guò)濾類型 | 樣例表達(dá)式 | 描述 |
|---|---|---|
| annotation(默認(rèn)) | org.example.SomeAnnotation |
目標(biāo)組件在使用的注解類型 |
| assignable | org.example.SomeClass |
目標(biāo)組件可以擴(kuò)展或者實(shí)現(xiàn)的類 |
| aspectj | org.example..*Service+ |
目標(biāo)組件匹配的AspectJ表達(dá)式 |
| regex | org\.example\.Default.* |
目標(biāo)組件匹配的正則表達(dá)式 |
| custom | org.example.MyTypeFilter |
一個(gè)自定義的實(shí)現(xiàn)了org.springframework.core.type.TypeFilter的類 |
下面的例子展示了配置會(huì)無(wú)視@Repository注解,而是通過(guò)stub的倉(cāng)庫(kù)類代替。
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
...
}
XML等價(jià)的配置如下:
<beans>
<context:component-scan base-package="org.example">
<context:include-filter type="regex"
expression=".*Stub.*Repository"/>
<context:exclude-filter type="annotation"
expression="org.springframework.stereotype.Repository"/>
</context:component-scan>
</beans>
開(kāi)發(fā)者可以去使能默認(rèn)的過(guò)濾器,Java Config的話,就配置
useDefaultFilters=false,如果是XML的話,就在<component-scan/>標(biāo)簽中配置use-default-filters="false"。這些配置會(huì)使配置了@Component,@Repository,@Service,@Controller或者@Configuration的類再自動(dòng)注冊(cè)為Bean。
通過(guò)組件定義Bean元數(shù)據(jù)
Spring組件也能夠在Bean的元數(shù)據(jù)配置上起作用。開(kāi)發(fā)者可以通過(guò)在配置了@Configuration的類中使用@Bean注解來(lái)定義元數(shù)據(jù),代碼如下:
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// Component method implementation omitted
}
}
上面的類是一個(gè)Spring的組件,在容器中有著應(yīng)用功能的方法doWork(),然而,也提供了一個(gè)工廠方法publicInstance()來(lái)構(gòu)造一個(gè)Bean實(shí)例。@Bean注解能夠識(shí)別工廠方法和其他的Bean屬性,比如通過(guò)@Qualifier注解來(lái)知道限定符的值。其他的方法級(jí)別的注解,比如@Scope,@Lazy等都可以指定。
@Lazy注解出了用在組件的初始化上面,也可以用在注入的地方,比如標(biāo)記了@Autowired或者Inject的地方。它能夠延遲依賴注入的解析。
自動(dòng)裝載實(shí)例變量和方法在前文已經(jīng)有過(guò)描述,自動(dòng)裝載也支持識(shí)別裝載了@Bean方法的Bean的:
@Component
public class FactoryMethodComponent {
private static int i;
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
// use of a custom qualifier and autowiring of method parameters
@Bean
protected TestBean protectedInstance(
@Qualifier("public") TestBean spouse,
@Value("#{privateInstance.age}") String country) {
TestBean tb = new TestBean("protectedInstance", 1);
tb.setSpouse(spouse);
tb.setCountry(country);
return tb;
}
@Bean
private TestBean privateInstance() {
return new TestBean("privateInstance", i++);
}
@Bean
@RequestScope
public TestBean requestScopedInstance() {
return new TestBean("requestScopedInstance", 3);
}
}
上面的例子裝載了String類型的方法參數(shù)到一個(gè)名為privateInstance的Bean的Age屬性中。Spring表達(dá)式語(yǔ)言元素通過(guò)#{ <expression> }來(lái)定義屬性的值。而@Value注解,令表達(dá)式的解析器在配置之前就開(kāi)始查找名字匹配的Bean。
在Spring的處理注解了@Bean的方法時(shí),在組件中的方法和在@Configuration類中的方法處理是不同的。不同之處就在于@Component注解的類是沒(méi)有通過(guò)CGLIB增強(qiáng)來(lái)攔截方法和實(shí)例的調(diào)用的。CGLIB代理就是在@Configuration注解的類創(chuàng)建Bean引用和鏈接對(duì)象的方法。這些方法的調(diào)用不是通過(guò)Java語(yǔ)義調(diào)用的,而是而是便利容器按照順序提供正常的生命周期管理和代理Spring的Bean注入。相反,在@Component注解的類是通過(guò)標(biāo)準(zhǔn)Java語(yǔ)義來(lái)注入和引用Bean的,而沒(méi)有CGLIB處理或者其他的約束。
開(kāi)發(fā)者可能會(huì)將注解了
@Bean的方法配置為靜態(tài)的,來(lái)讓其他的調(diào)用無(wú)需創(chuàng)建對(duì)象即可執(zhí)行。者在定義后置處理Bean的時(shí)候尤其正常,比如,BeanFactoryPostProcessor或者BeanPostProcessor,因?yàn)檫@一類Bean再容器的生命周期中初始化很早,應(yīng)該避免觸發(fā)其他部分的配置。
需要注意的是,調(diào)用靜態(tài)的注解了@Bean的方法在容器中是不會(huì)被攔截的,就算是注解了@Configuration的類也是一樣。這取決于一些技術(shù)的限制,CGLIB自雷只能重寫非靜態(tài)的方法,因此,直接調(diào)用令一個(gè)注解了@Bean的方法只能有標(biāo)準(zhǔn)的Java語(yǔ)義,也就是直接返回由之調(diào)用而產(chǎn)生的對(duì)象。
Java語(yǔ)言并不能立刻在Spring容器中看到Bean實(shí)例的。開(kāi)發(fā)者也可以在非@Configuration的類中聲明靜態(tài)方法。然而,常規(guī)的由@Bean注解的方法是需要被覆蓋的,所以不能聲明為private或者final。
@Bean注解的方法也能夠從組件或者配置的積累來(lái)獲取,在Java 8中,接口注解了@Bean的方法,也是可以的。所以在Java 8中,使用Spring 4.2可以令配置十分靈活。
最后,需要注意的是,一個(gè)類可以為同一個(gè)Bean來(lái)提供多個(gè)不同的方法,這樣可以在運(yùn)行時(shí)來(lái)選擇可以使用的Bean。
命名自動(dòng)注冊(cè)的Bean
當(dāng)組件通過(guò)掃描的過(guò)程來(lái)實(shí)現(xiàn)自動(dòng)注冊(cè)的時(shí)候,該Bean的名字是通過(guò)BeanNameGenerator來(lái)生成的。默認(rèn)情況下,任何Spring的組件標(biāo)簽(@Component,@Repository,@Service以及@Controller)都是包含一個(gè)名字的value的,這個(gè)值就是Bean所相關(guān)聯(lián)的名字。
如果注解不包含名字的value的話,默認(rèn)的名字生成器會(huì)返回小寫的非限制的類名。比如說(shuō),兩個(gè)組件按照如下代碼自動(dòng)注冊(cè)的話,名字會(huì)分別為myMovieLister以及movieFinderImpl:
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
如果開(kāi)發(fā)者不希望名字依賴于默認(rèn)的Bean名字生成器,開(kāi)發(fā)者也可以自定義一個(gè)Bean名字生成的策略。首先,實(shí)現(xiàn)
BeanNameGenerator接口,然后確定包含一個(gè)無(wú)參的默認(rèn)構(gòu)造函數(shù),然后,將其提供給配置的掃描器即可:
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
...
}
如下是使用XML方式
<beans>
<context:component-scan base-package="org.example"
name-generator="org.example.MyNameGenerator" />
</beans>
關(guān)于命名,普遍的原則是,如果其他組件需要明確的指向一個(gè)組件的話,最好特指其名字。另一方面來(lái)說(shuō),自動(dòng)生成的名字通常就足夠容器來(lái)裝載了。
定義自動(dòng)注冊(cè)Bean的作用域
Spring管理的組件,默認(rèn)是都會(huì)被注冊(cè)為singleton的作用域的。然而,如果開(kāi)發(fā)者需要一個(gè)不同的作用域的話,可以通過(guò)使用@Scope注解來(lái)指定。代碼如下:
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
組件的作用信息可以參考前文Spring中Bean的作用域來(lái)了解更多的信息。
如果不想通過(guò)使用注解的方法來(lái)修改作用域的話,可以考慮實(shí)現(xiàn)
ScopeMetadataResolver接口,包含一個(gè)默認(rèn)的無(wú)參構(gòu)造函數(shù),就可以通過(guò)類名來(lái)配置掃描器了:
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
...
}
XML代碼如下:
<beans>
<context:component-scan base-package="org.example"
scope-resolver="org.example.MyScopeResolver" />
</beans>
當(dāng)使用非單例的作用域時(shí),對(duì)于那些Bean是需要使用代理的。原因在前文Spring中Bean的作用域之中有所描述。為了這個(gè)目的,scopedProxy屬性也是需要在掃描器上使用的。其中包括三個(gè)可選的值,分別是no,interfaces以及targetClass。比如說(shuō),下列配置會(huì)使用JDK動(dòng)態(tài)代理:
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
...
}
XML代碼如下:
<beans>
<context:component-scan base-package="org.example"
scoped-proxy="interfaces" />
</beans>
通過(guò)注解提供限定符
@Qualifier注解在前文Spring自動(dòng)裝載的注解中有所描述。前文中所舉的例子是通過(guò)使用限定符來(lái)控制自動(dòng)裝載的依賴。因?yàn)槟切├邮腔赬ML的,限定符元數(shù)據(jù)等信息是在Bean中使用qualifier或者meta子標(biāo)簽來(lái)實(shí)現(xiàn)的。當(dāng)在基于classpath的掃描和自動(dòng)裝載組件的過(guò)程中,開(kāi)發(fā)者可以通過(guò)在候選的類上來(lái)提供限定符元數(shù)據(jù)。如以下的三個(gè)例子:
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}
和絕大多數(shù)的注解機(jī)制類似,注解的元數(shù)據(jù)是綁定到類的定義上的,而使用XML是允許多個(gè)相同類型的Bean來(lái)指定限定元數(shù)據(jù)的,因?yàn)槟莻€(gè)元數(shù)據(jù)是基于實(shí)例的,而不是基于類的。