6. 抹平差异,统一类型转换服务ConversionService
分享、成长,拒绝浅藏辄止。关注公众号【BAT的乌托邦】,回复关键字
专栏
有Spring技术栈、中间件等小而美的原创专栏供以免费学习。本文已被 https://www.yourbatman.cn 收录。
✍前言
你好,我是YourBatman。
通过前两篇文章的介绍已经非常熟悉Spirng 3.0全新一代的类型转换机制了,它提供的三种类型转换器(Converter、ConverterFactory、GenericConverter),分别可处理1:1、1:N、N:N的类型转换。按照Spring的设计习惯,必有一个注册中心来统一管理,负责它们的注册、删除等,它就是ConverterRegistry
。
对于
ConverterRegistry
在文首多说一句:我翻阅了很多博客文章介绍它时几乎无一例外的提到有查找的功能,但实际上是没有的。Spring设计此API接口并没有暴露其查找功能,选择把最为复杂的查找匹配逻辑私有化,目的是让开发者使可无需关心,细节之处充分体现了Spring团队API设计的卓越能力。
另外,内建的绝大多数转换器访问权限都是default/private,那么如何使用它们,以及屏蔽各种转换器的差异化呢?为此,Spring提供了一个统一类型转换服务,它就是ConversionService
。
版本约定
- Spring Framework:5.3.1
- Spring Boot:2.4.0
✍正文
ConverterRegistry和ConversionService的关系密不可分,前者为后者提供转换器管理支撑,后者面向使用者提供服务。本文涉及到的接口/类有:
ConverterRegistry
:转换器注册中心。负责转换器的注册、删除ConversionService
:统一的类型转换服务。属于面向开发者使用的门面接口ConfigurableConversionService
:上两个接口的组合接口GenericConversionService
:上个接口的实现,实现了注册管理、转换服务的几乎所有功能,是个实现类而非抽象类DefaultConversionService
:继承自GenericConversionService
,在其基础上注册了一批默认转换器(Spring内建),从而具备基础转换能力,能解决日常绝大部分场景
ConverterRegistry
Spring 3.0引入的转换器注册中心,用于管理新一套的转换器们。
public interface ConverterRegistry {
void addConverter(Converter<?, ?> converter);
<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
void addConverter(GenericConverter converter);
void addConverterFactory(ConverterFactory<?, ?> factory);
// 唯一移除方法:按照转换pair对来移除
void removeConvertible(Class<?> sourceType, Class<?> targetType);
}
它的继承树如下:
ConverterRegistry有子接口FormatterRegistry,它属于格式化器的范畴,故不放在本文讨论。但仍旧属于本系列专题内容,会在接下来的几篇内容里介入,敬请关注。
ConversionService
面向使用者的统一类型转换服务。换句话说:站在使用层面,你只需要知道ConversionService
接口API的使用方式即可,并不需要关心其内部实现机制,可谓对使用者非常友好。
public interface ConversionService {
boolean canConvert(Class<?> sourceType, Class<?> targetType);
boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
<T> T convert(Object source, Class<T> targetType);
Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
它的继承树如下:
可以看到ConversionService和ConverterRegistry的继承树殊途同归,都直接指向了ConfigurableConversionService
这个分支,下面就对它进行介绍。
ConfigurableConversionService
ConversionService
和ConverterRegistry
的组合接口,自己并未新增任何接口方法。
public interface ConfigurableConversionService extends ConversionService, ConverterRegistry {
}
它的继承树可参考上图。接下来就来到此接口的直接实现类GenericConversionService。
GenericConversionService
对ConfigurableConversionService
接口提供了完整实现的实现类。换句话说:ConversionService和ConverterRegistry接口的功能均通过此类得到了实现,所以它是本文重点。
该类很有些值得学习的地方,可以细品,在我们自己设计程序时加以借鉴。
public class GenericConversionService implements ConfigurableConversionService {
private final Converters converters = new Converters();
private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<ConverterCacheKey, GenericConverter>(64);
}
它用两个成员变量来管理转换器们,其中converterCache是缓存用于加速查找,因此更为重要的便是Converters喽。
Converters是GenericConversionService
的内部类,用于管理(添加、删除、查找)转换器们。也就说对ConverterRegistry
接口的实现最终是委托给它去完成的,它是整个转换服务正常work的内核,下面我们对它展开详细叙述。
1、内部类Converters
它管理所有转换器,包括添加、删除、查找。
GenericConversionService:
// 内部类
private static class Converters {
private final Set<GenericConverter> globalConverters = new LinkedHashSet<GenericConverter>();
private final Map<ConvertiblePair, ConvertersForPair> converters = new LinkedHashMap<ConvertiblePair, ConvertersForPair>(36);
}
说明:这里使用的集合/Map均为
LinkedHashXXX
,都是有序的(存入顺序和遍历取出顺序保持一致)
用这两个集合/Map存储着注册进来的转换器们,他们的作用分别是:
globalConverters
:存取通用的转换器,并不限定转换类型,一般用于兜底converters
:指定了类型对,对应的转换器们的映射关系。- ConvertiblePair:表示一对,包含sourceType和targetType
- ConvertersForPair:这一对对应的转换器们(因为能处理一对的可能存在多个转换器),内部使用一个双端队列Deque来存储,保证顺序
- 小细节:Spring 5之前使用LinkedList,之后使用Deque(实际为ArrayDeque)存储
final class ConvertiblePair {
private final Class<?> sourceType;
private final Class<?> targetType;
}
private static class ConvertersForPair {
private final Deque<GenericConverter> converters = new ArrayDeque<>(1);
}
添加add
public void add(GenericConverter converter) {
Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
if (convertibleTypes == null) {
... // 放进globalConverters里
} else {
... // 放进converters里(若支持多组pair就放多个key)
}
}
在此之前需要了解个前提:对于三种转换器Converter、ConverterFactory、GenericConverter
在添加到Converters之前都统一被适配为了GenericConverter
,这样做的目的是方便统一管理。对应的两个适配器是ConverterAdapter和ConverterFactoryAdapter,它俩都是ConditionalGenericConverter的内部类。
添加的逻辑被我用伪代码简化后其实非常简单,无非就是一个非此即彼的关系而已:
- 若转换器没有指定处理的类型对,就放进全局转换器列表里,用于兜底
- 若转换器有指定处理的类型对(可能还是多个),就放进converters里,后面查找时使用
删除remove
public void remove(Class<?> sourceType, Class<?> targetType) {
this.converters.remove(new ConvertiblePair(sourceType, targetType));
}
移除逻辑非常非常的简单,这得益于添加时候做了统一适配的抽象。
查找find
@Nullable
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
// 找到该类型的类层次接口(父类 + 接口),注意:结果是有序列表
List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
// 双重遍历
for (Class<?> sourceCandidate : sourceCandidates) {
for (Class<?> targetCandidate : targetCandidates) {
ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
... // 从converters、globalConverters里匹配到一个合适转换器后立马返回
}
}
return null;
}
查找逻辑也并不复杂,有两个关键点需要关注:
getClassHierarchy(class)
:获取该类型的类层次(父类 + 接口),注意:结果List是有序的List- 也就是说转换器支持的类型若是父类/接口,那么也能够处理器子类
- 根据convertiblePair匹配转换器:优先匹配专用的converters,然后才是globalConverters。若都没匹配上返回null
2、管理转换器(ConverterRegistry)
了解了Converters
之后再来看GenericConversionService
是如何管理转换器,就如鱼得水,一目了然了。
添加
为了方便使用者调用,ConverterRegistry接口提供了三个添加方法,这里一一给与实现。
说明:暴露给调用者使用的API接口使用起来应尽量的方便,重载多个是个有效途径。内部做适配、归口即可,用户至上
@Override
public void addConverter(Converter<?, ?> converter) {
// 获取泛型类型 -> 转为ConvertiblePair
ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
...
// converter适配为GenericConverter添加
addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
}
@Override
public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
addConverter(new ConverterAdapter(converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType)));
}
@Override
public void addConverter(GenericConverter converter) {
this.converters.add(converter);
invalidateCache();
}
前两个方法都会调用到第三个方法上,每调用一次addConverter()
方法都会清空缓存,也就是converterCache.clear()
。所以动态添加转换器对性能是有损的,因此使用时候需稍加注意一些。
查找
ConverterRegistry接口并未直接提供查找方法,而只是在实现类内部做了实现。提供一个钩子方法用于查找给定sourceType/targetType对的转换器。
@Nullable
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
// 1、查缓存
GenericConverter converter = this.converterCache.get(key);
if (converter != null) {
... // 返回结果
}
// 2、去converters里查找
converter = this.converters.find(sourceType, targetType);
if (converter == null) {
// 若还没有匹配的,就返回默认结果
// 默认结果是NoOpConverter -> 什么都不做
converter = getDefaultConverter(sourceType, targetType);
}
... // 把结果装进缓存converterCache里
return null;
}
有了对Converters查找逻辑的分析,这个步骤就很简单了。绘制成图如下:
3、转换功能(ConversionService)
上半部分介绍完GenericConversionService
对转换器管理部分的实现(对ConverterRegistry接口的实现),接下来就看看它是如何实现转换功能的(对ConversionService接口的实现)。
判断
@Override
public boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType) {
return canConvert((sourceType != null ? TypeDescriptor.valueOf(sourceType) : null), TypeDescriptor.valueOf(targetType));
}
@Override
public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
if (sourceType == null) {
return true;
}
// 查找/匹配对应的转换器
GenericConverter converter = getConverter(sourceType, targetType);
return (converter != null);
}
能否执行转换判断的唯一标准:能否匹配到可用于转换的转换器。而这个查找匹配逻辑,稍稍抬头往上就能看到。
转换
@Override
@SuppressWarnings("unchecked")
@Nullable
public <T> T convert(@Nullable Object source, Class<T> targetType) {
return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType));
}
@Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
if (sourceType == null) {
return handleResult(null, targetType, convertNullSource(null, targetType));
}
// 校验:source必须是sourceType的实例
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" + sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
// ============拿到转换器,执行转换============
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
// 若没进行canConvert的判断直接调动,可能出现此种状况:一般抛出ConverterNotFoundException异常
return handleConverterNotFound(source, sourceType, targetType);
}
同样的,执行转换的逻辑很简单,非常好理解的两个步骤:
- 查找匹配到一个合适的转换器(查找匹配的逻辑同上)
- 拿到此转换器执行转换
converter.convert(...)
说明:其余代码均为一些判断、校验、容错,并非核心,本文给与适当忽略。
GenericConversionService实现了转换器管理、转换服务的所有功能,是可以直接面向开发者使用的。但是开发者使用时可能并不知道需要注册哪些转换器来保证程序正常运转,Spring并不能要求开发者知晓其内建实现。基于此,Spring在3.1又提供了一个默认实现DefaultConversionService,它对使用者更友好。
DefaultConversionService
Spirng容器默认使用的转换服务实现,继承自GenericConversionService
,在其基础行只做了一件事:构造时添加内建的默认转换器们。从而天然具备有了基本的类型转换能力,适用于不同的环境。如:xml解析、@Value解析、http协议参数自动转换等等。
小细节:它并非Spring 3.0就有,而是Spring 3.1新推出的API
// @since 3.1
public class DefaultConversionService extends GenericConversionService {
// 唯一构造器
public DefaultConversionService() {
addDefaultConverters(this);
}
}
本类核心代码就这一个构造器,构造器内就这一句代码:addDefaultConverters(this)
。接下来需要关注Spring默认情况下给我们“安装”了哪些转换器呢?也就是了解下addDefaultConverters(this)
这个静态方法
默认注册的转换器们
// public的静态方法,注意是public的访问权限
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
addScalarConverters(converterRegistry);
addCollectionConverters(converterRegistry);
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}
该静态方法用于注册全局的、默认的转换器们,从而让Spring有了基础的转换能力,进而完成绝大部分转换工作。为了方便记忆这个注册流程,我把它绘制成图供以你保存:
特别强调:转换器的注册顺序非常重要,这决定了通用转换器的匹配结果(谁在前,优先匹配谁,first win)。
针对这幅图,你可能还会有如下疑问:
- JSR310转换器只看到TimeZone、ZoneId等转换,怎么没看见更为常用的LocalDate、LocalDateTime等这些类型转换呢?难道Spring默认是不支持的?
- 答:当然不是。 这么常见的场景Spring怎能会不支持呢?不过与其说这是类型转换,倒不如说是格式化更合适。所以放在该系列后几篇关于格式化章节中再做讲述
- 一般的Converter都见名之意,但StreamConverter有何作用呢?什么场景下会生效
- 答:上文已讲述
- 对于兜底的转换器,有何含义?这种极具通用性的转换器作用为何
- 答:上文已讲述
最后,需要特别强调的是:它是一个静态方法,并且还是public的访问权限,且不仅仅只有本类调用。实际上,DefaultConversionService
仅仅只做了这一件事,所以任何地方只要调用了该静态方法都能达到前者相同的效果,使用上可谓给与了较大的灵活性。比如Spring Boot环境下不是使用DefaultConversionService
而是ApplicationConversionService
,后者是对FormattingConversionService扩展,这个话题放在后面详解。
Spring Boot在web环境默认向容易注册了一个WebConversionService,因此你有需要可直接@Autowired使用
ConversionServiceFactoryBean
顾名思义,它是用于产生ConversionService
类型转换服务的工厂Bean,为了方便和Spring容器整合而使用。
public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean {
@Nullable
private Set<?> converters;
@Nullable
private GenericConversionService conversionService;
public void setConverters(Set<?> converters) {
this.converters = converters;
}
@Override
public void afterPropertiesSet() {
// 使用的是默认实现哦
this.conversionService = new DefaultConversionService();
ConversionServiceFactory.registerConverters(this.converters, this.conversionService);
}
@Override
@Nullable
public ConversionService getObject() {
return this.conversionService;
}
...
}
这里只有两个信息量需要关注:
- 使用的是DefaultConversionService,因此那一大串的内建转换器们都会被添加进来的
- 自定义转换器可以通过
setConverters()
方法添加进来- 值得注意的是方法入参是
Set<?>
并没有明确泛型类型,因此那三种转换器(1:1/1:N/N:N)你是都可以添加.
- 值得注意的是方法入参是
✍总结
通读本文过后,相信能够给与你这个感觉:曾经望而却步的Spring类型转换服务ConversionService
,其实也不过如此嘛。通篇我用了多个简单字眼来说明,因为拆开之后,无一高复杂度知识点。
迎难而上是积攒涨薪底气和勇气的途径,况且某些知识点其实并不难,所以我觉得从性价比角度来看这类内容是非常划算的,你pick到了麽?
正所谓类型转换和格式化属于两组近义词,在Spring体系中也经常交织在一起使用,有种傻傻分不清楚之感。从下篇文章起进入到本系列关于Formatter格式化器知识的梳理,什么日期格式化、@DateTimeFormat、@NumberFormat都将帮你捋清楚喽,有兴趣者可保持持续关注。
推荐阅读
【Spring类型转换】系列:
- 1. 揭秘Spring类型转换 - 框架设计的基石
- 2. Spring早期类型转换,基于PropertyEditor实现
- 3. 搞定收工,PropertyEditor就到这
- 4. 上新了Spring,全新一代类型转换机制
- 5. 穿过拥挤的人潮,Spring已为你制作好高级赛道
【Jackson】系列:
- 1. 初识Jackson -- 世界上最好的JSON库
- 2. 妈呀,Jackson原来是这样写JSON的
- 3. 懂了这些,方敢在简历上说会用Jackson写JSON
- 4. JSON字符串是如何被解析的?JsonParser了解一下
- 5. JsonFactory工厂而已,还蛮有料,这是我没想到的
- 6. 二十不惑,ObjectMapper使用也不再迷惑
- 7. Jackson用树模型处理JSON是必备技能,不信你看
【数据校验Bean Validation】系列:
- 1. 不吹不擂,第一篇就能提升你对Bean Validation数据校验的认知
- 2. Bean Validation声明式校验方法的参数、返回值
- 3. 站在使用层面,Bean Validation这些标准接口你需要烂熟于胸
- 4. Validator校验器的五大核心组件,一个都不能少
- 5. Bean Validation声明式验证四大级别:字段、属性、容器元素、类
- 6. 自定义容器类型元素验证,类级别验证(多字段联合验证)
【新特性】系列:
- IntelliJ IDEA 2020.3正式发布,年度最后一个版本很讲武德
- IntelliJ IDEA 2020.2正式发布,诸多亮点总有几款能助你提效
- IntelliJ IDEA 2020.1正式发布,你要的Almost都在这!
- Spring Framework 5.3.0正式发布,在云原生路上继续发力
- Spring Boot 2.4.0正式发布,全新的配置文件加载机制(不向下兼容)
- Spring改变版本号命名规则:此举对非英语国家很友好
- JDK15正式发布,划时代的ZGC同时宣布转正
【程序人生】系列:
还有诸如【Spring配置类】【Spring-static】【Spring数据绑定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原创专栏,关注BAT的乌托邦
回复专栏
二字即可全部获取,也可加我fsx1056342982
,交个朋友。
有些已完结,有些连载中。我是A哥(YourBatman),咱们下期见
6. 抹平差异,统一类型转换服务ConversionService的更多相关文章
- 奔跑吧,OpenStack现场分享:超融合架构如何抹平物理硬件差异?
转自:https://www.ustack.com/blog/moping/ “通过引入OpenStack这一中间层,实现了云平台统一的管理调度支配向上交付,解决了业务的灵活性问题.但是在抹平下层物理 ...
- AMQ学习笔记 - 15. 实践方案:基于ActiveMQ的统一日志服务
概述 以ActiveMQ + Log4j + Spring的技术组合,实现基于消息队列的统一日志服务. 参考:Spring+Log4j+ActiveMQ实现远程记录日志——实战+分析 与参考文章的比较 ...
- 基于ActiveMQ的统一日志服务
概述 以ActiveMQ + Log4j + Spring的技术组合,实现基于消息队列的统一日志服务. 参考:Spring+Log4j+ActiveMQ实现远程记录日志——实战+分析 与参考文章的比较 ...
- CoSky-Mirror 就像一个镜子放在 Nacos、CoSky 中间,构建一个统一的服务发现平台
CoSky 基于 Redis 的服务治理平台(服务注册/发现 & 配置中心) Consul + Sky = CoSky CoSky 是一个轻量级.低成本的服务注册.服务发现. 配置服务 SDK ...
- 【SpringCloud构建微服务系列】使用Spring Cloud Config统一管理服务配置
一.为什么要统一管理微服务配置 对于传统的单体应用而言,常使用配置文件来管理所有配置,比如SpringBoot的application.yml文件,但是在微服务架构中全部手动修改的话很麻烦而且不易维护 ...
- 2 weekend110的zookeeper的原理、特性、数据模型、节点、角色、顺序号、读写机制、保证、API接口、ACL、选举、 + 应用场景:统一命名服务、配置管理、集群管理、共享锁、队列管理
在hadoop生态圈里,很多地方都需zookeeper. 启动的时候,都是普通的server,但在启动过程中,通过一个特定的选举机制,选出一个leader. 只运行在一台服务器上,适合测试环境:Zoo ...
- 统一ID服务
代码已经修改 调用方式 为restful请求 或者 feign请求 请参考 wiki: http://192.168.120.46:8090/display/peixun/akucun+Gui ...
- 在centos7.6上部署前后端分离项目Nginx反向代理vue.js2.6+Tornado5.1.1,使用supervisor统一管理服务
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_102 这一次使用vue.js+tornado的组合来部署前后端分离的web项目,vue.js不用说了,前端当红炸子鸡,泛用性非常广 ...
- Spring Cloud 2020.0.0正式发布,再见了Netflix
目录 ✍前言 版本约定 ✍正文 Spring Cloud版本管理 与Spring Boot版本对应关系 当前支持的版本 阻断式升级(不向下兼容) 1.再见了,Netflix Netflix组件替代方案 ...
随机推荐
- ④SpringCloud 实战:引入Hystrix组件,分布式系统容错
这是SpringCloud实战系列中第4篇文章,了解前面第两篇文章更有助于更好理解本文内容: ①SpringCloud 实战:引入Eureka组件,完善服务治理 ②SpringCloud 实战:引入F ...
- 【GDKOI2014】JZOJ2020年8月13日提高组T4 内存分配
[GDKOI2014]JZOJ2020年8月13日提高组T4 内存分配 题目 Description Input Output 输出m行,每行一个整数,代表输入中每次程序变化后系统所需要的空闲内存单位 ...
- moviepy音视频剪辑:视频半自动追踪人脸打马赛克
一.引言 在<moviepy1.03音视频剪辑:使用manual_tracking和headblur实现追踪人脸打马赛克>介绍了使用手动跟踪跟踪人脸移动轨迹和使用headblur对人脸进行 ...
- PyQt(Python+Qt)学习随笔:窗口layout布局的SizeConstraint和部件的大小约束策略sizePolicy不起作用的问题
专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 在写一个测试代码时,发现无论怎么设置窗口以及子部件 ...
- PyQt(Python+Qt)学习随笔:QTabWidget部件信号简介
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 QTabWidget自身提供的信号包括如下: currentChanged(int index):每 ...
- [BJDCTF 2nd]duangShell 反弹shell
[BJDCTF 2nd]duangShell [BJDCTF 2nd]duangShell 点击进去之后提示我们swp源代码泄露,访问http://xxx/.index.php.swp下载该文件 ...
- 索引优化之Explain 及慢查询日志
索引:本质是数据结构,简单理解为:排好序的快速查找数据结构,以索引文件的形式存储在磁盘中.目的:提高数据查询的效率,优化查询性能,就像书的目录一样.优势:提高检索效率,降低IO成本:排好序的表,降低C ...
- 揭秘 VMAF 视频质量评测标准
作者:杨洋,阿里云技术专家,从事直播相关媒体处理引擎开发 背景 图像质量的衡量是个老问题,对此人们提出过很多简单可行的解决方案.例如均方误差(Mean-squared-error,MSE).峰值信噪比 ...
- Libp2p 简介
这是一个翻译的系列文章,原文参考:Introduction :: libp2p Documentation 欢迎来阅读libp2p相关文档,不论你是刚开始学习如何用libp2p来搭建P2P系统, 还是 ...
- H5相关知识点整理
01-HTML5基础 了解HTML5 ☞HTML5属于上一代HTML的新迭代语言,设计HTML5最主要的目的是为了在移动设备上支持多媒体!!! 例如: video 标签和 audio 及 canvas ...