基于最新Spring 5.x,详细介绍了Spring入门案例的搭建,以及基于XML的核心IoC机制的配置和使用。适合Spring初学者。
本次我们介绍Spring核心 IoC机制,以及基于XML的IoC机制的核心配置方式,包括bean的配置、依赖项注入配置,以及各种常用配置信息。没有讲过多源码,提供了大量的案例,对于会使用Spring的人来说可能比较啰嗦,但是比较适合Spring初学者!关于Spring的介绍:Spring 5.x 学习(1)—Spring与Spring Framework的概述。
Spring第一例,我们来学习如何通过maven来搭建Spring项目,并且从通过IoC机制从IoC容器中获取对象(maven学习资料)。首先创建一个空的maven项目: 到此空的maven项目创建完毕! 我们在pom.xml中添加Spring核心模块功能的坐标依赖:
<properties> <spring-framework.version>5.2.8.RELEASE</spring-framework.version> </properties> <dependencies> <!--spring 核心组件所需依赖--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring-framework.version}</version> <scope>compile</scope> </dependency> </dependencies>只需要这一个依赖即可,实际上它会将其他的核心依赖都一起引入进来的: 可以看到这就是Spring的Core technologies模块的核心功能依赖了。然后添加一个Bean类。
/** * @author lx */ public class HelloSpring { private String hello = "hello"; public void say() { System.out.println("hello"); } public String getHello() { return hello; } public void setHello(String hello) { this.hello = hello; } public HelloSpring(String hello) { this.hello = hello; System.out.println("初始化"); } public HelloSpring() { System.out.println("初始化"); } }在resources目录下面新增一个配置文件,命名建议spring-config.xml或者applicationContext.xml,这就是Spring的核心配置文件! 然后,就是重头戏了,我们在配置文件里加入一个< bean >标签。
<!--加入bean标签--> <bean id="helloSpring" class="com.spring.core.HelloSpring"/>最后加入一个测试类: 编写我们的代码:
/** * @author lx */ public class SpringCoreFirst { public static void main(String[] args) { //通过配置文件创建容器对象 ApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml"); //向容器要对象 返回的是object类型,可以强转 HelloSpring helloSpring = (HelloSpring) ac.getBean("helloSpring"); //此方法直接返回对应的类型的对象,只有一个该类型对象的话可以省略前面的name或id HelloSpring helloSpring1 = ac.getBean("helloSpring", HelloSpring.class); System.out.println(helloSpring); System.out.println(helloSpring1); System.out.println(helloSpring.getHello()); System.out.println(helloSpring1.getHello()); } }运行之后,有如下输出:
初始化 com.spring.core.HelloSpring@754ba872 com.spring.core.HelloSpring@754ba872 hello hello到此我们的第一个Spring最简单的IoC案例搭建、运行完毕!
Spring的核心机制就是Inversion of Control (IoC),中文译名“控制反转”。控制反转就是将对象创建的方式、属性设置方式反转,以前是开发人员自己通过new控制对象的创建,自己为对象属性赋值。使用Spring之后,将对象以及属性的创建和管理交给了Spring,由Spring来负责控制对象的生命周期、属性控制以及和其他对象间的关系,达到类与类之间的解耦功能,同时还能实现类实例的复用。 dependency injection (DI),中文译名依赖注入。在Spring官方文档中有这样一句话:“IoC is also known as dependency injection (DI)”,即IoC也被称为DI。DI是Martin Fowler 在2004年初的一篇论文中首次提出的,用于具体描述一个对象获得依赖对象的方式,不是自己主动查找和设置(比如new、set),而是被动的通过IoC容器注入(设置)进来。
Spring中管理对象的容器称为IoC容器,IoC容器负责实例化、配置和组装bean。 org.Springframework.beans和org.Springframework.context包是Springframework的IoC容器的基础。 IoC是一个抽象的概念,具体到Spring中就是以代码的形式实现的,Spring提供了许多IoC容器的实现,其核心是BeanFactory接口以及它的实现类。 BeanFactory接口可以理解为IoC容器的抽象,提供了IoC容器的最基本的功能,比如对单个bean的获取、对bean的作用域判断、获取bean类型、获取bean别名等等功能。BeanFactory直译过来就是Bean工厂,实际上IoC容器中bean的获取就是一种典型的工厂模式,里面的Bean常常就是单例的(当然也可以是其它类型的)。简单的说,IoC容器可以理解为一个大的Map,我们通过配置的id或者name或者其他唯一标识就可以从容器中获取到对应的对象(当然实际上没这么简单,后面讲源码的时候会仔细分析,但是肯定是用到了Map作为容器的)! BeanFactory仅仅作为IoC容器的超级接口,但是真正可用的容器实现却不是它,而是它的一系列子类。BeanFactory有两个主要的容器实现:DefaultListableBeanFactory(类)和ApplicationContext(接口)。这里没有讲解源码,后面会讲到,现在不必过于深究!
DefaultListableBeanFactory是IoC容器的一种真正实现,也是原始的默认实现,通常作为自定义BeanFactory的父类。它通过Resource加载Spring的xml配置信息,通过XmlBeanDefinitionReader解析配置xml文件,随后Bean信息会被存储到IoC容器中,启动IOC容器就可以使用getBean方法从IOC容器中获取bean对象。 DefaultListableBeanFactory加载Bean对象的方式被称为“消极/懒加载”,消极加载在启动IoC容器的时只是将配置文件中的配置信息加载进容器,而不会创建容器中所配置的bean对象,当使用getBean方法访问对象的时候才创建所需的对象。 DefaultListableBeanFactory的使用如下(基于第一个Spring项目):
@Test public void test() { //懒加载 Resource res = new ClassPathResource("spring-config.xml"); DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory(); XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(defaultListableBeanFactory); xmlBeanDefinitionReader.loadBeanDefinitions(res); //将这两行getBean代码注释掉再运行,IoC容器将不会真正的创建对象 HelloSpring helloSpring = defaultListableBeanFactory.getBean("helloSpring", HelloSpring.class); HelloSpring helloSpring2 = defaultListableBeanFactory.getBean("helloSpring", HelloSpring.class); System.out.println(helloSpring); System.out.println(helloSpring2); }Spring中还有一个XmlBeanFactory容器,继承了DefaultListableBeanFactory,实际上XmlBeanFactory就是对DefaultListableBeanFactory和XmlBeanDefinitionReader的封装调用而已,从Spring 3.1开始就已被废弃。XmlBeanFactory的使用如下:
@Test public void test2() { //懒加载 Resource res = new ClassPathResource("spring-config.xml"); BeanFactory ioc = new XmlBeanFactory(res); HelloSpring helloSpring = ioc.getBean("helloSpring", HelloSpring.class); HelloSpring helloSpring1 = ioc.getBean("helloSpring", HelloSpring.class); System.out.println(helloSpring); System.out.println(helloSpring1); }DefaultListableBeanFactory和XmlBeanFactory都是是适用于单体应用的IoC容器,而且用的比较少。
org.Springframework.context.ApplicationContext接口是org.Springframework.beans.factory.BeanFactory的子接口,它继承了BeanFactory的全部功能,负责实例化、配置和组装bean,同时添加了与Spring AOP集成、消息资源处理(用于国际化)、事件发布、应用层特定的上下文WebApplicationContext等等新功能。 ApplicationContext通过读取配置元数据获取关于要实例化、配置和组装的对象的指令。配置元数据以XML、Java注释或Java代码来表示,它定义了组成应用程序的对象以及这些对象之间的丰富依赖关系,实际上我们案例中的xml中的bean配置方式就是一种配置原数据。 由于ApplicationContext包含BeanFactory的所有功能,并且还包含更多的功能,现在,Spring推荐我们使用ApplicationContext的实现类来作为IoC容器,ApplicationContext提供了很多IoC容器实现。 ClassPathXmlApplicationContext主要是从类路径去加载配置文件,也支持从文件路径加载配置文件,这就是Spring案例中的方法。FileSystemXMLApplicationContext主要是从文件路径去加载配置文件,也支持读取类路径下的配置文件,它们都是适用于单体应用的IoC容器。还有一个XmlWebApplicationContext,它是专门为web开发所准备的,用于通过监听器启动并加载web根目录下的配置文件信息,这一个IoC容器也是我们后面源码学习的重点。 ApplicationContext采用的是非消极加载,也就就是说在IoC容器启动的时候就将配置的所有默认对象都创建起来并保存在容器中。并且默认的懒加载和饿加载的对象都是单例的,构造方法只被调用一次!
如同我们的案例一样,当我们配置好需要交给IoC管理的对象,然后启动IoC容器,此时我们就可以直接从IoC容器中获取我们想要的对象了。其中,交给IoC容器管理的对象被称为bean,或者说bean是一个由SpringIOC容器实例化、组装和管理的对象。 Spring官方文档将我们定义的bean以及它们的依赖关系信息称为配置元数据(configuration metadata),配置元数据可以以XML、Java注释或Java代码来表示,它定义了组成应用程序的对象以及这些对象之间的丰富依赖关系。
基于XML的配置:最原始的Spring配置方式,现正在被取代!基于注解的配置:Spring 2.5引入了对基于注解的配置元数据的支持。比如@Autowired、@PostConstruct、@PreDestroy方法。基于Java的配置:从Spring 3开始,Spring JavaConfig项目提供的许多特性成为核心Spring框架的一部分。因此,可以使用Java而不是XML文件来定义应用程序的bean。比如@Configuration、@Bean、@Import和@DependsOn注解。实际上,我们定义的配置元数据最开始被加载到Spring中之后会被转变为BeanDefinition,BeanDefinition会记录下所有解析到的bean定义,将解析结果保存起来的好处就是此后就不至于每次用到配置信息的时候都去解析一遍配置元数据。 BeanDefinition中包含以下数据:
包限定类名:通常是要定义的 bean 的实际实现类。Bean所属的包的全限定类名:表示bean的实际类型。Bean行为的配置元素:用于说明bean在容器中的作用范围、生命周期回调函数等。对 Bean完成其工作所需的其他豆类的引用。这些引用也称为协作者或依赖项。要在新创建的对象中设置的其他配置设置。例如,用于管理连接池的 bean中的连接数的大小限制。……其他属性。配置元数据信息被记录到BeanDefinition容器中之后,随即启动IoC容器,随后通过IoC容器才会真正的开始依赖注入的过程,即bean的初始化,以及随后的将依赖关系(依赖的bean)注入到bean中,这里实际上涉及到源码原理,后面会专门讲,现在不必过于深究! 现在我们以ApplicationContext为容器,以最基础的XML配置的方式来介绍怎么定义配置元数据!随后的而文章中我们会介绍使用注解和Java代码的方式怎么定义配置元数据!
基于XML的元数据配置是最原始的一种配置方式,到今天看起来有点过时了,但是这对于我们后面学习基于注解和Java代码方式的元数据配置有一定帮助! 基于XML的元数据配置必定离不开XML标签,Spring为bean的配置提供了多个标签和多个属性可以选择。 < beans >标签通常作为< bean >和其他标签的容器,并且作为文档中的根元素。< beans >标签可以为内部的全部< bean >标签提供默认值,< beans >标签也能够嵌套使用,用来为部分< bean >标签提供默认值。在XML中,使用< bean >标签表示将一个bean交给IoC容器管理。另外还有一个< alias >标签为< bean >指定别名。
通常,一个企业级的项目的不同模块可能会使用不同的XML配置文件,这有利于分清楚每个模块自己的配置。 我们可以在使用ApplicationContext的时候,在构造函数中传递多个XML文件地址参数来将这些文件都读取。当然如果不想使用构造函数传递多个xml文件地址,我们可以使用一个或多个< import >标签将其他XML配置文件的信息导入到一个配置文件中。 例如我们现在增加一个名为spring-config2.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/Spring-beans.xsd"> <bean name="helloSpring2" class="com.spring.core.HelloSpring"/> </beans>然后我们使用构造器传递两个资源的路径,即可解析到这两个配置文件:
@Test public void constructor() { //构造函数传递多个参数 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml","spring-config2.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); }结果如下:
初始化 初始化 [helloSpring, helloSpring2]或者在spring-config.xml文件中使用< import resource=“spring-config2.xml”/>来引入另一个配置文件,随后的构造函数中我们只需要传递spring-config.xml文件路径即可解析到这两个配置文件:
<import resource="spring-config2.xml"/> @Test public void imports() { //构造函数传递1个参数 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); }结果如下:
初始化 初始化 [helloSpring, helloSpring2]在IoC容器中,< bean >标签的name和id属性可以为bean命名,通过名字可以找容器获取这个bean的实例!id是全局唯一的,name可以有多个,可以通过英文逗号,分号或者空格来区分多个name。同一个bean的id和name可以一样,不同的bean的name和id都不能一样。
<bean id="helloSpring" name="helloSpring helloSpring3" class="com.spring.core.HelloSpring"/>当没有指定name或者id时,IoC容器会为Bean自动分配一个唯一名字,但是如果希望依靠ref注入bean,那么仍然需要命名。 对于XML装配的bean(使用< bean >标签),IoC的默认命名规则是: 如果没有指定id或者name,那么将使用“类的全路径名#0”、“类的全路径名#1”、“类的全路径名#2”……的方式来为bean命名,有n个没有命名的同类型bean,那么名字后面就是从[0,n-1]类似于索引递增的进行命名,如果中途遇到同名bean,那么跳过这个索引,使用下一个。 另外,标签还可以为bean定义别名,别名和bean的名称具有同样的效果!
加入如下配置:
<bean name="com.spring.core.HelloSpring#1 helloSpring " class="com.spring.core.HelloSpring"/> <!--加入两个未命名的Bean--> <bean class="com.spring.core.HelloSpring"/> <bean class="com.spring.core.HelloSpring"/> <!--别名--> <alias name="helloSpring" alias="helloSpring2"/>测试:
@Test public void idName() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml"); HelloSpring helloSpring0 = ac.getBean("com.spring.core.HelloSpring#1", HelloSpring.class); HelloSpring helloSpring1 = ac.getBean("helloSpring", HelloSpring.class); HelloSpring helloSpring4 = ac.getBean("helloSpring2", HelloSpring.class); HelloSpring helloSpring2 = ac.getBean("com.spring.core.HelloSpring#0", HelloSpring.class); HelloSpring helloSpring3 = ac.getBean("com.spring.core.HelloSpring#2", HelloSpring.class); System.out.println(helloSpring0); System.out.println(helloSpring1); System.out.println(helloSpring4); System.out.println(helloSpring2); System.out.println(helloSpring3); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); }结果如下,都能够获取成功:
初始化 初始化 初始化 HelloSpring{hello='hello'} HelloSpring{hello='hello'} HelloSpring{hello='hello'} HelloSpring{hello='hello'} HelloSpring{hello='hello'} [com.spring.core.HelloSpring#1, com.spring.core.HelloSpring#0, com.spring.core.HelloSpring#2]在XML文件中,< bean >标签不是一个对象,其本质上是用来描述创建一个或多个对象的方式。当需要创建Bean的时候,容器会查看bean的创建方式,并使用由该bean定义封装的配置元数据来创建实际对象。 使用XML配置的bean,通常至少需要指定class属性(实例工厂方法和parent继承< bean >除外),这个class属性(在内部对应BeanDefinition实例上的Class属性)表示IoC容器实例化的对象的类型。 IoC容器在帮我们实例化bean对象的方法包括构造器、静态工厂、实例工厂三种,我们可以指定实例化方式。
大多数情况下,比如我们前面写的的所有案例。IoC容器使用构造器来创建bean实例,底层是基于反射的机制。因此我们必须提供无参构造器或者对应的参数的构造器,否则将抛出异常! 我们将HelloSpring的无参构造器注释掉,然后运行上面随便一个案例,都会抛出下面的异常(针对无参构造器)。
No default constructor found; nested exception is java.lang.NoSuchMethodException: com.spring.core.HelloSpring.<init>()意思很明显就是找不到无参构造方法!
有时候我们想要在XML中配置一个类的内部类,那么当然可以,我们只需要在class中指明内部类的全路径类名就行了,和外部类路径一样的格式,当然我们也可以使用$将内部类名与外部类名分开(如果嵌套超过两层的内部类,那么就必须使用 $。另外,对于type属性,则同样需要使用 $)。 需要注意的是,对于非静态内部类,它的构造器实际上是需要依赖注入一个外部类对象(在反编译之后就能看见),即它的构造器不是无参构造器,因此我们需要使用constructor-arg注入一个外部类的bean,这样才不会报错!而静态内部类而不需要依赖外部类对象! 我们在HelloSpring中添加两个内部类:
public static class StaticInnerClass { public StaticInnerClass() { System.out.println("静态内部类初始化"); } } public class InnerClass { public InnerClass() { System.out.println("内部类初始化"); } }然后在配置文件中:
<!--静态内部类的初始化,和外部类一样的。 使用.或者$将内部类名与外部类名分开都行--> <bean class="com.spring.core.HelloSpring.StaticInnerClass"/> <bean class="com.spring.core.HelloSpring.StaticInnerClass"/> <bean class="com.spring.core.HelloSpring$StaticInnerClass"/> <bean class="com.spring.core.HelloSpring$StaticInnerClass"/> <!--非静态内部类的初始化需要依赖外部类对象--> <bean name="helloSpring " class="com.spring.core.HelloSpring"/> <bean class="com.spring.core.HelloSpring.InnerClass"> <!--属性依赖注入,后面会讲--> <constructor-arg ref="helloSpring"/> </bean> <bean class="com.spring.core.HelloSpring$InnerClass"> <!--属性依赖注入,后面会讲--> <constructor-arg ref="helloSpring"/> </bean>测试:
@Test public void inner() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); }结果如下,成功实例化:
初始化 静态内部类初始化 静态内部类初始化 静态内部类初始化 静态内部类初始化 内部类初始化 内部类初始化 [helloSpring, com.spring.core.HelloSpring.StaticInnerClass#0, com.spring.core.HelloSpring.StaticInnerClass#1, com.spring.core.HelloSpring$StaticInnerClass#0, com.spring.core.HelloSpring$StaticInnerClass#1, com.spring.core.HelloSpring.InnerClass#0, com.spring.core.HelloSpring$InnerClass#0]实际上就是工厂模式的应用,这种方式实际上是我们自己创建对象,但是由Spring调用,通过Spring标签获取!但是也只是调用一次创建方法,将创建的对象存入容器,后续同样只是取出(单例情况下)!注意创建对象的静态工厂并不会实例化! 使用静态工厂方法实例化bean时,在< bean >标签中的class属性不再是要获取的bean的全路径类名,而是静态工厂的全路径类名,同时使用名为factory-method的属性指定获取bean对象的工厂方法的名称(注意该方法必须是静态方法)。 首先我们要有一个静态工厂类,提供一个静态方法用于获取bean对象:
/** * @author lx */ public class HelloSpringStaticFactory { private static HelloSpring helloSpring = new HelloSpring(); /** * 静态工厂方法 * * @return 返回HelloSpring实例 */ public static HelloSpring getHelloSpring() { System.out.println("静态工厂方法"); return helloSpring; } public HelloSpringStaticFactory() { System.out.println("静态工厂不会初始化"); } }随后就是配置文件的编写:
<!--class表示静态工厂的全路径类名--> <!--factory-method表示静态工厂方法--> <bean name="helloSpring" class="com.spring.core.HelloSpringStaticFactory" factory-method="getHelloSpring"/>测试:
@Test public void staticMethod() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("helloSpring", HelloSpring.class)); }结果如下,成功实例化:
静态工厂方法 初始化 [helloSpring] com.spring.core.HelloSpring@45820e51很明显,静态工厂方法违背了Spring的初衷,因为我们还是要编写代码new对象,但是适用于那种需要对一个集合进行实例化的情况,因为集合的实例化如果使用配置文件编写的话,那也挺麻烦的。
实例工厂本身要实例化工厂类,随后从工厂实例的非静态方法中调用方法获取所需的bean。 使用实例工厂方法实例化bean时,在< bean >标签中的class属性置空,使用factory-bean的属性指定实例工厂的名字,使用factory-method属性指定非静态工厂方法的名称。factory bean虽然代表一个工厂,但是其实例仍然交给Spring管理,另外Spring中还有一个FactoryBean,这只是一个类! 首先我们要有一个实例工厂类,提供一个实例方法用于获取Bean对象:
/** * @author lx */ public class HelloSpringInstanceFactory { private static HelloSpring helloSpring = new HelloSpring(); /** * 静态工厂方法 * * @return 返回HelloSpring实例 */ public HelloSpring getHelloSpring() { System.out.println("实例工厂方法"); return helloSpring; } public HelloSpringInstanceFactory() { System.out.println("实例工厂会初始化"); } }随后就是配置文件的编写:
<!--实例化工厂--> <bean id="helloSpringInstanceFactory" class="com.spring.core.HelloSpringInstanceFactory"/> <!--factory-bean表示实例工厂的名字--> <!--factory-method表示实例工厂方法--> <bean name="helloSpring" factory-bean="helloSpringInstanceFactory" factory-method="getHelloSpring"/>测试:
@Test public void instanceMethod() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("spring-config.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("helloSpring", HelloSpring.class)); }结果如下,工厂和bean都成功实例化:
初始化 实例工厂会初始化 实例工厂方法 [helloSpringInstanceFactory, helloSpring] com.spring.core.HelloSpring@45820e51实际上,一个静态工厂或者实例工厂都可以配置多个工厂方法,那样更方便管理bean!另外,采用实例工厂方法实例化时,bean的初始化可以写在配置文件中(后面讲属性注入的时候会讲到),相比静态工厂方法更加灵活! 另外还有parent属性继承< bean >也可以实例化bean,这个后面会讲到!
一个企业级项目的bean不可能仅仅是像上面我们讲的案例那样,仅仅只实例化一个对象,实际上可能会依赖到很多的属性,下面来看看依赖属性的注入,这是IoC的重点。 依赖项注入(Dependency injection 、DI)是指对象仅仅通过构造函数参数、工厂方法的参数或从工厂方法构造或返回对象实例后在其上设置属性来定义其依赖(要使用)的其他对象。随后,IoC容器在创建bean时会自动注入这个bean的依赖项。这个过程基本上和bean主动通过类的构造器和setter方法来设置其依赖项的过程是相反的,因此DI也称为控制反转(Inversion of Control、IoC),或者说是IoC的实现。 我们前面讲的IoC案例,将bean的创建交给Spring来管理,这是一种IoC。但是bean之间的依赖关系却没有实现,此前我们在一个对象中依赖到另一个对象时,需要手动引入依赖的对象,有了DI之后,由来DI维护bean之间的依赖关系,并且自动注入需要的依赖项,这不也是一种IoC吗?而且使用DI之后,具有依赖关系的对象之间没有了强耦合关系,对象不需要主动查找、获取其依赖项,甚至不知道依赖项的具体位置,它们都在容器中,这一切交给DI就行了。 和bean的多种实例化的方式一样,属性依赖注入(DI)的方式也是有两种:构造器依赖注入和setter方法依赖注入。我们可以指定注入方式!
构造器依赖注入是由IoC容器调用带有许多参数的构造器来完成的,每个参数表示一个依赖项。这和调用带有特定参数的静态工厂方法来构造bean几乎是一样的。 首先我们创建一个ConstructorBased类,用来测试构造器依赖注入:
/** * @author lx * 构造器依赖注入 */ public class SimpleConstructorBased { /** * 依赖的两个属性 */ private String property1; private String property2; /** * 测试构造器依赖注入 */ public SimpleConstructorBased(String property1, String property2) { this.property1 = property1; this.property2 = property2; System.out.println("构造器依赖注入"); } @Override public String toString() { return "SimpleConstructorBased{" + "property1='" + property1 + '\'' + ", property2='" + property2 + '\'' + '}'; } }要想使用构造器依赖注入方式,需要依赖< bean >标签的子标签< constructor-arg >,一个< constructor-arg >标签表示一个属性。 这里我们新建一个DI.xml配置文件,用于测试依赖注入。在文件中配置我们的bean:
<!--构造函数属性注入--> <bean id="simpleConstructorBased" class="com.spring.core.SimpleConstructorBased"> <!--一个constructor-arg表示一个属性--> <constructor-arg value="v1"/> <constructor-arg value="v2"/> </bean>测试:
/** * @author lx */ public class DITest { /** * 构造器依赖注入 */ @Test public void simpleConstructorBased() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("simpleConstructorBased", SimpleConstructorBased.class)); } }结果如下,说明注入依赖属性成功:
构造器依赖注入 [simpleConstructorBased] SimpleConstructorBased{property1='v1', property2='v2'}我们配置的< constructor-arg >标签表示一个参数,容器会对该标签进行解析,以保证能够匹配到一个合适的构造函数。 解析方式有很多种,最直接的就是通过< constructor-arg >标签的value属性指定该标签对应的参数的值,容器会自动解析参数个数和参数值的类型并选择对应的构造器,上面的案例就是根据参数值value直接解析!
对于引用类型的参数如果它有明确的不同的类型,容器有可能能够正常解析,但是对于基本类型和String类型,容器有时候不能正常解析!特别是对于多个构造器并且具有相同参数个数的情况! 这里有一个SimpleConstructorBased2类:
/** * @author lx * 构造器依赖注入 */ public class SimpleConstructorBased2 { /** * 依赖的两个属性 */ private int property1; private String property2; private boolean property3; /** * 测试构造器依赖注入1 */ public SimpleConstructorBased2(int property1, String property2) { this.property1 = property1; this.property2 = property2; System.out.println("构造器依赖注入1"); } /** * 测试构造器依赖注入2 */ public SimpleConstructorBased2(int property1, boolean property3) { this.property1 = property1; this.property3 = property3; System.out.println("构造器依赖注入2"); } @Override public String toString() { return "SimpleConstructorBased2{" + "property1=" + property1 + ", property2='" + property2 + '\'' + ", property3=" + property3 + '}'; } }可以看到,它有两个构造器,参数都是基本类型,且参数个数一样多,现在我们来写配置文件:
<bean id="simpleConstructorBased2" class="com.spring.core.SimpleConstructorBased2"> <constructor-arg value="1"/> <constructor-arg value="true"/> </bean>我们可能会希望调用第二个构造器,运行测试一下:
/** * 构造器依赖注入 */ @Test public void simpleConstructorBased2() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("simpleConstructorBased2", SimpleConstructorBased2.class)); }会发现实际上调用了第一个构造器:
构造器依赖注入1 [simpleConstructorBased2] SimpleConstructorBased2{property1=1, property2='true', property3=false}这就是由于Spring不能分辨基本类型和String造成的不能确定到底使用哪一个构造器的情况,这种情况怎么办呢?我们可以使用< constructor-arg >标签的name属性来指定参数名字。 我们对配置文件进行改造,指定参数名。注意:是构造器的参数名字,而不是依赖的属性的名字:
<bean id="simpleConstructorBased2" class="com.spring.core.SimpleConstructorBased2"> <constructor-arg value="1" name="property1"/> <constructor-arg value="true" name="property3"/> </bean>继续测试之后结果如下,确实按照我们的要求调用了第二个构造器:
构造器依赖注入2 [simpleConstructorBased2] SimpleConstructorBased2{property1=1, property2='null', property3=true}指定参数名能在一定程度上解决找不到对应构造函数的情况。但是,有可能存在这样一种情况:多个构造器,具有相同的参数名和数量,但是参数类型不一致的情况,这样的情况下,仍然不能确定到底使用哪一个构造器。 这里有一个SimpleConstructorBasedx类:
/** * @author lx * 构造器依赖注入 */ public class SimpleConstructorBasedx { /** * 依赖的四个属性 */ private String property1; private String property2; private int property3; private boolean property4; /** * 测试构造器依赖注入1 */ public SimpleConstructorBasedx(String property1, boolean property2) { this.property1 = property1; this.property4 = property2; System.out.println("构造器依赖注入1"); } /** * 测试构造器依赖注入2 */ public SimpleConstructorBasedx(int property1, boolean property2) { this.property3 = property1; this.property4 = property2; System.out.println("构造器依赖注入2"); } /** * 测试构造器依赖注入3 */ public SimpleConstructorBasedx(String property1, String property2) { this.property1 = property1; this.property2 = property2; System.out.println("构造器依赖注入3"); } /** * 测试构造器依赖注入4 */ public SimpleConstructorBasedx(String property1, int property3, String property2) { this.property1 = property1; this.property2 = property2; this.property3 = property3; System.out.println("构造器依赖注入4"); } /** * 测试构造器依赖注入5 */ public SimpleConstructorBasedx(String property1, String property2, int property3) { this.property1 = property1; this.property2 = property2; this.property3 = property3; System.out.println("构造器依赖注入5"); } @Override public String toString() { return "SimpleConstructorBasedx{" + "property1='" + property1 + '\'' + ", property2='" + property2 + '\'' + ", property3=" + property3 + ", property4=" + property4 + '}'; } }对于前三个构造器,构造器的形参列表参数名字完全一致,现在我们来写配置文件:
<bean id="simpleConstructorBasedx" class="com.spring.core.SimpleConstructorBasedx"> <constructor-arg name="property1" value="1"/> <constructor-arg name="property2" value="true"/> </bean>我们可能想要的是调用第2个构造器,即为property3和property4属性注入值,测试一下:
@Test public void simpleConstructorBasedx() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("simpleConstructorBasedx", SimpleConstructorBasedx.class)); }运行之后我们发现实际上调用的第3个构造函数,即把它们都解析为了String类型:
构造器依赖注入3 [simpleConstructorBasedx] SimpleConstructorBasedx{property1='1', property2='true', property3=0, property4=false}这种情况怎么办呢?我们可以使用< constructor-arg >标签的type属性来指定参数类型,type的值为该属性类型的类的全路径名,基本类型就等于该类型的名字。 我们对配置文件进行改造,指定类型:
<bean id="simpleConstructorBasedx" class="com.spring.core.SimpleConstructorBasedx"> <constructor-arg name="property1" value="1" type="int"/> <constructor-arg name="property2" value="true" type="boolean"/> </bean>继续测试之后结果如下,确实按照我们的要求调用了第2个构造器:
构造器依赖注入2 [simpleConstructorBasedx] SimpleConstructorBasedx{property1='null', property2='null', property3=1, property4=true}虽然可以指定参数名和参数类型,但是在某些情况下仍然有问题,比如当存在两个构造器形参列表类型一致,但是参数顺序不一致,这样的情况下,Spring仍然不能确定到底使用哪一个构造器。 比如对于SimpleConstructorBasedx的第4、第5个构造器,它们的形参列表名字和类型一致,但是参数顺序不一致,我们对配置文件进行改造:
<bean id="simpleConstructorBasedx" class="com.spring.core.SimpleConstructorBasedx"> <!--一个constructor-arg表示一个属性--> <constructor-arg name="property1" value="xx" type="java.lang.String"/> <constructor-arg name="property3" value="1" type="int"/> <constructor-arg name="property2" value="yy" type="java.lang.String"/> </bean>运行之后,可能我们想要调用第4个构造器,但是实际上调用的第5个构造器:
构造器依赖注入5 [simpleConstructorBasedx] SimpleConstructorBasedx{property1='xx', property2='yy', property3=1, property4=false}这种情况怎么办呢?我们可以使用< constructor-arg >标签的index属性来指定参数出现的索引位置,index从0开始,0就表示第一个参数,1就表示第二个参数……以此类推。注意:你可以不把顺序写完整,Spring会自动寻找能通过你写的顺序识别的构造器,但是你不能把顺序写错了,那样找不到构造器就会报错! 我们对配置文件进行改造,指定参数顺序,这里我们发现只需要指定int类型的参数顺序1或者2,Spring就能分辨出两个构造器:
<bean id="simpleConstructorBasedx" class="com.spring.core.SimpleConstructorBasedx"> <!--一个constructor-arg表示一个属性--> <constructor-arg name="property1" value="xx" type="java.lang.String"/> <constructor-arg name="property3" value="1" type="int" index="1"/> <constructor-arg name="property2" value="yy" type="java.lang.String"/> </bean>继续测试之后结果如下,确实按照我们的要求调用了第4个构造器:
构造器依赖注入4 [simpleConstructorBasedx] SimpleConstructorBasedx{property1='xx', property2='yy', property3=1, property4=false}setter依赖注入是由IoC容器调用参数的setter方法完成,setter方法是在调用构造器以实例化bean之后完成的。 ApplicationContext对于它所管理的bean的支持同时基于构造器和基于setter方法的依赖注入。依赖注入的属性在开始都是value字符串被保存起来,随后会通过PropertyEditor(属性编辑器,Spring内部扩展了java的原生PropertyEditor)转换为对应的实际类型,这个转换过程我们一般不需要编写代码,由IoC容器自动转换,当然我们也可以定义自己的转换器。
setter方法实际上就是常说的get、set方法中的set方法,因此在使用setter注入的时候需要提供setXXX方法,这个XXX一般就是表示属性名,方法名应该按照Java方法名的规定来定义,属性的第一个字母大写,通常我们可以使用idea来自动生成set方法。 在XML文件中使用< bean >的子标签< property >来表示一个setter方法,name属性表示属性名 value 属性表示属性值。注意:实际上,name属性只要是使用setXXX方法除了前面的“set”后面的字符串部分都行,不一定是属性名。怎么说呢,setter注入就是调用setter方法,传递参数,至于setter的代码逻辑,则要看你怎么编写了…… 下面是一个测试类,将使用构造器和setter两种方式混合注入:
/** * @author lx */ public class SimpleSetterBased { /** * 依赖的5个属性 */ private String property1; private String property2; private int property3; private boolean property4; private int property5; /** * 构造器依赖注入 */ public SimpleSetterBased(String property1, String property2) { this.property1 = property1; this.property2 = property2; System.out.println("构造器依赖注入"); } //setter方法依赖注入,idea生成stter方法 public void setProperty3(int property3) { System.out.println("setter注入property3"); this.property3 = property3; } public void setPr11operty5(int property5) { System.out.println("setter注入property5"); this.property5 = property5; } public void setProperty4(boolean property4) { System.out.println("setter注入property4"); this.property4 = property4; } @Override public String toString() { return "SimpleSetterBased{" + "property1='" + property1 + '\'' + ", property2='" + property2 + '\'' + ", property3=" + property3 + ", property4=" + property4 + ", property5=" + property5 + '}'; } }配置文件:
<!--setter and constructor--> <bean id="simpleSetterBased" class="com.spring.core.SimpleSetterBased"> <!--构造器参数 name表示参数名 value 表示参数值--> <constructor-arg name="property1" value="xxx"/> <constructor-arg name="property2" value="yyy"/> <!--setter方法 name表示属性名 value 表示属性值--> <property name="property3" value="123"/> <property name="property4" value="true"/> <!--name还可以表示方法名除了set后面的部分,不一定是属性名--> <property name="Pr11operty5" value="321"/> </bean>测试:
@Test public void setter() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("simpleSetterBased", SimpleSetterBased.class)); }结果如下,我们成功注入了属性:
构造器依赖注入 setter注入property3 setter注入property5 setter注入property4 [simpleSetterBased] SimpleSetterBased{property1='xxx', property2='yyy', property3=123, property4=true, property5=321}使用静态工厂方法或者实例工厂方式实例bean时,同样可以注入依赖,方法上的参数可视为bean的依赖项,用于构造器注入,同样使用< constructor-arg >标签,而setter注入则不受影响。 下面是两个工厂:
/** * @author lx */ public class SimpleSetterBasedInstanceFactory { private static SimpleSetterBased simpleSetterBased; /** * 实例工厂依赖注入 */ public SimpleSetterBased getSimpleSetterBased(String property1, String property2) { System.out.println("实例工厂方法"); simpleSetterBased = new SimpleSetterBased(property1, property2); return simpleSetterBased; } } /** * @author lx */ public class SimpleSetterBasedStaticFactory { private static SimpleSetterBased simpleSetterBased; /** * 静态工厂依赖注入 */ public static SimpleSetterBased getSimpleSetterBased(String property1, String property2) { System.out.println("静态工厂方法"); simpleSetterBased = new SimpleSetterBased(property1, property2); return simpleSetterBased; } }配置文件:
<!--静态工厂依赖注入--> <bean id="staticSimpleSetterBased" class="com.spring.core.SimpleSetterBasedStaticFactory" factory-method="getSimpleSetterBased"> <!--构造器参数 name表示参数名 value 表示参数值--> <constructor-arg name="property1" value="xxx"/> <constructor-arg name="property2" value="yyy"/> <!--setter方法 name表示属性名 value 表示属性值--> <property name="property3" value="123"/> <property name="property4" value="true"/> </bean> <!--实例工厂依赖注入--> <bean class="com.spring.core.SimpleSetterBasedInstanceFactory" name="simpleSetterBasedInstanceFactory"/> <bean id="instanceSimpleSetterBased" factory-bean="simpleSetterBasedInstanceFactory" factory-method="getSimpleSetterBased"> <!--构造器参数 name表示参数名 value 表示参数值--> <constructor-arg name="property1" value="xxx"/> <constructor-arg name="property2" value="yyy"/> <!--setter方法 name表示属性名 value 表示属性值--> <property name="property3" value="123"/> </bean>测试:
@Test public void factory() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("instanceSimpleSetterBased", SimpleSetterBased.class)); System.out.println(ac.getBean("staticSimpleSetterBased", SimpleSetterBased.class)); }结果如下,我们成功注入了属性:
静态工厂方法 构造器依赖注入 实例工厂方法 构造器依赖注入 [staticSimpleSetterBased, simpleSetterBasedInstanceFactory, instanceSimpleSetterBased] SimpleSetterBased{property1='xxx', property2='yyy', property3=123, property4=false} SimpleSetterBased{property1='xxx', property2='yyy', property3=123, property4=true}容器执行 bean 依赖项解析简单流程如下:
ApplicationContext容器被实例化之后,它包含了所有bean的配置元数据。这些配置元数据可以通过XML、Java代码或注解来指定。对于每个bean,其依赖项以属性的set方法、构造函数参数或静态工厂方法的参数的形式表示。当实际创建 bean 时,这些依赖项将提供给 bean。每个需要注入的依赖项要设置的实际注入的value值,或对容器中另一个 bean 的ref引用(下面会讲),最开始统一为一个字符串格式最后,属性值会从字符串的描述转换为实际属性类型。通过value设置的值, Spring可以自动将以字符串格式提供的值转换为所有内置的类型,如int、long、String、boolean等。当然我们也可以自定义转换方式,比如字符串转换为Date时间类型!bean的依赖项及其依赖项的依赖项等等会在bean的创建之前被创建,因此,如果我们使用构造器注入,那么可能出现循环依赖(Circular dependencies)的情况。 例如:A 类需要依赖类 B 的实例,通过构造函数注入,B 类需要依赖类 A 的实例,也是通过构造函数注入。如果为要相互注入的类 A 和 B 配置 bean,这种类似于“蛋生鸡鸡生蛋”场景。IoC容器将在运行时检测到此循环引用,并抛出BeanCurrentlyCreationException。 一种解决方式就是使用setter注入,当两个互相依赖的bean都创建完毕之后,才会调用set方法进行依赖注入!
通常我们对于强制依赖项使用构造器,对于可选依赖项使用setter方法,当然也可以在setter方法上加上@Required注解使属性成为必需的依赖项。 Spring团队现在推荐使用构造器注入,构造器注入能够保证注入的组件不可变,并且确保需要的依赖不为null。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态,还能检测循环依赖。另外,大量的构造函数参数是一种糟糕的代码,这意味着类可能做了太多的事情承担了太多职责,应该重构以做适当的在责任分离。 Setter注入应该主要用于可在类内分配合理默认值的可选依赖项。否则,在代码使用依赖项的任何地方都必须执行非空检查。 Setter注入的一个好处是Setter方法使该类的对象能够在以后重新配置或重新注入。 有时,在处理没有源代码的第三方类时,只有一种选择。例如,如果第三方类不公开任何setter方法,那么构造器注入是唯一的可用方式。
上面我们学习了依赖注入的两种方式,现在我们来看看一些更详细的配置。
对于基本类型、String、包装类类型的属性,我们可以直接使用value属性的字符串值来描述具体的值,这样可读性也更强。在最后注入的时候Spring的转换服务会将这些值从 String 转换为属性或参数的实际类型。 并且Spring支持使用< value >标签表示具体的字面量值:
<bean id="simpleSetterBased" class="com.spring.core.SimpleSetterBased"> <constructor-arg name="property1"> <value>xxx</value> </constructor-arg> <constructor-arg name="property2"> <value>yyy</value> </constructor-arg> <!--setter方法 name表示属性名 value 表示属性值--> <property name="property3"> <value>123</value> </property> <property name="property4"> <value>false</value> </property> </bean>Spring容器支持通过PropertyEditor直接解析value中的特定格式的字符串字面量值,并转换为一个Properties集合。后面我们也会学习集合的注入方式,但是这是一个非常好用的快捷方式! 我们来测试一下,首先有一个PropertiesDI类,内部有一个Properties(Hashtable是Properties的父类)属性:
/** * @author lx */ public class PropertiesDI { private Hashtable properties; /** * setter */ public void setProperties(Properties properties) { this.properties = properties; } @Override public String toString() { return "PropertiesDI{" + "properties=" + properties + '}'; } }配置如下:
<!--properties--> <bean class="com.spring.core.PropertiesDI" id="propertiesDI"> <property name="properties"> <!--直接写配置即可,自动转换为Properties--> <value> ! 注释 # 注释 # “#”“!”开头的一行被算作注释不会解析。 # key和value可以使用 “=”、“:”、“ ”等符号分割,详见properties说明 key=value jdbc.driver.className=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/mydb ccc:ddd aaa bbb eee fff </value> </property> </bean>测试:
@Test public void properties() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("propertiesDI", PropertiesDI.class)); }结果如下,成功注入:
[propertiesDI] PropertiesDI{properties={jdbc.url=jdbc:mysql://localhost:3306/mydb, jdbc.driver.className=com.mysql.jdbc.Driver, eee=fff, key=value, aaa=bbb, ccc=ddd}}在< constructor-arg >、< property >、< entry >标签中有一个ref属性,用于将bean的指定属性的值设置为对容器管理的另一个bean的引用。这就是引用类型属性依赖的设置方式。被引用的bean是要设置其属性的bean的依赖项,在设置该属性之前,需要对其进行初始化。ref属性的值需要与引用的目标bean的id或者name属性中的一个值相同。 当然还有一个< ref >标签,可以作为< constructor-arg >、< property >以及某些集合标签的子标签,通过< ref >标签的bean属性也可以来指定引用的目标bean。< ref >标签允许在同一容器或父容器中创建对任何bean的引用,而不管它是否在同一XML文件中。bean属性的值需要与引用的目标bean的id或者name属性中的一个值相同。 如下案例,首先有一个RefDI类,用于ref测试:
/** * @author lx */ public class RefDI { private HelloSpring helloSpring1; private HelloSpring helloSpring2; public RefDI(HelloSpring helloSpring1, HelloSpring helloSpring2) { this.helloSpring1 = helloSpring1; this.helloSpring2 = helloSpring2; } @Override public String toString() { return "RefDI{" + "helloSpring1=" + helloSpring1 + ", helloSpring2=" + helloSpring2 + '}'; } }配置文件:
<!--ref--> <!--定义一个Bean--> <bean name="helloSpring3" class="com.spring.core.HelloSpring"/> <bean class="com.spring.core.RefDI" id="refDI"> <!--使用ref属性引用helloSpring3的bean--> <constructor-arg name="helloSpring1" ref="helloSpring3"/> <!--使用ref标签引用helloSpring3的bean--> <constructor-arg name="helloSpring2"> <ref bean="helloSpring3"/> </constructor-arg> </bean>测试:
@Test public void ref() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("refDI", RefDI.class)); }成功注入了其他bean:
初始化 [helloSpring3, refDI] RefDI{helloSpring1=HelloSpring{hello='hello'}, helloSpring2=HelloSpring{hello='hello'}}< bean >、< ref >等标签中还有一个parent属性,这个属性用于指定目标bean将使用父bean的属性和配置,除了autowire、scope和lazy init属性,parent属性用于相同属性以及值的复用。parent属性的值与目标父bean的id属性或name属性中的一个值相同。 对于parent继承,这里又分几种情况:
如果父bean有class属性,而子bean没有class属性,那么子bean就是和父bean同一个class类型,相当于创建两个相同的对象。如果父bean有class属性,而子bean也有class属性,那么允许它们是不同的类型,但是子bean必须含有父bean中定义的所有的注入方式。如果父bean没有class属性,那么子bean必须定义class属性,这个父bean实际上类似于一个属性和值的模版,仅仅被值bean引用,实现配置复用,不能实例化,(这时父bean必须添加abstract="true"属性,表示父bean不会被创建,类似于于抽象类,否则启动容器会尝试父bean,但是由于父bean没有class而抛出异常:No bean class specified on bean definition)。这种情况下,子bean同样必须含有父bean中定义的所有的注入方式。这里的父bean和子bean 以及“继承”,并不是Java中的继承关系,仅仅是复用了注入方式,精简了代码!如果子bean和父bean中注入对相同依赖同时注入的值的话,那么可能会相互覆盖对方的值。这根据依赖注入的先后顺序:父bean的构造器注入->子bean的构造器注入->父bean的setter注入->子bean的setter注入,排序在后面的对相同依赖的注入值将会覆盖之前注入的值!如下案例,首先有三个类:
/** * @author lx */ public class ParentOne { private String property1; public void setProperty1(String property1) { this.property1 = property1; } @Override public String toString() { return "ParentOne{" + "property1='" + property1 + '\'' + '}'; } }一个有意思的地方是,虽然ParentTwo继承了ParentOne,但是并没有继承私有属性property1,不过由于继承了setProperty1方法,因此仍然能够正常工作,这就是前面说的“子bean必须含有父bean中定义的所有的注入方式”的含义,对于setter方法注入来说,你没这个属性没关系,只要有个同名方法,参数类型能够兼容(从String转为参数类型)就不会报错,与返回值无关(见ParentThree)!
< idref >标签通常可以作为< constructor-arg >、< property >以及某些集合标签的子标签,用于将容器中另一个 bean的id或者name的字符串值(并不是引用)传递给< constructor-arg >、< property >以及某些集合标签,同时使用idref容器在部署的时候还会验证这个名称的bean是否真实存在(被定义了),这是一种防止错误的方法。该标签目前用的比较少。 如下案例,首先有一个IdrefCheck类,用于校验bean是否被定义了:
public class IdrefCheck { private String targetName; public void setTargetName(String targetName) { this.targetName = targetName; } @Override public String toString() { return "IdrefDI{" + "targetName='" + targetName + '\'' + '}'; } }配置文件:
<!--idref--> <!--定义一个Bean--> <bean name="helloSpring3" class="com.spring.core.HelloSpring" /> <!--idrefCheck校验bean--> <bean class="com.spring.core.IdrefCheck" name="idrefCheck"> <!--实际上就等于<property name="targetName" value="helloSpring3">--> <!--但是多了bean校验的功能--> <property name="targetName"> <idref bean="helloSpring3"/> </property> </bean>实际上< idref >的bean属性引用的值就是等于一个String类型的值,都是字符串,但是< idref >多了一个校验对应名称的bean是否存在的功能! 在idea中,如果idref的bean属性指定的bean名字不存在容器中,那么直接报红,如果运行,那么会抛出:Invalid bean name ‘helloSpring3’ in bean reference for bean property ‘targetName’。
@Test public void idref() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("idrefDI", IdrefCheck.class)); }结果如下:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'idrefDI' defined in class path resource [DI.xml]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanDefinitionStoreException: Invalid bean name 'helloSpring3' in bean reference for bean property 'targetName'在很久之前(Spring2.0之前的版本中),< idref >标签的一个常用的作用是在用在ProxyFactoryBean中定义的AOP拦截器里。指定拦截器名称时使用< idref >元素可防止你拼写错误拦截器ID。但是目前用的比较少了!
< bean >标签的内部可以使用< constructor-arg >、< property >以及某些集合标签,表示依赖注入。同样,在< constructor-arg >、< property >以及某些集合标签中也可以使用< bean >子标签,表示一个内部bean。原因很简单,如果我们注入的是一个对象,并且我们不想要通过ref引用其他已存在的bean,那么只有定义自己的内部的bean。 和“外部”bean的区别是,内部bean不需要定义id或者name属性,因为这个对象就相当于一个外部bean自己的对象。就算指定了,容器也不会使用这些值作为bean的名字,我们也不能通过IoC容器获取。容器在创建时也会忽略内部bean的scope作用域属性(后面会讲),因为内部 bean 始终是匿名的,并且始终使用外 bean 创建。无法独立访问内部bean,也无法将它们注入其他外部bean中。 如下案例,首先有一个InnerBean类,用于内部bean测试:
/** * 内部bean * * @author lx */ public class InnerBean { private InnerBeanInner innerBeanInner1; private InnerBeanInner innerBeanInner2; public void setInnerBeanInner1(InnerBeanInner innerBeanInner1) { this.innerBeanInner1 = innerBeanInner1; } public void setInnerBeanInner2(InnerBeanInner innerBeanInner2) { this.innerBeanInner2 = innerBeanInner2; } @Override public String toString() { return "InnerBean{" + "innerBeanInner1=" + innerBeanInner1 + ", innerBeanInner2=" + innerBeanInner2 + '}'; } public static class InnerBeanInner { private String property1; private int property2; public void setProperty1(String property1) { this.property1 = property1; } public void setProperty2(int property2) { this.property2 = property2; } @Override public String toString() { return "InnerBeanInner{" + "property1='" + property1 + '\'' + ", property2=" + property2 + '}'; } } }配置文件:
<!--内部bean--> <bean id="innerBean" class="com.spring.core.InnerBean"> <property name="innerBeanInner1"> <!--内部bean 不需要指定id或者name--> <bean class="com.spring.core.InnerBean.InnerBeanInner"> <property name="property1" value="aaa"/> <property name="property2" value="111"/> </bean> </property> <property name="innerBeanInner2"> <!--内部bean 指定id或者name也没用,不能通过容器获取到--> <bean id="innerBeanInner" class="com.spring.core.InnerBean.InnerBeanInner"> <property name="property1" value="bbb"/> <property name="property2" value="222"/> </bean> </property> </bean>测试:
@Test public void innerBean() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); InnerBean innerBean = ac.getBean("innerBean", InnerBean.class); System.out.println(innerBean); }结果:
[innerBean] InnerBean{innerBeanInner1=InnerBeanInner{property1='aaa', property2=111}, innerBeanInner2=InnerBeanInner{property1='bbb', property2=222}}Spring提供了详细的集合注入方式。< list >、< set >、< map >、< props >、< array >标签分别用来注入Java中的List、Set、Map、Properties、array类型的集合,主要是用于集合类型的依赖项的注入。因为集合的元素既可以是基本类型也可以是对象甚至集合,因此集合注入非常灵活。另外集合注入支持泛型转换,注入的时候会自动将value的字符串值转换为对应泛型类型! 如下案例,首先有一个CollectionDI类,用于集合注入测试:
/** * 集合注入 * * @author lx */ public class CollectionDI { //集合属性注入 private List list; private Set set; private Map map; private Properties properties; private Object[] array; public CollectionDI(List list, Set set, Map map, Properties properties, Object[] array) { this.list = list; this.set = set; this.map = map; this.properties = properties; this.array = array; } static class CollectionInner { private String property1; private int property2; public void setProperty1(String property1) { this.property1 = property1; } public void setProperty2(int property2) { this.property2 = property2; } @Override public String toString() { return "CollectionInner{" + "property1='" + property1 + '\'' + ", property2=" + property2 + '}'; } } @Override public String toString() { return "CollectionDI{" + "\n" + "list=" + list + "\n" + ", set=" + set + "\n" + ", map=" + map + "\n" + ", properties=" + properties + "\n" + ", array=" + Arrays.toString(array) + '}'; } }配置文件:
<!--Collection注入--> <bean id="collectionInner" class="com.spring.core.CollectionDI.CollectionInner"> <property name="property1" value="refs"/> <property name="property2" value="111"/> </bean> <bean id="collectionDI" class="com.spring.core.CollectionDI"> <!--list只有一个元素时,可以使用value属性赋值或者ref引用就行了--> <constructor-arg name="list"> <!--list标签表示list集合,用于定义多个元素--> <list> <!--value标签用于定义字面量的值作为集合元素--> <value>111</value> <!--也可以使用Bean标签定义一个bean(对象)作为集合元素--> <bean class="com.spring.core.CollectionDI.CollectionInner"> <property name="property1" value="list"/> <property name="property2" value="1"/> </bean> <!--也可以引用外部bean--> <ref bean="collectionInner"/> <value>null</value> <!--当然集合元素也可以定义集合--> </list> </constructor-arg> <!--set只有一个元素时,可以使用value属性赋值或者ref引用就行了--> <constructor-arg name="set"> <!--set标签表示set集合,用于定义多个元素--> <set> <!--value标签用于定义字面量的值作为集合元素--> <value>111</value> <!--也可以使用Bean标签定义一个bean(对象)作为集合元素--> <bean class="com.spring.core.CollectionDI.CollectionInner"> <property name="property1" value="set"/> <property name="property2" value="2"/> </bean> <!--也可以引用外部bean--> <ref bean="collectionInner"/> <value>null</value> <!--也可以使用idref,仅作为字符串--> <idref bean="collectionInner"/> <!--当然集合元素也可以定义集合--> </set> </constructor-arg> <constructor-arg name="map"> <!--map标签表示map集合--> <map> <!--map标签中首先需要定义entry标签,表示一个键值对--> <entry key="key" value="value"/> <!--key和value都可以使用标签--> <entry key-ref="collectionInner" value-ref="collectionInner"/> <!--key和value都可以引用外部bean--> <entry> <!--key可以使用内部bean,或者集合等等--> <key> <bean class="com.spring.core.CollectionDI.CollectionInner"> <property name="property1" value="mapkey"/> <property name="property2" value="3"/> </bean> </key> <!--注意value标签只能是注入字面量值,如果想要对象类型的value,那么直接使用Bean标签就行了--> <bean class="com.spring.core.CollectionDI.CollectionInner"> <property name="property1" value="mapvalue"/> <property name="property2" value="3"/> </bean> <!--value也可以是集合等等类型,但是只能有一个大标签--> <!--<map>--> <!-- <entry key="innermap" value="innermap"/>--> <!-- <entry key="inner2map" value="inner2map"/>--> <!--</map>--> <!--也可以使用idref,仅作为字符串--> <!--<idref bean="collectionInner"/>--> </entry> </map> </constructor-arg> <constructor-arg name="properties"> <!--props标签表示properties集合--> <props> <!--props标签中首先需要定义prop标签,表示一个String键值对--> <prop key="111">111</prop> <prop key="111">222</prop> <prop key="222">222</prop> <prop key="null">null</prop> </props> <!--实际上也可以放map,但是要求String类型的key和value--> <!--<map>--> <!-- <entry key="key" value="value"/>--> <!--</map>--> </constructor-arg> <!--数组只有一个元素时,可以使用value属性赋值或者ref引用就行了--> <constructor-arg name="array"> <!--array标签表示array数组,用于定义多个元素--> <array> <!--value标签用于定义字面量的值作为集合元素--> <value>111</value> <!--也可以使用Bean标签定义一个bean(对象)作为集合元素--> <bean class="com.spring.core.CollectionDI.CollectionInner"> <property name="property1" value="array"/> <property name="property2" value="4"/> </bean> <!--也可以引用外部bean--> <ref bean="collectionInner"/> <value>null</value> <!--也可以使用idref,仅作为字符串--> <idref bean="collectionInner"/> <!--当然集合元素也可以定义为集合--> </array> </constructor-arg> </bean>测试:
@Test public void collectionDI() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("collectionDI", CollectionDI.class)); }结果如下:
[collectionInner, collectionDI] CollectionDI{ list=[111, CollectionInner{property1='list', property2=1}, CollectionInner{property1='refs', property2=111}, null] , set=[111, CollectionInner{property1='set', property2=2}, CollectionInner{property1='refs', property2=111}, null] , map={key=value, CollectionInner{property1='refs', property2=111}=CollectionInner{property1='refs', property2=111}, CollectionInner{property1='mapkey', property2=3}=CollectionInner{property1='mapvalue', property2=3}} , properties={null=null, 222=222, 111=222} , array=[111, CollectionInner{property1='array', property2=4}, CollectionInner{property1='refs', property2=111}, null]}Spring容器还支持集合的继承和合并。我们可以定义父< list >、< map >、< set >、< props >、< array >集合bean,并且支持子< list >、< map >、< set >、< props >、< array >集合bean从父集合bean继承值,当然子集合bean也可以重写值。实际上这里的集合继承这就是上面讲的parent继承中的父bean和子bean的延伸,集合也是一种bean,用法都是一样的,都是使用parent属性,只不过集合多了合并的选项! 子集合bean定义的依赖值默认会覆盖父集合bean的依赖值,但是< list >、< map >、< set >、< props >、< array >标签支持集合元素的合并。通过在子集合bean标签中(在父集合bean标签中设置无效)设置属性merge=true,子集合bean的值最终就是合并父集合bean和子集合bean的元素的结果。根据集合的性质,map会替换相同key的value,set则会去重,他们都是无序的,list、array则会累计并且有序(子集合bean的元素会合并到父集合bean的元素之后)。 采用merge合并时,子、父集合bean注入的集合元素将也可能相互覆盖而不是合并。其次,构造器注入的方式则会被setter注入的方式完全替换,父集合bean的setter注入则会被子集合bean的setter注入完全替换,因为setter注入相当于重新设置了一个新集合,因此这里所说的集合合并仅针对两个子、父集合bean都是构造器注入的方式! 注意:使用merge合并的时候,父集合bean对相同依赖的注入即使只注入一个值也不能使用value或者ref快捷注入! 如下案例,首先有一个CollectionDI类,用于集合继承合并测试:
/** * 集合继承合并 * * @author lx */ public class CollectionMerging { //集合继承合并 private List list; private Set set; private Properties properties; private Map map; private Object[] array; public CollectionMerging(List list, Set set, Properties properties, Map map, Object[] array) { this.list = list; this.set = set; this.properties = properties; this.map = map; this.array = array; } public void setProperties(Properties properties) { this.properties = properties; } @Override public String toString() { return "CollectionMerging{" + "list=" + list + ", set=" + set + ", properties=" + properties + ", map=" + map + ", array=" + Arrays.toString(array) + '}'; } }配置文件:
<!-- 集合继承合并--> <bean name="collectionMergingParent" abstract="true"> <constructor-arg name="list"> <list> <value>1111</value> </list> </constructor-arg> <constructor-arg name="set"> <set> <value>2222</value> <value>3333</value> </set> </constructor-arg> <constructor-arg name="properties"> <props> <prop key="1">1</prop> <prop key="2">2</prop> <prop key="3">3</prop> <prop key="5">5</prop> </props> </constructor-arg> <property name="properties"> <props> <prop key="1">1</prop> <prop key="2">10</prop> <prop key="3">3</prop> </props> </property> <constructor-arg name="map"> <map> <entry key="1" value="1"/> <entry key="2" value="1"/> <entry key="3" value="1"/> </map> </constructor-arg> <!--相同依赖使用merge合并时不可以使用value或者ref简单注入--> <!--<constructor-arg name="array" value="111"/>--> <constructor-arg name="array"> <array merge="true"> <value>111</value> <value>121</value> </array> </constructor-arg> </bean> <bean id="collectionMergingChild" class="com.spring.core.CollectionMerging" parent="collectionMergingParent"> <!--list不会去重--> <constructor-arg name="list"> <list merge="true"> <value>1111</value> <value>2222</value> </list> </constructor-arg> <!--set会去重--> <constructor-arg name="set"> <set merge="true"> <value>1111</value> <value>2222</value> </set> </constructor-arg> <!--KV集合会替换value,由于对于properties依赖最终有一个父bean的setter注入方式, 因此最终父bean的setter注入会覆盖前面的全部注入 --> <constructor-arg name="properties"> <props merge="true"> <prop key="1">0</prop> <prop key="2">2</prop> <prop key="3">4</prop> <prop key="4">4</prop> </props> </constructor-arg> <!--<property name="properties">--> <!-- <props merge="true">--> <!-- <prop key="1">0</prop>--> <!-- <prop key="2">2</prop>--> <!-- <prop key="3">4</prop>--> <!-- <prop key="4">4</prop>--> <!-- </props>--> <!--</property>--> <constructor-arg name="array"> <array merge="true"> <value>111</value> <value>111</value> </array> </constructor-arg> </bean>测试:
@Test public void collectionMerging() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("collectionMergingChild", CollectionMerging.class)); }结果如下:
[collectionMergingParent, collectionMergingChild] CollectionMerging{list=[1111, 1111, 2222], set=[2222, 3333, 1111], properties={3=3, 2=10, 1=1}, map={1=1, 2=1, 3=1}, array=[111, 121, 111, 111]}Spring将默认值为空的属性的值视为空字符串。另外,就算value字面量值设置为null,那也看做是“null”字符串。如果真的想要设置为null而不是空字符串值,那么应该使用标签,这个标签才表示真正的注入null值。 如下案例,首先有一个NullDI类,用于null注入测试:
/** * null注入 * * @author lx */ public class NullDI { //null注入 public String property1; public String property2; public NullDI nullDI; public void setProperty1(String property1) { this.property1 = property1; } public void setProperty2(String property2) { this.property2 = property2; } public void setNullDI(NullDI nullDI) { this.nullDI = nullDI; } @Override public String toString() { return "NullDI{" + "property1='" + property1 + '\'' + ", property2='" + property2 + '\'' + ", nullDI=" + nullDI + '}'; } }配置文件:
<bean class="com.spring.core.NullDI" id="nullDI"> <!--空的value属性被当做空字符串--> <property name="property1" value=""/> <!--只有null标签才表示真正的注入null--> <property name="property2"> <null/> </property> <property name="nullDI"> <null/> </property> </bean>测试:
@Test public void nullDI() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); NullDI nullDI = ac.getBean("nullDI", NullDI.class); System.out.println(nullDI.property1 == null); System.out.println(nullDI.property2 == null); System.out.println(nullDI.nullDI == null); System.out.println(nullDI); }结果如下:
[nullDI] false true true NullDI{property1='', property2='null', nullDI=null}该知识点了解即可。 p-namespace(p-命名空间)允许你使< bean >元素的属性而不是嵌套的< property >子标签来描述依赖,实际上是简化setter方式注入的配置!该知识点了解即可。 如下案例,首先有一个PNameSpaceDI类,用于p-namespace注入测试:
/** * p-namespace属性注入 * * @author lx */ public class PNameSpaceDI { //p-namespace属性注入 private String property1; private boolean property2; private PNameSpaceDIInner pNameSpaceDIInner; public void setProperty1(String property1) { this.property1 = property1; } public void setProperty2(boolean property2) { this.property2 = property2; } public void setpNameSpaceDIInner(PNameSpaceDIInner pNameSpaceDIInner) { this.pNameSpaceDIInner = pNameSpaceDIInner; } public static class PNameSpaceDIInner { private String property1; private int property2; public void setProperty1(String property1) { this.property1 = property1; } public void setProperty2(int property2) { this.property2 = property2; } @Override public String toString() { return "InnerBeanInner{" + "property1='" + property1 + '\'' + ", property2=" + property2 + '}'; } } @Override public String toString() { return "PNameSpaceDI{" + "property1='" + property1 + '\'' + ", property2=" + property2 + ", pNameSpaceDIInner=" + pNameSpaceDIInner + '}'; } }配置文件: p-namespace是Spring2.x版本后提供的方式。p-namespace没有在xsd文件中默认定义,需要我们手动引入:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">bean配置:
<!--p-namespace注入--> <!--普通setter注入--> <bean id="pNameSpaceDI1" class="com.spring.core.PNameSpaceDI"> <property name="property1" value="vv"/> <property name="property2" value="true"/> <property name="pNameSpaceDIInner"> <bean class="com.spring.core.PNameSpaceDI.PNameSpaceDIInner"> <property name="property1" value="p1"/> <property name="property2" value="11"/> </bean> </property> </bean> <!--p-namespace注入--> <bean id="pNameSpaceDI2" class="com.spring.core.PNameSpaceDI" p:property1="xxx" p:property2="true" p:pNameSpaceDIInner-ref="pNameSpaceDIInner"/> <!--p-namespace引用的对象,只能是外部对象--> <bean id="pNameSpaceDIInner" class="com.spring.core.PNameSpaceDI.PNameSpaceDIInner" p:property1="p1" p:property2="11"/>将p-namespace引入进来之后就可以使用了。可以看到,用法很简单:对于String、基本类型及其包装类,我们使用 p:属性名=“xxx”即可;对于对象类型,我们应该使用p:属性名-ref=“xxx”,表示引用要给一个bean。 p-namespace在一定程度上简化了setter注入的方式,但是也有缺点,那就是对于引用类型的注入只能引用外部容器的bean,不能定义内部bean。 测试:
@Test public void pNameSpaceDI() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); PNameSpaceDI pNameSpaceDI = ac.getBean("pNameSpaceDI2", PNameSpaceDI.class); System.out.println(pNameSpaceDI); }结果如下,成功注入:
[pNameSpaceDI1, pNameSpaceDI2, pNameSpaceDIInner] PNameSpaceDI{property1='xxx', property2=true, pNameSpaceDIInner=InnerBeanInner{property1='p1', property2=11}}该知识点了解即可。 和前面的p-namespace类似,Spring 3.1继续引入了c-namespace快捷注入,允许你使< bean >元素的属性而不是嵌套的< constructor-arg >子标签来描述依赖,实际上是简化构造器方式注入的配置! 如下案例,首先有一个PNameSpaceDI类,用于c-namespace注入测试:
/** * c-namespace属性注入 * * @author lx */ public class CNameSpaceDI { //c-namespace属性注入 private String property1; private boolean property2; private CNameSpaceDIInner cNameSpaceDIInner; /** * 需要构造器 */ public CNameSpaceDI(String property1, boolean property2, CNameSpaceDIInner cNameSpaceDIInner) { this.property1 = property1; this.property2 = property2; this.cNameSpaceDIInner = cNameSpaceDIInner; } public static class CNameSpaceDIInner { private String property1; private int property2; public CNameSpaceDIInner(String property1, int property2) { this.property1 = property1; this.property2 = property2; } @Override public String toString() { return "CNameSpaceDIInner{" + "property1='" + property1 + '\'' + ", property2=" + property2 + '}'; } } @Override public String toString() { return "CNameSpaceDI{" + "property1='" + property1 + '\'' + ", property2=" + property2 + ", cNameSpaceDIInner=" + cNameSpaceDIInner + '}'; } }配置文件: c-namespace没有在xsd文件中默认定义,同样需要我们手动引入:
xmlns:c="http://www.springframework.org/schema/c"bean配置:
<!--c-namespace注入--> <!--普通构造器注入--> <bean id="cNameSpaceDI" class="com.spring.core.CNameSpaceDI"> <constructor-arg name="property1" value="yy"/> <constructor-arg name="property2" value="true"/> <constructor-arg name="cNameSpaceDIInner"> <bean class="com.spring.core.CNameSpaceDI.CNameSpaceDIInner"> <constructor-arg name="property1" value="c1"/> <constructor-arg name="property2" value="22"/> </bean> </constructor-arg> </bean> <!--c-namespace注入--> <!--通过参数名--> <bean id="cNameSpaceDI2" class="com.spring.core.CNameSpaceDI" c:property1="xxx" c:property2="true" c:cNameSpaceDIInner-ref="cNameSpaceDIInner"/> <!--通过参数位置索引--> <bean id="cNameSpaceDI3" class="com.spring.core.CNameSpaceDI" c:_0="xxxx" c:_1="true" c:_2-ref="cNameSpaceDIInner"/> <!--c-namespace引用的对象,只能是外部对象--> <bean id="cNameSpaceDIInner" class="com.spring.core.CNameSpaceDI.CNameSpaceDIInner"> <constructor-arg name="property1" value="c1"/> <constructor-arg name="property2" value="22"/> </bean>将c-namespace引入进来之后就可以使用了。可以看到,用法很简单:对于String、基本类型及其包装类,我们使用 c:属性名=“xxx”即可;对于对象类型,我们应该使用c:属性名-ref=“xxx”,表示引用一个外部bean。另外,c-namespace可以使用参数索引来代替参数名,索引从0开始,格式为c:_index=“xxx”或者c:_index-ref=“xxx”。 c-namespace在一定程度上简化了构造器注入的方式,但是也有缺点,那就是对于引用类型的注入只能引用外部容器的bean,不能定义内部bean。 测试:
@Test public void cNameSpaceDI() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("cNameSpaceDI2", CNameSpaceDI.class)); System.out.println(ac.getBean("cNameSpaceDI3", CNameSpaceDI.class)); }结果如下,成功注入:
[cNameSpaceDI, cNameSpaceDI2, cNameSpaceDI3, cNameSpaceDIInner] CNameSpaceDI{property1='xxx', property2=true, cNameSpaceDIInner=CNameSpaceDIInner{property1='c1', property2=22}} CNameSpaceDI{property1='xxxx', property2=true, cNameSpaceDIInner=CNameSpaceDIInner{property1='c1', property2=22}}该知识点了解即可。 设置bean的< property >依赖时,可以对name属性使用复合属性名称或嵌套属性名称来为内层的属性设置依赖项,这要求除最终属性名称外,路径上的所有依赖组件都不为null。如果中途有为null的依赖,那么抛出NullPointerException。注意:除了setter方法之外,我们一定要有对于路径上依赖的组件的getter方法,否则不能实现复合属性注入!
对于对象属性,属性名之间使用“.”分隔;对于集合或者数组索引,在属性名之后使用“[index]”;对于Map,在属性名之后使用“[key]”;支持多重集合嵌套,比如< property name=“property5[0][1]” value=“33”/>,表示设置property5属性集合内部的第一个集合元素内部的第二个元素值为33;当然支持更复杂的对象属性+集合嵌套,按照语法来写就行了。如下案例,首先有一个CompoundPropertyDI类,具有多层嵌套内部类,用于复合属性名称注入测试:
/** * 复合属性注入 * * @author lx */ public class CompoundPropertyDI { //复合属性注入 public String property1; public boolean property2; public CompoundPropertyDIInner compoundPropertyDIInner; public void setProperty1(String property1) { this.property1 = property1; } public void setProperty2(boolean property2) { this.property2 = property2; } public void setCompoundPropertyDIInner(CompoundPropertyDIInner compoundPropertyDIInner) { this.compoundPropertyDIInner = compoundPropertyDIInner; } /** * getter是复合属性名称注入所必须的方法 */ public CompoundPropertyDIInner getCompoundPropertyDIInner() { return compoundPropertyDIInner; } public static class CompoundPropertyDIInner { public String property1; public int property2; public int property3; public CompoundPropertyDIInnerInner compoundPropertyDIInnerInner; public void setProperty1(String property1) { this.property1 = property1; } public void setProperty2(int property2) { this.property2 = property2; } public void setProperty3(int property3) { this.property3 = property3; } public void setCompoundPropertyDIInnerInner(CompoundPropertyDIInnerInner compoundPropertyDIInnerInner) { this.compoundPropertyDIInnerInner = compoundPropertyDIInnerInner; } /** * getter是复合属性名称注入所必须的方法 */ public CompoundPropertyDIInnerInner getCompoundPropertyDIInnerInner() { return compoundPropertyDIInnerInner; } public static class CompoundPropertyDIInnerInner { public String property1; public int property2; public void setProperty1(String property1) { this.property1 = property1; } public void setProperty2(int property2) { this.property2 = property2; } @Override public String toString() { return "CompoundPropertyDIInnerInner{" + "property1='" + property1 + '\'' + ", property2=" + property2 + '}'; } } @Override public String toString() { return "CompoundPropertyDIInner{" + "property1='" + property1 + '\'' + ", property2=" + property2 + ", property3=" + property3 + ", compoundPropertyDIInnerInner=" + compoundPropertyDIInnerInner + '}'; } } @Override public String toString() { return "CompoundPropertyDI{" + "property1='" + property1 + '\'' + ", property2=" + property2 + ", compoundPropertyDIInner=" + compoundPropertyDIInner + '}'; } }配置文件:
<!--复合属性注入--> <!--多层嵌套bean--> <bean id="compoundPropertyDI" class="com.spring.core.CompoundPropertyDI"> <property name="property1" value="com"/> <property name="property2" value="false"/> <property name="compoundPropertyDIInner"> <bean class="com.spring.core.CompoundPropertyDI.CompoundPropertyDIInner"> <property name="property1" value="111"/> <property name="property2" value="222"/> <property name="compoundPropertyDIInnerInner"> <!--超过两层的内部类嵌套需要使用$分隔--> <bean class="com.spring.core.CompoundPropertyDI$CompoundPropertyDIInner$CompoundPropertyDIInnerInner"> <property name="property1" value="111"/> <property name="property2" value="222"/> </bean> </property> </bean> </property> <!--复合属性注入 在外部bean中直接为内部bean的属性赋值--> <!--注意:除了setter方法之外,我们一定要有对于路径上依赖的组件的getter方法,否则不能实现复合属性注入--> <property name="compoundPropertyDIInner.property3" value="333"/> <property name="compoundPropertyDIInner.compoundPropertyDIInnerInner.property1" value="444"/> </bean>测试:
@Test public void compoundPropertyDI() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("compoundPropertyDI", CompoundPropertyDI.class)); }结果如下,成功实现复合属性名称注入:
[compoundPropertyDI] CompoundPropertyDI{property1='com', property2=false, compoundPropertyDIInner=CompoundPropertyDIInner{property1='111', property2=222, property3=333, compoundPropertyDIInnerInner=CompoundPropertyDIInnerInner{property1='444', property2=222}}}该知识点了解即可。 如果某个bean是另一个bean的依赖项,则通常意味着这个bean被设置为另一个 bean的属性。但是,有时bean之间的依赖关系不太直接,他们之间没有直接的引用关系。例如,web应用中的Database的bean必须先于DAO的bean初始化,但是DAO的bean不需要持有Database的bean实例。 < bean >标签的depends-on属性可以显式强制在初始化使用此元素的bean之前初始化一个或多个指定bean。depends-on的值是先初始化的bean的id或者name,需要指定多个depends-on的bean时,可以使用英文的“,”、“;”“ ”符号分隔,Spring会按照depend-on中定义的顺序来初始化bean。 另外,depends-on属性也可以指定相应依赖项的销毁时间顺序,最先初始化的bean将最后被销毁。无论是指定初始化顺序还是销毁顺序,都要求设置该属性的bean的作用范围是singleton,否则depends-on属性无效! 如下案例,首先有一个DependsOnDI类,用于depends-on注入测试:
/** * depends-on * * @author lx */ public class DependsOnDI { public static class DependsOnDIA { public DependsOnDIA() { System.out.println("DependsOnDIA初始化"); } public void init() { System.out.println("DependsOnDIA初始化回调"); } public void destroy() { System.out.println("DependsOnDIA销毁回调"); } } public static class DependsOnDIB { public DependsOnDIB() { System.out.println("DependsOnDIB初始化"); } public void init() { System.out.println("DependsOnDIB初始化回调"); } public void destroy() { System.out.println("DependsOnDIB销毁回调"); } } public static class DependsOnDIC { public DependsOnDIC() { System.out.println("DependsOnDIC初始化"); } public void init() { System.out.println("DependsOnDIC初始化回调"); } public void destroy() { System.out.println("DependsOnDIC销毁回调"); } } }配置文件,要求在dependsOnDIA初始化之前,先顺序初始化dependsOnDIC和dependsOnDIB:
<!--depends-on 如果不是scope=singleton(默认),那么depends-on属性无效--> <bean class="com.spring.core.DependsOnDI.DependsOnDIA" depends-on="dependsOnDIC dependsOnDIB" id="dependsOnDIA" destroy-method="destroy" init-method="init"/> <bean class="com.spring.core.DependsOnDI.DependsOnDIB" id="dependsOnDIB" destroy-method="destroy" init-method="init"/> <bean class="com.spring.core.DependsOnDI.DependsOnDIC" id="dependsOnDIC" destroy-method="destroy" init-method="init"/>测试:
@Test public void dependsOnDI() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); ac.close(); }结果如下,确实是按照顺序初始化、销毁的:
DependsOnDIC初始化 DependsOnDIC初始化回调 DependsOnDIB初始化 DependsOnDIB初始化回调 DependsOnDIA初始化 DependsOnDIA初始化回调 [dependsOnDIA, dependsOnDIB, dependsOnDIC] com.spring.core.DependsOnDI$DependsOnDIA@7a9273a8 DependsOnDIA销毁回调 DependsOnDIB销毁回调 DependsOnDIC销毁回调该知识点了解即可。 默认情况下,ApplicationContext在初始化的时候,就会创建所有定义好的作用范围是singleton的bean及其依赖项。这样做的好处是配置中的错误能够被立即发现,而不是等到使用时才发现。 < bean >标签的lazy-init属性可以设置bean为延迟初始化,在首次请求这个bean的时后才创建 bean 实例,而不是在启动容器时就创建 bean 实例。 另外,当一个延迟初始化bean是一个非延迟初始化的singleton bean的依赖项时,ApplicationContext会在启动时就创建延迟初始化bean,因为它必须满足singleton bean的依赖项的要求。延迟初始化bean被立即初始化并注入到其他非延迟初始化的singleton bean中。 lazy-init属性只有在scope="singleton"时才会有效,如果scope=“pototype”,那么即使设置了lazy-init=“false”(默认就是false),也会延迟初始化。 我们也可以在< beans >标签上设置default-lazy-init="true"来为该标签下面的全部bean统一设置是否延迟初始化,默认false,非延迟初始化! 如下案例,首先有一个LazyInitDI类,用于lazy-init注入测试:
/** * lazy-init * * @author lx */ public class LazyInitDI { public static class LazyInitDIA { public LazyInitDIA() { System.out.println("LazyInitDIA初始化"); } } /** * 依赖LazyInitDIC */ public static class LazyInitDIB { private LazyInitDIC lazyInitDIC; public LazyInitDIB(LazyInitDIC lazyInitDIC) { this.lazyInitDIC = lazyInitDIC; System.out.println("LazyInitDIB初始化"); } } public static class LazyInitDIC { public LazyInitDIC() { System.out.println("LazyInitDIC初始化"); } } }配置文件:
<!--lazy-init 延迟初始化--> <!--lazyInitDIA延迟初始化--> <bean class="com.spring.core.LazyInitDI.LazyInitDIA" lazy-init="true" id="lazyInitDIA"/> <!--lazyInitDIB属于singleton的非延迟初始化--> <bean class="com.spring.core.LazyInitDI.LazyInitDIB" id="lazyInitDIB"> <!--依赖lazyInitDIC--> <constructor-arg name="lazyInitDIC" ref="lazyInitDIC"/> </bean> <!--lazyInitDIC属于延迟初始化,但是被singleton的lazyInitDIB依赖,因此会立即初始化--> <bean class="com.spring.core.LazyInitDI.LazyInitDIC" lazy-init="true" id="lazyInitDIC"/>测试:
@Test public void lazyInit() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); //使用lazyInitDIA的bean实例时才会初始化 //System.out.println(ac.getBean("lazyInitDIA", LazyInitDI.LazyInitDIA.class)); }结果如下:
LazyInitDIC初始化 LazyInitDIB初始化 [lazyInitDIA, lazyInitDIB, lazyInitDIC]该知识点了解即可。 这里的“自动注入”,不是前面说的“依赖注入DI”,而是一种对于XML配置文件的省略写法。前面的案例中,我们需要手写配置文件指明依赖和注入方式。Spring支持某些情况下对于依赖自动注入的情况,通过检查ApplicationContext的内容,可以让Spring自动解析、配置bean的依赖。让我们不需要手写配置文件。 自动注入的优点如下:
自动注入可以显著减少手动指定属性或构造函数参数的需要。(前面讲的parent继承也有这个功能)。自动注入可以自动更新对象的配置。例如,如果您需要向类添加依赖项,则无需修改配置文件即可自动装配该依赖。使用基于XML的配置元数据时,可以通过设置< bean >标签的autowire属性来设置自动注入模式,共有四种:
no:默认值。不启用自动装配。bean必须引用由ref定义的元素。虽然配置要多一点,但是这样可以让系统结构和bean之间的关系更加清晰。byName:通过属性的名字的方式查找bean依赖的对象并为其注入。比如说某个bean的autowire属性为byName,并且有个属性名为a,有个setA方法,那么IoC容器会在配置文件中查找id/name的值包含a的bean,然后使用setter方法模式为其注入。注意这里的自动setter相比前面讲的手动setter注入有限制,即setXXX方法的XXX一定要对应一个容器中实例的name才行,否则不会注入。byType:通过属性的类型查找bean依赖的对象并为其注入。比如说某个bean的autowire属性为byType,并且有个属性名为a,有个setA方法,这个属性a的类型为com.A,那么IoC容器会在配置文件中查找class为com.A的bean,然后使用setter方法模式为其注入。注意这里的自动setter注入相比手动setter配注入有限制,即setXXX方法的XXX一定是属性名才行(第一个字母可以改变大小写)。如果容器有多个同类型的bean,那么抛出异常:expected single matching bean but found x: xxx。constructor:同byType一样,也是通过类型查找依赖对象。与byType的区别在于它不是使用setter方法注入,而是使用构造器注入。如果容器有多个同类型的bean,并且不能找到最合适的依赖,那么同样会抛出异常:expected single matching bean but found x: xxx。另外,< beans >标签的default-autowire属性同样有上面几个选项,可以指定内部的全部< bean >的通用自动注入方式,但是如果某些< bean >主动设置了自己的自动注入方式,那么会覆盖< beans >标签设置的方式!如下案例,首先有一个AutowireDI类,用于autowire注入测试:
/** * autowire注入 * * @author lx */ public class AutowireDI { /** * 依赖AutowireDIB和AutowireDIC */ public static class AutowireDIA { private AutowireDIB autowireDIB; private AutowireDIC autowireDIC; public void setAutowireDIC(AutowireDIC autowireDIC) { this.autowireDIC = autowireDIC; } public AutowireDIA() { } public AutowireDIA(AutowireDIB autowireDIB) { this.autowireDIB = autowireDIB; } @Override public String toString() { return "AutowireDIA{" + "autowireDIB=" + autowireDIB + ", autowireDIC=" + autowireDIC + '}'; } } public static class AutowireDIB { private String property1; } public static class AutowireDIC { private String property1; } }配置文件:
<!--autowire--> <!--用于构造器注入的依赖--> <bean id="autowireDIB" class="com.spring.core.AutowireDI.AutowireDIB"/> <bean id="autowireDIB2" class="com.spring.core.AutowireDI.AutowireDIB"/> <!--用于setter注入的依赖--> <bean id="autowireDIC" class="com.spring.core.AutowireDI.AutowireDIC"/> <!--<bean id="autowireDIC2" class="com.spring.core.AutowireDI.AutowireDIC"/>--> <!--默认,不自动注入--> <bean id="autowireDIA0" class="com.spring.core.AutowireDI.AutowireDIA" /> <!--byType 将自动setter注入autowireDIC 如果容器有多个同类型的bean,并且找不到最合适的bean,那么抛出异常:--> <!--expected single matching bean but found 2: autowireDIC,autowireDIC2--> <bean id="autowireDIA1" class="com.spring.core.AutowireDI.AutowireDIA" autowire="byType"/> <!--byName 将自动setter注入autowireDIC--> <bean id="autowireDIA2" class="com.spring.core.AutowireDI.AutowireDIA" autowire="byName"/> <!--constructor 将自动构造器注入autowireDIC 如果容器有多个同类型的bean,并且找不到最合适的bean,那么抛出异常:--> <bean id="autowireDIA3" class="com.spring.core.AutowireDI.AutowireDIA" autowire="constructor"/>测试:
@Test public void autowire() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println(ac.getBean("autowireDIA0", AutowireDI.AutowireDIA.class)); System.out.println(ac.getBean("autowireDIA1", AutowireDI.AutowireDIA.class)); System.out.println(ac.getBean("autowireDIA2", AutowireDI.AutowireDIA.class)); System.out.println(ac.getBean("autowireDIA3", AutowireDI.AutowireDIA.class)); }结果如下,成功自动注入:
[autowireDIB, autowireDIB2, autowireDIC, autowireDIA0, autowireDIA1, autowireDIA2, autowireDIA3] AutowireDIA{autowireDIB=null, autowireDIC=null} AutowireDIA{autowireDIB=null, autowireDIC=com.spring.core.AutowireDI$AutowireDIC@782663d3} AutowireDIA{autowireDIB=null, autowireDIC=com.spring.core.AutowireDI$AutowireDIC@782663d3} AutowireDIA{autowireDIB=com.spring.core.AutowireDI$AutowireDIB@1990a65e, autowireDIC=null}自动注入确实在一定程度上能够节省配置文件的编写,然而还有自己的问题:
对于byName注入,没找到该name的依赖就不会注入。对于byType注入,如果容器有多个同类型的bean,并且不能找到最合适的依赖,那么抛出异常:expected single matching bean but found x: xxx。对于constructor注入,如果容器有多个同类型的bean,并且不能找到最合适的依赖,那么同样会抛出异常。expected single matching bean but found x: xxx。使用自动注入之后,不容易明确bean之间的依赖关系!实际上它们的依赖注入规则都比较复杂,后面讲源码的时候会讲解它的具体规则。比如构造器注入时如果有多个同类型的实例,则可以根据参数名进行进一步匹配,但是setter注入时却不会按照参数名匹配。对于这些的问题,常见的解决办法是:
最常见的选择是放弃自动注入(默认就是不开启),虽然多了一些配置代码,但是保证不出问题,并且bean的关系更加明显。将< bean >的autowire-candidate属性设置为false,该bean从自动注入候选bean中排除。将< bean >的primary属性设置为true,该bean设置为自动注入的主要候选bean。使用注解方式注入(后面会讲,实际上注解就是一种自动注入)。首先,< beans >标签具有一个default-autowire-candidates属性,用于统一筛选当前容器下具有哪些id/name的bean可以作为主动注入的候选bean。default-autowire-candidates的值可以使用模式匹配。例如,要将候选bean的名称限制为以Repository结尾的任何bean,可以使用*Repository。可以提供多个模式,使用逗号分隔。default-autowire-candidates属性只会影响通过类型的自动注入,它不会影响显式的byName的自动注入。 另外,将< bean >标签的autowire-candidate属性设置为false(默认为true),也会将该bean从自动注入候选bean中排除,则这个bean在自动注入时将会不可用(包括注解样式的注入,如@Autowired)。如果autowire-candidate属性显示设置了值为true或者false,那么default-autowire-candidates属性将对该bean的筛选失效!需要注意的是,autowire-candidate属性只会影响通过类型的自动注入,它不会影响显式的byName的自动注入。 当然还可以反过来,将< bean >标签的primary属性设置为true(默认为fasle),将该bean指定为主要候选对象,一般指定一个就行了,可以指定多个,但是那样的话又不能确定是注入哪一个bean了。注意,即使显示设置了primary属性的值为true或者false,那么default-autowire-candidates属性将对该bean的筛选仍然有效!并且优先级小于autowire-candidate的显示定义的属性!
配置文件:
<!--default-autowire-candidates设置模式,筛选指定规则的name、id的bean--> <!--只会影响通过类型的自动注入,它不会影响显式的byName的自动注入。--> <beans default-autowire-candidates="*DIB1*"> <!--用于构造器注入的依赖 即使设置了primary="true",仍然受到default-autowire-candidates的影响--> <bean id="autowireDIB" class="com.spring.core.AutowireDI.AutowireDIB" primary="true"/> <!--autowire-candidate显示设置的属性不会受到default-autowire-candidates的影响,且优先级大于primary设置的属性--> <bean id="autowireDIB2" class="com.spring.core.AutowireDI.AutowireDIB" primary="false" autowire-candidate="true"/> <!--用于setter注入的依赖 autowire-candidate="false"不会影响通过byName注入 --> <bean id="autowireDIC" class="com.spring.core.AutowireDI.AutowireDIC" autowire-candidate="false"/> <!--<bean id="autowireDIC2" class="com.spring.core.AutowireDI.AutowireDIC"/>--> <!--默认,不自动注入--> <bean id="autowireDIA0" class="com.spring.core.AutowireDI.AutowireDIA"/> <!--byType 将自动setter注入autowireDIC 如果容器有多个同类型的bean,那么抛出异常:--> <!--expected single matching bean but found 2: autowireDIC,autowireDIC2--> <bean id="autowireDIA1" class="com.spring.core.AutowireDI.AutowireDIA" autowire="byType"/> <!--byName 将自动setter注入autowireDIC--> <bean id="autowireDIA2" class="com.spring.core.AutowireDI.AutowireDIA" autowire="byName"/> <!--constructor 将自动构造器注入autowireDIC 如果容器有多个同类型的bean,那么不能确定注入哪一个--> <bean id="autowireDIA3" class="com.spring.core.AutowireDI.AutowireDIA" autowire="constructor"/> </beans>测试:
@Test public void selectAutowire() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); System.out.println("autowireDIB:" + ac.getBean("autowireDIB", AutowireDI.AutowireDIB.class)); System.out.println("autowireDIB2:" + ac.getBean("autowireDIB2", AutowireDI.AutowireDIB.class)); System.out.println("autowireDIA0:" + ac.getBean("autowireDIA0", AutowireDI.AutowireDIA.class)); System.out.println("autowireDIA1;" + ac.getBean("autowireDIA1", AutowireDI.AutowireDIA.class)); System.out.println("autowireDIA2:" + ac.getBean("autowireDIA2", AutowireDI.AutowireDIA.class)); System.out.println("autowireDIA3:" + ac.getBean("autowireDIA3", AutowireDI.AutowireDIA.class)); }结果如下,成功选择注入:
[autowireDIB, autowireDIB2, autowireDIC, autowireDIA0, autowireDIA1, autowireDIA2, autowireDIA3] autowireDIB:com.spring.core.AutowireDI$AutowireDIB@64485a47 autowireDIB2:com.spring.core.AutowireDI$AutowireDIB@25bbf683 autowireDIA0:AutowireDIA{autowireDIB=null, autowireDIC=null} autowireDIA1;AutowireDIA{autowireDIB=null, autowireDIC=null} autowireDIA2:AutowireDIA{autowireDIB=null, autowireDIC=com.spring.core.AutowireDI$AutowireDIC@6ec8211c} autowireDIA3:AutowireDIA{autowireDIB=com.spring.core.AutowireDI$AutowireDIB@25bbf683, autowireDIC=null}该知识点了解即可。 IoC容器中的bean之间的依赖关系通常是通过属性来确定的,这样就可能由于bean的声明周期不同而造成某些问题。比如某个bean a是单例(singleton)的,只会在岂容启动时创建一次,它依赖了一个bean b,但是这个bean b是非单例的,业务要求是,每一个访问bean a,都需要注入最新的bean b实例。 显然,单单设置bean b的scope属性为prototype是不能解决问题的,因为容器只创建一次singleton的bean a,因此只获得一次设置属性bean b的机会,以后每次访问都是bean a访问自己保存的bean b的引用,而不是向IoC容器要bean b(getBean),所以造成了bean b相当于单例的情况。 如下案例,首先有一个MethodIn类,用于上面的问题的测试:
/** * @author lx */ public class MethodIn { private MethodInInner methodInInner1; private MethodInInner methodInInner2; public MethodIn(MethodInInner methodInInner1) { this.methodInInner1 = methodInInner1; } public void setMethodInInner2(MethodInInner methodInInner2) { this.methodInInner2 = methodInInner2; } public MethodInInner getMethodInInner1() { return methodInInner1; } public MethodInInner getMethodInInner2() { return methodInInner2; } public static class MethodInInner { public MethodInInner() { System.out.println("MethodInInner初始化:" + this); } } }配置文件:
<!--bean methodIn 每一个调用时需要获取最新的methodInInner对象--> <bean id="methodIn" class="com.spring.core.MethodIn"> <constructor-arg name="methodInInner1" ref="methodInInner"/> <property name="methodInInner2" ref="methodInInner"/> </bean> <!--methodInInner的scope设置为prototype,看看行不行--> <bean class="com.spring.core.MethodIn.MethodInInner" id="methodInInner" scope="prototype"/>测试:
@Test public void methodNi() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); MethodIn methodIn = ac.getBean("methodIn", MethodIn.class); //多次获取methodInInner,发现都是同一个对象 System.out.println("methodInInner1:" + methodIn.getMethodInInner1()); System.out.println("methodInInner1:" + methodIn.getMethodInInner1()); System.out.println("methodInInner2:" + methodIn.getMethodInInner2()); System.out.println("methodInInner2:" + methodIn.getMethodInInner2()); }结果如下,发现MethodInInner设置为prototype并没有解决问题,仍然只是在第一次初始化methodIn的时候初始化一次:
MethodInInner初始化:com.spring.core.MethodIn$MethodInInner@4d41cee MethodInInner初始化:com.spring.core.MethodIn$MethodInInner@631330c [methodIn, methodInInner] methodIn:com.spring.core.MethodIn@12c8a2c0 methodIn:com.spring.core.MethodIn@12c8a2c0 methodIn:com.spring.core.MethodIn@12c8a2c0这种情况,我们可以使用方法注入来解决!
查找方法注入(Lookup Method Injection),是指IoC容器重写配置文件中指定 的方法,注入方法的返回结果,将会返回容器中另一个命名的bean。 原理比较简单,在解析< bean>标签的时候,会对具有< lookup-method >标签的bean利用cglib的动态代理技术,生成该类的动态子类,该代理类将会代理< lookup-method >的name属性指定的方法,最终返回bean属性指定名称的bean对象,这个返回是向容器要对象。那么,如果这个bean对象是prototype类型,必然每一次生成一个新的对象并返回,如果这个bean对象是singleton类型,必然每一次返回同一个对象! 如下案例,首先有一个LookupMethodIn类,用于查找方法注入的测试:
public class LookupMethodIn { public static class LookupMethodInA { private LookupMethodInB lookupMethodInB; /** * 实际上lookupMethodInC属性根本你没有注入过 */ private LookupMethodInC lookupMethodInC; public LookupMethodInB getLookupMethodInB() { return lookupMethodInB; } public void setLookupMethodInB(LookupMethodInB lookupMethodInB) { this.lookupMethodInB = lookupMethodInB; } /** * 实际上每一个都是找容器要对象 */ public LookupMethodInC getLookupMethodInC() { //调用createLookupMethodInC方法 return createLookupMethodInC(); } /** * 将会被动态代理替换的方法,找容器要对象 */ public LookupMethodInC createLookupMethodInC() { return lookupMethodInC; } public void setLookupMethodInC(LookupMethodInC lookupMethodInC) { this.lookupMethodInC = lookupMethodInC; } @Override public String toString() { return "LookupMethodInA{" + "lookupMethodInB=" + lookupMethodInB + ", lookupMethodInC=" + lookupMethodInC + '}'; } } public static class LookupMethodInB { } public static class LookupMethodInC { } }配置文件:
<bean class="com.spring.core.LookupMethodIn.LookupMethodInA" id="lookupMethodInA"> <!--普通setter注入--> <property name="lookupMethodInB" ref="lookupMethodInB"/> <!--查找方法注入 name表示要被动态替换的方法名,bean表示容器中的一个bean的名字,没有过i嗯都会从容器返回该bean的实例--> <lookup-method name="createLookupMethodInC" bean="lookupMethodInC"/> </bean> <!--需要被注入的bean--> <bean class="com.spring.core.LookupMethodIn.LookupMethodInB" id="lookupMethodInB" scope="prototype"/> <bean class="com.spring.core.LookupMethodIn.LookupMethodInC" id="lookupMethodInC" scope="prototype"/>测试:
@Test public void lookupmethodNi() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); LookupMethodIn.LookupMethodInA lookupMethodInA = ac.getBean("lookupMethodInA", LookupMethodIn.LookupMethodInA.class); System.out.println("获取lookupMethodInB,同一个对象"); System.out.println("lookupMethodInB:" + lookupMethodInA.getLookupMethodInB()); System.out.println("lookupMethodInB:" + lookupMethodInA.getLookupMethodInB()); System.out.println("lookupMethodInB:" + lookupMethodInA.getLookupMethodInB()); System.out.println("获取lookupMethodInC,不同的对象"); System.out.println("lookupMethodInC:" + lookupMethodInA.getLookupMethodInC()); System.out.println("lookupMethodInC:" + lookupMethodInA.getLookupMethodInC()); System.out.println("lookupMethodInC:" + lookupMethodInA.getLookupMethodInC()); //实际上lookupMethodInA的lookupMethodInC属性根本没有被注入过,每一次的getLookupMethodInC都是直接找容器要对象 //而由于lookupMethodInC被设置为prototype,因此每一次都会获取新的对象 System.out.println("lookupMethodInA:" + lookupMethodInA); }结果如下,查找方法注入能够保证对于prototype的属性每一次返回一个新对象。
[lookupMethodInA, lookupMethodInB, lookupMethodInC] 获取lookupMethodInB,同一个对象 lookupMethodInB:com.spring.core.LookupMethodIn$LookupMethodInB@ba8d91c lookupMethodInB:com.spring.core.LookupMethodIn$LookupMethodInB@ba8d91c lookupMethodInB:com.spring.core.LookupMethodIn$LookupMethodInB@ba8d91c 获取lookupMethodInC,不同的对象 lookupMethodInC:com.spring.core.LookupMethodIn$LookupMethodInC@7364985f lookupMethodInC:com.spring.core.LookupMethodIn$LookupMethodInC@5d20e46 lookupMethodInC:com.spring.core.LookupMethodIn$LookupMethodInC@709ba3fb lookupMethodInA:LookupMethodInA{lookupMethodInB=com.spring.core.LookupMethodIn$LookupMethodInB@ba8d91c, lookupMethodInC=null}另外,由于查找方法注入使用的cglib动态代理技术,那么肯定有些限制。如果方法是抽象的,动态生成的子类会自动实现该方法,否则会将其覆盖。要注意的是因为采用的是继承,bean和代理的方法都不能是final修饰的。 如果我们使用的注解开发,那么查找方法注入的注解就是@Lookup,标注在某个方法上即可。注解的value属性可以不写,会根据类型自动匹配容器中同类型的一个bean,但是如果有多个同类型bean,那么抛出异常:expected single matching bean but found 2:xxx。(前提是annotation-config开启注解配置,后面会讲)。
/** * 将会被动态代理替换的方法,找容器要对象。 * <p> * 参数注解配置时,注解的value属性指定bean的名字,可以不写,会根据类型自动匹配容器中同类型的一个bean * 但是如果有多个同类型bean,那么抛出异常:expected single matching bean but found 2:xxx */ @Lookup("lookupMethodInC") public LookupMethodInC createLookupMethodInC() { return lookupMethodInC; }任意方法替换(Arbitrary Method Replacement):可以实现方法主体和返回结果的替换,相当于运行时使用一个方法替换另一个方法。 如下案例,首先有一个ArbitraryMethodRe类,用于任意方法替换的测试,内部有一个TimeConverter,它的convert方法本来只有两种格式化逻辑,现在需要增加一种,那么我们可以使用任意方法替换,来修改代码!任意方法替换需要定义一个实现MethodReplacer方法替换器接口的方法替换器,并重写reimplement方法,这个方法中代码以及返回值就是要替换的方法的代码和返回值。
/** * @author lx */ public class ArbitraryMethodRe { /** * 时间格式化器 */ public static class TimeConverter { /** * 需要被替换的方法 */ public String convert(byte type) { DateTimeFormatter dateTimeFormater; if (type == 1) { dateTimeFormater = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒"); } else { dateTimeFormater = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒"); } LocalDateTime localDateTime = LocalDateTime.now(); return dateTimeFormater.format(localDateTime); } } /** * 方法替换器 */ public static class ReplaceTimeConverter implements MethodReplacer { /** * 重新实现给定的方法,增加功能。还是走的cglib的逻辑 * * @param obj 重新实现的方法的实例 * @param method 需要重新实现的方法 * @param args 方法的参数数组 * @return 方法的返回值 * @throws Throwable 异常 */ @Override public Object reimplement(Object obj, Method method, Object[] args) throws Throwable { byte type = (byte) args[0]; //增加一种格式化逻辑 DateTimeFormatter dateTimeFormater; switch (type) { case 1: dateTimeFormater = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒"); break; case 2: dateTimeFormater = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时mm分ss秒 SSS毫秒"); break; default: dateTimeFormater = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss SSS"); } LocalDateTime localDateTime = LocalDateTime.now(); return dateTimeFormater.format(localDateTime); } } }配置文件:
<!--任意方法替换--> <!--方法替换器--> <bean class="com.spring.core.ArbitraryMethodRe.ReplaceTimeConverter" id="replaceTimeConverter"/> <!--需要替换的bean--> <bean class="com.spring.core.ArbitraryMethodRe.TimeConverter" id="timeConverter"> <!--任意方法替换 name表示要替换的方法 replacer表示方法替换器的引用--> <replaced-method name="convert" replacer="replaceTimeConverter"> <!--在方法重载的情况下标识替换方法的参数,只有当方法被重载并且类中存在多个变量时,该标签才是必需的。--> <!-- <arg-type match="byte"/>--> </replaced-method> </bean>可以在元素中使用多个< arg-type >标签来指示被重写的方法的方法签名。只有当方法重载且类中存在多个变体时,参数的签名才是必要的。为方便起见,参数的类型字符串可以是完全限定的类型名称的子字符串。例如,以下所有内容都是匹配 java.lang.String:
java.lang.String String Str由于参数数量通常足以区分每个可能的选择,只需键入与参数类型匹配的最短字符串,即可节省大量输入。测试:
@Test public void arbitraryMethodRe() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println(Arrays.toString(ac.getBeanDefinitionNames())); ArbitraryMethodRe.TimeConverter timeConverter = ac.getBean("timeConverter", ArbitraryMethodRe.TimeConverter.class); System.out.println(timeConverter.convert((byte) 3)); }结果如下,成功实现了方法替换:
[replaceTimeConverter, timeConverter] 2020/09/02 00:03:04 396< bean >标签的scope属性用于定义该bean的作用域(作用范围),这是一个非常强大的属性,我们在前面的学习内容中就已经见过很多次了。 所谓作用域,实际上应该是“用来声明容器中的对象应该出于的限定场景或者说该对象的特定存活时间(范围),即容器在对象进入其相应的scope之前生成并装配这些对象,在该对象不再处于这些scope的限定之后,容器通常会销毁这些对象”——《Spring揭秘》 Spring 5.x框架支持六个作用域,其中后面四个作用域仅在使用web开发的ApplicationContext中可用,比如XmlWebApplicationContext。当然,Spring支持创建自定义作用域。
singleton: 默认值,单例。单个bean定义绑定到单个bean实例,每个IoC容器仅保存单个对象实例,所有随后的请求和对这个名为bean的请求都返回缓存的对象。当应用加载,创建容器时,对象就被创建了。只要容器在,对象一直活着,并且不会再创建。 当应用卸载,销毁容器时,对象就被销毁了。 prototype: 原型,单个bean定义绑定到多个bean实例。当具有prototype作用域的bean被注入到另一个即将要被初始化的bean中,或者通过对容器的getBean()方法调用获取该bean时,都会创建一个新的bean实例。通常来说,应该为所有有状态bean使用Prototype作用域,为无状态bean使用Singleton作用域。大部分bean都是无状态的。Spring不负责prototype bean的完整生命周期。容器实例化、配置或以其他方式组装原型对象,然后将其交给客户端,之后就不再记录该原型实例。因此,初始化生命周期回调方法会在所有对象上被调用,不管生命周期如何,但在原型的情况下,不会调用配置的销毁生命周期回调。 request:单个bean定义被绑定到单个的Http请求生命周期。表示该针对每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。session:单个bean的定义被绑定到一个Http Session的生命周期。表示该针对每个独立的session都会产生一个新的bean,它仅仅比request scope的bean会存活更长的时间,在其他的方面真是没什么区别。application:单个bean的定义被绑定到ServletContext的生命周期中(一个web程序中可能有多个IoC容器)。websocket:单个bean的定义被绑定到WebSocket的生命周期中。据本人经验,Spring Boot基于注解开发的web项目中其他作用域都用的比较少,一般都是默认的singleton。
当一个prototype bean是一个singleton bean的依赖项时,ApplicationContext在创建singleton bean是就会立即创建一次prototype bean,因为它必须满足singleton bean的依赖项的要求:prototype bean需要非null的被立即初始化并注入到singleton bean中。 如果希望singleton bean在运行时重复获取prototype bean的新实例。则不能将prototype bean通过传统方式(构造器或者setter)注入到singleton bean中,因为当Spring容器实例化singleton bean并解析和注入其依赖项时,该注入只发生一次。如果在运行时需要多个原型bean的新实例,应该使用前面讲的方法注入。
Spring框架提供了许多bean相关的回调接口,用于实现不同的功能。
生命周期回调接口(Lifecycle Callbacks),是与容器对 bean 生命周期的管理进行交互的接口,有两个:初始化之后和销毁之前。对于自己创建的bean,即容器不能管理的bean的生命周期则接口定义无效。比如prototype bean由于创建之后bean的生命周期就不归容器管理,因此只能调用初始化回调方法,并且当具有prototype作用域的bean被注入到另一个即将要被初始化的bean中,或者通过对容器的getBean()方法调用获取该bean时都会调用,但永远不会调用销毁回调方法。而对于singleton bean,则仅仅调用一次初始化回调方法,因为仅仅初始化一次,并且在容器关闭时才会调用销毁回调方法。注意:这里的“销毁”是从容器中移除该bean实例,至于这个对象到底会不会立即被GC回收则不一定! 要与容器中bean的生命周期的管理进行交互,最原始的办法是对应的bean实现Spring提供的InitializingBean和DisposableBean接口。容器为前者调用afterPropertiesSet()方法,为后者调用destroy()方法,以便bean在被容器管理的初始化之后和销毁之前执行某些操作。 但是,JSR-250注解规范中的@PostConstruct(初始回调)和@PreDestroy(销毁回调)注解是目前Spring 应用程序中定义生命周期回调方法的最佳做法。使用这些注解意味着 bean 不会耦合到特定于 Spring 的接口(不需要向上面那样继承接口)。当然如果不想使用JSR-250的注解,但仍要删除耦合,我们可以考虑基于XML配置文件的配置元数据方式:init-method和destroy-method属性,分别定义初始化之后的回调和销毁之前的回调。
如下案例,首先有一个LifecycleCallback类,用于生命周期回调的测试:
/** * bean生命周期回调 * * @author lx */ public class LifecycleCallback { /** * 基于实现InitializingBean和DisposableBean接口的回调 */ public static class LifecycleCallbackImp implements InitializingBean, DisposableBean { public LifecycleCallbackImp() { System.out.println("LifecycleCallbackImp构造器调用"); } @Override public void afterPropertiesSet() { System.out.println("LifecycleCallbackImp初始化回调"); } @Override public void destroy() { System.out.println("LifecycleCallbackImp销毁回调"); } @Override protected void finalize() { System.out.println("LifecycleCallbackImp销毁"); } } /** * 基于XML的回调 */ public static class LifecycleCallbackXml { public LifecycleCallbackXml() { System.out.println("LifecycleCallbackXml构造器调用"); } public void afterPropertiesSet() { System.out.println("LifecycleCallbackXml初始化回调"); } public void destroy() { System.out.println("LifecycleCallbackXml销毁回调"); } @Override protected void finalize() { System.out.println("LifecycleCallbackXml销毁"); } } /** * 基于注解的回调 */ public static class LifecycleCallbackAnn { public LifecycleCallbackAnn() { System.out.println("LifecycleCallbackAnn构造器调用"); } @PostConstruct public void afterPropertiesSet() { System.out.println("LifecycleCallbackAnn初始化回调"); } @PreDestroy public void destroy() { System.out.println("LifecycleCallbackAnn销毁回调"); } @Override protected void finalize() { System.out.println("LifecycleCallbackAnn销毁"); } } /** * prototype回调测试 */ public static class LifecycleCallbackPro { public LifecycleCallbackPro() { System.out.println("prototype LifecycleCallbackPro构造器调用"); } @PostConstruct public void afterPropertiesSet() { System.out.println("prototype LifecycleCallbackPro初始化回调"); } @PreDestroy public void destroy() { System.out.println("prototype LifecycleCallbackPro销毁回调"); } @Override protected void finalize() { System.out.println("prototype LifecycleCallbackPro销毁"); } } }配置文件:
<!--bean生命周期回调--> <!--基于实现接口--> <bean class="com.spring.core.LifecycleCallback.LifecycleCallbackImp" id="lifecycleCallbackImp"/> <!--基于xml配置 init-method值为初始化回调方法名 destroy-method值为销毁回调方法名 --> <bean class="com.spring.core.LifecycleCallback.LifecycleCallbackXml" id="lifecycleCallbackXml" init-method="afterPropertiesSet" destroy-method="destroy"/> <!--基于注解 需要开启注解支持,后面会讲--> <context:annotation-config/> <bean class="com.spring.core.LifecycleCallback.LifecycleCallbackAnn" id="lifecycleCallbackAnn"/> <!--prototype bean在每次从容器中获取实例时都会调用初始化回调,但是永远不会调用销毁回调--> <bean class="com.spring.core.LifecycleCallback.LifecycleCallbackPro" id="lifecycleCallbackPro" scope="prototype"/>测试:
@Test public void lifecycleCallback() { //开启容器 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); //主动获取bean才会回调 ac.getBean("lifecycleCallbackPro", LifecycleCallback.LifecycleCallbackPro.class); ac.getBean("lifecycleCallbackPro", LifecycleCallback.LifecycleCallbackPro.class); //close关闭容器,一定是close才会触发 ac.close(); //gc不一定发生,对象不一定被彻底销毁 System.gc(); }结果如下:
LifecycleCallbackImp构造器调用 LifecycleCallbackImp初始化回调 LifecycleCallbackXml构造器调用 LifecycleCallbackXml初始化回调 LifecycleCallbackAnn构造器调用 LifecycleCallbackAnn初始化回调 prototype LifecycleCallbackPro构造器调用 prototype LifecycleCallbackPro初始化回调 prototype LifecycleCallbackPro构造器调用 prototype LifecycleCallbackPro初始化回调 LifecycleCallbackAnn销毁回调 LifecycleCallbackXml销毁回调 LifecycleCallbackImp销毁回调 prototype LifecycleCallbackPro销毁 prototype LifecycleCallbackPro销毁 LifecycleCallbackAnn销毁 LifecycleCallbackXml销毁 LifecycleCallbackImp销毁如果我们基于XML或者注解方式设置回调方法,如果一个项目有自己的规范,那么通常初始化方法和回调方法在不同类中的方法声明都是一致的,即统一命名为init()、initialize()、dispose()等等。 如果方法命名有统一的规范,那么可以配置Spring容器去“查找”特定名字的初始化和销毁方法。通过配置< beans >的default-init-method=”xxx”,表明该beans下面所有的所有的bean在IOC容器进行初始化的时候,都会在合适的时间调用该bean中名称为xxx的方法。通过配置< beans >的default-destroy-method =”yyy”,表明该beans下面所有的所有的bean在IOC容器进行关闭的时候,都会在合适的时间调用该bean中名称yyy的方法。如果想覆盖默认的回调函数,可以在< bean >中定义init-method或者destroy-method。
如下案例,首先有一个DefaultLifecycleCallback类,用于默认回调的测试:
/** * @author lx */ public class DefaultLifecycleCallback { public static class DefaultLifecycleCallbackA { public DefaultLifecycleCallbackA() { System.out.println("DefaultLifecycleCallbackA构造器调用"); } public void init() { System.out.println("DefaultLifecycleCallbackA初始化回调"); } public void destroy() { System.out.println("DefaultLifecycleCallbackA销毁回调"); } @Override protected void finalize() { System.out.println("DefaultLifecycleCallbackA销毁"); } } public static class DefaultLifecycleCallbackB { public DefaultLifecycleCallbackB() { System.out.println("DefaultLifecycleCallbackB构造器调用"); } public void init() { System.out.println("DefaultLifecycleCallbackB初始化回调"); } public void destroy() { System.out.println("DefaultLifecycleCallbackB销毁回调"); } @Override protected void finalize() { System.out.println("DefaultLifecycleCallbackB销毁"); } } public static class DefaultLifecycleCallbackC { public DefaultLifecycleCallbackC() { System.out.println("DefaultLifecycleCallbackC构造器调用"); } public void initialize() { System.out.println("DefaultLifecycleCallbackC初始化回调"); } public void dispose() { System.out.println("DefaultLifecycleCallbackC销毁回调"); } @Override protected void finalize() { System.out.println("DefaultLifecycleCallbackC销毁"); } } public static class DefaultLifecycleCallbackD { public DefaultLifecycleCallbackD() { System.out.println("DefaultLifecycleCallbackD构造器调用"); } public void initialize() { System.out.println("DefaultLifecycleCallbackD初始化回调"); } public void dispose() { System.out.println("DefaultLifecycleCallbackD销毁回调"); } @Override protected void finalize() { System.out.println("DefaultLifecycleCallbackD销毁"); } } public static class DefaultLifecycleCallbackE { public DefaultLifecycleCallbackE() { System.out.println("prototype DefaultLifecycleCallbackE构造器调用"); } public void initialize() { System.out.println("prototype DefaultLifecycleCallbackE初始化回调"); } public void dispose() { System.out.println("prototype DefaultLifecycleCallbackE销毁回调"); } @Override protected void finalize() { System.out.println("prototype DefaultLifecycleCallbackE销毁"); } } }配置文件:
<!--默认生命周期回调--> <beans default-init-method="init" default-destroy-method="destroy"> <bean class="com.spring.core.DefaultLifecycleCallback.DefaultLifecycleCallbackA" id="defaultLifecycleCallbackA"/> <bean class="com.spring.core.DefaultLifecycleCallback.DefaultLifecycleCallbackB" id="defaultLifecycleCallbackB"/> </beans> <beans default-init-method="initialize" default-destroy-method="dispose"> <bean class="com.spring.core.DefaultLifecycleCallback.DefaultLifecycleCallbackC" id="defaultLifecycleCallbackC"/> <bean class="com.spring.core.DefaultLifecycleCallback.DefaultLifecycleCallbackD" id="defaultLifecycleCallbackD"/> <bean class="com.spring.core.DefaultLifecycleCallback.DefaultLifecycleCallbackE" id="defaultLifecycleCallbackE" scope="prototype"/> </beans>测试:
@Test public void defaultlifecycleCallback() { //开启容器 ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); //主动获取bean才会回调 ac.getBean("defaultLifecycleCallbackE", DefaultLifecycleCallback.DefaultLifecycleCallbackE.class); ac.getBean("defaultLifecycleCallbackE", DefaultLifecycleCallback.DefaultLifecycleCallbackE.class); //close关闭容器,一定是close才会触发 ac.close(); //gc不一定发生,对象不一定被彻底销毁 System.gc(); }结果如下:
DefaultLifecycleCallbackA构造器调用 DefaultLifecycleCallbackA初始化回调 DefaultLifecycleCallbackB构造器调用 DefaultLifecycleCallbackB初始化回调 DefaultLifecycleCallbackC构造器调用 DefaultLifecycleCallbackC初始化回调 DefaultLifecycleCallbackD构造器调用 DefaultLifecycleCallbackD初始化回调 prototype DefaultLifecycleCallbackE构造器调用 prototype DefaultLifecycleCallbackE初始化回调 prototype DefaultLifecycleCallbackE构造器调用 prototype DefaultLifecycleCallbackE初始化回调 DefaultLifecycleCallbackD销毁回调 DefaultLifecycleCallbackC销毁回调 DefaultLifecycleCallbackB销毁回调 DefaultLifecycleCallbackA销毁回调 prototype DefaultLifecycleCallbackE销毁 prototype DefaultLifecycleCallbackE销毁 DefaultLifecycleCallbackD销毁 DefaultLifecycleCallbackC销毁 DefaultLifecycleCallbackB销毁 DefaultLifecycleCallbackA销毁在new 容器时会调用refresh方法,该方法会销毁已存在的bean(执行销毁回调),并且重新初始化单例的bean(执行初始化回调),调用close方法时,则同样会销毁已存在bean(执行销毁回调)。 自Spring2.5开始,有3种方式来控制bean的生命周期回调:
实现InitializingBean和DisposableBean回调接口;自定义init() 和destroy() 方法,使用XML配置init-method和destroy-method;自定义init() 和destroy()方法,使用@PostConstruct 和 @PreDestroy注解;可以将3种方式结合使用。如果多种方式一起使用,每种方式都配置了一个不同的方法名,那么他们的执行顺序将会如下面的顺序所示。但是如果他们都配置了同一个方法名,那么该方法只会执行一次。 如果为初始化方法配置了多个不同的方法,那么执行顺序如下:
PostConstruct 注解修饰的方法;InitializingBean接口里面的afterPropertiesSet() 方法;基于XML的自定义的初始化方法。如果为销毁方法配置了多个不同的方法,那么执行顺序如下:
@PreDestroy 注解修饰的方法;DisposableBean接口里面的destroy() 方法;基于XML的自定义的销毁方法。下面的容器回调知识点了解即可! ApplicationContext容器也有自己的状态,Spring提供了一些接口,我们可以注册监听不同的状态变化,并调用不同的回调方法。
Lifecycle接口,一般用于一些后台组件活动的开启和关闭。当ApplicationContext收到一个start(start方法)或者stop(stop、close方法方法)信号时,它会将该信号传递给所有的Lifecycle接口的实现,判断并尝试调用相应的方法。 除了bean初始化和销毁回调之外,Spring管理的bean还可以实现Lifecycle接口,以便这些bean可以参与由容器自身生命周期驱动的启动和关闭过程。
/** * Lifecycle 生命周期回调 * 监听容器的start和stop事件 */ public interface Lifecycle { /** * 启动当前组件 */ void start(); /** *停止该组件,当该方法执行完成后,该组件会被完全停止。当需要异步停 * 止行为时,考虑实现SmartLifecycle 和它的 stop(Runnable) 方法变体。 */ void stop(); /** * 检查此组件是否正在运行,调用start或者stop方法前都会调用该方法 * 1. 只有该方法返回false时,start方法才会被执行。 * 2. 只有该方法返回true时,stop(Runnable callback)或stop()方法才会被执行。 * * @return 当前组件是否正在运行 */ boolean isRunning(); }如下案例,首先有一个StartShutCallback类,用于Lifecycle测试:
/** * @author lx */ public class StartShutCallback { public static class StartShutCallbackA implements Lifecycle { private boolean running; @Override public void start() { System.out.println("StartShutCallbackA start"); running = true; } @Override public void stop() { System.out.println("StartShutCallbackA stop"); running = false; } @Override public boolean isRunning() { return running; } } public static class StartShutCallbackB implements Lifecycle{ private boolean running; @Override public void start() { System.out.println("StartShutCallbackB start"); running = true; } @Override public void stop() { System.out.println("StartShutCallbackB stop"); running = false; } @Override public boolean isRunning() { return running; } public StartShutCallbackB() { LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(2)); } } }配置文件:
<!--Startup and Shutdown Callbacks--> <bean class="com.spring.core.StartShutCallback.StartShutCallbackA" id="startShutCallbackA"/> <bean class="com.spring.core.StartShutCallback.StartShutCallbackB" id="startShutCallbackB"/>测试:
@Test public void startShutCallback() { ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); ac.start(); //ac.stop(); ac.close(); }结果如下:
StartShutCallbackA start StartShutCallbackB start StartShutCallbackA stop StartShutCallbackB stoporg.springframe.context.Lifecycle接口仅仅响应start和stop行为,但是并没有实现上下文刷新时自动启动的功能。想要监听这个行为,可以使用LifecycleProcessor接口。实际上Lifecycle也是委托LifecycleProcessor来实习的。 LifecycleProcessor接口继承了Lifecycle接口,是Lifecycle的扩展。它还添加了对正在刷新和关闭的容器做出反应的另外两个方法。对于组件的控制更加详细!
/** * 生命周期处理器,继承了Lifecycle的功能 * 新增监听容器的refresh和close事件 */ public interface LifecycleProcessor extends Lifecycle { /** * 容器在refresh时的回调 */ void onRefresh(); /** * 容器在close时的回调 */ void onClose(); }如果要对特定 bean 的自动启动进行更加细粒度控制(包括启动等级),应该实现org.springframework.context.SmartLifecycle接口。 组件启动和关闭调用的顺序可能很重要。如果任何两个对象之间存在“依赖”关系,则依赖方在依赖之后开始,在依赖之前停止。然而,有时,直接依赖性是未知的。你可能只知道某个类型的对象应该先于另一个类型的对象开始。在这种情况下,应该使用SmartLifecycle接口。在其父接口Phased上定义的getPhase()方法。
/** * 启动时期 */ public interface Phased { /** * 返回对象启动时期int值 */ int getPhase(); }SmartLifecycle提供了一些方法的默认实现:
/** * 智能周期回调 */ public interface SmartLifecycle extends Lifecycle, Phased { /** * 默认启动时期,Integer.MAX_VALUE */ int DEFAULT_PHASE = Integer.MAX_VALUE; /** * 如果容器在调用refresh方法时,希望能够自己自动进行回调(即调用start方法),则返回true,默认返回true * 返回false的表示组件打算通过显式的调用start()来启动,类似于普通的Lifecycle实现。 */ default boolean isAutoStartup() { return true; } /** * SmartLifecycle自定义的Stop方法接受回调函数,将在stop完成之后执行 * * @param callback 回调函数 */ default void stop(Runnable callback) { stop(); callback.run(); } /** * 返回当前bean组件的启动时间,默认返回DEFAULT_PHASE */ @Override default int getPhase() { return DEFAULT_PHASE; } }getPhase()方法返回的int值表示启动顺序,具有最低phase的对象首先启动,停止时,则按相反顺序执行。默认的实现返回Integer.MAX_VALUE,表示组件将在最后启动并首先停止。不实现SmartLifecycle的任何其他Lifecycle对象的默认阶段是0。另外,任何负phase值都表示一个对象应该在这些标准组件开始之前开始(并在它们停止之后停止)。 另外,SmartLifecycle自己定义的Stop方法接受一个回调线程任务。任何实现的对象都必须在stop方案完成后执行该任务。在需要的时候,可以实现异步的关闭,因为LifecycleProcessor接口的默认实现DefaultLifecycleProcessor在每个phase中的对象中执行该回调任务时,可以等待一个超时值。每个phase默认超时为30秒。可以在xml中配置一个名为lifecycleProcessor的bean来覆盖默认的 LifecycleProcessor实例。如果只想修改超时,那么修改默认的LifecycleProcessor实例定义就足够了:
<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor"> <!-- timeout value in milliseconds --> <property name="timeoutPerShutdownPhase" value="10000"/> </bean>如下案例,首先有一个StartShutCallback类,用于Lifecycle测试:
/** * @author lx */ public class SmartLifecycleCallback { /** * 智能组件,phase=Integer.MAX_VALUE AutoStartup=true */ public static class SmartLifecycleCallbackA implements SmartLifecycle { private boolean running; private static int count; /** * refresh或者start的时候会调用,同样根据isRunning返回值判断 */ @Override public void start() { System.out.println("SmartLifecycleCallbackA start"); if (++count == 2) { running = true; } } /** * close或者stop的时候会调用,同样根据isRunning返回值判断 */ @Override public void stop() { System.out.println("SmartLifecycleCallbackA stop"); if (++count == 4) { running = false; } } @Override public boolean isRunning() { return running; } //也可以是重写isAutoStartup、getPhase、stop默认方法来实现自己的逻辑 @Override public int getPhase() { return 1; } } /** * 智能组件,phase=-1 AutoStartup=false */ public static class SmartLifecycleCallbackB implements SmartLifecycle { private boolean running; private static int count; /** * refresh或者start的时候会调用,同样根据isRunning返回值判断 */ @Override public void start() { System.out.println("SmartLifecycleCallbackB start"); if (++count == 1) { running = true; } } /** * close或者stop的时候会调用,同样根据isRunning返回值判断 */ @Override public void stop() { System.out.println("SmartLifecycleCallbackB stop"); if (++count == 3) { running = false; } } @Override public boolean isRunning() { return running; } //也可以是重写isAutoStartup、getPhase、stop默认方法来实现自己的逻 @Override public int getPhase() { return -1; } @Override public boolean isAutoStartup() { return false; } } /** * 普通组件,用作对比 默认值: phase=0 AutoStartup=false */ public static class SmartLifecycleCallbackC implements Lifecycle { private boolean running; @Override public void start() { System.out.println("SmartLifecycleCallbackC start"); running = true; } @Override public void stop() { System.out.println("SmartLifecycleCallbackC stop"); running = false; } @Override public boolean isRunning() { return running; } } }配置文件:
<bean class="com.spring.core.SmartLifecycleCallback.SmartLifecycleCallbackA" id="smartLifecycleCallbackA"/> <bean class="com.spring.core.SmartLifecycleCallback.SmartLifecycleCallbackB" id="smartLifecycleCallbackB"/> <bean class="com.spring.core.SmartLifecycleCallback.SmartLifecycleCallbackC" id="smartLifecycleCallbackC"/>测试:
@Test public void smartLifecycleCallback() { System.out.println("new容器的代码内部实际上会调用一次refresh操作,因此会自动启动"); ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); System.out.println("start调用"); ac.start(); System.out.println("stop调用"); ac.stop(); System.out.println("close调用"); ac.close(); }结果如下:
new 容器的代码内部实际上会调用一次refresh操作,因此会自动启动 SmartLifecycleCallbackA start start调用 SmartLifecycleCallbackB start SmartLifecycleCallbackC start SmartLifecycleCallbackA start close调用 SmartLifecycleCallbackA stop SmartLifecycleCallbackC stop SmartLifecycleCallbackB stopA组件实现了SmartLifecycle并且设置了AutoStartup=true,因此new容器时就会调用一次start启动。随后的容器的start方法则对三个组件都有效,按照phase从小到大启动:B->C->A,随后的close方法同样对三个组件都有效,按照phase从大到小停止:A->C->B。
目前,我们的案例都是非web应用案例,基本上容器随着JVM的结束而结束,并没有安全关闭的过程。我们可以为容器注册一个shutdown hook钩子方法到JVM中,当JVM关闭时,将会安全的关闭容器,让所有的资源得到释放(实际上是回调close方法的逻辑),这实际上也是一个回调方法。 测试:
@Test public void shutdownHook() { System.out.println("new容器的代码内部实际上会调用一次refresh操作,因此会自动启动"); ClassPathXmlApplicationContext ac = new ClassPathXmlApplicationContext("DI.xml"); //添加回调方法,在JVM关闭时会调用容器close方法的逻辑,如果不加那么容器并不会正常关闭 //ac.registerShutdownHook(); System.out.println("start调用"); ac.start(); }结果如下,没有开启关闭回调时:
new容器的代码内部实际上会调用一次refresh操作,因此会自动启动 SmartLifecycleCallbackA start start调用 SmartLifecycleCallbackB start SmartLifecycleCallbackC start SmartLifecycleCallbackA start开启关闭回调时:
new容器的代码内部实际上会调用一次refresh操作,因此会自动启动 SmartLifecycleCallbackA start start调用 SmartLifecycleCallbackB start SmartLifecycleCallbackC start SmartLifecycleCallbackA start SmartLifecycleCallbackA stop SmartLifecycleCallbackC stop SmartLifecycleCallbackB stop可以看到,容器正常关闭!
本文侧重于Spring的入门以及IoC的XML核心配置,并没有讲源码,适合Spring初学者。先学会用,然后再看源码,是本系列文章的核心思想,后面会有专门的文章对核心原理进行源码解析! 关于IoC或者DI,我觉得初学者不要看太多理论的文章,先大概了解是什么,然后跟着案例练习,在不断的实践中自然就能明白IoC和DI到底是什么,以及相比于传统的代码到底带来了什么的好处。 本次我们只是介绍了基于XML的配置方法,实际上现在的新的Spring项目基本是基于注解和Java代码的配置方式,XML方式一般在一些老项目中才会用到,这些东西后面我们都会一一介绍,到时候就会知道基于注解配置的好处!Spring 5.x作为一系列文章,将会不断更新,可以随时关注!
参考: https://spring.io/
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!
L-Java 认证博客专家 Javaer 博客专家 原采矿工程师,现Java研发工程师,半路出家的程序员,转行之路的探索者,终生学习的践行人!你不会找到路,除非你敢于迷路!