Spring组件注册(转载)

警告
本文最后更新于 2023-03-21,文中内容可能已过时,请谨慎使用。

User类:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;
}

resources目录下创建一个user.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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="user" class="cc.bnblogs.springbasis.demo.pojo.User">
            <!--构造方法注入-->
            <constructor-arg name="name" value="tom"/>
            <constructor-arg name="age" value="18"/>
        </bean>
    
    </beans>
    
  • setter方法注入

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean id="user" class="cc.bnblogs.springbasis.demo.pojo.User">
        	 <!--setter方法注入-->
            <property name="name" value="admin"/>
            <property name="age" value="19"/>
        </bean>
    
    </beans>
    
  • p名称空间注入

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
           xmlns:p="http://www.springframework.org/schema/p">
    
        <bean id="user" class="cc.bnblogs.springbasis.demo.pojo.User"
              p:name="alice" p:age="25">
        </bean>
    
    </beans>
    

SpringBoot中,ClassPathXmlApplicationContext是通过读取resources目录下的的user.xml,而FileSystemXmlApplicationContext是读取该项目的相对路径中的user.xml

// 方式1
ClassPathXmlApplicationContext classPathXmlApplicationContext = new ClassPathXmlApplicationContext("user.xml");
User user = (User) classPathXmlApplicationContext.getBean("user");
System.out.println(user.getName());
System.out.println(user.getAge());

// 方式2
FileSystemXmlApplicationContext fileSystemXmlApplicationContext = new FileSystemXmlApplicationContext("src/main/resources/user.xml");
User user = (User) fileSystemXmlApplicationContext.getBean("user");
System.out.println(user.getName());
System.out.println(user.getAge());

// 方式3
ApplicationContext context = new FileSystemXmlApplicationContext("src/main/resources/user.xml");
User user = context.getBean("user", User.class);
System.out.println(user.getName());
System.out.println(user.getAge());

User类:

@AllArgsConstructor
@Data
public class User {
    private String name;
    private Integer age;
}

注册user组件

通过@Bean注解,我们向 IOC 容器注册了一个名称为user(Bean 名称默认为方法名,我们也可以通过@Bean("myUser")方式来将组件名称指定为myUser)

@Configuration
public class WebConfig {
    @Bean
    public User user() {
        return new User("tom", 18);
    }
}

组件注册完成后,我们写一个测试方法从 IOC 容器中获取这个组件

@Test
void test() {
    // 使用AnnotationConfigApplicationContext来获取相应的IOC容器,入参为配置类
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    User user = context.getBean(User.class);
    System.out.println(user);
    // 查看容器中已经存在的组件
    String[] beanNames = context.getBeanNamesForType(User.class);
    Arrays.stream(beanNames).forEach(System.out::println);
}

ClassPathXmlApplicationContextFileSystemXmlApplicationContext都是继承于抽象类AbstractRefreshableConfigApplicationContext,所以他们来是支持刷新的,而AnnotationConfigApplicationContext是不支持的。

支持刷新的意思是ApplicationContext会将容器里面的bean全部销毁,然后重新生成bean

@Component 注解及其衍生注解@RestController、@Controller、@Configration、@Service 和@Repository 都是组件注册注解。

@ComponentScan 注解主要是从约定的扫描路径中,识别标注了组件注册注解的类,并且把这些类自动注册到 spring IoC 容器中,这些类就是我们通常所言的bean。IoC 容器是 Spring 的特色之一,可以使用它管理 bean。

@ComponentScan(value = {"cc.bnblogs.springbasis.demo"},
               excludeFilters = {
                   @ComponentScan.Filter(type = FilterType.ANNOTATION,
                                         classes = {Controller.class, Repository.class}),
                   @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = User.class)
               })

上面我们指定了两种排除扫描的规则:

  1. 根据注解来排除(type = FilterType.ANNOTATION),这些注解的类型为classes = {Controller.class, Repository.class}。即ControllerRepository注解标注的类不再被纳入到 IOC 容器中。
  2. 根据指定类型类排除(type = FilterType.ASSIGNABLE_TYPE),排除类型为User.class,其子类,实现类都会被排除。

includeFilters的作用和excludeFilters相反,其指定的是哪些组件需要被扫描:

@ComponentScan(value = "cc.bnblogs.springbasis.demo",
               includeFilters = {
                   @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class)
               }, useDefaultFilters = false)

上面配置了只将Service纳入 IOC 容器,并且需要用useDefaultFilters = false来关闭 Spring 默认的扫描策略才能让我们的配置生效

