JAVA-编程基础-08-Java异常处理全面解析

news2025/1/10 17:07:41

Lison <dreamlison@163.com>, v1.0.0, 2023.04.01

JAVA-编程基础-08-Java异常处理全面解析

文章目录

  • JAVA-编程基础-08-Java异常处理全面解析
    • 什么是异常
    • Exception和Error的区别
    • checked和unchecked异常”
    • 关于 throw 和 throws
    • 关于 try-catch-finally
    • 小结
  • try-with-resource
  • 异常处理的20个实践
    • 尽量不要捕获 RuntimeException
    • 尽量使用 try-with-resource 来关闭资源
    • 不要捕获 Throwable
    • 不要省略异常信息的记录
    • 不要记录了异常又抛出了异常
    • 不要在 finally 块中使用 return
    • 抛出具体定义的检查性异常而不是 Exception
    • 捕获具体的子类而不是捕获 Exception 类
    • 自定义异常时不要丢失堆栈跟踪
    • finally 块中不要抛出任何异常
    • 不要在生产环境中使用 `printStackTrace()`
    • 、对于不打算处理的异常,直接使用 try-finally,不用 catch
    • 记住早 throw 晚 catch 原则
    • 只抛出和方法相关的异常
    • 切勿在代码中使用异常来进行流程控制
    • 尽早验证用户输入以在请求处理的早期捕获异常
    • 一个异常只能包含在一个日志中
    • 将所有相关信息尽可能地传递给异常
    • 终止掉被中断线程
    • 对于重复的 try-catch,使用模板方法

什么是异常

“那到底什么是异常呢?”。

“异常是指中断程序正常执行的一个不确定的事件。当异常发生时,程序的正常执行流程就会被打断。一般情况下,程序都会有很多条语句,如果没有异常处理机制,前面的语句一旦出现了异常,后面的语句就没办法继续执行了。”

“有了异常处理机制后,程序在发生异常的时候就不会中断,我们可以对异常进行捕获,然后改变程序执行的流程。”

“除此之外,异常处理机制可以保证我们向用户提供友好的提示信息,而不是程序原生的异常信息——用户根本理解不了。”

“不过,站在开发者的角度,我们更希望看到原生的异常信息,因为这有助于我们更快地找到 bug 的根源,反而被过度包装的异常信息会干扰我们的视线。”

“Java 语言在一开始就提供了相对完善的异常处理机制,这种机制大大降低了编写可靠程序的门槛,这也是 Java 之所以能够流行的原因之一。”

“那导致程序抛出异常的原因有哪些呢?”三妹问。

比如说:

  • 程序在试图打开一个不存在的文件;
  • 程序遇到了网络连接问题;
  • 用户输入了糟糕的数据;
  • 程序在处理算术问题时没有考虑除数为 0 的情况;

等等等等。

挑个最简单的原因来说吧。

public class Demo {
    public static void main(String[] args) {
        System.out.println(10/0);
    }
}

这段代码在运行的时候抛出的异常信息如下所示:

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.itwanger.s41.Demo.main(Demo.java:8)

“你看,三妹,这个原生的异常信息对用户来说,显然是不太容易理解的,但对于我们开发者来说,简直不要太直白了——很容易就能定位到异常发生的根源。”

Exception和Error的区别

从单词的释义上来看,error 为错误,exception 为异常,错误的等级明显比异常要高一些。

从程序的角度来看,也的确如此。

Error 的出现,意味着程序出现了严重的问题,而这些问题不应该再交给 Java 的异常处理机制来处理,程序应该直接崩溃掉,比如说 OutOfMemoryError,内存溢出了,这就意味着程序在运行时申请的内存大于系统能够提供的内存,导致出现的错误,这种错误的出现,对于程序来说是致命的。

Exception 的出现,意味着程序出现了一些在可控范围内的问题,我们应当采取措施进行挽救。

比如说之前提到的 ArithmeticException,很明显是因为除数出现了 0 的情况,我们可以选择捕获异常,然后提示用户不应该进行除 0 操作,当然了,更好的做法是直接对除数进行判断,如果是 0 就不进行除法运算,而是告诉用户换一个非 0 的数进行运算。

checked和unchecked异常”

checked 异常(检查型异常)在源代码里必须显式地捕获或者抛出,否则编译器会提示你进行相应的操作;而 unchecked 异常(非检查型异常)就是所谓的运行时异常,通常是可以通过编码进行规避的,并不需要显式地捕获或者抛出。

“我先画一幅思维导图给你感受一下。”

在这里插入图片描述

首先,Exception 和 Error 都继承了 Throwable 类。换句话说,只有 Throwable 类(或者子类)的对象才能使用 throw 关键字抛出,或者作为 catch 的参数类型。

面试中经常问到的一个问题是,NoClassDefFoundError 和 ClassNotFoundException 有什么区别?

它们都是由于系统运行时找不到要加载的类导致的,但是触发的原因不一样。

  • NoClassDefFoundError:程序在编译时可以找到所依赖的类,但是在运行时找不到指定的类文件,导致抛出该错误;原因可能是 jar 包缺失或者调用了初始化失败的类。
  • ClassNotFoundException:当动态加载 Class 对象的时候找不到对应的类时抛出该异常;原因可能是要加载的类不存在或者类名写错了。

其次,像 IOException、ClassNotFoundException、SQLException 都属于 checked 异常;像 RuntimeException 以及子类 ArithmeticException、ClassCastException、ArrayIndexOutOfBoundsException、NullPointerException,都属于 unchecked 异常。

unchecked 异常可以不在程序中显示处理,就像之前提到的 ArithmeticException 就是的;但 checked 异常必须显式处理。

比如说下面这行代码:

Class clz = Class.forName("com.cundream.s41.Demo1");

如果没做处理,比如说在 Intellij IDEA 环境下,就会提示你这行代码可能会抛出 java.lang.ClassNotFoundException

在这里插入图片描述

建议你要么使用 try-catch 进行捕获:

