Spring核心——Stereotype組件與Bean掃描

註解自動裝載中介紹了經過註解(Annotation)自動向Bean中注入其餘Bean的方法,本篇將介紹經過註解(Annotation)向容器添加Bean的方法。html

 Spring的核心容器提供了@Component和@Bean註解來標記如何向IoC容器添加Bean。在覈心包中@Component又派生了@Service、@Controller和@Repository這三個註解(在其餘的Spring工程或包中還有更多的派生),本文主要介紹@Component及其派生註解的使用。前端

一個簡單的使用例子

要想使用@Component等註解來向容器添加Bean,須要向IoC容器指明什麼類有這個註解,因此Spring提供了一個掃描機制讓使用者指定要檢查的路徑。配置很是簡單,只要使用上下文的component-scan標籤便可。咱們經過下面的例子來簡單說明如何配置。java

例子中的代碼僅用於說明問題,並不能運行。源碼請到https://gitee.com/chkui-com/spring-core-sample自行clone,例子在chkui.springcore.example.hybrid.component包中。git

有一個接口和一個實現類做爲要添加到IoC容器的Bean:web

package chkui.springcore.example.hybrid.component.bean;

public interface NameService {
	String getName();
}
package chkui.springcore.example.hybrid.component.bean;

@Component
public class NameServiceImpl implements NameService{

	@Override
	public String getName() {
		return "This is My Component";
	}
}

在實現類NameServiceImpl上使用了@Component註解。spring

而後XML(/spring-core-sample/src/main/resources/hybrid/component)配置爲:express

<?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指定了掃描的包路徑。json

而後咱們加載這個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容器中。api

 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{}

@Service
class UserServiceImpl implements UserService{
	
}

@Controller
class 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來禁用這個功能。

這個功能若是沒有什麼明確的需求,慎重使用,會提升工程的管理成本。

相關文章
相關標籤/搜索