目录
十五、单例设计模式
十六、bean标签的scope属性
十七、Spring 循环注入问题
十五、单例设计模式
设计模式:根据面向对象五大设计思想衍生出的23种常见代码写法,每种写法可以专门解决一类问题。
单例设计模式:保证某个类在整个应用程序中只有一个实例。
单例设计默认有很多种写法,我们讲解其中两种:饿汉式、懒汉式。
重要提示: 单例设计模式必须达到用纸手写的能力。
1.饿汉式
package com.tong.singleton;
/*
单例:希望类只有一个
核心思想:
1. 构造方法私有
2. 对外提供一个能够获取对象的方法。
饿汉式:
优点:实现简单
缺点:无论是否使用当前类对象,加载类时一定会实例化。
*/
public class Singleton {
// 之所以叫做饿汉式:因为类加载时就创建了对象
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
2.懒汉式
package com.tong.singleton;
/**
* 核心思想:
* 1. 构造方法私有
* 2. 对外提供一个能够获取对象的方法。
*
* 懒汉式优点和缺点:
* 优点:
* 按需创建对象。不会在加载类时直接实例化对象。
* 缺点:
* 写法相对复杂
* 多线程环境下,第一次实例化对象效率低。
*/
public class Singleton2 {
//懒汉式:不会立即实例化
private static Singleton2 singleton2;
private Singleton2() {}
public static Singleton2 getInstance() {
if (singleton2 == null) {// 不是第一次访问的线程,直接通过if判断条件不成立。直接
return
synchronized (Singleton2.class) {
if(singleton2==null) {// 防止多个线程已经执行到synchronized
singleton2 = new Singleton2();
}
}
}
return singleton2;
}
}
十六、bean标签的scope属性
1. 官方默认支持的scope属性值
Spring中 的scope控制的是Bean的作用域。也可以用注解@Scope("singleton")控制。
通过调整scope属性的取值,可以控制bean的有效范围。
一共有6个可取值,官方截图如下:
翻译过来:
singleton:默认值。bean是单例的,每次获取Bean都是同一个对象。
prototype:原型,每次获取bean都重新实例化。
request:每次请求重新实例化对象,同一个请求中多次获取时单例的。
session:每个会话内bean是单例的。
application:整个应用程序对象内bean是单例的。
websocket:同一个websocket对象内对象是单例的。
里面的singleton和prototype在Spring最基本的环境中就可以使用,不需要web环境。
但是里面的request、session、application、websocket都只有在web环境才能使用。
<bean id="people" class="com.tong.pojo.People" scope="singleton"></bean>
衍生问题:Spring 中 Bean是否是线程安全的?
如果bean的scope是单例的,bean不是线程安全的。
如果bean的scope是prototype,bean是线程安全的。
2.ThreadLocal 复习
ThreadLocal是JDK 1.2 出现类。通过ThreadLocal可以给每个线程提供一个局部变量。只要线程对象不 变,可以随时获取。
一个ThreadLocal可以存储一个Object类型值。具体可以是一个String,一个List或一个Map,具体存储值 类型可以使用泛型进行控制。
@Test
void testThreadLocal(){
ThreadLocal<String> tl = new ThreadLocal<String>();
tl.set("smallming");
new Thread(){
@Override
public void run() {
System.out.println("其他线程:"+tl.get());
}
}.start();
System.out.println(tl.get());
}
在ThreadLocal中有很多子类。
其中NamedThreadLocal是一个允许给这个局部变量起名字的实现类。使用全局final属性记录这个名 字。
如果ThreadLocal类及子类设置的泛型需要赋予初始值,可以重写initilaValue()方法。
示例代码:
@Test
void test2(){
ThreadLocal<Map<String,Object>> tl = new NamedThreadLocal<>("名字"){
@Override
protected Map<String, Object> initialValue() {
return new HashMap<>();
}
};
tl.get().put("name","smallming");// 主线程放一个值进去
new Thread(){
@Override
public void run() {
System.out.println("子线程:"+tl.get().size());
}
}.start();
System.out.println("主线程:"+tl.get().size());
}
3.SimpleThreadScope 源码分析
bean的作用域是允许自定义的。
想要自定义scope,必须让自定义类实现org.springframework.beans.factory.config.Scope接口。接口 中一共有5个方法。
在Spring框架内部,Scope接口有且只有一个实现类SimpleThreadScope,表示在同一个线程内Bean是单例的。
// 实现Scope接口,这是自定义Scope的强制语法要求
public class SimpleThreadScope implements Scope {
// 定义日志对象
private static final Log logger = LogFactory.getLog(SimpleThreadScope.class);
// 创建一个ThreadLocal对象
// 线程变量类型为Map<String,Object>
private final ThreadLocal<Map<String, Object>> threadScope =
// ThreadLocal 名称为SimpleThreadScope
new NamedThreadLocal<>("SimpleThreadScope") {
@Override
protected Map<String, Object> initialValue() {
// 给线程变量初始化
return new HashMap<>();
}
};
// 每次从IoC容器获取Bean时会被触发
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
// 取到线程变量
Map<String, Object> scope = this.threadScope.get();
// 从Map中取出值
Object scopedObject = scope.get(name);
// 如果值为null,重试实例化
if (scopedObject == null) {
// 重新获取对象
scopedObject = objectFactory.getObject();
// 把对象放入到map中
scope.put(name, scopedObject);
}
// 返回对象,不返回后续操作空指针
return scopedObject;
}
@Override
@Nullable
public Object remove(String name) {
// 获取线程变量
Map<String, Object> scope = this.threadScope.get();
// 把指定name值从Map中移除
return scope.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// 日志记录
logger.warn("SimpleThreadScope does not support destruction callbacks. " +
"Consider using RequestScope in a web environment.");
}
@Override
@Nullable
public Object resolveContextualObject(String key) {
// 没做任何处理
return null;
}
@Override
public String getConversationId() {
// 会话ID为当前线程的名字。
return Thread.currentThread().getName();
}
}
4.自定义Scope具体实现步骤
4.1 有一个类实现Scope接口
因为SimpleThreadScope是Spring框架内置实现Scope实现的实现类,我们就模仿它,写一个类。 SimpleThreadScope 表示的含义是在同一个线程内,多次获取都是单例的。
4.2 注册自定义类,并配置Bean的scope
首先在配置文件注册这个Scope。
<!-- CustomScopeConfigurer 是Spring框架中提供的注册 -->
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<!-- key 的值是自定义的,此处叫什么,下面people2的scope中就多一个什么值 -->
<entry key="thread123">
<bean class="org.springframework.context.support.SimpleThreadScope"/>
</entry>
</map>
</property>
</bean>
<!-- 指定people2的scope为thread123 -->
<bean id="people2" class="com.tong.scope.People" scope="thread123">
<property name="name" value="smallming"></property>
</bean>
4.3 测试效果
在测试类中,在不同线程内查看获取到的对象是否是同一个对象。
@Autowired
ApplicationContext ac;
@Test
void test2(){
People p1 = ac.getBean("people2", People.class);
new Thread(){
@Override
public void run() {
People p2 = ac.getBean("people2", People.class);
System.out.println("p2:"+p2);
System.out.println("p1:"+p1);
System.out.println("p1==p2:"+(p1==p2)); // 结果为false
}
}.start();
People p3 = ac.getBean("people2", People.class);
System.out.println("p1==p3:"+(p1==p3)); // 结果为true
}
十七、Spring 循环注入问题
我们前面学习了DI的两种方式,分别是构造注入和设值注入。这两种方式,官方更推荐使用构造注入。
网址:https://docs.spring.io/springframework/docs/current/reference/html/core.html#beans-setter-injection
但是有一种情况确实无法使用构造注入,就是循环依赖的问题。 循环依赖就是多个Bean相互依赖,形成一个闭环。
下面我们先看看Spring 官方中对构造注入时出现循环注入的解释。
当两个类都是用构造注入时,没有等当前类实例化完成就需要注入另一个类,而另一个类没有实例化完 整还需要注入当前类,所以这种情况是无法解决循环注入问题的的。会出现 BeanCurrentlyInCreationException异常。
其实Spring循环注入问题并不是我们开发者去解决的,而是Spring本身会根据我们的代码进行解决。其 中有的情况能解决,有的会直接报异常。汇总如下:
第一种:两个Bean都是用构造注入时,且scope为singleton是有循环注入异常的。
第二种:两个Bean都是用构造注入时,且scope为prototype是有循环注入异常的。
第三种:如果Bean的scope属性为prototype时,使用设值注入是有循环注入异常的。
第四种:如果Bean的scope属性都为singleton时,使用设值注入Spring没有循环注入异常。
第五种:如果一个Bean的scope为singleton,另一个Bean的scope为prototype,都使用设置注入时没有循环注入异常。
第六种:如果一个Bean使用设值注入,且scope为singleton,另一个Bean使用构造注入,是没有循环注入异常的。
通过这六种情况可以看出来,只要一个Bean使用设值注入,并且scope为singleton,就没有循环注入异 常。
下面通过代码给小伙伴们演示一下构造注入时循环注入的效果。
在搭建好Spring环境的项目中新建两个类: 先新建com.tong.circular.Teacher类代表老师
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
private Student student;
}
然后在新建个com.tong.circular.Student类,代表学生
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
private Teacher teacher;
}
在Spring的配置文件applicationContext.xml中设置两个Bean的循环注入
<?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 id="student" class="com.tong.circular.Student">
<constructor-arg name="teacher" ref="teacher"></constructor-arg>
</bean>
<bean id="teacher" class="com.tong.circular.Teacher">
<constructor-arg name="student" ref="student"></constructor-arg>
</bean>
</beans>
最后在测试类com.tong.test.CircularTest中编写测试代码
@SpringJUnitConfig
@ContextConfiguration("classpath:applicationContext-circular.xml")
public class CircularTest {
@Autowired
Teacher teacher;
@Test
void test(){
System.out.println(teacher);
}
}
运行测试类后会发现IDEA控制台出现异常。最后一个Cased by的异常类型是Caused by: org.springframework.beans.factory.BeanCreationException,代表着发生了循环注入问题。
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'student': Requested bean is currently in creation: Is there an unresolvable circular reference?
这种方式可以理解,因为在Java代码中下面代码就编译错误了。
Teacher teacher = new Teacher(student);// 编译错误
Student student = new Student(teacher);
第二种情况,两个Bean的scope都是prototype类型,依然使用构造注入。运行后依然出现 BeanCurrentlyInCreationException
<bean id="student" class="com.tong.circular.Student" scope="prototype">
<constructor-arg name="teacher" ref="teacher"></constructor-arg>
</bean>
<bean id="teacher" class="com.tong.circular.Teacher" scope="prototype">
<constructor-arg name="student" ref="student"></constructor-arg>
</bean>
第三种情况,使用设值注入。如果Bean的scope属性为prototype时,循环注入的效果。 我们先把配置修改一下,注入的方式修改为设置注入,并设置Bean的scope="prototype"
<bean id="student" class="com.tong.circular.Student" scope="prototype">
<property name="teacher" ref="teacher"></property>
</bean>
<bean id="teacher" class="com.tong.circular.Teacher" scope="prototype">
<property name="student" ref="student"></property>
</bean>
运行测试类,发现依然会产生循环注入问题。控制台还是出现 BeanCurrentlyInCreationException异常。
第四种情况,使用设值注入。Bean的scope属性为singleton时,循环注入的效果。
只需要修改配置文件中,把<bean> 的scope属性设置为singleton就可以了。
<bean id="student" class="com.tong.circular.Student" scope="singleton">
<property name="teacher" ref="teacher"></property>
</bean>
<bean id="teacher" class="com.tong.circular.Teacher" scope="singleton">
<property name="student" ref="student"></property>
</bean>
运行后没有循环注入异常了,但是出现StackOverflowError.是因为@Data生成的toString()中循环包含对方对象。
java.lang.StackOverflowError
修改两个类的,在关联属性都添加上不被toString()输出。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@ToString.Exclude
private Teacher teacher;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Teacher {
@ToString.Exclude
private Student student;
}
再次运行,程序可以正常输出。
这种方式之所以可以成功运行是因为单例默认下有三级缓存(DefaultSingletonBeanRegistry),可以暂时 缓存没有被实例化完成的Bean。这样就不用考虑Bean实例化时先后问题,也就不会出现循环注入问题 了。
第四种情况,使用设值注入。一个类scope="singleton",另外一个类scope="prototype"。
<bean id="student" class="com.tong.circular.Student" scope="prototype">
<property name="teacher" ref="teacher"></property>
</bean>
<bean id="teacher" class="com.tong.circular.Teacher" scope="singleton">
<property name="student" ref="student"></property>
</bean>
这时发现,两个类都可以被成功注入。 第五种情况,一个类使用构造注入,另一个类使用设置注入并且scope="singleton"
<bean id="student" class="com.tong.circular.Student" scope="singleton">
<property name="teacher" ref="teacher"></property>
</bean>
<bean id="teacher" class="com.tong.circular.Teacher" >
<constructor-arg name="student" ref="student"></constructor-arg>
</bean>
通过这些演示后小伙伴们知道了只要Bean的scope="singleton"就不会出现循环注入问题。那么在平时 我们进行代码编写时,尽量避开循环注入。如果实在无法避开,类中涉及到两个类的相互引用。例如: 双向多对一、双向一对一的关系中就必须有双向引用。这时最好使用设值注入,并且scope设置为 singleton。