Spring系列第6篇:玩转bean scope,避免跳坑里!

本文内容

  1. 详细介绍5中bean的sope及使用注意点java

  2. 自定义做用域的实现web

应用中,有时候咱们须要一个对象在整个应用中只有一个,有些对象但愿每次使用的时候都从新建立一个,spring对咱们这种需求也提供了支持,在spring中这个叫作bean的做用域,xml中定义bean的时候,能够经过scope属性指定bean的做用域,如:算法

<bean id="" class="" scope="做用域" /> 

spring容器中scope常见的有5种,下面咱们分别来介绍一下。spring

singleton

当scope的值设置为singleton的时候,整个spring容器中只会存在一个bean实例,经过容器屡次查找bean的时候(调用BeanFactory的getBean方法或者bean之间注入依赖的bean对象的时候),返回的都是同一个bean对象,singleton是scope的默认值,因此spring容器中默认建立的bean对象是单例的,一般spring容器在启动的时候,会将scope为singleton的bean建立好放在容器中(有个特殊的状况,当bean的lazy被设置为true的时候,表示懒加载,那么使用的时候才会建立),用的时候直接返回。数据库

案例

bean xml配置
<!-- 单例bean,scope设置为singleton -->
<bean id="singletonBean" class="com.javacode2018.lesson001.demo4.BeanScopeModel" scope="singleton">
    <constructor-arg index="0" value="singleton"/>
</bean>
BeanScopeModel代码
package com.javacode2018.lesson001.demo4;

public class BeanScopeModel {
    public BeanScopeModel(String beanScope) {
        System.out.println(String.format("create BeanScopeModel,{sope=%s},{this=%s}", beanScope, this));
    }
}

上面构造方法中输出了一段文字,一会咱们能够根据输出来看一下这个bean何时建立的,是从容器中获取bean的时候建立的仍是容器启动的时候建立的。缓存

测试用例
package com.javacode2018.lesson001.demo4;

import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * 公众号:路人甲Java,工做10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!
 * <p>
 * bean做用域
 */
public class ScopeTest {

    ClassPathXmlApplicationContext context;

    @Before
    public void before() {
        System.out.println("spring容器准备启动.....");
        //1.bean配置文件位置
        String beanXml = "classpath:/com/javacode2018/lesson001/demo4/beans.xml";
        //2.建立ClassPathXmlApplicationContext容器,给容器指定须要加载的bean配置文件
        this.context = new ClassPathXmlApplicationContext(beanXml);
        System.out.println("spring容器启动完毕!");
    }

    /**
     * 单例bean
     */
    @Test
    public void singletonBean() {
        System.out.println("---------单例bean,每次获取的bean实例都同样---------");
        System.out.println(context.getBean("singletonBean"));
        System.out.println(context.getBean("singletonBean"));
        System.out.println(context.getBean("singletonBean"));
    }

}

上面代码中before方法上面有@Before注解,这个是junit提供的功能,这个方法会在全部@Test标注的方法以前以前运行,before方法中咱们对容器进行初始化,而且在容器初始化先后输出了一段文字。安全

上面代码中,singletonBean方法中,3次获取singletonBean对应的bean。session

运行测试用例
spring容器准备启动.....
create BeanScopeModel,{sope=singleton},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@e874448}
spring容器启动完毕!
---------单例bean,每次获取的bean实例都同样---------
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448
com.javacode2018.lesson001.demo4.BeanScopeModel@e874448
结论

从输出中获得2个结论多线程

  • 前3行的输出能够看出,BeanScopeModel的构造方法是在容器启动过程当中调用的,说明这个bean实例在容器启动过程当中就建立好了,放在容器中缓存着并发

  • 最后3行输出的是同样的,说明返回的是同一个bean对象

单例bean使用注意

单例bean是整个应用共享的,因此须要考虑到线程安全问题,以前在玩springmvc的时候,springmvc中controller默认是单例的,有些开发者在controller中建立了一些变量,那么这些变量实际上就变成共享的了,controller可能会被不少线程同时访问,这些线程并发去修改controller中的共享变量,可能会出现数据错乱的问题;因此使用的时候须要特别注意。

prototype

若是scope被设置为prototype类型的了,表示这个bean是多例的,经过容器每次获取的bean都是不一样的实例,每次获取都会从新建立一个bean实例对象。

案例

bean xml配置
<!-- 多例bean,scope设置为prototype-->
<bean id="prototypeBean" class="com.javacode2018.lesson001.demo4.BeanScopeModel" scope="prototype">
    <constructor-arg index="0" value="prototype"/>
</bean>
新增一个测试用例

ScopeTest中新增一个方法

/**
 * 多例bean
 */
