Spring Framework 6.0.3 、使用JSR 330标准注释和基于java的容器配置 翻译

86 阅读24分钟

从Spring 3.0开始,Spring提供了对JSR-330标准注释(依赖注入)的支持。这些注释的扫描方式与Spring注释相同。要使用它们,您需要在类路径中有相关的jar。

如果您使用Maven, jakarta。注入工件可在标准Maven存储库(repo1.maven.org/maven2/jaka…:

<dependency>
    <groupId>jakarta.inject</groupId>
    <artifactId>jakarta.inject-api</artifactId>
    <version>1</version>
</dependency>

1.11.1. 使用@Inject和@Named的依赖注入

Instead of @Autowired, you can use @jakarta.inject. Inject as follows:

import jakarta.inject.Inject;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    public void listMovies() {
        this.movieFinder.findMovies(...);
        // ...
    }
}

和@Autowired一样,你可以在字段级、方法级和构造函数参数级使用@Inject。此外,您可以将注入点声明为Provider,从而允许按需访问范围较短的bean或通过Provider.get()调用延迟访问其他bean。下面的例子提供了前面例子的一个变体:

import jakarta.inject.Inject;
import jakarta.inject.Provider;

public class SimpleMovieLister {

    private Provider<MovieFinder> movieFinder;

    @Inject
    public void setMovieFinder(Provider<MovieFinder> movieFinder) {
        this.movieFinder = movieFinder;
    }

    public void listMovies() {
        this.movieFinder.get().findMovies(...);
        // ...
    }
}

如果你想为应该被注入的依赖项使用限定名,你应该使用@Named注释,如下面的例子所示:

import jakarta.inject.Inject;
import jakarta.inject.Named;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}


和@Autowired一样,@Inject也可以和java.util.Optional或@Nullable一起使用。这在这里更适用,因为@Inject没有必需的属性。下面的两个例子展示了如何使用@Inject和@Nullable:

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        // ...
    }
}
public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        // ...
    }
}

1.11.2 @Named和@ManagedBean: @Component注释的标准等价物

可以使用@jakarta.inject而不是@Component@jakarta.inject.Namedjakarta.annotation.ManagedBean,如下例所示:

import jakarta.inject.Inject;
import jakarta.inject.Named;

@Named("movieListener")  // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}


使用@Component而不指定组件名是很常见的。@Named可以以类似的方式使用,如下例所示:

import jakarta.inject.Inject;
import jakarta.inject.Named;

@Named
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Inject
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}


当您使用@Named或@ManagedBean时,您可以以与使用Spring注释完全相同的方式使用组件扫描,如下面的示例所示:

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}


与@Component相反,JSR-330 @Named和JSR-250 @ManagedBean注释是不可组合的。您应该使用Spring的原型模型来构建自定义组件注释。

1.11.3. JSR-330标准注释的局限性 当你使用标准注释时,你应该知道一些重要的特性是不可用的,如下表所示:

Springjakarta.inject.*jakarta.inject restrictions / comments
@Autowired@Inject@Inject has no 'required' attribute. Can be used with Java 8’s Optional instead.
@Component@Named / @ManagedBeanJSR-330 does not provide a composable model, only a way to identify named components.
@Scope("singleton")@SingletonThe JSR-330 default scope is like Spring’s prototype. However, in order to keep it consistent with Spring’s general defaults, a JSR-330 bean declared in the Spring container is a singleton by default. In order to use a scope other than singleton, you should use Spring’s @Scope annotation. jakarta.inject also provides a jakarta.inject.Scope annotation: however, this one is only intended to be used for creating custom annotations.
@Qualifier@Qualifier / @Namedjakarta.inject.Qualifier is just a meta-annotation for building custom qualifiers. Concrete String qualifiers (like Spring’s @Qualifier with a value) can be associated through jakarta.inject.Named.
@Value-no equivalent
@Lazy-no equivalent
ObjectFactoryProviderjakarta.inject.Provider is a direct alternative to Spring’s ObjectFactory, only with a shorter get() method name. It can also be used in combination with Spring’s @Autowired or with non-annotated constructors and setter methods.

1.12. 基于java的容器配置

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

1.12.1. Basic Concepts: @Bean and @Configuration (基本概念:@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();
    }
}

上面的AppConfig类等价于下面的Spring <beans/> XML:

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

full @Configuration vs “lite”@Bean模式?

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

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

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

@Bean和@Configuration注释将在下面几节中深入讨论。但是,首先,我们将介绍使用基于java的配置创建spring容器的各种方法。

1.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();
}

如前所述,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();
}