try {
    Class clz = Class.forName("com.cundream..s41.Demo1");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

注意打印异常堆栈信息的 printStackTrace() 方法,该方法会将异常的堆栈信息打印到标准的控制台下,如果是测试环境,这样的写法还 OK,如果是生产环境,这样的写法是不可取的,必须使用日志框架把异常的堆栈信息输出到日志系统中,否则可能没办法跟踪。

要么在方法签名上使用 throws 关键字抛出:

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class clz = Class.forName("com.cundream..s41.Demo1");
    }
}

这样做的好处是不需要对异常进行捕获处理,只需要交给 Java 虚拟机来处理即可;坏处就是没法针对这种情况做相应的处理。

checked 异常在业界是有争论的,它假设我们捕获了异常,并且针对这种情况作了相应的处理,但有些时候,根本就没法处理。”我说,“就拿上面提到的 ClassNotFoundException 异常来说,我们假设对其进行了 try-catch,可真的出现了 ClassNotFoundException 异常后,我们也没多少的可操作性,再 Class.forName() 一次?”

另外,checked 异常也不兼容函数式编程,后面如果你写 Lambda/Stream 代码的时候,就会体验到这种苦涩。

当然了,checked 异常并不是一无是处,尤其是在遇到 IO 或者网络异常的时候,比如说进行 Socket 链接,我大致写了一段:

public class Demo2 {
    private String mHost;
    private int mPort;
    private Socket mSocket;
    private final Object mLock = new Object();

    public void run() {
    }

    private void initSocket() {
        while (true) {
            try {
                Socket socket = new Socket(mHost, mPort);
                synchronized (mLock) {
                    mSocket = socket;
                }
                break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

当发生 IOException 的时候,socket 就重新尝试连接,否则就 break 跳出循环。意味着如果 IOException 不是 checked 异常,这种写法就略显突兀,因为 IOException 没办法像 ArithmeticException 那样用一个 if 语句判断除数是否为 0 去规避。

或者说,强制性的 checked 异常可以让我们在编程的时候去思考,遇到这种异常的时候该怎么更优雅的去处理。显然,Socket 编程中,肯定是会遇到 IOException 的,假如 IOException 是非检查型异常,就意味着开发者也可以不考虑,直接跳过,交给 Java 虚拟机来处理,但我觉得这样做肯定更不合适。

关于 throw 和 throws

throw 和 throws 两个关键字的区别:

“throw 关键字,用于主动地抛出异常;正常情况下,当除数为 0 的时候,程序会主动抛出 ArithmeticException;但如果我们想要除数为 1 的时候也抛出 ArithmeticException,就可以使用 throw 关键字主动地抛出异常。”

throw new exception_class("error message");

语法也非常简单,throw 关键字后跟上 new 关键字,以及异常的类型还有参数即可。

举个例子。

public class ThrowDemo {
    static void checkEligibilty(int stuage){
        if(stuage<18) {
            throw new ArithmeticException("年纪未满 18 岁,禁止观影");
        } else {
            System.out.println("请认真观影!!");
        }
    }

    public static void main(String args[]){
        checkEligibilty(10);
        System.out.println("愉快地周末..");
    }
}

这段代码在运行的时候就会抛出以下错误:

Exception in thread "main" java.lang.ArithmeticException: 年纪未满 18 岁,禁止观影
    at com.itwanger.s43.ThrowDemo.checkEligibilty(ThrowDemo.java:9)
    at com.itwanger.s43.ThrowDemo.main(ThrowDemo.java:16)

“throws 关键字的作用就和 throw 完全不同。”我说,“前面的小节里已经讲了 checked exception 和 unchecked exception,也就是检查型异常和非检查型异常;对于检查型异常来说,如果你没有做处理,编译器就会提示你。”

Class.forName() 方法在执行的时候可能会遇到 java.lang.ClassNotFoundException 异常,一个检查型异常,如果没有做处理,IDEA 就会提示你,要么在方法签名上声明,要么放在 try-catch 中。

在这里插入图片描述

“那什么情况下使用 throws 而不是 try-catch 呢?”

“假设现在有这么一个方法 myMethod(),可能会出现 ArithmeticException 异常,也可能会出现 NullPointerException。这种情况下,可以使用 try-catch 来处理。”

public void myMethod() {
    try {
        // 可能抛出异常 
    } catch (ArithmeticException e) {
        // 算术异常
    } catch (NullPointerException e) {
        // 空指针异常
    }
}

“但假设有好几个类似 myMethod() 的方法,如果为每个方法都加上 try-catch,就会显得非常繁琐。代码就会变得又臭又长,可读性就差了。”

“一个解决办法就是,使用 throws 关键字,在方法签名上声明可能会抛出的异常,然后在调用该方法的地方使用 try-catch 进行处理。”

public static void main(String args[]){
    try {
        myMethod1();
    } catch (ArithmeticException e) {
        // 算术异常
    } catch (NullPointerException e) {
        // 空指针异常
    }
}
public static void myMethod1() throws ArithmeticException, NullPointerException{
    // 方法签名上声明异常
}

总结下 throw 和 throws 的区别

1)throws 关键字用于声明异常,它的作用和 try-catch 相似;而 throw 关键字用于显式的抛出异常。

2)throws 关键字后面跟的是异常的名字;而 throw 关键字后面跟的是异常的对象。

示例。

throws ArithmeticException;
throw new ArithmeticException("算术异常");

3)throws 关键字出现在方法签名上,而 throw 关键字出现在方法体里。

4)throws 关键字在声明异常的时候可以跟多个,用逗号隔开;而 throw 关键字每次只能抛出一个异常。

关于 try-catch-finally

try 关键字后面会跟一个大括号 {},我们把一些可能发生异常的代码放到大括号里;try 块后面一般会跟 catch 块,用来处理发生异常的情况;当然了,异常不一定会发生,为了保证发不发生异常都能执行一些代码,就会跟一个 finally 块。”

try 块的语法很简单:

try{
// 可能发生异常的代码
}

“注意,如果一些代码确定不会抛出异常,就尽量不要把它包裹在 try 块里,因为加了异常处理的代码执行起来要比没有加的花费更多的时间。”

catch 块的语法也很简单:

try{
// 可能发生异常的代码
}catch (exception(type) e(object)){
// 异常处理代码
}

