1. 程式人生 > 其它 >Spring註解開發01--------元件註冊

Spring註解開發01--------元件註冊

注入元件的幾種方式

在Spring中,有如下四種方式像容器中注入元件:

下面我們來詳細聊聊以上四種方式。

前期準備

1.引入Spring相關依賴

<dependencies>
    <!-- Spring5.0之後,Spring相關依賴都包含在spring-webmv這個以來之中-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.5</version>
    </dependency>

    <!-- junit測試 -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13.2</version>
        <scope>test</scope>
    </dependency>
</dependencies>
  • 我們使用的Spring版本為5.3.5,Spring5.0之後,Spring的一些核心依賴都包含在Spring-mvc當中,所以在匯入依賴的時候,我們只需要匯入spring-webmvc這一個即可!
  • junit我們用來作為單元測試


檢視專案依賴,發現我們需要的jar包已經全部匯入!

2.專案結構設計

3.編寫Spring配置類,並使用@Configuration將配置檔案注入到ioc容器中

package com.xdw.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MainConfigOfComponent {
}

4.編寫測試類,檢視配置檔案是否註冊成功

import com.xdw.config.MainConfigOfComponent;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TestBeanComponent {

    @Test
    public void test01() {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfComponent.class);
        printAllBeanNames(applicationContext);
    }


    public void printAllBeanNames(ApplicationContext applicationContext) {
        String[] names = applicationContext.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }

}
  • 這裡因為我們使用的是配置類的方法,所以獲取ioc容器使用的是AnnotationConfigApplicationContext,而不是之前xml檔案配置時使用的ClassPathXmlApplicationContext
  • applicationContext.getBeanDefinitionNames();方法用來獲取所有在容器中的bean的id
  • @Configuration本質也是一個@Component註解,生成的bean的id預設時類名(首字母小寫)

測試,發現我們編寫的配置類id已經成功註冊到ioc容器中

方式一: 包掃描 + 元件標註註解

測試

1.新建測試類(Controller, Service, Dao),分別使用註解@Controller,@Service, @Repository修飾
BookController:

package com.xdw.controller;

import org.springframework.stereotype.Controller;

@Controller
public class BookController {
}

BookService:

package com.xdw.service;

import org.springframework.stereotype.Service;

@Service
public class BookService {
}

BookDao:

package com.xdw.dao;

import org.springframework.stereotype.Repository;

@Repository
public class BookDao {
}

2.在配置類上使用註解 @ComponentScan,指定需要自動掃描的包

@ComponentScan(value={"com.xdw"})
@Configuration
public class MainConfigOfComponent {

}

3.編寫測試方法,執行,檢視結果!

public class TestBeanComponent {

    @Test
    public void test01() {

        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfComponent.class);
        printAllBeanNames(applicationContext);
    }

    public void printAllBeanNames(ApplicationContext applicationContext) {
        String[] names = applicationContext.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}

執行測試方法,結果如下:

可以發現,我們剛剛新建的幾個類已經成功注入到ioc容器中了!

@ComponentScan 註解屬性說明

在上面的測試中,我們通過使用@ComponentScan自動包掃描註解 + 元件標註註解(@Controller,@Service, @Repository, @Component)完成了我們的元件註冊!

@ComponentScan 註解提供了很多屬性,供我們靈活的註冊元件至容器中。

  1. value屬性: 用來指定要掃描的包。上面的例子中我們設定為"com.xdw",表示掃描com.xdw包下的所有類及子包下的所有類

  2. excludeFilters屬性:掃描的時候按照指定規則排除某些元件

  3. includeFilters = Filter[]; 指定掃描的時候只需要包含哪些元件,使用的時候一定要useDefaultFilters設為false

  4. 我們也可以使用@ComponentScans註解來配置多個@ComponentScan

  5. excludeFilters與includeFilters屬性都需要配合@Filter註解使用,@Filter為我們提供了以下幾種方式進行元件的過濾或者選擇:
    FilterType.ANNOTATION: 按照註解(常用)
    FilterType.ASSIGNABLE_TYPE:表示按照指定型別(也比較常用)
    FilterType.ASPECTJ: 使用ASPECTJ表示式,不常用
    FilterType.REGEX: 使用正則表示式
    FilterType.CUSTOM: 自定義規則

