Spring容器刷新obtainFreshBeanFactory示例详解

目录
  • Spring容器刷新—02—obtainFreshBeanFactory
    • BeanFactory和ApplicationContext
    • obtainFreshBeanFactory
      • 1.GenericApplicationContext系列的实现
      • 2.AbstractRefreshableApplicationContext系列的实现
    • 该使用哪个BeanFactory?
      • Servlet环境
      • SpringBoot环境

Spring容器刷新—02—obtainFreshBeanFactory

先声明一下,这篇文章是原创,不过首发在今日头条。

这次的内容是上图中的第2步,大概内容就是创建一个 BeanFactory 的实例。

BeanFactory和ApplicationContext

你要是看 spring 源码会发现 BeanFactory 的实现类相当多,而且还有各种子接口以及子接口的实现类。

ApplicationContextBeanFactory,但是你不能说 BeanFactoryApplicationContext

ApplicationContext 实现了 BeanFactory 的同时增强了 BeanFactory,所谓的增强大体上指的是上图中 ApplicationContext 实现的除了 BeanFactory 接口之外的其他接口的功能。

本文仅仅列出常用的两类实现。如下图所示:

那么这么多的实现类,实际应用中到底实例化的是哪个?

这个问题嘛…… 看情况……

上图列出来两大类实现:

  • xml 版的实现: 大体上指的是(不是绝对) AbstractRefreshableApplicationContext 的子类

    • 也就是当年我们哼哧哼哧集成 springservlet 那个年代的事情
    • 大多数不就是 ClassPathXmlApplicationContext 吗?(略过)
  • 注解版 的实现: 大体上指的是(不是绝对) GenericApplicationContext 的子类
    • Webflux 环境下一般是 AnnotationConfigReactiveWebServerApplicationContext
    • Servlet 环境下一般是 AnnotationConfigServletWebServerApplicationContext

这里提到的实现类,无论是哪个,都是派生自 AbstractApplicationContext 的。他们都是 BeanFactory

既然有 N 个 BeanFactory 的实现类,那么我们应用程序中到底使用的是哪一个呢? 文章末尾再说,先把我们本期的重点 obtainFreshBeanFactory() 的逻辑介绍完。

obtainFreshBeanFactory

obtainFreshBeanFactory() 的工作就是在刷新之前搞到一个 热乎的 BeanFactory 实例,涉及到的两个方法都是由子类实现的。

  • refreshBeanFactory(): 刷新 BeanFactory,由不同的子类提供各自的实现。
  • getBeanFactory(): 返回当前 ApplicationContext 中创建好的 BeanFactory,简单的实现往往就是一个 getter 方法。
public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext {
    /**
     * Tell the subclass to refresh the internal bean factory.
     * @return the fresh BeanFactory instance
     * @see #refreshBeanFactory()
     * @see #getBeanFactory()
     */
    protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
        refreshBeanFactory();
        return getBeanFactory();
    }
    /**
     * Subclasses must implement this method to perform the actual configuration load.
     * The method is invoked by {@link #refresh()} before any other initialization work.
     * <p>A subclass will either create a new bean factory and hold a reference to it,
     * or return a single BeanFactory instance that it holds. In the latter case, it will
     * usually throw an IllegalStateException if refreshing the context more than once.
     * @throws BeansException if initialization of the bean factory failed
     * @throws IllegalStateException if already initialized and multiple refresh
     * attempts are not supported
     */
    protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;
    /**
     * Subclasses must return their internal bean factory here. They should implement the
     * lookup efficiently, so that it can be called repeatedly without a performance penalty.
     * <p>Note: Subclasses should check whether the context is still active before
     * returning the internal bean factory. The internal factory should generally be
     * considered unavailable once the context has been closed.
     * @return this application context's internal bean factory (never {@code null})
     * @throws IllegalStateException if the context does not hold an internal bean factory yet
     * (usually if {@link #refresh()} has never been called) or if the context has been
     * closed already
     * @see #refreshBeanFactory()
     * @see #closeBeanFactory()
     */
    @Override
    public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;
}

下面看看 AbstractRefreshableApplicationContextGenericApplicationContext 这两种经典的实现类中 refreshBeanFactory()getBeanFactory() 方法的的逻辑。

1.GenericApplicationContext系列的实现

