目录
2.6.primary属性和autowire-candidate属性的使用
IoC容器是Spring最核心的概念和内容。他替代了传统的new方式初始化对象,通过读取在配置中的Bean定义,自动创建并管理容器的Bean实例及其生命周期;最重要的是可以在Bean的定义中进行依赖对象的配置,并根据依赖配置自动注入相关依赖,降低对象之间的耦合程度,以达到解耦的效果。Spring提供了多种依赖注入的方式,包括构造函数注入和设置值注入等。
一、组件、框架、容器
组件、框架和容器是所有开发语言都适用的概念。组件时为了提高代码的重用而对代码进行隔离和封装;框架在提供一系列组件的基础上,定义了更高层级的规范和开发方式;容器对不同层级的对象进行存放和管理。
1.组件
组件是实现特定功能、符合某种规范的类集合。组件是一个通用的概念,适合所有开发语言,比如JavaScript中的JQuery库、Java语言AWT的UI组件等。从组件实现功能的角度可以将组件划分为两类:实现特定功能的组件和用来进行界面呈现的UI组件。在Java中,实现数据库连接的JDBC驱动和实现日志的Log4j都可以称为逻辑功能组件;AWT、Swing等界面开发库提供的输入框、按钮和单选框等是UI组件。组件可以自行开发,用来提高代码的重用性。组件最终呈现方式是单个或多个.class类文件,或者打包的.jar文件。
2.框架
框架一般包含具备结构关系的多个组件,这些组件相互协作构成特定的功能。Java EE是官方定义的Java企业级开发的一系列标准规范。这些标准规范中,有的只是定义了规范和接口,有的除了规范和接口外,官方也提供了实现框架,这其中有一些基于Java EE的子标准的实现框架,具体包括Enterprise JavaBean管理的EJB框架、Java Web应用程序开发的JSF框架等。除了Java官方JCP提供的框架之外,一些开源组织和厂商根据Java EE规范的接口,也提供了实现框架,如实现JMS规范接口的ActiveMQ和RabbitMQ的消息队列框架,实现JPA规范的Hibernate和MyBatis的对象数据映射框架。此外,还有一些框架没有完全遵循Java EE的标准,而是一种更贴近现实和便捷的方式来规范和实现,典型代表就是Spring Bean 管理的依赖注入框架。
3.容器
容器的概念可以应用在多种场合,具体如下:
- Java数据类型中容器类型,如List、Set和Map,这些集合类型用于存放其他对象。
- Java UI组件的容器类型。如AWT中的Windows容器,用来盛放Button等组件。
- 用来存放Java对象的Bean容器,并且对其中的Bean实例进行管理,与基本的容器类型不同的是,除了存放,还可以对该Bean实例的生命周期和依赖进行管理,比如Spring Bean容器、EJB容器等。
- Java程序运行所需要的环境,如支持Servlet的Web容器(比如Tomcat)、运行EJB的容器(如JBoss)。这里的容器更多地被称为服务器。
二、JavaBean、POJO和EJB
1.JavaBean对象
JavaBean是JCP定义地一种Java类的标准,包括属性、方法和事件三方面的规范,内容如下:
- 是一个公共作用域的类。(为了提供给其他类使用)
- 这个类有默认的无参构造函数。(为了让框架和容器可以用反射机制进行实例化)
- 这个类需要被序列化且实现自Serializable接口。(为了可以序列化和反序列化来进行对象的传输或保存到文件中)
- 可能有一系列可读写属性、通过getter和setter方法存取属性值。(为了不暴露属性,可以通过反射机制来进行属性值的读写)
此外,对于使用Java进行桌面开发的UI组件Bean,还需要支持发送外部或者从外部接收的事件,包括Click事件和Keyboard事件等。
2.POJO(简单Java对象)
POJO的具体含义是指没有继承任何类,也没有实现任何接口,不需要遵从框架的定义,更没有被其他框架侵入的Java对象。其不依赖于任何框架,不担任特殊角色,不需要是实现任何Java框架指定的接口。当一个POJO可序列化,有无参构造函数,使用getter和setter方法来访问属性时,他就是一个JavaBean。
3.EJB(企业级JavaBean)
EJB是一种规范,EJB定义的组件模型,开发人员不需要关注事务处理、安全性、资源缓存池和容错性。EJB是用来定义分布式业务逻辑的组件Bean。EJB规定的Bean定义需要遵循EJB定义的规范,继承特定的接口,所以EJB也常用来指定这种类型的Bean规范。EJB包含3种类型Bean,分别是会话Bean(Session Bean)、实体Bean(Entity Bean)和消息驱动Bean(MessageDriven Bean)。EJB设计的目标是分布式应用。
三、IoC与DI
IoC(Inversion of Control 控制反转)是一种编程思想,或者说是一种设计模式。设计模式中的抽象工厂方法从工厂类中获取同一接口的不同实现,一定程度上缓减了耦合,但代码耦合的实质还是存在。IoC模式将耦合从代码中移出去,放入配置中,容器在启动时依据依赖配置生成依赖对象并注入。使用IoC容器后,代码从内部的耦合转到外部容器,解耦性更高,也更灵活。IoC是一种软件的设计思想,DI是这种思想的一种实现。控制反转乃至依赖注入的目的不是提升系统性能,而是提升组件的重用性和系统的可维护性。依赖注入使用“反射”等底层技术,根据类名来生成相应的对象,注入依赖项和执行方法。但是与常规的方式相比,反射会消耗更多的资源并导致性能的衰减,虽然JVM改良优化后,反射方式与一般方式的性能差距逐渐缩小,但在提升代码的结构性和可维护性的同时,需要牺牲一定的系统性能为代价。
四、Spring容器的基本使用
1.Spring容器对象介绍
1.1BeanFactory
Spring可以通过Bean配置id或者Bean类来获取Bean实例。BeanFactory是IoC容器的一个重要接口。基于Spring框架的应用在启动时会根据配置创建一个实现BeanFactory接口的类对象,这个对象也就是所谓的容器对象
1.2ApplicationContext
ApplicationContext是IoC容器的另一个重要接口,被称为应用上下文,它继承自BeanFactory,包含了BeanFactory的所有功能,同时也提供了一些新的高级功能,具体如下:
- MessageSource(国际化资源接口),用于信息的国际化显示。
- ResourceLoader(资源加载接口),用于资源加载。
- ApplicationEventPublisher(应用事件发布接口)等,用于应用事件的处理。
作为Bean管理的容器接口,ApplicationContext定义了初始化、配置和组装Bean的方法,由继承接口的类实现。Spring提供了多种ApplicationContext接口的实现方式,使用XML配置的实现类包括ClassPathXmlApplicationContext和FileSystemXmlApplicationContext。两者的差别就是配置文件的读取位置不同,ClassPathXmlApplicationContext是从类的根路径开始获取XML配置文件,FileSystemXmlApplicationContext则默认从项目根路径查找配置文件。第一种方式更简洁,第二种方式较为灵活,除了从项目路径开始定位外,还可以通过file:///协议来定位配置文件。
相比于BeanFactory,ApplicationContext提供了更多的功能,一般我们使用ApplicationContext就可以,BeanFactory则主要是Spring框架本身在使用。如果是Web项目,则使用继承自ApplicationContext的WebApplicationContext,因为后者增加了对Web开发的相关支持,像ServletContext、Servlet作用域(request、session和application)等支持。
2.引入maven配置
<properties>
<spring.version>5.2.3.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
3.创建bean配置文件
XML是Spring最早使用,也是最为常见的配置方式。Bean定义是XML配置的主要内容之一。Spring基于一个或多个XML配置文件进行容器初始化,对配置的Bean进行创建和依赖注入。Spring配置文件最简单的就是以applicationContext.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-4.3.xsd">
<bean id="person" class="com.mec.spring.medol.Person"></bean>
</beans>
- <beans>标签的xmlns属性是XML Namespace的缩写,即XML命名空间。命名空间是为了解决XML元素重名的问题,类似与Java中类的包名作用。xmlns定义了默认的命名空间。除了默认的命名空间外,还有带前缀的命名空间。
- xmlns:xsi表示使用xsi作为前缀的命名空间,这个属性是为了下面的xsi:schemaLocation的定义。
- xsi:schemaLocation用于定义命名空间和对应的XSD(XML结构定义)文档的位置关系,两者是成对配置的。Schema处理器将从指定的位置读取Schema文档,根据描述的文档结构验证XML文件是否符合规范。
上面示例中<beans>是根元素,在<beans>下可以配置任意多个<bean>元素。一个Bean元素包含两个基本属性:class和id。class的值是类的全路径名,这个类的要求很简单,不需要特定的规范,也不需要继承任何接口,一般的POJO即可,但必须是类,不能使用接口。id属性给这个类的实例一个标识(bean名称,bean名称必须唯一,否则会报错),以方便容器和程序查找使用。name属性可以用来指定bean的名称或别名。这两者具体的规则如下:
- 当id存在的时候,取id值为bean的名称。
- 当id不存在,name存在时,若name的值可以通过,;或者空格分割,最后会按照分隔符得到一个String数组,数组的第一个元素作为bean名称,其他作为bean的别名。
- 当id和name都存在的时候,id为bean名称,name用来定义多个别名。
- 当id和name都不指定的时候,bean名称会自动生成。
除配置<bean>元素外,还可以在<beans>下配置任意多个import和alias元素,如下:
<import resource="引入其他bean配置文件"/>
<alias name = "bean标识" alias = "别名"/>
alias元素也可以用来给bean定义别名。对于import元素,当我们配置文件比较多的情况下,可以利用import元素将所有bean配置文件汇总到一个配置文件里面。
4.ApplicationContext初始化方式
在Spring中,使用“classpath:”表示类的根路径,如果加上*号,也就是“classpath*:”,则除了自身的类路径之外,同时也会查找依赖库(.jar)下的目录。需要注意的是,这里的类路径指的是编译后的class文件的目录,在Web项目中是WEB-INF/classes目录。
4.1.配置文件位于项目的类的根路径下
可以使用如下方式加载配置文件:
public static void main(String[] args) {
String beanXml = "applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(beanXml);
}
public static void main(String[] args) {
String beanXml = "classpath:applicationContext.xml";
ApplicationContext context = new FileSystemXmlApplicationContext(beanXml);
}
4.2.配置文件位于类的根路径的子目录下
可以使用 “/” 作为路径分割符定位,如下:
// 方式一:
ApplicationContext context = new ClassPathXmlApplicationContext("com/mec/spring/medol/applicationContext.xml");
// 方式二:
ApplicationContext context = new FileSystemXmlApplicationContext(
"classpath:com/mec/spring/medol/applicationContext.xml");
4.3.配置文件位于项目的根路径下
ApplicationContext context = new FileSystemXmlApplicationContext("applicationContext.xml");
4.4.配置文件在项目的根路径的子目录下
比如在项目的config目录下:
ApplicationContext context = new FileSystemXmlApplicationContext("config/applicationContext.xml");
4.5.使用file协议访问
ApplicationContext context = new FileSystemXmlApplicationContext(
"file:///E:/EclipseWorkSpaces/HelloSpring/applicationContext.xml");
5.容器的关闭
基于web的ApplicationContext实现会在Web应用关闭时恰当地关闭Spring IoC容器。但对于非Web应用,建议使用close()方法关闭容器。容器关闭会释放一些容器占用地资源。在实际的场景中,很多时候应用不是正常关闭的,有可能遇到RuntimeException异常关闭或者使用操作系统强制关闭。Java从JDK 1.3开始就提供了关闭钩子(Shutdown-Hook)的解决方案。在Spring中可以很容易地使用这种方式,确保容器在任何状况下恰当地关闭及释放占用地资源。如下:
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"com/mec/spring/medol/applicationContext.xml");
/**
在JVM运行时注册一个名为SpringContextShutdownHook的关闭钩子,在JVM关闭时关闭这个上下文,除非它当时已经关闭。
将实际关闭过程委托给doClose()。
*/
context.registerShutdownHook();
close()方法和registerShutdownHook()方法都是从AbstractApplicationContext抽象类才开始具有的。
五、Bean的实例化
1.通过反射调用构造方法创建bean对象
<bean id="bean名称" name="bean名称或别名" class="bean的全路径名">
<constructor-arg index="0" value="参数值" ></constructor-arg>
<constructor-arg index="1" value="参数值" ref="引用的bean名称"></constructor-arg>
<constructor-arg index="n" value="参数值" ref="引用的bean名称"></constructor-arg>
</bean>
- <constructor-arg>用于指定构造方法的参数值
- index:指定构造方法中参数的位置,从0开始一次递增
- value:指定参数值
- ref:当插入的值为容器内其他bean时,这个值为容器中对应的bean名称
示例Person类的构造方法如下:
public Person(int age, String name) {
this.age = age;
this.name = name;
}
bean配置如下:
<bean id="person" class="com.mec.spring.medol.Person">
<constructor-arg index="0" value="30"></constructor-arg>
<constructor-arg index="1" value="张三疯子"></constructor-arg>
</bean>
那么通过该配置获取到的Person对象的age就是30,name就是“张三疯子”。注意如果没有<constructor-arg>的配置,那么Bean对象必须提供无参构造方法。
2.实例化静态内部类
静态内部类是使用static关键字修饰的内部类,我们可以在不实例化外部类的情况下实例化静态内部类,可以通过外部类.静态内部类名方式访问它,如下定义一个静态内部类:
public class OuterClass {
static class InnerClass {
public void innerMethod() {
System.out.println("静态内部类的方法");
}
}
}
如果需要使用Spring容器来初始化和管理该内部类的实例,则可以在配置文件中做如下配置(使用$符连接外部类和静态内部类):
<bean id="innerObject" class="com.mec.spring.medol.OuterClass$InnerClass"></bean>
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
context.registerShutdownHook();
OuterClass.InnerClass innerObject = (OuterClass.InnerClass) context.getBean("innerObject");
innerObject.innerMethod();
}
3.通过静态工厂方法创建bean对象
public class StaticFactoryService {
private static StaticFactoryService service = new StaticFactoryService();
public StaticFactoryService getInstance() {
return service;
}
}
<bean id="staticFactoryService" class="com.mec.spring.medol.StaticFactoryService" factory-method="getInstance"></bean>
使用Spring配置静态方法时,除了指定id和class,还需要指定factory-method属性为静态方法名称,返回需要的对象。当然也可以配置Bean的<constructor-arg>元素进行构造函数参数注入。
4.通过实例工厂方法创建bean对象
我们通常会使用另外一个类作为工厂类用于获取目标类。将目标类实例的创建放在另外一个工厂类的方法中,通过配置工厂类的Bean实例和方法得到目标类实例。
public class FruitFactory {
public IFruit getFruit(String name) {
if ("apple".equals(name)) {
return new Apple();
} else if ("banana".equals(name)) {
return new Banana();
} else {
return null;
}
}
}
public class Apple implements IFruit {}
public class Banana implements IFruit {}
在配置文件中,需要配置工厂类的Bean和目标类的Bean。工厂类的Bean按一般配置即可,目标类的Bean配置不需要class属性,但需要使用factory-bean属性指定工厂类的Bean的id、使用factory-method属性指定获取对象实例的工厂类的方法。配置如下:
<!-- 配置工厂类 -->
<bean id="fruitFactory" class="com.mec.spring.medol.FruitFactory"></bean>
<!-- 配置两个目标类 -->
<bean id="apple" factory-bean="fruitFactory" factory-method="getFruit">
<constructor-arg index="0" value="apple"></constructor-arg>
</bean>
<bean id="banana" factory-bean="fruitFactory" factory-method="getFruit">
<constructor-arg index="0" value="banana"></constructor-arg>
</bean>
5.通过FactoryBean来创建bean对象
FactoryBean是一个接口,它可以让容器通过这个接口的实现来创建我们需要的bean对象。如下,通过实现FactoryBean创建Person实例:
public class UserFactoryBean implements FactoryBean<Person> {
@Override
public Person getObject() throws Exception {
return new Person();
}
@Override
public Class<?> getObjectType() {
return Person.class;
}
@Override
public boolean isSingleton() {
// 返回false表示我们每次从容器获取的对象不是单例的,那么我们每次从容器中getBean的时候都会重新调用上面的getObject方法,
//如果返回true,则表示单例的,那么容器只会调用一次getObject方法
return false;
}
}
<bean id="createPersonByFactoryBean" class="com.mec.spring.medol.UserFactoryBean"></bean>
六、Bean的其他属性配置
在<bean>的标签元素中,除了class和id属性之外,还可以通过设置更多的属性来进行个性化设定。常见的包括使用scope属性指定Bean的作用域范围;使用init-method和destroy-method指定该Bean在初始化和销毁时回调的方法;如果希望推迟该Bean初始化以加快容器初始化的速度,可以使用lazy-init属性来声明懒加载;使用parent属性可以从父Bean继承配置数据。
1.Bean的作用域配置(scope属性)
作用域在面向对象语言的定义是对象和变量的可见范围,在Spring中指定的是当前配置创建的Bean相对于其他Bean的可见范围。scope属性用来进行Bean的作用域配置,可以配置6种类型的作用域,分别是singleton、prototype、request、session、application和websocket。singleton和prototype是较为常见的类型,后面4中应用在Web应用程序中。此外如果有需要,还可以定制自己的作用域。配置方式如下:
<bean id="bean名称" class="bean全路径名" scope="作用域"></bean>
1.1.singleton(单例作用域)
默认作用域。Spring容器只创建和维护一个该类型的Bean实例,并将这个实例存储到单例缓存中,针对该Bean的请求和引用,使用的都是同一个实例。从容器启动或者第一次调用实例化开始,只要容器没有退出或者销毁,还类型的单一实例就会一直存活。需要注意的是,单例bean是整个应用共享的,所以需要考虑到线程安全问题。singleton的配置区别与单例模式,单例模式基本上是一个类对应一个单例对象,而Spring可以同一个类进行多个单例Bean的配置,也就是一个类可以对应到多个不同bean名称的对象。
1.2.prototype(多例作用域)
如果scope被配置成prototype,则在使用容器getBean时,每次都会得到一个新的对象,作为依赖对象注入到其他Bean的时候也会产生一个新的类对象。需要注意的是,容器不负责该作用域Bean实例的完整生命周期,在初始化或装配完该Bean的类对象之后,容器就不再进行管理,而需要由客户端对该对象进行管理,特别是如果该对象占用了一些资源就需要手动释放。此外,对于singleton类型的Bean,如果有配置对象生命周期的回调方法,则容器会根据配置进行调用,而prototype类型的Bean即使配置了回调方法也不会调用。
1.3.request(请求作用域)
针对每次HTTP请求,Spring都会创建一个Bean实例。
1.4.session(会话作用域)
适用于HTTP Session,同一个Session共享同一个Bean实例。
1.5.application(应用作用域)
整个Web应用,也就是早ServletContext生命周期中使用一个Bean实例。
1.6.websocket
WebSocket是HTML 5支持的一种协议和新特性,websocket作用域的配置是在一个WebSocket连接的生命周期中共用一个Bean实例。
1.7.自定义作用域
步骤如下:
- 实现Scope接口用于自定义scope;
- 将自定义的scope注册到容器中;
- 使用自定义的作用域。
Scope接口中的方法定义如下:
public interface Scope {
// 返回当前作用域中name(bean名称)对应的bean对象,如果name对应的bean在当前作用域中没有找到则可以利用objectFactory创建
Object get(String name, ObjectFactory<?> objectFactory);
// 将name对应的bean从当前作用域中移除
@Nullable
Object remove(String name);
// 用于注册销毁时的回调
void registerDestructionCallback(String name, Runnable callback);
// 用于解析相应的上下文数据,比如request作用域将返回request中的属性
@Nullable
Object resolveContextualObject(String key);
// 作用域的会话标识,比如session作用域是sessionId
@Nullable
String getConversationId();
}
示例:自定义线程级别的作用域,同一个线程中同名的bean是同一个实例,不同线程中的bean是不同的实例。
public class ThreadScope implements Scope {
public static final String THREAD_SCOPE = "thread"; //用于注册ThreadScope
private ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<Map<String, Object>>() {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<String, Object>();
}
};
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> beanMap = threadLocal.get();
Object result = beanMap.get(name);
if (result == null) {
result = objectFactory.getObject();
beanMap.put(name, result);
}
return result;
}
@Override
public Object remove(String name) {
Map<String, Object> beanMap = threadLocal.get();
return beanMap.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {}
@Override
public Object resolveContextualObject(String key) {return null;}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
配置如下:
<bean id="threadPerson" class="com.mec.spring.medol.Person" scope="thread"></bean>
测试如下:
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
context.registerShutdownHook();
// 向容器注册ThreadScope
context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());
for (int index = 0; index < 5; index++) {
new Thread(() -> {
// 同一个线程内获取的应该是同一个实例
System.out.println(Thread.currentThread() + "---" + context.getBean("threadPerson"));
System.out.println(Thread.currentThread() + "---" + context.getBean("threadPerson"));
}).start();
}
}
结果如下:
2.Bean初始化或销毁的生命周期回调
如果要在Bean实例化时完成更多的初始化操作,而偏偏这些初始化操作又依赖于注入对象,那么仅通过构造函数无法实现这些功能,可以使用回调的方式解决这个问题,在容器实例化对象并设置完属性值之后执行自定义初始化方法。销毁方法也类似,在Bean真正销毁前进行一些额外的处理,类似于资源处理或日志写入等。通过设置Bean的init-method和destroy-method属性配置对应的初始化和销毁回调方法。
<bean id="bean名称" class="bean的全路径名" init-method="初始化回调方法名" destroy-method="销毁回调方法名"></bean>
除过这种方式,还可以通过实现特定的接口和使用注解的方式实现回调。可以实现InitializingBean和DisposableBean两个接口,实现对应的初始化和销毁方法:afterPropertiesSet()和destory()。示例如下:
public class ImplCallbackService implements InitializingBean, DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("初始化方法回调...");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("销毁方法回调...");
}
}
这种方式的业务代码和框架发生耦合,违背了框架解耦的初衷,所以逐渐发展成使用注解代替实现接口的方式。
3.懒加载Bean
默认情况下,Spring容器在启动时会将所有的单例Bean实例化。这样做的好处是,如果配置有问题,实例化出错在容器启动的时候就可以提前发现。但是,如果一次需要实例的Bean太多,势必会延缓服务启动的速度,影响系统的性能。为此,Spring提供了懒加载的方式去创建对象,对于一些不需要在容器启动的时候就创建的对象,可以推迟到需要用的时候(getBean时)才去加载和创建。但是如果某个懒加载Bean作为依赖注入其他非懒加载的Bean时,则容器初始化的时候就会创建该懒加载Bean实例。Bean的懒加载可以全局设置,也可以个别设置。
3.1.全局设置
<beans default-lazy-init="true"></beans>
设置为true表示开启全局懒加载,设置为false或者default则是非懒加载。全局设置会应用在所有Bean上,一般较少使用。
3.2.个别设置
个别设置通过设置每个Bean的lazy-init属性实现懒加载。
<bean id="bean名称" class="bean的全路径名" lazy-init="true"></bean>
lazy-init的取值有以下3种:
- default:从default-lazy-init继承
- true:懒加载
- false:非懒加载
如果全局和个别两种设置共存的情况下,个别设置会覆盖全局设置。需要注意的是,对于prototype作用域的Bean,lazy-init的配置无效,它始终是以懒加载的方式创建。
4.Bean定义的继承(parent和abstract)
Spring支持Bean的继承,子Bean从父Bean的定义继承配置数据。与Java类继承不同,Java类继承是类之间的关系,是方法、属性的延续;而Bean的继承用于Bean实例之间的参数值的延续,也可以覆盖父Bean中的某些定义。Bean继承通过配置Bean的parent属性实现,parent的值设置为其他Bean(父Bean)的bean名称。
示例,下面ParentBean和ChildBean之间并没有类之间的继承关系,只不过具有几个相同的属性,而ChildBean的实例只是复用一些ParentBean的配置,目的是减少XML中重复的配置而已。
public class ParentBean {
private String name;
private int age;
private String personId;
//……省略getter和setter方法
}
public class ChildBean {
private String name;
private int age;
private String personId;
private String stuId;
//……省略getter和setter方法
}
配置如下:
<bean id="parentBean" class="com.mec.spring.medol.ParentBean" >
<property name="name" value="张三"></property>
<property name="age" value="18"></property>
<property name="personId" value="xxxxxxxxxxxxxxxxx"></property>
</bean>
<!--这里利用parent属性就复用了上面parentBean中注入的属性值-->
<bean id="childBean" class="com.mec.spring.medol.ChildBean" parent="parentBean" >
<property name="stuId" value="ssssssss"></property>
</bean>
上面的示例是子Bean包含父Bean的属性(name、age、personId),经过上面的配置,子Bean实例的属性值就会包含子Bean和父Bean的所有依赖注入(name、age、personId、stuId)。
加入现在在来一个Bean(name,age,address),那么如果这三个Bean实例的name和age值都是需要一致的,那么就可以将name和age抽取出来,作为模板配置。如下:
<!-- 抽象父bean,此处不需要指定class,但是abstract必须设置为true -->
<bean id="parentBean" abstract="true">
<property name="name" value="张三"></property>
<property name="age" value="18"></property>
</bean>
七、依赖注入的配置
1.手动注入
构造器注入和属性注入是主要的依赖注入方式,对于同一个Bean,这两种方式都可以用来注入多个依赖对象,容器会根据依赖对象类型进行一定程度的自动匹配,这两种方式也可以混合使用。如果所依赖的Bean只是该Bean所特有的,不需要托管给容器供其他Bean使用的话,可以使用内部Bean注入。
1.1.构造器注入
构造器注入,就是参数值通过构造方法被设置进去,我们可以通过<constructor-arg>标签来完成。对于多个参数且参数类型不一致时的注入,我们需要将注入的值和参数一一对应起来,也就是说需要告诉框架哪个值是注入到哪个参数身上的,构造器注入提供了三种告知方式:
- 根据参数索引注入
- 根据参数类型注入
- 根据参数名称注入
示例:
public Person(int age, String name, Address address) {
this.age = age;
this.name = name;
this.address = address;
}
我们按如下配置(根据参数索引注入)即可注入这三个参数值(其中Address也是Bean)
<bean id="address" class="com.mec.spring.medol.Address"></bean>
<bean id="person" class="com.mec.spring.medol.Person">
<constructor-arg index="0" value="18"></constructor-arg>
<constructor-arg index="1" value="张三"></constructor-arg>
<constructor-arg index="2" ref="address"></constructor-arg>
</bean>
注意value属性只能用来给简单类型设置值(byte,int,long,float,double,boolean,Byte,Long,Float,Double,枚举),spring容器内部注入的时候会将value的值转换为对应的类型。
这种方式对参数顺序有很强的依赖性,若构造函数参数位置调整,则会导致注入出错。对于根据参数类型注入,则需要使用type属性指定类型,如下:
<constructor-arg type="参数类型" value="参数值"></constructor-arg>
对于参数类型相同的参数,则按照配置顺序赋值,要么就再利用index指定参数位置。
以上两种方式都很难通过配置文件,知道所注入的参数到底是对应类种具体哪个属性的,必须要去看源码,代码的可读性不好。下面的按照参数名称注入的方式要比这两种方式好一些。配置方式如下:
<constructor-arg name="参数名称" value="参数值"></constructor-arg>
java通过反射的方式可以获取到方法的参数名称,不过源码种的参数通过编译之后就会变成class对象,通常情况下源码变成class文件之后,参数的真实名称会丢失,参数名称会变成arg0,arg1,arg2这种,和实际源码种的参数名称不一样,如果需要将源码中的参数名称保留在编译之后的class文件中,编译的时候需要用下面的命令:
javac -parameters .java文件
针对上述问题Spring也提供了解决方案,我们可以通过ConstructorProperties注解来解决。
@ConstructorProperties({ "第1个参数的名称", "第2个参数的名称", "第n个参数的名称" })
public 类名(int arg0, String arg1, Address arg2) {
}
1.2.属性注入
和构造器注入类似,属性注入是在配置文件中利用<property>标签告知框架必要注入的参数值。示例如下:
<bean id="address" class="com.mec.spring.medol.Address"></bean>
<bean id="person" class="com.mec.spring.medol.Person">
<property name="age" value="18"></property>
<property name="name" value="张三"></property>
<property name="address" ref="address"></property>
</bean>
其中name属性用于指定参数名称。属性注入其实是框架利用反射的方式调用setter方法设置值。所以必须提供setter方法。
1.3.内部Bean的依赖注入
上述注入的参数address也是一个Bean,上述是利用ref属性指定这个外部Bean的bean名称注入这个依赖的Bean。那么假如当依赖的Bean只是被某个外部Bean独立使用,而不需要暴露成一个独立的Bean提供给其他Bean使用时,则可以以内部Bean的方式进行注入。内部Bean时匿名的,不能独立访问,不需要指定id或name作为标识,即使指定了,容器也不会使用。配置方式如下:
构造器方式:
<bean id="outBeanClass" class="com.mec.spring.medol.OutBeanClass">
<constructor-arg>
<!-- 内部Bean定义 -->
<bean class="com.mec.spring.medol.InnerBeanClass">
<!-- 注入内部Bean的属性值 -->
<property name="name" value="Inner Bean Class"></property>
</bean>
</constructor-arg>
</bean>
属性注入方式:
<bean id="outBeanClass" class="com.mec.spring.medol.OutBeanClass">
<property name="innerBean">
<!-- 内部Bean定义 -->
<bean class="com.mec.spring.medol.InnerBeanClass">
<!-- 注入内部Bean的属性值 -->
<property name="name" value="Inner Bean Class"></property>
</bean>
</property>
</bean>
1.4.数组集合类型的依赖注入
除了java的简单类型之外,Spring也支持数组、java.util.List、java.util.Set、java.util.Map、java.util.Properties的集合类型的依赖注入,分别对应使用<array>、<list>、<set>、<map>、<props>元素定义。
<list>和<set>的配置方式类似,使用<value>子元素配置字符串等简单类型,使用<ref>元素配置其他Bean依赖,以属性注入,示例如下:
<property name="属性名">
<list> <!-- 注入java.util.list -->
<value>简单类型的值</value>
<value>简单类型的值</value>
<ref bean="bean名称"/>
</list>
</property>
<map>使用<entry>子元素配置键值对的元素,key指定键, value配置对应的值,如果值的类型时一个其他Bean的引用的话,则使用value-ref属性。
<property name="属性名">
<map><!-- java.util.map -->
<entry key="key 1" value="value 1"></entry><!-- 方式一 -->
<entry key="key 2"><!-- 方式二 -->
<value>value 2</value>
</entry>
<entry key-ref="bean名称" value-ref="bean名称"></entry>
<entry><!-- 方式三 -->
<key>
key对应的值可以为任意类型
</key>
<value>
value对应的值可以为任意类型
</value>
</entry>
</map>
</property>
<props>对应的时java.util.Properties的对象,用来配置字符串类型的键和值的属性,可以看成是<map>的简化。使用<prop>子元素的key指定键,子元素的内容为值,同样以属性注入,示例如下:
<property name="属性名">
<props>
<prop key="Properties Key 1">Properties Value 1</prop>
</props>
</property>
对于数组可以使用<array>元素来指定,可以结合<value>来注入简单类型的数组元素,也可以配合其他标签注入其他类型的元素。 示例如下:
<property name="属性名">
<array>
<value>数组元素</value>
</array>
</property>
综合示例:
public class OtherTypeModel {
private List<String> list;
private Set<Person> set;
private Map<String, Integer> map;
private int[] array;
private Properties properties;
//省略getter和getter方法
}
<bean id="otherTypeModel" class="com.mec.spring.medol.OtherTypeModel">
<!-- 注入List对象 -->
<property name="list">
<list>
<value>张三</value>
<value>李四</value>
</list>
</property>
<!-- 注入Set对象 -->
<property name="set">
<set>
<ref bean="person"/>
</set>
</property>
<!-- 注入Map对象 -->
<property name="map">
<map>
<entry key="key1" value="1"></entry>
<entry key="key2" value="2"></entry>
</map>
</property>
<!-- 注入数组对象 -->
<property name="array">
<array>
<value>11</value>
<value>18</value>
<value>20</value>
</array>
</property>
<!--注入properties对象-->
<property name="properties">
<props>
<prop key="key1">Properties Value1</prop>
<prop key="key2">Properties Value2</prop>
</props>
</property>
</bean>
2.自动注入
以上的依赖注入是手动注入,就是说在xml文件中采用硬编码的方式配置注入对象,这种注入方式会有一些不足之处。不利于维护和扩展。比如:
- 需要注入的对象太多,那么<property>元素或者<constructor-arg>就需要写很多,这会导致配置文件的代码量暴增
- 如果某个类中调整了一些依赖,那么对应的配置文件中的配置也需要调整。
Spring提供了自动注入(autowire)的配置,自动注入可以在全局范围内设定,也可以对单个Bean进行个别设定。
2.1.全局设定
在<beans>根元素设置default-autowire的值可以开启整个应用中配置Bean的依赖自动注入,容器会根据default-autowire设置的匹配类型自动查找符合的Bean示例进行注入。default-autowire属性可以有三种值:byName、byType、constructor,分别对应根据Bean的标识(bean名称或别名)、类的类型和构造器中参数类型来查找依赖对象。全局设定对所有Bean会自动生效,如果希望某个Bean不作为依赖被其他Bean使用的话,可以在该Bean上设置autowire-candidate的属性值为false。
2.2.个别设定
全局的自动注入智能化程度很高,但其隐藏了注入的细节,在开发中很容器出错,要谨慎使用。基于这个原因,Spring也提供了对单个Bean进行依赖自动注入的设定,通过配置<bean>的autowire来指定依赖自动装配的方式,autowire属性可设置的值有byName、byType、constructor、default(默认注入方式)。示例如下:
<bean id="bean名称" class="类的全路径名" autowire="byName|byType|constructor|default"></bean>
2.3.按照名称进行注入(byName)
Spring会按照set属性的名称去容器容器中查找同名的bean对象,然后将查找到的对象通过set方法注入到对应的bean中,未找到对应名称的bean对象则set方法不进行注入(需要注入的set属性的名称和被注入的bean的名称必须一致)
示例:
public class DiAutowireByName {
private Person person;
private String s;
private int index;
//注意这里set后面是Person
public void setPerson(Person person) {
this.person = person;
}
//省略其他setter和getter方法
}
配置如下:
<bean id="person" class="com.mec.spring.medol.Person">
<property name="age" value="18"></property>
<property name="name" value="张三"></property>
</bean>
<bean id="p" class="com.mec.spring.medol.Person">
<property name="age" value="22"></property>
<property name="name" value="李四"></property>
</bean>
<bean id="diAutowireByName" class="com.mec.spring.medol.DiAutowireByName" autowire="byName">
<!-- 这里可以使用手动注入来覆盖自动注入
<property name="person" ref="p"></property>
-->
<property name="s" value="sss"></property>
<property name="index" value="1"></property>
</bean>
这里配置了两个Bean,bean名称分别是(person和p),当容器创建diAutowireByName对应的bean时,会遍历DiAutowireByName这个类中的所有set方法,然后得到set对应的属性名称列表,接着遍历这个列表在容器中查找和这个列表中元素同名的bean进行注入。此处注入的是person对应的Bean。
2.4.按照类型进行自动注入(byType)
Spring容器会遍历类中所有的set方法,会在容器中查找和set方法的参数类型相同的bean对象,将其通过set方法进行注入,未找到对应类型的bean对象则不进行注入。需要注入的set属性的类型和被注入的bean类型需要满足isAssignableFrom关系。(按照类型自动注入的时候,如果按照类型找到了多个符合条件的bean,系统会报错)
Class.isAssignableFrom方法
c1.isAssignableFrom(c2); //该方法用于判断c2和c1是否相等,或者c2是否是c1的子类
如果基于2.3中的示例,只将autowire的值改为byType,则运行会报错,因为按照类型注入,容器找到了两个符合条件的bean(person和p)。
set方法的参数如果是下面的类型或者下面类型的数组的时候,这个set方法会被跳过注入:
Object
boolean,byte,char,double,float,int,short,long
Boolean,Byte,Character,Double,Float,Integer,Short,Long
CharSequence,Number,Date
java.time.temporal.Temporal, java.net.URI, java.util.Locale, java.lang.Class
2.5.按照构造器中的参数类型进行自动注入
Spring会找到类中所有的构造方法,然后将这些构造方法进行排序(先按修饰符进行排序,public的在前面,其他的在后面;若修饰符一样,则按照构造函数参数数量倒叙,也就是采用贪婪的模式进行匹配,Spring会尽量多注入一些需要的对象)得到构造函数列表,然后轮询这个构造器列表,判断当前构造器所有参数是否在容器中可以找到匹配的bean对象,如果可以找到就使用这个构造器进行注入,如果不能找到,那么就会跳过这个构造器,继续采用同样的方式匹配写一个构造器,直到找到一个合适的为止。
2.6.primary属性和autowire-candidate属性的使用
上面按照类型进行自动注入的时候有一个需要注意的地方就是:如果容器找到了多个符合条件的bean就会报错。
示例一:
public interface IFruit {}
public class Apple implements IFruit {}
public class Banana implements IFruit {}
上面是定义了一个IFruit的接口以及它的两个实现类Apple和Banana。
public class FruitService {
private IFruit fruit;
public IFruit getFruit() {
return fruit;
}
public void setFruit(IFruit fruit) {
this.fruit = fruit;
}
}
FruitService这个类依赖IFruit的实现类对象,我们进行按照类型自动注入的配置,如下:
<bean id="apple" class="com.mec.spring.medol.Apple"></bean>
<bean id="banana" class="com.mec.spring.medol.Banana"></bean>
<bean id="fruitService" class="com.mec.spring.medol.FruitService" autowire="byType"></bean>
运行之后报错如下:
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'fruitService' defined in class path resource [applicationContext.xml]: Unsatisfied dependency expressed through bean property 'fruit';
nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.mec.spring.medol.IFruit'
available: expected single matching bean but found 2: apple,banana
当容器对fruitService这个bean进行依赖注入的时候,发现需要注入一个IFruit接口的子类,然后通过查找发现当前存在两个该接口的bean实例:apple和banana。容器此时不知道到底该注入哪一个所以报错。
示例二:
配置如下:
<bean id="apple" class="com.mec.spring.medol.Apple"></bean>
<bean id="banana" class="com.mec.spring.medol.Banana"></bean>
这两个类都是IFruit接口的实现类,我们通过getBean(Class<T> requiredType)方法来获取该接口实现类的实例如下:
IFruit fruit = (IFruit) context.getBean(IFruit.class);
运行之后也会报错如下:
org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'com.mec.spring.medol.IFruit' available: expected single matching bean but found 2: apple,banana
可供选择的bean有两个apple和banana,容器不知道该选择哪个,所以报错。
以上两个问题可以通过<bean>的primary属性来解决,该属性的默认值为false,当遇到这两种情况的时候我们将我们想注入的<bean>的primary属性设置为true,就表示将其作为首选的注入对象,如果容器通过查找发现有多个符合条件的bean,那么就会注入这个首选对象。
除了使用primary属性可以解决该问题,还可以通过autowire-candidate属性来解决。该属性就是用来设置存在多个Bean符合注入条件的时候当前Bean是否作为依赖被其他Bean使用,如果为true就表示可以作为依赖被其他Bean使用,如果为false就表示不可以作为依赖被其他Bean使用。
所以上面的问题可以通过如下配置解决:
<bean id="apple" class="com.mec.spring.medol.Apple"></bean>
<bean id="banana" class="com.mec.spring.medol.Banana" autowire-candidate="false"></bean>
当容器发现存在两个Bean符合查找条件的时候,又发现我们已经告诉它banana这个Bean不作为依赖被其他Bean使用,所以它就确定了注入对象apple。当然如果配置文件中仅存在一个符合条件的Bean,如下:
<bean id="banana" class="com.mec.spring.medol.Banana" autowire-candidate="false"></bean>
通过下面的方式获取Bean:
IFruit fruit = (IFruit) context.getBean(IFruit.class);
此时虽然autowire-candidate属性被配置成false,但是当前只有一个符合条件的Bean,那么这个Bean也是可以作为依赖被使用的。
示例三:
public class FruitService {
private List<IFruit> fruitList;
public List<IFruit> getFruitList() {
return fruitList;
}
public void setFruitList(List<IFruit> fruitList) {
this.fruitList = fruitList;
}
}
注意此时该类中需要自动注入的是一个IFruit类型的集合。配置如下:
<bean id="apple" class="com.mec.spring.medol.Apple"></bean>
<bean id="banana" class="com.mec.spring.medol.Banana" primary="true"></bean>
<bean id="fruitService" class="com.mec.spring.medol.FruitService" autowire="byType"></bean>
将其中一个符合条件的Bean配置为首选注入对象(primary=“true”),那么被自动注入的集合中的元素会有两个(apple实例和banana实例共存),这个跟不配置primary属性的效果是一样的,由于可以将所有符合条件的Bean注入到一个集合中,容器自然不会出现难以抉择的局面,只需要将符合条件的全部装入集合中去即可(这个不同于示例一,示例一是需要容器必须做出二选一,所以报错)。但是如果配置改变如下:
<bean id="apple" class="com.mec.spring.medol.Apple"></bean>
<bean id="banana" class="com.mec.spring.medol.Banana" autowire-candidate="false"></bean>
<bean id="fruitService" class="com.mec.spring.medol.FruitService" autowire="byType"></bean>
将banana的autowire-candidate属性设置为false,这个表示的是当存在两个符合条件的Bean时,当前的Bean不能作为依赖被其他Bean使用,所以自动注入的结果是:集合中只包含apple这一个bean元素。
3.Bean方法的替换
属性和方法是类的主要组成成分,Spring可以对属性进行注入配置,也提供了对方法替换的配置。方法替换配置的使用场景:第三方提供的类方法无法满足应用需求,但是又不想通过反编译改写这个类的方法或此方法还在其他地方使用,就可以配置Bean的replaced-method元素来实现对方法的重写。
示例:如下是一个需要被替换方法的类
public class OldService {
public void method(int index, String str) {
System.out.println("OldService:method()");
}
}
定义一个新的类,用来替换目标类中旧的方法,这个新的类需要实现MethodReplacer接口,并完成其中的reimplement方法。
public class NewService implements MethodReplacer {
/**
* obj: 原目标类生成的CGLIB代理对象
* method:目标类的方法对象
* args: 方法的参数
*/
@Override
public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
System.out.println("obj : " + obj);
System.out.println("method: " + method);
System.out.println("args : ");
for (Object arg : args) {
System.out.println("\t" + arg);
}
return null;
}
}
配置如下:
<bean id="oldService" class="com.mec.spring.medol.OldService">
<replaced-method name="method" replacer="newService"></replaced-method>
</bean>
<bean id="newService" class="com.mec.spring.medol.NewService"></bean>
测试执行原类的方法,如下:
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
context.registerShutdownHook();
OldService oldService = (OldService) context.getBean("oldService");
oldService.method(1, "ssss");
}
4.特殊的依赖配置
在依赖注入配置的时候,总会遇到一些特殊情况。例如,非显式的依赖关系如何配置?在单例作用域(singleton)的Bean中如何注入多例作用域(prototype)的Bean?遇到循环依赖该如何处理?
4.1.depends-on前置依赖配置
有时候Bean对象之间虽然没有明显的依赖关系,也就是依赖Bean并不是当前Bean的属性,但是却有前后的逻辑关系。举例来说:有两个Bean,A和B,A不是B的属性,但是B的某些值的初始化又是依赖于A的,这种特殊类型的依赖关系称作前置依赖。因为彼此之间没有属性的强连接,无法使用ref属性进行关联配置,所以Spring提供了depends-on属性用于这种关系的配置。和ref属性一样,depends-on的值设置为前置依赖Bean的id。
<bean id="bean名称" class="类的全路径名" depends-on="前置依赖的bean名称"></bean>
前置依赖Bean会在本Bean实例化之前创建,在其之后被销毁。前置依赖Bean的个数可以是多个,通过分号、逗号或空格进行分割。
1>无依赖Bean的创建和销毁顺序测试
创建三个Bean,分别实现DisposableBean接口,其中方法都一致。
public class BeanModel1 implements DisposableBean {
public BeanModel1() {
System.out.println("执行BeanModel1的构造方法");
}
//当我们关闭spring容器的时候,该方法会被调用,用来做一些清理工作
@Override
public void destroy() throws Exception {
System.out.println("执行BeanModel1的销毁方法");
}
}
对这三个Bean做如下配置(配置顺序为beanModel1、beanModel2、beanModel3):
<bean id="beanModel1" class="com.mec.spring.medol.BeanModel1"></bean>
<bean id="beanModel2" class="com.mec.spring.medol.BeanModel2"></bean>
<bean id="beanModel3" class="com.mec.spring.medol.BeanModel3"></bean>
输出结果如下:
2>通过构造器强依赖的Bean的创建和销毁顺序测试
配置如下:
<bean id="beanModel3" class="com.mec.spring.medol.BeanModel3">
<constructor-arg name="beanModel2" ref="beanModel2"></constructor-arg>
</bean>
<bean id="beanModel2" class="com.mec.spring.medol.BeanModel2">
<constructor-arg name="beanModel1" ref="beanModel1"></constructor-arg>
</bean>
<bean id="beanModel1" class="com.mec.spring.medol.BeanModel1"></bean>
这里由于是通过构造器的强依赖关系,所以不论怎么配置,它的实例化顺序永远是1,2,3(因为3依赖2,2依赖1,1无依赖项),销毁顺序刚好与这个顺序相反。
3>前置依赖关系下的Bean的创建和销毁顺序测试
<bean id="beanModel3" class="com.mec.spring.medol.BeanModel3" depends-on="beanModel1 beanModel2"></bean>
<bean id="beanModel2" class="com.mec.spring.medol.BeanModel2"></bean>
<bean id="beanModel1" class="com.mec.spring.medol.BeanModel1"></bean>
4.2方法注入---不同作用域Bean的依赖配置
<bean>默认是单例作用域(singleton),使用scope属性可以进行其他作用域的配置,对于非Web应用来说,一般需要额外配置的是多例(原型)作用域(prototype)的Bean。假设现在有两个不同作用域的Bean(A和B),A的作用域singleton,B的作用域prototype。如果B依赖A(A是B的属性),则容器获取B时,每次创建一个新的B实例,但A是同一个,这自然没什么问题。但是,如果A依赖B(B是A的属性),A是单例的,只会实例化一次,那么这时作为A的属性的B会在A实例化的时候被注入,造成它也只会被实例化一次。这显然和定义B为prototype作用域的预期不符。实际场景中有可能A是功能服务类的Bean,B是用户登录相关信息的Bean。
考虑prototype作用域的Bean通过容器getBean()方法每次从容器中获取的都是一个新的实例,如果在Bean(A)类中能够获取容器对象(ApplicationContext),那么通过它就可以解决上面的问题。Spring提供了一系列Aware接口,继承这些接口就可以获取容器的相关资源,其中最主要的就是ApplicationContextAware接口。Bean类继承ApplicationContextAware接口,在接口方法里就可以得到容器的上下文。
容器本身的对象(ApplicationContext)是单例的,为了避免每个类都通过继承接口的方式获取ApplicationContext,我们可以定义一个获取容器对象的Bean类(ApplicationContextHelper)并配置成单例Bean,该Bean用来维护ApplicationContext实例。如下:
public class ApplicationContextHelper implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public ApplicationContext getApplicationContext() {
return this.applicationContext;
}
}
将其配置成单例的,通过配置告诉容器我们定义了这个ApplicationContextAware这个接口的实现类,那么容器实例化完它之后就会帮我们执行其中的setApplicationContext方法:
<bean id="applicationContextHelper" class="com.mec.spring.medol.ApplicationContextHelper"></bean>
定义ServiceA和ServiceB两个Bean:
public class ServiceB {}
public class ServiceA {
private ServiceB serviceB;
private ApplicationContextHelper applicationContextHelper;
public ServiceB getServiceB() {
return (ServiceB) this.applicationContextHelper.getApplicationContext().getBean("serviceB");
}
//省略其他getter和setter方法
}
其中ServiceA是单例的,但是它依赖的ServiceB属性是多例的,所以改写其中getServiceB()方法,这样我们每次从容器中获取时都是不同的实例。配置如下:
<bean id="serviceA" class="com.mec.spring.medol.ServiceA">
<!--这里是不需要注入serviceB,因为我们获取的时候是通过容器对象直接获取-->
<property name="applicationContextHelper" ref="applicationContextHelper"></property></bean>
<bean id="serviceB" class="com.mec.spring.medol.ServiceB" scope="prototype"></bean>
通过继承容器提供的接口虽然可以获取应用上下文对象,但是却使应用代码和容器耦合。除了继承接口的方式,Spring还提供了lookup-method的方式解决这个问题。这个lookup-method是用来配置方法的,被其指定的方法会被框架重写,然后根据其返回值类型,容器自动调用getBean()方法来返回一个Bean实例。如下为ServiceA和ServiceB:
public class ServiceB {}
public class ServiceA {
private ServiceB serviceB;
public ServiceB getServiceB() {
return null;
}
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
配置如下:
<bean id="serviceA" class="com.mec.spring.medol.ServiceA">
<lookup-method name="getServiceB" bean="serviceB"/>
</bean>
<bean id="serviceB" class="com.mec.spring.medol.ServiceB" scope="prototype"></bean>