基于最新Spring 5.x,详细介绍了基于注解和Java Config的核心IoC机制的配置和使用。
本次我们介绍基于注解和Java Config的Spring IoC机制的核心配置方式,包括bean的配置、依赖项注入配置,以及各种常用配置信息。没有讲过多源码,提供了大量的案例,对于会使用Spring的人来说可能比较啰嗦,但是比较适合Spring初学者!关于Spring IoC的XML配置:Spring 5.x 学习(2)—两万字的IoC入门以及基于XML的IoC配置全解。
Spring基于注解的配置提供了基于XML配置的替代方案,它依赖注解(基于字节码元数据和反射)获取信息并描述bean,而不是使用XML标签来声明bean。开发人员不需要写XML文件,而是使用注解标注在相关类、方法或字段声明上。很明显,注解将配置信息从XML文件中移动到组件类本身。
基于注解的配置的引入提出了一个问题,即这种方法是否比XML配置"更好"。 简短的回答是"视情况而定"。更好的回答是,每种方法都有其优缺点,通常由开发人员决定哪种策略更适合他们。由于它们的定义方式,注释在声明中提供了大量上下文,从而导致配置更短、更简洁。但是,XML擅长在不接触其源代码或重新编译组件的情况下配置组件信息。比如对于外部jar包中的类,此时不能在源码上加入注解,那就只能使用XML配置。一些开发人员喜欢将配置靠近源代码,而另一些开发人员则认为带注解的类不再是纯粹的POJO。此外,注解配置的一个缺点是配置信息变得分散且难以控制。
我们可以同时使用注解和XML配置,注解注入在XML注入之前执行。因此,当两者同时对相同的某些属性使用时,XML配置会覆盖注解注入的配置。
随着Spring版本的不断更新,对于注解的支持也在不断进步,一些最早的注解被抛弃了,另一些更强大的注解被推荐使用,到目前(Spring 5.x),已经提供了大量的注解方便开发者使用。本文将讲解其中用的最常用的一些最基本的IoC容器的注解,对于其他注解,我们会在后续相关的文章中一一介绍。
例如,在Spring 2.0的时候,曾提供了@Required注解,用于标注bean的属性对应的setter方法,它表示受影响的属性必须在配置时必须在XML中配置注入或者自动注入。如果未注入受影响的bean属性,则容器将抛出BeanInitializationException异常。然而从Spring5.1开始,@Required注解正式被弃用,因为Spring建议将必须的依赖通过构造器注入或使用初始化回调方式注入。Spring2.5还增加了对JSR-250注解的支持,如@PostConstruct和@PreDstroy。Spring 3则为javax.inject包中包含的JSR-330(Java依赖注入)注解添加了支持,例如@Inject和@Named。
学习基于注解的IoC配置之前,我强烈建议学习Java注解的基本原理:深入理解Java中的注解的概念、用法以及案例演示。同时本文会有和基于XML配置的对照,Spring 5.x 学习(2)—两万字的IoC入门以及基于XML的IoC配置全解。
开启注解支持的快捷方式是在原始的XML配置文件中加入context Schema约束。
原始的Spring XML配置文件:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
beans>
加入context Schema约束之后的配置文件:
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
beans>
随后我们需要开启注解支持配置,简单的说,开启检测、处理某些注解的功能。仅仅对于Spring Ioc核心注解支持,这里有两个配置可选:< context:annotation-config />和< context:component-scan base-package="" />,它们有什么区别呢?
< context:annotation-config/ >用于对已注册在容器中bean的内部进行注解检测、激活,对于bean的注册注解则不检测,因此可能还需要使用XML或者其他方式将bean交给IoC容器之后,该annotation-config才会对容器中的bean的相关内部注解进行激活控制。
这个配置实际上是帮助我们尝试向容器注册AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor、PersistenceAnnotationBeanPostProcessor、RequiredAnnotationBeanPostProcessor这四个BeanPostProcessor的bean,不同的BeanPostProcessor用于检测、处理各自对应的的注解服务,相当于注解处理器。常见< context:annotation-config/ >可激活的注解如下:
注意:不会激活Spring的@Transactional注解;可以使用
<context:annotation-config/>
<bean id="autowiredAnnotationBeanPostProcessor" class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor "/>
<bean id="commonAnnotationBeanPostProcessor" class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor"/>
<bean id="requiredAnnotationBeanPostProcessor" class="org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor"/>
<bean id="persistenceAnnotationBeanPostProcessor" class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
< context:component-scan base-package=""/ >具有context:annotation-config的全部功能。另外,必须要指定一个base-package属性,表示将会自动扫描的指定的包路径下面的所有类,并将带有注册注解的bean注册到IoC容器中。默认情况下,将检测Spring @Component、@Repository、@Service、@Controller、@RestController、@ControllerAdvice 和@Configuration等注解。
通常,我们不会在XML中注册bean,而在代码中使用注解注入依赖,因此我们只需要使用< context:component-scan base-package=""/ >就行了。可以配置多个component-scan 标签,当然也可以在base-package属性中配置扫描多个包路径,可以使用“ ”、“,”、“;”等符号分隔,将会扫描该路径下的所有子包的类。
另外,可以使用通配符 * 。一个 * 就表示其中一级路径,两个 ** 就表示其中多级路径或者没有路径。比如base-package="com. * .core. * ",表示扫描com.xx.core包路径的core包下面的所有的子包下面的类。base-package="com. ** .core. ** ",表示扫描具有com和core包路径的core包下面的类和core包的所有的子包下面的类,等价于base-package=“com.**.core”。超过两个 * 就和一个 * 的含义一样!
另外还有其他的属性,后面再介绍!
@Component注解可以标注在类上。IoC容器启动时,会自动在component-scan配置的包范围之中扫描被@Component注解标注的类,将它们注册到IoC容器中,我们称这些类为“组件”,就是相当于XML的< bean >标签,即定义了一个bean,并且交给容器。
通常,任何需要交给Spring管理的组件(bean)都可以使用@Component注解标注。自Spring2.5起,在web分层项目架构中,为了区分某个Spring管理的组件属于哪一层架构,因此根据@Component注解衍生出来了三个注解:@Repository、@Service和@Controller。它们默认都是单例bean。
@Controller用于标注表示层的组件,@Service用于标注服务层的组件,@Repository用于标注持久层的组件。当组件不好归类的时候,才建议使用@Component。
目前来看这几个注解没有太多实际意义的差别,但是未来的Spring版本将可能对这三个衍生组件进行增强,比如目前@Repository已经被支持作为持久性层中自动异常转换的标记。
@Component、@Repository、@Service和@Controller将会使用类似XML的构造函数的方式实例化bean。实际上基于注解的配置相比于XML的配置更加灵活,也有许多不同的地方,我们后面会讲到!
新建一个空的maven工程,引入如下依赖:
<properties>
<spring-framework.version>5.2.8.RELEASEspring-framework.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>${spring-framework.version}version>
<scope>compilescope>
dependency>
dependencies>
添加spring-config.xml配置文件,开启注解支持:
<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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.spring.core.*"/>
beans>
新建ComponentOne类,用于测试@Component注解
/**
* @author lx
*/
@Component("componentOne")
public class ComponentOne {
public ComponentOne() {
System.out.println("ComponentOne:" + this);
}
@Component("componentTwo")
public class ComponentTwo {
public ComponentTwo() {
System.out.println("ComponentTwo:" + this);
}
}
@Component("componentThree")
public static class ComponentThree {
public ComponentThree() {
System.out.println("ComponentThree:" + this);
}
}
}
Annotation类用于测试:
@Test
public void anno() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("componentOne", ComponentOne.class));
System.out.println(ac.getBean("componentOne", ComponentOne.class));
System.out.println(ac.getBean("componentTwo", ComponentOne.ComponentTwo.class));
System.out.println(ac.getBean("componentTwo", ComponentOne.ComponentTwo.class));
System.out.println(ac.getBean("componentThree", ComponentOne.ComponentThree.class));
System.out.println(ac.getBean("componentThree", ComponentOne.ComponentThree.class));
}
结果如下,可以看到,成功从容器获取了bean实例,并且默认都是单例的bean实例:
ComponentThree:com.spring.core.ann.ComponentOne$ComponentThree@145eaa29
ComponentOne:com.spring.core.ann.ComponentOne@275710fc
ComponentTwo:com.spring.core.ann.ComponentOne$ComponentTwo@17c386de
com.spring.core.ann.ComponentOne@275710fc
com.spring.core.ann.ComponentOne@275710fc
com.spring.core.ann.ComponentOne$ComponentTwo@17c386de
com.spring.core.ann.ComponentOne$ComponentTwo@17c386de
com.spring.core.ann.ComponentOne$ComponentThree@145eaa29
com.spring.core.ann.ComponentOne$ComponentThree@145eaa29
在扫描到一个被托管的组件时,它的对应的bean名称由BeanNameGenerator策略和@Component、@Repository、@Service、@Controller注解指定的value值一起生成。如果注解设置了value值,那么value值作为bean的名字,外部类bean name 不能重复;普通内部类之间bean name 可以重复,启动时不会异常,但是不建议重复,可能造成bean覆盖,在运行时抛出异常;静态内部类之间bean name 不能重复。因此,指定的bean name都不应该重复。
如果没有指定value值和BeanNameGenerator,默认bean name生成的策略为:
案例如下,一个ComponentName类,仅仅使用@Component注解,不设置value值,不设置BeanNameGenerator,用于测试默认命名。
/**
* @author lx
*/
@Component
public class ComponentName {
public ComponentName() {
System.out.println("ComponentName:" + this);
}
@Component
public class ComponentNameTwo {
public ComponentNameTwo() {
System.out.println("ComponentNameTwo:" + this);
}
}
@Component
public static class ComponentNameThree {
public ComponentNameThree() {
System.out.println("ComponentNameThree:" + this);
}
}
}
@Component
public class COOmponentName {
public COOmponentName() {
System.out.println("COOmponentName:" + this);
}
}
@Component
public class COOmponentName {
public COOmponentName() {
System.out.println("COOmponentName:" + this);
}
}
测试:
@Test
public void annoName() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println("--------------------");
//普通外部类,默认bean名称生成器将返回小写的非限定类名。
System.out.println(ac.getBean("componentName", ComponentName.class));
//普通外部类,如果类名有多个字符并且前两个字符都是大写时,将直接返回简单类名。
System.out.println(ac.getBean("COOmponentName", COOmponentName.class));
//普通内部类,默认bean名称生成器将返回全限定外部类名$内部类名。
System.out.println(ac.getBean("com.spring.core.ann.ComponentName$ComponentNameTwo", ComponentName.ComponentNameTwo.class));
//静态内部类,默认bean名称生成器将返回小写的非限定外部类名.内部类名。
System.out.println(ac.getBean("componentName.ComponentNameThree", ComponentName.ComponentNameThree.class));
}
结果如下,成功获取到了bean实例:
COOmponentName:com.spring.core.ann.COOmponentName@400cff1a
ComponentNameThree:com.spring.core.ann.ComponentName$ComponentNameThree@4c6e276e
ComponentName:com.spring.core.ann.ComponentName@534df152
ComponentNameTwo:com.spring.core.ann.ComponentName$ComponentNameTwo@3ee0fea4
--------------------
com.spring.core.ann.ComponentName@534df152
com.spring.core.ann.COOmponentName@400cff1a
com.spring.core.ann.ComponentName$ComponentNameTwo@3ee0fea4
com.spring.core.ann.ComponentName$ComponentNameThree@4c6e276e
如果不想依赖注解设置的value和默认的bean命名策略,那么我们可以提供一个自定义的bean name生成器。
首先,需要实现BeanNameGenerator接口,并确保该实现包含一个默认的无参数构造器。然后,在配置组件扫描器时,在name-generator属性上设置该生成器的完全限定的类名。
/**
* bean名称生成器
*
* @author lx
*/
public class MyNameGenerator implements BeanNameGenerator {
/**
* bean的命名接口
*
* @param definition 用于生成名称的bean的定义
* @param registry bean定义注册中心
* @return 自定义的bean name
*/
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
//简单的直接返回bean的类名,仅供测试
return definition.getBeanClassName();
}
}
配置文件,加入name-generator属性:
<context:component-scan base-package="com.spring.core.*" name-generator="com.spring.core.ann.MyNameGenerator"/>
再一次测试,bean name全部填写全限定类名:
@Test
public void annoGenerateName() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
//全部都要求全限定类名,则可以获取该bean的实例
System.out.println(ac.getBean("com.spring.core.ann.ComponentName", ComponentName.class));
System.out.println(ac.getBean("com.spring.core.ann.ComponentName", ComponentName.class));
System.out.println(ac.getBean("com.spring.core.ann.COOmponentName", COOmponentName.class));
System.out.println(ac.getBean("com.spring.core.ann.COOmponentName", COOmponentName.class));
System.out.println(ac.getBean("com.spring.core.ann.ComponentName$ComponentNameTwo", ComponentName.ComponentNameTwo.class));
System.out.println(ac.getBean("com.spring.core.ann.ComponentName$ComponentNameTwo", ComponentName.ComponentNameTwo.class));
System.out.println(ac.getBean("com.spring.core.ann.ComponentName$ComponentNameThree", ComponentName.ComponentNameThree.class));
System.out.println(ac.getBean("com.spring.core.ann.ComponentName$ComponentNameThree", ComponentName.ComponentNameThree.class));
}
结果如下,成功获取到bean实例:
COOmponentName:com.spring.core.ann.COOmponentName@57d5872c
ComponentNameThree:com.spring.core.ann.ComponentName$ComponentNameThree@3bbc39f8
ComponentName:com.spring.core.ann.ComponentName@4ae3c1cd
ComponentThree:com.spring.core.ann.ComponentOne$ComponentThree@29f69090
ComponentOne:com.spring.core.ann.ComponentOne@568bf312
ComponentNameTwo:com.spring.core.ann.ComponentName$ComponentNameTwo@4034c28c
ComponentTwo:com.spring.core.ann.ComponentOne$ComponentTwo@14ec4505
com.spring.core.ann.ComponentName@4ae3c1cd
com.spring.core.ann.ComponentName@4ae3c1cd
com.spring.core.ann.COOmponentName@57d5872c
com.spring.core.ann.COOmponentName@57d5872c
com.spring.core.ann.ComponentName$ComponentNameTwo@4034c28c
com.spring.core.ann.ComponentName$ComponentNameTwo@4034c28c
com.spring.core.ann.ComponentName$ComponentNameThree@3bbc39f8
com.spring.core.ann.ComponentName$ComponentNameThree@3bbc39f8
如果此时运行最开始的测试案例,则会抛出异常!一般情况下,我们要么通过value指定bean名值,要么使用默认命名规则,不建议自定义bean name生成器!
在Spring的一系列Component组件注解或者@Configuration配置注解标注的类的内部,可以使用@Bean注解标注方法,表示向IoC容器添加bean定义。
@Bean的作用同样类似于< bean >标签,和@Component系列注解的区别是相当于使用工厂模式实例化bean。即我们在方法中编写实例化bean的逻辑,然后返回该bean,这样bean就会被注册到IoC容器中,IoC容器会自动调用该方法创建bean实例。可以类比基于XML的工厂方法实例化bean那部分。
对于使用@Bean注册的bean,它的默认命名策略就是取方法名作为bean的名字。
@Bean一般用于注册外部jar包中的类,因为这些类不能的源码不能够改,不能添加注解,此时只有使用@Bean的方式了。实际上@Bean注解更多的使用在@Configuration配置注解类内部,@Bean里面也有很多配置可以设置,后面会讲到。
案例如下,一个BeanFactoryDemo类,该类本身被@Component注解标注,有一个被@Bean标注的方法:
/**
* @author lx
*/
@Component
public class BeanFactoryDemo {
// /**
// * 由于方法名为beanFactoryDemo,和@Component的命名策略冲突导致名字重复,进而抛出异常
// */
// @Bean
// public BeanFactoryDemo beanFactoryDemo() {
// return new BeanFactoryDemo();
// }
/**
* 使用@Bean的方式注册bean
*/
@Bean
public BeanFactoryDemo getBeanFactoryDemo() {
return new BeanFactoryDemo();
}
}
测试:
@Test
public void beanFactory() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
//这是@Component注册的bean
System.out.println(ac.getBean("beanFactoryDemo", BeanFactoryDemo.class));
//这是@Bean注册的bean
System.out.println(ac.getBean("getBeanFactoryDemo", BeanFactoryDemo.class));
}
结果如下,成功实例化:
com.spring.core.ann.BeanFactoryDemo@a4102b8
com.spring.core.ann.BeanFactoryDemo@11dc3715
基于注解的组件的默认作用域都是singleton单例,如果想要指定不同的作用域,可以使用@Scope注解,可以在注解中提供作用域的名称。
@Scope注解仅仅在一系列Component组件注解标注的类和@Bean标注的工厂方法上有效果,scope含有的的作用域和XML配置的作用域参数一致,就是相当于< bean >标签的scope属性,详情参考XML的配置文章。
通常,我们只会配置singleton和prototype两种作用域,甚至基本不会配置,默认就是singleton。如果要配置,这两个值在 ConfigurableBeanFactory 接口中有SCOPE_SINGLETON 和 SCOPE_PROTOTYPE定义好的字面常量值,我们可以直接使用,当然也可以直接写字符串。
案例如下,新建一个ScopeDemo类,用于测试@Scope注解:
/**
* @author lx
*/
@Component
public class ScopeDemo {
public ScopeDemo() {
System.out.println("ScopeDemo初始化:" + this);
}
/**
* 默认就是singleton
*/
@Component("scopeO")
public static class ScopeO {
public ScopeO() {
System.out.println("ScopeO初始化:" + this);
}
}
/**
* 加上注解不指定作用域也是默认singleton
*/
@Component("scopeA")
@Scope
public static class ScopeA {
public ScopeA() {
System.out.println("ScopeA初始化:" + this);
}
}
/**
* 可以通过ConfigurableBeanFactory.SCOPE_SINGLETON引用singleton字符串
*/
@Component("scopeB")
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public static class ScopeB {
public ScopeB() {
System.out.println("ScopeB初始化:" + this);
}
}
/**
* 可以通过ConfigurableBeanFactory.SCOPE_PROTOTYPE引用prototype字符串
*/
@Component("scopeC")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Qualifier
public static class ScopeC {
public ScopeC() {
System.out.println("ScopeC初始化:" + this);
}
}
/**
* 默认@Bean就是singleton
* 加上注解不指定作用域也是默认singleton
*/
@Bean
@Scope
public ScopeDemo getScopeDemo1() {
return new ScopeDemo();
}
/**
* 指定prototype
*/
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public ScopeDemo getScopeDemo2() {
return new ScopeDemo();
}
}
测试:
@Test
public void scope() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println("----@Component和@Bean singleton 的bean,在创建容器时就会初始化----");
System.out.println(ac.getBean("scopeO", ScopeDemo.ScopeO.class));
System.out.println(ac.getBean("scopeO", ScopeDemo.ScopeO.class));
System.out.println(ac.getBean("scopeA", ScopeDemo.ScopeA.class));
System.out.println(ac.getBean("scopeA", ScopeDemo.ScopeA.class));
System.out.println(ac.getBean("scopeB", ScopeDemo.ScopeB.class));
System.out.println(ac.getBean("scopeB", ScopeDemo.ScopeB.class));
System.out.println(ac.getBean("getScopeDemo1", ScopeDemo.class));
System.out.println(ac.getBean("getScopeDemo1", ScopeDemo.class));
System.out.println("----@Component和@Bean prototype的bean,在获取时才会初始化----");
System.out.println(ac.getBean("scopeC", ScopeDemo.ScopeC.class));
System.out.println(ac.getBean("scopeC", ScopeDemo.ScopeC.class));
System.out.println(ac.getBean("getScopeDemo2", ScopeDemo.class));
System.out.println(ac.getBean("getScopeDemo2", ScopeDemo.class));
}
结果如下,符合预期:
ScopeA初始化:com.spring.core.ann.ScopeDemo$ScopeA@6ee12bac
ScopeB初始化:com.spring.core.ann.ScopeDemo$ScopeB@ca263c2
ScopeO初始化:com.spring.core.ann.ScopeDemo$ScopeO@589b3632
com.spring.core.ann.ScopeDemo$ScopeO@589b3632
com.spring.core.ann.ScopeDemo$ScopeO@589b3632
com.spring.core.ann.ScopeDemo$ScopeA@6ee12bac
com.spring.core.ann.ScopeDemo$ScopeA@6ee12bac
com.spring.core.ann.ScopeDemo$ScopeB@ca263c2
com.spring.core.ann.ScopeDemo$ScopeB@ca263c2
ScopeC初始化:com.spring.core.ann.ScopeDemo$ScopeC@21507a04
com.spring.core.ann.ScopeDemo$ScopeC@21507a04
ScopeC初始化:com.spring.core.ann.ScopeDemo$ScopeC@143640d5
com.spring.core.ann.ScopeDemo$ScopeC@143640d5
当然,我们可以自定义scope注解解析器,实际上Spring的注解都有对应的注解解析器。自定义解析策略,只需要实现ScopeMetadataResolver接口即可,类似于上面的自定义bean name生成器,都是不建议使用的,实际上注解默认解析器就是AnnotationScopeMetadataResolver,是一个ScopeMetadataResolver接口的实现,有兴趣可以去看看。
前面我们讲了一些bean注册的注解,有@Repository、@Service、@Controller、@Bean,对于类或者方法是欧元那个这些注解表示将bean交给IoC容器管理,然后我们就可以从IoC容器中获取对应bean名称的实例对象了。我们同样可以使用注解实现属性注入。
@Autowired注解实际上就是Spring提供的一个依赖自动注入的注解,在此前基于XML的配置文章中,我们就曾经了解过自动注入,XML的自动注入都是通过构造器或者setter方法,并且还有各种各样的限制,@Autowired注解则使用更加灵活方便,功能也更加强大!同样,@Autowired要求其所在的类被注册到IoC容器中。JSR-330的@Inject注解可以一定程度上代替Spring的@Autowired注解。
@Autowired可以用在构造器、属性、方法上,甚至,某些时候不使用该注解就能实现自动注入。@Autowired注解是使用AutowiredAnnotationBeanPostProcessor处理器来解析的,后面的文章会分析BeanPostProcessor的原理,现在先学习如何使用。
@Autowired的自动注入规则如下:
@Autowired标注在构造器上时,表示对该构造器的所有参数从IoC中查找匹配的对象注入进去,查找的类型就是参数类型,查找的name就是参数名。
如下案例,一个AutowiredConstructorDemo,用于测试标注在构造器上:
/**
* @author lx
*/
@Component
public class AutowiredConstructorDemo {
private DemoA demoA;
private DemoB demoB;
/**
* 要求在IoC中查找并注入两个依赖属性
*/
@Autowired
public AutowiredConstructorDemo(DemoA demoA, DemoA demoB) {
this.demoA = demoA;
this.demoB = (DemoB) demoB;
}
@Component("demoA")
public static class DemoA {
public DemoA() {
System.out.println("DemoA:"+this);
}
}
@Component("demoB")
public static class DemoB extends DemoA {
public DemoB() {
System.out.println("DemoB:"+this);
}
}
@Override
public String toString() {
return "AutowiredConstructorDemo{" +
"demoA=" + demoA +
", demoB=" + demoB +
'}';
}
}
测试:
@Test
public void autowired() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("autowiredConstructorDemo", AutowiredConstructorDemo.class));
}
结果如下,成功注入:
DemoA:com.spring.core.ann.AutowiredConstructorDemo$DemoA@2357d90a
DemoA:com.spring.core.ann.AutowiredConstructorDemo$DemoB@64c87930
DemoB:com.spring.core.ann.AutowiredConstructorDemo$DemoB@64c87930
AutowiredConstructorDemo{
demoA=com.spring.core.ann.AutowiredConstructorDemo$DemoA@2357d90a, demoB=com.spring.core.ann.AutowiredConstructorDemo$DemoB@64c87930}
在上面的案例中,由于DemoB继承了DemoA,而参数类型为DemoA,因此在按照类型查找bean是会查找到两个bean(多态,类型向下兼容,会将子类bean也查询出来),一个demoA、一个demoB,随后会根据参数名匹配bean name继续注入,最终会注入成功,如果我们的参数名不是demoA和demoB,那么会由于匹配不到唯一的bean而抛出异常!
现在,我们尝试把构造器上的注解去掉,会发生什么呢?实际上,我们测试之后会发现,所依赖的对象照样被注入进来了。实际上,从Spring4.3开始,如果目标bean只定义了一个构造函数,那么就不再需要在此类构造函数上使用@Autowired注解,Spring会自动尝试注入。当然,这要求要么是无参构造器,要么构造器参数类型的bean在Spring容器中都能找到,否则还是会报错!下面有规律总结!
@Autowired注解标注的构造器上的参数默认都必须全部注入,即要保证所有的参数都能在IoC容器中找到对应的bean。
如果我们想要一种能找到就注入,找不到就不注入的情况,那么我们可以设置@Autowired注解的required属性为false(默认为true),为false就表示构造构造器上的参数都是非必须的依赖,这样即使找不到依赖项也不会报错!
我们将@Autowired的required设为false,并且demoB的Component注解注释掉,看看会发生什么。
/**
* 要求在IoC中查找并注入两个依赖属性
*/
@Autowired(required = false)
public AutowiredConstructorDemo(DemoA demoA, DemoB demoB) {
System.out.println("demoA"+"demoB");
this.demoA = demoA;
this.demoB = demoB;
}
//@Component("demoB")
public static class DemoB extends DemoA {
public DemoB() {
System.out.println("DemoB:"+this);
}
}
还是刚才的测试,结果抛出了异常,并没有出现所希望的按需注入:
expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {
}
实际上@Autowired的规则没这么简单,它的规则如下:
有了上面的规则,我们现在知道怎么处理这个问题,那就是加一个无参构造器即可,或则加一个只有demoA参数的构造器,并且让它作为候选构造器,这样就能满足注入demoA并且也不会抛出异常。
/**
* @author lx
*/
@Component
public class AutowiredConstructorDemo {
private DemoA demoA;
private DemoB demoB;
@Autowired(required = false)
public AutowiredConstructorDemo(DemoA demoA, DemoB demoB) {
System.out.println("demoA"+"demoB");
this.demoA = demoA;
this.demoB = demoB;
}
/**
* 添加只有demoA参数的构造器,并且作为候选构造器
*/
@Autowired(required = false)
public AutowiredConstructorDemo(DemoA demoA) {
this.demoA = demoA;
}
public AutowiredConstructorDemo() {
}
@Component("demoA")
public static class DemoA {
public DemoA() {
System.out.println("DemoA:"+this);
}
}
//@Component("demoB")
public static class DemoB extends DemoA {
public DemoB() {
System.out.println("DemoB:"+this);
}
}
@Override
public String toString() {
return "AutowiredConstructorDemo{" +
"demoA=" + demoA +
", demoB=" + demoB +
'}';
}
}
测试结果如下:
DemoA:com.spring.core.ann.AutowiredConstructorDemo$DemoA@7f9fcf7f
AutowiredConstructorDemo{
demoA=com.spring.core.ann.AutowiredConstructorDemo$DemoA@7f9fcf7f, demoB=null}
现在继续测试,继续添加只有一个只有demoB参数的构造器,并且让它作为候选构造器,然后将DemoA的bean移除容器,将DemoB的bean交给容器。
/**
* @author lx
*/
@Component
public class AutowiredConstructorDemo {
private DemoA demoA;
private DemoB demoB;
@Autowired(required = false)
public AutowiredConstructorDemo(DemoA demoA, DemoB demoB) {
System.out.println("demoA"+"demoB");
this.demoA = demoA;
this.demoB = demoB;
}
/**
* 添加只有demoA参数的构造器,并且作为候选构造器
*/
@Autowired(required = false)
public AutowiredConstructorDemo(DemoA demoA) {
System.out.println("demoA");
this.demoA = demoA;
}
/**
* 添加只有demoA参数的构造器,并且作为候选构造器
*/
@Autowired(required = false)
public AutowiredConstructorDemo(DemoB demoB) {
System.out.println("demoB");
this.demoB = demoB;
}
public AutowiredConstructorDemo() {
System.out.println("无参");
}
//@Component("demoA")
public static class DemoA {
public DemoA() {
System.out.println("DemoA:"+this);
}
}
@Component("demoB")
public static class DemoB extends DemoA {
public DemoB() {
System.out.println("DemoB:"+this);
}
}
@Override
public String toString() {
return "AutowiredConstructorDemo{" +
"demoA=" + demoA +
", demoB=" + demoB +
'}';
}
}
测试结果如下:
DemoA:com.spring.core.ann.AutowiredConstructorDemo$DemoB@2357d90a
DemoB:com.spring.core.ann.AutowiredConstructorDemo$DemoB@2357d90a
demoAdemoB
AutowiredConstructorDemo{
demoA=com.spring.core.ann.AutowiredConstructorDemo$DemoB@2357d90a, demoB=com.spring.core.ann.AutowiredConstructorDemo$DemoB@2357d90a}
我们发现,调用了最多参数的构造器。虽然DemoA的bean被移除容器,但是DemoA的bean还在容器中,在根据类型查找时,由于类型向下兼容,这两个参数都能匹配到demoB的bean,由于该构造器匹配到的依赖项最多,因此选择该构造器进行依赖注入。并且最终两个属性都注入一个相同的bean,那就是demoB。
根据上面的@Autowired的规则,我们发现,即使设置了required=false,只要有一个依赖项不能注入,那么该构造器就不会使用。为此,我们必须将可能的构造器参数组合都列出来全部作为候选构造器,才能最终实现“有就注入,没有就不注入”的功能,非常麻烦。
现在,我们可以使用Java的Optional来实现真正的“有就注入,没有就不注入”的功能。关于Optional类,在Java 8 lambda的文章中有详细描述,它是Java8新增的一个类,表示可能包含或不包含非空值的容器对象,用来防止空指针异常。
我们来对比一下使用Optional和不使用Optional的区别!
如下案例,一个AutowiredConstructorOptionalDemo类,测试Optional的作用,首先提供两个构造器,两个参数的构造器为候选构造器,一个无参构造器。OptionalDemoA注册到了容器中,OptionalDemoB并没有注册到容器中:
/**
* @author lx
*/
@Component
public class AutowiredConstructorOptionalDemo {
private OptionalDemoA optionalDemoA;
private OptionalDemoB optionalDemoB;
@Autowired(required = false)
public AutowiredConstructorOptionalDemo(OptionalDemoA optionalDemoA, OptionalDemoB optionalDemoB) {
System.out.println("optionalDemoA" + "optionalDemoB");
this.optionalDemoA = optionalDemoA;
this.optionalDemoB = optionalDemoB;
}
public AutowiredConstructorOptionalDemo() {
System.out.println("无参");
}
@Component("optionalDemoA")
public static class OptionalDemoA {
public OptionalDemoA() {
System.out.println("OptionalDemoA:" + this);
}
}
//@Component("optionalDemoB")
public static class OptionalDemoB extends OptionalDemoA {
public OptionalDemoB() {
System.out.println("OptionalDemoB:" + this);
}
}
@Override
public String toString() {
return "AutowiredConstructorOptionalDemo{" +
"optionalDemoA=" + optionalDemoA +
", optionalDemoB=" + optionalDemoB +
'}';
}
}
测试:
OptionalDemoA:com.spring.core.ann.AutowiredConstructorOptionalDemo$OptionalDemoA@7f9fcf7f
无参
AutowiredConstructorOptionalDemo{
optionalDemoA=null, optionalDemoB=null}
可以看到,最终调用了无参构造器的方法,optionalDemoA的依赖虽然在容器中,但是也没有注入进来,仅仅是因为optionalDemoB的依赖不在容器中,并且如果我们不提供无参构造器还会抛出异常。
现在使用Optional来改造,将上面两个构造器都注释掉,使用Optional来包装我们的参数类型,只提供两个参数的构造器,其他参数不变:
/**
* @author lx
*/
@Component
public class AutowiredConstructorOptionalDemo {
private OptionalDemoA optionalDemoA;
private OptionalDemoB optionalDemoB;
// @Autowired(required = false)
// public AutowiredConstructorOptionalDemo(OptionalDemoA optionalDemoA, OptionalDemoB optionalDemoB) {
// System.out.println("optionalDemoA" + "optionalDemoB");
// this.optionalDemoA = optionalDemoA;
// this.optionalDemoB = optionalDemoB;
// }
//
// public AutowiredConstructorOptionalDemo() {
// System.out.println("无参");
// }
public AutowiredConstructorOptionalDemo(Optional<OptionalDemoA> optionalDemoA,
Optional<OptionalDemoB> optionalDemoB) {
System.out.println("optionalDemoA" + "optionalDemoB");
optionalDemoA.ifPresent(i -> this.optionalDemoA = i);
optionalDemoB.ifPresent(i -> this.optionalDemoB = i);
}
@Component("optionalDemoA")
public static class OptionalDemoA {
public OptionalDemoA() {
System.out.println("OptionalDemoA:" + this);
}
}
//@Component("optionalDemoB")
public static class OptionalDemoB extends OptionalDemoA {
public OptionalDemoB() {
System.out.println("OptionalDemoB:" + this);
}
}
@Override
public String toString() {
return "AutowiredConstructorOptionalDemo{" +
"optionalDemoA=" + optionalDemoA +
", optionalDemoB=" + optionalDemoB +
'}';
}
}
继续测试,结果如下:
OptionalDemoA:com.spring.core.ann.AutowiredConstructorOptionalDemo$OptionalDemoA@7f9fcf7f
optionalDemoAoptionalDemoB
AutowiredConstructorOptionalDemo{
optionalDemoA=com.spring.core.ann.AutowiredConstructorOptionalDemo$OptionalDemoA@7f9fcf7f, optionalDemoB=null}
我们发现,不但没有报错,而且IoC容器中存在的依赖OptionalDemoA也注入进来了,不存在的依赖则默认为null,这样才是真正符合我们的“有就注入,没有就不注入”的功能,而且实现的非常简单!
我们可以对非必需的依赖都使用Optional包装,表示该项依赖是非必需的。实际上,这可以看作装饰设计模式的应用,Spring无论找没找到对应的依赖,都会将返回值分别使用一个Optional对象包装。这样,就是返回null,但是Optional对象不为null,因此不会报错。
Optional还会抵消required = true带来的强制依赖注入非null检查。即,即使没有这个依赖项并且required=true,但是如果使用Optional包装依赖参数的类型,那么永远不会抛出异常。因此,对于必须依赖项,不建议使用Optional包装!对于非必须依赖项,则建议使用Optional包装!
在Spring 5.0之后,我们还可以使用Spring提供的@Nullable注解或者JSR-305中的@Nullable注解标注到参数字段上,实现和Optional同样的“有就注入,没有就不注入”的功能。并且,@Nullable同样会抵消无论是默认的还是手动指定的required=true带来的强制依赖注入非null检查。
如下案例,有一个AutowiredConstructorNullableDemo类,用于测试@Nullable注解:
/**
* @author lx
*/
@Component
public class AutowiredConstructorNullableDemo {
private NullableDemoA nullableDemoA;
private NullableDemoB nullableDemoB;
public AutowiredConstructorNullableDemo(@Nullable NullableDemoA nullableDemoA,
@Nullable NullableDemoB nullableDemoB) {
System.out.println("调用两参数构造器");
this.nullableDemoA = nullableDemoA;
this.nullableDemoB = nullableDemoB;
}
@Component("nullableDemoA")
public static class NullableDemoA {
public NullableDemoA() {
System.out.println("NullableDemoA:" + this);
}
}
//@Component("nullableDemoB")
public static class NullableDemoB extends NullableDemoA {
public NullableDemoB() {
System.out.println("NullableDemoB:" + this);
}
}
@Override
public String toString() {
return "AutowiredConstructorNullableDemo{" +
"nullableDemoA=" + nullableDemoA +
", nullableDemoB=" + nullableDemoB +
'}';
}
}
测试:
@Test
public void autowiredConstructorNullable() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("autowiredConstructorNullableDemo", AutowiredConstructorNullableDemo.class));
}
结果如下,成功实现“有就注入,没有就不注入”的功能,并且没有抛出异常:
NullableDemoA:com.spring.core.ann.AutowiredConstructorNullableDemo$NullableDemoA@7f9fcf7f
调用两参数构造器
AutowiredConstructorNullableDemo{
nullableDemoA=com.spring.core.ann.AutowiredConstructorNullableDemo$NullableDemoA@7f9fcf7f, nullableDemoB=null}
@Autowired标注在方法上时,表示对该方法的所有参数从IoC中查找匹配的对象注入进去,查找的类型就是参数类型,查找的name就是参数名。
实际上,我们上面讲的构造器就是一种特殊的方法。因此,标注在方法上时,和标注在构造器上差不多。对于@Bean标注的方法,方法参数的依赖从IoC容器中会自动获取,这就类似于为一个构造含义中的参数会自动获取一样!
如下案例,有一个AutowiredSetterDemo类,用于测试标注在方法上:
/**
* @author lx
*/
@Component
public class AutowiredSetterDemo {
private SetterDemoA setterDemoA;
private SetterDemoB setterDemoB;
//标注在方法上
@Autowired
public void setSetterDemoA(SetterDemoA setterDemoA) {
this.setterDemoA = setterDemoA;
}
@Autowired
public void setSetterDemoB(SetterDemoB setterDemoB) {
this.setterDemoB = setterDemoB;
}
/**
* 在@Bean注解标注的方法上,参数会自动尝试注入,不需要标注@Autowired,如果没找到依赖就会抛出异常
* 因此如果是非必须依赖,那么可以使用@Autowired(required = false)或者Optional包装参数类
*/
@Bean
public SetterDemoA getSetterDemoA(Optional<SetterDemoB> setterDemoB) {
setterDemoB.ifPresent(demoB -> System.out.println("@Bean自动注入参数依赖:" + demoB));
return new SetterDemoA();
}
@Component
public static class SetterDemoA {
public SetterDemoA() {
System.out.println("SetterDemoA初始化:" + this);
}
}
@Component
public static class SetterDemoB {
public SetterDemoB() {
System.out.println("SetterDemoB初始化:" + this);
}
}
@Override
public String toString() {
return "AutowiredSetterDemo{" +
"setterDemoA=" + setterDemoA +
", setterDemoB=" + setterDemoB +
'}';
}
}
测试:
测试:
@Test
public void autowiredSetter() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("autowiredSetterDemo", AutowiredSetterDemo.class));
}
结果如下,成功注入:
SetterDemoA初始化:com.spring.core.ann.AutowiredSetterDemo$SetterDemoA@5b0abc94
SetterDemoB初始化:com.spring.core.ann.AutowiredSetterDemo$SetterDemoB@14a2f921
@Bean自动注入参数依赖:com.spring.core.ann.AutowiredSetterDemo$SetterDemoB@14a2f921
SetterDemoA初始化:com.spring.core.ann.AutowiredSetterDemo$SetterDemoA@4b44655e
AutowiredSetterDemo{
setterDemoA=com.spring.core.ann.AutowiredSetterDemo$SetterDemoA@5b0abc94, setterDemoB=com.spring.core.ann.AutowiredSetterDemo$SetterDemoB@14a2f921}
@Autowired标注在属性上时,表示对该属性从IoC中查找匹配的对象注入进去,查找的类型就是属性类型,查找的name就是属性名。
这种方式实际上是在开发过程中最常见的方式,因为只需要标注在属性上即可,不需要提供构造器,或者方法,非常简单。它的原理是通过反射获取该类对应的字段,然后将依赖设置进去的,因此必须要提供相应的构造器和方法!
如下案例,有一个AutowiredFieldDemo类,用于测试标注在属性上:
/**
* @author lx
*/
@Component
public class AutowiredFieldDemo {
//直接标注在字段上即可,如果是非必须依赖,那么可以使用@Autowired(required = false)
@Autowired
private FieldDemoA fieldDemoA;
@Autowired(required = false)
private FieldDemoB fieldDemoB;
@Component
public static class FieldDemoA {
public FieldDemoA() {
System.out.println("FieldDemoA初始化:" + this);
}
}
@Component
public static class FieldDemoB {
public FieldDemoB() {
System.out.println("FieldDemoB初始化:" + this);
}
}
@Override
public String toString() {
return "AutowiredFieldDemo{" +
"fieldDemoA=" + fieldDemoA +
", fieldDemoB=" + fieldDemoB +
'}';
}
}
测试:
@Test
public void autowiredField() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("autowiredFieldDemo", AutowiredFieldDemo.class));
}
结果如下,成功注入:
FieldDemoA初始化:com.spring.core.ann.AutowiredFieldDemo$FieldDemoA@2357d90a
FieldDemoB初始化:com.spring.core.ann.AutowiredFieldDemo$FieldDemoB@64c87930
AutowiredFieldDemo{
fieldDemoA=com.spring.core.ann.AutowiredFieldDemo$FieldDemoA@2357d90a, fieldDemoB=com.spring.core.ann.AutowiredFieldDemo$FieldDemoB@64c87930}
我们也可以使用以上方式混合标注。另外,还可以通过将注解添加到需要该类型数组的字段或方法,就可以从ApplicationContext中获取到该特定类型的所有bean,并且回向下兼容,这同样适用于类型化集合List、Set、Map……,同样支持上面三种注入方式。
对于有顺序的集合,我们还可以指定注入的bean排列顺序,通过在bean定义上添加@Order注解,并且设置int类型的值来对有序集合的依赖注入顺序进行排序,@Order的值越小,则排序越靠前,@Order对应有一个Ordered接口,内部有两个int类型的常量字段HIGHEST_PRECEDENCE和LOWEST_PRECEDENCE,代表int类型的最小值和最大值,即表示排在有序集合头部或者尾部,我们可以直接使用这两个字段!注意@Order不代表单例bean的启动顺序,仅仅是注入到集合的顺序。
如下案例,有一个AutowiredAllDemo类,用于测试注入全部bean:
/**
* @author lx
*/
@Component
public class AutowiredAllDemo {
//获取该类型的全部bean,会向下兼容
@Autowired
private AutowiredAllA[] autowiredAllAS;
@Autowired
private AutowiredAllB[] autowiredAllBS;
private AutowiredAllC[] autowiredAllCS;
@Autowired
private List<AutowiredAllA> autowiredAllAList;
private Set<AutowiredAllA> autowiredAllASet;
/**
* Map的key为String类型,就是bean的名字
*/
@Autowired
private Map<String, AutowiredAllA> autowiredAllAMap;
/**
* 支持方法注入
*/
@Autowired
public void setAutowiredAllASet(Set<AutowiredAllA> autowiredAllASet) {
this.autowiredAllASet = autowiredAllASet;
}
/**
* 支持构造器注入
*/
public AutowiredAllDemo(AutowiredAllC[] autowiredAllCS) {
this.autowiredAllCS = autowiredAllCS;
}
@Component
public static class AutowiredAllA {
}
@Component
public static class AutowiredAllB extends AutowiredAllA {
}
/**
* 指定最高优先
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public static class AutowiredAllC extends AutowiredAllB {
}
@Override
public String toString() {
return "AutowiredAllDemo{" + "\n" +
"autowiredAllAS=" + Arrays.toString(autowiredAllAS) + "\n" +
", autowiredAllBS=" + Arrays.toString(autowiredAllBS) + "\n" +
", autowiredAllCS=" + Arrays.toString(autowiredAllCS) + "\n" +
", autowiredAllAList=" + autowiredAllAList + "\n" +
", autowiredAllASet=" + autowiredAllASet + "\n" +
", autowiredAllAMap=" + autowiredAllAMap +
'}';
}
}
测试:
@Test
public void autowiredAll() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("autowiredAllDemo", AutowiredAllDemo.class));
}
结果如下,都成功注入:
AutowiredAllDemo{
autowiredAllAS=[com.spring.core.ann.AutowiredAllDemo$AutowiredAllC@6107227e, com.spring.core.ann.AutowiredAllDemo$AutowiredAllA@184cf7cf, com.spring.core.ann.AutowiredAllDemo$AutowiredAllB@2fd6b6c7]
, autowiredAllBS=[com.spring.core.ann.AutowiredAllDemo$AutowiredAllC@6107227e, com.spring.core.ann.AutowiredAllDemo$AutowiredAllB@2fd6b6c7]
, autowiredAllCS=[com.spring.core.ann.AutowiredAllDemo$AutowiredAllC@6107227e]
, autowiredAllAList=[com.spring.core.ann.AutowiredAllDemo$AutowiredAllC@6107227e, com.spring.core.ann.AutowiredAllDemo$AutowiredAllA@184cf7cf, com.spring.core.ann.AutowiredAllDemo$AutowiredAllB@2fd6b6c7]
, autowiredAllASet=[com.spring.core.ann.AutowiredAllDemo$AutowiredAllA@184cf7cf, com.spring.core.ann.AutowiredAllDemo$AutowiredAllB@2fd6b6c7, com.spring.core.ann.AutowiredAllDemo$AutowiredAllC@6107227e]
, autowiredAllAMap={
autowiredAllDemo.AutowiredAllA=com.spring.core.ann.AutowiredAllDemo$AutowiredAllA@184cf7cf, autowiredAllDemo.AutowiredAllB=com.spring.core.ann.AutowiredAllDemo$AutowiredAllB@2fd6b6c7, autowiredAllDemo.AutowiredAllC=com.spring.core.ann.AutowiredAllDemo$AutowiredAllC@6107227e}}
如果使用三种混合注入,那么它们的先后顺序是:构造器注入->属性反射注入->方法注入,因此它们对同一个属性设置的依赖可能会相互覆盖,后面的注入行为将覆盖前面的注入行为。
我们还可以使用@Autowired直接注入已知可解析依赖项:BeanFactory、ApplicationContext、Environment、ResourceLoader、ApplicationEventPublisher和MessageSource。这些接口及其扩展接口(如ConfigurableApplicationContext或ResourcePatternResolver)将自动解析并注入,无需特殊设置。
而由于@Autowired, @Inject, @Value, 和 @Resource 注解是在Spring的BeanPostProcessor中处理的,这意味着我们不能将这些注解用在自定义的BeanPostProcessor,BeanFactoryPostProcessor类中。这些类的bean配置必须使用XML或者@Bean方法来显示指定。
前面我们说过,对于@Bean注解标注的方法,它的方法的参数会被自动注入,但是它的自动注入功能远远没有这么简单。
实际上,@Bean注解标注的方法,一般我们会在内部创建的返回的bean实例,并设置相应的属性,这个属性就是通过参数传递来的。如果我们没有主动设置属性,按理说应该返回的bean应该是一个空属性的bean,然而,如果我们在该bean的类中设置了注解将该bean交给了容器管理,比如类上面标注了@Component(@Controller、@Service、@Repository)、@Configuration……,并且使用了方法或者属性的属性自动依赖注入,那么通过@Bean即使返回该类的空对象,IoC容器同样会对我们自己实例化的bean自动注入依赖,并且我们自己在@Bean的方法中自己设置的其他属性也将会被替代。所以说,为了避免这种问题,通常@Bean用于为那些不能通过直接添加@Component等注解交给IoC容器管理的bean设置属性并交给IoC容器,这样就不会出现问题,一定要注意!(在后面的@Qualifier注解部分会见识到)。
@Autowired在按照类型无法选择唯一的依赖注入时(比如存在多个类型兼容或者相同的候选依赖),可以使用Spring的@Primary注解,@Primary注解表示当有多个bean是该类型依赖项的候选对象时,应该优先考虑特定的被@Primary注解修饰的bean。
@Primary类似于XML装配中的primary属性,区别是:XML装配中的primary是绑定到一个bean上面的,而@Primary注解装配则是绑定到类上面的。
如果同一个类型(包括兼容类型)的不同bean中有超过一个bean被标注了@Primary注解。那么如果是采用方法注入或者字段注入或者采用构造器注入并且required=true,在运行时将直接抛出异常:available: more than one ‘primary’ bean found among candidates……。如果采用构造器注入并且required=false,那么将不会选择该构造器,最终可能会选择无参构造器,即不注入该依赖,如果没有其他构造器可选,那么同样抛出异常。
使用了@Primary注解,那么就不会走后面根据name匹配的逻辑,如果@Primary注解无法筛选出唯一的依赖,那么直接抛出异常或者不注入该依赖(上面讲了)。
如下案例,有一个AutowiredPrimaryDemo类,测试@primary:
/**
* @author lx
*/
@Component
public class AutowiredPrimaryDemo {
@Autowired
private NullableDemoA nullableDemo;
@Component("nullableDemoA")
public static class NullableDemoA {
public NullableDemoA() {
System.out.println("NullableDemoA:" + this);
}
}
/**
* 选择主要依赖
*/
@Component("nullableDemoB")
@Primary
public static class NullableDemoB extends NullableDemoA {
public NullableDemoB() {
System.out.println("NullableDemoB:" + this);
}
}
@Override
public String toString() {
return "AutowiredPrimaryDemo{" +
"nullableDemo=" + nullableDemo +
'}';
}
}
测试:
@Test
public void autowiredPrimary() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("autowiredPrimaryDemo", AutowiredPrimaryDemo.class));
}
结果如下,成功选择主依赖(选择的NullableDemoB):
NullableDemoA:com.spring.core.ann.AutowiredPrimaryDemo$NullableDemoA@15bb6bea
NullableDemoA:com.spring.core.ann.AutowiredPrimaryDemo$NullableDemoB@525f1e4e
NullableDemoB:com.spring.core.ann.AutowiredPrimaryDemo$NullableDemoB@525f1e4e
AutowiredPrimaryDemo{
nullableDemo=com.spring.core.ann.AutowiredPrimaryDemo$NullableDemoB@525f1e4e}
@Qualifier注解可以设置一个value值,该值就是一个bean的名字,由于制定就有改名字的bean被注入进来。@Qualifier只有被标注在属性或者方法/构造器的参数前面才会生效。基于XML的配置中,我们也可以使用标签完成类似的功能。
如下案例,有一个AutowiredQualifierDemo类,用于测试@Qualifier注解:
/**
* @author lx
*/
@Component
public class AutowiredQualifierDemo {
/**
* Qualifier指定bean name为qualifierDemoB的bean注入进来
*/
@Autowired
@Qualifier("qualifierDemoA")
private QualifierDemoA qualifierDemo;
public void setQualifierDemo(QualifierDemoA qualifierDemo) {
this.qualifierDemo = qualifierDemo;
}
/**
* 标注在参数前面才会生效
*/
@Bean
public AutowiredQualifierDemo getAutowiredQualifierDemo(@Qualifier("qualifierDemoB") QualifierDemoA qualifierDemo) {
System.out.println("qualifierDemo--------" + qualifierDemo);
AutowiredQualifierDemo autowiredQualifierDemo = new AutowiredQualifierDemo();
//虽然现在设置的是qualifierDemoB对象,但是交给容器之后会被替换,相当于又被注入了一次qualifierDemoA
autowiredQualifierDemo.setQualifierDemo(qualifierDemo);
return autowiredQualifierDemo;
}
@Component("qualifierDemoA")
public static class QualifierDemoA {
public QualifierDemoA() {
System.out.println("QualifierDemoA:" + this);
}
}
@Component("qualifierDemoB")
public static class QualifierDemoB extends QualifierDemoA {
public QualifierDemoB() {
System.out.println("QualifierDemoB:" + this);
}
}
@Override
public String toString() {
return "AutowiredQualifierDemo{" +
"nullableDemo=" + qualifierDemo +
'}';
}
}
测试:
@Test
public void autowiredQualifier() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("autowiredQualifierDemo", AutowiredQualifierDemo.class));
//通过@Bean返回的getAutowiredQualifierDemo对象和交给IoC容器管理的autowiredQualifierDemo对象的属性是同一个对象
//这就是在前面说的@Bean对于已经交给IoC容器自动管理的bean的自动注入问题,我们在@Bean中设置的属性被覆盖了。
System.out.println(ac.getBean("getAutowiredQualifierDemo", AutowiredQualifierDemo.class));
}
结果如下,成功选择bean name:
QualifierDemoA:com.spring.core.ann.AutowiredQualifierDemo$QualifierDemoA@5b0abc94
QualifierDemoA:com.spring.core.ann.AutowiredQualifierDemo$QualifierDemoB@14a2f921
QualifierDemoB:com.spring.core.ann.AutowiredQualifierDemo$QualifierDemoB@14a2f921
qualifierDemo--------com.spring.core.ann.AutowiredQualifierDemo$QualifierDemoB@14a2f921
AutowiredQualifierDemo{
nullableDemo=com.spring.core.ann.AutowiredQualifierDemo$QualifierDemoA@5b0abc94}
AutowiredQualifierDemo{
nullableDemo=com.spring.core.ann.AutowiredQualifierDemo$QualifierDemoA@5b0abc94}
Java泛型类型也可以隐式作为类型选择,可以用在属性或者参数上。如果某一些bean都实现了一个通用的接口,那么可以使用接口类型和泛型类型作为属性的类型,这同样适用于数组和集合。
如下案例,有一个AutowiredGenericDemo,用于测试泛型选择注入:
/**
* 泛型选择
*
* @author lx
*/
@Component
public class AutowiredGenericDemo {
@Autowired
private AutowiredGeneric<AutowiredGenericA> autowiredGenericAAutowiredGeneric;
@Autowired
private AutowiredGeneric<AutowiredGenericB> autowiredGenericBAutowiredGeneric;
@Autowired
private AutowiredGeneric<AutowiredGenericC> autowiredGenericCAutowiredGeneric;
@Autowired
private AutowiredGeneric<AutowiredGenericA>[] autowiredGenerics;
//……其他集合也可以
@Component
public static class AutowiredGenericA implements AutowiredGeneric<AutowiredGenericA> {
}
@Component
public static class AutowiredGenericB implements AutowiredGeneric<AutowiredGenericB> {
}
@Component
public static class AutowiredGenericC implements AutowiredGeneric<AutowiredGenericC> {
}
public interface AutowiredGeneric<T> {
}
@Override
public String toString() {
return "AutowiredGenericDemo{" + "\n" +
"autowiredGenericAAutowiredGeneric=" + autowiredGenericAAutowiredGeneric + "\n" +
", autowiredGenericBAutowiredGeneric=" + autowiredGenericBAutowiredGeneric + "\n" +
", autowiredGenericCAutowiredGeneric=" + autowiredGenericCAutowiredGeneric + "\n" +
", autowiredGenerics=" + Arrays.toString(autowiredGenerics) +
'}';
}
}
测试:
@Test
public void autowiredGeneric() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("autowiredGenericDemo", AutowiredGenericDemo.class));
}
结果如下,成功选择注入:
AutowiredGenericDemo{
autowiredGenericAAutowiredGeneric=com.spring.core.ann.AutowiredGenericDemo$AutowiredGenericA@a4102b8
, autowiredGenericBAutowiredGeneric=com.spring.core.ann.AutowiredGenericDemo$AutowiredGenericB@3b07a0d6
, autowiredGenericCAutowiredGeneric=com.spring.core.ann.AutowiredGenericDemo$AutowiredGenericC@11a9e7c8
, autowiredGenerics=[com.spring.core.ann.AutowiredGenericDemo$AutowiredGenericA@a4102b8]}
类似于@Autowired注解,@Resource注解也是一个以来依赖注入注解。
相比于@Autowired,@Resource有一下特点:
如下案例,有一个ResourceDemo类,用于测试@Resource注解:
/**
* @author lx
*/
@Component
public class ResourceDemo {
//注入已知可解析依赖项
@Resource
private ApplicationContext context;
//默认按照属性名->类型
@Resource
private AutowiredGeneric autowiredGenericA;
@Resource
private AutowiredGenericB autowiredGeneric;
/**
* 指定bean name
*/
@Resource(name = "autowiredGenericA")
private AutowiredGeneric autowiredGeneric1;
/**
* 指定bean type
*/
@Resource(type = AutowiredGenericB.class)
private AutowiredGeneric autowiredGeneric2;
/**
* 指定bean name and class 要求都要匹配
*/
@Resource(name = "autowiredGenericB", type = AutowiredGeneric.class)
private AutowiredGeneric autowiredGeneric3;
private AutowiredGeneric autowiredGeneric4;
private AutowiredGeneric autowiredGeneric5;
private AutowiredGeneric autowiredGeneric6;
private AutowiredGeneric autowiredGeneric7;
private AutowiredGeneric autowiredGeneric8;
/**
* 可以使用在方法上 同样是默认先name后type
*/
@Resource
public void setter4(AutowiredGeneric autowiredGenericA) {
this.autowiredGeneric4 = autowiredGenericA;
}
/**
* 可以配合Qualifier使用
*/
@Resource
public void setter5(@Qualifier("autowiredGenericB") AutowiredGeneric autowiredGeneric) {
this.autowiredGeneric5 = autowiredGeneric;
}
/**
* Resource和Qualifier都存在时,Resource的name 优先级更高
*/
@Resource(name = "autowiredGenericA")
public void setter6(@Qualifier("autowiredGenericB") AutowiredGeneric autowiredGeneric) {
this.autowiredGeneric6 = autowiredGeneric;
}
// /**
// * 多参数方法将抛出异常
// */
// @Resource
// public void setterTwo(AutowiredGeneric autowiredGenericA, AutowiredGeneric autowiredGenericB) {
// this.autowiredGeneric7 = autowiredGenericA;
// this.autowiredGeneric8 = autowiredGenericB;
// }
/**
* 支持Optional包装
*/
@Resource
public void setterTwo(Optional<AutowiredAllDemo> autowiredAllDemo) {
System.out.println(autowiredAllDemo);
}
@Component("autowiredGenericA")
public static class AutowiredGenericA implements AutowiredGeneric {
}
@Component("autowiredGenericB")
public static class AutowiredGenericB implements AutowiredGeneric {
}
public interface AutowiredGeneric {
}
@Override
public String toString() {
return "ResourceDemo{" + "\n" +
"context=" + context + "\n" +
"autowiredGenericA=" + autowiredGenericA + "\n" +
", autowiredGeneric=" + autowiredGeneric + "\n" +
", autowiredGeneric1=" + autowiredGeneric1 + "\n" +
", autowiredGeneric2=" + autowiredGeneric2 + "\n" +
", autowiredGeneric3=" + autowiredGeneric3 + "\n" +
", autowiredGeneric4=" + autowiredGeneric4 + "\n" +
", autowiredGeneric5=" + autowiredGeneric5 + "\n" +
", autowiredGeneric6=" + autowiredGeneric6 + "\n" +
", autowiredGeneric7=" + autowiredGeneric7 + "\n" +
", autowiredGeneric8=" + autowiredGeneric8 +
'}';
}
}
测试:
@Test
public void resource() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("resourceDemo", ResourceDemo.class));
}
结果如下,符合规则:
Optional.empty
ResourceDemo{
context=org.springframework.context.support.ClassPathXmlApplicationContext@198e2867, started on Sat Sep 05 15:40:03 CST 2020
autowiredGenericA=com.spring.core.ann.ResourceDemo$AutowiredGenericA@184cf7cf
, autowiredGeneric=com.spring.core.ann.ResourceDemo$AutowiredGenericB@2fd6b6c7
, autowiredGeneric1=com.spring.core.ann.ResourceDemo$AutowiredGenericA@184cf7cf
, autowiredGeneric2=com.spring.core.ann.ResourceDemo$AutowiredGenericB@2fd6b6c7
, autowiredGeneric3=com.spring.core.ann.ResourceDemo$AutowiredGenericB@2fd6b6c7
, autowiredGeneric4=com.spring.core.ann.ResourceDemo$AutowiredGenericA@184cf7cf
, autowiredGeneric5=com.spring.core.ann.ResourceDemo$AutowiredGenericB@2fd6b6c7
, autowiredGeneric6=com.spring.core.ann.ResourceDemo$AutowiredGenericA@184cf7cf
, autowiredGeneric7=null
, autowiredGeneric8=null}
@Value类似于XML配置中的value属性和标签,可以用于字面量值的直接注入,或者提取外部properties/yml文件中的属性,基于SPEL的语法的字符串操作,提取容器中的bean的属性,转换为复杂集合,甚至进行值的计算……。实际上,在XML的配置中也可以使用SPEL的语法,关于SPEL属性注入的语法,后面会专门有文章讲解!
@Value的使用非常灵活,下面仅仅介绍常见用法。后面在Spring Boot中还会遇到另一个更加灵活的@ConfigurationProperties注解。
@Value可以用在属性、方法(普通方法、@Bean方法都行),参数(普通方法、@Bean方法、构造器都行)上。用在方法上时,表示对方法都注入相同的字面量值。
@Value可以直接注入基本类型、包装类型、字符串类型等字面量值,Spring提供的内置转换器支持允许自动处理简单的类型转换,还可以转换为数组、集合等复杂结构,甚至通过SPEL操作字符串方法比如大小写、截取、拼接……等等,更多操作详情看下面的案例。
案例如下,一个ValueDemoStr类,测试字面量的注入:
/**
* Value直接注入字面量
*
* @author lx
*/
@Component
public class ValueDemoStr {
//基本类型
@Value("1")
private byte b;
@Value("1")
private char c;
@Value("1")
private short s;
@Value("1")
private int i;
@Value("1")
private long l;
@Value("1")
private float f;
@Value("1")
private double d;
//boolean类型机器姬包装类可以使用true或者false字符串
//也可以使用0代表false,1代表true
@Value("0")
private boolean boo;
//字符串
@Value("1")
private String string;
@Value("1")
private CharSequence charSequence;
//包装类型
@Value("1")
private Byte aByte;
@Value("1")
private Character character;
@Value("1")
private Short aShort;
@Value("1")
private Integer integer;
@Value("1")
private Long aLong;
@Value("1")
private Float aFloat;
@Value("1")
private Double aDouble;
@Value("1")
private Boolean aBoolean;
//某些整形基本类型数组,使用,分隔即可自动拆分
@Value("1,2,3,0,11,2")
private short[] shorts;
@Value("1,2,3,0,11,2")
private int[] ints;
@Value("1,2 3")
private long[] longs;
//下面的要使用SPEL 表达式 #{'要分割的数据'.进行的操作}
@Value("#{'1,2,3,0,11,2'.split(',')}")
private byte[] bytes;
@Value("#{'1,0,1,1'.split(',')}")
private char[] chars;
@Value("#{'1,0,1,1'.split(',')}")
private float[] floats;
@Value("#{'1,0,1,1'.split(',')}")
private double[] doubles;
@Value("#{'1,0,1,1'.split(',')}")
private boolean[] booleans;
//SPEL可以调用String类的方法很多方法
//大写
@Value("#{'abc'.toUpperCase()}")
private String upperCase;
//截取
@Value("#{'abc'.substring(1)}")
private String substring;
//拼接
@Value("#{'abc'.concat('def')}")
private String concat;
//当然支持更多字符串操作…………
//SPEL计算
@Value("#{4+2}")
private int add;
@Value("#{4-2}")
private int subtract;
@Value("#{4*2}")
private int multiply;
@Value("#{4/2}")
private int divide;
//SPEL取一个随机数
@Value("#{ T(java.lang.Math).random() * 100.0}")
public double property1;
//当然支持更多复杂计算…………
//SPEL转换数组和集合
//单value集合 #{'value1,value2'.split(',')}
@Value("#{'11,22,xx'.split(',')}")
private String[] strings;
@Value("#{'11,22, x x'.split(',')}")
private CharSequence[] charSequences;
@Value("#{'property1,property2,property2,property3'.split(',')}")
private List<String> list;
//set会自动去重
@Value("#{'property1,property2,property2,property3'.split(',')}")
private Set<String> set;
//map集合 #{mapJsonValue} 类似于JSON格式 会自动去重
@Value("#{
{key1: 'value1', key2: 'value2', key2: 'value3'}}")
private Map<String,String> map;
//@Value可以用在方法上,表示对方法的所有参数都注入同一个指定值
private int anInt1;
private int anInt2;
private boolean anInt3;
@Value("11")
public void setAnInt(int anInt1) {
this.anInt1 = anInt1;
}
@Value("1")
public void setTwo(int anInt1, boolean anInt2) {
this.anInt2 = anInt1;
this.anInt3 = anInt2;
}
//@Value用在方法参数上也行,表示对指定参数注入指定值
private int anInt4;
public ValueDemoStr(@Value("22") int anInt4) {
this.anInt4 = anInt4;
}
@Override
public String toString() {
return "ValueDemoStr{" +
"b=" + b +
", c=" + c +
", s=" + s +
", i=" + i +
", l=" + l +
", f=" + f +
", d=" + d +
", boo=" + boo +
", string='" + string + '\'' +
", charSequence=" + charSequence +
", aByte=" + aByte +
", character=" + character +
", aShort=" + aShort +
", integer=" + integer +
", aLong=" + aLong +
", aFloat=" + aFloat +
", aDouble=" + aDouble +
", aBoolean=" + aBoolean +
", shorts=" + Arrays.toString(shorts) +
", ints=" + Arrays.toString(ints) +
", longs=" + Arrays.toString(longs) +
", bytes=" + Arrays.toString(bytes) +
", chars=" + Arrays.toString(chars) +
", floats=" + Arrays.toString(floats) +
", doubles=" + Arrays.toString(doubles) +
", booleans=" + Arrays.toString(booleans) +
", upperCase='" + upperCase + '\'' +
", substring='" + substring + '\'' +
", concat='" + concat + '\'' +
", add=" + add +
", subtract=" + subtract +
", multiply=" + multiply +
", divide=" + divide +
", property1=" + property1 +
", strings=" + Arrays.toString(strings) +
", charSequences=" + Arrays.toString(charSequences) +
", list=" + list +
", set=" + set +
", map=" + map +
", anInt1=" + anInt1 +
", anInt2=" + anInt2 +
", anInt3=" + anInt3 +
", anInt4=" + anInt4 +
'}';
}
}
测试:
@Test
public void valueDemoStr() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("valueDemoStr", ValueDemoStr.class));
}
@Value可以获取外部proerties或者yml文件的属性并注入。
我们在resource目录下建立一个value.proerties文件:
然后在里面写一些键值对:
key=value
a.b.c=xxx
#一定要统一编码解码格式,否则中文乱码
maohao:冒号分隔
kongge 空格分隔
int=4
double=2
boolean=1
#可以获取其他key的值作为value
xxx=${key}
#array 指定你想要的分隔符号,然后使用split拆分即可
booleans=1,0,0,1
#list 指定你想要的分隔符号,然后使用split拆分即可
list=1;xx;0;1;11
#set 指定你想要的分隔符号,然后使用split拆分即可 会自动去重
set=1,xx,0,1,11
#properties和map都是Map类型 会自动去重
#类似于JSON格式,可以获取其他key的值作为value作为map的key或者value
#properties
properties={key1: 'value1', ${xxx}: '${xxx}'}
#map
redirectUrl={key: '${key}',key: '${a.b.c}',${xxx}:'${booleans}'}
添加一个如下的类,用于测试读取配置文件的属性:
/**
* Value注入外部文件的值
* 需要@PropertySource("classpath:value.properties")读取指定外部文件
*
* @author lx
*/
@Component
@PropertySource(value="classpath:value.properties",encoding="UTF-8")
public class ValueProperties {
//注入字面量,通过 ${键} 格式来获取值并注入到属性中
@Value("${key}")
private String value;
@Value("${int}")
private char c;
@Value("${double}")
private double d;
@Value("${boolean}")
private boolean b;
@Value("${xxx}")
private String xxx;
@Value("${maohao}")
private String maohao;
@Value("${kongge}")
private String kongge;
//取多个属性
@Value("${kongge}${maohao}")
private String concat;
//SPEL注入数组和单value集合的样式
@Value("#{'${booleans}'.split(',')}")
private boolean[] booleans;
@Value("#{'${list}'.split(';')}")
private List<String> stringList;
@Value("#{'${set}'.split(',')}")
private Set<String> stringSet;
//SPEL注入map的样式
@Value("#{${properties}}")
private Properties properties;
@Value("#{${redirectUrl}}")
private Map<String,String> map;
//SPEL计算,当然还有其他更多复杂操作
@Value("#{${int}+${double}}")
private int add;
@Value("#{${int}-${double}}")
private int subtract;
@Value("#{${int}*${double}}")
private int multiply;
@Value("#{${int}/${double}}")
private int divide;
@Override
public String toString() {
System.out.println("------properties-------");
properties.forEach((o, o2) -> {
System.out.println(o);
System.out.println(o2);
});
System.out.println("------map-------");
map.forEach((o, o2) -> {
System.out.println(o);
System.out.println(o2);
});
System.out.println("------str-------");
return "ValueProperties{" +
"value='" + value + '\'' +
", c=" + c +
", d=" + d +
", b=" + b +
", xxx='" + xxx + '\'' +
", maohao='" + maohao + '\'' +
", kongge='" + kongge + '\'' +
", concat='" + concat + '\'' +
", add='" + add + '\'' +
", subtract='" + subtract + '\'' +
", multiply='" + multiply + '\'' +
", divide='" + divide + '\'' +
", booleans=" + Arrays.toString(booleans) +
", stringList=" + stringList +
", stringSet=" + stringSet +
", properties=" + properties +
", map=" + map +
'}';
}
}
测试:
@Test
public void valueProperties() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("valueProperties", ValueProperties.class));
}
在上面的案例中,我们使用@PropertySource注解加载单个配置文件,如果有多个配置文件,那么可以使用@PropertySources加载多个配置文件,如果多个配置文件中有相同的key,那么后加载的配置文件将覆盖之前加载的配置文件的value值。先后加载的顺序就是声明加载的顺序,先声明的先加载。也可以就使用@PropertySource,填写多个文件参数,使用,分隔。另外,加载进来的配置全局通用,其他的bean也可以直接使用不需要再加载了。因此,实际上配置文件常常在@Configuration配置类中加载,而不是普通的bean中加载。
Spring默认情况下,如果配置文件没有key,则将$ {key}本身将尝试作为值注入,如果注入成功(比如属性是String类型则肯定能注入成功)则不会抛出异常。
如果我们需要启动检查没有的key,要对不存在的值保持严格控制,让它抛出异常,我们可以向IoC容器注册一个PropertySourcesPlaceholderConfigurer对象!
通常我们使用@Bean方式将PropertySourcesPlaceholderConfigurer注册到IoC容器中,并且要求对应的方法是static方法。在Spring Boot中,默认情况下,会有一个PropertySourcesPlaceholderConfigurer被初始化,自动从application.properties and application.yml中加载配置。
另外,可以重写PropertySourcesPlaceholderConfigurer的setPlaceholderPrefix、setPlaceholderSuffix、setValueSeparator方法来配置我们自己的占位符和解析。默认占位符实际上就是“${”、“}”等符号,Spring通过解析前置、后置占位符来获取key的信息,或者对里面的值进行拆分。
在此前基于XML的配置中,我们使用property-placeholder来引入外部的配置文件,然后就可以在XML文件或者类中使用同样的语法获取配置文件的信息。比如,引入前面的配置文件:< context:property-placeholder location=“classpath:value.properties,value3.properties” />。由于基于XML的配置就是使用的PropertySourcesPlaceholderConfigurer。因此基于XML的配置和Spring Boot项目一样,如果没有对应的key都会直接启动报错!
/**
* Value注入外部文件的值
*
* @author lx
*/
@Component
//使用@PropertySources
@PropertySources(value = {
//value3.properties在后面,因此value3将会覆盖value的同名key的值
@PropertySource(value = "classpath:value.properties", encoding = "UTF-8"),
@PropertySource(value = "classpath:value3.properties", encoding = "UTF-8")
})
//也可以就使用@PropertySource,同样按照定义顺序加载配置文件
//@PropertySource(value = {"classpath:value3.properties", "classpath:value.properties"})
public class ValuePropertiesCheck {
@Value("${key}")
private String value;
//Spring默认情况下,如果配置文件没有对应的key,则将${key}本身将尝试作为值注入
@Value("${zzz}")
private String zzz;
@Override
public String toString() {
return "ValuePropertiesCheck{" +
"value='" + value + '\'' +
", zzz='" + zzz + '\'' +
'}';
}
/**
* 添加一个PropertySourcesPlaceholderConfigurer,将会在启动时检查配置文件没有对应的key的情况,并且抛出异常
* 一定要static修饰的方法,在一开始就执行!
*/
@Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
测试:
@Test
public void valuePropertiesCheck() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("valuePropertiesCheck", ValuePropertiesCheck.class));
}
启动后将直接抛出异常而停止运行!因为没有key为zzz的属性。
上面讲了,通过@PropertySource 引入的配置文件时,如果@Value引用的key不存在,那么默认将整个$ {key}作为值注入,或者通过XML/Spring Boot使用PropertySourcesPlaceholderConfigurer引入配置文件时,如果@Value引用的key不存在,那么直接启动抛出异常。
实际上,我们也可以为key设置默认值,当某个属性的key在配置文件中不存在时,就使用我们指定的默认值。这种情况在不同web服务模块使用同一套后端service业务模块时非常有用。
语法非常简单:${key:defaultValue},如果只有“:”而不写默认值,那么表示注入空字符串,因此注意类型转换异常。defaultValue可以使用SPEL表达式,实际上这里的defaultvalue只要是字面量部分的值都可以。
如下案例,有一个ValuePropertiesDefault,用于测试默认值:
/**
* Value注入外部文件的值设置默认值
*
* @author lx
*/
@Component
@PropertySource(value = {
"classpath:value3.properties", "classpath:value.properties"})
public class ValuePropertiesDefault {
@Value("${key:}")
private String key;
/**
* 外部文件是没有zzz这个key的,因此设置默认值,这里默认注入空字符串""
*/
@Value("${qaz:}")
private String qaz;
/**
* 外部文件是没有zzz这个key的,因此设置默认值,这里默认注入1
* 如果不写默认值,那么Spring尝试将""转换为int类型,进而会报错
*/
@Value("${wsx:1}")
private int wsx;
/**
* 可以默认注入
*/
@Value("${edc:1,2,3,4}")
private int[] edc;
//可以使用SPEL表达式
/**
* 外部文件有key这里的属性,因此不会使用默认值
*/
@Value("${key:#{'abc'.concat('def')}}")
private String concat;
@Value("${rfv:#{'1,0,1,1'.split(',')}}")
private double[] rfv;
@Value("${tgb:#{
{key1: 'value1', key2: 'value2', key2: 'value3'}}}")
private Map<String,String> map;
//………………更多功能
@Override
public String toString() {
return "ValuePropertiesDefault{" +
"key='" + key + '\'' +
", qaz='" + qaz + '\'' +
", wsx=" + wsx +
", edc=" + Arrays.toString(edc) +
", rfv=" + Arrays.toString(rfv) +
", concat='" + concat + '\'' +
", map=" + map +
'}';
}
/**
* 添加一个PropertySourcesPlaceholderConfigurer,将会在启动时检查配置文件没有对应的key的情况,并且抛出异常
* 一定要static修饰的方法,在一开始就执行!
*/
@Bean
public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
测试:
@Test
public void valuePropertiesDefault() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("valuePropertiesDefault", ValuePropertiesDefault.class));
}
结果如下:
ValuePropertiesDefault{
key='value', qaz='', wsx=1, edc=[1, 2, 3, 4], rfv=[1.0, 0.0, 1.0, 1.0], concat='value', map={
key1=value1, key2=value3}}
我们发现,即使没有对应的key,也没有抛出异常,而是使用默认值。
@Value可以获取其他bean的属性值然后注入到自己的属性中,这实际上也是SPEL的作用,但是这要求尝试获取属性的bean的属性提供相应的getter方法或者属性是public修饰的。对于集合类型,也可以注入集合的索引元素或者map的key的value。
如下案例,有一个ValueOtherBean类,用于测试获取其他bean的属性值:
/**
* @author lx
*/
@Component
public class ValueOtherBean {
//SPEL获取其他bean的属性值
/**
* 字面量
*/
@Value("#{valueOtherBeanA.property1}")
private String valueOtherBeanAproperty1;
@Value("#{valueOtherBeanA.property2}")
private double valueOtherBeanAproperty2;
/**
* 数组/集合
*/
@Value("#{valueOtherBeanA.property3}")
private String[] valueOtherBeanAproperty3;
@Value("#{valueOtherBeanA.property4}")
private String[] valueOtherBeanAproperty4;
@Value("#{valueOtherBeanA.property5}")
private Map<String,Integer> valueOtherBeanAproperty5;
/**
* 数组/集合某个索引/key的数据
*/
@Value("#{valueOtherBeanA.property3[0]}")
private String valueOtherBeanAproperty3One;
@Value("#{valueOtherBeanA.property4[1]}")
private String[] valueOtherBeanAproperty4One;
@Value("#{valueOtherBeanA.property5[key2]}")
private Integer valueOtherBeanAproperty5Key3;
//其他bean
//想要使用其他bean的属性,要求其他bean提供属性的getter方法或者属性必须是public修饰的
@Component("valueOtherBeanA")
public static class ValueOtherBeanA {
/**
* 一个字符串
*/
@Value("property1")
private String property1;
/**
* SPEL取一个随机数
*/
@Value("#{ T(java.lang.Math).random() * 100.0}")
private double property2;
@Value("#{'property1,property2,property3'.split(',')}")
private String[] property3;
@Value("#{'property1,property2,property3'.split(',')}")
public List<String> property4;
@Value("#{
{key1: '1', key2: '2'}}")
public Map<String,Integer> property5;
public String getProperty1() {
return property1;
}
public double getProperty2() {
return property2;
}
public String[] getProperty3() {
return property3;
}
}
@Override
public String toString() {
return "ValueOtherBean{" +
"valueOtherBeanAproperty1='" + valueOtherBeanAproperty1 + '\'' +
", valueOtherBeanAproperty2=" + valueOtherBeanAproperty2 +
", valueOtherBeanAproperty3=" + Arrays.toString(valueOtherBeanAproperty3) +
", valueOtherBeanAproperty4=" + Arrays.toString(valueOtherBeanAproperty4) +
", valueOtherBeanAproperty5=" + valueOtherBeanAproperty5 +
", valueOtherBeanAproperty3One='" + valueOtherBeanAproperty3One + '\'' +
", valueOtherBeanAproperty4One=" + Arrays.toString(valueOtherBeanAproperty4One) +
", valueOtherBeanAproperty5Key3=" + valueOtherBeanAproperty5Key3 +
'}';
}
}
测试:
@Test
public void valueOtherBean() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("valueOtherBean", ValueOtherBean.class));
}
结果如下,成功获取并注入:
ValueOtherBean{
valueOtherBeanAproperty1='property1', valueOtherBeanAproperty2=1.5384076971526883, valueOtherBeanAproperty3=[property1, property2, property3], valueOtherBeanAproperty4=[property1, property2, property3], valueOtherBeanAproperty5={
key1=1, key2=2}, valueOtherBeanAproperty3One='property1', valueOtherBeanAproperty4One=[property2], valueOtherBeanAproperty5Key3=2}
@Value除了注入字面量、外部属性文件、SPEL的表达式计算、SPEL的复杂类型属性注入、SPEL其他bean的属性注入……等等,还可以注入一些特别的属性或者资源。
@Value还可以使用SPEL直接注入操作系统相关资源属性、以及Resource资源。
如下案例,测试直接注入操作系统相关资源属性、以及Resource资源:
/**
* Value注入资源
*
* @author lx
*/
@Component
public class ValueResource {
//SPEL直接注入系统属性
@Value("#{systemProperties['java.home']}")
private String javaHome;
@Value("#{systemProperties['java.version']}")
private String javaVersion;
//SPEL注入Resource资源
/**
* 注入文件资源
*/
@Value("classpath:value.properties")
private Resource classPathResource;
@Value("G:\\Idea\\springcoreannotation\\src\\main\\resources\\value3.properties")
private Resource classPathContextResource;
/**
* 注入URL资源
*/
@Value("https://spring.io/")
private Resource urlResource;
@Override
public String toString() {
return "ValueResource{" +
"javaHome='" + javaHome + '\'' +
", javaVersion='" + javaVersion + '\'' +
", classPathResource=" + classPathResource +
", classPathContextResource=" + classPathContextResource +
", urlResource=" + urlResource +
'}';
}
}
测试:
@Test
public void valueResource() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("valueResource", ValueResource.class));
}
结果如下:
ValueResource{
javaHome='C:\Java\jdk1.8.0_144\jre', javaVersion='1.8.0_144', classPathResource=class path resource [value.properties], classPathContextResource=class path resource [G:/Idea/springcoreannotation/src/main/resources/value3.properties], urlResource=URL [https://spring.io/]}
我们在基于XML的配置部分详细已经介绍过bean的生命周期回调功能,详情看那一部分。@PostConstruct和@PreDestroy是来自JSR-250的bean生命周期注解,在Spring2.5开始同样引入了对这些注解的支持,同样使用CommonAnnotationBeanPostProcessor处理器来解析这两个注解,component-scan默认将CommonAnnotationBeanPostProcessor已经注册了。
@PostConstruct表示初始化bean回调,@PreDestroy表示销毁bean回调。和Resource一样,这三个注解是 JDK 6 到 8 的标准 Java 库的一部分,位于rt.jar包下的java.annotation包中。使用时无需引入任何依赖。但是JDK9及其之后的版本,javax.annotation脱离了Java核心库,想要使用时只能引入maven依赖:javax.annotation-api(artifactId)。
<dependency>
<groupId>javax.annotationgroupId>
<artifactId>javax.annotation-apiartifactId>
<version>1.3.2version>
dependency>
如下案例,一个BeanCallback类,用于测试生命周期回调:
/**
* @author lx
*/
@Component
public class BeanCallback {
/**
* 单例bean
* 可以进行bean创建回调和bean销毁回调
*/
@Component("beanCallbackA")
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public static class BeanCallbackA{
@PostConstruct
public void postConstruct() {
System.out.println("BeanCallbackA bean创建回调");
}
@PreDestroy
public void preDestroy() {
System.out.println("BeanCallbackA bean销毁回调");
}
public BeanCallbackA() {
System.out.println("BeanCallbackA 构造器");
}
}
/**
* 原型bean
* 只能进行bean创建回调,不会进行bean销毁回调
*/
@Component("beanCallbackB")
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public static class BeanCallbackB{
@PostConstruct
public void postConstruct() {
System.out.println("BeanCallbackB bean创建回调");
}
@PreDestroy
public void preDestroy() {
System.out.println("BeanCallbackB bean销毁回调");
}
public BeanCallbackB() {
System.out.println("BeanCallbackB 构造器");
}
}
}
测试:
@Test
public void beanCallback() {
System.out.println("---创建容器---");
//新建容器,将会实例化单例的bean,触发bean创建回调
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println("---获取bean实例---");
//获取bean实例,将会实例化原型的bean,触发bean创建回调
System.out.println(ac.getBean("beanCallbackA", BeanCallback.BeanCallbackA.class));
System.out.println(ac.getBean("beanCallbackB", BeanCallback.BeanCallbackB.class));
//关闭容器,将会销毁bean,触发单例bean的销毁回调
System.out.println("---容器关闭---");
ac.close();
}
结果如下:
---创建容器---
BeanCallbackA 构造器
BeanCallbackA bean创建回调
---获取bean实例---
com.spring.core.ann.BeanCallback$BeanCallbackA@2aa5fe93
BeanCallbackB 构造器
BeanCallbackB bean创建回调
com.spring.core.ann.BeanCallback$BeanCallbackB@5c1a8622
---容器关闭---
BeanCallbackA bean销毁回调
这个注解就类似于XML配置中的depends-on属性,用于控制某一个或者多个bean在该bean实例化之前先顺序实例化,在该bean销毁之后倒序销毁。对于注解标注的bean如果是prototype则DependsOn无效,而对于被依赖的bean即使是prototype的也会顺序初始化,但不会执行销毁回调。更多详情可以看基于XML配置的depends-on部分。
如下案例,有一个BeanDependsOn类,在beanDependsOnA实例化之前,beanDependsOnC和beanDependsOnB顺序实例化,在beanDependsOnA销毁之后,beanDependsOnC和beanDependsOnB倒序销毁。
/**
* @author lx
*/
@Component
public class BeanDependsOn {
/**
* 可以指定多一个依赖的bean多个bean将按照顺序先后初始化
* 销毁时按照倒序销毁
*/
@DependsOn({
"beanDependsOnC", "beanDependsOnB"})
@Component("beanDependsOnA")
//@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public static class BeanDependsOnA {
@PostConstruct
public void postConstruct() {
System.out.println("BeanDependsOnA bean创建回调");
}
@PreDestroy
public void preDestroy() {
System.out.println("BeanDependsOnA bean销毁回调");
}
public BeanDependsOnA() {
System.out.println("BeanDependsOnA 构造器");
}
}
@Component("beanDependsOnB")
public static class BeanDependsOnB {
@PostConstruct
public void postConstruct() {
System.out.println("BeanDependsOnB bean创建回调");
}
@PreDestroy
public void preDestroy() {
System.out.println("BeanDependsOnB bean销毁回调");
}
public BeanDependsOnB() {
System.out.println("BeanDependsOnB 构造器");
}
}
@Component("beanDependsOnC")
//@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public static class BeanDependsOnC {
@PostConstruct
public void postConstruct() {
System.out.println("BeanDependsOnC bean创建回调");
}
@PreDestroy
public void preDestroy() {
System.out.println("BeanDependsOnC bean销毁回调");
}
public BeanDependsOnC() {
System.out.println("BeanDependsOnC 构造器");
}
}
}
测试:
@Test
public void beanDependsOn() {
System.out.println("---创建容器---");
//新建容器,将会实例化单例的bean,触发bean创建回调
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
//System.out.println(ac.getBean("beanDependsOnA", BeanDependsOn.BeanDependsOnA.class));
//关闭容器,将会销毁bean,触发单例bean的销毁回调
System.out.println("---容器关闭---");
ac.close();
}
结果如下,bean的创建和销毁都保证了顺序(对于singleton的bean):
---创建容器---
BeanDependsOnC 构造器
BeanDependsOnC bean创建回调
BeanDependsOnB 构造器
BeanDependsOnB bean创建回调
BeanDependsOnA 构造器
BeanDependsOnA bean创建回调
---容器关闭---
BeanDependsOnA bean销毁回调
BeanDependsOnB bean销毁回调
BeanDependsOnC bean销毁回调
@Scope注解可以与@Component(@Repository、@Service、@Controller)、@Bean、@Configuration、@Autowired、@Inject等注解使用,@Lazy和XML配置的lazy-init属性效果基本一致,默认为true,详情参考XML的配置文章。
从Spring3.0之开始,Spring提供了对于JSR-330依赖注入标准注解的支持。这些注解和Spring注解一样被扫描和解析。JSR-330 是 Java 的依赖注入标准,定义了一些规范:
Java EE中的javax.inject包就对应该标准提供的相关注解,但是并没有定义依赖注入的配置和实现方式,由具有依赖注入功能的框架自己实现依赖注入器,实际上就是注解处理器,Spring框架自然提供了相应的实现。
JSR-330提供了5个注解(Inject、Qualifier、Named、Scope、Singleton)和1个扩展接口(Provider),Spring的spring-context依赖中没有集成JSR-330标准注解,想要使用它,我们必须引入另一个maven依赖:
<dependency>
<groupId>javax.injectgroupId>
<artifactId>javax.injectartifactId>
<version>1version>
dependency>
@Named可以用在类上,用来代替Spring的@Component组件。
@Named可以用在参数上,指定注入bean的名称,用来代替Spring的@Qualifier。内部提供的@Qualifier只能用来构建注解。
@Inject可以用在方法、构造器、参数上,用来代替Spring的@Autowired。不能指定限定名,因此常与@Named连用,没有required属性,因此也可以与Optional或@Nullable一起使用。
@Singleton注解,类似于Spring的@Scope(“singleton”),没有提供其他作用域。内部的@Scope注解只能用来构建注解。
如下案例,一个JSR330Demo类,用于测试JSR-330:
/**
* @author lx
*/
@Named
public class JSR330Demo {
public JSR330Demo() {
System.out.println("JSR330Demo: "+this);
}
private JSR330DemoA getJSR330DemoA;
@Inject
public void setJsr330DemoA(@Named("getJSR330DemoA") JSR330DemoA jsr330DemoA) {
this.getJSR330DemoA = jsr330DemoA;
}
@Inject
private JSR330DemoA jsr330DemoA2;
private JSR330DemoA jsr330DemoA3;
@Inject
public JSR330Demo( JSR330DemoA jsr330DemoA3) {
this.jsr330DemoA3 = jsr330DemoA3;
}
@Named("JSR330DemoA")
public static class JSR330DemoA {
public JSR330DemoA() {
System.out.println("JSR330DemoA: "+this);
}
}
@Bean
public JSR330DemoA getJSR330DemoA(){
JSR330DemoA jsr330DemoA = new JSR330DemoA();
System.out.println("getJSR330DemoA: "+jsr330DemoA);
return jsr330DemoA;
}
@Override
public String toString() {
return "JSR330Demo{" +
"getJSR330DemoA=" + getJSR330DemoA +
", jsr330DemoA2=" + jsr330DemoA2 +
", jsr330DemoA3=" + jsr330DemoA3 +
'}';
}
}
测试:
@Test
public void jSR330Demo() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println(ac.getBean("JSR330Demo", JSR330Demo.class));
}
结果如下:
JSR330DemoA: com.spring.core.ann.JSR330Demo$JSR330DemoA@12d3a4e9
JSR330DemoA: com.spring.core.ann.JSR330Demo$JSR330DemoA@2a32de6c
getJSR330DemoA: com.spring.core.ann.JSR330Demo$JSR330DemoA@2a32de6c
JSR330Demo{
getJSR330DemoA=com.spring.core.ann.JSR330Demo$JSR330DemoA@2a32de6c, jsr330DemoA2=com.spring.core.ann.JSR330Demo$JSR330DemoA@12d3a4e9, jsr330DemoA3=com.spring.core.ann.JSR330Demo$JSR330DemoA@12d3a4e9}
在上面我们开启注解支持的方式是在XML中配置context:component-scan标签,以及扫描包,此后我们就可以使用各种注解来配置此前需要在XML文件中编写的bean的定义。其实,对于这些最基本的配置,比如上面的注解支持和扫描包等等,都可以使用注解来完成。
spring-config.xml文件通常被称为核心配置文件,Java代码来取代XML文件之后,对应的Java类通常被称为Java配置中心(Java-configuration)。使用@Configuration注解来支持Java配置中心的工作,具有@Configuration注解的类也被称为Java配置类。@Configuration就是类似于XML文件中的< beans >标签。
此前我们的@Bean都是和@Component搭配使用,实际上,@Bean更常和@Configuration搭配使用,通常对于外部jar包中的bean我们会在java配置类中使用@Bean的方式交给IoC容器管理。
最简单的@Configuration可能如下所示:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
等价于下面的XML配置:
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
beans>
实际上@Configuration和@Component标注的类都可以作为配置类(内部都可以使用@Bean为容器提供对象),但是它们还是有很大的区别。
@Bean在@Component(@Controller、@Service、@Repository)注解标注的类中声明时,它们将在“精简(lite)”模式下处理;而在@Configuration注解标注的类中声明时,它们将在“完整(full)”模式下处理。
@Configuration标注的配置类,将会通过cglib生成代理对象,因此这要求配置类不能是final的类。代理类会对内部被@Bean标注的方法进动态代理, 当一个被@Bean标注的方法调用另一个被@Bean标注的方法时,实际上是调用的代理方法,该方法会直接返回IoC容器中的bean对象,因此在不同@Bean方法中调用另一个被@Bean标注的方法时,将会被重定向道向容器“要”对象,因此将会返回同一个对象。而@Component标注的类不会走cglib来代理@Bean 方法的调用,对于类可以是final的,不同@Bean方法中调用另一个被@Bean标注的方法时,就是相当于直接调用一次方法,因此会返回一个新的对象。为了避免这个问题,通常@Bean和@Configuration一起使用!
但是如果你希望避免任何cglib代理,那么请考虑在非@Configuration类中声明@Bean方法。然后,@Bean方法之间的跨方法调用不会被拦截。
如下案例,有一个ComponentBean和ConfigurationBean类,用于测试二者的区别:
/**
* 被@Component修饰的类可以是final的
*
* @author lx
*/
@Component
public class ComponentBean {
public static class ComponentBeanA {
public ComponentBeanA() {
}
}
public static class ComponentBeanB {
private ComponentBeanA componentBeanA;
public ComponentBeanB(ComponentBeanA componentBeanA) {
this.componentBeanA = componentBeanA;
}
}
@Bean("componentBeanA")
public ComponentBeanA getComponentBeanA() {
ComponentBeanA componentBeanA = new ComponentBeanA();
System.out.println("Component-----"+componentBeanA);
return componentBeanA;
}
/**
* 在该bean方法中尝试调用getComponentBeanA()
* 将会直接调用该方法获取一个新的对象,因此会打印三次,
* 它和容器中的componentBeanA不是同一个对象
*/
@Bean
public ComponentBeanB getComponentBeanB() {
ComponentBeanA componentBeanA = getComponentBeanA();
System.out.println("Component-----"+componentBeanA);
return new ComponentBeanB(componentBeanA);
}
}
//------------------------------------
/**
* 被@Configuration修饰的类不可以是final的
*
* @author lx
*/
@Configuration
public class ConfigurationBean {
public static class ConfigurationBeanA {
public ConfigurationBeanA() {
}
}
public static class ConfigurationBeanB {
private ConfigurationBeanA configurationBeanA;
public ConfigurationBeanB(ConfigurationBeanA configurationBeanA) {
this.configurationBeanA = configurationBeanA;
}
}
@Bean("configurationBeanA")
public ConfigurationBeanA getConfigurationBeanA() {
ConfigurationBeanA configurationBeanA = new ConfigurationBeanA();
System.out.println("Configuration-----" + configurationBeanA);
return configurationBeanA;
}
/**
* 在该bean方法中尝试调用getConfigurationBeanA()
* 将会走代理方法获取容器中的对象,因此会打印两次,
* 它们是同一个容器中的对象
*/
@Bean
public ConfigurationBeanB getConfigurationBeanB() {
ConfigurationBeanA configurationBeanA = getConfigurationBeanA();
System.out.println("Configuration-----" + configurationBeanA);
return new ConfigurationBeanB(configurationBeanA);
}
}
测试:
@Test
public void componentAndConfigurationBean() {
ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml");
System.out.println("--------------------");
System.out.println(ac.getBean("componentBean", ComponentBean.class));
System.out.println(ac.getBean("componentBeanA", ComponentBean.ComponentBeanA.class));
//输出configurationBean,在控制台可以明显看出来是一个CGLIB代理类对象
System.out.println(ac.getBean("configurationBean", ConfigurationBean.class));
System.out.println(ac.getBean("configurationBeanA", ConfigurationBean.ConfigurationBeanA.class));
}
结果如下:
Component-----com.spring.core.ann.ComponentBean$ComponentBeanA@c267ef4
Component-----com.spring.core.ann.ComponentBean$ComponentBeanA@30ee2816
Component-----com.spring.core.ann.ComponentBean$ComponentBeanA@30ee2816
Configuration-----com.spring.core.ann.ConfigurationBean$ConfigurationBeanA@7ed7259e
Configuration-----com.spring.core.ann.ConfigurationBean$ConfigurationBeanA@7ed7259e
--------------------
com.spring.core.ann.ComponentBean@5fdcaa40
com.spring.core.ann.ComponentBean$ComponentBeanA@c267ef4
com.spring.core.ann.ConfigurationBean$$EnhancerBySpringCGLIB$$4b2d6e0a@6dc17b83
com.spring.core.ann.ConfigurationBean$ConfigurationBeanA@7ed7259e
下面了我们来看如何基于Java配置类来启动和实例化IoC容器!
如果我们使用Jasva配置类来代替XML文件,那么我们的IoC容器就不能是ClassPathXmlApplicationContext,而是使用在Spring3.0的时候推出的AnnotationConfigApplicationContext。它是ApplicationContext的一个实现,他可以接收@Configuration, @Component ,和使用JSR-330元注解的类作为输入。
当@Configuration标注的类作为输入提供时,@Configuration类本身将被注册为一个 bean ,并且类内声明的 @Bean 方法也被注册成为一个 bean。当@Component和JSR-330类作为输入时,它们同样被注册为一个bean。在这些bean的所在类中,并可以使用诸如@Autowired或@Inject之类的注解用于依赖注入。
使用起来很简单,类似于XML,不过只需要传递@Configuration、@Component(@Controller、@Service、@Repository)、JSR-330注解注解标注的类的class即可:
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(JSR330Demo.class,ConfigurationBean.class,ComponentBean.class);
当然也可以初始化无参容器,然后使用register(Class>…)方法对其进行配置:
AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext();
ac2.register(JSR330Demo.class,ConfigurationBean.class,ComponentBean.class);
ac2.refresh();
使用AnnotationConfigApplicationContext启动时,此前的JSR330Demo将报错,需要注释掉有参构造器。开发中不建议使用JSR-330注解,Spring提供的注解更加强大。
上面的做法还是太麻烦了,如果有很多这样的类,那怎么办,难道还要一个一个的写吗?自然不需要,Spring也提供了扫描包的注解@ComponentScan。
@Configuration
@ComponentScan("com.spring.core.*")
public class JavaConfig {
}
这就等于XML中的component-scan的功能:
<context:component-scan base-package="com.spring.core.*"/>
此时,我们只需要传递JavaConfig.class就行了:
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
同样,AnnotationConfigApplicationContext 还提供了scan(String…)方法和component-scan配置、@ComponentScan注解一样的功能:
AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext();
ac2.scan("com.spring.core.*");
ac2.refresh();
AnnotationConfigWebApplicationContext是专门为web开发提供的ApplicationContext。当你配置ContextLoaderListener,或者DispatcherServlet时,可以使用它,也可以是不使用。
Spring Boot项目中,我们通常也是使用注解配置包扫描!@ComponentScan将会扫描指定包下面的任何基于@Component元注解或者JSR-330注解的类,这些类将被注入到Spring容器中。另外,@Configuration也是基于@Component元注解的:
因此,如果@Configuration类也处于扫描包的范围之类,那么@Configuration类也会被作为一个bean注入到Spring容器中,因此也支持使用@Value、@Autowired等注解,而它内部的所有@Bean方法也都被处理并注册到为容器中(记住@Configuration不同@Bean方法之间的调用返回的是同一个容器中的对象)。
此前我们就简单介绍过@Bean注解,@Bean是方法级的注解,和XML中的< bean >标签有同样的作用。现在来看看它的一些属性和其他用法。
@Bean注解定义的任何类中都支持常规的生命周期回调,并且可以使用jsr-250中的@PostConstruct和@PreDestroy注解。
@Bean注解定义的任何类中还支持Spring的回调。如果bean实现了InitialingBean、DisposableBean或Lifecycle接口,那么容器将会在正确的时间调用它们各自的回调方法。
上面的操作此前就讲过了,现在我们要讲的是@Bean的initMethod和destroyMethod特有属性,实际上也是回调方法的定义,这类似于基于XML配置的init-method和destroy-method属性。
如下案例,用于测试@Bean的initMethod和destroyMethod属性:
public class JavaConfigOne {
public JavaConfigOne() {
System.out.println("JavaConfigOne constructor");
}
public void init() {
System.out.println("JavaConfigOne init");
}
public void destroy() {
System.out.println("JavaConfigOne destroy");
}
}
/**
* @author lx
*/
@Configuration()
@ComponentScan("com.spring.core.*")
public class JavaConfig {
/**
* 回调
*/
@Bean(initMethod = "init",destroyMethod = "destroy")
public JavaConfigOne javaConfigOne(){
return new JavaConfigOne();
}
}
测试:
@Test
public void javaConfigBeanCallback() {
AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext(JavaConfig.class);
ac2.close();
}
结果如下:
JavaConfigOne constructor
JavaConfigOne init
JavaConfigOne destroy
默认情况下,使用@Bean标注的方法的名称作为结果bean的名字。但是可以使用name属性覆盖,另外也可以传递一个字符串数组,提供多个名字, bean名字重复不会报错,但是会发生意想不到的问题,比如通过某个名字取到的确实另外的bean实例,一定要主动避免不同@Bean的名字重复。
@description注解用于向@Bean(包括@Component)天加文档描述,在jmx中更可能会用到。
@Bean
@Description("bean name 默认方法名")
public JavaConfigOne getJavaConfigOne() {
return new JavaConfigOne();
}
/**
* bean name
*/
@Bean(name = {
"javaConfigOne2", "javaConfigOne3"})
@Description("指定bean name")
public JavaConfigOne getJavaConfigOne2() {
return new JavaConfigOne();
}
@Bean的autowire属性用于确定是否自动注入,即 bean 是否由 Spring 容器使用 setter 自动注入其依赖项,或者指定byName还是byType。很明显,这里的autowire属性和XML配置中的autowire属性有一些区别,可选配置更少!
@Bean的autowire属性默认为NO,即不自动注入。可选BY_NAME,根据name注入,根据set方法的“set”后面的字符串去匹配bean name,没有匹配到就不注入,或者BY_TYPE,通过setter方法的参数类型自动注入,如果有多个同类型的bean,则抛出异常!
无论是BY_NAME还是BY_TYPE,匹配到多个方法就会将这些方法全部执行。自 Spring 5.1 开始,已经使用@Bean工厂方法参数制动注入和@Autowired注入取代基于名称/类型的 autowire属性自动注入!
在JavaConfig中加入如下配置:
@Bean(autowire = Autowire.BY_NAME)
@Description("ByName自动注入")
public JavaConfigAutowire1 javaConfigAutowireByName() {
return new JavaConfigAutowire1();
}
// @Bean(autowire = Autowire.BY_TYPE)
// @Description("ByType自动注入,多个相同类型的bean将抛出异常")
// public JavaConfigAutowire1 javaConfigAutowireByType() {
// return new JavaConfigAutowire1();
// }
@Bean
public JavaConfigAutowire2 javaConfigAutowire2() {
return new JavaConfigAutowire2();
}
@Bean
public JavaConfigAutowire2 javaConfigAutowire221() {
return new JavaConfigAutowire2();
}
public static class JavaConfigAutowire1 {
private JavaConfigAutowire2 javaConfigAutowire2;
//无论是BY_NAME还是BY_TYPE,匹配到多个方法就会将这些方法全部执行
//该属性自Spring 5.1开始已经废弃
public void setJavaConfigAutowire2(JavaConfigAutowire2 xxx) {
System.out.println("setJavaConfigAutowire2");
this.javaConfigAutowire2 = xxx;
}
public void setJavaConfigAutowire221(Object xxx) {
System.out.println("setJavaConfigAutowire221");
this.javaConfigAutowire2 = (JavaConfigAutowire2) xxx;
}
@Override
public String toString() {
return "JavaConfigAutowire1{" +
"javaConfigAutowire2=" + javaConfigAutowire2 +
'}';
}
}
public static class JavaConfigAutowire2 {
}
结果如下:
setJavaConfigAutowire2
setJavaConfigAutowire221
JavaConfigAutowire1{
javaConfigAutowire2=com.spring.core.ann.JavaConfig$JavaConfigAutowire2@6580cfdd}
com.spring.core.ann.JavaConfig$JavaConfigAutowire2@7e0b85f9
com.spring.core.ann.JavaConfig$JavaConfigAutowire2@6580cfdd
与在XML的配置中使用< import >元素来帮助模块化配置一样,@Import注解允许从另一个配置类加载@Bean定义以及其他配置信息,即引入其他配置类。这样在实例化容器时,不需要显示的引入其他的配置类了,实际上@ComponentScan也有这个功能,因为其他配置类也是一个bean,如果其他配置类不在包扫描的范围里面,那么就可以使用@Import注解了。被引入的其他配置类上,可以不用写@Configuration注解。当然,写上也没问题。
@ImportResource则用于导入Spring的配置文件,让配置文件里面的内容生效,当某些配置写在外部XML中而容器使用注解的容器时,就可以使用@ImportResource将外部配置导入进来;Spring Boot里面没有Spring的XML配置文件,我们自己编写的配置文件,也不能自动识别;想让Spring的配置文件生效,就需要@ImportResource将其他配置文件加载进来。
本次我们介绍了Spring 5.x的IoC核心容器的基于注解和Java配置的基本用法,回想上文中的基于XML的配置,这样看起来基于注解的配置却是简单得多。
回到最开始的问题,即注解配置是否比XML配置"更好"。从目前的Spring发展趋势来看,似乎注解配置更占上峰。
根据我本人的项目经验,对于传统的SSM项目,一般我们自己写的业务类,比如Controller、Service、Mapper等模块的类都是通过注解方式配置的,这样可以让我们省去很多的配置,而对于Spring及其相关组件的核心配置,比如component-scan扫描包路径(用来支持相关注解)、数据库连接、mvc相关组件(annotation-driven、视图解析器、资源处理)、文件上传处理等等都是采用XML配置的,对于外部组件的集成,比如redis、solr等等也都是有专门的XML配置文件。
后来到了Spring Boot项目,那就更简单了。此前的很多组件的配置文件都不用写了,比如mvc的配置、redis的配置……,因为Spring Boot帮我们做了。我们只需要将一些硬编码的配置信息放在指定的properties/yml文件中以指定的key保存,Spring Boot即可进行自动配置。就算是我们需要自己配置,也是大多采用本次讲的Java Config读取properties中的配置然后使用@Bean注解的形式,不再是XML配置文件的形式了。
本文只是开始,仅仅是单体应用,只是最基础的IoC部分。只有经历过SSM中的繁琐的组件配置,才能明白Spring Boot搭建一个企业级项目到底有多轻松,这些东西我们会在后面的文章中一一见识到!
相关文章:
注解:深入理解Java中的注解的概念、用法以及案例演示
XML:Spring 5.x 学习(2)—两万字的IoC入门以及基于XML的IoC配置全解
https://docs.spring.io/
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!