一个 try 块后面可以跟多个 catch 块,用来捕获不同类型的异常并做相应的处理,当 try 块中的某一行代码发生异常时,之后的代码就不再执行,而是会跳转到异常对应的 catch 块中执行。

如果一个 try 块后面跟了多个与之关联的 catch 块,那么应该把特定的异常放在前面,通用型的异常放在后面,不然编译器会提示错误。举例来说。

static void test() {
    int num1, num2;
    try {
        num1 = 0;
        num2 = 62 / num1;
        System.out.println(num2);
        System.out.println("try 块的最后一句");
    } catch (ArithmeticException e) {
        // 算术运算发生时跳转到这里
        System.out.println("除数不能为零");
    } catch (Exception e) {
        // 通用型的异常意味着可以捕获所有的异常,它应该放在最后面,
        System.out.println("异常发生了");
    }
    System.out.println("try-catch 之外的代码.");
}

“为什么 Exception 不能放到 ArithmeticException 前面呢”

“因为 ArithmeticException 是 Exception 的子类,它更具体,我们看到就这个异常就知道是发生了算术错误,而 Exception 比较泛,它隐藏了具体的异常信息,我们看到后并不确定到底是发生了哪一种类型的异常,对错误的排查很不利。”我说,“再者,如果把通用型的异常放在前面,就意味着其他的 catch 块永远也不会执行,所以编译器就直接提示错误了。”

static void test1 () {
    try{
        int arr[]=new int[7];
        arr[4]=30/0;
        System.out.println("try 块的最后");
    } catch(ArithmeticException e){
        System.out.println("除数必须是 0");
    } catch(ArrayIndexOutOfBoundsException e){
        System.out.println("数组越界了");
    } catch(Exception e){
        System.out.println("一些其他的异常");
    }
    System.out.println("try-catch 之外");
}

这段代码在执行的时候,第一个 catch 块会执行,因为除数为零;

static void test1 () {
    try{
        int arr[]=new int[7];
        arr[9]=30/1;
        System.out.println("try 块的最后");
    } catch(ArithmeticException e){
        System.out.println("除数必须是 0");
    } catch(ArrayIndexOutOfBoundsException e){
        System.out.println("数组越界了");
    } catch(Exception e){
        System.out.println("一些其他的异常");
    }
    System.out.println("try-catch 之外");
}

“第二个 catch 块会执行,因为没有发生算术异常,但数组越界了。”

static void test1 () {
    try{
        int arr[]=new int[7];
        arr[9]=30/1;
        System.out.println("try 块的最后");
    } catch(ArithmeticException | ArrayIndexOutOfBoundsException e){
        System.out.println("除数必须是 0");
    }
    System.out.println("try-catch 之外");
}

“当有多个 catch 的时候,也可以放在一起,用竖划线 | 隔开,就像上面这样。”。

finally 块的语法也不复杂。

try {
    // 可能发生异常的代码
}catch {
   // 异常处理
}finally {
   // 必须执行的代码
}

在没有 try-with-resources 之前,finally 块常用来关闭一些连接资源,比如说 socket、数据库链接、IO 输入输出流等。

OutputStream osf = new FileOutputStream( "filename" );
OutputStream osb = new BufferedOutputStream(opf);
ObjectOutput op = new ObjectOutputStream(osb);
try{
    output.writeObject(writableObject);
} finally{
    op.close();
}

“注意,使用 finally 块的时候需要遵守这些规则。”

  • finally 块前面必须有 try 块,不要把 finally 块单独拉出来使用。编译器也不允许这样做。
  • finally 块不是必选项,有 try 块的时候不一定要有 finally 块。
  • 如果 finally 块中的代码可能会发生异常,也应该使用 try-catch 进行包裹。
  • 即便是 try 块中执行了 return、break、continue 这些跳转语句,finally 块也会被执行。
static int test2 () {
    try {
        return 112;
    }
    finally {
        System.out.println("即使 try 块有 return,finally 块也会执行");
    }
}

来看一下输出结果:

即使 try 块有 return,finally 块也会执行

“那,会不会有不执行 finally 的情况呀?”

“有的。”

  • 遇到了死循环。
  • 执行了 System. exit() 这行代码。

System.exit()return 语句不同,前者是用来退出程序的,后者只是回到了上一级方法调用。

“来看一下源码的文档注释就全明白了!”

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TJF4zoSZ-1688096851891)(typora-user-images/try-catch-finally-01.png)]

至于参数 status 的值也很好理解,如果是异常退出,设置为非 0 即可,通常用 1 来表示;如果是想正常退出程序,用 0 表示即可。

小结

Java 的异常处理是一种重要的机制,可以帮助我们处理程序执行期间发生的错误❎或异常。

异常分为两类:Checked Exception 和 Unchecked Exception,其中 Checked Exception 需要在代码中显式地处理或声明抛出,而 Unchecked Exception 不需要在代码中显式地处理或声明抛出。异常处理通常使用 try-catch-finally 块来处理,也可以使用 throws 关键字将异常抛出给调用者处理。

下面是 Java 异常处理的一些总结:

  • 使用 try-catch 块捕获并处理异常,可以避免程序因异常而崩溃。
  • 可以使用多个 catch 块来捕获不同类型的异常,并进行不同的处理。
  • 可以使用 finally 块来执行一些必要的清理工作,无论是否发生异常都会执行。
  • 可以使用 throw 关键字手动抛出异常,用于在程序中明确指定某些异常情况。
  • 可以使用 throws 关键字将异常抛出给调用者处理,用于在方法签名中声明可能会出现的异常。
  • Checked Exception 通常是由于外部因素导致的问题,需要在代码中显式地处理或声明抛出。
  • Unchecked Exception 通常是由于程序内部逻辑或数据异常导致的,可以不处理或者在需要时进行处理。
  • 在处理异常时,应该根据具体的异常类型进行处理,例如可以尝试重新打开文件、重新建立网络连接等操作。
  • 异常处理应该根据具体的业务需求和设计原则进行,避免过度捕获和处理异常,从而降低程序的性能和可维护性。

try-with-resource

