源码分析Spring @Configuration注解如何巧夺天空,偷梁换柱。

news2024/11/26 15:24:49
前言

回想起五年前的一次面试,面试官问@Configuration注解和@Component注解有什么区别?记得当时的回答是:

  • 相同点:@Configuration注解继承于@Component注解,都可以用来通过ClassPathBeanDefinitionScanner装载Spring bean的配置信息。

  • 不同点:Component注解为通用组件类模式注解,Configuration注解为配置类模式注解,主要是在做代码分层上的有差别(当然也是从字面意思上理解)。

很显然不是面试官想要到答案,最后还是挂了。回去看了一下注解本身定,Configuration继承于Component,多了个proxyBeanMethods 属性,注释中提到在运行中可以生成子类进行增强,但是类类型必须不是final的,当proxyBeanMethods配置为false的时候不会进行增强。当时也就草率的下了定义,Configuration可以选择是否通过生成代理类进行增强。

进阶

多余的属性是proxyBeanMethods,字面的意思是代理Bean的方法,代理了个什么东西?是如何增强?带着问题写了一段测试代码:

@Component
public class Demo {


public static void main(String[] args) {
    AnnotationConfigApplicationContext annotationConfigApplicationContext =
            new AnnotationConfigApplicationContext();
    annotationConfigApplicationContext.register(Demo.class);
    annotationConfigApplicationContext.refresh();
    Test test = annotationConfigApplicationContext.getBean(Test.class);
    System.out.println("spring ioc容器中管理的person对象:" + annotationConfigApplicationContext.getBean(Person.class));
    for (int i = 0; i < 2; i++) {
        System.out.println("通过bean作用的方法创建对象:" + test.createUser());
    }
    for (int i = 0; i < 2; i++) {
        System.out.println("通过没用bean作用的方法创建对象:" + test.createUserNoMethodBean());
    }
    annotationConfigApplicationContext.close();
}
    @Configuration
    public static class Test {

            @Bean
            public Person createUser() {
             return createUserNoMethodBean();
            }

            public Person createUserNoMethodBean(){
                Person person = new Person();
                person.setName(UUID.randomUUID().toString());
                return person;
            }
    }

    public static class Person {

        private String name;

        //get set 省略

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}

代码输出

spring ioc容器中管理的person对象:Person{name='2a8c8844-c979-4784-942b-af4299d32866'}
通过bean作用的方法创建对象:Person{name='2a8c8844-c979-4784-942b-af4299d32866'}
通过bean作用的方法创建对象:Person{name='2a8c8844-c979-4784-942b-af4299d32866'}
通过没用bean作用的方法创建对象:Person{name='67707608-79bf-44c0-a83a-9ebaa7c17bb1'}
通过没用bean作用的方法创建对象:Person{name='27028430-9f82-4379-993a-6edd884ce145'}

我们调用Test对象创建用户方法,带有bean注解的返回的是同一个对象,并且与注入到spring ioc容器中的person对象是同一个,而且没有通过@Bean注解作用的方法真正执行了。不得不说好神奇。

猜想

我们先做一个大胆的猜想~ 注解Configuration注解proxyBeanMethods默认为true,也就是说默认会进行代理增强。调用通过bean注解的方法时会进行拦截,并且会舍弃调用真正的目标方法。其中会对带有bean的方法进行代理,对不带有bean的方法进行过滤。拦截带有bean方法返回对象时会从spring ioc容器中进行依赖查找并返回该对象。
如图流程图

  • 带有bean方法不需要进行执行目标方法,也就是我们的原始方法;
  • 我们需要注入我们的BeanFactory对象,来完成我们带有Bean方法依赖查找;
  • 需要对我们的方法进行过滤,需要对特定方法进行回调。
源码

由上一章我们熟悉了Spring ioc容器解析注册的流程,ConfigurationClassPostProcessor.class类比较重要,前半部分为BeanDefinitionRegistry逻辑,后半部为配置类的增强。
接下来开始查看对给定BeanFactoryPostProcessor的处理。跟踪源码到ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry(beanfactory);

/**
 * Prepare the Configuration classes for servicing bean requests at runtime
 * by replacing them with CGLIB-enhanced subclasses.
 */
 //通过 CGLIB 增强的子类来代替配置类来为 bean 请求提供支持
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
  int factoryId = System.identityHashCode(beanFactory);
  if (this.factoriesPostProcessed.contains(factoryId)) {
    throw new IllegalStateException(
        "postProcessBeanFactory already called on this post-processor against " + beanFactory);
  }
  this.factoriesPostProcessed.add(factoryId);
  if (!this.registriesPostProcessed.contains(factoryId)) {
    // BeanDefinitionRegistryPostProcessor hook apparently not supported...
    // Simply call processConfigurationClasses lazily at this point then.
    processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
  }