幾種過率選擇方式例項

因為excludeFilters與includeFilters屬性使用起來基本一樣,我們這裡就是用excludeFilters屬性進行測試。
我們選擇比較常用的以下三種進行測試:

1.FilterType.ANNOTATION: 按照註解

在配置類MainConfigOfComponent新增如下註解:

@ComponentScan(value={"com.xdw"},
        includeFilters = {@Filter(type= FilterType.ANNOTATION, value= Controller.class)},
        useDefaultFilters = false)
@Configuration
public class MainConfigOfComponent {
}

注意:使用includeFilters 屬性時,一定要設定useDefaultFilters = false,否則includeFilters 不會生效!

測試執行:

發現只有bookController注入容器成功!

2.FilterType.ASSIGNABLE_TYPE 按照指定型別
修改配置類如下:

@ComponentScan(value={"com.xdw"},
        includeFilters = {@Filter(type= FilterType.ASSIGNABLE_TYPE, value= BookService.class)},
        useDefaultFilters = false)
@Configuration
public class MainConfigOfComponent {

}

測試執行:

發現當前只有bookService成功註冊到容器中

3.FilterType.CUSTOM自定義型別

點開FilterType列舉類,我們發現

/** Filter candidates using a given custom
* {@link org.springframework.core.type.filter.TypeFilter} implementation.
*/
CUSTOM

要使用自定義型別,我們需要編寫一個類來實現TypeFilter介面。

編寫MyTypeFilter類,實現TypeFilter介面:

package com.xdw.filter;

import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;

public class MyTypeFilter implements TypeFilter {

    /**
     *
     * @param metadataReader    當前掃描到的類資訊
     * @param metadataReaderFactory 可以讀取到其他任何類的資訊
     *
     */
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {

        // 獲取當前類的所有註解資訊
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();

        // 獲取當前掃描類的資訊
        ClassMetadata classMetadata = metadataReader.getClassMetadata();

        // 獲取當前類路徑資訊
        Resource resource = metadataReader.getResource();

        // 這裡我們使用名稱,如果包含Dao就注入到ioc容器中
        if(classMetadata.getClassName().contains("Dao")) {
            return true;
        }
        return false;
    }
}

修改配置類,設定includeFilters,Filter的type設定為CUSTOM,value設定為我們剛剛編寫的MyTypeFilter.class

@ComponentScan(value={"com.xdw"},
        includeFilters = {@Filter(type= FilterType.CUSTOM, value= MyTypeFilter.class)},
        useDefaultFilters = false)
@Configuration
public class MainConfigOfComponent {
}

測試:

我們發現只有bookDao成功註冊到容器中!

總結

1. @ComponentScan 要與 @Controller,@Service, @Repository, @Component等註解配合使用
2. @ComponentScan的includeFilters與excludeFilters屬性使用時,需要結合Filter[]使用
3. 使用@ComponentScan的includeFilters屬性時,一定要同時設定useDefaultFilters = false否則不會生效

方式二:@Bean注入元件

測試

1.建立Person類

package com.xdw.pojo;


public class Person {

    private Integer age;

    private String name;

    public Person() {
    }

    public Person(Integer age, String name) {
        this.age = age;
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }


    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

2.編寫測試類

package com.xdw.config;


import com.xdw.pojo.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;


@ComponentScan(value={"com.xdw"})
@Configuration
public class MainConfigOfComponent {


    @Bean
    public Person person() {
        return new Person(10, "法外狂徒:張三");
    }

}

3.編寫測試類,執行測試

import com.xdw.config.MainConfigOfComponent;
import com.xdw.pojo.Person;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class TestBeanComponent {

    @Test
    public void test01() {

        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfComponent.class);
        printAllBeanNames(applicationContext);

        Person person = (Person) applicationContext.getBean("person");
        System.out.println(person);
    }