先回顾一下 try–catch-finally

package com.lison.architecture.foundation.java02;

import java.io.*;
import java.net.URLDecoder;

public class TrycatchfinallyDecoder {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream("/Users/lison/Downloads/test1"),"utf-8");
            br = new BufferedReader(inputStreamReader);

            String str = null;
            while ((str =br.readLine()) != null) {
                System.out.println(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在 try 块中读取文件中的内容,并一行一行地打印到控制台。如果文件找不到或者出现 IO 读写错误,就在 catch 中捕获并打印错误的堆栈信息。最后,在 finally 中关闭缓冲字符读取器对象 BufferedReader,有效杜绝了资源未被关闭的情况下造成的严重性能后果。在 Java 7 之前,try–catch-finally 的确是确保资源会被及时关闭的最佳方法,无论程序是否会抛出异常

try–catch-finally 至始至终存在一个严重的隐患:try 中的 br.readLine() 有可能会抛出 IOException,finally 中的 br.close() 也有可能会抛出 IOException。假如两处都不幸地抛出了 IOException,那程序的调试任务就变得复杂了起来,到底是哪一处出了错误,就需要花一番功夫,这是我们不愿意看到的结果

定义这样一个类 MyfinallyReadLineThrow,它有两个方法,分别是 readLine()close(),方法体都是主动抛出异常

class MyfinallyReadLineThrow {
    public void close() throws Exception {
        throw new Exception("close");
    }

    public void readLine() throws Exception {
        throw new Exception("readLine");
    }
}

然后在 main() 方法中使用 try-catch-finally 的方式调用 MyfinallyReadLineThrow 的 readLine()close() 方法

public class TryfinallyCustomReadLineThrow {
    public static void main(String[] args) throws Exception {
        MyfinallyReadLineThrow myThrow = null;
        try {
            myThrow = new MyfinallyReadLineThrow();
            myThrow.readLine();
        } finally {
            myThrow.close();
        }
    }
}

运行上述代码后,错误堆栈如下所示:

Exception in thread "main" java.lang.Exception: close
	at com.lison.architecture.foundation.java08.MyfinallyReadLineThrow.close(MyfinallyReadLineThrow.java:11)
	at com.lison.architecture.foundation.java08.TryfinallyCustomReadLineThrow.main(TryfinallyCustomReadLineThrow.java:17)

readLine() 方法的异常信息竟然被 close() 方法的堆栈信息覆盖了。

但有了 try-with-resources 后,这些问题就迎刃而解了。前提条件只有一个,就是需要释放的资源(比如 BufferedReader)实现了 AutoCloseable 接口。”

try (BufferedReader br = new BufferedReader(new FileReader(decodePath));) {
    String str = null;
    while ((str =br.readLine()) != null) {
        System.out.println(str);
    }
} catch (IOException e) {
    e.printStackTrace();
}

finally 块消失了,取而代之的是把要释放的资源写在 try 后的 () 中。如果有多个资源(BufferedReader 和 PrintWriter)需要释放的话,可以直接在 () 中添加。

try (BufferedReader br = new BufferedReader(new FileReader(decodePath));
     PrintWriter writer = new PrintWriter(new File(writePath))) {
    String str = null;
    while ((str =br.readLine()) != null) {
        writer.print(str);
    }
} catch (IOException e) {
    e.printStackTrace();
}

如果想释放自定义资源的话,只要让它实现 AutoCloseable 接口,并提供 close() 方法即可。

public class TrywithresourcesCustom {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource();) {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("关闭自定义资源");
    }
}

运行代码后输出结果:在 try () 中只是 new 了一个 MyResource 的对象,其他什么也没干,close() 方法就执行了

关闭自定义资源

try () 中只是 new 了一个 MyResource 的对象,其他什么也没干,close() 方法就执行了

public class TrywithresourcesCustom {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource();) {
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("关闭自定义资源");
    }
}

反编译后的字节码:

class MyResource implements AutoCloseable {
    MyResource() {
    }

    public void close() throws Exception {
        System.out.println("关闭自定义资源");
    }
}

public class TrywithresourcesCustom {
    public TrywithresourcesCustom() {
    }

    public static void main(String[] args) {
        try {
            MyResource resource = new MyResource();
            resource.close();
        } catch (Exception var2) {
            var2.printStackTrace();
        }

    }
}

编译器主动为 try-with-resources 进行了变身,在 try 中调用了 close() 方法

MyResourceOut 类中再添加一个 out() 方法

class MyResourceOut implements AutoCloseable {
    @Override
    public void close() throws Exception {
        System.out.println("关闭自定义资源");
    }

    public void out() throws Exception{
        System.out.println("我是out方法,我被调用了");
    }
}

看一下反编译的字节码:


public class TrywithresourcesCustomOut {
    public TrywithresourcesCustomOut() {
    }

    public static void main(String[] args) {
        try {
            MyResourceOut resource = new MyResourceOut();

            try {
                resource.out();
            } catch (Throwable var5) {
                try {
                    resource.close();
                } catch (Throwable var4) {
                    var5.addSuppressed(var4);
                }

                throw var5;
            }

            resource.close();
        } catch (Exception var6) {
            var6.printStackTrace();
        }

    }
}


catch块主动调用了resource.close(),并且有一段很关键的代码 var5.addSuppressed(var4)

当一个异常被抛出的时候,可能有其他异常因为该异常而被抑制住,从而无法正常抛出。这时可以通过 addSuppressed() 方法把这些被抑制的方法记录下来,然后被抑制的异常就会出现在抛出的异常的堆栈信息中,可以通过 getSuppressed() 方法来获取这些异常。这样做的好处是不会丢失任何异常,方便我们进行调试

在 try-catch-finally 中,readLine() 方法的异常信息竟然被 close() 方法的堆栈信息吃了。现在有了 try-with-resources,再来看看和 readLine() 方法一致的 out() 方法会不会被 close() 吃掉

class MyResourceOutThrow implements AutoCloseable {
    @Override
    public void close() throws Exception {
        throw  new Exception("close()");
    }

    public void out() throws Exception{
        throw new Exception("out()");
    }
}

