Spring基礎八:基於java代碼配置

前兩章介紹了,利用java註解,我們可以大大減少xml配置的篇幅,只需在xml裏開啓對相關注解的支持即可。這一節介紹基於java代碼的容器配置,讓我們完全擺脫對xml的依賴。

java代碼配置實際上是結合註解和java代碼,其中的關鍵在@Configuration和@Bean這兩個註解。@Configuration標註java類,指明這是一個Spring的配置類,我們可以看做xml配置文件的替代物;@Bean標註配置類裏的方法,指明這是一個定義bean的工廠方法,可以看做<bean>標籤的替代物。

@Configuration類

@Configuration可以類比一個xml配置文件,它通過成員方法來定義bean;@Configuration類上可以附加其他註解來開啓一些容器級別的功能,比如:java package掃描,屬性佔位符替換等。

@Configuration和@Bean

我們直接看一個示例就能明白這兩個註解的工作方式:

@Configuration
public class AppConfig {

    @Bean
    public ClientService clientService() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }
    
    @Bean
    public ClientDao clientDao() {
        return new ClientDaoImpl();
    }
}

從@Configuration的定義可知,它標註的類本身也定義了一個bean;只不過容器會特殊對待,會掃描到所有的@Bean方法,識別這些bean定義。

與xml配置相比,@Bean方法是樸素的java代碼,非常直觀地創建bean對象並初始化它,java編譯器還能幫我們做類型檢查,有一種返璞歸真的感覺。

組合@Configuration

一個Configuration類,可以Import另外一個Configuration類。這樣一來,當前者被加載的時候,後者也被加載。

@Configuration
public class ConfigA {
	...
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
	...
}

@ComponentScan

該註解用來自動掃描java package,找到這些package下面用@Component註解定義的bean,與前面介紹的xml component-scan標籤作用一致;也可以添加過濾器,具體方式也是一致的。

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

還可用調用context.scan方法來手動掃描java package:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();

加載Configuration

Spring 3.0提供了一種新類型的Context叫做AnnotationConfigApplicationContext,可以接受一個或多個@Configuration標註的類class對象,作爲容器初始化的入口。

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

動態註冊Configuration

可以調用AnnotationConfigApplicationContext.register動態註冊Configuration類:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

@Bean

@Bean註解相當於xml的<bean>標籤,基本實現了後者的全部功能。而且由於@Bean註解的是方法,可以使用java代碼來初始化bean,充分利java語言的能力,避免xml這種文本式配置的不便。

Bean生命週期回調

@Bean註解可以指定任意的回調方法:

@Configuration
public class AppConfig {

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

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

值得注意的是,@Bean註解定義的bean,如果包含public的close或shutdown方法,會被認定爲默認的bean銷燬方法,可以通過以下的方式來取消這種行爲:

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

上面的代碼裏面,DataSource是來自外部的資源,所以並不希望在容器關閉的時候,DataSource實例的close方法被調用,通過將destroyMethod=""取消了對close的自動調用。

最後,如果bean的初始化方法,在bean對象創建好了就能調用,那麼直接在@Bean方法裏面調用就好,沒必要使用@Bean的initMethod屬性來指定:

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

Bean作用域

通過@Scope註解來聲明:

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

Spring提供了一種Scoped proxies機制來解決不同作用域bean之間的依賴問題,@Scope註解通過proxyMode屬性來支持該機制。proxyMode默認值是ScopedProxyMode.NO(沒有代理),你可以指定ScopedProxyMode.TARGET_CLASS或ScopedProxyMode.INTERFACES。

@SessionScope是spring-web基於@Scope創建的一個註解,可以更加方便地聲明bean爲session作用域,它默認將代理模式設定爲ScopedProxyMode.TARGET_CLASS。

Bean的名字

bean的名字默認就是@Bean方法的名字,當然可以通過註解屬性來指定:

@Configuration
public class AppConfig {

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

還可以一次聲明多個bean的別名:

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

Bean描述

通過註解@Description指定,在通過JMX監控bean的時候比較有用:

@Configuration
public class AppConfig {

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

static @Bean方法

一般@Bean都聲明爲Configuration的實例方法,這樣的bean總是晚於Configuration的實例被創建。但是之前講過,像BeanPostProcessor和BeanFactoryPostProcessor這樣的特殊bean,需要在容器的初始化早期被創建。

因此,我們需要static的@Bean方法,以便Spring容器提前初始化他們:

@Configuration
public class Config {
    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        configurer.setLocation(new ClassPathResource("q.properties"));
        return configurer;
    }
}

依賴注入

上一章節介紹的@Autowire註解仍然有效,不過在Configuration類裏,Spring更推薦將依賴注入到bean的構造參數裏,有幾種方法可以做到這一點。

@Bean方法參數

最簡單最直接的方式是通過@Bean方法的參數:

@Configuration
public class ServiceConfig {

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

容器在調用ServiceConfig.transferService初始化bean的時候,會自動尋找匹配的AccountRepository bean作爲參數。

調用@Bean方法

如果被依賴的bean在同一個Configuration裏定義,那麼可以直接調用對應的@Bean方法:

@Configuration
public class ServiceConfig {

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

配置類Autowire配置類

如果被依賴的bean在其他Configuration裏,那麼可以這個配置類Autowire進來,這利用了配置類也是一個bean的事實。
這種方式的好處是,可以明確地限定被依賴的bean來自哪個配置類。

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

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

配置類Autowire依賴bean

最後一種方式,先把外部Bean作爲字段Autowire進來,稍微有些畫蛇添足之感:

@Configuration
public class ServiceConfig {
    @Autowired
    private AccountRepository accountRepository;

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

結合XML和JAVA配置

如果系統中存在xml和java兩種配置形式,那麼有兩種結合方式,一種是以xml爲中心,創建ClassPathXmlApplicationContext,在xml裏面開啓annotation-config支持。

@Configuration
public class AppConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }

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

<beans>
    <!-- 開啓對@Configuration支持 -->
    <context:annotation-config/>
    
