使用JavaConfig形式配置Spring框架,了解一下

Posted by wotrd on 09-19,2019

使用JavaConfig形式配置Spring框架,了解一下

本文将介绍Spring 3.0后一种新的配置方式:JavaConfig,使用这种配置方式,既可以作为xml配置的补充,使xml配置精简化,也可以完全替代xml配置,实现无xml配置项目。

同时Spring-Boot就是通过JavaConfig来实现“约定大于配置”的功能,因此学习一下JavaConfig方式对于理解Spring-Boot加载配置的方式也是很有必要的。

JavaConfig:Spring 3.0的新功能 我们知道,Spring容器运行时,需要有一个提供Bean定义的载体,在3.0前,这个载体主要是xml配置文件,而从Spring 3.0开始,一个新的@Configuration注解被添加进框架,使用该注解可以快速标识一个类成为Spring的Java Class配置类,官方称这种方式为JavaConfig,这种方式旨在使用Java类对Spring框架进行配置,提供Bean定义,是xml配置文件的一种替代或协同工作的形式。

也就是说使用JavaConfig,其各个功能都是可以在原本的xml配置里找到对应的。

JavaConfig主要通过注解来实现Bean定义,配合新的基于注解的AnnotationConfigApplicationContext,可以实现将xml配置转移到代码中的特性,这种配置方式既可以完全替代xml配置,也可以和xml配置相结合,使xml配置更加精简化。

现在Spring中定义一个Bean,我们就有了三种方式:1.xml配置中使用;2.使用@Component等注解配合注解扫描;3.使用JavaConfig配合@Configuration、@Bean注解。

新的JavaConfig注解和基于注解的ApplicationContext介绍 从Spring3.0开始,新增的与JavaConfig有关的注解主要为@Configuration、@Bean,新增的ApplicationContext主要为AnnotationConfigApplicationContext。

此外,与配置有关的注解还有@Import、@ImportSource、@ComponentScan、@Profile、@Conditional、@Scope、@Lazy、@DependsOn、@Primary,这里既有4.0后新的注解,也有2.5前就已经存在的注解,同时Spring3.0中新增了一个AnnotationConfigWebApplicationContext,作为一个新的Web项目ApplicationContext被添加进框架,后面我会一一介绍它们的。

本节先来介绍一下主要注解和新的AnnotationConfigApplicationContext的功能,注意本文在撰写时是以Spring当前版本5.0.7 Release为准,相关文档可以在Java-based container configuration找到,同时每小段的标题都附上了current(最新正式)版本api的连接,如果想了解更多细节,可以点进去阅读,如果新版本Spring api文档与下面的内容有出入,请根据你使用的Spring版本来决定内容的可靠性。

@Configuration @Configuration只能标记在类上,表示该类为JavaConfig类,使其可以被Spring IOC容器扫描识别并创建Bean加入到容器中。

@Configuration类就相当于以往的一个xml文件。

下面是一个Configuration注解官方文档的例子:

@Configuration public class AppConfig{ @Bean public MyBean myBean(){ // instantiate, configure and return bean ... return new MyBean(); } } 这个JavaConfig就相当于原来的xml配置:

也就是该类实现了定义一个id为myBean,类名为MyBean,由Spring容器托管的bean。

需要注意的是,Spring规定了几条使用@Configuration注解类的约束,这些约束多半是由于Spring要使用AOP来增强Configuration类所引起的,不过我个人觉得实践中会出现这些问题的情况几乎很少吧:

Configuration类必须以类的方式提供,比如不能是通过工厂方法返回的实例,这样在运行时框架可以通过创建其子类来实现AOP增强功能。 Configuration类不能被标记为final。 Configuration类不能是局部类(本地类,英文为local class),比如代码块里的局域内部类。 嵌套的configuration类必须被定义为static类型。 @Bean方法不能返回一个Configuration类的实例,如果你这么做了,那么它只是按照一个正常的bean被返回,Configuration类内部的配置相关注解如@Bean是无法被识别处理的。 @Configuration本身也是一个复合的注解,它还整合了@Component,也就是说它可以被Component-Scan识别,并以一个Bean的形式由Spring IOC容器托管,当然在大部分情况下开发者并不会需要使用这个bean,但也因此我们可以利用注解扫描的方式式来控制Spring容器处理这些JavaConfig类,实现依赖注入、@Bean方法扫描等功能。

@Bean @Bean只能标记在方法上,表示该方法返回一个Spring Bean,可以被IOC容器托管,相当于以前在xml文件中写的元素。

在上面的例子中,已经出现了@Bean的用法。

@Bean注解有几个属性可以配置:

name:指定一个或者多个bean的名字,当没有设置name时,Spring容器会默认将@Bean方法名作为bean name,当设置了name后,就不会再使用方法名,同时设置多个name时,除第一个name外,其他的都会作为bean的别名。相当于xml配置中的name属性。 initMethod:指定容器在初始化完bean后调用的方法。相当于xml配置中的init-method属性。 destroyMethod:指定在容器在销毁bean前调用的方法。相当于xml配置中的 destroy-method。 autowire:指定bean在自动装配时依赖注入使用的策略,取值可以参考Enum类Autowire 的三个常量:Autowire.BY_NAME,Autowire.BY_TYPE,Autowire.NO。 同时如果我们创建bean时,需要注入依赖的话,可以在方法参数里指定要注入的bean,或是在同一个@Configuration类里直接调用@Bean标记的方法:

@Bean public MyBean2 myBean2(MyBean myBean){ return new MyBean2(myBean); }

@Bean public MyBean3 myBean3(){ return new MyBean3(mybean2()); } Spring容器会自动将依赖的bean注入,在这里的注入new MyBean2(myBean)就是构造器注入了,我们也可以在new出来对象后,使用set方法注入,这和以往的创建Bean时的几种依赖注入方式是一致的。

你可能注意到创建MyBean3时,是调用了同@Configuration类里的其他@Bean方法,在这种情况下,Spring会利用CGLIB实现的AOP,在调用方法前到IOC容器里去找到对应的myBean2并返回。因此在创建MyBean3时,它被注入的myBean2是IOC容器里的myBean2,而非直接调用myBean2()方法,同时由于Bean默认的Scope是singleton,因此myBean2被注入时也以单例形式注入,如果你不太明白,那么可以看看下面这个例子:

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

在该例中,clientService1()和clientService2()方法都调用了clientDao()方法,并且clientDao()方法是被@Bean标注的方法,因此被注入到clientService1和clientService2的clientDao此时指向的同一个bean对象。

你可能注意到了我前面强调了“同@Configuration类里的其他@Bean方法”,你可能会想@Bean能不能放在没有@Configuration标记的类的方法上,实际上确实是有这种用法的。

@Bean并不一定非要标记在@Configuration类里,当标记在普通的类的方法上时,Spring容器会使用精简模式来创建Bean,此时调用其他@Bean方法只是普通的方法调用,而非通过AOP方式进行依赖注入,并且由于没有@Configuration注解,该类无法通过扫描的方式装配bean,必须手动注册到ApplicationContext中才能装配bean。

AnnotationConfigApplicationContext 下文我将以”ACAC”来代替称呼”AnnotationConfigApplicationContext”。

ACAC的主要功能是识别类中的Spring注解,然后自动装配bean,不同于ClassPathXmlApplicationContext使用xml作为框架配置的入口,并直接在xml文件里定义bean,ACAC需要使用@Configuration注解标记的类作为框架配置入口,并且ACAC提供了完全基于注解的形式来处理bean的方式,因此它能识别出@Configuration类并进行处理。

通过使用AnnotationConfigApplicationContext取代原来的ClassPathXmlApplicationContext,可以达到使用JavaConfig替代XmlConfig的目的。

该类的主要方法如下:

构造方法:ACAC的构造方法主要有三种,无参、(Class<?>… annotatedClasses)、(String… basePackages) register(java.lang.Class<?>… annotatedClasses):传入一个或多个被Spring注解标记的类,比如@Configuration标记的JavaConfig类、@Service、@Controller、@Repository等注解标记的类。 scan(java.lang.String… basePackages):类似component-scan,可以传入一个或多个包路径,Spring容器会扫描这些包路径内的所有注解并进行处理,其中包括了@Configuration以及其他我们常用的如@Service、@Controller、@Repository等。 我们先来看看ACAC的几个构造方法:

无参ACAC():

public AnnotationConfigApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
ACAC(Class<?>… annotatedClasses):

public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
this();
register(annotatedClasses);
refresh();
}
ACAC(String… basePackages):

public AnnotationConfigApplicationContext(String... basePackages) {
this();
scan(basePackages);
refresh();
}

我们观察到在构造ACAC时,一个reader和一个scaner被同时创建,其中reader为AnnotatedBeanDefinitionReader对象,scanner为ClassPathBeanDefinitionScanner对象,它们的作用实际上很简单,我们先来看看register()和scan()方法的代码:

register(Class<?>… annotatedClasses):

public void register(Class<?>... annotatedClasses) {
Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified");
this.reader.register(annotatedClasses);
}
scan(String… bashPackages):

public void scan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
this.scanner.scan(basePackages);
}