调用这 2 个方法

public class TrywithresourcesCustomOutThrow {
    public static void main(String[] args) {
        try (MyResourceOutThrow resource = new MyResourceOutThrow();) {
            resource.out();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果


java.lang.Exception: out()
	at com.lison.architecture.foundation.java08.MyResourceOutThrow.out(MyResourceOutThrow.java:16)
	at com.lison.architecture.foundation.java08.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:12)
	Suppressed: java.lang.Exception: close()
		at com.lison.architecture.foundation.java08.MyResourceOutThrow.close(MyResourceOutThrow.java:12)
		at com.lison.architecture.foundation.java08.TrywithresourcesCustomOutThrow.main(TrywithresourcesCustomOutThrow.java:11)

这次不会了,out() 的异常堆栈信息打印出来了,并且 close() 方法的堆栈信息上加了一个关键字 Suppressed,一目了然

总结:在处理必须关闭的资源时,始终有限考虑使用 try-with-resources,而不是 try–catch-finally。前者产生的代码更加简洁、清晰,产生的异常信息也更靠谱

异常处理的20个实践

尽量不要捕获 RuntimeException

阿里出品的 Java 开发手册上这样规定:

尽量不要 catch RuntimeException,比如 NullPointerException、IndexOutOfBoundsException 等等,应该用预检查的方式来规避。

正例:

if (obj != null) {
  //...
}

反例:

try { 
  obj.method(); 
} catch (NullPointerException e) {
  //...
}

通常有些异常预是检查不出来,也的确会存在这样的情况,比如说 NumberFormatException,虽然也属于 RuntimeException,但没办法预检查,所以还是应该用 catch 捕获处理

尽量使用 try-with-resource 来关闭资源

反例:

public void doNotCloseResourceInTry() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
        inputStream.close();
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

原因也很简单,因为一旦 close() 之前发生了异常,那么资源就无法关闭。直接使用 try-with-resource来处理是最佳方式

public void automaticallyCloseResource() {
    File file = new File("./tmp.txt");
    try (FileInputStream inputStream = new FileInputStream(file);) {
    } catch (FileNotFoundException e) {
        log.error(e);
    } catch (IOException e) {
        log.error(e);
    }
}

若资源没有实现 AutoCloseable 接口:就在finally 块关闭流

public void closeResourceInFinally() {
    FileInputStream inputStream = null;
    try {
        File file = new File("./tmp.txt");
        inputStream = new FileInputStream(file);
    } catch (FileNotFoundException e) {
        log.error(e);
    } finally {
        if (inputStream != null) {
            try {
                inputStream.close();
            } catch (IOException e) {
                log.error(e);
            }
        }
    }
}

不要捕获 Throwable

Throwable 是 exception 和 error 的父类,如果在 catch 子句中捕获了 Throwable,很可能把超出程序处理能力之外的错误也捕获了

public void doNotCatchThrowable() {
    try {
    } catch (Throwable t) {
        // 不要这样做
    }
}

有些 error 是不需要程序来处理,程序可能也处理不了,比如说 OutOfMemoryError 或者 StackOverflowError,前者是因为 Java 虚拟机无法申请到足够的内存空间时出现的非正常的错误,后者是因为线程申请的栈深度超过了允许的最大深度出现的非正常错误,如果捕获了,就掩盖了程序应该被发现的严重错误

不要省略异常信息的记录

public void doNotIgnoreExceptions() {
    try {
    } catch (NumberFormatException e) {
        // 没有记录异常
    }
}

应该把错误信息记录下来

public void logAnException() {
    try {
    } catch (NumberFormatException e) {
        log.error("错误发生了: " + e);
    }
}

不要记录了异常又抛出了异常

反例:

try {
} catch (NumberFormatException e) {
    log.error(e);
    throw e;
}

要抛出就抛出,不要记录,记录了又抛出,等于多此一举

反例:

public void wrapException(String input) throws MyBusinessException {
    try {
    } catch (NumberFormatException e) {
        throw new MyBusinessException("错误信息描述:", e);
    }
}

既然已经捕获了,就不要在方法签名上抛出了

不要在 finally 块中使用 return

阿里出品的 Java 开发手册上这样规定

try 块中的 return 语句执行成功后,并不会马上返回,而是继续执行 finally 块中的语句,如果 finally 块中也存在 return 语句,那么 try 块中的 return 就将被覆盖。

反例:

private int x = 0;
public int checkReturn() {
    try {
        return ++x;
    } finally {
        return ++x;
    }
}

try 块中 x 返回的值为 1,到了 finally 块中就返回 2 了

抛出具体定义的检查性异常而不是 Exception

public void foo() throws Exception { //错误方式
}

它破坏了检查性(checked)异常的目的。声明的方法应该尽可能抛出具体的检查性异常。

例如,如果一个方法可能会抛出 SQLException 异常,应该显式地声明抛出 SQLException 而不是 Exception 类型的异常。这样可以让其他开发者更好地理解代码的意图和异常处理的方式,并且可以根据 SQLException 的定义和文档来确定异常的处理方式和策略

捕获具体的子类而不是捕获 Exception 类

try {
   someMethod();
} catch (Exception e) { //错误方式
   LOGGER.error("method has failed", e);
}

在 catch 块中捕获 Exception 类型的异常,会将所有异常都捕获,从而可能会给程序带来不必要的麻烦。具体来说,如果捕获 Exception 类型的异常,可能会导致以下问题:

  • 难以识别和定位异常:如果捕获 Exception 类型的异常,可能会捕获到一些不应该被处理的异常,从而导致程序难以识别和定位异常。
  • 难以调试和排错:如果捕获 Exception 类型的异常,可能会使得调试和排错变得更加困难,因为无法确定具体的异常类型和异常发生的原因。

下面举一个例子来说明为什么应该尽可能地捕获具体的子类而不是 Exception 类型的异常。

假设我们有一个方法 readFromFile(String filePath),用于从指定文件中读取数据。在方法实现过程中,可能会出现两种异常:FileNotFoundException 和 IOException

如果在方法中使用以下 catch 块来捕获异常:

try {
    // 读取数据的代码
} catch (Exception e) {
    // 异常处理的代码
}

这样做会捕获所有类型的异常,包括 Checked Exception 和 Unchecked Exception。这可能会导致以下问题:

  • 发生 RuntimeException 类型的异常时,也会被捕获,从而可能会掩盖实际的异常信息。
  • 在调试和排错时,无法确定异常的具体类型和发生原因,从而增加了调试和排错的难度。
  • 在程序运行时,可能会捕获一些不需要处理的异常(如 NullPointerException、IllegalArgumentException 等),从而降低程序的性能和稳定性

为了更好地定位和处理异常,应该尽可能地捕获具体的子类,例如:

try {
    // 读取数据的代码
} catch (FileNotFoundException e) {
    // 处理文件未找到异常的代码
} catch (IOException e) {
    // 处理输入输出异常的代码
}

这样做可以更准确地捕获异常,从而提高程序的健壮性和稳定性。

自定义异常时不要丢失堆栈跟踪

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " + e.getMessage());  //错误方式
}

这破坏了原始异常的堆栈跟踪,正确的做法是:

catch (NoSuchMethodException e) {
   throw new MyServiceException("Some information: " , e);  //正确方式
}

例如,下面是一个自定义异常类,它重写了 printStackTrace() 方法来打印堆栈跟踪信息:

public class MyException extends Exception {
    public MyException(String message, Throwable cause) {
        super(message, cause);
    }

