接上一篇内容,继续分享 Java 代码优化技巧

本篇内容预览:

  • 标准日志打印

  • 标准反常处理

  • 一致反常处理

  • 运用 try-with-resource

  • 封闭资源

  • 不要把反常界说为静态变量

  • 其他反常处理留意事项

  • 接口不要直接回来数据库目标

  • 一致接口回来值

  • 长途调用设置超时时刻

  • 正确运用线程池

  • 灵敏数据处理

标准日志打印

1、不要随意打印日志,保证自己打印的日志是后面能用到的。

打印太多无用的日志不但影响问题排查,还会影响性能,加剧磁盘担负。

2、打印日志中的灵敏数据比方身份证号、电话号、暗码需求进行脱敏。相关阅览:Spring Boot 3 步完成日志脱敏,简略有用!!

3、选择合适的日志打印等级。最常用的日志等级有四个:DEBUG、INFO、WARN、ERROR。

  • DEBUG(调试):开发调试日志,首要开发人员开发调试过程中运用,出产环境制止输出 DEBUG 日志。

  • INFO(告诉):正常的体系运转信息,一些外部接口的日志,一般用于排查问题运用。

  • WARN(正告):正告日志,提示体系某个模块或许存在问题,但对体系的正常运转没有影响。

  • ERROR(过错):过错日志,提示体系某个模块或许存在比较严峻的问题,会影响体系的正常运转。

4、出产环境制止输出 DEBUG 日志,防止打印的日志过多(DEBUG 日志十分多)。

5、运用中不可直接运用日志体系(Log4j、Logback)中的 API,而应依靠运用日志结构 SLF4J 中的 API,运用门面方法的日志结构,有利于保护和各个类的日志处理办法一致。

Spring Boot 运用程序能够直接运用内置的日志结构 Logback,Logback 便是按照 SLF4J API 标准完成的。

6、反常日志需求打印完好的反常信息。

反例:

try {    //读文件操作    readFile();} catch (IOException e) {    // 只保留了反常音讯,栈没有记载    log.error("文件读取过错, {}", e.getMessage());}

正例:

try {    //读文件操作    readFile();} catch (IOException e) {    log.error("文件读取过错", e);}

7、防止层层打印日志。

举个比方:method1 调用 method2,method2 呈现 error 并打印 error 日志,method1 也打印了 error 日志,等同于一个过错日志打印了 2 遍。

8、不要打印日志后又将反常抛出。

反例:

try {    ...} catch (IllegalArgumentException e) {    log.error("呈现反常啦", e);    throw e;}

在日志中会对抛出的一个反常打印多条过错信息。

正例:

try {    ...} catch (IllegalArgumentException e) {    log.error("呈现反常啦", e);}// 或许包装成自界说反常之后抛出try {    ...} catch (IllegalArgumentException e) {    throw new MyBusinessException("一段对反常的描述信息.", e);}

标准反常处理

阿里巴巴 Java 反常处理规约如下:

阿里巴巴 Java 开发手册
 19/38
二、反常日志 
(一)反常处理 
1. 【强制】Java 类库中界说的能够经过预查看办法规避的 RuntimeException 反常不应该经过
catch 的办法来处理,比方:NullPointerException,IndexOutOfBoundsException 等等。
阐明:无法经过预查看的反常除外,比方,在解析字符串方法的数字时,不得不经过 catch
NumberFormatException 来完成。
正例:if (obj != null) {...}
反例:try { obj.method(); } catch (NullPointerException e) {…}
2. 【强制】反常不要用来做流程操控,条件操控。
阐明:反常设计的初衷是处理程序运转中的各种意外状况,且反常的处理效率比条件判别办法
要低许多。
3. 【强制】catch 时请辨明安稳代码和非安稳代码,安稳代码指的是无论如何不会犯错的代码。
关于非安稳代码的 catch 尽或许进行区别反常类型,再做对应的反常处理。
阐明:对大段代码进行 try-catch,使程序无法根据不同的反常做出正确的应激反应,也晦气
于定位问题,这是一种不负职责的体现。
正例:用户注册的场景中,假如用户输入非法字符,或用户名称已存在,或用户输入暗码过于
简略,在程序上作出分门别类的判别,并提示给用户。
4. 【强制】捕获反常是为了处理它,不要捕获了却什么都不处理而抛弃之,假如不想处理它,请
将该反常抛给它的调用者。最外层的事务运用者,有必要处理反常,将其转化为用户能够理解的
内容。
5. 【强制】有 try 块放到了事务代码中,catch 反常后,假如需求回滚事务,必定要留意手动回
滚事务。
6. 【强制】finally 块有必要对资源目标、流目标进行封闭,有反常也要做 try-catch。
阐明:假如 JDK7 及以上,能够运用 try-with-resources 办法。
7. 【强制】不要在 finally 块中运用 return。
阐明:finally 块中的 return 回来后办法结束履行,不会再履行 try 块中的 return 句子。
8. 【强制】捕获反常与抛反常,有必要是完全匹配,或许捕获反常是抛反常的父类。
阐明:假如预期对方抛的是绣球,实践接到的是铅球,就会发生意外状况。
9. 【引荐】办法的回来值能够为 null,不强制回来空调集,或许空目标等,有必要添加注释充分
阐明什么状况下会回来 null 值。
阐明:本手册明确防止 NPE 是调用者的职责。即使被调用办法回来空调集或许空目标,对调用阿里巴巴 Java 开发手册
者来说,也并非高枕无忧,有必要考虑到长途调用失利、序列化失利、运转时反常等场景回来
null 的状况。
10. 【引荐】防止 NPE,是程序员的根本涵养,留意 NPE 发生的场景:
1)回来类型为根本数据类型,return 包装数据类型的目标时,自动拆箱有或许发生 NPE。
 反例:public int f() { return Integer 目标}, 假如为 null,自动解箱抛 NPE。
2) 数据库的查询结果或许为 null3) 调集里的元素即使 isNotEmpty,取出的数据元素也或许为 null4) 长途调用回来目标时,一概要求进行空指针判别,防止 NPE。
5) 关于 Session 中获取的数据,主张 NPE 查看,防止空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易发生 NPE。
正例:运用 JDK8 的 Optional 类来防止 NPE 问题。
11. 【引荐】界说时区别 unchecked / checked 反常,防止直接抛出 new RuntimeException(),
更不允许抛出 Exception 或许 Throwable,应运用有事务含义的自界说反常。引荐业界已界说
过的自界说反常,如:DAOException / ServiceException 等。
12. 【参阅】关于公司外的 http/api 敞开接口有必要运用“过错码”;而运用内部引荐反常抛出;
跨运用间 RPC 调用优先考虑运用 Result 办法,封装 isSuccess()办法、“过错码”、“过错简
短信息”。
阐明:关于 RPC 办法回来办法运用 Result 办法的理由:
1)运用抛反常回来办法,调用方假如没有捕获到就会发生运转时过错。
2)假如不加栈信息,只是 new 自界说反常,加入自己的理解的 error message,关于调用
端处理问题的协助不会太多。假如加了栈信息,在频频调用犯错的状况下,数据序列化和传输
的性能损耗也是问题。
13. 【参阅】防止呈现重复的代码(Don’t Repeat Yourself),即 DRY 原则。
阐明:随意仿制和张贴代码,必然会导致代码的重复,在以后需求修正时,需求修正一切的副
本,简略遗漏。必要时抽取共性办法,或许抽象公共类,乃至是组件化。
正例:一个类中有多个 public 办法,都需求进行数行相同的参数校验操作,这个时分请抽取:
private boolean checkParam(DTO dto) {...} 

一致反常处理

一切的反常都应该由最上层捕获并处理,这样代码更简练,还能够防止重复输出反常日志。 假如咱们都在事务代码中运用try-catch或许try-catch-finally处理的话,就会让事务代码中冗余太多反常处理的逻辑,关于同样的反常咱们还需求重复编写代码处理,还或许会导致重复输出反常日志。这样的话,代码可保护性、可阅览性都十分差。

Spring Boot 运用程序能够借助 @RestControllerAdvice@ExceptionHandler 完成全局一致反常处理。

@RestControllerAdvicepublic class GlobalExceptionHandler {    @ExceptionHandler(BusinessException.class)    public Result businessExceptionHandler(HttpServletRequest request, BusinessException e){        ...        return Result.faild(e.getCode(), e.getMessage());    }    ...}

运用 try-with-resource 封闭资源

  1. 适用范围(资源的界说): 任何完成 java.lang.AutoCloseable或许 java.io.Closeable 的目标

  2. 封闭资源和 finally 块的履行次序:try-with-resources 句子中,任何 catch 或 finally 块在声明的资源封闭后运转

《Effective Java》中明确指出:

面临有必要要封闭的资源,咱们总是应该优先运用 try-with-resources 而不是try-finally。随之发生的代码更简略,更清晰,发生的反常对咱们也更有用。try-with-resources句子让咱们更简略编写有必要要封闭的资源的代码,若选用try-finally则几乎做不到这点。

Java 中类似于InputStreamOutputStreamScannerPrintWriter等的资源都需求咱们调用close()办法来手动封闭,一般状况下咱们都是经过try-catch-finally句子来完成这个需求,如下:

//读取文本文件的内容Scanner scanner = null;try {    scanner = new Scanner(new File("D://read.txt"));    while (scanner.hasNext()) {        System.out.println(scanner.nextLine());    }} catch (FileNotFoundException e) {    e.printStackTrace();} finally {    if (scanner != null) {        scanner.close();    }}

运用 Java 7 之后的 try-with-resources 句子改造上面的代码:

try (Scanner scanner = new Scanner(new File("test.txt"))) {    while (scanner.hasNext()) {        System.out.println(scanner.nextLine());    }} catch (FileNotFoundException fnfe) {    fnfe.printStackTrace();}

当然多个资源需求封闭的时分,运用 try-with-resources 完成起来也十分简略,假如你仍是用try-catch-finally或许会带来许多问题。

经过运用分号分隔,能够在try-with-resources块中声明多个资源。

try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));     BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {    int b;    while ((b = bin.read()) != -1) {        bout.write(b);    }}catch (IOException e) {    e.printStackTrace();}

不要把反常界说为静态变量

不要把反常界说为静态变量,因为这样会导致反常栈信息错乱。每次手动抛出反常,咱们都需求手动 new 一个反常目标抛出。

// 过错做法public class Exceptions {    public static BusinessException ORDEREXISTS = new BusinessException("订单已经存在", 3001);...}

其他反常处理留意事项

  • 抛出完好详细的反常信息(防止 throw new BIZException(e.getMessage()这种方法的反常抛出),尽量自界说反常,而不是直接运用 RuntimeExceptionException

  • 优先捕获详细的反常类型。

  • 捕获了反常之后必定要处理,防止直接吃掉反常。

  • ……

接口不要直接回来数据库目标

接口不要直接回来数据库目标(也便是 DO),数据库目标包括类中一切的属性。

// 过错做法public UserDO getUser(Long userId) {  return userService.getUser(userId);}

原因:

  • 假如数据库查询不做字段限制,会导致接口数据庞大,浪费用户的宝贵流量。

  • 假如数据库查询不做字段限制,简略把灵敏字段露出给接口,导致呈现数据的安全问题。

  • 假如修正数据库目标的界说,接口回来的数据紧跟着也要改动,晦气于保护。

主张的做法是独自界说一个类比方 VO(能够看作是接口回来给前端展示的目标数据)来对接口回来的数据进行挑选,乃至是封装和组合。

public UserVo getUser(Long userId) {  UserDO userDO = userService.getUser(userId);  UserVO userVO = new UserVO();  BeanUtils.copyProperties(userDO, userVO);//演示运用  return userVO;}

一致接口回来值

接口回来的数据必定要一致格局,遮掩更方面临接前端开发的同学以及其他调用该接口的开发。

一般来说,下面这些信息是必备的:

  1. 状况码和状况信息:能够经过枚举界说状况码和状况信息。状况码标识恳求的结果,状况信息归于提示信息,提示成功信息或许过错信息。

  2. 恳求数据:恳求该接口实践要回来的数据比方用户信息、文章列表。

    public enum ResultEnum implements IResult { SUCCESS(2001, “接口调用成功”), VALIDATE_FAILED(2002, “参数校验失利”), COMMON_FAILED(2003, “接口调用失利”), FORBIDDEN(2004, “没有权限访问资源”); private Integer code; private String message; …}public class Result { private Integer code; private String message; private T data; … public static Result success(T data) { return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data); } public static Result<?> failed() { return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null); } …}

关于 Spring Boot 项目来说,能够运用 @RestControllerAdvice 注解+ ResponseBodyAdvic接口一致处理接口回来值,完成代码无侵入。篇幅问题这儿就不贴详细完成代码了,比较简略,详细完成办法能够参阅这篇文章:Spring Boot 无侵入式 完成 API 接口一致 JSON 格局回来[6] 。

