实战:java基础系列之【Exception:如何处理异常】

实战:java基础系列之【Exception:如何处理异常】

概叙

异常定义(Error/Exception)

异常:在Java语言中, 将程序执行中发生的不正常情况称为“异常” 。 (开发过程中的语法错误和逻辑错误不是异常) Java程序在执行过程中所发生的异常事件可分为两类:

  1. Error: Java虚拟机无法解决的严重问题。 如: JVM系统内部错误、 资源耗尽等严重情况。 比如: StackOverflowError和OOM。 一般不编写针对性的代码进行处理。
  2. Exception: 其它因编程错误或偶然的外在因素导致的一般性问题, 可以使用针对性的代码进行处理。 例如: 1、空指针访问 2、试图读取不存在的文件 3、网络连接中断 4、数组角标越界

对于这些错误, 一般有两种解决方法:

  1. 一种是遇到错误就终止程序的运行。
  2. 一种方法是由程序员在编写程序时, 就考虑到错误的检测、 错误消息的提示, 以及错误的处理。
www.zeeklog.com  - 实战:java基础系列之【Exception:如何处理异常】

要理解 Java 异常处理是如何工作的,你需要掌握以下三种类型的异常:受检异常(编译器检查)、运行时异常(编译器不检查、业务代码bug)、错误(不需要处理)

  1. 检查性异常:最具代表的检查性异常是用户错误或问题引起的异常,这些异常在编译时强制要求程序员处理。
  2. 例如,如果程序从磁盘读入数据,而系统找不到含有数据的文件,将会发生受检异常,这个异常的类名是FileNotFoundException,发生的原因可能是用户给程序提供了一个错误的文件名。
  3. 非检查性异常/运行时异常:运行时异常的所有类都是RuntimeException的子类。 这些异常在编译时不强制要求处理,通常是由程序中的错误引起的。
  4. 例如 NullPointerException、ArrayIndexOutOfBoundsException 等,这类异常可以选择处理,但并非强制要求。
  5. 例如,数组下标越界导致IndexOutOfBoundsException异常,被0除导致ArithmeticException异常。虽然可以添加代码来处理运行时异常,但一般只需要修改程序中的错误即可。
  6. 错误Error: 错误是标准类Error或其后代类的一个对象。程序中无法处理的错误,表示运行应用程序中出现了严重的错误。错误不是异常,而是脱离程序员控制的问题,错误在代码中通常被忽略。它是指发生了不正确的情况,
  7. 如内存溢出。这些情况都比较严重,一般很难通过程序处理。例如OOM(OutOfMemoryError),简单的解决方法就是通过设置Java程序的启动参数来设置运行时内存大小。当栈溢出时,一个错误就发生了StackOverFlowError,它们在编译也检查不到的。
  8. Error错误是不受检异常,非代码性错误。因此,当此类错误发生时,应用程序不应该去处理此类错误。按照Java惯例,我们是不应该实现任何新的Error子类的!

为什么要处理异常

目的只有一个:为了更加健壮的程序(本质是少背锅)。

事实上,领导是不会拿自己的脑袋宣言的:“我们的程序绝不存在任何一个 bug。”但当程序出现 bug 的时候,领导会毫不犹豫地选择让程序员背锅。

为了让自己少背锅,我们可以这样做:

  • 在编码阶段合理使用异常处理机制,并记录日志以备后续分析
  • 在测试阶段进行大量有效的测试,在用户发现错误之前发现错误

Java中异常提供了一种识别及响应错误情况的一致性机制,有效地异常处理能使程序更加健壮、易于调试。

在有效使用异常的情况下,异常类型回答了“什么”被抛出,异常堆栈跟踪回答了“在哪“抛出,异常信息回答了“为什么“会抛出,如果你的异常没有回答以上全部问题,那么可能你没有很好地使用它们。

异常处理方式

参考:

www.zeeklog.com  - 实战:java基础系列之【Exception:如何处理异常】

在Springboot工程中:@ControllerAdvice和@ExceptionHandler实现全局异常处理。

异常处理建议

可参考:

4)使用异常还是错误码返回

在分布式微服务应用中,系统调用依赖关系复杂,系统内部之间抛异常传递,系统与系统之间使用错误码返回,这样能够清晰看到异常错误原因、定位问题。

5)统一错误码

错误码本身不算是代码问题,不过基于整个组织和工程的可维护性来说,可以将错误码不符合规范作为一种错误加以避免。方法:对错误码进行可控的管理和遵循规范使用。可以使用公共文档维护, 也可以开发错误码管理系统来避免相同的错误码。

