基于Java的容器配置

使用Java代码配置Spring容器

围绕着@Bean@Configuration为中心:
@Configuration的类里面有@Bean的方法生成bean。
使用@Configuration可以获得增强的CGLIB支持。

full还是lite的@Bean模式:

定义在非@Configuration类里的@Bean方法被认为是lite模式。
Lite模式的@Bean方法不能声明bean内依赖,只能操作其容器类的内部有的,或者它的方法参数。
这种@Bean方法不能调用别的@Bean方法,仅仅作为bean的工厂方法参考,没有运行时意义。
正向的副作用是在类设计方面没有限制,比如final。
Full模式@Bean方法的跨方法引用都会被重定向到容器的生命周期管理,
可以防止同样的@Bean方法被普通的Java代码调用,以减少不易察觉的bug。

通过AnnotationConfigApplicationContext类实例化Spring容器:

不止可以接受@Configuration类,还能接受@ComponentJSR-330注解类。
@Configuration类作为参数时,这个类和类内的@Bean方法都会被注册成bean定义。
@ComponentJSR-330类作为参数时,会被注册为bean定义,并假定用了@Autowired@Inject

如何使用:new一个AnnotationConfigApplicationContext类,将配置类作为参数传递进去:

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

它的构造函数接受可变参数,想传递几个类就传递几个。

编程式构建容器:调用它的register方法注册bean:

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

开启组件扫描:

@Configuration类添加@ComponentScan:

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

或者手动启动扫描:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}

两者都能实现同样的功能,而且scan方法支持可变参数。

等价的xml配置:

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

注意:@Configuration注解含有@Component元注解,所以也可以是候选。

使用@Bean注解:

用于方法,用这个方法在容器内注册bean definition,类型是方法的返回值。
默认,bean的名称就是方法名:

@Configuration
public class AppConfig {

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

等价于xml中的:

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

@Bean方法的返回类型也可以是接口类型:

@Configuration
public class AppConfig {

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

但是这样会限制更进一步的类型匹配,容器仅在实例化这个bean的时候知道它的完全类型。
非lazy初始化的bean按照定义顺序进行实例化,可能会导致不同的匹配结果。
对于实现了多个接口的bean最好定义成返回最确切的类型。

bean依赖:2

可以将任意数量的依赖作为@Bean方法的参数,和基于构造器的DI一样:

@Configuration
public class AppConfig {

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

生命周期调用:

可以使用来自JSR-250的@PostConstruc@PreDestroy注解;
bean实现InitializingBeanDisposableBean, 或Lifecycle接口;
实现标准的*Aware之类的接口;
@Bean注解提供的两个属性:initMethod和destroyMethod。

比如:

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有一个public的名为closeshutdown方法会自动当作销毁调用。
可以用@Bean(destroyMethod=””)将其禁用(inferred模式)。

可以通过直接调用初始化方法达成相同的目的:

@Configuration
public class AppConfig {

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

    // ...
}

使用@Scope声明bean作用域:

默认bean是singleton作用域,可声明为别的:

@Configuration
public class MyConfiguration {

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

@Scope和scoped-proxy:

用于不同作用域的依赖注入。
创建一个代理最简单的方法就是在xml配置文件里添加:<aop:scoped-proxy/>元素。
@Scope注解提供等价的proxyMode属性,默认值是ScopedProxyMode.DEFAULT,不创建scoped-proxy,除非在扫描阶段提供不同的默认值。
可选的值为:
ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACESScopedProxyMode.NO

定制bean名称:

默认的bean名称就是工厂方法名,通过@Bean注解的name属性重新设定:

@Configuration
public class AppConfig {

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

bean别名:

name属性接受一个字符串数组参数,可以给bean声明多个别名/名称:

@Configuration
public class AppConfig {

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

bean描述:

提供更多的bean的细节描述,通过@Description注解:

@Configuration
public class AppConfig {

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

使用@Configuration注解:

类级别注解,表示一个对象就是一个bean的定义源。
通过@Bean方法声明bean。也可以用于直接调用bean方法达成bean内依赖:

@Configuration
public class AppConfig {

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

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

注意:仅可用于@Configuration类内!

方法注入:

singletin作用域的bean依赖于prototype作用域的bean。反正少用就对了,创建singleton bean的时候创建一个prototype的bean就行。

更多的@Configuration的内部信息:

看看下面的代码片段:

@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()创建了几个实例?实际上就创建了一个实例。
@Configuration类在启动时就成为CGLIB的子类,所以@Configuration类不能是final的。
其内部的方法被调用时会先检查容器,如果容内没有这个bean,就会调用bean的工厂方法生成一个实例,生成后bean实例后就相当于被缓存了。

有些许限制,由于在开始时候CGLIB动态新增的功能,@Configuration类不可以是final的,但是允许有构造器。如果想要避开这个限制就不要使用@Configuration,但是不能进行bean内依赖。

组合基于Java的配置

使用@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();
    }
}

使用时只要注册B类就能获取到类A的bean:

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

可以导入没有注解的类,类似于通过AnnotationConfigApplicationContext.register方法实现的。
在不想被自动扫描的时候很有用。

在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定义BeanPostProcessorBeanFactoryPostProcessor,这些方法应该被声明称static,这样就不会触发初始化它们的容器类。否则@Configuration类的@Autowired@Value可能会不工作,因为可以在AutowiredAnnotationBeanPostProcessor之前实例化bean。

使用全限定路径导入bean让代码更好理解:

如果要在代码里寻找类似@Autowired AccountRepository的内容会很麻烦。
如果不想找来找去,可以考虑在代码里装配配置类:

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // 通过bean方法直接获得bean
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

但是这样ServiceConfig就和RepositoryConfig紧耦合了。可以通过装配接口类型缓解。

条件化包含@Configuration类或@Bean方法:

有条件地开启或禁用@Configuration类或@Bean方法,可以让代码有多种状态,如开发版,测试版等。
常用@Profile注解。可以通过Spring Environment修改启用的profile。
@Profile被更灵活的@Conditional注解实现,指示特定的org.springframework.context.annotation.Condition实现类应该在bean被注册之前访问。

Condition接口有一个返回boolean名为matches的方法,比如@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配置

@Configuration类并不是为了100%替代xml而生的。

Java配置为中心添加xml:使用@ImportResource注解来导入xml配置。

xml配置为中心添加Java配置:将@Configuration类声明成<bean/>元素:

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

使用:

public static void main(String[] args) {
    ApplicationContext ctx = new ClassPathXmlApplicationContext("xxx.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

由于开启了<context:annotation-config/>,所以AppConfig内的bean都会被处理。
${jdbc.url}等样子的代码是占位符,Spring会读取配置文件,就是 <context:property-placeholder location=”classpath:/com/acme/jdbc.properties”/>,之后会自动替换。

jdbc.properties文件:

jdbc.url=jdbc:mysql://localhost/xdb
jdbc.username=sa
jdbc.password=pw

或者通过<context:component-scan/>开启自动扫描。

Java配置包含xml:
使用@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>

留下评论

您的邮箱地址不会被公开。 必填项已用 * 标注