  enhanceConfigurationClasses(beanFactory);
  beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}

继续跟踪到ConfigurationClassPostProcessor#enhanceConfigurationClasses(beanFactory)方法中,


public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
		StartupStep enhanceConfigClasses = this.applicationStartup.start("spring.context.config-classes.enhance");
		Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
		for (String beanName : beanFactory.getBeanDefinitionNames()) {
			BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
			Object configClassAttr = beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE);
			AnnotationMetadata annotationMetadata = null;
			MethodMetadata methodMetadata = null;
			if (beanDef instanceof AnnotatedBeanDefinition) {
				AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDef;
				annotationMetadata = annotatedBeanDefinition.getMetadata();
				methodMetadata = annotatedBeanDefinition.getFactoryMethodMetadata();
			}
			if ((configClassAttr != null || methodMetadata != null) && beanDef instanceof AbstractBeanDefinition) {
				// Configuration class (full or lite) or a configuration-derived @Bean method
				// -> eagerly resolve bean class at this point, unless it's a 'lite' configuration
				// or component class without @Bean methods.
				AbstractBeanDefinition abd = (AbstractBeanDefinition) beanDef;
				if (!abd.hasBeanClass()) {
					boolean liteConfigurationCandidateWithoutBeanMethods =
							(ConfigurationClassUtils.CONFIGURATION_CLASS_LITE.equals(configClassAttr) &&
								annotationMetadata != null && !ConfigurationClassUtils.hasBeanMethods(annotationMetadata));
					if (!liteConfigurationCandidateWithoutBeanMethods) {
						try {
							abd.resolveBeanClass(this.beanClassLoader);
						}
						catch (Throwable ex) {
							throw new IllegalStateException(
									"Cannot load configuration class: " + beanDef.getBeanClassName(), ex);
						}
					}
				}
			}
      //判断BeanDefinition的configurationClass是否为full,然后加入集合后续进行特殊处理
			if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) {
				if (!(beanDef instanceof AbstractBeanDefinition)) {
					throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +
							beanName + "' since it is not stored in an AbstractBeanDefinition subclass");
				}
				else if (logger.isInfoEnabled() && beanFactory.containsSingleton(beanName)) {
					logger.info("Cannot enhance @Configuration bean definition '" + beanName +
							"' since its singleton instance has been created too early. The typical cause " +
							"is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +
							"return type: Consider declaring such methods as 'static'.");
				}
				configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
			}
		}
		if (configBeanDefs.isEmpty() || NativeDetector.inNativeImage()) {
			// nothing to enhance -> return immediately
			enhanceConfigClasses.end();
			return;
		}

		ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
    //遍历进行cglib增强子类
		for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
			AbstractBeanDefinition beanDef = entry.getValue();
			// If a @Configuration class gets proxied, always proxy the target class
			beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
			// Set enhanced subclass of the user-specified bean class
			Class<?> configClass = beanDef.getBeanClass();
			Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
			if (configClass != enhancedClass) {
				if (logger.isTraceEnabled()) {
					logger.trace(String.format("Replacing bean definition '%s' existing class '%s' with " +
							"enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));
				}
				beanDef.setBeanClass(enhancedClass);
			}
		}
		enhanceConfigClasses.tag("classCount", () -> String.valueOf(configBeanDefs.keySet().size())).end();

ConfigurationClassEnhancer类中,我们看到增强的具体实现:


/**
 * Creates a new CGLIB {@link Enhancer} instance.
 */
 //创建cglib的实例。
private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
  Enhancer enhancer = new Enhancer();
  //为增强类设置父类
  enhancer.setSuperclass(configSuperClass);
  //为增强类设置接口,该接口继承BeanFactoryAware,在实例化非lazy对象处理中接口回调阶段initializeBean    进行(BeanNameAware  ClassLoaderAware BeanFactoryAware 回调),我们能通过BeanFactoryAware  获取我们beanfactory类。
  enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
  enhancer.setUseFactory(false);
  //设置beanfactory字段。方便进行依赖查找
  enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
  enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
  //具体我们可以看到此处配置的filter 也就是特殊方法才会执行回调,否则调用父类目标方法
  enhancer.setCallbackFilter(CALLBACK_FILTER);
  enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
  return enhancer;
}

CALLBACK_FILTER:并不是所有方法进行拦截, 首先需要拦截的是内部调用 @Bean 注解的方法时,进行ioc依赖查找返回;其次是依赖查找依赖的beanfactory字段的赋值;其他方法不进行拦截(当然也可以拦截,直接在调用父类的方法proxy.invokeSuper(obj, args));这样多实现不如不实现,没用!!!。