1.  统一的系统范围的异常类

www.zeeklog.com  - 实战:java基础系列之【Exception:如何处理异常】

不要针对每种异常类型创建单独的类,而是只创建一个,并使它继承RuntimeException。这可以减少类的数量,并移除你不会去处理的需要声明的异常。

我知道你在想什么:如何告诉异常处理程序,这些是否是同一类型?如何跟踪特定类型的属性?继续读下去。

2.  为错误代码使用枚举

大多数开发者会把导致异常的原因放到消息中,出现异常时,查看日志文件即可。但是这也有一些缺点:

消息不能被翻译
消息不能很容易地映射为易读的文本
无法从程序方面对消息进行检查
由于每个开发者的语言习惯不一样,同样的错误可能有不同的描述形式。

更好的办法是使用枚举来表示异常的类型。针对每种错误创建一个枚举,并使枚举实现一个ErrorCode接口,然后将它引用为异常中的一个字段。

当抛出异常时,只需在适当的枚举中传递即可。throw new SystemException(PaymentCode.CREDIT_CARD_EXPIRED);

现在,当你需要测试具体情况时,只需比较异常代码即可。
catch (SystemException e) {
  if (e.getErrorCode() == PaymentCode.CREDIT_CARD_EXPIRED) {
  ...
  }
}

在资源包中使用错误代码作为关键字进行检索,即可取回易读的、国际化的文本。

1. public class
2.     
3. public static void
4.         System.out.println(getUserText(ValidationCode.VALUE_TOO_SHORT));   
5.     }   
6.     
7. public static
8. if (errorCode == null) {    
9. return null;    
10.         }   
11. "__"
12. "com.northconcepts.exception.example.exceptions");    
13. return
14.     }   
15.     
16. }


public class SystemExceptionExample3 {
 
    public static void main(String[] args) {
        System.out.println(getUserText(ValidationCode.VALUE_TOO_SHORT));
    }
 
    public static String getUserText(ErrorCode errorCode) {
        if (errorCode == null) {
            return null;
        }
        String key = errorCode.getClass().getSimpleName() + "__" + errorCode;
        ResourceBundle bundle = ResourceBundle.getBundle("com.northconcepts.exception.example.exceptions");
        return bundle.getString(key);
    }
 
}

3.  在枚举类型中添加错误代码

在某些情况下,一个数字形式的错误代码可以对应一个异常,例如HTTP响应。在这种情况下,在ErrorCode接口中添加一个getNumber方法,并在每个枚举类型中实现它。

public enum PaymentCode implements ErrorCode {
  SERVICE_TIMEOUT(101),
  CREDIT_CARD_EXPIRED(102),
  AMOUNT_TOO_HIGH(103),
  INSUFFICIENT_FUNDS(104);
 
  private final int number;
 
  private PaymentCode(int number) {
    this.number = number;
  }
 
  @Override
  public int getNumber() {
    return number;
  }
 
}

这些数字可以是全局唯一的,或者每个枚举类型只对应一个数字。你甚至可以使用隐式ordinal()方法,或者从一个文件/数据库中加载一些数字。

4.  将动态字段添加到异常处理中

好的异常处理也意味着记录相关数据,而不仅仅是堆栈跟踪。这样做会节省你大量用于诊断和重现错误的时间。当你的程序停止工作时,也无需客户告诉你,他们做了什么。

做到这一点最简单的方法是在异常处理中增加一个java.util.Map字段。该字段主要作用是保留所有异常相关的数据。如果你使用fluent interface(连贯接口)模式,你还需要添加一个通用的setter方法。

抛出异常,并带有相关的数据,类似于下面的形式:

throw new SystemException(ValidationCode.VALUE_TOO_SHORT)
  .set("field", field)
  .set("value", value)
  .set("min-length", MIN_LENGTH);

5.  防止不必要的嵌套

长且多余的堆栈跟踪,对谁都没有好处。更糟的是,它们浪费你的时间和资源。当重新抛出异常时,调用一个静态封装方法,而不是异常的构造函数。封装方法将决定何时嵌套异常,以及何时返回原来的实例。

1. public static
2. if (exception instanceof
3.     SystemException se = (SystemException)exception;   
4. if (errorCode != null
5. return new
6.     }   
7. return
8. else
9. return new
10.   }   
11. }   
12.     
13. public static
14. return wrap(exception, null);    
15. }