GenericApplicationContextobtainFreshBeanFactory() 的实现几乎什么也没做:

  • 所有对 Bean的注册 相关的方法都委托给了内部维护的 DefaultListableBeanFactory beanFactory
  • 该系列的实现是不支持多次刷新操作的
  • refreshBeanFactory() 也仅仅是给内部的 beanFactory 初始化了一个 ID
  • getBeanFactory() 的实现更干脆: 直接将内部维护的 beanFactory 返回接结束了
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
    // 和 BeanDefinitionRegistry 相关的方法都委托给了 `beanFactory` 这个成员变量
    private final DefaultListableBeanFactory beanFactory;
    // 状态位: 当前容器是不是已经 `刷新`过了, 也就是说 GenericApplicationContext 是不支持多次刷新操作的
    private final AtomicBoolean refreshed = new AtomicBoolean();
    /**
     * Do nothing: We hold a single internal BeanFactory and rely on callers
     * to register beans through our public methods (or the BeanFactory's).
     * @see #registerBeanDefinition
     */
    @Override
    protected final void refreshBeanFactory() throws IllegalStateException {
        if (!this.refreshed.compareAndSet(false, true)) {
            throw new IllegalStateException(
                    "GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
        }
        this.beanFactory.setSerializationId(getId());
    }
    @Override
    public final ConfigurableListableBeanFactory getBeanFactory() {
        return this.beanFactory;
    }
}

2.AbstractRefreshableApplicationContext系列的实现

AbstractRefreshableApplicationContext 的名字就能知道,这个系列的实现是支持多次刷新操作的(不像上面说的 GenericApplicationContext 这种只支持刷新一次)。

内部也维护着一个 DefaultListableBeanFactory beanFactory, 值得注意的是这个 beanFactory 是被 volatile 修饰的(涉及到多次刷新,频繁修改 beanFactory 的引用指向)。

refreshBeanFactory() 的实现分为两大步骤:

  • 销毁之前可能存在的旧的 beanFactory

    destroyBeans: 销毁 beanFactory 中所有单例(毕竟此时beanFactory都要销毁了,beanFactory中的单例肯定要顺带给销毁掉)

    closeBeanFactory: 实际上就是 this.beanFactory = null;

  • 新建一个 beanFactory 并做一些必要的初始化

    DefaultListableBeanFactory temp = createBeanFactory();: 重新创建一个 BeanFactory 实例

    temp.setSerializationId(getId());: 设置 id

    customizeBeanFactory(temp); 实际上就是给 allowCircularReferencesallowBeanDefinitionOverriding 赋值

    loadBeanDefinitions(temp); 给新创建的 BeanFactory 中加载 BeanDefinition

    • 这一步是抽象方法,不同子类的实现不同
    • 但基本上都是委托各种各样的 BeanDefinitionReader 给新创建的 BeanFactory 中添加 BeanDefinition

    this.beanFactory = temp; 新创建的 temp 上位

public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext {
    // 是否允许覆盖重复的 Bean 定义信息
    @Nullable
    private Boolean allowBeanDefinitionOverriding;
    // 当前容器是不是要支持循环依赖(spring-boot-2.6中默认禁用)
    @Nullable
    private Boolean allowCircularReferences;
    // 刷新之前可能已经存在的一个 beanFactory
    // 每次刷新都会将当前 beanFactory 销毁重建
    @Nullable
    private volatile DefaultListableBeanFactory beanFactory;
    /**
     * This implementation performs an actual refresh of this context's underlying
     * bean factory, shutting down the previous bean factory (if any) and
     * initializing a fresh bean factory for the next phase of the context's lifecycle.
     */
    @Override
    protected final void refreshBeanFactory() throws BeansException {
        if (hasBeanFactory()) { // this.beanFactory != null: 刷新之前已经有一个 beanFactory
            // 销毁旧的 beanFactory
            // 1. 调用的实际是: getBeanFactory().destroySingletons();
            destroyBeans();
            // 2. this.beanFactory = null;
            closeBeanFactory();
        }
        try {
            // 1. 重新创建一个 beanFactory
            DefaultListableBeanFactory beanFactory = createBeanFactory();
            // 2.
            beanFactory.setSerializationId(getId());
            // 3. 实际上是给 allowBeanDefinitionOverriding 和 allowCircularReferences 赋值
            customizeBeanFactory(beanFactory);
            // 4. 这是一个抽象方法: 就是给新创建的 beanFactory 中加载 `BeanDefinition`
            // BeanDefinition 的加载一般都是在子类中委托给了各种 `BeanDefinitionReader`
            loadBeanDefinitions(beanFactory);
            this.beanFactory = beanFactory;
        } catch (IOException ex) {
            throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
        }
    }
    @Override
    protected final void closeBeanFactory() {
        DefaultListableBeanFactory beanFactory = this.beanFactory;
        if (beanFactory != null) {
            beanFactory.setSerializationId(null);
            this.beanFactory = null;
        }
    }
    protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
        if (this.allowBeanDefinitionOverriding != null) {
            beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
        }
        if (this.allowCircularReferences != null) {
            beanFactory.setAllowCircularReferences(this.allowCircularReferences);
        }
    }
    protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)
            throws BeansException, IOException;
    @Override
    public final ConfigurableListableBeanFactory getBeanFactory() {
        DefaultListableBeanFactory beanFactory = this.beanFactory;
        if (beanFactory == null) {
            throw new IllegalStateException("BeanFactory not initialized or already closed - " +
                    "call 'refresh' before accessing beans via the ApplicationContext");
        }
        return beanFactory;
    }
}