@Test
public void prototypeBean() {
    System.out.println("---------单例bean,每次获取的bean实例都同样---------");
    System.out.println(context.getBean("prototypeBean"));
    System.out.println(context.getBean("prototypeBean"));
    System.out.println(context.getBean("prototypeBean"));
}
运行测试用例
spring容器准备启动.....
spring容器启动完毕!
---------单例bean,每次获取的bean实例都同样---------
create BeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@289d1c02}
com.javacode2018.lesson001.demo4.BeanScopeModel@289d1c02
create BeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@22eeefeb}
com.javacode2018.lesson001.demo4.BeanScopeModel@22eeefeb
create BeanScopeModel,{sope=prototype},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@17d0685f}
com.javacode2018.lesson001.demo4.BeanScopeModel@17d0685f
结论

输出中能够看出,容器启动过程当中并无去建立BeanScopeModel对象,3次获取prototypeBean获得的都是不一样的实例,每次获取的时候才会去调用构造方法建立bean实例。

多例bean使用注意

多例bean每次获取的时候都会从新建立,若是这个bean比较复杂,建立时间比较长,会影响系统的性能,这个地方须要注意。

下面要介绍的3个:request、session、application都是在spring web容器环境中才会有的。

request

当一个bean的做用域为request,表示在一次http请求中,一个bean对应一个实例;对每一个http请求都会建立一个bean实例,request结束的时候,这个bean也就结束了,request做用域用在spring容器的web环境中,这个之后讲springmvc的时候会说,spring中有个web容器接口WebApplicationContext,这个里面对request做用域提供了支持,配置方式:

<bean id="" class="" scope="request" />

session

这个和request相似,也是用在web环境中,session级别共享的bean,每一个会话会对应一个bean实例,不一样的session对应不一样的bean实例,springmvc中咱们再细说。

<bean id="" class="" scope="session" />

application

全局web应用级别的做用于,也是在web环境中使用的,一个web应用程序对应一个bean实例,一般状况下和singleton效果相似的,不过也有不同的地方,singleton是每一个spring容器中只有一个bean实例,通常咱们的程序只有一个spring容器,可是,一个应用程序中能够建立多个spring容器,不一样的容器中能够存在同名的bean,可是sope=aplication的时候,无论应用中有多少个spring容器,这个应用中同名的bean只有一个。

<bean id="" class="" scope="application" />

自定义scope

有时候,spring内置的几种sope都没法知足咱们的需求的时候,咱们能够自定义bean的做用域。

自定义Scope 3步骤

第1步:实现Scope接口

咱们来看一下这个接口定义

package org.springframework.beans.factory.config;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.lang.Nullable;

public interface Scope {

    /**
    * 返回当前做用域中name对应的bean对象
    * name:须要检索的bean的名称
    * objectFactory:若是name对应的bean在当前做用域中没有找到,那么能够调用这个ObjectFactory来建立这个对象
    **/
    Object get(String name, ObjectFactory<?> objectFactory);

    /**
     * 将name对应的bean从当前做用域中移除
     **/
    @Nullable
    Object remove(String name);

    /**
     * 用于注册销毁回调,若是想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义做用域选择是否是要销毁相应的对象
     */
    void registerDestructionCallback(String name, Runnable callback);

    /**
     * 用于解析相应的上下文数据,好比request做用域将返回request中的属性。
     */
    @Nullable
    Object resolveContextualObject(String key);

    /**
     * 做用域的会话标识,好比session做用域将是sessionId
     */
    @Nullable
    String getConversationId();

}
第2步:将自定义的scope注册到容器

须要调用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope的方法,看一下这个方法的声明

/**
* 向容器中注册自定义的Scope
*scopeName:做用域名称
* scope:做用域对象
**/
void registerScope(String scopeName, Scope scope);
第3步:使用自定义的做用域

定义bean的时候,指定bean的scope属性为自定义的做用域名称。

案例

需求

下面咱们来实现一个线程级别的bean做用域,同一个线程中同名的bean是同一个实例,不一样的线程中的bean是不一样的实例。

实现分析

需求中要求bean在线程中是贡献的,因此咱们能够经过ThreadLocal来实现,ThreadLocal能够实现线程中数据的共享。

下面咱们来上代码。

ThreadScope
package com.javacode2018.lesson001.demo4;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.lang.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 自定义本地线程级别的bean做用域,不一样的线程中对应的bean实例是不一样的,同一个线程中同名的bean是同一个实例
 */
public class ThreadScope implements Scope {

    public static final String THREAD_SCOPE = "thread";//@1

