Spring核心——Stereotype組件與Bean掃描
在註解自動裝載中介紹了通過註解(Annotation)自動向Bean中注入其他Bean的方法,本篇將介紹通過註解(Annotation)向容器添加Bean的方法。
Spring的核心容器提供了@Component和@Bean註解來標記如何向IoC容器添加Bean。在核心包中@Component又派生了@Service、@Controller和@Repository這三個註解(在其他的Spring工程或包中還有更多的派生),本文主要介紹@Component及其派生註解的使用。
一個簡單的使用例子
要想使用@Component等註解來向容器添加Bean,需要向IoC容器指明什麼類有這個註解,所以Spring提供了一個掃描機制讓使用者指定要檢查的路徑。配置非常簡單,只要使用上下文的component-scan標籤即可。我們通過下面的例子來簡單說明如何配置。
例子中的代碼僅用於說明問題,並不能運行。源碼請到https://gitee.com/chkui-com/spring-core-sample自行clone,例子在chkui.springcore.example.hybrid.component包中。
有一個介面和一個實現類作為要添加到IoC容器的Bean:
- package chkui.springcore.example.hybrid.component.bean;public interface NameService { String getName();
- }
- package chkui.springcore.example.hybrid.component.bean;@Componentpublic class NameServiceImpl implements NameService{ @Override
- public String getName() { return "This is My Component";
- }
- }
在實現類NameServiceImpl上使用了@Component註解。
然後XML(/spring-core-sample/src/main/resources/hybrid/component)配置為:
- <?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="chkui.springcore.example.hybrid.component.bean"/></beans>
XML配置文件中沒有任何<bean>的聲明,僅僅是通過component-scan啟用了路徑掃描功能,base-package指定了掃描的包路徑。
然後我們載入這個XML運行Spring IoC容器:
- package chkui.springcore.example.hybrid.component;public class SimpleScanApp { public static void main(String[] args) {
- print(new ClassPathXmlApplicationContext("hybrid/component/scanConfig.xml"));
- }
- private static void print(ApplicationContext context) {
- NameService service = context.getBean(NameService.class);
- System.out.println(service.getName());
- }
- }
運行之後NameServiceImpl就會作為一個Bean添加到IoC容器中。
在 IOC功能擴展點 一文中已經介紹通過XML、@Component、@Bean任何一種方式去聲明一個Bean都會轉化為一個 BeanDefinition 的實現類交給BeanFactory來創建實例,所以實際上通過@Component註解和在XML文件中編寫一個<bean>標籤在結果上並沒有什麼區別——都是向容器添加了一個Bean實例。但是Spring偏偏提供了@Bean和@Component(以及他的派生註解)2個註解來聲名Bean,這當中肯定是有一些差異的。
@Bean在後續的文章會介紹,它就等價與在XML編寫一個<bean>標籤。而@Component以及他的派生註解除了是一個IoC容器中的Bean還有許多附加的含義。
Stereotype與功能分層
觀察@Bean和@Component兩個註解的包,前者是在 org.springframework.context.annotation ,而後者是在 org.springframework.stereotype 。不僅僅是@Component,他的派生註解@Service、@Controller和@Repository都在這個包中,實際上它就是在告訴使用者這些註解提供stereotype的特性(或者稱為功能、作用)。
那什麼是stereotype特性呢?這很難通過Stereotype這個詞的字面意思(這個詞能翻譯的意思很多,這裡最接近的翻譯應該是「舊規矩」或者「使固定」)來理解。
Stereotype特性最早出現在J2EE6中(忘記是哪個JSR提出的了),可以理解為圍繞著「元數據」功能而發展出來的一種設計模式,雖然我很難說清楚他屬於23個設計模式中的哪一個,但是這確實已經是一種約定俗成的做法,只要看到Stereotype就應該像看到「Factory——工廠模式」、「Adapter——適配器模式」、「Facade——外觀模式」一樣,一眼就知道他的作用。
Stereotype特性的目標就是為「組合模式的分層系統」按層標記一個類的功能。所謂的「組合模式的分層系統」實際上就是我們常用的Controller-Service-Dao這種分層模式,只不過有些系統可能會多幾層(比如Controller和Service之間加個RPC框架什麼的)。根據Stereotype特性的Java官網原文介紹,它是一個用來標記註解的註解(annotating annotation)。一個註解如果被@Stereotype標記證明他提供Stereotype模式的功能,例如下面這樣:
- @Stereotype @Target(TYPE)
- @Retention(RUNTIME)
- @interface controller {}@Stereotype @Target(TYPE)
- @Retention(RUNTIME)
- @interface service {}
然後我們在使用時可以為不同層的類打上這些標記,表示他們屬於不同的分層:
- interface UserService{}@Serviceclass UserServiceImpl implements UserService{
- }@Controllerclass UseController{ @Autowired
- UserService userService;
- }
一個類的實例可能會被用於0到多個分層中(比如Spring的一個Bean既可以是Controller也可以是Service,只要標記對應的註解即可),但是通常情況下一個類最多只會用在一個分層中使用。簡單的說Stereotype特性就是用註解來告訴框架某個類是屬於系統功能中的哪一層。
Java的文檔上要求提供Stereotype特性的註解需要用@Stereotype來標記。但是Spring的開發大神並沒有理會這個事,@Component並沒有使用@Stereotype來標記,但是他確實提供了Stereotype的模式。
在Stereotype模式下,Spring核心工程為Controller-Service-Dao的分層模型分別提供了@Controller、@Service、@Repository註解。我們按照Stereotype的模式為對應的類標記3個註解,然後在引入MVC、ORM、JPA相關的框架之後這些註解會告訴框架對應的類扮演著什麼樣的功能角色,框架就能很清晰的根據註解提供相關的功能服務。
例如引入Spring-webmvc之後,一個類如果用@Controller註解標記了之後框架就知道他們都是處理前端請求的,MVC框架就會為他提供RequestMapping之類的功能。隨後我們需要將框架調整為WebFlux,基本上直接更換依賴的Jar包就可以了,因為大家都是按照一個模式來開發的。
所以,如果我們的某個類是用於指定的分層功能,那麼最好使用org.springframework.stereotype包中的註解來標記他所屬的分層。如果類沒有明確的功能(例如用於存儲配置數據的類,或者Helper類),使用@Bean等其他方式添加到容器中更合適(@Bean會在後續的文章中介紹)。
使用Stereotype特性來標記分層,還有一個好處是即使工程的結構再複雜多樣,都可以很輕鬆的使用註解(Annotation)來實現攔截器或者AOP功能。因為我們能夠很清晰的知道每個分層的作用,開發AOP的功能就非常便利。
掃描配置
本文開篇使用了一個簡單的例子說明使用<context:component-scan>掃描功能來自動添加被註解標記的Bean。除了使用base-package屬性還有其他的標籤來控制掃描的路徑。
<context:include-filter>和<context:exclude-filter>標籤用來指定包含和排除的過濾規則。他們提供2個參數——type和expression,用來指定過濾類型和過濾參數,例如:
- <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>
此外還可以使用use-default-filters屬性來指定是否掃描默認註解(@Component、@Repository、@Service、@Controller、@Configuration),默認值為ture。如果設定成false,需要我們在include-filter中增加對應的annotation。
除了使用XML配置,還可以使用@ComponentScan註解來指定掃描的路徑,他提供和XML配置一樣的功能。在後續的文章會介紹純Java配置的功能。
關於掃描的詳細說明見官網的過濾規則說明。
組件命名
和普通的Bean一樣,我們也可以在@Component上添加註解來指定Bean在IoC容器的名稱:
- package chkui.springcore.example.hybrid.component.bean;@Service("implementNameService")public class NameServiceImpl implements NameService{ @Override
- public String getName() { return "This is My Component";
- }
- }
這樣在容器中這個Bean的名稱被命名為"implementNameService"。除了直接在註解上添加內容,我們還可以實現 BeanNameGenerator 介面來實現全局的命名方法。看下面這個例子。(源碼請到https://gitee.com/chkui-com/spring-core-sample自行clone,例子在chkui.springcore.example.hybrid.component包中。)
首先在XML中使用 "name-generator" 指定名稱的生成器:
- <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="chkui.springcore.example.hybrid.component.bean"
- name-generator="chkui.springcore.example.hybrid.component.bean.NameGenerator" /></beans>
然後編寫我們的命名生成規則:
- package chkui.springcore.example.hybrid.component.bean;public class NameGenerator implements BeanNameGenerator { @Override
- public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
- AnnotatedBeanDefinition annotdef = AnnotatedBeanDefinition.class.cast(definition);
- AnnotationMetadata meta = annotdef.getMetadata(); //生成規則:如果已經命名不做任何調整,如果未命名則在類名車後面增加」_NoDefinedName「字元串
- return Optional.of(meta).map(met -> met.getAnnotationTypes()).map(set -> set.toArray(new String[] {}))
- .map(array -> array[0]).map(name -> meta.getAnnotationAttributes(name)).map(entry -> entry.get("value"))
- .map(obj -> "".equals(obj) ? null : obj).orElse(definition.getBeanClassName() + "_NoDefinedName")
- .toString();
- }
- }
使用索引提升啟動速度
通常情況下,即使是對整個classpath進行掃描並不會佔用太多的時間,但是某些應用對啟動時間有極高的要求,對此Spring提供了索引功能。索引功能並不複雜,就是第一次掃描之後生成一個靜態文件記錄所有的組件,然後下一次掃描就直接讀取文件中的內容,而不去執行掃描過程。
首先引入spring-context-indexer包:
- <dependencies>
- <dependency>
- <groupId>org.springframework</groupId>
- <artifactId>spring-context-indexer</artifactId>
- <version>5.0.7.RELEASE</version>
- <optional>true</optional>
- </dependency></dependencies>
- dependencies {
- compileOnly("org.springframework:spring-context-indexer:5.0.7.RELEASE")
- }
然後在運行後會生成一個 META-INF/spring.components 的文件,之後只要運行工程發現這個文件都會直接使用他。可以通過環境變數或工程根目錄的spring.properties中設置spring.index.ignore=ture來禁用這個功能。
這個功能如果沒有什麼明確的需求,慎重使用,會提高工程的管理成本。


※Mybatis中動態sql小結
※用python實現小豬佩奇
TAG:程序員小新人學習 |