因为我们多个拦截器,所以我们需要进行组合,选出符合条件的拦截器下标。

/**
	 * A {@link CallbackFilter} that works by interrogating {@link Callback Callbacks} in the order
	 * that they are defined via {@link ConditionalCallback}.
	 */
	private static class ConditionalCallbackFilter implements CallbackFilter {

		private final Callback[] callbacks;

		private final Class<?>[] callbackTypes;

    //初始化的数据为固定顺序
		public ConditionalCallbackFilter(Callback[] callbacks) {
			this.callbacks = callbacks;
			this.callbackTypes = new Class<?>[callbacks.length];
			for (int i = 0; i < callbacks.length; i++) {
				this.callbackTypes[i] = callbacks[i].getClass();
			}
		}

		@Override
		public int accept(Method method) {
      //遍历callbacks,首先判断是否符合
			for (int i = 0; i < this.callbacks.length; i++) {
				Callback callback = this.callbacks[i];
				if (!(callback instanceof ConditionalCallback) || ((ConditionalCallback) callback).isMatch(method)) {
					return i;
				}
			}
			throw new IllegalStateException("No callback available for method " + method.getName());
		}

		public Class<?>[] getCallbackTypes() {
			return this.callbackTypes;
		}
	}


  // The callbacks to use. Note that these callbacks must be stateless.
private static final Callback[] CALLBACKS = new Callback[] {
    new BeanMethodInterceptor(),
    new BeanFactoryAwareMethodInterceptor(),
    NoOp.INSTANCE
};
private static final ConditionalCallbackFilter CALLBACK_FILTER = new ConditionalCallbackFilter(CALLBACKS);

CALLBACKS 根据ConditionalCallbackFilter#accept(method)``方法逻辑,首先会判断是是继承了ConditionalCallback,然后调用isMatch(method)的方法;组合条件中 NoOp.INSTANCE非ConditionalCallback子类与BeanMethodInterceptor和BeanFactoryAwareMethodInterceptor互斥;BeanMethodInterceptorBeanFactoryAwareMethodInterceptor`互斥。

BeanMethodInterceptor#isMatch(method);源码

@Override
public boolean isMatch(Method candidateMethod) {
  return (candidateMethod.getDeclaringClass() != Object.class &&
      !BeanFactoryAwareMethodInterceptor.isSetBeanFactory(candidateMethod) &&
      BeanAnnotationHelper.isBeanAnnotated(candidateMethod));
}

接下来创建我们目标类的子类,注册子类的回调:


/**
*为派生子类的设置回调。
*/
private Class<?> createClass(Enhancer enhancer) {
  Class<?> subclass = enhancer.createClass();
  // Registering callbacks statically (as opposed to thread-local)
  // is critical for usage in an OSGi environment (SPR-5932)...
  //CALLBACKS为
  Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
  return subclass;
}

CALLBACKS 为数组,

  • BeanMethodInterceptor:对于配置类中内部 @Bean 注解的方法的调用将会被拦截器拦截。拦截器的逻辑是判断声明的 Spring bean 在容器中是否已经存在,如果存在则直接返回容器中的 Spring bean。否则真正的配置类的方法创建 Spring bean 实例,避免了多例的出现。主要是解析获取我们beanfactory字段(该字段受益于CALLBACKS第二个元素的赋值),进行依赖查找。