    @Override
    public void printStackTrace() {
        System.err.println("MyException:");
        super.printStackTrace();
    }
}

这样做可以保留堆栈跟踪信息,同时也可以提供自定义的异常信息。在抛出 MyException 异常时,可以得到完整的堆栈跟踪信息,从而更好地定位和解决异常。

finally 块中不要抛出任何异常

try {
  someMethod();  //Throws exceptionOne
} finally {
  cleanUp();    //如果finally还抛出异常,那么exceptionOne将永远丢失
}

finally 块用于定义一段代码,无论 try 块中是否出现异常,都会被执行。finally 块通常用于释放资源、关闭文件等必须执行的操作。

如果在 finally 块中抛出异常,可能会导致原始异常被掩盖。比如说上例中,一旦 cleanup 抛出异常,someMethod 中的异常将会被覆盖。

不要在生产环境中使用 printStackTrace()

在 Java 中,printStackTrace() 方法用于将异常的堆栈跟踪信息输出到标准错误流中。这个方法对于调试和排错非常有用。但在生产环境中,不应该使用 printStackTrace() 方法,因为它可能会导致以下问题:

  • printStackTrace() 方法将异常的堆栈跟踪信息输出到标准错误流中,这可能会暴露敏感信息,如文件路径、用户名、密码等。
  • printStackTrace() 方法会将堆栈跟踪信息输出到标准错误流中,这可能会影响程序的性能和稳定性。在高并发的生产环境中,大量的异常堆栈跟踪信息可能会导致系统崩溃或出现意外的行为。
  • 由于生产环境中往往是多线程、分布式的复杂系统,printStackTrace() 方法输出的堆栈跟踪信息可能并不完整或准确。

在生产环境中,应该使用日志系统来记录异常信息,例如 log4j、slf4j、logback 等。日志系统可以将异常信息记录到文件或数据库中,而不会暴露敏感信息,也不会影响程序的性能和稳定性。同时,日志系统也提供了更多的功能,如级别控制、滚动日志、邮件通知等。

例如,可以使用 logback 记录异常信息,如下所示:
try {
    // some code
} catch (Exception e) {
    logger.error("An error occurred: ", e);
}

、对于不打算处理的异常,直接使用 try-finally,不用 catch

try {
  method1();  // 会调用 Method 2
} finally {
  cleanUp();    //do cleanup here
}

如果 method1 正在访问 Method 2,而 Method 2 抛出一些你不想在 Method 1 中处理的异常,但是仍然希望在发生异常时进行一些清理,可以直接在 finally 块中进行清理,不要使用 catch 块。

记住早 throw 晚 catch 原则

“早 throw, 晚 catch” 是 Java 中的一种异常处理原则。这个原则指的是在代码中尽可能早地抛出异常,以便在异常发生时能够及时地处理异常。同时,在 catch 块中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。

来举个 “早 throw” 例子,如果一个方法需要传递参数,并且该参数必须满足一定的条件,如果参数不符合条件,则应该立即抛出异常,而不是在方法中进行其他操作。这可以确保异常在发生时能够及时被处理,避免更严重的问题。

再来举个“晚 catch”的例子,如果一个方法调用了其他方法,可能会抛出异常,如果在方法内部立即捕获异常,则可能会导致对异常的处理不充分。

来看这段代码:

public class ExceptionDemo1 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String str = sc.nextLine();
        try {
            int num = parseInt(str);
            System.out.println("转换结果:" + num);
        } catch (NumberFormatException e) {
            System.out.println("转换失败:" + e.getMessage());
        }
    }

    public static int parseInt(String str) {
        if (str == null || "".equals(str)) {
            throw new NullPointerException("字符串为空");
        }
        if (!str.matches("\\d+")) {
            throw new NumberFormatException("字符串不是数字");
        }
        return Integer.parseInt(str);
    }
}

这个示例中,定义了一个 parseInt() 方法,用于将字符串转换为整数。在该方法中,首先检测字符串是否为空,如果为空,则立即抛出 NullPointerException 异常。然后,检测字符串是否为数字,如果不是数字,则抛出 NumberFormatException 异常。最后,使用 Integer.parseInt() 方法将字符串转换为整数,并返回。

在示例的 main() 方法中,调用 parseInt() 方法,并使用 try-catch 块捕获可能抛出的 NumberFormatException 异常。如果转换成功,则输出转换结果,否则输出转换失败信息。

这个示例使用了 “早 throw, 晚 catch” 的原则,在 parseInt() 方法中尽可能早地抛出异常,在 main() 方法中尽可能晚地捕获异常,以便在捕获异常时能够获得更多的上下文信息,从而更好地处理异常。

运行该示例,输入一个数字字符串,可以看到输出转换结果。如果输入一个非数字字符串,则输出转换失败信息。

