SpringMVC系列-5 消息转换器

背景

SpringMVC系列的第五篇介绍消息转换器,本文讨论的消息转换指代调用Controller接口后,对结果进行转换处理的过程。
内容包括介绍自定义消息转换器、SpringMVC常见的消息转换器、Spring消息转换器工作原理等三部分。

本文以 SpringMVC系列-2 HTTP请求调用链 和 SpringMVC系列-4 参数解析器 为基础,对相同内容不再重述。

1.自定义消息转换器

自定义消息转换器,需要实现HttpMessageConverter接口,该接口定义如下:

public interface HttpMessageConverter<T> {
	boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

	void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;

	List<MediaType> getSupportedMediaTypes();
	
	// ⚠️:read相关逻辑不是本文关注的部分
	boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
	T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
}

有三个比较重要的方法:
(1) getSupportedMediaTypes方法返回该解析器支持的MIME媒体类型;
(2) canWrite方法判断该解析器能否将目标类型的对象转化为指定的MIME媒体类型;
(3) write方法将目标对象转化为mediaType的二进制流并写入到outputMessage流对象中。

自定义消息转换器:

public class UserInfoHttpMessageConverter implements HttpMessageConverter<UserInfo> {
    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        return clazz == UserInfo.class;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return Collections.singletonList(MediaType.APPLICATION_JSON);
    }

    @Override
    public void write(UserInfo userInfo, MediaType contentType, HttpOutputMessage outputMessage)
        throws IOException, HttpMessageNotWritableException {
        String result = userInfo.getId() + "_" + userInfo.getName() + "_" + LocalDateTime.now();
        outputMessage.getBody().write(result.getBytes(StandardCharsets.UTF_8));
    }
    //...read Ignore
}

该自定义转换器表示可以将UserInfo类型的消息以"application/json"媒体格式写出。

将自定义的消息转换器注册到SpringMVC:

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new UserInfoHttpMessageConverter());
    }
}

注意:这里通过SpringMVC的配置类WebMvcConfigurer进行注册,注册原理在本文第三章中说明。

用例涉及的Controller接口和基础类:

@RestController
@RequestMapping("/api/user")
public class UserInfoController {
    @GetMapping("/query")
    public UserInfo query() {
        return new UserInfo().setName("test_sy").setId(28);
    }
}

@Data
@Accessors(chain = true)
public class UserInfo {
    private Integer id;

    private String name;
}

使用postman调用结果如下所示:
在这里插入图片描述

2.消息转换器

SpringBoot版本为2.3.2.RELEASE

2.1 框架内置的消息解析器

框架内置的消息解析器支持的MIME类型分布如下所示:
ByteArrayHttpMessageConverter:用于处理字节数组(byte array)的转换。

ByteArrayHttpMessageConverter
    application/octet-stream
    */*

StringHttpMessageConverter:用于处理字符串的转换。

StringHttpMessageConverter
    text/plain
    */*
   
StringHttpMessageConverter
    text/plain
    */*

ResourceHttpMessageConverter:用于处理Spring Resource的实现类的转换。Spring Resource是一个抽象类,它封装了对各种资源(如文件、数据库连接等)的操作。

ResourceHttpMessageConverter
    */*

ResourceRegionHttpMessageConverter:这个类是ResourceHttpMessageConverter的子类,它用于处理Resource的某个特定区域(如文件的某个部分)。

