@ControllerAdvice:你可以没用过,但是不能不了解

1.概述

最近在梳理Spring MVC相关扩展点时发现了@ControllerAdvice这个注解,用于定义全局的异常处理、数据绑定、数据预处理等功能。通过使用 @ControllerAdvice,可以将一些与控制器相关的通用逻辑提取到单独的类中进行集中管理,从而减少代码重复,提升代码的可维护性。

定义如下

/**
 * Specialization of {@link Component @Component} for classes that declare
 * {@link ExceptionHandler @ExceptionHandler}, {@link InitBinder @InitBinder}, or
 * {@link ModelAttribute @ModelAttribute} methods to be shared across
 * multiple {@code @Controller} classes.
 * ........
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};
	Class<?>[] basePackageClasses() default {};
	Class<?>[] assignableTypes() default {};
	Class<? extends Annotation>[] annotations() default {};

}

从定义来看,@ControllerAdvice@Component的一个派生注解,这就意味着使用该注解的类会被Spring扫描到放入bean容器中。从上面注释也可以得知@ControllerAdvice一般与这三个注解@ExceptionHandler@InitBinder@ModelAttribute配合使用,从而作用于所有的@Controller类的接口上。@ExceptionHandler想来我们并不陌生,是用来全局异常统一处理的,但另外两个注解@InitBinder@ModelAttribute在日常中个人感觉并不常用,我们稍后会浅浅分析下它们是做什么用的。

2.@ExceptionHandler

这个注解我们并不陌生,进行统一异常处理使用的,程序由于运行时异常导致报错的结果,有些异常我们可能无法提前预知,接口不能正常返回结果,因此我们需要定义一个统一的全局异常来捕获这些信息,并作为一种结果返回给控制层。

先来看看没有进行全局异常处理的报错,搞一个Java常出现的示例如下:

    @GetMapping("/111")
    public void test111() {
        User user = null;
        String userNo = user.getUserNo();
        System.out.println(userNo);
    }

调接口报错如下:

{
    "timestamp": "2024-06-13T06:25:01.508+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/test/111"
}

这对于前端来说是不太友好的。下面来看看全局统一异常处理

package com.shepherd.basedemo.advice;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;

import java.util.HashMap;
import java.util.Map;

/**
 * @author fjzheng
 * @version 1.0
 * @date 2024/6/13 14:41
 */
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 全局异常处理
     * @param e
     * @return
     */
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(Exception.class)
    public ResponseVO exceptionHandler(Exception e){
        // 处理业务异常
        if (e instanceof BizException) {
            BizException bizException = (BizException) e;
            if (bizException.getCode() == null) {
                bizException.setCode(ResponseStatusEnum.BAD_REQUEST.getCode());
            }
            return ResponseVO.failure(bizException.getCode(), bizException.getMessage());
        } else if (e instanceof MethodArgumentNotValidException) {
            // 参数检验异常
            MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) e;
            Map<String, String> map = new HashMap<>();
            BindingResult result = methodArgumentNotValidException.getBindingResult();
            result.getFieldErrors().forEach((item)->{
                String message = item.getDefaultMessage();
                String field = item.getField();
                map.put(field, message);
            });
            log.error("数据校验出现错误:", e);
            return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST, map);
        } else if (e instanceof HttpRequestMethodNotSupportedException) {
            log.error("请求方法错误:", e);
            return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求方法不正确");
        } else if (e instanceof MissingServletRequestParameterException) {
            log.error("请求参数缺失:", e);
            MissingServletRequestParameterException ex = (MissingServletRequestParameterException) e;
            return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数缺少: " + ex.getParameterName());
        } else if (e instanceof MethodArgumentTypeMismatchException) {
            log.error("请求参数类型错误:", e);
            MethodArgumentTypeMismatchException ex = (MethodArgumentTypeMismatchException) e;
            return ResponseVO.failure(ResponseStatusEnum.BAD_REQUEST.getCode(), "请求参数类型不正确:" + ex.getName());
        } else if (e instanceof NoHandlerFoundException) {
            NoHandlerFoundException ex = (NoHandlerFoundException) e;
            log.error("请求地址不存在:", e);
            return ResponseVO.failure(ResponseStatusEnum.NOT_EXIST, ex.getRequestURL());
        } else {
            //如果是系统的异常,比如空指针这些异常
            log.error("【系统异常】", e);
            return ResponseVO.failure(ResponseStatusEnum.SYSTEM_ERROR.getCode(), ResponseStatusEnum.SYSTEM_ERROR.getMsg());
        }
    }

}

再次调用接口结果如下:

这时候正常返回统一的格式,方便前端处理。关于接口返回结果格式全局统一和异常统一处理详解请看之前总结的:Spring Boot如何优雅实现结果统一封装和异常统一处理

