spring容器初始化过程中出现异常_spring容器什么时候启动

spring容器初始化过程中出现异常_spring容器什么时候启动前言我们知道,spring的启动其实就是容器的启动,而一般情况下,容器指的其实就是上下文ApplicationContext。AbstractApplicationContext作为整个A

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

前言

我们知道,spring 的启动其实就是容器的启动,而一般情况下,容器指的其实就是上下文 ApplicationContext

AbstractApplicationContext 作为整个 ApplicationContext 体系中最高级的抽象类,为除了 ComplexWebApplicationContextSimpleWebApplicationContext 这两个容器外的全部容器,规定好了 refresh 的整体流程,所有的容器在完成一些自己的初始化配置后,都需要调用该 refresh 方法,依次完成指定内容的初始化。

也就是说,读懂了 AbstractApplicationContext.refresh() 方法,其实就读懂了容器的启动流程:

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {

        // ================= 一、上下文的初始化 =================
        // 准备上下文
        prepareRefresh();
        // 通知子类刷新内部工厂
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
        // 准备bean工厂以便在当前上下文中使用
        prepareBeanFactory(beanFactory);

        try {
            // ================= 二、BeanFactory的初始化 =================
            // 对工厂进行默认后置处理
            postProcessBeanFactory(beanFactory);
            // 使用后置处理器对工厂进行处理
            invokeBeanFactoryPostProcessors(beanFactory);
            // 注册Bean后置处理器
            registerBeanPostProcessors(beanFactory);

            // ================= 三、事件,Bean及其他配置的初始化 =================
            // 初始化此上下文的消息源
            initMessageSource();
            // 为此上下文初始化事件广播者
            initApplicationEventMulticaster();
            // 初始化特定上下文子类中的其他特殊bean
            onRefresh();
            // 检查侦听器bean并注册
            registerListeners();
            // 实例化所有非懒加载的剩余单例
            finishBeanFactoryInitialization(beanFactory);
            // 完成刷新
            finishRefresh();
        }


        // ================= 异常处理 =================
        catch (BeansException ex) {
            if (logger.isWarnEnabled()) {
                logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
            }
            // 销毁已创建的单例
            destroyBeans();
            // 重置上下文的激活状态
            cancelRefresh(ex);
            throw ex;
        }
        finally {
            // 重置内部的一些元数据缓存
            resetCommonCaches();
        }
    }
}

从总体来看,该方法描述的初始化过程大概分为三步:

笔者将基于 spring 源码 5.2.x 分支,分别通过五篇文章从源码分析 spring 容器的初始化过程。

本文是其中的第三篇文章,将介绍上下文中事件,Bean及其他配置的初始化。

相关文章:

一、初始化数据源

调用 AbstarctApplicationContext.initMessageSource() 用于初始化上下文所使用的数据源对象 MessageSource,这个配置用于支持国际化的信息处理,一般情况下比较少会直接去配置它:

protected void initMessageSource() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    // 如果当前上下文否存在名为“messageSource”的Bean
    if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
        this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
        // Make MessageSource aware of parent MessageSource.
        if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
            // 若父容器没有MessageSource,就把它设置为父容器的MessageSource
            HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
            if (hms.getParentMessageSource() == null) {
                // Only set parent context as parent MessageSource if no parent MessageSource
                // registered already.
                hms.setParentMessageSource(getInternalParentMessageSource());
            }
        }
        if (logger.isTraceEnabled()) {
            logger.trace("Using MessageSource [" + this.messageSource + "]");
        }
    }
    else {
        // 不存在就尝试获取父容器的MessageSource
        DelegatingMessageSource dms = new DelegatingMessageSource();
        dms.setParentMessageSource(getInternalParentMessageSource());
        this.messageSource = dms;
        beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
        if (logger.isTraceEnabled()) {
            logger.trace("No '" + MESSAGE_SOURCE_BEAN_NAME + "' bean, using [" + this.messageSource + "]");
        }
    }
}

二、初始化事件广播者

调用 AbstarctApplicationContext.initApplicationEventMulticaster() 是初始化 spring 事件机制的第一步,它的作用很简单:

如果当前 BeanFactory 有名为 “applicationEventMulticaster”ApplicationEventMulticaster,就把它设置为当前上下文的事件广播器,否则就创建并在 BeanFactory 中注册一个SimpleApplicationEventMulticaster 实例作为当前上下文的事件广播器。

protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    // 是否存在“applicationEventMulticaster”这个Bean
    if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
        // 如果存在就把它设置为当前上下文的事件广播器
        this.applicationEventMulticaster =
            beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
        if (logger.isTraceEnabled()) {
            logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
        }
    }
    else {
        // 没有就创建一个SimpleApplicationEventMulticaster作为当前上下文的事件广播器
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
        if (logger.isTraceEnabled()) {
            logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
                         "[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");
        }
    }
}

三、初始化特殊Bean

AbstarctApplicationContext.onRefresh() 用于在完成上下文与 BeanFactory 初始化后去初始化一些特殊的 Bean,其实从方法名就可以看出来,这个方法主要是作为上下文初步刷新完毕后的回调使用。

AbstarctApplicationContext 中只提供了空实现,实际上也只有很少的实现类会去重新实现这个方法,至少在 5.2.x 里面,关于这个方法的有用实现只有:

UiApplicationContextUtils.initThemeSource(this)

该代码用于初始化一些 spring 的“主题资源”,一般用于配合消息国际化进行一些处理。

四、注册事件监听器

当消息和事件相关的内容都准备就绪后,上下文会调用 AbstarctApplicationContext.registerListeners 方法以注册事件监听器 ApplicationListener

这一步代码不动,实际上逻辑也很简单:

  • 向事件广播器注册已经被注册的上下文中的监听器;
  • 向事件广播器注册还没有被实例化的监听器的 BeanName
  • 发布一些早期事件;
protected void registerListeners() {
    // 向事件广播器注册已经被注册的上下文中的监听器
    for (ApplicationListener<?> listener : getApplicationListeners()) {
        getApplicationEventMulticaster().addApplicationListener(listener);
    }

    // 向事件广播器注册指定的监听器,不过这里只注册BeanName,
    // 因为有些监听器Bean是由FactoryBean生产的,而在这里FactoryBean实际上还没被生成出来
    String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
    for (String listenerBeanName : listenerBeanNames) {
        getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    }

    // 发布一些早期事件
    Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
    this.earlyApplicationEvents = null;
    if (!CollectionUtils.isEmpty(earlyEventsToProcess)) {
        for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
            getApplicationEventMulticaster().multicastEvent(earlyEvent);
        }
    }
}

这里有两个比较有意思的地方:

  • getApplicationListeners 获取的监听器实际上也是通过一个名为 EventListenerMethodProcessorBeanFactoryPostProcessor 注册到上下文的;
  • 注册 BeanName 而不是直接注册 Bean 这一点是为了迁就 FactoryBean。实际上在初始化 BeanFactory 的时候,调用 BeanFactoryPostProcessor 和注册 BeanPostProcessor 也都专门对此进行了处理;

五、实例化工厂中的Bean

当调用 AbstarctApplicationContext.finishBeanFactoryInitialization() 的时候,spring 会根据 BeanFactory 中已经注册的 BeanDefinition 实例化所有非懒加载的单例 Bean

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    // 为BeanFactory设置ConversionService
    // 该接口为spring转换器体系的入口
    if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
        beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
        beanFactory.setConversionService(
            beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
    }

    // 注册一个StringValueResolver,没有就从上下文的环境对象中获取
    // 该解析器用于解析配置文件中的一些占位符以及SpEL表达式
    if (!beanFactory.hasEmbeddedValueResolver()) {
        beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
    }

    // 若存在AOP使用的支持类加载时织入切面逻辑的类加载器,则优先将该Bean初始化
    String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
    for (String weaverAwareName : weaverAwareNames) {
        getBean(weaverAwareName);
    }

    // 由于类加载器已经初始化完成,所以可以停用临时的类加载器了
    beanFactory.setTempClassLoader(null);

    // 锁定当前工厂的配置
    beanFactory.freezeConfiguration();

    // 初始化剩余未初始化的非懒加载单例Bean
    beanFactory.preInstantiateSingletons();
}

这个方法总共干了五件事:

  • BeanFactory 设置类型转换服务 ConversionService
  • BeanFactory 设置占位符转换器 StringValueResolver
  • 禁用临时的类加载器,若有则启用支持类加载时织入切面逻辑的类加载器;
  • 锁定当前 BeanFactory 的配置;
  • 初始化剩余未初始化的非懒加载单例 Bean