AbstractRefreshableApplicationContextgetBeanFactory() 的实现也仅仅是返回了 this.beanFactory

该使用哪个BeanFactory?

ApplicationContext 的实现类有一大堆,在应用程序中到底怎么确定使用哪个实现类的呢?下面就以传统的 Servlet 环境和 spring-boot 环境为例大概看一下流程。

Servlet环境

在传统的 Servlet 环境下,都会配置一个 ContextLoaderListener 来加载上下文。

  • 获取名为 contextClassServlet 初始化参数
  • 如果能获取到 contextClass 配置, 就直接反射创建一个 contextClass 指定的类作为 ApplicationContext
  • 如果获取不到 contextClass 配置,就走默认策略
    • 所谓默认策略就是从 spring-web.jarorg.springframework.web.context.ContextLoader.ContextLoader.properties 文件中读取默认的 WebApplicationContext 实现类型
    • 默认的 WebApplicationContext 的实现类是 XmlWebApplicationContext

下面是和这个过程相关的几个源码文件:

  • web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <!-- 主要关注一下这个配置项, 如果不配置就从 spring-web.jar 的 `org.springframework.web.context.ContextLoader.ContextLoader.properties` 文件中获取 -->
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.XmlWebApplicationContext</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <!-- 省略其他配置 -->
    <!-- 省略其他配置 -->
    <!-- 省略其他配置 -->
</web-app>
  • ContextLoaderListener.java
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    // 就是在这里初始化 ApplicationContext 的
    @Override
    public void contextInitialized(ServletContextEvent event) {
        // 父类 ContextLoader 中的方法
        initWebApplicationContext(event.getServletContext());
    }
}
  • ContextLoader.java
public class ContextLoader {
    private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";
    private static final Properties defaultStrategies;
    static {
        // Load default strategy implementations from properties file.
        // This is currently strictly internal and not meant to be customized
        // by application developers.
        try {
            // 去 classpath 下加载 `ContextLoader.properties`
            // 这个文件在 spring-web.jar 的 `org.springframework.web.context.ContextLoader.ContextLoader.properties`
            ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
            defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
        } catch (IOException ex) {
            throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
        }
    }
    // 这里就是创建具体的 ApplicationContext 实例
    // 因为是 web 环境,所以创建的是 `WebApplicationContext` 的实现类的实例
    protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
        // 这里才是确定到底创建什么类型的 `WebApplicationContext`
        Class&lt;?&gt; contextClass = determineContextClass(sc);
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                    "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
        }
        return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    }
    protected Class&lt;?&gt; determineContextClass(ServletContext servletContext) {
        // CONTEXT_CLASS_PARAM常量值就是: contextClass(在 web.xml 中配置的那个)
        // 1. 如果你指定了 `contextClass` 就使用你指定的 `WebApplicationContext` 实现类
        String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
        if (contextClassName != null) {
            try {
                return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
            } catch (ClassNotFoundException ex) {
                throw new ApplicationContextException(
                        "Failed to load custom context class [" + contextClassName + "]", ex);
            }
        }
        // 2. 如果没有指定 `contextClass` 配置就使用  `defaultStrategies` 来
        else {
            contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
            try {
                return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
            } catch (ClassNotFoundException ex) {
                throw new ApplicationContextException(
                        "Failed to load default context class [" + contextClassName + "]", ex);
            }
        }
    }
}
  • ContextLoader.properties
# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.
# 指定默认的 `WebApplicationContext` 的实现类是: `XmlWebApplicationContext`
org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

下面再简单提一下 spring-boot 环境中 ApplicationContext 的创建。

SpringBoot环境

这里特指基于 spring-bootweb 项目。他是通过 ApplicationContextFactory 来创建 ApplicationContext

