• 12、IOC 之基于 Java 的容器配置


    12、IOC 之基于 Java 的容器配置

    本节介绍如何在 Java 代码中使用注释来配置 Spring 容器。它包括以下主题:

    12.1、基本概念:@Bean@Configuration

    Spring 新的Java配置支持中的核心构件是 @Configuration注释的类和 @Bean注释的方法。

    @Bean 注释用于指示方法实例化、配置和初始化一个由Spring IoC容器管理的新对象。对于那些熟悉Spring的 XML配置的人来说,@Bean 注释扮演着与 元素相同的角色。你可以在任何Spring @Component 中使用 @Bean 注释的方法。但是,它们最常与 @Configuration Bean一起使用。

    @Configuration 注释类表明它的主要用途是作为Bean定义的源。此外,@Configuration 类允许通过在同一个类中调用其他 @Bean 方法来定义Bean间的依赖关系。最简单的 @Configuration 类如下:

    @Configuration
    public class AppConfig {
        @Bean
        public MyService myService() {
            return new MyServiceImpl();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    前面的 AppConfig 类等效于下面的Spring XML:

    <beans>
        <bean id="myService" class="com.acme.services.MyServiceImpl"/>
    beans>
    
    • 1
    • 2
    • 3

    完整的 @Configuration VS “精简版” @Bean?

    @Bean方法在没有使用 @Configuration 注释的类中声明时,它们被称为以“精简”模式处理。在 @Component 或甚至在一个普通的旧类中声明的 Bean方法被认为是“精简”的,包含类的主要目的不同,而 @Bean方法是一种额外的好处。例如,服务组件可以通过每个适用组件类上的附加 @Bean方法向容器公开管理视图。在这种情况下,@Bean方法是一种通用的工厂方法机制。

    与完整的 @Configuration不同,精简版 @Bean方法不能声明 Bean间的依赖。相反,它们对包含它们的组件的内部状态进行操作,并可选地对它们可能声明的参数进行操作。因此,这样的 @Bean方法不应该调用其他的 @Bean方法。每个这样的方法实际上只是特定 Bean引用的工厂方法,没有任何特殊的运行时语义。这里的积极的副作用是没有 CGLIB子类必须在运行时应用,所以在类设计方面没有限制(也就是说,包含的类可能是 final 类等等)。

    在常见的场景中,@Bean方法将在 @Configuration类中声明,以确保始终使用 “full”模式,并因此将跨方法引用重定向到容器的生命周期管理。这可以防止通过常规 Java调用意外调用相同的 @Bean方法,这有助于减少在“精简”模式下操作时难以跟踪的微妙 bug。

    下面几小节将深入讨论 @Bean@Configuration注释。不过,我们首先介绍使用基于Java的配置创建Spring容器的各种方法。

    12.2、使用 AnnotationConfigApplicationContext 实例化Spring容器

    以下章节记录了Spring 3.0中引入的 AnnotationConfigApplicationContext。这个通用的 ApplicationContext 实现不仅可以接受 @Configuration 类作为输入,还可以接受普通的 @Component 类和带有JSR-330元数据注释的类。

    @Configuration 类作为输入提供时,@Configuration 类本身被注册为Bean定义,并且类内所有声明的 @Bean 方法也被注册为Bean定义。

    当提供 @Component 和JSR-330类时,它们被注册为Bean定义,并且假定在必要的时候在这些类中使用像@Autowired@Inject这样的DI元数据。

    结构简单

    在实例化一个 ClassPathXmlApplicationContext 时,使用Spring XML文件作为输入的方式非常类似,在实例化一个 AnnotationConfigApplicationContext 时,可以使用 @Configuration 类作为输入。这样就可以完全不用XML来使用Spring容器,如下所示:

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        MyService myService = ctx.getBean(MyService.class);
        myService.doStuff();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    正如前面提到的,AnnotationConfigApplicationContext 不仅仅限于使用 @Configuration 类。任何 @Component或JSR-330注释类都可以作为构造函数的输入,如下所示:

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(
            MyServiceImpl.class, Dependency1.class, Dependency2.class);
        MyService myService = ctx.getBean(MyService.class);
        myService.doStuff();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    前面的例子假设 MyServiceImplDependency1Dependency2 使用Spring依赖注入注释,比如 @Autowired

    使用 register(Class…) 编程方式构建容器

    可以使用无参数构造函数实例化一个 AnnotationConfigApplicationContext,然后使用 register() 方法配置它。当以编程方式构建 AnnotationConfigApplicationContext 时,这种方法特别有用。下面的例子展示了如何这样做:

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.register(AppConfig.class, OtherConfig.class);
        ctx.register(AdditionalConfig.class);
        ctx.refresh();
        MyService myService = ctx.getBean(MyService.class);
        myService.doStuff();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通过 scan(String…) 启用组件扫描

    要启用组件扫描,你可以这样注释你的 @Configuration 类:

    @Configuration
    @ComponentScan(basePackages = "com.acme")  // 此注释启用组件扫描
    public class AppConfig  {
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    有经验的 Spring用户可能熟悉来自 Spring context: 命名空间的等效 XML声明,如下所示:

    <beans>
        <context:component-scan base-package="com.acme"/>
    beans>
    
    • 1
    • 2
    • 3

    在上述示例中,com.acme 包被扫描以查找任何带有 @Component 注释的类,并且这些类被注册为容器内的Spring Bean定义。AnnotationConfigApplicationContext 公开了 scan(String…) 方法来允许相同的组件扫描功能,如下面的例子所示:

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.scan("com.acme");
        ctx.refresh();
        MyService myService = ctx.getBean(MyService.class);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    记住 @Configuration 类是用 @Component进行元注释的,所以它们是组件扫描的候选者。在前面的示例中,假设 AppConfig 是在com.acme包(或下面的任何包)中声明的,它是在调用 scan() 期间获取的。在 refresh() 时,它的所有 @Bean方法都被处理并注册为容器内的 Bean定义。

    支持带有 AnnotationConfigWebApplicationContext 的Web应用程序

    AnnotationConfigApplicationContextWebApplicationContext 变体与 AnnotationConfigWebApplicationContext 一起可用。可以在配置Spring ContextLoaderListener servlet监听器、Spring MVC DispatcherServlet等等时使用此实现。下面的web.xml片段配置了一个典型的Spring MVC web应用程序(注意使用了contextClass上下文参数和 init-param):

    <web-app>
        
        <context-param>
            <param-name>contextClassparam-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            param-value>
        context-param>
    
        
        <context-param>
            <param-name>contextConfigLocationparam-name>
            <param-value>com.acme.AppConfigparam-value>
        context-param>
    
        
        <listener>
            <listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
        listener>
    
        
        <servlet>
            <servlet-name>dispatcherservlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
            
            <init-param>
                <param-name>contextClassparam-name>
                <param-value>
    				org.springframework.web.context.support.AnnotationConfigWebApplicationContext
                param-value>
            init-param>
            
            <init-param>
                <param-name>contextConfigLocationparam-name>
                <param-value>com.acme.web.MvcConfigparam-value>
            init-param>
        servlet>
    
        
        <servlet-mapping>
            <servlet-name>dispatcherservlet-name>
            <url-pattern>/app/*url-pattern>
        servlet-mapping>
    web-app>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    对于编程用例,GenericWebApplicationContext 可以作为 AnnotationConfigWebApplicationContext 的替代。详细信息请参见GenericWebApplicationContext

    12.3、使用 @Bean 注释

    @Bean 是一个方法级注释,它直接模拟了XML 元素。注释支持提供的一些属性,例如:

    可以在 @Configuration 注释的类中使用 @Bean 注释,也可以在 @Component 注释的类中使用。

    声明一个 Bean

    要声明Bean,可以使用 @Bean 注释对方法进行注释。可以使用此方法在 ApplicationContext 中注册Bean定义,ApplicationContext 的类型指定为该方法的返回值。默认情况下,Bean名与方法名相同。下面的例子显示了一个 @Bean 方法声明:

    @Configuration
    public class AppConfig {
        @Bean
        public TransferServiceImpl transferService() {
            return new TransferServiceImpl();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面的配置完全等价于下面的Spring XML:

    <beans>
        <bean id="transferService" class="com.acme.TransferServiceImpl"/>
    beans>
    
    • 1
    • 2
    • 3

    这两个声明都使一个名为 transferService 的Bean在 ApplicationContext 中可用,它被绑定到一个 TransferServiceImpl 类型的对象实例,如下所示:

    transferService -> com.acme.TransferServiceImpl

    还可以使用默认方法来定义Bean。这允许通过在默认方法上使用bean定义实现接口来组合Bean配置。

    public interface BaseConfig {
        @Bean
        default TransferServiceImpl transferService() {
            return new TransferServiceImpl();
        }
    }
    
    @Configuration
    public class AppConfig implements BaseConfig {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    也可以用接口(或基类)返回类型声明 @Bean 方法,如下面的例子所示:

    @Configuration
    public class AppConfig {
        @Bean
        public TransferService transferService() {
            return new TransferServiceImpl();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    但是,这限制了对指定接口类型(TransferService)的提前类型预测的可见性。然后,只有当受影响的单例Bean被实例化时,容器才知道完整类型(TransferServiceImpl)。非惰性单例Bean根据它们的声明顺序进行实例化,因此你可能会看到不同的类型匹配结果,这取决于另一个组件试图通过非声明类型进行匹配的时间(例如@Autowired TransferServiceImpl,它只在transferService Bean被实例化之后解析)。

    如果你始终通过声明的服务接口引用你的类型,你的 @Bean返回类型可以安全地加入该设计决策。但是,对于实现了多个接口的组件或可能由其实现类型引用的组件,更安全的方法是声明尽可能特定的返回类型(至少与引用 Bean的注入点所需的特定类型相同)。

    Bean 的依赖关系

    @Bean 注释的方法可以有任意数量的参数,这些参数描述构建该Bean所需的依赖项。例如,如果我们的 TransferService 需要一个 AccountRepository,我们可以用一个方法参数实现该依赖,如下面的例子所示:

    @Configuration
    public class AppConfig {
        @Bean
        public TransferService transferService(AccountRepository accountRepository) {
            return new TransferServiceImpl(accountRepository);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    解析机制与基于构造函数的依赖注入非常相似。有关更多详细信息,请参阅相关部分

    接受生命周期回调

    任何用 @Bean 注释定义的类都支持常规的生命周期回调,并且可以使用JSR-250中的 @PostConstruct@PreDestroy 注释。有关更多细节,请参见 JSR-250 注释

    常规的Spring生命周期回调也完全受支持。如果一个 Bean 实现了 InitializingBeanDisposableBeanLifecycle,容器就会调用它们各自的方法。

    也完全支持*Aware接口的标准集(例如BeanFactoryAware, BeanNameAware, MessageSourceAware, ApplicationContextAware,等等)。

    @Bean 注释支持指定任意的初始化和销毁回调方法,很像bean元素上Spring XML的 init-methoddestroy-method 属性,如下所示:

    public class BeanOne {
    
        public void init() {
            // 初始化逻辑
        }
    }
    
    public class BeanTwo {
    
        public void cleanup() {
            // 摧毁逻辑
        }
    }
    
    @Configuration
    public class AppConfig {
    
        @Bean(initMethod = "init")
        public BeanOne beanOne() {
            return new BeanOne();
        }
    
        @Bean(destroyMethod = "cleanup")
        public BeanTwo beanTwo() {
            return new BeanTwo();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    默认情况下,使用 Java配置定义的具有公共 closeshutdown方法的 Bean会自动使用销毁回调调用。如果有一个公共的closeshutdown方法,并且不希望在容器关闭时调用它,那么可以在 Bean定义中添加 @Bean(destroyMethod="") 来禁用默认(inferred)模式。

    对于使用 JNDI 获得的资源,可能希望在默认情况下这样做,因为它的生命周期是在应用程序外部管理的。特别是,一定要为 DataSource执行此操作,因为众所周知,这在 J2EE应用服务器上是有问题的。

    下面的例子展示了如何防止一个 DataSource的自动销毁回调:

    @Bean(destroyMethod="")
    public DataSource dataSource() throws NamingException {
        return (DataSource) jndiTemplate.lookup("MyDS");
    }
    
    • 1
    • 2
    • 3
    • 4

    同样,对于 @Bean方法,通常使用编程式的 JNDI查找,通过使用 Spring的 JndiTemplateJndiLocatorDelegate 帮助程序,或者直接使用JNDI InitialContext,但不使用 JndiObjectFactoryBean变体(这将强制你声明返回类型为 FactoryBean类型而不是实际的目标类型,使得在其他 @Bean方法中使用意图引用此处提供的资源的交互引用调用变得更加困难)。

    对于上面例子中的 BeanOne,在构造过程中直接调用 init() 方法同样有效,如下面的例子所示:

    @Configuration
    public class AppConfig {
    
        @Bean
        public BeanOne beanOne() {
            BeanOne beanOne = new BeanOne();
            beanOne.init();
            return beanOne;
        }
    
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    当直接在 Java中工作时,你可以对对象执行任何你喜欢的操作,并且并不总是需要依赖容器生命周期。

    指定 Bean 的范围

    Spring包含@Scope注释,以便你可以指定 Bean 的范围。

    使用 @Scope 注释

    你可以指定用 @Bean 注释定义的Bean应该具有特定的作用域。你可以使用Bean作用域部分中指定的任何标准作用域。

    默认的作用域是 singleton,但是你可以用 @Scope 注释覆盖它,如下面的例子所示:

    @Configuration
    public class MyConfiguration {
    
        @Bean
        @Scope("prototype")
        public Encryptor encryptor() {
            // ...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    @Scopescoped-proxy

    Spring 提供了一种通过作用域代理处理作用域依赖项的方便方式。在使用XML配置时,创建这样一个代理的最简单方法是 元素。在Java中使用 @Scope 注释配置Bean提供了与 proxyMode 属性相同的支持。默认值为 ScopedProxyMode.DEFAULT,它通常表示不应创建作用域代理,除非在组件扫描指令级别配置了不同的默认值。你可以指定ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACESScopedProxyMode.NO

    如果使用Java将范围代理示例从XML参考文档(请参阅作用域代理)移植到我们的 @Bean,它类似于以下内容:

    // 作为代理公开的HTTP会话作用域bean
    @Bean
    @SessionScope
    public UserPreferences userPreferences() {
        return new UserPreferences();
    }
    
    @Bean
    public Service userService() {
        UserService service = new SimpleUserService();
        // 对代理userPreferences bean的引用
        service.setUserPreferences(userPreferences());
        return service;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    自定义命名 Bean

    默认情况下,配置类使用 @Bean 方法的名称作为生成的Bean的名称。但是,这个功能可以被 name 属性覆盖,如下面的例子所示:

    @Configuration
    public class AppConfig {
    
        @Bean("myThing")
        public Thing thing() {
            return new Thing();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Bean 别名

    正如在命名Bean中所讨论的,有时希望给一个Bean赋予多个名称,或者称为Bean别名。@Bean 注释的 name 属性为此接受一个String数组。下面的例子展示了如何为一个bean设置多个别名:

    @Configuration
    public class AppConfig {
    
        @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
        public DataSource dataSource() {
            // 初始化、配置和返回数据源 Bean
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Bean 描述

    有时,提供Bean的更详细的文本描述是有帮助的。当Bean为监视目的而公开(可能通过JMX)时,这可能特别有用。

    要向 @Bean 添加描述,可以使用 @Description 注释,如下面的例子所示:

    @Configuration
    public class AppConfig {
    
        @Bean
        @Description("提供bean的基本示例")
        public Thing thing() {
            return new Thing();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    12.4、使用 @Configuration 注释

    @Configuration 是一个类级注释,指示对象是 Bean 定义的源。@Configuration 类通过 @Bean 注释的方法声明 Bean。对 @Configuration 类上的 @Bean 方法的调用也可以用来定义Bean间的依赖关系。请参阅基本概念:@Bean 和 @Configuration 的大致介绍

    注入 内部-bean 依赖性

    当Bean相互依赖时,表示这种依赖就像让一个Bean方法调用另一个Bean方法一样简单,如下面的示例所示:

    @Configuration
    public class AppConfig {
    
        @Bean
        public BeanOne beanOne() {
            return new BeanOne(beanTwo());
        }
    
        @Bean
        public BeanTwo beanTwo() {
            return new BeanTwo();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在前面的示例中,beanOne 通过构造函数注入接收到对 beanTwo 的引用。

    这种声明 Bean间依赖关系的方法只有在 @Bean方法在 @Configuration类中声明时才有效。你不能使用普通的@Component类来声明 Bean间的依赖关系。

    查找方法注入

    如前所述,查找方法注入是一项高级功能,应很少使用。在单例范围的 Bean 依赖于原型范围的 Bean 的情况下,它很有用。使用 Java 进行这种类型的配置为实现此模式提供了一种自然的方法。下面的示例演示如何使用查找方法注入:

    public abstract class CommandManager {
        public Object process(Object commandState) {
            // 获取相应 Command 接口的新实例
            Command command = createCommand();
            // 设置(希望是全新的)Command 实例的状态
            command.setState(commandState);
            return command.execute();
        }
    
        // okay... 但是这种方法的实现在哪里呢?
        protected abstract Command createCommand();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    通过使用Java配置,你可以创建 CommandManager 的子类,其中以查找新的(原型)命令对象的方式重写了抽象的 createCommand() 方法。下面的例子展示了如何这样做:

    @Bean
    @Scope("prototype")
    public AsyncCommand asyncCommand() {
        AsyncCommand command = new AsyncCommand();
        // 根据需要在此处注入依赖项
        return command;
    }
    
    @Bean
    public CommandManager commandManager() {
        // 使用 createCommand() 返回 CommandManager 的新匿名实现
        // 重写以返回新的原型 Command 对象
        return new CommandManager() {
            protected Command createCommand() {
                return asyncCommand();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    关于基于Java的配置如何在内部工作的进一步信息

    考虑下面的例子,它显示了一个 @Bean 注释方法被调用了两次:

    @Configuration
    public class AppConfig {
    
        @Bean
        public ClientService clientService1() {
            ClientServiceImpl clientService = new ClientServiceImpl();
            clientService.setClientDao(clientDao());
            return clientService;
        }
    
        @Bean
        public ClientService clientService2() {
            ClientServiceImpl clientService = new ClientServiceImpl();
            clientService.setClientDao(clientDao());
            return clientService;
        }
    
        @Bean
        public ClientDao clientDao() {
            return new ClientDaoImpl();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    clientDao()clientService1()clientService2() 中分别被调用了一次。因为这个方法会创建 ClientDaoImpl 的一个新实例并返回它,所以通常会期望有两个实例(每个服务一个实例)。这肯定会有问题:在Spring中,实例化的Bean默认有一个 singleton 作用域。这就是神奇的地方:所有 @Configuration 类在启动时通过 CGLIB 子类化。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器是否有缓存的(作用域的)Bean。

    根据 Bean的作用域,行为可能会有所不同。我们说的是单例。

    从 Spring 3.2开始,不再需要将 CGLIB 加到你的类路径中,因为 CGLIB 类已经被重新打包在 org.springframework.cglib下,并直接包含在 Spring核心 JAR中。

    由于 CGLIB 在启动时动态添加特性,因此有一些限制。特别是,配置类不能是 final 类。然而,从 4.3开始,在配置类上允许使用任何构造函数,包括使用 @Autowired或用于默认注入的单个非默认构造函数声明。

    如果你希望避免任何 CHLIB 强加的限制,请考虑在非 @Configuration类上声明你的 @Bean方法(例如,改为在普通的 @Component类上)。@Bean方法之间的交叉方法调用不会被拦截,因此你必须完全依赖于构造函数或方法级别的依赖项注入。

    12.5、编写基于 Java 的配置

    Spring 基于 Java的配置功能允许你编写注释,这可以降低配置的复杂性。

    使用 @Import 注解

    正如 元素在Spring XML文件中被用来帮助模块化配置一样,@Import 注释允许从另一个配置类加载 @Bean 定义,如下面的例子所示:

    @Configuration
    public class ConfigA {
        @Bean
        public A a() {
            return new A();
        }
    }
    
    @Configuration
    @Import(ConfigA.class)
    public class ConfigB {
        @Bean
        public B b() {
            return new B();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    现在,当实例化上下文时,不再需要同时指定 ConfigA.classConfigB.class,只需要显式地提供 ConfigB,如下例所示:

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
    
        // 现在,bean A和bean B都将可用...
        A a = ctx.getBean(A.class);
        B b = ctx.getBean(B.class);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这种方法简化了容器实例化,因为只需要处理一个类,而不是要求你在构造期间记住潜在的大量 @Configuration 类。

    从 Spring Framework 4.2中,@Import也支持对常规组件类的引用,类似于 AnnotationConfigApplicationContext.register 方法。如果你希望避免组件扫描,这尤其有用,可以使用一些配置类作为入口点,显式地定义所有组件。

    在导入的 @Bean 定义上注入依赖关系

    前面的例子是可行的,但过于简单。在大多数实际场景中,Bean跨配置类相互依赖。在使用XML时,这不是问题,因为不涉及编译器,你可以声明 ref="someBean",并相信Spring会在容器初始化期间解决这个问题。当使用 @Configuration 类时,Java编译器会对配置模型施加约束,因为对其他Bean的引用必须是有效的Java语法。

    幸运的是,解决这个问题很简单。正如我们已经讨论过的@Bean方法可以有任意数量的参数来描述Bean依赖项。考虑以下更真实的场景,其中有几个 @Configuration 类,每个类都依赖于在其他类中声明的bean:

    @Configuration
    public class ServiceConfig {
    
        @Bean
        public TransferService transferService(AccountRepository accountRepository) {
            return new TransferServiceImpl(accountRepository);
        }
    }
    
    @Configuration
    public class RepositoryConfig {
    
        @Bean
        public AccountRepository accountRepository(DataSource dataSource) {
            return new JdbcAccountRepository(dataSource);
        }
    }
    
    @Configuration
    @Import({ServiceConfig.class, RepositoryConfig.class})
    public class SystemTestConfig {
    
        @Bean
        public DataSource dataSource() {
            // return new DataSource
        }
    }
    
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
        // 所有东西都连接到配置类...
        TransferService transferService = ctx.getBean(TransferService.class);
        transferService.transfer(100.00, "A123", "C456");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    还有另一种方法可以达到同样的效果。请记住,@Configuration 类最终只是容器中的另一个Bean:这意味着它们可以像任何其他Bean一样利用 @Autowired@Value 注入以及其他特性。

    确保以这种方式注入的依赖关系是最简单的类型。@Configuration 类在上下文初始化的相当早的时候就被处理了,并且强制以这种方式注入依赖可能会导致意外的早期初始化。请尽可能采用基于参数的注入,如上例所示。

    另外,通过 @Bean 使用 BeanPostProcessorBeanFactoryPostProcessor 定义时要特别小心。它们通常应该声明为 static @Bean方法,而不是触发包含它们的配置类的实例化。否则,@Autowired@Value可能不会对配置类本身起作用,因为可以在 AutowiredAnnotationBeanPostProcessor 之前将其创建为 Bean实例。

    以下示例显示了如何将一个 Bean 自动连接到另一个 Bean:

    @Configuration
    public class ServiceConfig {
    
        @Autowired
        private AccountRepository accountRepository;
    
        @Bean
        public TransferService transferService() {
            return new TransferServiceImpl(accountRepository);
        }
    }
    
    @Configuration
    public class RepositoryConfig {
    
        private final DataSource dataSource;
    
        public RepositoryConfig(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        @Bean
        public AccountRepository accountRepository() {
            return new JdbcAccountRepository(dataSource);
        }
    }
    
    @Configuration
    @Import({ServiceConfig.class, RepositoryConfig.class})
    public class SystemTestConfig {
    
        @Bean
        public DataSource dataSource() {
            // return new DataSource
        }
    }
    
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
        // 所有东西都连接到配置类...
        TransferService transferService = ctx.getBean(TransferService.class);
        transferService.transfer(100.00, "A123", "C456");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    @Configuration类中的构造函数注入仅在 Spring Framework 4.3中支持。还要注意,如果目标 Bean只定义一个构造函数,则不需要指定 @Autowired

    完全限定导入的 Bean,便于导航

    在前面的场景中,使用 @Autowired 效果很好,并且提供了所需的模块化,但是准确地确定在哪里声明自动连接Bean定义仍然有些含糊不清。例如,作为一个查看 ServiceConfig 的开发人员,你如何确切地知道@Autowired AccountRepository Bean声明的位置?它在代码中不是显式的,这可能很好。请记住,用于Eclipse的Spring Tools提供了一些工具,这些工具可以呈现显示所有连接方式的图形,这可能就是你所需要的全部。另外,你的 Java IDE可以轻松地找到 AccountRepository 类型的所有声明和使用,并快速显示返回该类型的 @Bean 方法的位置。

    如果这种多义性是不可接受的,并且你希望在IDE中从一个 @Configuration 类直接导航到另一个,考虑自动装配配置类本身。下面的例子展示了如何这样做:

    @Configuration
    public class ServiceConfig {
    
        @Autowired
        private RepositoryConfig repositoryConfig;
    
        @Bean
        public TransferService transferService() {
            // 通过配置类导航到 @Bean 方法!
            return new TransferServiceImpl(repositoryConfig.accountRepository());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在上述情况下,定义 AccountRepository 的地方是完全显式的。然而,ServiceConfig 现在与 RepositoryConfig 紧密耦合。这是一种权衡。这种紧密耦合可以通过使用基于接口或基于抽象类的 @Configuration 类在一定程度上得到缓解。考虑下面的例子:

    @Configuration
    public class ServiceConfig {
    
        @Autowired
        private RepositoryConfig repositoryConfig;
    
        @Bean
        public TransferService transferService() {
            return new TransferServiceImpl(repositoryConfig.accountRepository());
        }
    }
    
    @Configuration
    public interface RepositoryConfig {
    
        @Bean
        AccountRepository accountRepository();
    }
    
    @Configuration
    public class DefaultRepositoryConfig implements RepositoryConfig {
    
        @Bean
        public AccountRepository accountRepository() {
            return new JdbcAccountRepository(...);
        }
    }
    
    @Configuration
    @Import({ServiceConfig.class, DefaultRepositoryConfig.class})  // 导入具体配置!
    public class SystemTestConfig {
    
        @Bean
        public DataSource dataSource() {
            // return DataSource
        }
    
    }
    
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
        TransferService transferService = ctx.getBean(TransferService.class);
        transferService.transfer(100.00, "A123", "C456");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    现在 ServiceConfig 与具体的 DefaultRepositoryConfig 松散耦合,内置的IDE工具仍然很有用:你可以很容易地获得 RepositoryConfig 实现的类型层次结构。通过这种方式,导航 @Configuration 类及其依赖就和导航基于接口的代码没有什么不同了。

    如果希望影响某些 Bean的启动创建顺序,可以考虑将其中一些 Bean声明为@Lazy(用于在第一次访问时创建,而不是在启动时创建)或 @DependsOn某些其他 Bean(确保在当前 Bean之前创建特定的其他 Bean,而不是后者的直接依赖关系所暗示的那样)。

    有条件地包含 @Configuration 类或 @Bean 方法

    根据某些任意的系统状态,有条件地启用或禁用一个完整的 @Configuration 类,甚至单个的 @Bean 方法,这通常很有用。一个常见的例子是,只有在Spring Environment 中启用了特定的概要文件时,才使用 @Profile 注释来激活Bean(详细信息请参阅 Bean 定义配置文件)。

    @Profile 注释实际上是通过使用更灵活的 @Conditional 注释实现的。@Conditional 注释指出了在注册 @Bean 之前应该咨询的特定的 org.springframework.context.annotation.Condition 实现。

    Condition 接口的实现提供了一个返回 truefalsematches(…) 方法。例如,下面的清单显示了用于 @Profile 的实际条件实现:

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 读取 @Profile 注释属性
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    有关更多详细信息,请参阅 @Conditional Java文档。

    结合 Java 和 XML 配置

    Spring 的 @Configuration 类支持的目标并不是100%完全替代Spring XML。有些工具(如Spring XML 命名空间)仍然是配置容器的理想方式。在XML方便或必要的情况下,你可以选择:

    1. 要么以“以XML为中心”的方式实例化容器,例如使用 ClassPathXmlApplicationContext
    2. 要么以“以Java为中心”的方式实例化它,使用 AnnotationConfigApplicationContext@ImportResource 注释在需要时导入XML
    ① 以 XML 为中心的使用 @Configuration

    从XML引导Spring容器并以特别的方式包含 @Configuration 类可能更可取。例如,在一个使用Spring XML的大型现有代码库中,更容易根据需要创建 @Configuration 类,并从现有的XML文件中包含它们。在本节的后面,我们将介绍在这种“以xml为中心”的情况下使用 @Configuration 类的选项。

    将@Configuration类声明为普通Spring 元素

    记住,@Configuration 类最终是容器中的Bean定义。在本系列示例中,我们创建了名为 AppConfig@Configuration 类,并将其作为 定义包含在 system-test-config.xml 中。因为 被打开,容器识别 @Configuration 注释并正确处理 AppConfig 中声明的 @Bean 方法。

    下面的例子展示了一个普通的Java配置类:

    @Configuration
    public class AppConfig {
    
        @Autowired
        private DataSource dataSource;
    
        @Bean
        public AccountRepository accountRepository() {
            return new JdbcAccountRepository(dataSource);
        }
    
        @Bean
        public TransferService transferService() {
            return new TransferService(accountRepository());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    下面的例子展示了 system-test-config.xml 文件的一部分:

    <beans>
        
        <context:annotation-config/>
        <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
    
        <bean class="com.acme.AppConfig"/>
    
        <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        bean>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    以下示例显示了可能的 文 jdbc.properties 件:

    jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
    jdbc.username=root
    jdbc.password=root
    
    • 1
    • 2
    • 3
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext(
            "classpath:/com/acme/system-test-config.xml");
        TransferService transferService = ctx.getBean(TransferService.class);
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    system-test-config.xml文件中,AppConfig没有声明 id元素。虽然这样做是可以接受的,但这是不必要的,因为没有其他 Bean引用它,而且不太可能按名称从容器显式地获取它。类似地,DataSource Bean只会根据类型自动连接,因此显式的 Bean id不是严格要求的。

    使用 拾取@Configuration

    因为 @Configuration 是用 @Component 进行元注释的,所以 @Configuration 注释的类自动成为组件扫描的候选者。使用与上一个示例相同的场景,我们可以重新定义 system-test-config.xml,以利用组件扫描的优势。注意,在这种情况下,我们不需要显式声明 ,因为 启用了相同的功能。

    修改后的 system-test-config.xml 文件示例如下:

    <beans>
        
        <context:component-scan base-package="com.acme"/>
        <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
    
        <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
        bean>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    @Configuration 类为主要配置, XML 为次要配置的 @ImportResource 的使用

    @Configuration 类是配置容器的主要机制的应用程序中,仍然可能需要至少使用一些XML。在这些场景中,你可以使用 @ImportResource 并只定义所需的XML。这样做实现了一种“以java为中心”的方法来配置容器,并将XML保持在最低限度。下面的示例(包括一个配置类、一个定义Bean的XML文件、一个属性文件和main类)展示了如何使用@ImportResource 注释来实现“以java为中心”的配置,该配置在需要时使用XML:

    @Configuration
    @ImportResource("classpath:/com/acme/properties-config.xml")
    public class AppConfig {
    
        @Value("${jdbc.url}")
        private String url;
    
        @Value("${jdbc.username}")
        private String username;
    
        @Value("${jdbc.password}")
        private String password;
    
        @Bean
        public DataSource dataSource() {
            return new DriverManagerDataSource(url, username, password);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    
    <beans>
        <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
    beans>
    
    • 1
    • 2
    • 3
    • 4
    # jdbc.properties
    jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
    jdbc.username=root
    jdbc.password=root
    
    • 1
    • 2
    • 3
    • 4
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        TransferService transferService = ctx.getBean(TransferService.class);
        // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    【Python+requests+unittest+excel】实现接口自动化测试框架
    构造与析构
    基于Springboot实现网上商城管理系统演示【项目源码+论文说明】
    Qt 信号和槽
    高云GW1N-9的SerDes笔记
    云原生开发:从容器到微服务的全栈指南
    第24章_瑞萨MCU零基础入门系列教程之内部温度传感器-TSN
    1.7-32:行程长度编码
    TikTok官方挑战赛和野生挑战赛
    WPF随笔收录-DataGrid固定右侧列
  • 原文地址:https://blog.csdn.net/qq_30769437/article/details/126788551