ResourceRegionHttpMessageConverter
    */*

SourceHttpMessageConverter:用于处理javax.xml.transform.Source的转换。javax.xml.transform.Source是用于XML转换的接口。

SourceHttpMessageConverter
    application/xml   
    text/xml 
    application/*+xml

AllEncompassingFormHttpMessageConverter:用于处理表单提交请求,能解析复杂的form表单,包括文件上传等。

AllEncompassingFormHttpMessageConverter
    application/x-www-form-urlencoded
    multipart/form-data
    multipart/mixed

MappingJackson2HttpMessageConverter:用于处理JSON序列化和反序列化。

MappingJackson2HttpMessageConverter
    application/json   
    application/*+json

MappingJackson2HttpMessageConverter
    application/json
    application/*+json

Jaxb2RootElementHttpMessageConverter:这个类使用JAXB(Java Architecture for XML Binding)进行XML序列化和反序列化。

Jaxb2RootElementHttpMessageConverter
    application/xml
    text/xml
    application/*+xml

上述内置转换器中包括2个StringHttpMessageConverter和2个MappingJackson2HttpMessageConverter。转换器的顺序决定了其优先级,因此第二个StringHttpMessageConverter和MappingJackson2HttpMessageConverter处于失效状态:
[1] HttpMessageConvertersAutoConfiguration自动装配类引入的StringHttpMessageConverter替代了默认的StringHttpMessageConverter(SpringMVC框架自带),区别是前者默认字符集为UTF_8,后者为ISO_8859_1

[2] JacksonHttpMessageConvertersConfiguration自动装配类引入的MappingJackson2HttpMessageConverter替代了默认的MappingJackson2HttpMessageConverter。区别是使用其内部实现序列化和反序列化的ObjectMapper对象来自全局Bean对象(来自JacksonAutoConfiguration自动装配类引入的ObjectMapper)。因此在配置文件中对spring.jackson属性的配置可以体现在MappingJackson2HttpMessageConverter转换器上。

2.2 MappingJackson2HttpMessageConverter转换器

(1) 匹配方法
由于MappingJackson2HttpMessageConverter是GenericHttpMessageConverter接口的实现类,匹配时根据canWrite(Type, Class<?>, MediaType)方法进行:

@Override
public boolean canWrite(@Nullable Type type, Class<?> clazz, @Nullable MediaType mediaType) {
	return canWrite(clazz, mediaType);
}

上述方法实现时吞掉了Type类型的参数, 调用重载的canWrite(Class<?>, MediaType)方法:

@Override
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) {
if (!canWrite(mediaType)) {
	return false;
}
if (mediaType != null && mediaType.getCharset() != null) {
	Charset charset = mediaType.getCharset();
	if (!ENCODINGS.containsKey(charset.name())) {
		return false;
	}
}

AtomicReference<Throwable> causeRef = new AtomicReference<>();
if (this.objectMapper.canSerialize(clazz, causeRef)) {
	return true;
}
logWarningIfNecessary(clazz, causeRef.get());
return false;
}

该方法可以分为三个部分:
[1] 调用canWrite(MediaType)判断媒体类型是否支持:

protected boolean canWrite(@Nullable MediaType mediaType) {
	if (mediaType == null || MediaType.ALL.equalsTypeAndSubtype(mediaType)) {
		return true;
	}
	for (MediaType supportedMediaType : getSupportedMediaTypes()) {
		if (supportedMediaType.isCompatibleWith(mediaType)) {
			return true;
		}
	}
	return false;
}

如果mediaType为空或者*/*或者与消息解析器支持的类型匹配则返回true;框架预置MappingJackson2HttpMessageConverter时,支持的MediaType已确定,为application/jsonapplication/*+json

[2] 判断编码类型是否符合, 支持的编码格式有UTF-8,UTF-16BE,UTF-16LE,UTF-32BE,UTF-32LE,US-ASCII

if (mediaType != null && mediaType.getCharset() != null) {
	Charset charset = mediaType.getCharset();
	if (!ENCODINGS.containsKey(charset.name())) {
		return false;
	}
}

MediaType对象的Charset为空时,默认支持;

[3] 调用ObjectMapper的canSerialize方法判断是否可被序列化;

AtomicReference<Throwable> causeRef = new AtomicReference<>();
if (this.objectMapper.canSerialize(clazz, causeRef)) {
	return true;
}

(2) 写方法

@Override
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
	final HttpHeaders headers = outputMessage.getHeaders();
	// 添加Content-type: application/json
	addDefaultHeaders(headers, t, contentType);
	if (outputMessage instanceof StreamingHttpOutputMessage) {
		StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
		streamingOutputMessage.setBody(outputStream -> writeInternal(t, type, new HttpOutputMessage() {
			@Override
			public OutputStream getBody() {
				return outputStream;
			}
			@Override
			public HttpHeaders getHeaders() {
				return headers;
			}
		}));
	} else {
		writeInternal(t, type, outputMessage);
		outputMessage.getBody().flush();
	}
}

write方法包含两个逻辑步骤:添加默认头域和写操作,写操作的实际执行方法在writeInternal中:

@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
	MediaType contentType = outputMessage.getHeaders().getContentType();
	 // 默认为UTF_8类型
	JsonEncoding encoding = getJsonEncoding(contentType);
	JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
	writePrefix(generator, object);

	Object value = object;
	Class<?> serializationView = null;
	FilterProvider filters = null;
	JavaType javaType = null;

	if (object instanceof MappingJacksonValue) {
		MappingJacksonValue container = (MappingJacksonValue) object;
		value = container.getValue();
		serializationView = container.getSerializationView();
		filters = container.getFilters();
	}
	if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
		javaType = getJavaType(type, null);
	}

	ObjectWriter objectWriter = (serializationView != null ?
			this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer());
	if (filters != null) {
		objectWriter = objectWriter.with(filters);
	}
	if (javaType != null && javaType.isContainerType()) {
		objectWriter = objectWriter.forType(javaType);
	}
	SerializationConfig config = objectWriter.getConfig();
	if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
			config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
		objectWriter = objectWriter.with(this.ssePrettyPrinter);
	}
	objectWriter.writeValue(generator, value);

	writeSuffix(generator, object);
	generator.flush();
}

上述方法可以分为三个步骤:添加前缀(如果有,内置的对象无前缀)、写内容、添加后缀(如果有,内置的对象无前缀),操作完全基于objectMapper对象;关于ObjectMappr的API用法不是本文的重点,不进行赘述。

3.工作原理

框架内置的消息转换器为处理HTTP请求和响应提供了强大的支持,基本可以满足项目的需要。这些转换器在容器启动时进行实例化和设置,后被保存在RequestMappingHandlerAdapter对象的messageConverters属性中。
当HTTP请求到达后,RequestMappingHandlerAdapter会构造一个ServletInvocableHandlerMethod对象,
且该对象拥有来自RequestMappingHandlerAdapter的消息转换器。
ServletInvocableHandlerMethod与HttpMessageConveter的关系图如下所示:
在这里插入图片描述当HTTP请求被DispatcherServlet接受时,调用链会进入RequestMappingHandlerAdapterinvokeHandlerMethod方法,构造ServletInvocableHandlerMethod对象并调用invokeAndHandle方法:

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
	 // 反射调用Controller接口获取返回结果
	 Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
	 
	 //... 
	
	 //将ModelAndViewContainer对象设置为请求未处理状态
	 mavContainer.setRequestHandled(false);
	
	 //处理结果
	 this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}

说明:
从Controller接口获取返回结果后,将结果处理工作委托给了returnValueHandlers属性,该属性是HandlerMethodReturnValueHandlerComposite组合类型,内部维持了一个List<HandlerMethodReturnValueHandler> returnValueHandlers列表;因此handleReturnValue实际会根据匹配关系分派到指定的HandlerMethodReturnValueHandler中。
框架内置的HandlerMethodReturnValueHandler和匹配关系如下:

ModelAndView及其子类->ModelAndViewMethodReturnValueHandler
Model及其子类->ModelMethodProcessor
View及其子类->ViewMethodReturnValueHandler
ResponseEntity及其子类或(ResponseEntity包裹)->ResponseBodyEmitterReturnValueHandler
StreamingResponseBody及其子类或(ResponseEntity包裹)->StreamingResponseBodyReturnValueHandler
HttpEntity,ResponseEntity->HttpEntityMethodProcessor
HttpHeaders及其子类->HttpHeadersReturnValueHandler
Callable及其子类->CallableMethodReturnValueHandler
DeferredResult、ListenableFuture、CompletionStage及其子类->DeferredResultMethodReturnValueHandler
WebAsyncTask及其子类->AsyncTaskMethodReturnValueHandler
ModelAttribute注解->ModelAttributeMethodProcessor
方法或类被ResponseBody注解->RequestResponseBodyMethodProcessor
void,CharSequence及其子类->ViewNameMethodReturnValueHandler
Map及其子类->MapMethodProcessor

本文重点关注RequestResponseBodyMethodProcessor, 该结果处理器的匹配规则如下:

public boolean supportsReturnType(MethodParameter returnType) {
	return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
			returnType.hasMethodAnnotation(ResponseBody.class));
}

即方法或者类被@ResponseBody注解的Controller接口使用RequestResponseBodyMethodProcessor。

当请求进入RequestResponseBodyMethodProcessor的handleReturnValue方法后:

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
	// 将该HTTP请求标记为已处理
	mavContainer.setRequestHandled(true);
	// 从webRequest获取HttpServletRequest的代理类
	ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
	// 从webRequest获取HttpServletResponse的代理类
	ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
	
	// Try even with null return value. ResponseBodyAdvice could get involved.
	writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

核心逻辑在writeWithMessageConverters方法:

protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
	// ...
}

该方法较长,主要步骤如下:
(1)获取返回对象类型,并使用Object对象接收返回对象

Object body;
Class<?> valueType;
Type targetType;
if (value instanceof CharSequence) {
    // 字符类型,则直接进行转换
    body = value.toString();
    valueType = String.class;
    targetType = String.class;
} else {
    body = value;
    valueType = getReturnValueType(body, returnType);
    targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}

注意:valueType为对象实际类型,不包括泛型信息;targetType包含泛型信息。
如:

public Map<String, Integer> getMap() {
    return new HashMap<>();
}

valueType为java.util.HashMap;而 targetType表示java.util.Map<java.lang.String, java.lang.Integer>

(2)InputStreamResource和Resource资源类型的特殊处理(Ignore);

(3)协商媒体类型,确定媒体类型

HttpServletRequest request = inputMessage.getServletRequest();
// 获取HTTP请求头中接收的媒体类型,代表客户端要求的MIME类型[标注1]
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
// 从所有的消息转换器中取媒体类型交集,代表服务器可以处理的媒体类型
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);

if (body != null && producibleTypes.isEmpty()) {
    throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
}
// 从服务器支持的媒体类型中筛选出客户端要求的MIME类型
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
    for (MediaType producibleType : producibleTypes) {
        if (requestedType.isCompatibleWith(producibleType)) {
            mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
        }
    }
}

if (mediaTypesToUse.isEmpty()) {
    if (body != null) {
        throw new HttpMediaTypeNotAcceptableException(producibleTypes);
    }
    if (logger.isDebugEnabled()) {
        logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
    }
    return;
}
// 排序,按照品质因子进行[标注2]
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
for (MediaType mediaType : mediaTypesToUse) {
    if (mediaType.isConcrete()) {
        selectedMediaType = mediaType;
        break;
    } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
        selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
        break;
    }
}

上述逻辑有两个地方需要补充说明一下:
[1] getAcceptableMediaTypes方法从HttpServletRequest对象中获取客户端允许的MIME类型,由于框架内置的媒体协商器是HeaderContentNegotiationStrategy,即从请求头中的ACCEPT字段获取MIME类型;
[2] Accept代表客户端允许的媒体类型,客户端可以同时支持多种类型的资源,且可通过品质因数进行排序,如下所示:
Accept: text/html;q=0.1,application/xhtml+xml;q=0.2,application/xml;q=0.3,application/json;q=0
Note: 不接受application/json类型,按照期望排序可接收text/htmlapplication/xhtml+xmlapplication/xml;类型
即q值越大,表示期望值越高。另外,出Accept外,Accept-Charset(字符集)、Accept-Encoding(压缩算法)、Accept-Language(国际化)在HTTP媒体协商过程也可携带品质因子.

(4)选择消息解析器,进行消息处理

// 删除选中的MIME的品质因子(即q值)
selectedMediaType = selectedMediaType.removeQualityValue();
// 遍历HttpMessageConverter,寻找第一个匹配的消息解析器处理body对象(待返回结果)
for (HttpMessageConverter<?> converter : this.messageConverters) {
    GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter<?>) converter : null);
    // [标注1]
    if (genericConverter != null ?((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :converter.canWrite(valueType, selectedMediaType)) {
        body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,(Class<? extends HttpMessageConverter<?>>) converter.getClass(),inputMessage, outputMessage);
        if (body != null) {
            Object theBody = body;
            // [标注2]
            addContentDispositionHeader(inputMessage, outputMessage);
            if (genericConverter != null) {
                genericConverter.write(body, targetType, selectedMediaType, outputMessage);
            } else {
                ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
            }
        }
        return;
    }
}

代码按照遍历+匹配+处理的思路铺开,逻辑比较清晰。有两个地方需要补充说明一下:
[1] 按照消息解析器是HttpMessageConverter还是GenericHttpMessageConverter,会使用不同的canWrite进行判断,后者多一个参数;write也有区别。
[2] addContentDispositionHeader用于为文件请求添加Content-Disposition头域,用于指示文件的名称和下载方式。取值范围有inlineattachmentinline表示文件直接浏览器中显示文本attachment表示文件下载到本地。

(5)异常场景处理
未匹配到消息处理器的场景,抛出异常。

----以上为所有内容----

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于机器学习与大数据的糖尿病预测 计算机竞赛

文章目录 1 前言1 课题背景2 数据导入处理3 数据可视化分析4 特征选择4.1 通过相关性进行筛选4.2 多重共线性4.3 RFE&#xff08;递归特征消除法&#xff09;4.4 正则化 5 机器学习模型建立与评价5.1 评价方式的选择5.2 模型的建立与评价5.3 模型参数调优5.4 将调参过后的模型重…

Mac风扇控制电脑降温软件Macs Fan Control Pro 简体中文

Macs Fan Control Pro是一款功能强大的Mac风扇控制软件&#xff0c;旨在帮助用户更好地管理和控制Mac电脑的风扇速度和温度传感器。以下是该软件的主要特色介绍&#xff1a; 监测和调整Mac电脑的风扇速度和温度传感器&#xff0c;帮助用户控制设备温度&#xff0c;提高电脑性能…

PyTorch基础(18)-- torch.stack()方法

一、方法详解 首先&#xff0c;看一下stack的直观解释&#xff0c;动词可以简单理解为&#xff1a;把……放成一堆、把……放成一摞。 有了对stack方法的直观感受&#xff0c;接下来&#xff0c;我们正式解析torch.stack方法。 PyTorch torch.stack() method joins (concaten…

每日汇评:黄金形态确认牛市,再次尝试上行2000美元

金价挑战1988美元的关键阻力位&#xff0c;向2000美元进发&#xff1b; 在避险情绪中&#xff0c;美元随着美债收益率扩大复苏&#xff1b; 黄金价格在日线图上确认了一个多头标志&#xff0c;RSI指数仍然指向更多的上涨&#xff1b; 随着中东紧张局势再次引起人们的关注&#…

《动手学深度学习 Pytorch版》 10.1 注意力提示

10.1.1 生物学中的注意力提示 “美国心理学之父” 威廉詹姆斯提出的双组件&#xff08;two-component&#xff09;框架&#xff1a; 非自主性提示&#xff1a;基于环境中物体的突出性和易见性 自主性提示&#xff1a;受到了认知和意识的控制 10.1.2 查询、键和值 注意力机制…

个人记账理财软件 Money Pro mac中文版软件介绍

Money Pro for mac是一款综合性高的理财工具&#xff0c;Money Pro是一套能够协同工作的工具&#xff0c;可用来追踪账户、管理账单以及制作预算&#xff0c;您可以为每个时间段设置不同的预算限值。财务一切尽在掌控之中。 Money Pro for mac软件介绍 Money Pro for mac提供一…

【QT】对象树

一、QT对象树的概念 先来看一下 QObject 的构造函数&#xff1a; 通过帮助文档我们可以看到&#xff0c;QObject 的构造函数中会传入一个 Parent 父对象指针&#xff0c;children() 函数返回 QObjectList。即每一个 QObject 对象有且仅有一个父对象&#xff0c;但可以有很多个…

【JavaSE语法】数据类型与变量

一、字面常量 常量即程序运行期间&#xff0c;固定不变,不可修改的量称为常量 public class Demo {public static void main(String[] args) {System.out.println("hello World!");System.out.println(100);System.out.println(3.14);System.out.println(A);System…

PLC、触摸屏、上位机之间如何实现无线数据交互功能?

本文以组态王与西门子触摸屏和2台西门子S7-200SMART为例&#xff0c;介绍组态王、触摸屏与多台 PLC在Profinet协议下的自组网无线通信实现过程。在本方案中采用了西门子PLC无线通讯终端——DTD418M&#xff0c;作为实现无线通讯的硬件设备。我们无需更改网络参数和原有程序&…

CANoe-使用IG Ethernet Packet Builder实现IP包分片的若干问题

在文章《CANoe-Ethernet IG和Ethernet Packet Builder的使用和区别》中,我们讲过Packet Builder可以组装多种类型的以太网报文: 当我们想组装一条icmpv4 echo request报文,payload只有1个字节的数据FF时,选择ICMPv4 Packet,创建一条ICMPv4报文,把payload改为1个字节: 然…

[开源]一个低代码引擎,支持在线实时构建低码平台,支持二次开发

一、开源项目简介 TinyEngine低代码引擎使能开发者定制低代码平台&#xff0c;支持在线实时构建低码平台&#xff0c;支持二次开发或集成低码平台能力。 二、开源协议 使用MIT开源协议 三、界面展示 四、功能概述 TinyEngine是一个低代码引擎&#xff0c;基于这个引擎可以构…

腾讯云轻量应用服务器性能差吗?为什么便宜?

腾讯云轻量应用服务器性能如何&#xff1f;为什么便宜是不是性能不行&#xff1f;腾讯云百科txybk.com从轻量应用服务器的CPU型号、处理器主频、内存、公网带宽、月流量和系统盘多方面来详细测评轻量性能&#xff0c;轻量应用服务器性价比高&#xff0c;并不是性能不行&#xf…

react native 使用夜神模拟器开发调试 windows+android

执行adb devices, 提示List of devices attached 打开本地sdk目录中的platform-tools文件夹&#xff0c;复制下面3个文件 打开夜神模拟器安装目录中的bin目录&#xff0c;把复制出来的文件复制替换到bin目录中 在复制一份platform-tools目录中的adb.exe&#xff0c;重命名为…

TypeError: data.reduce is not a function:数据类型不匹配

错误展示&#xff1a; 错误分析&#xff1a; 首先来看看前端代码&#xff1a;我表格绑定的数据模型是tableData&#xff0c;而我tableData定义的是一个数组 其次看看后端给的数据&#xff1a; 传递的是一个对象&#xff0c;而不是一个数组&#xff01; 这样原因就找出了&…

计算机网络相关硬件介绍

计算机相关硬件 计算机由运算器、控制器、存储器、输入设备和输出设备等五个逻辑计算机硬件部件组成。 一、中央处理器&#xff08;CPU&#xff09;&#xff08;运算器、控制器&#xff09; &#xff08;1&#xff09;运算器 运算器是对数据进行加工处理的部件&#xff…

安卓主板_MTK联发科4G低功耗安卓主板开发板方案

ZM358-DP安卓主板是一款性能功能强大的4G安卓平台。它采用了联发科MTK6737、MTK8735、MTK6753、MTK6735等芯片平台&#xff0c;64位四核Cortex-A53架构&#xff0c;主频高达1.3GHz&#xff0c;搭载ARM Mail-T450 MP2 GPU。 安卓主板具备多路显示屏接口&#xff0c;包括双LVDS、…

可自由搭建的能源管理平台,轻松实现高效节能

随着科技的不断发展&#xff0c;能源问题越来越重要。为了提高能源的利用效率&#xff0c;减少能源浪费&#xff0c;能源用能企业纷纷开始注重能源管理工作&#xff0c;并想要一款可以进行高效管理的工具。智慧能源管理平台&#xff0c;是一款可自由搭建的能源管理平台&#xf…

一个全响应式的企业级物联网平台,开源了

JetLinks 是一个开源的、企业级的物联网平台&#xff0c;它集成了设备管理、数据安全通信、消息订阅、规则引擎等一系列物联网核心能力&#xff0c;支持以平台适配设备的方式连接海量设备&#xff0c;采集设备数据上云&#xff0c;提供云端 API&#xff0c;通过调用云端 API 实…

【计网 DNS】计算机网络 DNS协议详解:中科大郑烇老师笔记 (六)

目录 0 引言1 DNS概述1.1 定义1.2 DNS域名结构1.2 域名解析步骤 &#x1f64b;‍♂️ 作者&#xff1a;海码007&#x1f4dc; 专栏&#xff1a;计算机四大基础专栏&#x1f4dc; 其他章节&#xff1a;网络快速入门系列、计网概述、计网应用层详解、计网Web和HTTP、计网FTP、计网…

深度学习_4_实战_直线最优解

梯度 实战 代码&#xff1a; # %matplotlib inline import random import torch import matplotlib.pyplot as plt # from d21 import torch as d21def synthetic_data(w, b, num_examples):"""生成 Y XW b 噪声。"""X torch.normal(0,…