从开源项目中学习如何自定义 Spring Boot Starter 小组件

news/2024/7/10 19:43:48 标签: 开源, 学习, spring boot

前言

今天参考的开源组件Graceful Response——Spring Boot接口优雅响应处理器。

具体用法可以参考github以及官方文档。

image-20231214090633021

基本使用

引入Graceful Response组件

项目中直接引入如下maven依赖,即可使用其相关功能。

        <dependency>
            <groupId>com.feiniaojin</groupId>
            <artifactId>graceful-response</artifactId>
            <version>{latest.version}</version>
        </dependency>

开始使用

配置文件可不配置,直接全部默认配置

graceful-response:
  response-style: 1  # 配置响应格式类型(0 or 1)后续介绍
  print-exception-in-global-advice: true # 在全局异常处理器中打印异常,默认不打印

一切之前需在主启动类添加该注解开启功能:@EnableGracefulResponse。(后续介绍)

Controller直接返回我们查询的对象即可,Graceful Response会帮我们封装响应格式(响应格式可配置

Controller

@RestController
public class HelloController {
    
    @Resource
    private UserMapper userMapper;
    
    @GetMapping("/userList")
    public List<User> userList() {
        return userMapper.selectList(null);
    }
}

调用接口:

image-20231214113454576

默认成功/失败状态码和提示消息也都是可以配置的,是不是感觉非常的神奇(一切皆可配)

image-20231214114109561

其他具体用法见官网。

废话不多说,接下来我们直接步入主题探究下自定义starter组件核心步骤

自定义starter组件核心步骤

首先明确一下,自定义starter组件的三个核心步骤:

  1. 自定义注解
  2. 结合Spring AOP实现注解逻辑
  3. 实现starter组件自动装配以及可配置

接下来看开源组件都是怎么做的吧~

看看源码目录结构(麻雀虽小但五脏俱全):

image-20231214103700335

0. 注解使用

@ExceptionMapper注解为例,介绍一下相关功能及实现原理。

Graceful Response引入@ExceptionMapper注解,通过该注解将异常和错误码关联起来,这样Service方法就不需要再维护Response的响应码了,直接抛出业务异常,由Graceful Response进行异常和响应码的关联。 @ExceptionMapper的用法如下:

自定义业务类异常

/**
 * NotFoundException的定义,使用@ExceptionMapper注解修饰
 * code:代表接口的异常码
 * msg:代表接口的异常提示
 */
@ExceptionMapper(code = "1404", msg = "找不到对象")
public class NotFoundException extends RuntimeException {

}

Service接口定义

public interface QueryService {
    UserInfoView queryOne(Query query);
}

Service接口实现

@Service
public class QueryServiceImpl implements QueryService {
    @Resource
    private UserInfoMapper mapper;

    public UserInfoView queryOne(Query query) {
        UserInfo userInfo = mapper.findOne(query.getId());
        if (Objects.isNull(userInfo)) {
            // 这里直接抛自定义异常
            throw new NotFoundException();
        }
        // ……后续业务操作
    }
}

当Service层的queryOne方法抛出NotFoundException时,Graceful Response会进行异常捕获,并将NotFoundException对应的异常码和异常信息封装到统一的响应对象中,最终接口返回以下JSON(默认响应格式

{
  "status": {
    "code": "1404",
    "msg": "找不到对象"
  },
  "payload": {}
}

使用起来十分方便,接下来我们看下具体实现原理。

1. 自定义注解

首先看下注解定义:

/**
 * 异常映射注解.
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ExceptionMapper {

    /**
     * 异常对应的错误码.
     *
     * @return 异常对应的错误码
     */
    String code() default "ERROR";

    /**
     * 异常信息.
     *
     * @return 异常对应的提示信息
     */
    String msg() default "Poor network quality!";

    /**
     * 异常信息是否支持替换
     * 仅当msgReplaceable==ture,且异常实例的message不为空时才能替换
     */
    boolean msgReplaceable() default false;
}

2. 结合Spring AOP实现注解逻辑(核心)

配置类

配置类GracefulResponseProperties跟可配置项相关,所有配置项都映射为该类

@ConfigurationProperties(prefix = "graceful-response") // 配置前缀
public class GracefulResponseProperties {

    /**
     * 在全局异常处理器中打印异常,默认不打印
     */
    private boolean printExceptionInGlobalAdvice = false;

    /**
     * 默认的Response实现类名称,配置了responseClassFullName,则responseStyle不生效
     */
    private String responseClassFullName;

    /**
     * responseStyle的风格,responseClassFullName为空时才会生效
     * responseStyle==null或者responseStyle==0,Response风格为 DefaultResponseImplStyle0
     * responseStyle=1,Response风格为 DefaultResponseImplStyle1
     */
    private Integer responseStyle;

    /**
     * 默认的成功返回码 默认0
     */
    private String defaultSuccessCode = DefaultConstants.DEFAULT_SUCCESS_CODE;

    /**
     * 默认的成功提示 默认OK
     */
    private String defaultSuccessMsg = DefaultConstants.DEFAULT_SUCCESS_MSG;

    /**
     * 默认的失败码 默认1
     */
    private String defaultErrorCode = DefaultConstants.DEFAULT_ERROR_CODE;

    /**
     * 默认的失败提示 默认error
     */
    private String defaultErrorMsg = DefaultConstants.DEFAULT_ERROR_MSG;

    /**
     * Validate异常码,不提供的话默认DefaultConstants.DEFAULT_ERROR_CODE
     */
    private String defaultValidateErrorCode = DefaultConstants.DEFAULT_ERROR_CODE;

    /**
     * 例外包路径
     */
    private List<String> excludePackages;

    /**
     * 不使用@ExceptionMapper和@ExceptionAliasFor修饰的原生异常
     * 是否使用异常信息Throwable类的detailMessage进行返回
     * originExceptionUsingDetailMessage=false,则msg=defaultErrorMsg
     */
    private Boolean originExceptionUsingDetailMessage = false;

    // getter / setter
}

设置响应状态

ResponseStatusFactory:响应状态工厂(仅定义code和msg),默认实现成功默认响应、失败默认响应以及自定义code、msg响应状态

默认实现类:

// 响应状态
public interface ResponseStatus {

    void setCode(String code);

    String getCode();

    void setMsg(String msg);

    String getMsg();
}

// 默认响应状态
public class DefaultResponseStatus implements ResponseStatus {

    private String code;

    private String msg;

    public DefaultResponseStatus() {
    }

    public DefaultResponseStatus(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    @Override
    public void setCode(String code) {
        this.code = code;
    }

    @Override
    public String getCode() {
        return code;
    }

    @Override
    public void setMsg(String msg) {
        this.msg = msg;
    }

    @Override
    public String getMsg() {
        return msg;
    }
}
// 核心实现类
public class DefaultResponseStatusFactoryImpl implements ResponseStatusFactory {

    @Resource
    private GracefulResponseProperties properties;

    // 默认成功响应转态
    @Override
    public ResponseStatus defaultSuccess() {

        DefaultResponseStatus defaultResponseStatus = new DefaultResponseStatus();
        defaultResponseStatus.setCode(properties.getDefaultSuccessCode());
        defaultResponseStatus.setMsg(properties.getDefaultSuccessMsg());
        return defaultResponseStatus;
    }

    // 默认失败响应转态
    @Override
    public ResponseStatus defaultError() {
        DefaultResponseStatus defaultResponseStatus = new DefaultResponseStatus();
        defaultResponseStatus.setCode(properties.getDefaultErrorCode());
        defaultResponseStatus.setMsg(properties.getDefaultErrorMsg());
        return defaultResponseStatus;
    }

    // 自定code、msg状态
    @Override
    public ResponseStatus newInstance(String code, String msg) {
        return new DefaultResponseStatus(code, msg);
    }
}

设置响应格式

ResponseFactory:根据配置项,设置响应格式

默认实现类:

public class DefaultResponseFactory implements ResponseFactory {

    private final Logger logger = LoggerFactory.getLogger(DefaultResponseFactory.class);

    private static final Integer RESPONSE_STYLE_0 = 0;

    private static final Integer RESPONSE_STYLE_1 = 1;

    @Resource
    private ResponseStatusFactory responseStatusFactory;

    @Resource
    private GracefulResponseProperties properties;

    @Override
    public Response newEmptyInstance() {
        try {
            String responseClassFullName = properties.getResponseClassFullName();

            // 配置了Response的全限定名,即实现Response接口,用配置的进行返回
            if (StringUtils.hasLength(responseClassFullName)) {
                Object newInstance = Class.forName(responseClassFullName).getConstructor().newInstance();
                return (Response) newInstance;
            } else {
                // 没有配Response的全限定名,则创建DefaultResponse
                return generateDefaultResponse();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    // 响应格式判断
    private Response generateDefaultResponse() {
        Integer responseStyle = properties.getResponseStyle();
        // 未配置或者配置0
        if (Objects.isNull(responseStyle) || RESPONSE_STYLE_0.equals(responseStyle)) {
            return new DefaultResponseImplStyle0();
        } else if (RESPONSE_STYLE_1.equals(responseStyle)) {
            return new DefaultResponseImplStyle1();
        } else {
            logger.error("不支持的Response style类型,responseStyle={}", responseStyle);
            throw new IllegalArgumentException("不支持的Response style类型");
        }
    }

    @Override
    public Response newInstance(ResponseStatus responseStatus) {
        Response bean = this.newEmptyInstance();
        bean.setStatus(responseStatus);
        return bean;
    }

    @Override
    public Response newSuccessInstance() {
        Response emptyInstance = this.newEmptyInstance();
        emptyInstance.setStatus(responseStatusFactory.defaultSuccess());
        return emptyInstance;
    }

    @Override
    public Response newSuccessInstance(Object payload) {
        Response bean = this.newSuccessInstance();
        bean.setPayload(payload);
        return bean;
    }

    @Override
    public Response newFailInstance() {
        Response bean = this.newEmptyInstance();
        bean.setStatus(responseStatusFactory.defaultError());
        return bean;
    }

}

对应配置文件中两种响应格式:

image-20231214140225763

默认响应格式:

public interface Response {
    
    void setStatus(ResponseStatus statusLine);

    ResponseStatus getStatus();

    void setPayload(Object payload);

    Object getPayload();
}

image-20231214142333843

默认格式实现类:

{
  "status": {
    "code": "",
    "msg": ""
  },
  "payload": {}
}
public class DefaultResponseImplStyle0 implements Response {

    private ResponseStatus status;
    
    private Object payload = Collections.emptyMap();

    public DefaultResponseImplStyle0() {
    }

    public DefaultResponseImplStyle0(Object payload) {
        this.payload = payload;
    }

    @Override
    public void setStatus(ResponseStatus responseStatus) {
        this.status = responseStatus;
    }

    @Override
    public ResponseStatus getStatus() {
        return status;
    }

    @Override
    public void setPayload(Object obj) {
        this.payload = obj;
    }

    @Override
    public Object getPayload() {
        return payload;
    }
}

style设置为1,响应格式:

{

    "code": "1404",
    "msg": "找不到对象"
    "data": {}
}

另一种响应实现类

public class DefaultResponseImplStyle1 implements Response {

    private String code;

    private String msg;

    private Object data = Collections.emptyMap();

    @Override
    public void setStatus(ResponseStatus statusLine) {
        this.code = statusLine.getCode();
        this.msg = statusLine.getMsg();
    }

    @Override
    @JsonIgnore
    public ResponseStatus getStatus() {
        return null;
    }

    @Override
    public void setPayload(Object payload) {
        this.data = payload;
    }

    // 这里直接把 payload 忽略了(因此这种响应格式中没有payload字段)
    @Override
    @JsonIgnore
    public Object getPayload() {
        return null;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

核心处理逻辑类

核心处理逻辑类:全局异常处理

/**
 * 全局异常处理.
 */
@ControllerAdvice
@Order(200)
public class GlobalExceptionAdvice implements ApplicationContextAware {

    private final Logger logger = LoggerFactory.getLogger(GlobalExceptionAdvice.class);

    @Resource
    private ResponseStatusFactory responseStatusFactory;

    @Resource
    private ResponseFactory responseFactory;

    private ExceptionAliasRegister exceptionAliasRegister;

    // 配置类
    @Resource
    private GracefulResponseProperties properties;

    /**
     * 异常处理逻辑. 【统一捕获所有异常】
     *
     * @param throwable 业务逻辑抛出的异常
     * @return 统一返回包装后的结果
     */
    @ExceptionHandler({Throwable.class})
    @ResponseBody 
    public Response exceptionHandler(Throwable throwable) {
        // 配置项是否打印异常信息(默认false)
        if (properties.isPrintExceptionInGlobalAdvice()) {
            logger.error("Graceful Response:GlobalExceptionAdvice捕获到异常,message=[{}]", throwable.getMessage(), throwable);
        }
        ResponseStatus statusLine;
        if (throwable instanceof GracefulResponseException) {
            statusLine = fromGracefulResponseExceptionInstance((GracefulResponseException) throwable);
        } else {
            // 校验异常转自定义异常
            statusLine = fromExceptionInstance(throwable);
        }
        // 设置完code、msg之后直接返回
        return responseFactory.newInstance(statusLine);
    }

    private ResponseStatus fromGracefulResponseExceptionInstance(GracefulResponseException exception) {
        String code = exception.getCode();
        if (code == null) {
            code = properties.getDefaultErrorCode();
        }
        return responseStatusFactory.newInstance(code,
                exception.getMsg());
    }

    private ResponseStatus fromExceptionInstance(Throwable throwable) {

        Class<? extends Throwable> clazz = throwable.getClass();

        // 【直接获取抛出异常上的注解】
        ExceptionMapper exceptionMapper = clazz.getAnnotation(ExceptionMapper.class);

        // 1.有@ExceptionMapper注解,直接设置结果的状态
        if (exceptionMapper != null) {
            boolean msgReplaceable = exceptionMapper.msgReplaceable();
            //异常提示可替换+抛出来的异常有自定义的异常信息
            if (msgReplaceable) {
                String throwableMessage = throwable.getMessage();
                if (throwableMessage != null) {
                    return responseStatusFactory.newInstance(exceptionMapper.code(), throwableMessage);
                }
            }
            return responseStatusFactory.newInstance(exceptionMapper.code(),
                    exceptionMapper.msg());
        }

        // 2.有@ExceptionAliasFor异常别名注解,获取已注册的别名信息
        if (exceptionAliasRegister != null) {
            ExceptionAliasFor exceptionAliasFor = exceptionAliasRegister.getExceptionAliasFor(clazz);
            if (exceptionAliasFor != null) {
                return responseStatusFactory.newInstance(exceptionAliasFor.code(),
                        exceptionAliasFor.msg());
            }
        }
        ResponseStatus defaultError = responseStatusFactory.defaultError();

        // 3. 原生异常 + originExceptionUsingDetailMessage=true
        // 如果有自定义的异常信息,原生异常将直接使用异常信息进行返回,不再返回默认错误提示
        if (properties.getOriginExceptionUsingDetailMessage()) {
            String throwableMessage = throwable.getMessage();
            if (throwableMessage != null) {
                defaultError.setMsg(throwableMessage);
            }
        }
        return defaultError;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.exceptionAliasRegister = applicationContext.getBean(ExceptionAliasRegister.class);
    }
}

非空返回值封装处理类(ResponseBodyAdvice)

ResponseBodyAdvice:泛型T为响应data类型,直接Object即可

@ControllerAdvice
@Order(value = 1000)
public class NotVoidResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private final Logger logger = LoggerFactory.getLogger(NotVoidResponseBodyAdvice.class);

    @Resource
    private ResponseFactory responseFactory;
    @Resource
    private GracefulResponseProperties properties;

    /**
     * 路径过滤器
     */
    private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

    /**
     * 只处理不返回void的,并且MappingJackson2HttpMessageConverter支持的类型.
     *
     * @param methodParameter 方法参数
     * @param clazz           处理器
     * @return 是否支持
     */
    @Override
    public boolean supports(MethodParameter methodParameter,
                            Class<? extends HttpMessageConverter<?>> clazz) {
        Method method = methodParameter.getMethod();

        // method为空、返回值为void、非JSON,直接跳过
        if (Objects.isNull(method)
                || method.getReturnType().equals(Void.TYPE)
                || !MappingJackson2HttpMessageConverter.class.isAssignableFrom(clazz)) {
            logger.debug("Graceful Response:method为空、返回值为void、非JSON,跳过");
            return false;
        }

        // 有ExcludeFromGracefulResponse注解修饰的,也跳过
        if (method.isAnnotationPresent(ExcludeFromGracefulResponse.class)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Graceful Response:方法被@ExcludeFromGracefulResponse注解修饰,跳过:methodName={}", method.getName());
            }
            return false;
        }

        // 配置了例外包路径,则该路径下的controller都不再处理
        List<String> excludePackages = properties.getExcludePackages();
        if (!CollectionUtils.isEmpty(excludePackages)) {
            // 获取请求所在类的的包名
            String packageName = method.getDeclaringClass().getPackage().getName();
            if (excludePackages.stream().anyMatch(item -> ANT_PATH_MATCHER.match(item, packageName))) {
                logger.debug("Graceful Response:匹配到excludePackages例外配置,跳过:packageName={},", packageName);
                return false;
            }
        }
        logger.debug("Graceful Response:非空返回值,需要进行封装");
        return true;
    }

    // 满足上述条件,封装返回对象(只要是非Void返回值,不做任何配置都会封装返回结果)
    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> clazz,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        if (body == null) {
            return responseFactory.newSuccessInstance();
        } else if (body instanceof Response) {
            return body;
        } else {
            // 设置data数据
            return responseFactory.newSuccessInstance(body);
        }
    }

}

3. 实现starter组件自动装配以及可配置

全局自动装配类

该类可以将我们自定义的所有类全部加载到Spring容器中。

/**
 * 全局返回值处理的自动配置.
 */
@Configuration
// 将外部配置文件中的属性注入到配置类中(将配置类加载到Spring容器中)
@EnableConfigurationProperties(GracefulResponseProperties.class)
public class AutoConfig {

    @Bean
    // 这个注解很是重要
    @ConditionalOnMissingBean(value = GlobalExceptionAdvice.class)
    public GlobalExceptionAdvice globalExceptionAdvice() {
        return new GlobalExceptionAdvice();
    }

    @Bean
    @ConditionalOnMissingBean(value = ValidationExceptionAdvice.class)
    public ValidationExceptionAdvice validationExceptionAdvice() {
        return new ValidationExceptionAdvice();
    }

    @Bean
    @ConditionalOnMissingBean(NotVoidResponseBodyAdvice.class)
    public NotVoidResponseBodyAdvice notVoidResponseBodyAdvice() {
        return new NotVoidResponseBodyAdvice();
    }

    @Bean
    @ConditionalOnMissingBean(VoidResponseBodyAdvice.class)
    public VoidResponseBodyAdvice voidResponseBodyAdvice() {
        return new VoidResponseBodyAdvice();
    }

    @Bean
    @ConditionalOnMissingBean(value = {ResponseFactory.class})
    public ResponseFactory responseBeanFactory() {
        return new DefaultResponseFactory();
    }

    @Bean
    @ConditionalOnMissingBean(value = {ResponseStatusFactory.class})
    public ResponseStatusFactory responseStatusFactory() {
        return new DefaultResponseStatusFactoryImpl();
    }

    @Bean
    public ExceptionAliasRegister exceptionAliasRegister() {
        return new ExceptionAliasRegister();
    }

    @Bean
    public Init init(){
        return new Init();
    }
}

注解启动全局结果处理入口

通过元注解 @Import(AutoConfig.class) 实际上将 AutoConfig 这个配置类引入到标注了 @EnableGracefulResponse 注解的类中。

引入该组件,只有在某个类上添加了 @EnableGracefulResponse 注解时,AutoConfig 中定义的相关 Bean 才会被注册到 Spring 容器中。可以方便地启用特定功能或配置。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfig.class)
public @interface EnableGracefulResponse {
}

SpringBoot主启动类,添加该注解开启功能:

image-20231214153605858

另一种方式META-INF/spring.factories

如果想引入该组件就直接开启所有功能,可以不用上述全局启动注解

直接在组件项目的 resources 中创建 META-INF 目录,并在此目录下创建一个 spring.factories 文件,将starter组件的自动配置类的类路径写在文件上即可。

resources
	META-INF
		spring.factories
// 直接将自动装配类全限定名放入该文件即可
com.feiniaojin.gracefulresponse.AutoConfig

Spring Boot项目启动时,会扫描外部引入的Jar中的META-INF/spring.factories文件,将文件中配置的类信息装配到Spring容器中)【用的是Java的SPI机制,还没研究后续再说吧】

这样只要引入该组件,组件功能也就集成进来了。【这种不够灵活,视情况用哪种方式】(建议:能用全局启动注解就用全局启动注解)

配置智能提示

组件中记得添加该依赖:

       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>${spring.boot.version}</version>
            <optional>true</optional>
        </dependency>

组件中添加上述依赖可以用于处理配置类(@ConfigurationProperties 注解标注的类)以及生成相关的元数据。

IDE 支持: 提供更好的集成开发环境(IDE)支持。通过生成的配置元数据,IDE 可以在配置文件中提供智能提示,我们可以更方便地编辑配置。

引入该组件,在修改配置类可以有智能提示

如下:

image-20231214114109561

该文章只介绍了该组件部分功能,有兴趣可以自行研究呀~

最后看一下组件的目录结构(学习一下):

image-20231214153359444

小结

自定义starter组件的三个核心步骤:

  1. 自定义注解
  2. 注解结合AOP实现逻辑
  3. 自动装配和可配置(配置类、自动装配类以及全局启动注解/spring.factories文件

http://www.niftyadmin.cn/n/5262079.html

相关文章

C#中简单的继承和多态

今天我们来聊一聊继承&#xff0c;说实话今天也是我第一次接触。 继承的概念是什么呢&#xff1f;就是一个类可以继承另一个类的属性和方法&#xff08;成员&#xff09; 继承是面向对象编程中的一个非常重要的特性。 好了&#xff0c;废话不多说&#xff0c;下面切入正题&a…

Excel函数 - Filter函数六种查询用法

语法&#xff1a;filter(查询区域&#xff0c;条件&#xff0c;查不到结果返回的值) 注意条件是布尔表达式&#xff0c;这个条件为true&#xff0c;filter才返回结果&#xff0c;否则查不到结果。 一、单条件查询&#xff0c;返回多行多列记录 我们要查询“苹果”这个水果&a…

ubuntu解决问题:E: Unable to locate package manpages-posix-dev

sudo apt-get install manpages-posix-dev 想要在ubuntu里面安装manpages-posix-dev这个包&#xff0c;发现弹出错误 E: Unable to locate package manpages-posix-dev 解决方法如下&#xff1a; 1 查看当前ubuntu的版本 abhishekitsfoss:~$ lsb_release -a No LSB module…

阿里云SLS日志服务之数据导入与加工处理

一、背景 采集vm虚拟机上的Log日志文本&#xff0c;如果需要经过特殊的加工处理&#xff0c;在本文主要讲述如何在SLS把kafka采集上来的数据经导入并加工后存储。 二、数据流转图 三、数据导入 服务地址&#xff1a;填写kafka集群的地址数据格式&#xff1a;json字符串&#…

基于基于深度学习的表情识别人脸打分系统

1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 研究背景与意义 随着人工智能技术的快速发展&#xff0c;深度学习在计算机视觉领域取得了巨大的突破。表情识别是计算机视觉领域的一个重要研究方向&#xff0c;它可以通过分析人…

gd32F303串口接收的几种方式

一、串口接受到换行符停止 #include "usart.h"UCHAR arr_rxData[1024]{0};//存储串口接收的数据 UINT arr_len0;//串口已经接收数据的字节数个数 USART_RX_FLAG rxFlgFLASE;//默认串口未接收到数据void USART2_Init(void) {rcu_periph_clock_enable(RCU_GPIOB); //e…

JS 有哪些数据类型

JS 有哪些数据类型 7 大基本数据类型&#xff1a;Number、String、Boolean、Null、Undefined、Symbol、BigInt。 引用数据类型&#xff1a;Object。 Array、Date、RegExp、Map、Set 等本质上都属于 Object&#xff0c;typeof 出来的结果都是 object。 拓展&#xff1a; 1、B…

Linux/Android之od以字符格式、2进制、8进制、10进制、16进制显示文件内容(三十三)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…