java8新增了@Repeatable注解,使用该注解修饰的注解可以重复使用。ComponentScan也是一个可重复使用的注解,源码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
	...
}

有下面 4 种参数类型

  1. singleton:单实例(默认),在 Spring IOC 容器启动的时候会调用方法创建对象然后纳入到 IOC 容器中,以后每次获取都是直接从 IOC 容器中获取(map.get());
  2. prototype:多实例,IOC 容器启动的时候并不会去创建对象,而是在每次获取的时候才会去调用方法创建对象;
  3. request:一个请求对应一个实例;
  4. session:同一个 session 对应一个实例。

这里介绍一下前两种类型

默认就是单例模式,在 IOC 容器中只会有一个实例存在,每次getBean()获取的实例对象都是同一个

@Configuration
public class WebConfig {
    @Bean
    public User user() {
        return new User("tom",18);
    }
}
@Test
void test() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class); // 启动IOC容器
    User user1 = context.getBean(User.class);
    User user2 = context.getBean(User.class);
    System.out.println(user1 == user2); // 输出true
}

多实例模式,每次获取 Bean 的时候会有一个新的实例

@Configuration
public class WebConfig {
    @Bean
    @Scope(value = "prototype")
    public User user() {
        return new User("tom",18);
    }
}
@Test
void test() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    User user1 = context.getBean(User.class);
    User user2 = context.getBean(User.class);
    System.out.println(user1 == user2); // 输出true
}
  • 使用 singleton 单例,采用饿汉加载(容器启动,Bean 实例就创建好了)
  • 使用 prototype 多例,采用懒汉加载(IOC 容器启动的时候,并不会创建对象实例,而是在第一次使用的时候才会创建)

单例模式下使用懒汉加载只需要加上@Lazy注解

@Configuration
public class WebConfig {
    @Bean
    @Scope
    @Lazy
    public User user() {
        return new User("tom",18);
    }
}

这样 IOC 容器启动的时候,就不会创建对象实例

可以修改 User 类来测试一下

@Data
public class User {

    private String name;
    private Integer age;

    public User(String name,Integer age) {
        this.name = name;
        this.age = age;
        System.out.println("User初始化成功");
    }
}

没有加@Lazy之前

@Test
void test() {
	# IOC容器启动时直接创建一个User实例
	# 控制台输出: User初始化成功
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
}

加上@Lazy之后,单例模式采用懒汉加载

@Test
void test() {
	# IOC容器启动时不会创建实例
	# 控制台无输出
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
}

综上, 懒加载的功能是,在单例模式中,IOC 容器创建的时候不会马上去调用方法创建对象并注册,只有当组件第一次被使用的时候才会调用方法创建对象并加入到容器中。

使用@Conditional注解我们可以指定组件注册的条件,即满足特定条件才将组件纳入到 IOC 容器中。

在使用该注解之前,我们需要创建一个类,实现Condition接口:

public class MyCondition implements Condition {
    /**
     * @param context 上下文信息
     * @param metadata 注解信息
     * @return
     */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String osName = context.getEnvironment().getProperty("os.name");
        return osName != null && osName.contains("Linux");
    }
}

接着将这个条件添加到 User Bean 注册的地方

public class WebConfig {
    @Bean(name = "myUser")
    @Conditional(MyCondition.class)
    public User user() {
        return new User("tom",18);
    }
}

测试一下

@Test
void test4() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
    User user = context.getBean(User.class); // 报错
}
报错
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘cc.bnblogs.springbasis.demo.pojo.User’ available

在 Linux 环境下,User 这个组件将被成功注册,由于我用的是 windows,所以这个组件将不会被注册到 IOC 容器中。

@profile注解的作用是指定类或方法在特定的 Profile 环境生效,任何直接或者间接使用了@Component注解的类都可以使用@Profile注解。

新建一个接口CalculateService

public interface CalculateService {
    Integer sum(Integer... value);
}

接着添加两个实现类

Java7CalculateServiceImpl

@Service
@Profile("java7")
public class Java7CalculateServiceImpl implements CalculateService {
    @Override
    public Integer sum(Integer... value) {
        System.out.println("Java 7环境下执行");
        int result = 0;
        for (int i = 0; i <= value.length; i++) {
            result += i;
        }
        return result;
    }
}

Java8CalculateServiceImpl

@Service
@Profile("java8")
public class Java8CalculateServiceImpl implements CalculateService {
    @Override
    public Integer sum(Integer... value) {
        System.out.println("Java 8环境下执行");
        return Arrays.stream(value).reduce(0, Integer::sum);
    }
}