    private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new HashMap<>();
        }
    };

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get().get(name);
        if (Objects.isNull(bean)) {
            bean = objectFactory.getObject();
            beanMap.get().put(name, bean);
        }
        return bean;
    }

    @Nullable
    @Override
    public Object remove(String name) {
        return this.beanMap.get().remove(name);
    }

    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        //bean做用域范围结束的时候调用的方法,用于bean清理
        System.out.println(name);
    }

    @Nullable
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    @Nullable
    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

@1:定义了做用域的名称为一个常量thread,能够在定义bean的时候给scope使用

BeanScopeModel
package com.javacode2018.lesson001.demo4;

public class BeanScopeModel {
    public BeanScopeModel(String beanScope) {
        System.out.println(String.format("线程:%s,create BeanScopeModel,{sope=%s},{this=%s}", Thread.currentThread(), beanScope, this));
    }
}

上面的构造方法中会输出当前线程的信息,到时候能够看到建立bean的线程。

bean配置文件

beans-thread.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">

    <!-- 自定义scope的bean -->
    <bean id="threadBean" class="com.javacode2018.lesson001.demo4.BeanScopeModel" scope="thread">
        <constructor-arg index="0" value="thread"/>
    </bean>
</beans>

注意上面的scope是咱们自定义的,值为thread

测试用例
package com.javacode2018.lesson001.demo4;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import java.util.concurrent.TimeUnit;

/**
 * 公众号:路人甲Java,工做10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!
 * <p>
 * 自定义scope
 */
public class ThreadScopeTest {
    public static void main(String[] args) throws InterruptedException {
        String beanXml = "classpath:/com/javacode2018/lesson001/demo4/beans-thread.xml";
        //手动建立容器
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();
        //设置配置文件位置
        context.setConfigLocation(beanXml);
        //启动容器
        context.refresh();
        //向容器中注册自定义的scope
        context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());//@1

        //使用容器获取bean
        for (int i = 0; i < 2; i++) { //@2
            new Thread(() -> {
                System.out.println(Thread.currentThread() + "," + context.getBean("threadBean"));
                System.out.println(Thread.currentThread() + "," + context.getBean("threadBean"));
            }).start();
            TimeUnit.SECONDS.sleep(1);
        }
    }
}

注意上面代码,重点在@1,这个地方向容器中注册了自定义的ThreadScope。

@2:建立了2个线程,而后在每一个线程中去获取一样的bean 2次,而后输出,咱们来看一下效果。

运行输出
线程:Thread[Thread-1,5,main],create BeanScopeModel,{sope=thread},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530}
Thread[Thread-1,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530
Thread[Thread-1,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@4049d530
线程:Thread[Thread-2,5,main],create BeanScopeModel,{sope=thread},{this=com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da}
Thread[Thread-2,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da
Thread[Thread-2,5,main],com.javacode2018.lesson001.demo4.BeanScopeModel@87a76da

从输出中能够看到,bean在一样的线程中获取到的是同一个bean的实例,不一样的线程中bean的实例是不一样的。

总结

  1. spring容器自带的有2种做用域,分别是singleton和prototype;还有3种分别是spring web容器环境中才支持的request、session、application

  2. singleton是spring容器默认的做用域,一个spring容器中同名的bean实例只有一个,屡次获取获得的是同一个bean;单例的bean须要考虑线程安全问题

  3. prototype是多例的,每次从容器中获取同名的bean,都会从新建立一个;多例bean使用的时候须要考虑建立bean对性能的影响

  4. 一个应用中能够有多个spring容器

  5. 自定义scope 3个步骤,实现Scope接口,将实现类注册到spring容器,使用自定义的sope

案例源码

连接:https://pan.baidu.com/s/1p6rcfKOeWQIVkuhVybzZmQ 
提取码:zr99

Spring系列

  1. Spring系列第1篇:为什么要学spring?

  2. Spring系列第2篇:控制反转(IoC)与依赖注入(DI)

  3. Spring系列第3篇:Spring容器基本使用及原理

  4. Spring系列第4篇:xml中bean定义详解(-)

  5. Spring系列第5篇:建立bean实例这些方式大家都知道?

更多好文章

  1. Java高并发系列(共34篇)

  2. MySql高手系列(共27篇)

  3. Maven高手系列(共10篇)

  4. Mybatis系列(共12篇)

  5. 聊聊db和缓存一致性常见的实现方式

  6. 接口幂等性这么重要,它是什么?怎么实现?

  7. 泛型,有点难度,会让不少人懵逼,那是由于你没有看这篇文章!

感谢你们的阅读,也欢迎您把这篇文章分享给更多的朋友一块儿阅读!谢谢!

路人甲java

▲长按图片识别二维码关注

路人甲Java:工做10年的前阿里P7分享Java、算法、数据库方面的技术干货!坚信用技术改变命运,让家人过上更体面的生活!