Spring系列:Java-based的Spring容器配置

 2023-01-27
原文作者:空虚碧海 原文地址:https://juejin.cn/post/7058886790916079646

本文内容

  1. 快速入门
  2. @Configuration 和@Bean`详解
  3. 其他扫描方式

快速入门

Spring 新的 Java 配置支持中的核心是 @Configuration 类和 @Bean

@Bean 注解用于表示一个方法实例化、配置和初始化一个由 Spring IoC 容器管理的新对象。对比Spring 的 XML 配置,@Bean 注解与 元素的作用相同。

@Configuration 注释一个类表明它的主要目的是作为 bean 定义的来源。@Configuration 类允许通过调用同一类中的其他 @Bean 方法来定义 bean 间的依赖关系。对比pring 的 XML 配置,@Bean 注解与 元素的作用相同。

之前的基于XML 配置文件通常是使用ClassPathXmlApplicationContext来配置容器,而Java-based方式使用AnnotationConfigApplicationContext。下面上案例。

定义一个配置类AppConfig
    @Configuration //注解是配置类
    public class AppConfig {
     
        @Bean // 该方法定义了一个bean
        public RepositoryBase repositoryBase() {
            return new RepositoryA();
        }
    }

上面的配置类相当于下面的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
            https://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <bean class="com.crab.spring.ioc.demo07.RepositoryA" id="repositoryBase"/>
    
    </beans>

运行容器并获取bean使用。

        @org.junit.Test
        public void test_java_config() {
            AnnotationConfigApplicationContext context =
                    new AnnotationConfigApplicationContext(AppConfig.class);
            RepositoryBase repositoryBase = context.getBean(RepositoryBase.class);
            System.out.println(repositoryBase);
        }
    com.crab.spring.ioc.demo07.RepositoryA@159f197

使用起来很简洁丝滑。

@Bean详解

对比Spring 的 XML 配置,@Bean 注解与 元素的作用相同。所以类似地,支持nameinit-methoddestroy-methodautowiring。可以在 @Configuration@Component类中使用 @Bean 注解来声明一个bean。

最常见用法声明一个bean并指定名称、描述

    @Configuration
    public class AppConfig {
    
        @Bean
        public RepositoryBase repositoryBase() {
            return new RepositoryA();
        }
    }
    // 指定了名称
    @Configuration
    public class AppConfig {
    	// 指定了名称
        @Bean("repositoryBase")
        public RepositoryBase repositoryBase() {
            return new RepositoryA();
        }
    }
    // 指定了名称和别名
    @Configuration
    public class AppConfig {
    	// 指定了名称和别名
        @Bean({"repositoryBase", "repositoryBase2", "repositoryBase4"})
        public RepositoryBase repositoryBase() {
            return new RepositoryA();
        }
    }
    
    // 通过    @Description指定bean的描述
    
    @Bean({"repositoryBase", "repositoryBase2", "repositoryBase4"})
    @Description("这是一个bean描述")
    public RepositoryBase repositoryBase() {
        return new RepositoryA();
    }

通过接口的default方法声明bean

    public interface BaseConfig {
        @Bean
        default RepositoryA repositoryA(){
            return new RepositoryA();
        } 
    }
    @Configuration
    public class BaseConfigImpl implements BaseConfig {
    
    }

注入依赖项

可以类构造方法的方式注入以来,也可以在 同一个 @Configuration 内通过方法名直接注入依赖。

    @Configuration
    public class AppConfig2 {
    
        @Bean
        public RepositoryA repositoryA() {
            return new RepositoryA();
        }
    
        // 1  类似构造函数方式注入依赖 RepositoryA
        @Bean
        public Service1 service1(RepositoryA repositoryA) {
            return new Service1(repositoryA);
        }
    
        // 2 同一个 @Configuration 可通过方法名直接注入依赖
        @Bean
        public Service1 service2() {
            return new Service1(repositoryA());
        }
    
    }

接收生命周期回调

支持bean初始化和bean销毁生命周期的回调接口。

    @Configuration
    public class AppConfig3 {
    
        // 指定初始化和销毁回调方法
        @Bean(initMethod = "init", destroyMethod = "destroy")
        public Service2 service2() {
            return new Service2();
        }
    }
    
    // 测试方法
        @org.junit.Test
        public void test_callback() {
            System.out.println("容器开始初始化");
            AnnotationConfigApplicationContext context =
                    new AnnotationConfigApplicationContext(AppConfig3.class);
            Service2 bean = context.getBean(Service2.class);
            System.out.println(bean);
            System.out.println("容器开始销毁");
            context.close();
        }
    
    // 测试结果 成功回调
    容器开始初始化
    Service2 初始化
    com.crab.spring.ioc.demo07.Service2@159f197
    容器开始销毁
    Service2 销毁

默认情况下,使用 Java 配置定义的具有公共的closeshutdown方法的 bean 会自动加入销毁回调。若不想被调用,可在**@Bean**配置空的销毁方法。

    @Configuration
    public class AppConfig4 {
        // 有默认的公共的 shutdown 指定为空就不会被调用
        @Bean(initMethod = "")
        public Service4 service4() {
            return new Service4();
        }
    }

指定bean的作用域

@Bean声明一个bean的作用域是单例的,可以通过@Scope指定作用域。

    @Configuration
    public class AppConfig4 {
        @Bean
        @Scope("prototype")
        public Service4 service4() {
            return new Service4();
        }
    }

@Configuration详解

@Configuration 是一个类级别的注解,表明一个对象是 bean 定义的来源。 @Configuration 类通过 @Bean 注释的方法声明 bean。对@Configuration 类上的@Bean 方法的调用也可用于定义bean 间的依赖关系。

同一个Configuration内注入 bean之间依赖

    @Configuration
    public class AppConfig2 {
    
        @Bean
        public RepositoryA repositoryA() {
            return new RepositoryA();
        }
    
        // 1  类似构造函数方式注入依赖 RepositoryA
        @Bean
        public Service1 service1(RepositoryA repositoryA) {
            return new Service1(repositoryA);
        }
    
        // 2 同一个 @Configuration 可通过方法名直接注入依赖
        @Bean
        public Service1 service2() {
            return new Service1(repositoryA());
        }
    }

@Configuration注入内部工作细节

分析下面的案例,在同一个配置类,repositoryA()调用了2次,是执行了2次返回了2个RepositoryA?还是只执行一次返回同一个RepositoryA?单从编程的角度来看,是2次,看一下测试结果。

    @Configuration
    public class AppConfig5 {
    
        // 疑问: 此方法被调用了2次,是执行了2次返回了2个RepositoryA?还是只执行一次返回同一个RepositoryA?
        @Bean
        public RepositoryA repositoryA() {
            System.out.println("new 一个 RepositoryA");
            return new RepositoryA();
        }
    
    
        @Bean
        public Service5 service5() {
            Service5 service5 = new Service5();
            service5.setRepositoryA(repositoryA());
            return service5;
        }
    
        @Bean
        public Service6 service6() {
            Service6 service6 = new Service6();
            service6.setRepositoryA(repositoryA());
            return service6;
        }
    }

测试结果

        @org.junit.Test
        public void test_configuration_more() {
            AnnotationConfigApplicationContext context =
                    new AnnotationConfigApplicationContext(AppConfig5.class);
            RepositoryA repositoryA = context.getBean(RepositoryA.class);
            Service5 service5 = context.getBean(Service5.class);
            Service6 service6 = context.getBean(Service6.class);
    
            System.out.println("RepositoryA是否是同一个对象:");
            System.out.println(repositoryA);
            System.out.println(service5.getRepositoryA());
            System.out.println(service6.getRepositoryA());
            System.out.println("AppConfig5对象:");
            System.out.println(context.getBean(AppConfig5.class));
        }
        
    // 结果
    new 一个 RepositoryA
    RepositoryA是否是同一个对象:
    com.crab.spring.ioc.demo07.RepositoryA@4b14c583
    com.crab.spring.ioc.demo07.RepositoryA@4b14c583
    com.crab.spring.ioc.demo07.RepositoryA@4b14c583
    AppConfig5对象:
    // AppConfig5是一个被CGLIB代理的类,有$$EnhancerBySpringCGLIB$$标识
    com.crab.spring.ioc.demo07.AppConfig5$$EnhancerBySpringCGLIB$$a996d6ad@65466a6a

结论是只调用了1次,返回的是同一个实例。原因: 在 Spring 中,实例化的 bean 默认具有单例范围。所有 @Configuration 类在启动时都使用 CGLIB 进行子类化。在子类中,子方法在调用父方法并创建新实例之前首先检查容器中是否有任何缓存(作用域)bean,此处第一次调用repositoryA()实例化的bean被缓存了,后面调用直接返回了。

CGLIB代理在后面AOP的章节专门展开,此处当了解。

@Configuration@Component中使用@Bean的区别

在上面的AppConfig5基础上,将@Configuration改成@Component,直观看下区别。

    @Component
    public class AppConfig5 {
    	// 省略
    }

输出结果

    new 一个 RepositoryA
    new 一个 RepositoryA
    new 一个 RepositoryA
    RepositoryA是否是同一个对象:
    com.crab.spring.ioc.demo07.RepositoryA@226a82c4
    com.crab.spring.ioc.demo07.RepositoryA@731f8236
    com.crab.spring.ioc.demo07.RepositoryA@255b53dc
    AppConfig5对象:
    com.crab.spring.ioc.demo07.AppConfig5@1dd92fe2

从结论分析:

  1. repositoryA()被调用了3次,RepositoryA被实例化了3次
  2. Service5Service6中的RepositoryA依赖对象不是同一个
  3. AppConfig5只是一个普通的对象实例,不会被代理。

所以,在@Configuration@Component中使用@Bean的区别是:

  1. @Component内不能通过@Bean标记的方法处理依赖注入,@Configuration中可以
  2. @Component标识的类不会被代理,@Configuration标识的类会被代理

扩展:@Component中使用@Bean如何处理依赖注入?

总结

本文介绍Java-based方式的容器配置,使用起来更简洁,同时重点介绍了@Bean 和 @Configuration的用法和细节。下一篇接着深入Java-based方式的配置注解。

本篇源码地址: github.com/kongxubihai… 知识分享,转载请注明出处。学无先后,达者为先!