前面的例子假设MyServiceImpl、Dependency1和Dependency2使用Spring依赖注入注释,比如@Autowired。

使用寄存器以编程方式构建容器 register (Class<?>…)

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

//源码
 public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
        this();
        this.register(componentClasses);
        this.refresh();
    }
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();
}

使用scan(String…)

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

@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig  {
    // ...
}

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

public void scan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    this.scanner.scan(basePackages);
}
public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}

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

支持带有AnnotationConfigWebApplicationContext的Web应用程序

注解configapplicationcontextWebApplicationContext变体可以通过注解configwebapplicationcontext获得。您可以在配置Spring ContextLoaderListener servlet侦听器、Spring MVC DispatcherServlet等时使用此实现。下面的web.xml代码片段配置了一个典型的Spring MVC web应用程序(注意使用了contextClass context-param和init-param):

<web-app>
    <!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
        instead of the default XmlWebApplicationContext -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <!-- Configuration locations must consist of one or more comma- or space-delimited
        fully-qualified @Configuration classes. Fully-qualified packages may also be
        specified for component-scanning -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.AppConfig</param-value>
    </context-param>

    <!-- Bootstrap the root application context as usual using ContextLoaderListener -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Declare a Spring MVC DispatcherServlet as usual -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
            instead of the default XmlWebApplicationContext -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!-- Again, config locations must consist of one or more comma- or space-delimited
            and fully-qualified @Configuration classes -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.acme.web.MvcConfig</param-value>
        </init-param>
    </servlet>

    <!-- map all requests for /app/* to the dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>

1.12.3. 使用@Bean注释

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

您可以在@ configuration注释类或@ component注释类中使用@Bean注释。

申明bean

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

@Configuration
public class AppConfig {

    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}
<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

这两个声明使得一个名为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 {

}

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

@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}

但是,这限制了预先类型预测到指定接口类型(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);
    }
}

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

接收生命周期回调

任何用@Bean注释定义的类都支持常规的生命周期回调,并且可以使用来自JSR-250的@PostConstruct和@PreDestroy注释。有关详细信息,请参阅JSR-250注释。

也完全支持常规的Spring生命周期回调。如果bean实现了InitializingBean、DisposableBean或Lifecycle,容器将调用它们各自的方法。

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

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

public class BeanOne {

    public void init() {
        // initialization logic
    }
}

public class BeanTwo {

    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public BeanOne beanOne() {
        return new BeanOne();
    }

    @Bean(destroyMethod = "cleanup")
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

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

对于使用JNDI获取的资源,您可能希望在默认情况下这样做,因为它的生命周期是在应用程序外部管理的。特别是,要确保始终对DataSource执行此操作,因为在Jakarta EE应用程序服务器上这是有问题的。

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

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

同样,对于@Bean方法,您通常使用编程式的JNDI查找,要么使用Spring的JndiTemplate或JndiLocatorDelegate助手,要么直接使用JNDI InitialContext,但不使用JndiObjectFactoryBean变体(这将迫使您将返回类型声明为FactoryBean类型,而不是实际的目标类型,这使得在其他@Bean方法中交叉引用调用更难以使用,因为它们打算引用此处提供的资源)。

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

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        BeanOne beanOne = new BeanOne();
        beanOne.init();
        return beanOne;
    }

    // ...
}

当你直接在Java中工作时,你可以对你的对象做任何你想做的事情,并不总是需要依赖容器的生命周期。

指定Bean范围

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

使用@Scope注释

您可以指定使用@Bean注释定义的bean应该具有特定的作用域。您可以使用Bean作用域部分中指定的任何标准作用域。 默认作用域是单例的,但是你可以用@Scope注释覆盖它,如下面的例子所示:

@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
}

@Scope和scope-proxy

Spring提供了一种通过作用域代理处理作用域依赖关系的方便方法。在使用XML配置时,创建这种代理的最简单方法是<aop:scope -proxy/>元素。使用@Scope注释在Java中配置bean可以使用proxyMode属性提供相同的支持。默认为ScopedProxyModeDEFAULT,它通常表示不应该创建有作用域的代理,除非在组件扫描指令级别配置了不同的默认值。可以指定ScopedProxyMode。TARGET_CLASS ScopedProxyMode。INTERFACES或ScopedProxyMode.NO

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

// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
    return new UserPreferences();
}

@Bean
public Service userService() {
    UserService service = new SimpleUserService();
    // a reference to the proxied userPreferences bean
    service.setUserPreferences(userPreferences());
    return service;
}


定制Bean命名

默认情况下,配置类使用@Bean方法的名称作为生成bean的名称。然而,这个功能可以用name属性重写,如下例所示:

@Configuration
public class AppConfig {

    @Bean("myThing")
    public Thing thing() {
        return new Thing();
    }
}

bean多个别名

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

@Configuration
public class AppConfig {

    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}

Bean描述

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

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

@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Thing thing() {
        return new Thing();
    }
}

1.12.4. 使用@Configuration注释

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

注入bean间依赖项

当bean之间存在依赖关系时,表达这种依赖关系就像让一个bean方法调用另一个方法一样简单,如下面的示例所示:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

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

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

查找方法注入

如前所述,查找方法注入是一种高级特性,应该很少使用。它在单作用域bean依赖于原型作用域bean的情况下非常有用。对这种类型的配置使用Java为实现这种模式提供了一种自然的方法。下面的例子展示了如何使用查找方法注入:

public abstract class CommandManager {
    public Object process(Object commandState) {
        // grab a new instance of the appropriate Command interface
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

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

@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
    AsyncCommand command = new AsyncCommand();
    // inject dependencies here as required
    return command;
}

@Bean
public CommandManager commandManager() {
    // return new anonymous implementation of CommandManager with createCommand()
    // overridden to return a new prototype Command object
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}


关于基于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();
    }
}


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

根据bean的范围,行为可能不同。我们这里谈论的是单身人士。

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

由于CGLIB在启动时动态添加特性,因此存在一些限制。特别是,配置类不能是最终的。但是,从4.3开始,配置类上允许使用任何构造函数,包括使用@Autowired或用于默认注入的单个非默认构造函数声明。 如果您希望避免任何cglib强加的限制,可以考虑在non-@Configuration类上声明@Bean方法(例如,在普通的@Component类上声明)。@Bean方法之间的跨方法调用不会被拦截,因此您必须在那里的构造函数或方法级别上完全依赖依赖项注入。

1.12.5. 编写基于java的配置

Spring基于java的配置特性允许您编写注释,这可以降低配置的复杂性。

使用@Import注释

就像<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();
    }
}

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

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // now both beans A and B will be available...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

这种方法简化了容器实例化,因为只需要处理一个类,而不需要在构造过程中记住可能大量的@Configuration类。

从Spring Framework 4.2开始,@Import也支持对常规组件类的引用,类似于AnnotationConfigApplicationContext。注册方法。如果您希望避免组件扫描,可以使用一些配置类作为入口点显式地定义所有组件,那么这一点特别有用。

在导入的@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);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}


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

确保您以这种方式注入的依赖关系只是最简单的类型。@Configuration类在上下文初始化过程中很早就被处理了,强制以这种方式注入依赖项可能会导致意外的早期初始化。尽可能使用基于参数的注入,如前面的示例所示。

另外,要特别注意通过@Bean定义BeanPostProcessor和BeanFactoryPostProcessor。这些方法通常应该声明为静态的@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);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}


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

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

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

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // navigate 'through' the config class to the @Bean method!
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

在前面的情况中,定义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})  // import the concrete config!
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");
}


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

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

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

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

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

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

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // Read the @Profile annotation attributes
    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;
}

结合Java和XML配置

Spring的@Configuration类支持并不打算100%完全取代Spring XML。一些工具(如Spring XML名称空间)仍然是配置容器的理想方式。在XML方便或必要的情况下,您可以选择:以“以XML为中心”的方式实例化容器,例如使用ClassPathXmlApplicationContext,或者以“以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中。因为打开了<context:annotation-config/>,容器识别@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());
    }
}

system-test-config.xml文件

<beans>
    <!-- enable processing of annotations such as @Autowired and @Configuration -->
    <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>

jdbc.properties文件

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

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

因为@Configuration是用@Component进行元注释的,所以@Configuration注释的类会自动成为组件扫描的候选者。使用与前面示例中描述的相同的场景,我们可以重新定义system-test-config.xml以利用组件扫描。注意,在这种情况下,我们不需要显式声明<context:annotation-config/>,因为<context:component-scan/>启用了相同的功能。

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

<beans>
    <!-- picks up and registers AppConfig as a bean definition -->
    <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>

以配置类为中心使用XML @ImportResource

在@Configuration类是配置容器的主要机制的应用程序中,仍然可能需要至少使用一些XML。在这些场景中,您可以使用@ImportResource并只定义所需的XML。这样做可以实现“以java为中心”的容器配置方法,并将XML保持在最低限度。下面的示例(包括一个配置类、一个定义bean的XML文件、一个属性文件和主类)展示了如何使用@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);
    }
}

properties-config.xml

<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}