    public void printAllBeanNames(ApplicationContext applicationContext) {
        String[] names = applicationContext.getBeanDefinitionNames();
        for (String name : names) {
            System.out.println(name);
        }
    }
}

測試,結果如下:

發現我們剛建立的類已經成功注入到ioc容器中了。

@Bean註解說明

1.型別就是返回值,id預設就是方法名
2.修改id有兩種方式: a.修改方法名 b.為@Bean設定name或者value屬性

@Bean(value="person01")
public Person person() {
    return new Person(10, "法外狂徒:張三");
}

Bean的作用域@Scope

bean例項有如下四種作用域:

  • singleton: 單例項的,ioc啟動時就會建立物件放入容器中,以後每次獲取就直接從容器中獲取(map.get()),常用
  • prototype: 多例項的,ioc容器啟動時不會建立物件注入ioc容器中,以後每次獲取都會呼叫方法建立
  • request: 同一次請求建立一個例項
  • session: 同一個Session建立一個例項

在不指定作用域的情況下,預設式單例模式。

單例模式測試

配置類方法:

// 不指定作用域預設是單例模式
@Bean
public Person person() {
    System.out.println("person物件開始建立");
    return new Person(10, "法外狂徒:張三");
}

編寫測試方法:

@Test
public void test02() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfComponent.class);
    System.out.println("容器初始化完畢");
}

執行測試,結果如下:

我們發現,單例模式下,預設容器初始化的時候,物件就已經被載入進ioc容器中了。

修改測試方法如下:

@Test
public void test02() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfComponent.class);
    System.out.println("容器初始化完畢");
    Person person01 = (Person)applicationContext.getBean("person");
    Person person02 = (Person) applicationContext.getBean("person");
    System.out.println(person01 == person02);
}

測試,結果如下:

我們發現我們從容器中取了兩次person取出的兩個物件為同一個!

我們在建立bean的方法上新增@Lazy註解,程式碼如下:

@Lazy   // 表示我們在第一次獲取bean時,容器才會注入bean至容器中
@Bean
public Person person() {
    System.out.println("person物件開始建立");
    return new Person(10, "法外狂徒:張三");
}

執行,測試結果如下:

容器啟動的時候,沒有建立物件放入容器中,在我們第一次呼叫時,物件才被建立並放入容器中。

總結:
1. @Bean注入到容器中的bean,不指定作用域,預設是單例模式
2. 單例模式預設是容器啟動時就建立物件放入容器中,我們可以使用@Lazy懶載入註解,讓我們在第一次使用時,才建立物件並初始化。
3. 單例模式下,容器中只存在一個bean,無論取出多少次,取出的物件都為同一個。

多例項模式測試

為建立bean的方法新增如下註解:

@Scope("prototype")
@Bean
public Person person() {
    System.out.println("person物件開始建立");
    return new Person(10, "法外狂徒:張三");
}

測試代如下:

@Test
public void test02() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfComponent.class);
    System.out.println("容器初始化完畢");
    Person person01 = (Person)applicationContext.getBean("person");
    Person person02 = (Person) applicationContext.getBean("person");
    System.out.println(person01 == person02);
}

執行, 測試結果如下:

總結:
1. 多例項模式,容器在初始化的時候,不會建立例項
2. 每次去取bean的時候,容器都會新建一個bean

request與session兩種模式我們基本用不上,在這裡不做過多的說明。

@Conditional註解

我們可以使用 @Conditional註解,按照一定條件進行判斷,滿足條件給容器注入bean。

我們檢視@Conditional 原始碼

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

	/**
	 * All {@link Condition} classes that must {@linkplain Condition#matches match}
	 * in order for the component to be registered.
	 */
	Class<? extends Condition>[] value();

}