这里我们重点关注 BeanFactory.preInstantiateSingletons() 方法,此处是实际上完成 Bean 初始化的代码:

public void preInstantiateSingletons() throws BeansException {
    if (logger.isTraceEnabled()) {
        logger.trace("Pre-instantiating singletons in " + this);
    }

    // Iterate over a copy to allow for init methods which in turn register new bean definitions.
    // While this may not be part of the regular factory bootstrap, it does otherwise work fine.
    List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

    // 遍历beanName,若BeanName是可以实例化的非懒加载单例Bean,则将其实例化
    for (String beanName : beanNames) {
        RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
        if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
			// 如果是FactoryBean
            if (isFactoryBean(beanName)) {
                Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
                if (bean instanceof FactoryBean) {
                    FactoryBean<?> factory = (FactoryBean<?>) bean;
                    boolean isEagerInit;
                    // 类型为SmartFactoryBean,则是否立刻实例化由SmartFactoryBean.isEagerInit()决定
                    if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
                        isEagerInit = AccessController.doPrivileged(
                            (PrivilegedAction<Boolean>) ((SmartFactoryBean<?>) factory)::isEagerInit,
                            getAccessControlContext());
                    }
                    else {
                    // 类型不为SmartFactoryBean,则不立刻实例化
                        isEagerInit = (factory instanceof SmartFactoryBean &&
                                       ((SmartFactoryBean<?>) factory).isEagerInit());
                    }
                    if (isEagerInit) {
                        getBean(beanName);
                    }
                }
            }
            else {
                // 实例化bean
                getBean(beanName);
            }
        }
    }

    // 获取所有实现了SmartInitializingSingleton接口的Bean,调用Bean初始化后回调afterSingletonsInstantiated
    for (String beanName : beanNames) {
        Object singletonInstance = getSingleton(beanName);
        if (singletonInstance instanceof SmartInitializingSingleton) {
            SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
            if (System.getSecurityManager() != null) {
                AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
                    smartSingleton.afterSingletonsInstantiated();
                    return null;
                }, getAccessControlContext());
            }
            else {
                smartSingleton.afterSingletonsInstantiated();
            }
        }
    }
}

这一步主要干了两件事:

  • 如果是允许实例化的非懒加载普通 Bean,就直接初始化;
  • 如果是允许实例化的非懒加载 FactoryBean,则判断它是否是 SmartFactoryBean
    1. 如果不是,则放弃直接初始化;
    2. 如果是,则根据 SmartFactoryBean.isEagerInit() 判断是否要直接初始化;
  • 初始化所有可初始化的 Bean 后,如果这些 Bean 实现了 SmartInitializingSingleton 接口,则调用该接口提供的回调函数;

这里需要注意两点:

  • BeanFactory.getBean() 实际才是最终完成 BeanFactory 创建 Bean 实例操作的方法,在这个方法中将根据 BeanDefinition 完成各自依赖的自动装配、Bean 的后置处理等操作,三级缓存也是在这个时候使用的,这部分的内容将会在后续另起一篇文章分析;
  • 此处仅预加载了 FactoryBean,而没有懒加载 FactoryBean 里面的 Bean,因此 FactoryBean 提供的 Bean 总是懒加载的;
  • SmartInitializingSingleton 接口用于提供 BeanFactory 在初始化全部非懒加载 Bean 时调用的回调函数;

至此,BeanFactory 中所有可以预先初始化的 Bean 都完成的初始化,我们已经可以通过 BeanFactory 正常的去获取 Bean 了。

六、完成刷新

AbstractApplicationContext.finishRefresh() 是完成容器刷新的最后一步,它跟 AbstractApplicationContext.onRefresh() 一样是一个钩子方法。

protected void finishRefresh() {
    // 清空资源缓存
    clearResourceCaches();

    // 初始化上下文的生命周期处理器
    initLifecycleProcessor();

    // 调用上下文的生命周期处理器
    getLifecycleProcessor().onRefresh();

    // 发布上下文刷新完毕事件
    publishEvent(new ContextRefreshedEvent(this));

    // 注册用于支持通过JMX管理spring的组件,这里不过多分析,
    // 关于JMX具体可以参考这篇文章:https://www.wdbyte.com/java/jmx.html#_3-2-%E8%B5%84%E6%BA%90%E4%BB%A3%E7%90%86-mbean-server
    LiveBeansView.registerApplicationContext(this);
}