可以看到上面两个方法分别调用了AnnotatedBeanDefinitionReader的register()和ClassPathBeanDefinitionScanner的scan()方法。实际上,二者的目标是一致的,都是去找到Spring注解标记的类,来生成并注册bean到容器内。不同的是,AnnotatedBeanDefinitionReader的register是指定要注册bean的类,而ClassPathBeanDefinitionScanner的scan则会自动扫描给定包路径下的所有的类,依次处理被注解标记的类。

我们前面说过@Configuration也整合了@Component注解,因此它也是会被scan方法扫描到或是register方法直接处理的。

需要注意的是,这里并不是特殊处理@Configuration注解的地方,至于Spring框架具体处理@Configuration注解的类,就不在此深挖了,有兴趣的话大家可以自己从ACAC的注册、扫描bean方法作为切入点研究一下。

下面是一段以AppConfig作为ApplicationContext配置的容器初始化启动代码:

public class Chapter2s1Application {
public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
}
}

其中AppConfig.class就是前面在讲注解时,使用@Configuration和@Bean配合使用来替代applicationContext.xml内容的JavaConfig类。

如果非要写一个与之相对使用appConfig.xml作为配置的ClassPathXmlApplicationContext来作为对比,那么内容也是类似的:

public class Chapter2s1Application {
public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:appConfig.xml");
}
}

当然你可能会想,既然ACAC提供了扫描包的功能,那么我还是用AppConfig类来配置框架,但使用ClassPathXmlApplicationContext来替代ACAC启动,同时在xml里使用扫描功能

<context:component-scan base-package="AppConfig类所在包"/> 或是写一个AppConfig类的Bean定义,加上使用context:annotation-config/开启注解扫描

结果是不是也一样?答案是这种用法没有任何问题,它是JavaConfig作为xml配置补充的实现方式之一。JavaConfig配合xml文件对框架进行配置的内容,在后面我会详细地介绍。

另外,我们观察到在调用register()和scan()方法后,代码都使用了refresh()方法来刷新,refresh()方法主要是进行所有bean工厂的预处理、创建等工作,由于ACAC继承的org.springframework.context.support.GenericApplicationContext不支持多次多次调用refresh()方法,因此多次调用refresh()时是会报错的,如下代码:

public class Chapter2s1Application {
public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

    context.register(WebConfig.class);
    context.refresh();//报错:GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once
}
}

如果项目里有多次调用register()或scan()的需求,可以通过使用一个无参构造器,加上多次调用register()或scan(),最后调用refresh()实现,如下代码:

public class Chapter2s1Application {
public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

    context.register(AppConfig.class,WebConfig.class);
    context.scan("com.mcsoft.service","com.mcsoft.dao");
    context.refresh();
}
}

与JavaConfig紧密相关的其他注解 与配置有关的注解还有@Import、@ImportSource、@ComponentScan、@Profile、@Conditional、@Scope、@Lazy、@DependsOn、@Primary,这里会一一介绍它们,可能会有遗漏,如果未来学到了会再补充的。

@Import 我们在使用xml类型的配置时,为了配置文件的整洁,一般会分出多个面向不同方向不同模块的xml配置文件,然后再在入口xml配置里使用来集中起来,这个@Import对应的就是xml配置里的元素。使用时可以指定一个或多个其他JavaConfig类,Spring容器会对@Bean标记的方法进行处理,注意Spring处理的是@Bean方法,所以@Import指定的类不是必需要使用@Configuration注解标记。

@Import只能标记在类上或是被整合到其他注解里。

@Import只有一个属性:Class<?>[] value()。因此使用时以数组形式指定好要引入的其他@Configuration类就行了。

下面是一则使用方法示例:

@Configuration
@Import({WebConfig.class})
public class AppConfig {
//bean definition...
}

该JavaConfig类似于xml配置的

同时由于Spring容器是以bean形式管理@Configuration标记的类,因此可以将被import的类的bean注入进来,并且引用其他类定义好的bean:

//注意这是官方示例,实例里使用了构造器注入,实际上也可以使用set注入或是@Resource或@Autowired等注解标记实例变量来进行注入。

@Configuration
@Import({DatabaseConfig.class})
public class AppConfig {

private final DatabaseConfig dataConfig;

public AppConfig(DatabaseConfig dataConfig) {
    this.dataConfig = dataConfig;
}

@Bean
public MyBean myBean() {
    return new MyBean(dataConfig.dataSource());
}
}

@Configuration
public class DatabaseConfig{
@Bean
public DataSource dataSource(){
    return new DataSource();
}
}

前面我们讲过,Spring容器会利用AOP功能,在调用其他@Bean方法时去容器内查找相关的bean,再根据@Scope设定来返回一个bean对象,这里虽然跨越了不同的类,但也是同理的。

当然,我们也可以直接将@Bean方法创建的bean注入进来,如下代码:

@Configuration
@Import({DatabaseConfig.class})
public class AppConfig {

@Resource
private final DataSource dataSource;

@Bean
public MyBean myBean() {
    return new MyBean(dataSource);
}
}

@Configuration
public class DatabaseConfig{
@Bean
public DataSource dataSource(){
    return new DataSource();
}
}

@ImportSource @ImportSource只能标记在类上,用于直接引入xml配置,Spring容器会处理这些xml配置,这也是JavaConfig和XmlConfig相结合的实现方式之一。

路径可以使用’classpath:’、’file:’。被引入的xml配置里的bean可以直接通过依赖注入的方式注入到bean内。

@ImportSource可以指定一个或多个xml配置文件。

官方示例如下:

@Configuration
@ImportResource("classpath:/com/acme/database-config.xml")//database-config.xml定义了一个名为dataSource的bean
public class AppConfig {

@Inject DataSource dataSource; // from XML

@Bean
public MyBean myBean() {
    // inject the XML-defined dataSource bean
    return new MyBean(this.dataSource);
}
}

@ComponentScan @ComponentScan只能标记在类上,类似xml配置的context:component-scan/,该注解能指定一个或多个要扫描的包路径,当没有指定路径时,会从该类所在的包开始进行扫描。

@ComponentScan主要有三个属性:value,注解的默认属性,是basePackages属性的别名; basePackages,指定一个或多个静态字符串形式的包路径;basePackageClasses,指定一个或多个Class,会从指定的Class所在包开始扫描,这种方式可以保证包路径必然存在,否则在编译时就会提示找不到Class,是一种类型安全的做法。

示例如下,下面三条用法结果是一致的:

@ComponentScan({"com.mcsoft.services1","com.mcsoft.services2"}) @ComponentScan(basePackages = {"com.mcsoft.services1","com.mcsoft.services2"}) @ComponentScan(basePackageClasses = {com.mcsoft.services1.Service1.class,com.mcsoft.services2.Service2.class}) 如果你有使用basePackageClasses的需求,官方推荐的做法是在包内定义一个空的仅用于指定包路径的Class,这样不会影响代码,也能保证包路径安全。

该注解官方示例如下:

@Configuration
@ComponentScan("com.acme.app.services")
public class AppConfig {
// various @Bean definitions ...
}

注意,由于@Configuration也能被包扫描感知到并由Spring容器将其作为配置类正确处理,因此同时使用@Import和@ComponentScan可能会显得有些冗余,但@ComponentScan主要功能是扫描被Spring注解标记的类,而@Import主要功能是引入有@Bean定义的类,二者的功能在逻辑上还是有所不同的,我个人觉得都保留下来是比较好的做法。

@Profile @Profile及Environment接口是Spring里一个较抽象的概念和一个完整的体系,在这里我以自己理解尽量解释清楚,并且紧密围绕着配置来讲。

几乎所有程序都需要进行配置,我们可以认为这里的Environment就是程序运行环境的抽象,包括内部、外部配置的一个抽象,而@Profile是用于指定多套配置的配置名时使用的注解。