private ConfigurableBeanFactory getBeanFactory(Object enhancedConfigInstance) {
  //解析BEAN_FACTORY_FIELD beanfactory字段~
  Field field = ReflectionUtils.findField(enhancedConfigInstance.getClass(), BEAN_FACTORY_FIELD);
  Assert.state(field != null, "Unable to find generated bean factory field");
  Object beanFactory = ReflectionUtils.getField(field, enhancedConfigInstance);
  Assert.state(beanFactory != null, "BeanFactory has not been injected into @Configuration class");
  Assert.state(beanFactory instanceof ConfigurableBeanFactory,
      "Injected BeanFactory is not a ConfigurableBeanFactory");
  return (ConfigurableBeanFactory) beanFactory;
}
  • BeanFactoryAwareMethodInterceptor:为我们代理类新增的BEAN_FACTORY_FIELD字段进行赋值。源码如下:
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
  //查找我们的BEAN_FACTORY_FIELD字段
  Field field = ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD);
  Assert.state(field != null, "Unable to find generated BeanFactory field");
  //为我们字段进行赋值,args[0]->因为我们BeanFactoryAware接口回调方法为 void setBeanFactory(BeanFactory beanFactory) throws BeansException;只有一个参数~
  field.set(obj, args[0]);

  // Does the actual (non-CGLIB) superclass implement BeanFactoryAware?
  // If so, call its setBeanFactory() method. If not, just exit.
  //如果父类实现了BeanFactoryAware接口,改方法直接调用父类方法。
  if (BeanFactoryAware.class.isAssignableFrom(ClassUtils.getUserClass(obj.getClass().getSuperclass()))) {
    return proxy.invokeSuper(obj, args);
  }
  return null;
}
  • NoOp.INSTANCE:这个NoOp表示no operator,即什么操作也不做,代理类直接调用被代理的方法不进行拦截。
    配置中当然满足内部bean方法调用时走BeanMethodInterceptor;调用setBeanFactory(BeanFactory beanFactory)时走BeanFactoryAwareMethodInterceptor;其他方法走NoOp.INSTANCE。
    回到我们ConfigurationClassPostProcessor#enhanceConfigurationClasses(beanFactory)方法中,可以看到BeanDefinition的beanClass属性被赋值我们生成增强的代理子类,
    最后来到我们bean的实例化处理方法DefaultListableBeanFactory#preInstantiateSingletons。至此@Configuration的实现原理和我们猜想大致相同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/402988.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

IT服务发布管理过程文件

目的 规范发布管理的提交、审批、沟通、测试、回滚、实施等活动。 范围 适用于我公司的IT服务管理重大变更的内、外部发布。 术语定义 发布&#xff1a;将一个或多个变更交付、分发到实际运行环境中并可对其进行追溯。非项目的IT服务重大变更需要遵循本过程进行发布。发布主要包…

论文投稿指南——中文核心期刊推荐(工业经济)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

使用TorchGeo进行地理空间深度学习

前言TorchGeo是一个PyTorch域库&#xff0c;提供特定于地理空间数据的数据集、采样器、转换和预训练模型。https://github.com/microsoft/torchgeo几十年来&#xff0c;地球观测卫星、飞机和最近的无人机平台一直在收集越来越多的地球表面图像。有了关于季节和长期趋势的信息&a…

【算法】算法基础入门详解:轻松理解和运用基础算法

&#x1f600;大家好&#xff0c;我是白晨&#xff0c;一个不是很能熬夜&#x1f62b;&#xff0c;但是也想日更的人✈。如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下&#x1f440;白晨吧&#xff01;你的支持就是我最大的动力&#xff01;&#x1f4…

查找Pycharm跑代码下载模型存放位置以及有关模型下载小技巧(model_name_or_path参数)

目录一、前言二、发现问题三、删除这些模型方法一&#xff1a;直接删除注意方法二&#xff1a;代码删除一、前言 当服务器连不上&#xff0c;只能在本地跑代码时需要使用***预训练语言模型进行处理 免不了需要把模型下载到本地 时间一长就会发现C盘容量不够 二、发现问题 正…

c++11 标准模板(STL)(std::unordered_map)(九)

定义于头文件 <unordered_map> template< class Key, class T, class Hash std::hash<Key>, class KeyEqual std::equal_to<Key>, class Allocator std::allocator< std::pair<const Key, T> > > class unordered…

Python进阶-----高阶函数zip() 函数

目录 前言&#xff1a; zip() 函数简介 运作过程&#xff1a; 应用实例 1.有序序列结合 2.无序序列结合 3.长度不统一的情况 前言&#xff1a; 家人们&#xff0c;看到标题应该都不陌生了吧&#xff0c;我们都知道压缩包文件的后缀就是zip的&#xff0c;当然还有r…

Mybatis源码分析系列之第四篇:Mybatis中代理设计模型源码详解

一&#xff1a; 前言 我们尝试在前几篇文章的内容中串联起来&#xff0c;防止各位不知所云。 1&#xff1a;背景 我们基于Mybatis作为后台Orm框架进行编码的时候&#xff0c;有两种方式。 //编码方式1 UserDao userDao sqlSession.getMapper(UserDao.class); userDao.quer…

[入门必看]数据结构1.1:数据结构的基本概念

[入门必看]数据结构1.1&#xff1a;数据结构的基本概念第一章 绪论1.1 数据结构的基本概念知识总览1.1.1 基本概念和术语数据类型、抽象数据类型&#xff1a;1.1.2 数据结构的三要素数据的逻辑结构数据的物理结构&#xff08;存储结构&#xff09;数据的运算知识回顾与重要考点…

【数据库概论】第十一章 数据库并发控制

