Spring核心技術(shù)(十二)——基于Java的容器配置(二)

使用@Configuration注解

@Configuration注解是一個(gè)類級(jí)別的注解,表明該對(duì)象是用來指定Bean的定義的。@Configuration注解的類通過@Bean注解的方法來聲明Bean。通過調(diào)用注解了@Bean方法的返回的Bean可以用來構(gòu)建Bean之間的相互依賴關(guān)系,可以通過前文來了解其基本概念。

注入inter-bean依賴

當(dāng)@Bean方法依賴于其他的Bean的時(shí)候,可以通過在另一個(gè)方法中調(diào)用即可。

@Configuration
public class AppConfig {

    @Bean
    public Foo foo() {
        return new Foo(bar());
    }

    @Bean
    public Bar bar() {
        return new Bar();
    }
}

在上面的例子當(dāng)中,foo這個(gè)bean是通過構(gòu)造函數(shù)來注入另一個(gè)名為bar的Bean的。

前面提到過,使用@Component注解的類也是可以使用@Bean注解來聲明Bean的,但是通過@Component注解類內(nèi)的Bean之間是不能夠相互作為依賴的。

查找方法注入

前文中描述了一種我們很少使用的基于查找方法的注入。這個(gè)方法在單例Bean依賴原型Bean的時(shí)候尤為有效?;贘ava的配置也支持這種模式。

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的配置,開發(fā)者可以創(chuàng)建一個(gè)CommandManager的自雷來重寫createCommand()方法,這樣就會(huì)查找一個(gè)新的(原型)命令對(duì)象。

@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 command() overridden
    // to return a new prototype Command object
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}

基于Java配置的內(nèi)部工作原理

下面的例子展示了@Bean注解了的方法被調(diào)用了兩次:

@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()調(diào)用了一次,也被clientService2()調(diào)用了一次。因?yàn)檫@個(gè)方法創(chuàng)建了一個(gè)新的ClientDaoImpl的實(shí)例,開發(fā)者通常認(rèn)為這應(yīng)該是兩個(gè)實(shí)例(每個(gè)服務(wù)都有一個(gè)實(shí)例)。如果那樣的話,就產(chǎn)生問題了:在Spring中,實(shí)例化的Bean默認(rèn)情況下是單例的。這就是神奇的地方了,所有配置了@Configuration的類都是通過CGLIB來子類化的。在子類當(dāng)中,所有的子類方法都會(huì)檢測(cè)容器是否有緩存的對(duì)象,然后在調(diào)用父類方法,創(chuàng)建一個(gè)新的實(shí)例。在Spring 3.2 以后的版本之中,開發(fā)者不再需要在classpath中增加CGLIB的jar包了,因?yàn)镃GLIB的類已經(jīng)被打包到org.springframework.cglib之中,直接包含到了spring-core的jar包之中。

當(dāng)然,不同作用域的對(duì)象在CGLIB中的行為是不同的,前面提到的都是單例的Bean。

當(dāng)然,使用CGLIB的類也有一些限制和特性的:

  • 因?yàn)樾枰宇惢耘渲妙惒荒転閒inal的
  • 配置類需要包含一個(gè)無參的構(gòu)造函數(shù)

組合基于Java的配置

使用@Import注解

跟XML中通過使用<import/>標(biāo)簽來實(shí)現(xiàn)模塊化配置類似,基于Java的配置中也包含@Import注解來加載另一個(gè)配置類之中的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();
    }
}

現(xiàn)在,在啟動(dòng)的配置當(dāng)中不再需要制定ConfigA.class以及ConfigB.class來實(shí)例化上下文了,僅僅配置一個(gè)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);
}

這種方法簡(jiǎn)化了容器的實(shí)例化,僅僅處理一個(gè)類就可以了,而不需要開發(fā)者記住大量的配置類的信息。

對(duì)導(dǎo)入的Bean進(jìn)行依賴注入