举个例子,我们有一套开发环境的配置和一套生产环境的配置,假如我们希望能通过一个简单的方式,在开发时使用开发配置,在上线后使用生产环境配置,那么通过@Profile和Environment相结合,使用@Profile来标记配置名,再用Environment指定使用哪套配置就可以实现这种方式。

@Profile只有一个默认的java.lang.String[]类型的属性value,因此它可以指定一个或多个配置名。

@Profile有三种使用方式:

标记在@Configuration类上。 标记在@Bean方法上。 与其他注解整合使用,类似@Configuration整合了@Component的方式。 下面是@Profile使用方式的例子,这里假设有两套配置应用于开发和生产环境,分别对应配置名为”development”和”production”:

//标记在@Configuration类上

@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("classpath:com/bank/config/sql/schema.sql")
        .addScript("classpath:com/bank/config/sql/test-data.sql")
        .build();
}
}

@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}

//标记在@Bean方法上

@Configuration
public class AppConfig{
@Bean("dataSource")
@Profile("development")
public DataSource standaloneDataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("classpath:com/bank/config/sql/schema.sql")
        .addScript("classpath:com/bank/config/sql/test-data.sql")
        .build();
}

@Bean("dataSource")
@Profile("production")
public DataSource jndiDataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}

//与其他注解整合

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

并且@Profile也有一个与之对应的xml配置项”profile”,可以作为属性配置在内,如下例:

现在已经指定好配置的名字了,下面是启用配置的方法。

启用配置有两种形式,一种是通过ConfigurableEnvironment的setActiveProfiles(java.lang.String… profiles)方法来指定一个或多个配置名,另一种是通过程序运行参数来指定一个或多个配置名。

注意,不管是哪种方法,当需要有多个配置同时生效时,都可以使用逗号”,”来分割多个配置名。

首先我们来看通过编程的方式来指定运行配置:

在这种方式下,我们需要使用一个无参的AnnotationConfigApplicationContext构造器,然后在通过的ConfigurableEnvironment.setActiveProfiles()方法设定好配置名后,使用AnnotationConfigApplicationContext的register()或scan()方法注册beans,最后调用refresh()方法。

一定要注意先后顺序,ConfigurableEnvironment.setActiveProfiles()方法一定要在register()、scan()、refresh()之前,因为ACAC不支持多次调用refresh()方法,所以我们要用无参构造器。如果我们配置好了@Profile,但先调用了register()或scan()再调用setActiveProfiles(),那么除了配置名为”default”的配置或没有配置@Profile的配置外,其他的配置名都不会被Spring容器处理。

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

ConfigurableEnvironment env = context.getEnvironment(); env.setActiveProfiles("development"); ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); ctx.refresh(); 在这里,我们把所有配置都塞进register()方法里,但由于已经设定了启用的配置名,因此框架会检测@Profile注解,匹配的名字才会去处理,没有匹配的则会被抛弃。

注意,只有使用了@Profile注解的部分才会被匹配处理,没有使用@Profile注解的部分则会被正常处理,利用这个特性,我们只需要用@Profile标记多个环境下不同的配置部分,而共用的配置则不用@Profile标记,来避免相同配置重复多次。

再来看看使用程序运行参数来指定配置的方式:

我们需要通过参数spring.profiles.active来指定运行时使用的配置,这个参数可以配置在操作系统环境变量、JVM运行参数、web.xml里的Servlet Context Param等处。

比如在运行项目时,使用下面的参数来指定:

java -Dspring.profiles.active="p1,p2" ... 程序名 另外,在集成测试时,使用spring-test组件的@ActiveProfiles注解也可以指定运行时的配置名。

@Profile还有另外两个特性,在这里简单描述下:

如果没有设置启用的配置名,则默认配置名为”default”,此时标为”default”和没有@Profile注解的配置会被启用。 如果配置名前加了叹号’!’,则表示当指定的配置名没有启用时,则启用该配置。比如@Profile({“p1”,”!p2”}),当指定配置名为p1或配置名p2没有启用时,该配置就会被启用。 @Conditional @Conditional允许在通过注解创建bean时,增加验证创建条件的功能,以此来控制一个bean是否真正被创建。

@Conditional有三种用法:1.标记在类上;2.标记在方法上;3.和其他注解整合。

@Conditional注解只有一个强制属性java.lang.Class<? extends Condition>[] value,也就是要传入一个或多个Condition接口实现类的类对象。

@Conditional需要和Condition接口配合使用才能实现bean创建的控制。