只抛出和方法相关的异常

相关性对于保持代码的整洁非常重要。一种尝试读取文件的方法,如果抛出 NullPointerException,那么它不会给用户提供有价值的信息。相反,如果这种异常被包裹在自定义异常中,则会更好。NoSuchFileFoundException 则对该方法的用户更有用。

public class Demo {
    public static void main(String[] args) {
        try {
            int result = divide(10, 0);
            System.out.println("The result is: " + result);
        } catch (ArithmeticException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }

    public static int divide(int a, int b) throws ArithmeticException {
        if (b == 0) {
            throw new ArithmeticException("Division by zero");
        }
        return a / b;
    }
}

在该示例中,只抛出了和方法相关的异常 ArithmeticException,这可以使代码更加清晰和易于维护。

切勿在代码中使用异常来进行流程控制

在代码中使用异常来进行流程控制会导致代码的可读性、可维护性和性能出现问题。

public class Demo {
    public static void main(String[] args) {
        String input = "1,2,3,a,5";
        String[] values = input.split(",");
        for (String value : values) {
            try {
                int num = Integer.parseInt(value);
                System.out.println(num);
            } catch (NumberFormatException e) {
                System.err.println(value + " is not a valid number");
            }
        }
    }
}

虽然这个示例可以正确地处理输入字符串中的非数字字符,但是它使用异常进行流程控制,这就导致代码变得混乱、难以理解。应该使用其他合适的控制结构(如 if、switch、循环等)来管理程序的流程。

尽早验证用户输入以在请求处理的早期捕获异常

例如:在用户注册的业务中,如果按照这样来做:

  1. 验证用户
  2. 插入用户
  3. 验证地址
  4. 插入地址
  5. 如果出问题回滚一切

这是不正确的做法,它会使数据库在各种情况下处于不一致的状态,应该首先验证所有内容,然后再进行数据库更新。正确的做法是:

  1. 验证用户
  2. 验证地址
  3. 插入用户
  4. 插入地址
  5. 如果问题回滚一切

举个例子,我们用 JDBC 的方式往数据库插入数据,那么最好是先 validate 再 insert,而不是 validateUserInput、insertUserData、validateAddressInput、insertAddressData。

Connection conn = null;
try {
    // Connect to the database
    conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydatabase", "username", "password");

    // Start a transaction
    conn.setAutoCommit(false);

    // Validate user input
    validateUserInput();

    // Insert user data
    insertUserData(conn);

    // Validate address input
    validateAddressInput();

    // Insert address data
    insertAddressData(conn);

    // Commit the transaction if everything is successful
    conn.commit();

} catch (SQLException e) {
    // Rollback the transaction if there is an error
    if (conn != null) {
        try {
            conn.rollback();
        } catch (SQLException ex) {
            System.err.println("Error: " + ex.getMessage());
        }
    }
    System.err.println("Error: " + e.getMessage());
} finally {
    // Close the database connection
    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}

一个异常只能包含在一个日志中

不要这样做:

log.debug("Using cache sector A");
log.debug("Using retry sector B");

在单线程环境中,这样看起来没什么问题,但如果在多线程环境中,这两行紧挨着的代码中间可能会输出很多其他的内容,导致问题查起来会很难受。应该这样做:

LOGGER.debug("Using cache sector A, using retry sector B");

将所有相关信息尽可能地传递给异常

有用的异常消息和堆栈跟踪非常重要,如果你的日志不能定位异常位置,那要日志有什么用呢?

// Log exception message and stack trace
LOGGER.debug("Error reading file", e);

应该尽量把 String message, Throwable cause 异常信息和堆栈都输出。

终止掉被中断线程

while (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {} //别这样做
  doSomethingCool();
}

InterruptedException 提示应该停止程序正在做的事情,比如事务超时或线程池被关闭等。

应该尽最大努力完成正在做的事情,并完成当前执行的线程,而不是忽略 InterruptedException。修改后的程序如下:

while (true) {
  try {
    Thread.sleep(100000);
  } catch (InterruptedException e) {
    break;
  }
}
doSomethingCool();

对于重复的 try-catch,使用模板方法

类似的 catch 块是无用的,只会增加代码的重复性,针对这样的问题可以使用模板方法。

例如,在尝试关闭数据库连接时的异常处理。

class DBUtil{
    public static void closeConnection(Connection conn){
        try{
            conn.close();
        } catch(Exception ex){
            //Log Exception - Cannot close connection
        }
    }
}

这类的方法将在应用程序很多地方使用。不要把这块代码放的到处都是,而是定义上面的方法,然后像下面这样使用它:

public void dataAccessCode() {
    Connection conn = null;
    try{
        conn = getConnection();
        ....
    } finally{
        DBUtil.closeConnection(conn);
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

升级Xcode14.3,项目无法运行解决

报错&#xff1a;link command failed with exit code 1(use -v to see invocaiton) 原因&#xff1a;新版本Xcode删除了特定目录下的一些文件 解决&#xff1a; post_install do |installer|installer.pods_project.targets.each do |target|target.build_configurations.e…

O2OA(翱途)开发平台如何在流程表单中使用基于Vue的ElementUI组件?

本文主要介绍如何在O2OA中进行审批流程表单或者工作流表单设计&#xff0c;O2OA主要采用拖拽可视化开发的方式完成流程表单的设计和配置&#xff0c;不需要过多的代码编写&#xff0c;业务人员可以直接进行修改操作。 在流程表单设计界面&#xff0c;可以在左边的工具栏找到Ele…

Linux--显示当前路径下的所有文件指令:ls

一、ls是list的简写 二、语法&#xff1a; ls [选项] [目录或文件] 三、功能&#xff1a; ①对于目录&#xff0c;该命令列出当前目录下的所有子目录与文件。 ②对于文件&#xff0c;将列出文件名以及其他信息。 四、常用选项&#xff1a; 1.-a 列出目录下的所有文件&…

Java实现将数据转成xmind脑图(附有工具类)。

&#x1f61c;作 者&#xff1a;是江迪呀✒️本文关键词&#xff1a;Java、工具类、xmind、脑图、转换☀️每日 一言&#xff1a;昨日已成过去,未来充满可能,唯有珍惜现在。 [TOPC] 前言 当谈到Xmind时&#xff0c;这是一个非常流行的思维导图工具&#xff0c;可…

超级实用!详解Node.js中的path模块和events模块

文章目录 3. path 模块路径操作方法路径格式化方法路径拆分方法 4. events 模块EventEmitter 类创建事件对象注册事件处理函数触发事件一次性事件处理函数异步事件处理函数移除事件处理函数 继承 EventEmitter 类 3. path 模块 用于处理文件路径&#xff0c;包括解析、拼接、规…

删除MySQL中名字首尾固定关键字相同的表

删除MySQL中名字首尾固定关键字相同的表 SELECT CONCAT(drop table , group_concat(TABLE_NAME), ;) FROM information_schema.TABLES WHERE table_schema test AND TABLE_NAME LIKE t_%_history ;查看表列表 SHOW TABLES;通过上图观察发现所有的表都是以 t_ 开头 和以 _his…

Mybatis-Plus学习1

mybatis-plus需要两个依赖&#xff0c;一个lombok&#xff0c;一个mybatis-plus <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.1</version> </dependency> …

Service 基础

今天开始来分享Service 的基础知识&#xff0c;后续我们可以慢慢打磨&#xff0c;分享 Service 的进阶知识和原理 Service 基本概念 Service 是 K8S 最核心的概念了 我们可以通过创建 Service &#xff0c;为一组具有相同功能的容器应用提供一个统一的入口地址&#xff0c;并…

linux上搭建ftp服务

linux上搭建ftp服务简要过程。 1.安装 在目标主机上安装对应软件。 rpm -qa | grep vsftpd # 查看是否已经安装了vsftpd软件 yum install -y vsftpd # 安装2.配置 安装好了之后可在这个路径下编辑配置文件&#xff0c;按需配置&#xff0c;这里采用默认。 vi /etc/vsftp…

螺杆支撑座要怎么选?

螺杆支撑座是连接螺杆和电机的轴承固定座&#xff0c;使用螺杆支撑座可以获得高刚性、高精度的稳定的回转性能&#xff0c;这也是大部分厂商愿意使用的原因之一。 目前&#xff0c;市面上做螺杆支撑座的品牌还比较少&#xff0c;给大家选择的空间也不多&#xff0c;那么我们如何…

centos8运行cloudstack4.18

安装软件&#xff1a; mysql 5.7.42 node v10.24.0 git 2.34.1 jdk openjdk version "11.0.19" 2023-04-18 maven Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739)自行配置&#xff1a;nfs 代码克隆地址&#xff1a; git clone https://git-wip-us…

Lesson1-1:OpenCV简介

图像处理 学习目标 了解图像的起源知道数字图像的表示 1 图像的起源 1.1 图像是什么 图像是人类视觉的基础&#xff0c;是自然景物的客观反映&#xff0c;是人类认识世界和人类本身的重要源泉。“图”是物体反射或透射光的分布&#xff0c;“像“是人的视觉系统所接受的图在…

ss客服让您在Facebook 的客户服务更便捷

ss客服让您在Facebook Messenger 的客户服务更便捷 在这个信息时代&#xff0c;新兴通讯软件蓬勃兴起&#xff0c;比如Facebook Messenger。事实证明&#xff0c;这对企业来说非常有利&#xff0c;同时突出了电子邮件、网络聊天和电话等传统渠道的局限性。在传统渠道上&#xf…

fastadmin表格列表内部自定义按钮

效果图&#xff1a; 直接上代码&#xff1a; 打开js渲染文件---》找到渲染原生的按钮&#xff1a; {field: "operate",title: __("Operate"),table: table,events: Table.api.events.operate,buttons: [//可多个按钮{name: "record", //名称tex…

vue + js 实现导出excel

效果如下图所示&#xff1a; 下面是具体的步骤&#xff1a; 第一步&#xff1a;安装依赖 **注意&#xff1a;**安装的时候注意版本号 npm install --save file-saver xlsx第二步&#xff1a;新建导出文件 Export2Excel.js /* eslint-disable */ import { saveAs } from f…

C#winform listBox组件批量删除

修改listBox组件属性&#xff1a;可以选中多个板坯号 选中板坯列表&#xff0c;在界面上点击删除按钮&#xff0c;触发删除方法deleteList&#xff1a; private void deleteList() { ListBox.SelectedIndexCollection sic listBoxProducts.SelectedIndice…

亚马逊云科技推出Amazon AppFabric,SaaS安全不断加码

亚马逊云科技近日宣布推出Amazon AppFabric来增强公司在软件即服务&#xff08;SaaS&#xff09;应用程序方面的现有投入。Amazon AppFabric是一项无代码服务&#xff0c;可以为客户提高安全性&#xff0c;管理水平和生产力。只需在亚马逊云科技管理控制台上点击几下&#xff0…

gerrit 遇到的问题汇总

更新远程代码 git pull --rebase 回退到指定版本 get reset --hard commitid 修改之前的提交 git commit --amend 问题一 本地提交了两个记录到远程&#xff0c;远程还没有合并&#xff0c;本地使用 git reset --soft HEAD~1 回退到了上一个提交&#xff0c;现在需要将本地…

最强的“矛“ 验关键的“盾“ | “铸网-2023“ 赛宁数字孪生靶场深度验证湖南工业互联网安全

为深入推动“智赋万企”数字安全屏障工程&#xff0c;由工业和信息化部网络安全管理局指导&#xff0c;湖南省工信厅和湖南省通管局主办的“铸网—2023”湖南省工业互联网企业网络安全实战攻防演练&#xff0c;于近日圆满收官。赛宁网安以数字孪生靶场为基础&#xff0c;构建专…

学习Vue3——Ref全家桶

参考地址 小满Vue3视频 ref 创造响应式对象 这样修改num的值是可以&#xff0c;但是页面展示不正常&#xff0c;因为这不是响应式的&#xff0c;无法被vue追踪 <template><div>{{ num }}</div><button click"change">修改</button&g…