【Spring注解驱动开发】使用@Scope注解设置组件的作用域

简介:

【Spring注解驱动开发】使用@Scope注解设置组件的作用域

写在前面
Spring容器中的组件默认是单例的,在Spring启动时就会实例化并初始化这些对象,将其放到Spring容器中,之后,每次获取对象时,直接从Spring容器中获取,而不再创建对象。如果每次从Spring容器中获取对象时,都要创建一个新的实例对象,该如何处理呢?此时就需要使用@Scope注解设置组件的作用域。

项目工程源码已经提交到GitHub:https://github.com/sunshinelyz/spring-annotation

本文内容概览
@Scope注解概述
单实例bean作用域
多实例bean作用域
单实例bean作用域如何创建对象?
多实例bean作用域如何创建对象?
单实例bean注意的事项
多实例bean注意的事项
自定义Scope的实现
@Scope注解概述
@Scope注解能够设置组件的作用域,我们先来看@Scope注解类的源码,如下所示。

package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

@AliasFor("scopeName")
String value() default "";
/**
 * Specifies the name of the scope to use for the annotated component/bean.
 * <p>Defaults to an empty string ({@code ""}) which implies
 * {@link ConfigurableBeanFactory#SCOPE_SINGLETON SCOPE_SINGLETON}.
 * @since 4.2
 * @see ConfigurableBeanFactory#SCOPE_PROTOTYPE
 * @see ConfigurableBeanFactory#SCOPE_SINGLETON
 * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
 * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
 * @see #value
 */
@AliasFor("value")
String scopeName() default "";

ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

}
从源码中可以看出,在@Scope注解中可以设置如下值。

ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
很明显,在@Scope注解中可以设置的值包括ConfigurableBeanFactory接口中的SCOPE_PROTOTYPE和SCOPE_SINGLETON,以及WebApplicationContext类中SCOPE_REQUEST和SCOPE_SESSION。这些都是什么鬼?别急,我们来一个个查看。

首先,我们进入到ConfigurableBeanFactory接口中,发现在ConfigurableBeanFactory类中存在两个常量的定义,如下所示。

public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {

String SCOPE_SINGLETON = "singleton";
String SCOPE_PROTOTYPE = "prototype";
/*****************此处省略N多行代码*******************/

}
没错,SCOPE_SINGLETON就是singleton,SCOPE_PROTOTYPE就是prototype。

那么,WebApplicationContext类中SCOPE_REQUEST和SCOPE_SESSION又是什么鬼呢?就是说,当我们使用了Web容器来运行Spring应用时,在@Scope注解中可以设置WebApplicationContext类中SCOPE_REQUEST和SCOPE_SESSION的值,而SCOPE_REQUEST的值就是request,SCOPE_SESSION的值就是session。

综上,在@Scope注解中的取值如下所示。

singleton:表示组件在Spring容器中是单实例的,这个是Spring的默认值,Spring在启动的时候会将组件进行实例化并加载到Spring容器中,之后,每次从Spring容器中获取组件时,直接将实例对象返回,而不必再次创建实例对象。从Spring容器中获取对象,小伙伴们可以理解为从Map对象中获取对象。
prototype:表示组件在Spring容器中是多实例的,Spring在启动的时候并不会对组件进行实例化操作,而是每次从Spring容器中获取组件对象时,都会创建一个新的实例对象并返回。
request:每次请求都会创建一个新的实例对象,request作用域用在spring容器的web环境中。
session:在同一个session范围内,创建一个新的实例对象,也是用在web环境中。
application:全局web应用级别的作用于,也是在web环境中使用的,一个web应用程序对应一个bean实例,通常情况下和singleton效果类似的,不过也有不一样的地方,singleton是每个spring容器中只有一个bean实例,一般我们的程序只有一个spring容器,但是,一个应用程序中可以创建多个spring容器,不同的容器中可以存在同名的bean,但是sope=aplication的时候,不管应用中有多少个spring容器,这个应用中同名的bean只有一个。
其中,request和session作用域是需要Web环境支持的,这两个值基本上使用不到,如果我们使用Web容器来运行Spring应用时,如果需要将组件的实例对象的作用域设置为request和session,我们通常会使用request.setAttribute("key",object)和session.setAttribute("key", object)的形式来将对象实例设置到request和session中,通常不会使用@Scope注解来进行设置。

单实例bean作用域
首先,我们在io.mykit.spring.plugins.register.config包下创建PersonConfig2配置类,在PersonConfig2配置类中实例化一个Person对象,并将其放置在Spring容器中,如下所示。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**

  • @author binghe
  • @version 1.0.0
  • @description 测试@Scope注解设置的作用域
    */

@Configuration
public class PersonConfig2 {

@Bean("person")
public Person person(){
    return new Person("binghe002", 18);
}

}
接下来,在SpringBeanTest类中创建testAnnotationConfig2()测试方法,在testAnnotationConfig2()方法中,创建ApplicationContext对象,创建完毕后,从Spring容器中按照id获取两个Person对象,并打印两个对象是否是同一个对象,代码如下所示。