    //通過bean引入AppConfig
    <bean class="com.acme.AppConfig"/>
    
    //或者通過scan方式
    <context:component-scan base-package="com.acme"/>
    
    //其他xml配置
</beans>

二是,以java配置爲中心,使用AnnotationConfigApplicationContext,在@Configuration類通過@ImportResource註解來導入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>

@Bean方法的工作原理

先看看下面這個樣例代碼:

@Configuration
public class AppConfig {
    @Bean
    public ClientService clientService() {
        return  new ClientServiceImpl(clientDao());
    }
    @Bean
    public ClientService clientService2() {
        return  new ClientServiceImpl(clientDao());
    }
    @Bean
    public ClientDao clientDao() {
        return new ClientDaoImpl();
    }
}

clientDao方法雖然被調用了多次,但容器內只會有一個ClientDao實例,因爲它的作用域是SingleTon(@Bean的默認作用域)。這看起來與Java方法調用語義相違背,是如何發生的呢?答案是CGLIB技術。

AppConfig本身可以理解爲其他bean的FactoryBean,但是容器不會直接使用它,而是創建AppConfig的一個CGLIB子類,這個子類會攔截所有的@Bean方法,並插入bean生命週期管理邏輯。

同時,受制於java語言本身的規則:子類無法覆蓋父類的private、final方法,也無法覆蓋父類的static方法,因此Spring限制@Bean方法不能是pivate的或final的;而對於static的@bean方法,Spring既需要它,又無法提供任何保護機制,需要使用者來遵守:不直接調用static的@bean方法

注:CGLIB技術是AOP主題的內容,暫時不深究。

普通bean裏面定義@Bean方法

在一個非@Configuration註解的bean裏面也可以定義@Bean方法,例如:

@Component
public class FactoryMethodComponent {
	@Bean
    protected TestBean publicInstance() {
        TestBean tb = new TestBean("publicInstance", 1);
        return tb;
    }
}

此時對容器來說,@Bean依然定義了一個有效的bean,該bean能夠被正常使用;但是,由於有沒有CGLib子類來攔截publicInstance方法,每次對它的調用會創建一個新的實例。

注:雖然不會報錯,單絕對應該避免這種使用方式,不明白Sping爲什麼不禁止它。

總結

@Configuration註解使我們可以用Java代碼徹底取代了xml配置文件;@Bean註解讓我們可以編寫一段java代碼來定義一個bean,無論是對比xml格式的bean定義,還是@Component模式的bean定義,都要強大得多。

@Configuration配置類相比xml更乾淨整潔,相比@Component又能將配置信息集中起來;在一個大型系統裏,我們可以爲每個模塊創建單獨一個@Configuration配置類,是一種非常好的組織Spring配置的形式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章