上面的例子是OK的,但是太過簡(jiǎn)化了,在大多數(shù)場(chǎng)景下,Bean可能依賴另一個(gè)配置類中定義的Bean。當(dāng)使用XML的時(shí)候,這完全不是問題,因?yàn)椴]有編譯器參與其中,開發(fā)者可以通過簡(jiǎn)單的聲明ref="someBean"就引用了對(duì)應(yīng)的依賴,Spring完全可以正常初始化。當(dāng)然,當(dāng)使用@Configuration`注解類的時(shí)候,Java編譯器會(huì)約束配置的模型,對(duì)其他Bean的引用是必須符合Java的語(yǔ)法的。

幸運(yùn)的是,我們解決這個(gè)問題的方法很簡(jiǎn)單,如之前所討論的,@Bean注解的方法可以有任意數(shù)量的參數(shù)來描述其依賴,讓我們考慮一個(gè)包含多個(gè)配置類,且其中的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");
}

當(dāng)然,也有另一種方式來達(dá)到相同的結(jié)果,記得前文提到過,注解為@Configuration的配置類也會(huì)被聲明為Bean由容器管理的:這就意味著開發(fā)者可以通過使用@Autowired@Value來注入。

開發(fā)者需要確保需要注入的依賴是簡(jiǎn)單的那種依賴。@Configuration類在容器的初始化上下文中處理過早或者強(qiáng)迫注入可能產(chǎn)生問題。任何的時(shí)候,盡量如上面的例子那樣來確保依賴注入正確。
而且,需要特別注意通過@Bean注解定義的BeanPostProcessor以及BeanFactoryPostProcessor。這些方法通常應(yīng)定義為靜態(tài)的@Bean方法,而不要觸發(fā)其他配置類的實(shí)例化。否則,@Autowired@Value在配置類中無法生效,因?yàn)樗^早的實(shí)例化了。

@Configuration
public class ServiceConfig {

    @Autowired
    private AccountRepository accountRepository;

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

}

@Configuration
public class RepositoryConfig {

    private final DataSource dataSource;

    @Autowired
    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");
}

在Spring 4.3中,只支持基于構(gòu)造函數(shù)的注入。在4.3的版本中,如果Bean僅有一個(gè)構(gòu)造函數(shù)的話,是不需要指定@Autowired注解的。在上面的例子當(dāng)中,@AutowiredRepositoryConfig的構(gòu)造函數(shù)上是不需要的。

在上面的場(chǎng)景之中,使用@Autowired注解能很好的工作,但是精確決定裝載的Bean還是容易引起歧義的。比如,開發(fā)者在查看ServiceConfig配置,開發(fā)者怎么能夠知道@Autowired AccountRepositorybean在哪里聲明?在代碼中這點(diǎn)并不明顯,但是也不要緊。通過一些IDE或者是Spring Tool Suite提供了一些工具讓開發(fā)者來看到裝載的Bean之間的聯(lián)系圖。

如果開發(fā)者不希望通過IDE來找到對(duì)應(yīng)的配置類的話,可以通過注入的方式來消除語(yǔ)義的不明確:

@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這個(gè)Bean在哪里定義了。然而,ServiceConfig現(xiàn)在耦合到了RepositoryConfig上了,這也是折中的辦法。這種耦合可以通過使用基于接口或者抽象類的的方式在某種程度上將減少??聪旅娴拇a:

@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");
}

現(xiàn)在ServiceConfigDefaultRepositoryConfig松耦合了,而IDE中的工具仍然可用,讓開發(fā)者可以找到RepositoryConfig的實(shí)現(xiàn)。通過這種方式,在類依賴之間導(dǎo)航就普通接口代碼沒有區(qū)別了。

選擇性的包括配置類和Bean方法

通常有需求根據(jù)系統(tǒng)的狀態(tài)來選擇性的使能或者趨勢(shì)能一個(gè)配置類,或者獨(dú)立的@Bean方法。一個(gè)普遍的例子就是使用@Profile注解來在Spring的Environment中符合條件才激活Bean。

@Profile注解是通過使用一個(gè)更加靈活的注解@Confitional來實(shí)現(xiàn)的。@Conditional注解表明Bean需要實(shí)現(xiàn)org.springframework.context.annotation.Condition接口才能作為Bean注冊(cè)到容器中。

實(shí)現(xiàn)Condition接口僅僅提供一個(gè)matches(...)方法返回true或者false。如下為一個(gè)實(shí)際的Condition接口的實(shí)現(xiàn):

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() != null) {
        // 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;
}

更多的信息請(qǐng)參考@Conditional的Javadoc。

同時(shí)使用XML以及基于Java的配置

Spring中的@Configuration類的支持并不是為了100%的替代XML的配置的。SpringXML中的諸如命名空間等還是很理想的用來配置容器的方式。在那些使用XML更為方便的情況,開發(fā)者可以選擇使用XML方式的配置,比如ClassPathXmlApplicationContext,或者是以Java為核心的AnnotationConfigApplicationContext并使用@ImportResource注解來導(dǎo)入必須的XML配置。

以XML為中心,同時(shí)使用配置類

比較好的方式是在Spring容器相關(guān)的XML中包含@Configuration類的信息。舉例來說,在很多機(jī)遇Spring XML配置方式中,很容易創(chuàng)建一個(gè)@Configuration的類,然后作為Bean包含到XML文件中。下面的代碼中將會(huì)在以XML為中心的配置下使用配置類。

配置類其實(shí)根本上來說只是容器中定義的Bean集合而已。在這個(gè)例子中,我們創(chuàng)建了個(gè)配置類叫做AppConfig并將其包含在了system-test-config.xml之中。因?yàn)榇嬖?code><context:annotation-config/>標(biāo)簽,容器可以自發(fā)的識(shí)別@Configuration注解。

@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這個(gè)bean并沒有聲明id元素,當(dāng)然使用id屬性完全可以,但是通常沒有必要,因?yàn)橐话悴粫?huì)引用這個(gè)Bean,而且一般可以通過類型來獲取,比如其中定義的DataSource,除非是必須精確匹配,才需要一個(gè)id屬性。

因?yàn)?code>@Configuration是通使用@Component的元注解的,所以由@Configuration注解的類是會(huì)自動(dòng)匹配組件掃描的。所以在上面的配置中,我們可以利用這一點(diǎn)來很方便的配置Bean。而且我們也不需要指定<context:annotation-config>標(biāo)簽,因?yàn)?code><context:component-scan>會(huì)使能這一功能。

現(xiàn)在的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>

以配置類為中心,同時(shí)導(dǎo)入XML配置

在應(yīng)用中,如果使用了配置類作為主要的配置容器的機(jī)制的話,在某些場(chǎng)景仍然有必要用些XML來輔助配置容器。在這些場(chǎng)景之中,通過使用@ImportResource并且就可以了。如下:

@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=

代碼中就可以通過AnnotationConfigApplicationContext來配置容器了:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容