ApplicationContextFactory 就是一个专门用来生产 ApplicationContext 的工厂类。源码如下,具体细节会在 spring-boot 相关系列文章中提到,此处先略过。

@FunctionalInterface
public interface ApplicationContextFactory {
    // 省略几个 default 方法
    /**
     * Creates the {@link ConfigurableApplicationContext application context} for a
     * {@link SpringApplication}, respecting the given {@code webApplicationType}.
     * @param webApplicationType the web application type
     * @return the newly created application context
     */
    ConfigurableApplicationContext create(WebApplicationType webApplicationType);
}

以上就是Spring容器刷新obtainFreshBeanFactory示例详解的详细内容,更多关于Spring obtainFreshBeanFactory的资料请关注我们其它相关文章!

(0)

相关推荐

  • spring 自动注入AutowiredAnnotationBeanPostProcessor源码解析

    目录 一.MergedBeanDefinitionPostProcessor 1.1.postProcessMergedBeanDefinition 1.1.1 findAutowiringMetadata 查询属性或方法上有@Value和@Autowired注解的元素 1.1.2 检查元数据信息 二.SmartInstantiationAwareBeanPostProcessor 2.1.determineCandidateConstructors 一.MergedBeanDefinition

  • Spring populateBean属性赋值和自动注入

    目录 正文 一.postProcessAfterInstantiation:修改Bean实例 二.autowireByName:根据名称自动注入 三.autowireByType:根据类型自动注入 四.postProcessPropertyValues:处理属性值(@Resource.@Autowired.@Value) 五.applyPropertyValues:填充属性 5.1 解析依赖 5.2 解析List 正文 protected void populateBean(String bea

  • Spring createBeanInstance实例化Bean

    目录 Spring实例Bean的方法 一.determineConstructorsFromBeanPostProcessors:确定构造参数 二.autowireConstructor:有参构造创建Bean 2.1 resolveConstructorArguments:解析参数并返回最小参数个数 2.2 createArgumentArray:创建构造参数数组 2.3 instantiate:实例化Bean 三.instantiateBean:无参构造创建Bean 无参构造 四.使用实例化策

  • SpringBoot用配置影响Bean加载@ConditionalOnProperty

    目录 故事背景 调试&解决 SpringBoot 是怎么做的 故事的最后 故事背景 故事发生在几个星期前,自动化平台代码开放给整个测试团队以后,越来越多的同事开始探索平台代码.为了保障自动化测试相关的数据和沉淀能不被污染,把数据库进行了隔离.终于有了2个数据库实例,一个给dev环境用,一个给test环境用.可是随着平台的发展,越来越多的中间件被引用了.所以需要隔离的东西就越来越多了,比如MQ,Redis等.成本越来越高,如果像数据库实例一样全部分开搞一套,那在当下全域降本增效的大潮下,也是困难重

  • 全面详解Spring Bean生命周期教程示例

    目录 Spring 中 Bean 的生命周期 Bean 的实例化 构造方法注入 工厂方法注入 Bean 的属性赋值 setter注入 构造方法注入 Bean 的初始化 初始化方法 InitializingBean 接口 Bean 的销毁 销毁方法 DisposableBean 接口 总结 Spring 中 Bean 的生命周期 是当今最流行的 Java 开发框架之一,其强大的 Bean容器机制是其中的核心之一.Bean 是指在 Spring 容器中被管理的对象,它们可以被注入到其他对象中,也可以

  • SpringIOC容器Bean的作用域及生命周期实例

    目录 bean作用域 1. 默认单实例 2. 设置多实例 bean生命周期 一.生命周期过程示例 二.更完整的过程 1. 创建后置处理器 bean作用域 bean的作用域,其实就是设置创建 bean 的实例是属于单实例,还是多实例. 1. 默认单实例 默认情况下,创建的 bean 是单实例对象. 比如,用之前的代码为例: @Test public void testCollection2() { ApplicationContext context = new ClassPathXmlAppli

  • Spring容器刷新obtainFreshBeanFactory示例详解

    目录 Spring容器刷新—02—obtainFreshBeanFactory BeanFactory和ApplicationContext obtainFreshBeanFactory 1.GenericApplicationContext系列的实现 2.AbstractRefreshableApplicationContext系列的实现 该使用哪个BeanFactory? Servlet环境 SpringBoot环境 Spring容器刷新—02—obtainFreshBeanFactory

  • Spring JPA find分页示例详解

    目录 前言 源码 一.单纯分页查询 查询结果 结论 二.排序分页查询 查询结果 三.方法整理 总结: 前言 在现实项目中,数据量一般都不小,如果一次性全部请求出来,肯定是影响性能,而且大量数据展示到页面上观感也不好.这时我们就需要用到分页,给定一个 pageSize,每次请求的数量就是 pageSize 的大小,这样既可以节约时间,又可以美化页面.Spring JPA 就为我们提供这样一个方法,准确来说是提供了一个对象用来约束数据按照分页的形式进行请求. 源码 findAll(Pageable

  • Spring条件注解@Conditional示例详解

    前言 @Conditional是Spring4新提供的注解,它的作用是根据某个条件创建特定的Bean,通过实现Condition接口,并重写matches接口来构造判断条件.总的来说,就是根据特定条件来控制Bean的创建行为,这样我们可以利用这个特性进行一些自动的配置. 本文将分为三大部分,@Conditional源码的介绍.Condition的使用示例和@Conditional的扩展注解的介绍. 一.@Conditional的源码 @Target({ElementType.TYPE, Elem

  • Spring中统一异常处理示例详解

    前言 系统很多地方都会抛出异常, 而Java的异常体系目标就是与逻辑解耦,Spring提供了统一的异常处理注解,用户只需要在错误的时候提示信息即可 在具体的SSM项目开发中,由于Controller层为处于请求处理的最顶层,再往上就是框架代码的. 因此,肯定需要在Controller捕获所有异常,并且做适当处理,返回给前端一个友好的错误码. 不过,Controller一多,我们发现每个Controller里都有大量重复的.冗余的异常处理代码,很是啰嗦. 能否将这些重复的部分抽取出来,这样保证Co

  • Spring MVC异常处理机制示例详解

    前言 在Spring MVC中,当一个请求发生异常(Controller抛出一个异常时), DispatcherServlet 采用委托的方式交给一个处理链来处理或者解析这个抛出的异常,这是在request和Servlet Container之间的一道屏障,所以我们可以在这里做一些处理工作,如转换异常,转换成友好的error page或者http 状态码等. 核心接口 这个处理机制在Spring是以HandlerExceptionResolver接口为核心的,该接口只有一个处理方法: Model

  • spring mvc路径匹配原则详解

    在Spring MVC中经常要用到拦截器,在配置需要要拦截的路径时经常用到<mvc:mapping/>子标签,其有一个path属性,它就是用来指定需要拦截的路径的.例如: <mvc:interceptor> <mvc:mapping path="/**" /> <bean class="com.i360r.platform.webapp.runtime.view.interceptor.GenericInterceptor"

  • Spring Cloud动态配置刷新RefreshScope使用示例详解

    目录 引言 一.了解@RefreshScope,先要了解@Scope 二.RefreshScope 的实现原理 三.使用——@RefreshScope 使用流程 引言 用过Spring Cloud的同学都知道在使用动态配置刷新的我们要配置一个 @RefreshScope,在类上才可以实现对象属性的的动态更新. @RefreshScope 能实现动态刷新全仰仗着 @Scope这个注解. 一.了解@RefreshScope,先要了解@Scope 1.RefreshScope继承于GenericSco

  • spring结合hibernate示例详解

    单纯Hibernate程序 1.首先是导入hibernate的jar包. 2. 建立用户和用户操作记录实体,Log.Java和User.java.代码如下所示. Log.java import java.util.Date; public class Log { private int id; //日志的类别.日志一般起到一个不可否认性. //操作日志 安全日志 事件日志. private String type; private String detail; private Date time

  • spring在IoC容器中装配Bean详解

    1.Spring配置概述 1.1.概述 Spring容器从xml配置.java注解.spring注解中读取bean配置信息,形成bean定义注册表: 根据bean定义注册表实例化bean: 将bean实例放入bean缓存池: 应用程序使用bean. 1.2.基于xml的配置 (1)xml文件概述 xmlns------默认命名空间 xmlns:xsi-------标准命名空间,用于指定自定义命名空间的schema文件 xmlns:xxx="aaaaa"-------自定义命名空间,xx

  • Spring依赖注入(DI)两种方式的示例详解

    目录 一.依赖注入方式 二.setter注入 引用类型 简单类型 三.构造器注入 引用类型 简单类型 参数适配(了解) 四.依赖注入方式选择 一.依赖注入方式 思考:向一个类中传递数据的方式有几种? 普通方法(set方法) 构造方法 思考:依赖注入描述了在容器中建立bean与bean之间依赖关系的过程,如果bean运行需要的是数字或字符串呢? 引用类型 简单类型(基本数据类型与String) 依赖注入方式: setter注入 简单类型 引用类型 构造器注入 简单类型 引用类型 二.setter注

随机推荐