3.@InitBinder

该注解作用于方法上,用于将前端请求的特定类型的参数在到达controller之前进行处理,从而达到转换请求参数格式的目的。

先来看看我们接口示例:

    @GetMapping("/222")
    public void test222(User user) {
        System.out.println(user);
    }
@Data
public class User {
    private Long id;
    private Date birthday;
}

postman调接口:

报错了:

org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'user' on field 'birthday': rejected value [2024-06-30 12:00:00]; codes [typeMismatch.user.birthday,typeMismatch.birthday,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.birthday,birthday]; arguments []; default message [birthday]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'birthday'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.fasterxml.jackson.annotation.JsonFormat @com.alibaba.excel.annotation.ExcelProperty java.util.Date] for value [2024-06-30 12:00:00]; nested exception is java.lang.IllegalArgumentException]

这时候使用@InitBinder就能解决了

@ControllerAdvice
public class GlobalAdviceHandler {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        // 自定义数据绑定逻辑
        binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"), false));
    }
}

重新调接口控制台就正常输出了。但请注意@InitBinder仅作用于get接口,对于post接口的@RequestBody接收参数并不起效

    @PostMapping("/111")
    public void test111(@RequestBody User user) {
        System.out.println(user);
    }

针对于json传参我们可以在接参的实体日期字段上添加@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”, timezone = “GMT+8”)

@Data
public class User {
    private Long id;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date birthday;
}

或者在配置文件配置如下:

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
    locale: zh_CN
    time-zone: GMT+8
    default-property-inclusion: non_null

4.@ModelAttribute

该注解作用于方法和请求参数上,在方法上时设置一个值,可以直接在进入controller后传入该参数。全局绑定登录上下文参数:

@ControllerAdvice
public class GlobalAdviceHandler {
  
   @ModelAttribute("loginUser")
    public LoginUser setLoginUser() {
        return RequestUserHolder.getCurrentUser();
    }
}

接口方法就能使用@ModelAttribute绑定获取参数了

    // 使用
    @PostMapping("/student")
    public ResponseVO<Long> addStudent(@ModelAttribute("loginUser") LoginUser loginUser, @RequestBody Student student){
        return ResponseVO.success(studentService.addStudent(loginUser, student));
    }

其实完全没必要这么做,需要登录上下文信息时候直接使用RequestUserHolder.getCurrentUser()获取即可,看你怎么选择啦,是喜欢通过方法参数传递登录信息上下文,还是用的地方再获取。

5.@ControllerAdvice实现原理

我们都知道Spring MVC的核心处理器是DispatcherServlet,项目启动时会调用 DispatcherServlet#initStrategies(ApplicationContext context) 方法,初始化 Spring MVC 的各种组件

protected void initStrategies(ApplicationContext context) {
    // 初始化 MultipartResolver
	initMultipartResolver(context);
	// 初始化 LocaleResolver
	initLocaleResolver(context);
	// 初始化 ThemeResolver
	initThemeResolver(context);
	// 初始化 HandlerMappings
	initHandlerMappings(context);
	// 初始化 HandlerAdapters
	initHandlerAdapters(context);
	// 初始化 HandlerExceptionResolvers 
	initHandlerExceptionResolvers(context);
	// 初始化 RequestToViewNameTranslator
	initRequestToViewNameTranslator(context);
	// 初始化 ViewResolvers
	initViewResolvers(context);
	// 初始化 FlashMapManager
	initFlashMapManager(context);
}

一次请求会通过DispatcherServlet#doDispatch(HttpServletRequest request, HttpServletResponse response) 方法,执行请求的分发

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				// 获得请求对应的 HandlerExecutionChain 对象
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				// 获得当前 handler 对应的 HandlerAdapter 对象
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

Spring MVC是通过处理器适配器来进行具体方法的调用执行的,这时候来到适配器RequestMappingHandlerAdapter