需求留意的是,这种办法在 Spring Cloud OpenFeign 的承继方法下是有侵入性,处理办法见:SpringBoot 无侵入式 API 接口一致格局回来,在 Spring Cloud OpenFeign 承继方法具有了侵入性[7] 。

实践项目中,其实运用比较多的仍是下面这种比较直接的办法:

public class PostController { @GetMapping("/list") public R<List<SysPost>> getPosts() {  ...  return R.ok(posts); }}

上面介绍的无侵入的办法,一般改造旧项目的时分用的比较多。

长途调用设置超时时刻

开发过程中,第三方接口调用、RPC 调用以及服务之间的调用主张设置一个超时时刻。

咱们平时接触到的超时能够简略分为下面 2 种:

  • 衔接超时(ConnectTimeout) :客户端与服务端树立衔接的最长等待时刻。

  • 读取超时(ReadTimeout) :客户端和服务端已经树立衔接,客户端等待服务端处理完恳求的最长时刻。实践项目中,咱们重视比较多的仍是读取超时。

一些衔接池客户端结构中或许还会有获取衔接超时和空闲衔接整理超时。

假如没有设置超时的话,就或许会导致服务端衔接数爆炸和很多恳求堆积的问题。这些堆积的衔接和恳求会耗费体系资源,影响新收到的恳求的处理。严峻的状况下,乃至会拖垮整个体系或许服务。

我之前在实践项目就遇到过类似的问题,整个网站无法正常处理恳求,服务器负载直接快被拉满。后面发现原因是项目超时设置过错加上客户端恳求处理反常,导致服务端衔接数直接接近 40w+,这么多堆积的衔接直接把体系干趴了。

正确运用线程池

10 个运用线程池的留意事项:

  1. 线程池有必要手动经过 ThreadPoolExecutor 的构造函数来声明,防止运用 Executors 类创立线程池,会有 OOM 危险。

  2. 监测线程池运转状况。

  3. 主张不同类别的事务用不同的线程池。

  4. 别忘记给线程池命名。

  5. 正确装备线程池参数。

  6. 别忘记封闭线程池。

  7. 线程池尽量不要放耗时任务。

  8. 防止重复创立线程池。

  9. 运用 Spring 内部线程池时,必定要手动自界说线程池,装备合理的参数,否则会呈现出产问题(一个恳求创立一个线程)

  10. 线程池和 ThreadLocal 共用,或许会导致线程从 ThreadLocal 获取到的是旧值/脏数据。

灵敏数据处理

  1. 回来前端的灵敏数据比方身份证号、电话、地址信息要根据事务需求进行脱敏处理,示例:163****892

  2. 保存在数据库中的暗码需求加盐之后运用哈希算法(比方 BCrypt)进行加密。

  3. 保存在数据库中的银行卡号、身份号这类灵敏数据需求运用对称加密算法(比方 AES)保存。

  4. 网络传输的灵敏数据比方银行卡号、身份号需求用 HTTPS + 非对称加密算法(如 RSA)来保证传输数据的安全性。

  5. 关于暗码找回功能,不能明文存储用户暗码。能够选用重置暗码的办法,让用户经过验证身份后重新设置暗码。

  6. 在代码中不应该明文写入密钥、口令等灵敏信息。能够选用装备文件、环境变量等办法来动态加载这些信息。

  7. 定时更新灵敏数据的加密算法和密钥,以保证加密算法和密钥的安全性和有效性。

参阅资料

[1]

美团技能团队:如何高雅地记载操作日志?:

tech.meituan.com/2021/09/16/…

[2]

一个较重的代码坏味:“炫技式”的单行代码:

www.cnblogs.com/lovesqcc/p/…

[3]

Replace Conditional Logic with Strategy Pattern – IDEA:

www.jetbrains.com/help/idea/r…

[4]

聊一聊职责链方法:

/post/716084…

[5]

21 | 代码重复:搞定代码重复的三个绝技 – Java 事务开发常见过错 100 例 :

time.geekbang.org/column/arti…

[6]

Spring Boot 无侵入式 完成 API 接口一致 JSON 格局回来:

blog.csdn.net/qq\_3434762…

[7]

SpringBoot 无侵入式 API 接口一致格局回来,在 Spring Cloud OpenFeign 承继方法具有了侵入性:

blog.csdn.net/qq\_3434762…

[8]

超时&重试详解:

javaguide.cn/high-availa…

[9]

10 个线程池最佳实践和坑!:

javaguide.cn/java/concur…