發現該註解存在value屬性,該屬性值必須必須繼承Condition介面。

測試

要求:我們新建一類SystemData,在配置類中編寫兩個建立bean的方法,id分別為windows,linux;根據系統型別,如果時windows系統,就建立id分別為windows的例項,如果是linux,就建立id為linux的例項。

1.編寫兩個自定義的condition類,實現condition介面:

package com.xdw.condition;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class WindowsCondition implements Condition {

    /**
     *
     *
     * @param context  判斷條件能使用的上下文(環境)
     * @param metadata 註釋
     *
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 1.獲取ioc使用的beanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 2. 獲取類載入器
        ClassLoader classLoader = context.getClassLoader();
        // 3.獲取當前環境資訊
        Environment environment = context.getEnvironment();
        // 4.獲取到bean定義的註冊類,可以判斷容器中bean的註冊情況,也可以給容器中註冊bean
        BeanDefinitionRegistry registry = context.getRegistry();

//        BeanDefinition definition = registry.getBeanDefinition("person");

        String property = environment.getProperty("os.name");
        if(property.contains("Windows")) {
            return true;
        }
        return false;
    }
}
package com.xdw.condition;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class LinuxCondition implements Condition {

    /**
     *
     *
     * @param context  判斷條件上下文(環境)
     * @param metadata  元資料
     * @return {@code true} if the condition matches and the component can be registered,
     * or {@code false} to veto the annotated component's registration
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

        // 1.獲取ioc使用的beanFactory
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 2. 獲取類載入器
        ClassLoader classLoader = context.getClassLoader();
        // 3.獲取當前環境資訊
        Environment environment = context.getEnvironment();
        // 4.獲取到bean定義的註冊類,可以判斷容器中bean的註冊情況,也可以給容器中註冊bean
        BeanDefinitionRegistry registry = context.getRegistry();

        String property = environment.getProperty("os.name");
        if(property.contains("Linux")) {
            return true;
        }


        return false;
    }
}

2.在配置類中新增兩個註冊bean的方法

@Conditional(value={WindowsCondition.class})
@Bean("windows")
public SystemData systemData01() {
    System.out.println("windows已經建立");
    return new SystemData("windows系統!");
}


@Conditional(value={LinuxCondition.class})
@Bean("linux")
public SystemData systemData02() {
    System.out.println("linux已經建立");
    return new SystemData("linux系統!");
}

執行測試, 結果如下:

我們看出因為我們當前系統是windows,所以id為windows的bean被建立並注入到容器中了!

總結

1. @Bean註解,預設物件名稱是方法名,物件型別為方法的返回型別;物件名修改可以通過@Bean註解的name屬性或者直接修改方法名實現。
2. @Bean註解預設的作用域是單例模式,可以通過@Scope註解來修改
3. 單例模式預設是容器啟動時建立並初始化,我們可以使用@Lazy註解實現懶載入,第一次使用時才建立並初始化bean。
4. 我們可以使用@Conditional註解,來實現滿足一定條件才載入並初始化bean。這個註解既可以放在方法上,也可以放在類上.

方式三:使用@Import註解

我們可以使用@Import快速給容器中匯入一個元件。

直接使用@Import

新建一個Color類:

package com.xdw.pojo;

public class Color {
}

在配置類上新增如下註解:

@Import(value={Color.class})
@Configuration
public class MainConfigOfComponent {}

編寫測試方法,打印出所有註冊到容器中的bean,執行測試結果如下:

我們發現剛剛建立的類已經成功注入到容器中。

總結: @Import(要匯入到容器中的元件),容器中會自動註冊這些元件,id預設是全類名

ImportSlector

這種方法需要實現ImportSlector介面,介面路中需要重寫selectImports方法,該方法會返回一個需要例項化的陣列。

新增兩個類:

package com.xdw.pojo;

public class Red {
}
package com.xdw.pojo;

public class Blue {
}

編寫MyImportSelector類,實現ImportSlector介面如下:

package com.xdw.selector;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.xdw.pojo.Red", "com.xdw.pojo.Blue"};
    }
}

這個類重寫了selectImports方法,將我們剛剛新建的類的全類名返回。

修改配置類上的@Import註解,將我們建立MyImportSelector新增進@Import註解中:

@Import(value={Color.class, MyImportSelector.class})
@Configuration
public class MainConfigOfComponent {

測試,執行結果如下:

我們剛剛新建的類已經成功注入到容器中。

總結:該種方式建立的物件名預設也是全類名。

ImportBeanDefinitionRegistrar

我們還可以通過ImportBeanDefinitionRegistrar,手動註冊bean至容器中。

新建一個類RainBow:

package com.xdw.pojo;

public class RainBow {
}

編寫類MyImportBeanDefinitionRegistrar實現ImportBeanDefinitionRegistrar介面:

package com.xdw.selector;

import com.xdw.pojo.RainBow;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     *
     * AnnotationMetadata: 當前類的註解資訊
     * BeanDefinitionRegistry:BeanDefinition註冊類
     *               把所有需要新增進容器中得bean: 呼叫
     *                   BeanDefinitionRegistry.registerBeanDefinition()方法手動註冊進來
     * @param registry current bean definition registry
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        if(registry.containsBeanDefinition("com.xdw.pojo.Red") && registry.containsBeanDefinition("com.xdw.pojo.Blue")) {

            RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(RainBow.class);
            registry.registerBeanDefinition("rainBow", rootBeanDefinition);
        }
    }
}

這裡我們加了一個簡單的判斷,如果ioc容器中存在名稱為"com.xdw.pojo.Red"與"com.xdw.pojo.Blue"的物件時,就載入rainBow物件。

將我們的編寫的MyImportBeanDefinitionRegistrar類新增進@Import註解中:

@Import(value={Color.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class})
@Configuration
public class MainConfigOfComponent {}

測試:

我們新建的rainBow成功載入至ioc容器中。

總結

@Import有如下三種方式到bean至容器中:
1. @Import(要匯入到容器中的元件),容器中會自動註冊這些元件,id預設是全類名
2. ImportSlector:返回需要匯入的元件的全類名陣列(這種方式在Springboot中用得比較多)
3. ImportBeanDefinitionRegistrar:手動註冊bean到容器中

方式四: 使用Spring提供的FactoryBean

測試

我們還使用之前的Color類,註釋掉之前的@Import。

編寫ColorFactoryBean類,實現FactoryBean介面:

package com.xdw.factory;

import com.xdw.pojo.Color;
import org.springframework.beans.factory.FactoryBean;

public class ColorFactoryBean implements FactoryBean {

    /**
     * 是否是單例模式: true 單例模式   false多例項模式
     *
     * @return
     */
    @Override
    public boolean isSingleton() {
        return true;
    }


    /**
     * 相當於class
     * @return
     * @throws Exception
     */
    @Override
    public Object getObject() throws Exception {
        return new Color();
    }


    @Override
    public Class<?> getObjectType() {
        return Color.class;
    }
}

編寫配置類,使用@Bean註解註冊我們剛剛建立的ColorFactoryBean

@Configuration
public class MainConfigOfComponent {


    @Bean
    public ColorFactoryBean colorFactoryBean() {
        return new ColorFactoryBean();
    }
}

編寫測試類:

@Test
public void test04() {

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfComponent.class);
    // 實際上獲取到的是工廠bean呼叫getObject建立的物件
    Object factoryBean = applicationContext.getBean("colorFactoryBean");
    System.out.println(factoryBean.getClass());

    // 獲取工廠類本身
    Object bean = applicationContext.getBean("&colorFactoryBean");
    System.out.println(bean.getClass());

}

執行結果:

總結
使用Spring提供的FactoryBean
1. 預設獲取到的是工廠bean呼叫getObject建立的物件(使用名稱獲取)
2. 要獲取工廠bean本身,我們需要給前面加一個&