1、清空上下文资源缓存

public void clearResourceCaches() {
    this.resourceCaches.clear();
}

2、初始化上下文的生命周期处理器

protected void initLifecycleProcessor() {
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    // 若存在名为“lifecycleProcessor”的bean,则设置为生命周期处理器
    if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) {
        this.lifecycleProcessor =
            beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class);
        if (logger.isTraceEnabled()) {
            logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]");
        }
    }
    // 若不存在名为“lifecycleProcessor”的bean,则创建一个DefaultLifecycleProcessor并设置为生命周期处理器
    else {
        DefaultLifecycleProcessor defaultProcessor = new DefaultLifecycleProcessor();
        defaultProcessor.setBeanFactory(beanFactory);
        this.lifecycleProcessor = defaultProcessor;
        beanFactory.registerSingleton(LIFECYCLE_PROCESSOR_BEAN_NAME, this.lifecycleProcessor);
        if (logger.isTraceEnabled()) {
            logger.trace("No '" + LIFECYCLE_PROCESSOR_BEAN_NAME + "' bean, using " +
                         "[" + this.lifecycleProcessor.getClass().getSimpleName() + "]");
        }
    }
}

3、生命周期处理

这里需要着重研究一下生命周期处理器的调用。

getLifecycleProcessor().onRefresh() 这一步,将会获取上一步设置到上下文中的 LifecycleProcessor 然后调用:

// AbstraceApplicationContext.getLifecycleProcessor()
LifecycleProcessor getLifecycleProcessor() throws IllegalStateException {
    if (this.lifecycleProcessor == null) {
        throw new IllegalStateException("LifecycleProcessor not initialized - " +
                                        "call 'refresh' before invoking lifecycle methods via the context: " + this);
    }
    return this.lifecycleProcessor;
}

这里我们以默认的生命周期处理器 DefaultLifecycleProcessor 为例:

@Override
public void onRefresh() {
    startBeans(true);
    this.running = true;
}

private void startBeans(boolean autoStartupOnly) {
    // 获取所有实现了Lifecycle接口的Bean,并按阶段分组装到不同的LifecycleGroup里
    Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
    Map<Integer, LifecycleGroup> phases = new HashMap<>();
    lifecycleBeans.forEach((beanName, bean) -> {
        // 同时满足下述条件的Bean不会被处理
        // 1.入参的autoStartupOnly为true
        // 2.bean实现了SmartLifecycle接口
        // 3.SmartLifecycle.isAutoStartup()方法返回false
        if (!autoStartupOnly || (bean instanceof SmartLifecycle && ((SmartLifecycle) bean).isAutoStartup())) {
            // 若实现了SmartLifecycle接口,则返回SmartLifecycle.getPhase(),否则默认返回0
            int phase = getPhase(bean); 
            LifecycleGroup group = phases.get(phase);
            if (group == null) {
                group = new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly);
                phases.put(phase, group);
            }
            group.add(beanName, bean);
        }
    });
    
    // 按阶段从小到大排序,依次处理
    if (!phases.isEmpty()) {
        List<Integer> keys = new ArrayList<>(phases.keySet());
        Collections.sort(keys);
        for (Integer key : keys) {
            phases.get(key).start();
        }
    }
}

可以看到,这里针对 SmartLifecycle 接口的实现类做了很多特殊化的处理,默认情况下:

  • 实现了 SmartLifecycle 接口的 Bean,需要保证 SmartLifecycle.isAutoStartup 返回 true 才会被处理;
  • 没实现 SmartLifecycle 接口,但是实现了 Lifecycle 接口的 Bean 会被直接处理;

并且,在处理 Bean 的时候,还会根据声明周期“阶段”按顺序从小到大排序:

  • 实现了 SmartLifecycle 接口的 Bean,按照 SmartLifecycle.getPhase 返回值排序从小到大执行;
  • 没实现 SmartLifecycle 接口,但是实现了 Lifecycle 接口的 Bean ,“阶段”视为 0,会被最先处理;