@Override
public void afterPropertiesSet() {
	// Do this first, it may add ResponseBody advice beans
	//  初始化 ControllerAdvice 相关
	initControllerAdviceCache();

	// 初始化 argumentResolvers 属性
	if (this.argumentResolvers == null) {
		List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
		this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	// 初始化 initBinderArgumentResolvers 属性
	if (this.initBinderArgumentResolvers == null) {
		List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
		this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	// 初始化 returnValueHandlers 属性
	if (this.returnValueHandlers == null) {
		List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
		this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
	}
}
private void initControllerAdviceCache() {
	if (getApplicationContext() == null) {
		return;
	}

	// <1> 扫描 @ControllerAdvice 注解的 Bean 们,并将进行排序
	List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
	AnnotationAwareOrderComparator.sort(adviceBeans);

	List<Object> requestResponseBodyAdviceBeans = new ArrayList<>();

	//  遍历 ControllerAdviceBean 数组
	for (ControllerAdviceBean adviceBean : adviceBeans) {
		Class<?> beanType = adviceBean.getBeanType();
		if (beanType == null) {
			throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
		}
		// 扫描有 @ModelAttribute ,无 @RequestMapping 注解的方法,添加到 modelAttributeAdviceCache 中
		Set<Method> attrMethods = MethodIntrospector.selectMethods(beanType, MODEL_ATTRIBUTE_METHODS);
		if (!attrMethods.isEmpty()) {
			this.modelAttributeAdviceCache.put(adviceBean, attrMethods);
		}
		// 扫描有 @InitBinder 注解的方法,添加到 initBinderAdviceCache 中
		Set<Method> binderMethods = MethodIntrospector.selectMethods(beanType, INIT_BINDER_METHODS);
		if (!binderMethods.isEmpty()) {
			this.initBinderAdviceCache.put(adviceBean, binderMethods);
		}
		// 如果是 RequestBodyAdvice 或 ResponseBodyAdvice 的子类,添加到 requestResponseBodyAdviceBeans 中
		if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {
			requestResponseBodyAdviceBeans.add(adviceBean);
		}
		if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
			requestResponseBodyAdviceBeans.add(adviceBean);
		}
	}

	// 将 requestResponseBodyAdviceBeans 添加到 this.requestResponseBodyAdvice 属性种
	if (!requestResponseBodyAdviceBeans.isEmpty()) {
		this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
	}	
}

这就是@ControllerAdvice的实现原理底层分析咯。

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

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

相关文章

前端开发接单公司做到哪些点,客户才愿意把项目包给你。

作为前端外包接单公司&#xff0c;你知道客户选择和你合作都看中哪些因素吗&#xff1f;单纯是价格吗&#xff1f;未必&#xff0c;本位给大家列举7个要素&#xff0c;并对每个要素做了定位&#xff0c;大家查缺补漏吧。 作为前端外包接单公司&#xff0c;要吸引同行客户将前端…

优秀的“抗霾”神器:气膜体育馆—轻空间

随着空气污染问题日益严重&#xff0c;尤其是雾霾天气频发&#xff0c;体育运动的场地环境质量受到越来越多的关注。气膜体育馆作为一种新型的体育场馆解决方案&#xff0c;以其独特的设计和多重优势&#xff0c;成为了优秀的“抗霾”神器。轻空间将深入探讨气膜体育馆的特点和…

pycharm不能安装包的解决方法

一直使用VScode写python&#xff0c;最近使用pycharm&#xff0c;但是pycharm不能安装包&#xff0c;类似这种 后面直接使用ALT F12跳转终端&#xff1a; pip install 需要添加的包 -i https://pypi.tuna.tsinghua.edu.cn/simple不报错了

Gitee 的公钥删不掉

公钥管理里已经没有公钥了&#xff0c; 仓库里还有&#xff0c;这是怎么回事&#xff1f; 这两个好像又没什么关系。 那为啥要搞两处呢&#xff1f; 个人信息里的公钥一直就没有仓库里使用的公钥&#xff0c; 删掉个人信息里的也没什么影响。 在仓库管理页面导入新公钥提示已…

【shell脚本速成】mysql备份脚本

文章目录 案例需求脚本应用场景&#xff1a;解决问题脚本思路实现代码 &#x1f308;你好呀&#xff01;我是 山顶风景独好 &#x1f388;欢迎踏入我的博客世界&#xff0c;能与您在此邂逅&#xff0c;真是缘分使然&#xff01;&#x1f60a; &#x1f338;愿您在此停留的每一刻…

大数据集群数据传输

简单的服务器间的通信示例 netcat&#xff0c;简写为 nc&#xff0c;是 unix 系统下一个强大的命令行网络通信工具&#xff0c;用于在两台主机之间建立 TCP 或者 UDP 连接&#xff0c;并提供丰富的命令进行数据通信。nc 在网络参考模型属于应用层。使用 nc 可以做很多事情&…

探索Elastic Search:强大的开源搜索引擎,详解及使用

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 引入 全文搜索属于最常见的需求&#xff0c;开源的 Elasticsearch &#xff08;以下简称 Elastic&#xff09;是目前全文搜索引…

如何使用手机号查快递?2个方法,包裹信息全掌握

无论是网购、亲友间寄送礼物还是工作中的文件传递&#xff0c;快递都扮演着至关重要的角色。然而&#xff0c;有时候我们可能会忘记自己的快递单号&#xff0c;或者在收到快递时没有留意保存相关信息。 这时候&#xff0c;如果能通过手机号查询快递&#xff0c;无疑会大大方便…

【LLM之KG】CoK论文阅读笔记

研究背景 大规模语言模型&#xff08;LLMs&#xff09;在许多自然语言处理&#xff08;NLP&#xff09;任务中取得了显著进展&#xff0c;特别是在零样本/少样本学习&#xff08;In-Context Learning, ICL&#xff09;方面。ICL不需要更新模型参数&#xff0c;只需利用几个标注…

Spire.PDF for .NET【文档操作】演示:设置 PDF 文档的 XMP 元数据

XMP 是一种文件标签技术&#xff0c;可让您在内容创建过程中将元数据嵌入文件本身。借助支持 XMP 的应用程序&#xff0c;您的工作组可以以团队以及软件应用程序、硬件设备甚至文件格式易于理解的格式捕获有关项目的有意义的信息&#xff08;例如标题和说明、可搜索的关键字以及…

无源编缆测尺助力料场实现自动化堆取料作业

随着工业4.0时代的到来&#xff0c;智能化、无人化成为现代工业发展的重要趋势。在港口码头、钢铁冶金、焦化等高耗能行业中&#xff0c;如何实现物料的精准测量与无人化操作&#xff0c;成为企业提高生产效率、降低人工成本的关键。武汉市微深节能科技有限公司凭借其先进的分段…

如何配置taro

文章目录 step1. 全局安装wepacksetp2. 使用npm安装tarostep3. 项目初始化可能出现的问题 使用taro时需要在本地配置好nodejs环境&#xff0c;关于如何配置nodejs可参考我的这篇博文 如何配置nodejs环境 step1. 全局安装wepack 使用指令npm install webpack -g即可 安装完成…

电脑不小心删除的文件怎么恢复?4个必备恢复方法!

“刚刚在对电脑里的某些垃圾文件进行清理时&#xff0c;我一不小心误删了比较重要的数据。这些误删的数据还有机会恢复吗&#xff1f;希望大家帮帮我&#xff0c;非常感谢&#xff01;” 在这个数字化飞速发展的时代&#xff0c;电脑早已成为我们日常生活和工作中不可或缺的一部…

【arm扩容】docker load -i tar包 空间不足

背景&#xff1a; 首先我在/home/nvidia/work下导入了一些镜像源码tar包。然后逐个load进去。当我 load -i dev-aarch64-18.04-20210423_2000.tar包的时候&#xff0c;出现 Error processing tar file(exit status 1): write /9818cf5a7cbd5a828600d9a4d4e62185a7067e2a6f2ee…

如何解决app广告填充率低、广告填充异常,提升广告变现收益?

APP广告变现有助于开发者获得持续的收益来源&#xff0c;由于广告链路的封闭性和复杂化&#xff0c;一旦出现请求配置参数错误、返回广告源信息缺失、素材被拦截等异常&#xff0c;大部分开发者很难及时查清异常情况&#xff0c;导致广告填充率不理想&#xff0c;甚至填充率常常…

KUBIKOS - Cube Monsters

KUBIKOS - Cube Monsters 是一系列 18 个不同的可爱低多边形移动友好怪物角色!每个角色都有自己的动画集。(移动、空闲、攻击、击中、跳跃等)。 +URP支持+18种不同的动物! + 低多边形(400~900个三角形) + 操纵和动画! + 4096x4096 纹理图集 + Mecanim 准备就绪! + 移动…

【第十三课】区域经济可视化表达——符号表达与标注

一、前言 地图最直接的表达就是使用符号表达。使用符号可以把简单的点线面要 素渲染成最直观的地理符号&#xff0c;提高地图的可读性。只要掌握了 ArcGIS 符号制 作的技巧&#xff0c;分析符号并总结出规则&#xff0c;就可以制作符合要求的地图符号。 &#xff08;一&#…

关于正点原子stm32f103精英板v1的stlink通信失败问题解决方案

由于最新的固件不适配&#xff0c;我们要想其工作要下载007的固件。 https://www.st.com/en/development-tools/stsw-link007.html?dlredirect 版本选择最低的。然后选择windows文件夹&#xff0c;更新程序 然后进keil就能正常识别到了

在线装修管理系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;管理员管理&#xff0c;装修队管理&#xff0c;用户管理&#xff0c;装修管理&#xff0c;基础数据管理&#xff0c;论坛管理 前台账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;…

不要升级mmkv1.3.5

腾讯有点不负责任的感觉。1.3.5开始直接ban掉了v1.3.5 / 2024-04-24 Drop armv7 & x86 support.&#xff0c;x86和v7a的支持&#xff08;大概率是这个原因&#xff09;。 从打包后的包解压可以看到&#xff0c;只有arm64-v8a和x64目录里面有库。而1.3.4打包解压后&#x…