通过@Profile注解我们实现了:当环境变量包含java7的时候,Java7CalculateServiceImpl将会被注册到 IOC 容器中;当环境变量包含java8的时候,Java8CalculateServiceImpl将会被注册到 IOC 容器中。

修改启动方法测试一下:

public class SpringBasisApplication {
    public static void main(String[] args) {
        // SpringApplication.run(SpringBasisApplication.class, args);
        ConfigurableApplicationContext context1 = new SpringApplicationBuilder(SpringBasisApplication.class)
            .web(WebApplicationType.NONE)
            .profiles("java8")
            .run(args);

        CalculateService service = context1.getBean(CalculateService.class);
        System.out.println("求合结果: " + service.sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10));

    }
}

创建一个Test

public class Test {
    public Test() {
        System.out.println("Test类初始化成功");
    }
}

然后在配置类中导入这个组件:

@Configuration
@Import({Test.class})
public class WebConfig {
	...
}

查看容器中是否已经有上面这个组件

ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
String[] beanNames = context.getBeanDefinitionNames();
Arrays.stream(beanNames).forEach(System.out::println);

可以发现,通过@Import我们可以快速地往 IOC 容器中添加组件,Id 默认为全路径类名

通过@Import我们已经实现了组件的导入,如果需要一次性导入较多组件,我们可以使用ImportSelector来实现。

ImportSelector是一个接口,包含一个selectImports方法,方法返回类的全类名数组(即需要导入到 IOC 容器中组件的全类名数组),包含一个AnnotationMetadata类型入参,通过这个参数我们可以获取到使用ImportSelector的类的全部注解信息。

查看ImportSelector源码:

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata importingClassMetadata);

    @Nullable
    default Predicate<String> getExclusionFilter() {
        return null;
    }
}

ImportSelector是一个接口,包含一个selectImports方法,方法返回类的全类名数组(即需要导入到 IOC 容器中组件的全类名数组),包含一个AnnotationMetadata类型入参,通过这个参数我们可以获取到使用ImportSelector的类的全部注解信息。

现在我们想要导入AppleBanana两个组件

我们可以新建一个ImportSelector实现类MyImportSelector

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{
                "cc.bnblogs.springbasis.demo.test.Apple",
                "cc.bnblogs.springbasis.demo.test.Banana",
        };
    }
}

上面方法返回了新增的两个类的全类名数组,接着我们在配置类的@Import注解上使用MyImportSelector来把这三个组件快速地导入到 IOC 容器中:

@Import({MyImportSelector.class})
public class WebConfig {
    ...
}

查看容器中是否已经有上面这两个组件:

ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
String[] beanNames = context.getBeanDefinitionNames();
Arrays.stream(beanNames).forEach(System.out::println);

Spring还提供了一个FactoryBean接口,我们可以通过实现该接口来注册组件,该接口包含了两个抽象方法和一个默认方法:

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}

我们新增一个Cherry类,为了注册该类,创建FactoryBean的实现类CherryFactoryBean

public class CherryFactoryBean implements FactoryBean<Cherry> {
    /**
     * 返回注册的组件对象
     */
    @Override
    public Cherry getObject() throws Exception {
        return new Cherry();
    }

    /**
     *返回需要注册的组件类型
     */
    @Override
    public Class<?> getObjectType() {
        return Cherry.class;
    }

    /**
     *该组件使用单例模式
     */
    @Override
    public boolean isSingleton() {
        return true;
    }
}

定义好CherryFactoryBean后,我们在配置类中注册这个类:

@Bean
public CherryFactoryBean cherryFactoryBean() {
    return new CherryFactoryBean();
}

测试从容器中获取:

ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
Object object = context.getBean("cherryFactoryBean");
System.out.println(object.getClass());

输出结果:

class cc.bnblogs.springbasis.demo.test.Cherry

虽然我们获取的是Id为cherryFactoryBean的组件,但其获取到的实际是getObject方法里返回的对象

如果我们要获取cherryFactoryBean本身,则可以这样做,在id前加上&

ApplicationContext context = new AnnotationConfigApplicationContext(WebConfig.class);
Object object = context.getBean("&cherryFactoryBean");
System.out.println(object.getClass());

输出结果:

class cc.bnblogs.springbasis.demo.config.CherryFactoryBean

为什么加上&就可以获取到呢,查看BeanFactory源码

public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";
    ...
}

在BeanFactory接口中定义了一个&前缀,只要我们使用bean的id来从Spring容器中获取bean时,Spring就会知道我们是在获取FactoryBean本身。