实现Condition接口需要实现方法boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata),该方法用于通过注解创建bean时控制是否进行创建,方法返回为true时则允许创建,为false时则不允许创建。

matches()方法的两个参数在Spring处理注解时会传入,其中ConditionContext对象可以视为一个专门用于Condition实现类的ApplicationContext对象,通过它可以获取到Spring框架运行的多种具体构成,它主要提供了下面这几种方法:

BeanDefinitionRegistry getRegistry(); @Nullable ConfigurableListableBeanFactory getBeanFactory(); Environment getEnvironment(); ResourceLoader getResourceLoader(); @Nullable ClassLoader getClassLoader(); 举个例子,我们可以通过getEnvironment()方法来获取到项目运行参数,前面的@Profile就是利用了这个方法来获取spring.profiles.active的参数,后面我会详细说明。

而AnnotatedTypeMetadata对象则可以用于获取被@Conditional标记的类或方法的注解集合,它主要提供了下面这几种方法:

boolean isAnnotated(String annotationName); @Nullable Map<String, Object> getAnnotationAttributes(String annotationName); @Nullable Map<String, Object> getAnnotationAttributes(String annotationName, boolean classValuesAsString); @Nullable MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName); @Nullable MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName, boolean classValuesAsString); 在下面的例子里,我们要使用ACAC来创建一个UserService类的bean,由于ACAC的方法都是基于注解的,因此通过ACAC创建bean是受到@Conditional注解控制的,我们模仿@Profile的做法,设定一个名为”condition.mc”的JVM参数,当他为”1”时创建则UserService的bean,下面我们看看具体的代码:

首先实现一个Condition接口

public class UserServiceCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    Environment env = context.getEnvironment();
    String conditionMC = env.getProperty("condition.mc");
    return "1".equals(conditionMC);
}
}

接着实现UserService,并将@Service、@Conditional注解标记上去,这里我们为了验证bean创建成功,就只是简单地覆盖toString()方法

@Service
@Conditional(UserServiceCondition.class)
public class UserService {

@Override
public String toString() {
    return "userService bean created";
}
}

最后我们在程序入口处使用ACAC来创建bean(使用ClassPathXmlApplicationContext,并使用注解扫描式创建bean也没问题,只要保证bean是通过注解式创建就行)

public class conditionalApplication {
public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(UserService.class);
    
    UserService userService = (UserService) context.getBean("userService");

    System.out.println(userService);
}
}

此时运行,会提示 No bean named 'userService' available,接着我们增加一个JVM参数-Dcondition.mc=1,控制台打印userService bean created,我们的Conditional注解使用成功。

注意@Conditional只能在通过注解创建bean时才会生效,如果是xml文件里

这种情况@Conditional是无法生效的,因为这种创建bean的方式是完全和注解无关的。

实际上,前面提到过,@Profile注解就是通过整合@Conditional来实现启用配置的功能,见其源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

/**
 * The set of profiles for which the annotated component should be registered.
 */
String[] value();

}

可以看到@Conditional指定了一个ProfileCondition.class的参数,而ProfileCondition类的源码如下:

class ProfileCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    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;
}

}

我们可以看到代码中先是获取了@Profile注解的value属性值,然后利用Environment实现类中提供的acceptsProfiles((String[]) value)方法来校验参数与spring.profiles.active设定的值是否匹配,校验结果影响着最后bean是否进行创建。因此通过@Profile注解配合spring.profiles.active参数来启用不同配置,实际上是利用了@Conditional和Condition接口来实现的。

@Scope、@Lazy、@DependsOn、@Primary 这几个注解放在一起说,因为他们都是创建bean时使用的注解,可以配合@Bean注解来控制bean创建,也就是过去在xml配置的项:

就相当于下面的@Bean方法:

@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@DependsOn("user1")
@Lazy
@Primary
public UserService userService() {
UserService service = new UserServiceImpl();
service.setUser(user1());
return service;
}

它们也可以标记在类上,和@Component、@Service、@Controller、@Configuration这样的注解共同使用,来控制bean创建。

所以上面的例子又相当于下面这个类:

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@DependsOn("user1")
@Lazy
@Primary
public class UserServiceImpl implements UserService{
@Autowire
private User user1;
}

在被扫描创建bean时,这几个注解会控制创建时的行为。