public static SystemException wrap(Throwable exception, ErrorCode errorCode) {
  if (exception instanceof SystemException) {
    SystemException se = (SystemException)exception;
    if (errorCode != null && errorCode != se.getErrorCode()) {
      return new SystemException(exception.getMessage(), exception, errorCode);
    }
    return se;
  } else {
    return new SystemException(exception.getMessage(), exception, errorCode);
  }
}
 
public static SystemException wrap(Throwable exception) {
  return wrap(exception, null);
}

重新抛出异常的代码类似于:

} catch (IOException e) {
  throw SystemException.wrap(e).set("fileName", fileName);
}

6.  finally执行扫尾工作

当抛出异常的时候,剩余的代码就会终止执行,这时候一些资源就需要主动回收。Java 的解决方案就是 finally 子句——不管异常有没有被捕获,finally 子句里的代码都会执行。

所以finally 里面执行关闭连接、关闭流等释放资源操作。

public static void main(String[] args) {
	InputStream is = null;
	try {
		is = new FileInputStream("文件.txt");
		int b;
		while ((b = is.read()) != -1) {
            。。。 
       }
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
        // 不要说这里丑,那是你不知道它的香
		try {
			is.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

7.  catch 原始异常,不要自己吃了;也不要瞎抛异常。

InputStream is = null;
try {
	is = new FileInputStream("文件.txt");
} catch (Exception e) {
	e.printStackTrace();
}

上面的反例把 FileNotFoundException吃了却抛出泛化的 Exception。这样不负责的吃异常,非常不道德,codereview第一个被打回。

public static void main(String[] args) throws IOException {
	try (InputStream is = new FileInputStream("文件.txt")) {
	}catch (IOException e) {
		e.printStackTrace();
		throw e;
	} 
}



java.io.FileNotFoundException: 文件.txt (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at learning.Test.main(Test.java:10)
Exception in thread "main" java.io.FileNotFoundException: 文件.txt (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at learning.Test.main(Test.java:10)

不要重复打印异常"打印堆栈后再抛出异常" ,当异常发生时打印它,然后重新抛出它,以便调用者能够适当地处理它。就像上面这段代码一样。

8.  不要用异常处理机制代替判断

try {
    // 执行事务
    ...
} catch (SQLException e) {
    if (e.getErrorCode() == 1213) { // 1213 代表死锁错误代码
        // 死锁检测,进行重试
        retryTransaction();
    } else {
        // 其他异常处理
        throw e;
    }
}

前面死锁案例中,在catch里面做死锁检查和重试,尽量还是不要catch里面做过多的业务操作。

能拆出来,尽量拆出来。

8.  不要盲目地过早捕获异常

如果盲目地过早捕获异常的话,通常会导致更严重的错误和其他异常。请看下面的例子。

InputStream is = null;
try {
	is = new FileInputStream("文件.txt");

} catch (FileNotFoundException e) {
	e.printStackTrace();
}

int b;
try {
	while ((b = is.read()) != -1) {
	}
} catch (IOException e) {
	e.printStackTrace();
}

finally {
	try {
		is.close();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

java.io.FileNotFoundException: 文件.txt (系统找不到指定的文件。)
	at java.io.FileInputStream.open0(Native Method)
	at java.io.FileInputStream.open(FileInputStream.java:195)
	at java.io.FileInputStream.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(FileInputStream.java:93)
	at learning.Test.main(Test.java:12)
Exception in thread "main" java.lang.NullPointerException
	at learning.Test.main(Test.java:28)

假如文件没有找到的话,InputStream 的对象引用 is 就为 null,新的 NullPointerException 就会出现。NullPointerException 并不是程序出现问题的本因,但实际上它出现了,无形当中干扰了我们的视线。正确的做法是延迟捕获异常,让程序在第一个异常捕获后就终止执行。

9.  循环里面尽量不要捕捉异常(视情况而定)

代码片段 A
long a = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
	try {
		String str = null;
		String[] strs = str.split(",");
	} catch (NullPointerException e) {
	}
}
long b = System.currentTimeMillis();
System.out.println(b - a);


代码片段 B
long a = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
	String str = null;
	if (str != null) {
		String[] strs = str.split(",");
	}
}
long b = System.currentTimeMillis();
System.out.println(b - a);

100000 次的循环,有异常尽早中断循环;如果是while(true)的这种,容易死循环。

代码片段 A(异常处理机制)执行的时间大概需要 1983 毫秒;

代码片段 B(正常判断)执行的时间大概只需要 1 毫秒。

这样的比较虽然不够精确,但足以说明问题。


10.  使用一个有Web控制面板的中央记录器

根据你的情况,访问产品日志可能会相当麻烦。因为这可能会涉及到多个中间人。

如果你在一个多服务器环境中,事情会更糟。找到出问题的服务器,并确定该问题只影响这一台服务器,这可能相当令人头痛。
我的建议是:

  1. 将日志聚合到某一位置,最好是数据库
  2. 允许从Web浏览器访问该数据库

有很多方法可以做到这一点,比如:日志收集器、远程记录器、JMX代理、系统监控软件等,你甚至可以自己构建一个。一旦你拥有它,你将能够:

  1. 在几秒钟内解决问题。
  2. 每个异常都有一个URL对应。
  3. 维护人员无需通过其他人的帮助即可确定异常原因
  4. 防止测试人员为同样的错误创建多个tickets。
  5. 为企业节省资金
  6. 节省你的时间,不会影响你的周末度假

Read more

使用本地大模型调用代码,根本就是一场骗局!

使用本地大模型调用代码,根本就是一场骗局!

通过大模型调用其他工具到底可不可行?ChatGPT 或许能轻松搞定一切,但同样的需求落在本地大模型上,恐怕就要打个问号了。 法国开发工程师 Emilien Lancelot 尝试了多款号称具备工具调用功能的 agent 框架,来看看本地大模型到底能不能完成任务,但结果就像他总结的“一无所获”。是什么让这位工程师失望了? 用 AutoGPT,得会点“糊弄学” AutoGPT 是款貌似强大的框架,提供很酷的 CLI 外加 Flutter UI,能够通过浏览器创建 agent。其主要功能是处理用户的文档、音频、视频等本地内容。 但是……它主要依靠 ChatGPT 或其他专有大模型服务来完成繁重工作,至少给我们的感觉是如此。 我们必须“唬弄”AutoGPT 才能使用 Ollama 端点,让其误认为是 ChatGPT。 ## OPENAI_API_KEY - OpenAI API

By Ne0inhk
详细说明Encoder-only架构的特点以及他和encoder decoder的区别和联系

详细说明Encoder-only架构的特点以及他和encoder decoder的区别和联系

Poe Encoder-only 架构的特点 Encoder-only 模型只使用 Transformer 的编码器部分,专注于对输入序列的理解和表征学习。这类模型通常被用在需要对输入文本进行深度理解的任务上,如文本分类、问答和语义匹配。以下是 Encoder-only 架构的核心特点: 1. 双向上下文建模 * 特点:Encoder-only 模型通过自注意力机制(Self-Attention)同时关注输入序列的前后文。 * 优势:相比单向模型(如 Decoder-only),它可以更全面地捕捉输入序列的全局语义,适合需要理解复杂上下文的任务。 * 实现方式:在训练过程中,不对输入序列进行因果掩码(Causal Masking),允许模型在任何位置访问序列的所有位置。 * 例子:BERT 的 Masked Language Model(MLM)训练任务通过随机遮盖部分单词,依赖左侧和右侧的信息来预测被遮盖的词,即双向建模的典型体现。 2. 适用于理解任务 * 特点:Encoder-only 模型专注于理解输入序列,而不生成输出序列,因此适合处理分类、

By Ne0inhk
手把手教学,DeepSeek-R1微调全流程拆解

手把手教学,DeepSeek-R1微调全流程拆解

手把手教学,DeepSeek-R1微调全流程拆解 原创 极客见识  2025年02月09日 09:02 广东 DeepSeek 通过发布其开源推理模型 DeepSeek-R1 颠覆了 AI 格局,该模型使用创新的强化学习技术,以极低的成本提供与 OpenAI 的 o1 相当的性能。 更令人印象深刻的是,DeepSeek 已将其推理能力提炼成几个较小的模型。这篇文章,我们将使用其蒸馏版本之一引导大家完成 DeepSeek-R1 的整个微调过程。 本文章将演示了如何微调其中一个模型(使用我们自己的自定义思维链数据集),然后保存和部署微调后的模型。 高级推理模型微调 DeepSeek 简介 DeepSeek-R1 是由深度求索(DeepSeek)公司开发的突破性推理模型。DeepSeek-R1 基于 DeepSeek-V3-Base(总共 671B 个参数,每次推理 37B 处于活动状态)构建,使用强化学习 (RL) 在提供最终答案之前生成思路链

By Ne0inhk