@Test
public void testAnnotationConfig2(){

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
//从Spring容器中获取到的对象默认是单实例的
Object person1 = context.getBean("person");
Object person2 = context.getBean("person");
System.out.println(person1 == person2);

}
由于对象在Spring容器中默认是单实例的,所以,Spring容器在启动时就会将实例对象加载到Spring容器中,之后,每次从Spring容器中获取实例对象,直接将对象返回,而不必在创建新对象实例,所以,此时testAnnotationConfig2()方法会输出true。如下所示。

这也验证了我们的结论:对象在Spring容器中默认是单实例的,Spring容器在启动时就会将实例对象加载到Spring容器中,之后,每次从Spring容器中获取实例对象,直接将对象返回,而不必在创建新对象实例。

多实例bean作用域
修改Spring容器中组件的作用域,我们需要借助于@Scope注解,此时,我们将PersonConfig2类中Person对象的作用域修改成prototype,如下所示。

@Configuration
public class PersonConfig2 {

@Scope("prototype")
@Bean("person")
public Person person(){
    return new Person("binghe002", 18);
}

}
其实,使用@Scope设置作用域就等同于在XML文件中为bean设置scope作用域,如下所示。

此时,我们再次运行SpringBeanTest类的testAnnotationConfig2()方法,此时,从Spring容器中获取到的person1对象和person2对象还是同一个对象吗?

通过输出结果可以看出,此时,输出的person1对象和person2对象已经不是同一个对象了。

单实例bean作用域何时创建对象?
接下来,我们验证下在单实例作用域下,Spring是在什么时候创建对象的呢?

首先,我们将PersonConfig2类中的Person对象的作用域修改成单实例,并在返回Person对象之前打印相关的信息,如下所示。

@Configuration
public class PersonConfig2 {

@Scope
@Bean("person")
public Person person(){
    System.out.println("给容器中添加Person....");
    return new Person("binghe002", 18);
}

}
接下来,我们在SpringBeanTest类中创建testAnnotationConfig3()方法,在testAnnotationConfig3()方法中,我们只创建Spring容器,如下所示。

@Test
public void testAnnotationConfig3(){

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);

}
此时,我们运行SpringBeanTest类中的testAnnotationConfig3()方法,输出的结果信息如下所示。

从输出的结果信息可以看出,Spring容器在创建的时候,就将@Scope注解标注为singleton的组件进行了实例化,并加载到Spring容器中。

接下来,我们运行SpringBeanTest类中的testAnnotationConfig2(),结果信息如下所示。

说明,Spring容器在启动时,将单实例组件实例化之后,加载到Spring容器中,以后每次从容器中获取组件实例对象,直接返回相应的对象,而不必在创建新对象。

多实例bean作用域何时创建对象?
如果我们将对象的作用域修改成多实例,那什么时候创建对象呢?

此时,我们将PersonConfig2类的Person对象的作用域修改成多实例,如下所示。

@Configuration
public class PersonConfig2 {

@Scope("prototype")
@Bean("person")
public Person person(){
    System.out.println("给容器中添加Person....");
    return new Person("binghe002", 18);
}

}
我们再次运行SpringBeanTest类中的testAnnotationConfig3()方法,输出的结果信息如下所示。

可以看到,终端并没有输出任何信息,说明在创建Spring容器时,并不会实例化和加载多实例对象,那多实例对象是什么时候实例化的呢?接下来,我们在SpringBeanTest类中的testAnnotationConfig3()方法中添加一行获取Person对象的代码,如下所示。

@Test
public void testAnnotationConfig3(){

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
Object person1 = context.getBean("person");

}
此时,我们再次运行SpringBeanTest类中的testAnnotationConfig3()方法,结果信息如下所示。

从结果信息中,可以看出,当向Spring容器中获取Person实例对象时,Spring容器实例化了Person对象,并将其加载到Spring容器中。

那么,问题来了,此时Spring容器是否只实例化一个Person对象呢?我们在SpringBeanTest类中的testAnnotationConfig3()方法中再添加一行获取Person对象的代码,如下所示。

@Test
public void testAnnotationConfig3(){

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
Object person1 = context.getBean("person");
Object person2 = context.getBean("person");

}
此时,我们再次运行SpringBeanTest类中的testAnnotationConfig3()方法,结果信息如下所示。

从输出结果可以看出,当对象的Scope作用域为多实例时,每次向Spring容器获取对象时,都会创建一个新的对象并返回。此时,获取到的person1和person2就不是同一个对象了,我们也可以打印结果信息来进行验证,此时在SpringBeanTest类中的testAnnotationConfig3()方法中打印两个对象是否相等,如下所示。

@Test
public void testAnnotationConfig3(){

ApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig2.class);
Object person1 = context.getBean("person");
Object person2 = context.getBean("person");
System.out.println(person1 == person2);

}
此时,我们再次运行SpringBeanTest类中的testAnnotationConfig3()方法,结果信息如下所示。