简述一下这几个注解的行为:

Scope:定义获取bean时,bean工厂是返回单例对象(singleton),还是每次都返回新对象(prototype),web项目中使用WebApplicationContext时还可以设定为”session”及”request”,表示每个新session创建一个新bean,或是每个新请求创建一个新bean。 DependsOn:如果我们创建一个bean时,需要保证另一个bean一定已经被创建,就可以使用该注解指定那个bean的name。这个和依赖注入时差不多,容器在依赖注入时会保证被注入的bean先创建好再注入,但这个配置可以指定依赖注入以外的bean。 Lazy:延迟加载控制,用于控制Scope为singleton的bean创建时机,默认为true,Spring容器在启动时不会创建bean,而是在第一次使用时创建,注解在@Configuration类上时,所有内部的@Bean方法都会设为延迟加载。但如果一个非延迟化的singleton bean依赖一个延迟化的singleton bean时,容器也会忽视延迟加载,直接创建bean。 Primary:依赖注入使用byType策略时,如果被注入的接口有多个实现类的bean,容器根据byType无法确定要注入接口的哪个实现类的bean,这时可以用@Primary标记其中一个实现类,这样注入时就会优先注入该类。 AnnotationConfigWebApplicationContext介绍 Spring中,ApplicationContext的实现既可以作为框架启动器存在,也可以和应用的部署形式相结合,,在XML时代,

JavaConfig和xml配置结合的两种形式 现在我们的项目除了完全使用JavaConfig、完全使用xml配置外,还可以实现JavaConfig结合xml对Spring进行配置,而这种形式又分为以JavaConfig为中心及以xml配置为中心两种形式。

首先我们定义一个User类,拿他来创建Bean

public class User {
private String name;

public User() {
}

public User(String name) {
    this.name = name;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

@Override
public String toString() {
    return "User{name='" + name + "\'}";
}
}

接着我们定义JavaConfig和xml配置

@Configuration
//@ImportResource("classpath:applicationContext.xml")
public class AppConfig {
@Bean
public User user1() {
    return new User("u1");
}

@Bean
public User user2() {
    return new User("u2");
}
}

<!--<context:annotation-config/>-->
<!--<bean class="com.mcsoft.config.AppConfig"/>-->

<bean id="user3" class="com.mcsoft.bean.User">
    <constructor-arg name="name" value="u3"/>
</bean>
JavaConfig-centric 以JavaConfig为中心时,我们主要使用@ImportResource注解来引入xml配置,将AppConfig类中的//@ImportResource("classpath:applicationContext.xml")注释取消,接着使用ACAC来启动程序
public class JavaConfigCentricApp {
public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

    User user1 = (User) context.getBean("user1");
    System.out.println(user1);

    User user2 = (User) context.getBean("user2");
    System.out.println(user2);

    User user3 = (User) context.getBean("user3");
    System.out.println(user3);
}
}

控制台正确打印:

User{name='u1'} User{name='u2'} User{name='u3'} Xml-centirc 以Xml配置为中心时,我们主要通过xml配置注解式创建bean来使@Configuration类生效,这里有两种方式:1.定义JavaConfig类的bean,然后使用context:annotation-config/开启注解扫描;2.使用context:component-scan/来扫描@Configuration类。

这里我们用第一种方式实现,首先将xml配置里的context:annotation-config/注释取消,接着使用ClassPathXmlApplicationContext来启动项目

public class XmlConfigCentricApp {
public static void main(String[] args) {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext" +
            ".xml");

    User user1 = (User) context.getBean("user1");
    System.out.println(user1);

    User user2 = (User) context.getBean("user2");
    System.out.println(user2);

    User user3 = (User) context.getBean("user3");
    System.out.println(user3);
}
}

控制台正确打印:

User{name='u1'} User{name='u2'} User{name='u3'} 总结 Spring 3.0 在原本的xml配置bean、注解式配置Bean基础上,增加了使用Java类配置bean的方式,它实际上还是通过注解式来实现的,但在控制Bean生产上粒度更加细致,可以在代码里由开发者控制一个Bean生产的过程。

本文主要讲述了通过使用JavaConfig提供Bean定义,供Spring容器创建Bean的方式。

实质上,无论是注解式还是xml配置,本质上都是一种提供Bean定义的载体,但由于形式不同,他们也各自拥有各自的特性。