第十一章 并发控制 在多处理机系统中&#xff0c;每个处理机可以运行一个事务&#xff0c;多个处理机可以同时运行多个事务&#xff0c;实现多个事务并行运行&#xff0c;这就是同时并发方式。当多个用户并发存取数据库时会产生多个事务同时存取同一事务的情况&#xff0c;如果…

ESP32设备驱动-红外寻迹传感器驱动

红外寻迹传感器驱动 1、红外寻迹传感器介绍 红外寻迹传感器具有一对红外线发射管与接收管,发射管发射出一定频率的红外线,当检测方向遇到障碍物(反射面)时,红外线反射回来被接收管接收,经过比较器电路处理之后,输出接口会输出一个数字信号(低电平或高电平,取决于电路…

Nginx配置实例-反向代理案例二

实现效果&#xff1a;使用nginx反向代理&#xff0c;根据访问的路径跳转到不同端口服务 nginx监听端口为9000&#xff0c; 访问 http://127.0.0.1:9000/edu/ 直接跳转到127.0.0.1:8080 访问 http://127.0.0.1:9000/vod/ 直接跳转到127.0.0.1:8081 一、准备工作 1. 准备两个tom…

TCP相关概念

目录 一.滑动窗口 1.1概念 1.2滑动窗口存在的意义 1.3 滑动窗口的大小变化 1.4丢包问题 二.拥塞控制 三.延迟应答 四.捎带应答 五.面向字节流 六.粘包问题 七.TIME_WAIT状态 八.listen第2个参数 九.TCP总结 一.滑动窗口 1.1概念 概念&#xff1a;双方在进行通信时&a…

2023年软考高级-系统分析师考试学习指导计划!

2023年软考高级-系统分析师考试学习指导计划&#xff01; 一、导学阶段 第一天 考试情况及备考攻略&#xff1a;https://www.educity.cn/xuanke/rk/rjsp/?sywzggw 考试介绍&#xff1a;https://www.educity.cn/wangxiao2/c410653/sp110450.html 考试经验分享&#xff1a;h…

如何在Linux中优雅的使用 head 命令,用来看日志简直溜的不行

当您在 Linux 的命令行上工作时&#xff0c;有时希望快速查看文件的第一行&#xff0c;例如&#xff0c;有个日志文件不断更新&#xff0c;希望每次都查看日志文件的前 10 行。很多朋友使用文本编辑的命令是vim&#xff0c;但还有个命令head也可以让轻松查看文件的第一行。 在…

记录使用chatgpt的复杂经历

背景 由于最近要写论文&#xff0c;c站的gpt也变样了&#xff0c;无奈之下和同学借了一个gpt账号&#xff0c;才想起没有npv&#xff0c;不好意思去要&#xff0c;也不想买&#xff0c;于是我找了很多临时免费的直到我看到有一家&#xff0c;邀请10人即可&#xff0c;而且只用…

江苏专转本 专科生的最好出入

专转本 专科生的最好出入(1) 减少每天刷某音&#xff0c;刷朋友圈的时间无所事事、捧着手机刷的不是某音就是朋友圈&#xff0c;三年下来没去成一次图书馆。碎片化信息很大程度只是在浪费时间&#xff0c;不如每天比同龄人至少多出1小时时间&#xff0c;用来看书、护肤、健身。…

磨金石教育摄影技能干货分享|高邮湖上观花海

江苏高邮&#xff0c;说到这里所有人能想到的&#xff0c;就是那烟波浩渺的高邮湖。高邮在旅游方面并不出名&#xff0c;但是这里的自然人文景观绝对不输于其他地方。高邮不止有浩瀚的湖泊&#xff0c;春天的油菜花海同样壮观。春日的午后&#xff0c;与家人相约游玩&#xff0…

C语言--字符串函数1

目录前言strlenstrlen的模拟实现strcpystrcatstrcat的模拟实现strcmpstrcmp的模拟实现strncpystrncatstrncmpstrstrstrchr和strrchrstrstr的模拟实现前言 本章我们将重点介绍处理字符和字符串的库函数的使用和注意事项。 strlen 我们先来看一个我们最熟悉的求字符串长度的库…

【Echart多场景示例应用】Echarts柱状图、折线图、饼图、雷达图等完整示例。 echarts主标题和副标题的位置、样式等设置(已解决附源码)

**【写在前面】**前端时间做一个echarts的页面调整&#xff0c;临时客户要求加一个参数&#xff08;总容量&#xff09;显示&#xff0c;当时我就想用个默认的副标题吧&#xff0c;哪知客户和我说得紧跟在主标题后面&#xff0c;于是乎我就根据设置做了一个调整&#xff0c;我也…