Spring 6【单例设计模式、bean标签的scope属性、Spring 循环注入问题】(八)-全面详解(学习总结---从入门到深化)

 

目录

十五、单例设计模式

十六、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。

猜你喜欢

转载自blog.csdn.net/m0_58719994/article/details/131987396
今日推荐