当前位置: 首页 > news >正文

Java异常体系、UncaughtExceptionHandler、Spring MVC统一异常处理、Spring Boot统一异常处理

概述

在这里插入图片描述
所有异常都是继承自java.lang.Throwable类,Throwable有两个直接子类,Error和Exception。

Error用来表示程序底层或硬件有关的错误,这种错误和程序本身无关,如常见的NoClassDefFoundError。这种异常和程序本身无关,不需要检查,属于非受检异常。

Exception表示程序异常,可能是由于程序不严谨导致的,如NPE空指针异常。Exception下面派生RuntimeException和其他异常,其中RuntimeException表示运行时异常,也属于非受检异常。在编译时可以不需要强制检查的异常,不需要显式捕捉或抛出。

除Error和RuntimeException及派生类以外,其他异常都属于受检异常,如IOException、SQLException。在编译时强制进行检查的异常,这种异常需要显式的通过try/catch来捕捉,或通过throws抛出去,否则程序无法通过编译。设计强制检查的异常(受检异常),主要原因是考虑到程序的正确性、稳定性和可靠性。
在这里插入图片描述

try…catch…finally语句块

初中级笔试题可能会出现的知识点。这里直接给出一些结论:

  • 受检异常,需要使用try来包裹可能会抛出异常的代码块,catch用于捕获异常并处理异常的代码块,常见的处理策略包括:打印错误日志、抛出自定义业务异常、释放资源、设置局部变量等
  • 受检异常,还可以直接在方法签名上throws Exception,抛给方法调用者来处理。业务开发中,通常在Service层抛出自定义业务异常,然后在Controller层统一捕获异常并返回errCode和errMsg
  • 不管有没有出现异常,finally仍然会执行
  • 当try和catch中有return时,finally仍然会执行
  • finally常用于释放IO资源、(分布式)锁的持有、

常见异常

初中级Java开发工程师面试中,经常会遇到的一个问题:说说你工作中经常遇到的异常?

面试官指的应该包括Exception和Error,回答问题时,不能只列举Exception。

简单列举Exception如下:

  • NullPointerException:简称NPE。多少人栽在NPE上,多少资金损失是因为NPE。减少(无法杜绝)NPE的方法就是不停地空判断,或使用Optional类。可喜的是,升级到JDK 14以上版本,发生NPE时,JVM会打印具体哪个方法抛的空指针异常,避免同一行代码多个函数调用时无法判断具体是哪个函数抛异常的困扰,方便异常排查;
  • ConcurrentModificationException:简称CME。当有多个迭代器同时遍历和修改Java集合(如ArrayList或HashMap),就有可能抛出CME异常。避免出现CME异常的措施如:加锁,使用CopyOnWriteArrayList,ConcurrentHashMap等集合。
  • IndexOutOfBoundsException:索引越界,实现类有两个ArrayIndexOutOfBoundsException和StringIndexOutOfBoundsException。
  • ClassCastException:类型转换失败。
  • ClassNotFoundException:参考Java学习之NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError

简单列举Error如下:

  • OutOfMemoryError:OOM,报错信息为:java.lang.OutOfMemoryError:Java heap spacess。遇到OOM时,需要先分清楚是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
  • StackOverflowError:栈溢出。栈溢出的原因:递归调用(如求解斐波那契数列问题时),大量循环或死循环,全局变量过多,数组、List、Map数据过大。
  • NoClassDefFoundError:找不到类定义
  • NoSuchMethodError:找不到方法
  • NoSuchFieldError:找不到字段,上面这三种一般都是三方依赖冲突,通过使用maven工具来排查,如mvn dependency:tree > tmp.txt,或使用IDEA的Maven Helper插件

最佳实践

