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配置的形式。

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