可以看到,当对象是多实例时,每次从Spring容器中获取对象时,都会创建新的实例对象,并且每个实例对象都不相等。

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

多实例bean注意的事项
多例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,会影响系统的性能,这个地方需要注意。

自定义Scope
如果Spring内置的几种sope都无法满足我们的需求的时候,我们可以自定义bean的作用域。

1.如何实现自定义Scope
自定义Scope主要分为三个步骤,如下所示。

(1)实现Scope接口

我们先来看下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属性为自定义的作用域名称。

2.自定义Scope实现案例
例如,我们来实现一个线程级别的bean作用域,同一个线程中同名的bean是同一个实例,不同的线程中的bean是不同的实例。

这里,要求bean在线程中是共享的,所以我们可以通过ThreadLocal来实现,ThreadLocal可以实现线程中数据的共享。

此时,我们在io.mykit.spring.plugins.register.scope包下新建ThreadScope类,如下所示。

package io.mykit.spring.plugins.register.scope;

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";

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();
}

}
在ThreadScope类中,我们定义了一个常量THREAD_SCOPE,在定义bean的时候给scope使用。

接下来,我们在io.mykit.spring.plugins.register.config包下创建PersonConfig3类,并使用@Scope("thread")注解标注Person对象的作用域为Thread范围,如下所示。

package io.mykit.spring.plugins.register.config;

import io.mykit.spring.bean.Person;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

/**

  • @author binghe
  • @version 1.0.0
  • @description 测试@Scope注解设置的作用域
    */

@Configuration
public class PersonConfig3 {

@Scope("thread")
@Bean("person")
public Person person(){
    System.out.println("给容器中添加Person....");
    return new Person("binghe002", 18);
}

}
最后,我们在SpringBeanTest类中创建testAnnotationConfig4()方法,在testAnnotationConfig4()方法中创建Spring容器,并向Spring容器中注册ThreadScope对象,接下来,使用循环创建两个Thread线程,并分别在每个线程中获取两个Person对象,如下所示。

@Test
public void testAnnotationConfig4(){

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PersonConfig3.class);
//向容器中注册自定义的scope
context.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());

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

}
此时,我们运行SpringBeanTest类的testAnnotationConfig4()方法,输出的结果信息如下所示。

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

注意:这里,我将Person类进行了相应的调整,去掉Lombok的注解,手动写构造函数和setter与getter方法,如下所示。

package io.mykit.spring.bean;

import java.io.Serializable;

/**

  • @author binghe
  • @version 1.0.0
  • @description 测试实体类
    */

public class Person implements Serializable {

private static final long serialVersionUID = 7387479910468805194L;
private String name;
private Integer age;

public Person() {
}

public Person(String name, Integer age) {
    this.name = name;
    this.age = age;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public Integer getAge() {
    return age;
}

public void setAge(Integer age) {
    this.age = age;
}

}
好了,咱们今天就聊到这儿吧!别忘了给个在看和转发,让更多的人看到,一起学习一起进步!!

项目工程源码已经提交到GitHub:https://github.com/sunshinelyz/spring-annotation

原文地址https://www.cnblogs.com/binghe001/p/13067071.html

相关文章
|
9天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
527 8
|
7天前
|
Java Spring 容器
Spring使用异步注解@Async正确姿势
Spring使用异步注解@Async正确姿势,异步任务,spring boot
|
6天前
|
XML Java 数据格式
spring复习03,注解配置管理bean
Spring框架中使用注解配置管理bean的方法,包括常用注解的标识组件、扫描组件、基于注解的自动装配以及使用注解后的注意事项,并提供了一个基于注解自动装配的完整示例。
spring复习03,注解配置管理bean
|
7天前
|
XML 前端开发 Java
控制spring框架注解介绍
控制spring框架注解介绍
|
20天前
|
Java 数据库连接 数据格式
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
IOC/DI配置管理DruidDataSource和properties、核心容器的创建、获取bean的方式、spring注解开发、注解开发管理第三方bean、Spring整合Mybatis和Junit
【Java笔记+踩坑】Spring基础2——IOC,DI注解开发、整合Mybatis,Junit
|
6天前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
2月前
|
缓存 Java Maven
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
Java本地高性能缓存实践问题之SpringBoot中引入Caffeine作为缓存库的问题如何解决
|
3月前
|
Java 测试技术 数据库
Spring Boot中的项目属性配置
本节课主要讲解了 Spring Boot 中如何在业务代码中读取相关配置,包括单一配置和多个配置项,在微服务中,这种情况非常常见,往往会有很多其他微服务需要调用,所以封装一个配置类来接收这些配置是个很好的处理方式。除此之外,例如数据库相关的连接参数等等,也可以放到一个配置类中,其他遇到类似的场景,都可以这么处理。最后介绍了开发环境和生产环境配置的快速切换方式,省去了项目部署时,诸多配置信息的修改。
|
3月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
101 0
下一篇
无影云桌面