即所谓的Best Practice:

  • 在finally中清理资源;
  • 坚决要杜绝捕获异常后不做任何处理,即catch语句块为空;
  • 捕获异常后的日志打印规范,如记录错误类和方法,记录详细的错误堆栈stacktrace方便排查问题;
  • 使用Try-With-Resource语句,实现AutoCloseable接口的资源;
  • 优先捕获特定的异常,其次再考虑其父类异常;
  • 多使用自定义业务异常,一个异常对应有一个errCode和一个可读性良好的errMsg

进阶

异常表

在JVM中,异常处理不是由字节码指令(早期使用jsr、ret指令)来实现的,而是异常表。

如果一个方法定义有try-catch或try-finally,则会创建异常表,保存异常处理信息:

  • 起始位置
  • 结束位置
  • 程序计数器记录的代码处理的偏移地址
  • 被捕获的异常类在常量池中的索引

Exception table:

Exception table:from    to  target type0    12    15   Class java/lang/Exception

根据不同的type对应到不同的target上。在操作系统里,这个target也称为异常处理程序。就是特定问题出现时,去异常表查询这个问题对应的是哪个处理程序,然后去执行这个程序,完成异常处理。

面试可能会遇到的问题:finally为什么一定会执行?
查看编译后的字节码,可发现编译器把finally语句块里面的代码分别复制到try和catch语句块里面。

异常throw事件

jvmti中提供两个异常的事件,一个是包含throw和catch,一个是catch。选择功能多的那个方便一点。

void JNICALL Exception(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jmethodID method, jlocation location, jobject exception, jmethodID catch_method, jlocation catch_location)

通过方法签名,可以知道异常的线程,出异常的方法,行号,异常对象,catch的方法和行号。这里由于是触发的throw事件,所以如果只是new Exception的操作是不会触发事件的。有些代码通过创建Exception或Error来控制逻辑,只要不是throw,catch的这种逻辑,这里是检测不到的。如果异常只throw没有catch的话,catch的字段就是空的。

拓展

UncaughtExceptionHandler

在虚拟机中,当一个线程没有显式处理(即try catch)异常而抛出时,会将该异常事件报告给该线程对象的java.lang.Thread.UncaughtExceptionHandler进行处理,如果线程没有设置UncaughtExceptionHandler,则默认会把异常栈信息输出到终端而使程序直接崩溃。所以如果想在线程意外崩溃时做一些处理就可以通过实现UncaughtExceptionHandler来满足需求。

public class Thread {/*** 当一个线程因未捕获的异常而即将终止时虚拟机将使用 Thread.getUncaughtExceptionHandler()* 获取已经设置的 UncaughtExceptionHandler 实例,并通过调用其 uncaughtException(...) 方法而传递相关异常信息。* 如果一个线程没有明确设置其 UncaughtExceptionHandler,则将其 ThreadGroup 对象作为其handler,如果 ThreadGroup 对象对异常没有什么特殊的要求,则 ThreadGroup 会将调用转发给默认的未捕获异常处理器(即 Thread 类中定义的静态未捕获异常处理器对象)。*/@FunctionalInterfacepublic interface UncaughtExceptionHandler {/*** 未捕获异常崩溃时回调此方法*/void uncaughtException(Thread t, Throwable e);}/*** 静态方法,用于设置一个默认的全局异常处理器*/public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("setDefaultUncaughtExceptionHandler"));}defaultUncaughtExceptionHandler = eh;}/*** 针对某个Thread对象的方法,用于对特定的线程进行未捕获的异常处理*/public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {checkAccess();uncaughtExceptionHandler = eh;}/*** 当Thread崩溃时会调用该方法获取当前线程的 handler,获取不到就会调用 group(handler 类型)。* group是Thread类的ThreadGroup类型属性,在Thread构造中实例化*/public UncaughtExceptionHandler getUncaughtExceptionHandler() {if (isTerminated()) {// uncaughtExceptionHandler may be set to null after thread terminatesreturn null;} else {UncaughtExceptionHandler ueh = uncaughtExceptionHandler;return (ueh != null) ? ueh : getThreadGroup();}}/*** 线程全局默认handler*/public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() {return defaultUncaughtExceptionHandler;}
}

线程崩溃时异常抛出的顺序:

  • 先调用Thread.getUncaughtExceptionHandler()查看是否有自己对象特有的handler,如果有就直接处理
  • 如果没有就调用ThreadGroup(UncaughtExceptionHandler的默认实现类)
  • 如果ThreadGroup没啥特殊处理就会继续调用Thread.getDefaultUncaughtExceptionHandler()获取handler进行处理
  • 如果默认handler也没有处理就直接执行正常的异常流程使程序崩溃。

ThreadGroup核心实现源码:

// ThreadGroup在Thread对象构造方法中实例化
public class ThreadGroup implements Thread.UncaughtExceptionHandler {public void uncaughtException(Thread t, Throwable e) {// parent默认是nullif (parent != null) {parent.uncaughtException(t, e);} else {// 一般走进来,调用Thread.setDefaultUncaughtExceptionHandler(...)方法设置全局 handler进行处理Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler();if (ueh != null) {ueh.uncaughtException(t, e);} else if (!(e instanceof ThreadDeath)) {// 全局handler也不存在就输出异常栈System.err.print("Exception in thread \"" + t.getName() + "\" ");e.printStackTrace(System.err);}}}
}

Spring MVC异常处理机制

参考Spring MVC系列之九大核心组件中的HandlerExceptionResolver部分。

Spring MVC全局异常处理

每个Controller层里的方法都需要进行异常捕获及处理,显然太繁琐且效率低。

自定义类并实现HandlerExceptionResolver接口并重写resolveException方法进行全局异常处理:

@Slf4j
@Component
public class SimpleExceptionResolver implements HandlerExceptionResolver {@Overridepublic ModelAndView resolveException(HttpServletRequest request, @NonNull HttpServletResponse response, Object object, @NonNull Exception e) {// 业务异常对前端可见,否则统一归为系统异常Map<String, Object> map = new HashMap<>();map.put("success", false);// 自定义业务异常,可多次if判断对应多个异常类型,当然也可使用switch语句if (e instanceof BusinessException) {map.put("errorMsg", e.getMessage());} else {map.put("errorMsg", "system exception");}log.error(e.getMessage(), e);// 此处返回ModelandView对象,如error.jsp页面,也可考虑使用其他的模板引擎,如FreeMarker,Thymeleafreturn new ModelAndView("/error", map);}
}

可以以不同的方式将异常结果返回给调用者(前端或其他后端服务)

  • 返回ModelAndView
  • 返回页面的地址
  • 返回JSON
  • 返回HTTP错误码

当然也可以使用下面Spring Boot全局异常处理方案。

Spring Boot全局异常处理

直接给出配置类:

@Slf4j
// 复合注解 = @ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalExceptionHandler {// 别的方法都处理不了的异常@ExceptionHandler(Exception.class)public Response<Object> otherExceptionHandler(HttpServletResponse response, Exception ex) {response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());log.error(ex.getMessage(), ex);return Response.error("服务器内部异常!");}// 可捕获自定义异常、JDK或Spring异常,支持数组形式捕获多个不同类型的异常,但推荐一种异常对应一个方法@ExceptionHandler({ForbiddenException.class}) // 自定义业务异常// @ExceptionHandler({IllegalArgumentException.class}) // JDK异常// @ExceptionHandler(HttpMessageNotReadableException.class) // Spring异常// 返回Response Status Code@ResponseStatus(HttpStatus.FORBIDDEN)public Response<Object> forbidden(ForbiddenException e) {// 记录错误日志log.error(e.getMessage(), e);return Response.error(e.getMessage());}// 前端(或接口攻击者)使用非法的@RequestBody请求接口,解析异常字段,并将错误日志降级@ExceptionHandler(MethodArgumentNotValidException.class)public Response<Object> validationBodyException(MethodArgumentNotValidException exception) {BindingResult result = exception.getBindingResult();StringBuilder errorMsg = new StringBuilder();if (result.hasErrors()) {List<ObjectError> errors = result.getAllErrors();errors.forEach(p -> {FieldError fieldError = (FieldError) p;errorMsg.append(fieldError.getDefaultMessage()).append("!");// 设置warn而不是error,日志错误降级log.warn("Data check failure : object{" + fieldError.getObjectName() + "},field{" + fieldError.getField() + "},errorMessage{" + fieldError.getDefaultMessage() + "}");});}return Response.error(errorMsg.toString());}
}

Response是自定义的数据统一返回格式:

@Data
@NoArgsConstructor
public class Response<T> implements Serializable {private int code;private String msg;private T data;// 省略其他包装方法 
}

Dubbo处理异常

分布式调用链

参考

