基于注解的容器配置

注解式配置

注解一定比xml配置好吗?
不一定,取决于使用场景:注解提供更简洁的配置方式,xml文件更倾向于不侵入原有代码。
Spring都接受,甚至可以混用。
注解在xml之前被解析,所以xml配置会覆盖注解配置。

使用注解只需要在xml配置文件改成:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

</beans>

<context:annotation-config/>元素隐式注册以下post-processors:

ConfigurationClassPostProcessor
AutowiredAnnotationBeanPostProcessor
CommonAnnotationBeanPostProcessor
PersistenceAnnotationBeanPostProcessor
EventListenerMethodProcessor

<context:annotation-config/>元素只会在同一个容器内寻找bean,
意思是ApplicationContext不会在WebApplicationContext里面寻找bean。

@Required注解:用于bean的setter方法。
在bean配置的时候必须要有对应的属性值,否则报异常。可以避免NPE(NullPointerException)。
(咦,为什么是空指针异常不是空引用异常?)
还需要将RequiredAnnotationBeanPostProcessor注册成bean才能生效。
已废弃,可以通过构造器参数注入确保强制执行。

@Autowired注解:可以用于构造器:

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

也可以用于有多个参数的方法:

public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

从Spring4.3开始,在只有一个构造器的情况下没必要添加。
有多个构造器但是没有默认构造器就需要添加此注解告诉容器用哪个。

也可以将此注解用于setter方法:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

此注解甚至可以同时用于方法和用于变量:

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

甚至可以让Spring将所有类型符合的bean打包成一个数组:

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}

数组里的bean可以通过实现org.springframework.core.Ordered接口或@Order/@Priority完成排序。
否则就按定义顺序添加进数组。

强类型Map同样可以,key就是bean的名字。

@Autowired注解默认强制注入,如果没有匹配,就会报异常。
可通过设置required属性表示非必需:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired(required = false)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

对于构造器和工厂方法required属性的意义会有点不一样:对于含有多个bean的注入点,
如arrays, collections, maps,当没有合适的bean时会解析成空的实例。
只有一个构造方法可以被设置成@Autowired(required = true)
如果要声明多个构造器,就要改成@Autowired(required = false),以作为候选。

也可以通过Java 8 的java.util.Optional工具实现non-required。
Spring 5.0以上也可以通过@Nullable实现。

@Autowired@Inject@Value,和@Resource注解都是通过BeanPostProcessor实现的,不可以将这些注解用于BeanPostProcessorBeanFactoryPostProcessor类型。

通过@Primary进行更好的调整:

如果有多个匹配项时就选有此注解的那个:

@Configuration
public class MovieConfiguration {

    @Bean
    @Primary
    public MovieCatalog firstMovieCatalog() { ... }

    @Bean
    public MovieCatalog secondMovieCatalog() { ... }

    @Autowired
    private MovieCatalog movieCatalog;
}

上面的例子中firstMovieCatalog方法胜出。

在xml配置文件中对应<bean/>元素的primary属性,值为true:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog" primary="true"/>

    <bean class="example.SimpleMovieCatalog"/>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

通过@Qualifier进行更好的调整:

如果在有@Primary的bean可用时想选别的bean,可用它缩小匹配范围:

public class MovieRecommender {

    @Autowired
    @Qualifier("main")
    private MovieCatalog movieCatalog;

    // ...
}

也可以用于构造器参数或者方法参数:

public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(@Qualifier("main") MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

在xml配置文件中使用<bean/>元素的<qualifier/>子元素:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="main"/> 
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="action"/> 
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

默认使用bean的名称作为依据,即<bean/>元素的id属性。
和@Autowired一起用的时候会在相同类型的候选中选id匹配的bean。
好的qualifier名称应该表达bean的特点,并独立于id。
同样可以将所有匹配到的bean注入成集合。

这就暗示qualifiers并不需要是不同的,例如:
定义多个MovieCatalog类型bean,都含有相同的qualifier值:action,
都会被注入由@Qualifier(“action”)注释的Set<MovieCatalog>。

在同一类型候选范围内使用qualifier值选择而不是bean名称,可以不用在注入点添加@Qualifier注解。
在没有解析指示符而且有多个匹配的情况下,Spring会选择name和注入点名称相同的那个bean。

如果想在注解配置下通过name自动注入,就不应该主用@Autowired,虽然它也可以。
应该使用JSR-250 @Resource注解,在语义上就被定义成通过组件的名称确定匹配项,和类型无关。
@Autowired有完全不同的语义:在通过类型选择后,在这些候选里面选择qualifier值匹配的那个。
(类型有关)

对于类型是集合、Map或数组的bean来说,@Resource是一个好的选择,因为是通过名称匹配的。
从Spring4.3开始,@Autowired也能注入集合,只要类型信息通过@Bean函数签名或集合继承结构返回。

从Spring4.3开始,@Autowired也考虑自注入(引用已经注入的bean)。
常规的依赖之间有优先级,自注入不参加选择过程,所以就不会是primary的。
相反地,自注入总是最低优先级。自注入仅作为最后手段使用。

在@Configuration类内注入在这个类内定义的@Bean方法同样是自引用。
在方法签名内懒惰引用或者将@Bean方法定义成static都会导致和@Configuration类和其生命周期解耦。
如果不这么做,这种bean仅在fallback阶段会被考虑,而且会将别的@Configuration类作为primary。

@Autowired可被用于变量、构造器和多个参数的方法,作为区别,
@Resource仅支持用于变量和一个参数的setter方法。
所以对于构造器和多参数方法时最好使用qualifier。

用Java泛型作为Qualifier

可以注入泛型接口:

@Configuration
public class MyConfiguration {

    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}

假设StringStore实现了Store<String>接口、IntegerStore实现了Store<Integer>接口:

@Autowired
private Store<String> s1; // <String> qualifier,注入stringStore

@Autowired
private Store<Integer> s2; //<Integer> qualifier,注入integerStore

同样支持集合类型:

@Autowired
private List<Store<Integer>> s;
// Store<String> beans不会被注入

@Resource注解注入

Spring同样支持JSR-250 @Resource 注解(javax.annotation.Resource),作用在变量或者setter方法。

@Resource接受一个name属性,Spring默认将其解析成要被注入的bean名称:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") 
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

如果不提供那么属性,那就使用从变量名或者setter方法的参数名获得:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

上面的例子中@Resource会寻找名为movieFinder的bean。
名字必须是ApplicationContext内的CommonAnnotationBeanPostProcessor知道的。

name属性可以通过JNDI解析,如果明确设置SimpleJndiBeanFactory的话。

在解析某些众所周知的依赖(BeanFactoryApplicationContextResourceLoaderApplicationEventPublisher, and MessageSource接口)的时候,如果没有提供明确的name属性,就会按照类型匹配。

使用@Value注解

通常用于注入外部属性:

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name}") String catalog) {
        this.catalog = catalog;
    }
}

通过@PropertySource注解指定数据源:

@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }

然后新建或者编辑application.properties文件:

catalog.name=MovieCatalog

运行时,就会将上面String catalog变量的值设置成MovieCatalog。

${catalog.name}被称作占位符,
如果无法解析,默认就会将整个占位符注入到变量,如${catalog.name}。

可以通过声明一个PropertySourcesPlaceholderConfigurer类型bean,改成严格的控制策略,无法解析占位符将会导致初始化失败。(在Spring Boot中成为默认初始化策略):

@Configuration
public class AppConfig {

     @Bean
     public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
           return new PropertySourcesPlaceholderConfigurer();
     }
}

注意:这个bean方法必需是static的。
这个bean同样提供setPlaceholderPrefixsetPlaceholderSuffix,或setValueSeparator等方法自定义占位符。

Spring Boot中的PropertySourcesPlaceholderConfigurer默认会从application.propertiesapplication.yml文件读取属性信息。

Spring内置的converter允许自动转换简单类型:如转换成Integer或int,将多个用逗号隔开的值转换成String类型的Array。

可以为占位符提供一个默认值:

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
        this.catalog = catalog;
    }
}

当catalog.name无法解析时就将String catalog的值设置成defaultCatalog。

Spring使用ConversionService类型的BeanPostProcessor将字符串转换成对应参数的类型。
可以提供自定义的ConversionService来转换自定义类型:

@Configuration
public class AppConfig {

    @Bean
    public ConversionService conversionService() {
        DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
        conversionService.addConverter(new MyCustomConverter());
        return conversionService;
    }
}

当@Value含有SpEL时,在运行时会自动计算结果,并注入结果:

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
        this.catalog = catalog;
    }
}

@PostConstruct@PreDestroy

CommonAnnotationBeanPostProcessor同样可以识别JSR-250生命周期注解:
javax.annotation.PostConstructjavax.annotation.PreDestroy

和生命周期方法一样,只不过是注解形式。

留下评论

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