4、发布上下文刷新完毕事件

这个操作其实也很简单,其实就是调用时间广播器推送一个 ContextRefreshedEvent 事件:

public class ContextRefreshedEvent extends ApplicationContextEvent {
    public ContextRefreshedEvent(ApplicationContext source) {
        super(source);
    }
}

这个事件里唯一一个参数就是上下文本身。

这一部分主要逻辑在事件推送上,后续会在专门的文章分析 spring 提供的事件机制,这里就不过多展开。

总结

本文内容比较零散,主要干三件事:

  • 初始化消息源相关组件:
    1. initMessageSource:初始化上下文使用的消息源;
    2. onRefresh:上下文刷新时的回调函数,但是一般只用于加载 ThemeSource
  • 初始化事件相关组件:
    1. initApplicationEventMulticaster:初始化事件广播器;
    2. registerListeners:注册容器中的事件监听器 ApplicationListener
  • 初始化 BeanFactory 中所有非抽象的非懒加载 Bean
  • 完成刷新:
    1. 清空上下文中的资源缓存;
    2. 初始化并调用 Bean 生命周期处理器;
    3. 发布上下文刷新时间;
    4. 注册并初始化用于支持 JMX 的组件;
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/170760.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 摄像头-MIPI接口、DVP接口和CSI接口[通俗易懂]

    摄像头-MIPI接口、DVP接口和CSI接口[通俗易懂]本文转自摄像头的MIPI接口、DVP接口和CSI接口-百度经验(baidu.com),感谢作者分享一般来讲,摄像头的接口主要有MIPI接口、DVP接口、CSI接口三大类;我们常用的电脑摄像头接口是USB接口,而常见的智能手机上的摄像头是MIPI接口,还有一部分的摄像头(比如说某些支持DVP接口的硬件)是DVP接口;通俗的讲,USB是串行通用串行总线(UniversalSerialBus)的简称,而MIPI是移动行业处理器接口(MobileIndustryProcessorInterf

  • java8以后字符串常量池的位置,以及元空间的探秘,使用VisualVM进行实战验证

    java8以后字符串常量池的位置,以及元空间的探秘,使用VisualVM进行实战验证  在网上看了很多博客,解释也比较多,关于字符串常量池的具体位置难以分辨谁真谁假。  对于jdk8以后的版本有人说字符串常量池在元空间中,也有人说字符串常量池存在堆中。  到底谁说的对?他们的说法有依据吗?  今天让我们来一起探讨一下这个问题有人说字符串常量池在java堆中,可又有人说常量池存在元空间中。分享几篇知乎文章关于jvm运行时数据区的模型:1、面试官|JVM为什么使用元空间替换了永久代?2、Java方法区与元空间为了解决这个问题,下面我们通过Idea、VisualVm

  • 身份管理系统与解决方案[通俗易懂]

    身份管理系统与解决方案[通俗易懂]身份管理的进化式发展身份管理的需求来自于不同部门安全服务解决方案框架身份管理解决方案整体架构典型企业安全身份管理逻辑架构新员工入职帐号创建员工入职服务–基于策略身份供应实现统一认证

  • js动态创建元素,并控制元素样式

    js动态创建元素,并控制元素样式js动态创建元素,并控制元素样式

  • Unity实战篇 | 教你怎样将Unity的启动Logo 设置成 自己制作的 帧动画[通俗易懂]

    Unity实战篇 | 教你怎样将Unity的启动Logo 设置成 自己制作的 帧动画[通俗易懂]上一篇文章我们讲了怎样在不购买专业版的情况下自定义Unity的启动Logo。那本篇文章就来介绍一下,怎样在去除默认Logo的前提下制作自己的启动动画!

  • java %08d_总结Java中String.format()的使用[通俗易懂]

    java %08d_总结Java中String.format()的使用[通俗易懂]快速入门介绍Java中强大的String.format()前言从Java5.0开始,String类新增了一个强大的字符串格式化方法format()。这个方法到现在用的人还是不多,实在是一种浪费。本文带你快速过一遍这个方法的功能,将来你要用到格式化文本的时候,可能就不需要再借用第三方类库或自己去实现了。首先看一个简单例子:Stringformatted=String.format(“…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号