  • 谈谈异常
  • 从JVM角度理解try…catch
  • 利用jvmti查看java异常
  • UncaughtExceptionHandler相关问题解析

相关文章:

Java异常体系、UncaughtExceptionHandler、Spring MVC统一异常处理、Spring Boot统一异常处理

概述 所有异常都是继承自java.lang.Throwable类&#xff0c;Throwable有两个直接子类&#xff0c;Error和Exception。 Error用来表示程序底层或硬件有关的错误&#xff0c;这种错误和程序本身无关&#xff0c;如常见的NoClassDefFoundError。这种异常和程序本身无关&#xff0…...

bash终端快捷键

快捷键作用ShiftCtrlC复制ShiftCtrlV粘贴CtrlAltT新建终端ShiftPgUp/PgDn终端上下翻页滚动CtrlC终止命令CtrlD关闭终端CtrlA光标移动到最开始为止CtrlE光标移动到最末尾CtrlK删除此处到末尾的所有内容CtrlU删除此处至开始的所有内容CtrlD删除当前字符CtrlH删除当前字符的前一个…...

【Visual Studio】Visual Studio报错合集及解决办法

目录 Visual Studio报错:error LNK2001 Visual Studio报错:error C2061 Visual Studio报错:error C1075 Visual Studio报错:error C4430 Visual Studio报错error C3867 概述 持续更细Visual Studio报错及解决方法 Visual Studio报错:error LNK2001 问题 : error LNK2001…...

【微信小程序知识点】转发功能的实现

转发功能&#xff0c;主要帮助用户更流畅地与好友分享内容与服务。 想实现转发功能&#xff0c;有两种方式&#xff1a; 1.页面js文件必须声明onShareAppMessage事件监听函数&#xff0c;并自定义转发内容。只有定义了此事件处理函数&#xff0c;右上角菜单才会显示“转发”按…...

用python识别二维码(python实例二十三)

目录 1.认识Python 2.环境与工具 2.1 python环境 2.2 Visual Studio Code编译 3.识别二维码 3.1 代码构思 3.2 代码实例 3.3 运行结果 4.总结 1.认识Python Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。 Python 的设计具有很强的可读性&…...

电脑文件夹怎么设置密码?让你的文件更安全!

在日常使用电脑的过程中&#xff0c;我们常常会有一些需要保护的个人文件或资料。为了防止这些文件被他人未经授权访问&#xff0c;对重要文件夹设置密码是一种有效的保护措施&#xff0c;可是电脑文件夹怎么设置密码呢&#xff1f;本文将介绍2种简单有效的方法帮助您为电脑文件…...

paddla模型转gguf

在使用ollama配置本地模型时&#xff0c;只支持gguf格式的模型&#xff0c;所以我们首先需要把自己的模型转化为bin格式&#xff0c;本文为paddle&#xff0c;onnx&#xff0c;pytorch格式的模型提供说明&#xff0c;safetensors格式比较简单请参考官方文档&#xff0c;或其它教…...

Memcached vs Redis——Java项目缓存选择

在Java项目开发中&#xff0c;缓存系统作为提升性能、优化资源利用的关键技术之一&#xff0c;扮演着至关重要的角色。Memcached和Redis作为两种流行的缓存解决方案&#xff0c;各有其独特的优势和应用场景。本文旨在通过分析项目大小、用户访问量、业务复杂度以及服务器部署情…...

大模型最新黑书:基于GPT-3、ChatGPT、GPT-4等Transformer架构的自然语言处理 PDF

今天给大家推荐一本丹尼斯罗斯曼(Denis Rothman)编写的关于大语言模型&#xff08;LLM&#xff09;权威教程<<大模型应用解决方案> 基于GPT-3、ChatGPT、GPT-4等Transformer架构的自然语言处理>&#xff01;Google工程总监Antonio Gulli作序&#xff0c;这含金量不…...

【电子数据取证】电子数据司法鉴定

文章关键词&#xff1a;电子数据取证、司法鉴定服务、司法鉴定流程 一、定义 什么是司法鉴定&#xff1f; 在诉讼活动中鉴定人运用科学技术或者专业知识对诉讼涉及的专门性问题进行鉴别和判断并提供鉴定意见的活动。 电子数据司法鉴定 那么电子数据司法鉴定&#xff0c;就…...

使用 OpenCV 的 inRange 函数进行颜色分割

使用 OpenCV 的 inRange 函数进行颜色分割 在图像处理领域&#xff0c;颜色分割是一个常见的任务&#xff0c;常用于识别和提取图像中的特定颜色区域。OpenCV 提供了一个非常方便的函数 inRange 来实现这一功能。在这篇博客中&#xff0c;我们将详细介绍 inRange 函数的用法&a…...

OpenAI终止对中国提供API服务,对国内AI市场产生重大冲击?

6月25日&#xff0c;OpenAI突然宣布终止向包括中国在内的国家地区提供API服务&#xff0c;本月9日这一政策已经正式生效了&#xff01; 有人说&#xff0c;这个事件给中国AI行业带来很大冲击&#xff01;是这样吗&#xff1f;在展开讨论前&#xff0c;我们先来看看什么是API服务…...

JavaDS —— 栈 Stack 和 队列 Queue

栈的概念 栈是一种先进后出的线性表&#xff0c;只允许在固定的一端进行插入和删除操作。 进行插入和删除操作的一端被称为栈顶&#xff0c;另一端被称为栈底 栈的插入操作叫做进栈/压栈/入栈 栈的删除操作叫做出栈 现实生活中栈的例子&#xff1a; 栈的模拟实现 下面是Jav…...

C++进阶:继承和多态

文章目录 ❤️继承&#x1fa77;继承与友元&#x1f9e1;继承和静态成员&#x1f49b;菱形继承及菱形虚拟继承&#x1f49a;继承和组合 ❤️多态&#x1fa77;什么是多态&#xff1f;&#x1f9e1;多态的定义以及实现&#x1f49b;虚函数&#x1f49a;虚函数的重写&#x1f499…...

【八大排序】java版(上)(冒泡、快排、堆排、选择排序)

文章目录 一、冒泡排序(重点)思路代码 二、快排(面试重点)思路代码 三、堆排序(面试重点)思路代码 四、选择排序思路代码 一、冒泡排序(重点) 思路 前后两两数据进行比较&#xff0c;小的数据往前走&#xff0c;大的数据往后走&#xff0c;每一轮结束之后&#xff0c;最大的数…...

.Net Core 微服务之Consul(二)-集群搭建

引言: 集合上一期.Net Core 微服务之Consul(一)(.Net Core 微服务之Consul(一)-CSDN博客) 。 目录 一、 Consul集群搭建 1. 高可用 1.1 高可用性概念 1.2 高可用集群的基本原理 1.3 高可用集群的架构设计 1.3.1 主从复制架构 1.3.2 共享存储架构 1.3.3 负载均衡…...

C++ --> 类和对象(二)

前言 在前面简单的介绍了OOP&#xff0c;什么是类&#xff0c;在类中的this指针。接下来就深入理解类和对象。 默认成员函数 默认构造函数&#xff1a;用于在创建对象时初始化对象的成员变量。默认拷贝构造函数&#xff1a;用于使用已存在的对象来初始化新创建的对象。默认析构…...

利用宝塔安装一套linux开发环境

更新yum&#xff0c;并且更换阿里镜像源 删除yum文件 cd /etc/yum.repos.d/ 进入yum核心目录 ls sun.repo rm -rf * 删除之前配置的本地源 ls 配置阿里镜像源 wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo 配置扩展包 wge…...

VB 实例:掌握 Visual Basic 编程的精髓

VB 实例:掌握 Visual Basic 编程的精髓 引言 Visual Basic(简称VB)是一种由微软开发的高级编程语言,它结合了易于使用的界面和强大的编程功能,使得初学者和专业人士都能快速开发Windows桌面应用程序。本文将通过一系列实例,深入探讨VB编程的基础知识和高级技巧,帮助读…...

层次分析法:matlab代码实现

计算权重&#xff1a; 一、算术平均法 关于矩阵&#xff1a; 1、矩阵的输入写法 [ ; ; ]同行用空格或逗号隔开&#xff0c;不同行用分号间隔 2、矩阵求和 默认按列求和 asum(E) 等同于 asum(E,1) 得到行向量 按行求和 asum(E,2) 得到列向量 对整个矩阵求和 asum(E,"all&…...

【kafka】Golang实现分布式Masscan任务调度系统

要求&#xff1a; 输出两个程序&#xff0c;一个命令行程序&#xff08;命令行参数用flag&#xff09;和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽&#xff0c;然后将消息推送到kafka里面。 服务端程序&#xff1a; 从kafka消费者接收…...

三维GIS开发cesium智慧地铁教程(5)Cesium相机控制

一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点&#xff1a; 路径验证&#xff1a;确保相对路径.…...

Oracle查询表空间大小

1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

渲染学进阶内容——模型

最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...

Nginx server_name 配置说明

Nginx 是一个高性能的反向代理和负载均衡服务器&#xff0c;其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机&#xff08;Virtual Host&#xff09;。 1. 简介 Nginx 使用 server_name 指令来确定…...

【决胜公务员考试】求职OMG——见面课测验1

2025最新版&#xff01;&#xff01;&#xff01;6.8截至答题&#xff0c;大家注意呀&#xff01; 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:&#xff08; B &#xff09; A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…...

深入解析C++中的extern关键字:跨文件共享变量与函数的终极指南

&#x1f680; C extern 关键字深度解析&#xff1a;跨文件编程的终极指南 &#x1f4c5; 更新时间&#xff1a;2025年6月5日 &#x1f3f7;️ 标签&#xff1a;C | extern关键字 | 多文件编程 | 链接与声明 | 现代C 文章目录 前言&#x1f525;一、extern 是什么&#xff1f;&…...

【7色560页】职场可视化逻辑图高级数据分析PPT模版

7种色调职场工作汇报PPT&#xff0c;橙蓝、黑红、红蓝、蓝橙灰、浅蓝、浅绿、深蓝七种色调模版 【7色560页】职场可视化逻辑图高级数据分析PPT模版&#xff1a;职场可视化逻辑图分析PPT模版https://pan.quark.cn/s/78aeabbd92d1...

基于 TAPD 进行项目管理

起因 自己写了个小工具&#xff0c;仓库用的Github。之前在用markdown进行需求管理&#xff0c;现在随着功能的增加&#xff0c;感觉有点难以管理了&#xff0c;所以用TAPD这个工具进行需求、Bug管理。 操作流程 注册 TAPD&#xff0c;需要提供一个企业名新建一个项目&#…...