Técnicas de optimización de código Java elegantes y prácticas

Continuando con el artículo anterior, continúe compartiendo habilidades de optimización de código Java

Un adelanto del contenido de este artículo:

  • Impresión de registros canónicos

  • Manejo de excepciones canónicas

  • Manejo unificado de excepciones

  • Usar prueba con recurso

  • cerrar recurso

  • No defina excepciones como variables estáticas

  • Otras consideraciones sobre el manejo de excepciones

  • La interfaz no debe devolver directamente el objeto de la base de datos.

  • Valor de retorno de interfaz uniforme

  • Tiempo de espera de configuración de llamada remota

  • Uso correcto del grupo de subprocesos

  • Procesamiento de datos confidenciales

Impresión de registros canónicos

1. No imprima registros a voluntad, asegúrese de que los registros que imprima se puedan usar más adelante.

Imprimir demasiados registros inútiles no solo afecta la solución de problemas, sino que también afecta el rendimiento y aumenta la carga en los discos.

2. Es necesario desensibilizar los datos confidenciales del registro de impresión, como el número de ID, el número de teléfono y la contraseña. Lectura relacionada: Spring Boot completa la desensibilización de registros en 3 pasos, ¡simple y práctico! !

3. Seleccione el nivel de impresión de registro adecuado. Hay cuatro niveles de registro más utilizados: DEBUG, INFO, WARN, ERROR.

  • DEBUG (depuración): registros de desarrollo y depuración, que utilizan los principales desarrolladores durante el proceso de desarrollo y depuración, y la salida de registros DEBUG está prohibida en el entorno de producción.

  • INFO (notificación): información sobre el funcionamiento normal del sistema, registros de algunas interfaces externas, generalmente utilizadas para la resolución de problemas.

  • WARN (advertencia): registro de advertencia, que indica que puede haber un problema con un determinado módulo del sistema, pero no tiene ningún efecto en el funcionamiento normal del sistema.

  • ERROR (error): registro de errores, indicando que puede haber un problema grave en un módulo determinado del sistema, lo que afectará el funcionamiento normal del sistema.

4. Está prohibido generar registros DEBUG en el entorno de producción para evitar imprimir demasiados registros (hay muchos registros DEBUG).

5. La aplicación no puede usar directamente la API en el sistema de registro (Log4j, Logback), pero debe confiar en la API en el marco de registro SLF4J y usar el marco de registro del modo de fachada, que es propicio para el mantenimiento y la unificación de la métodos de procesamiento de registro de cada clase.

Las aplicaciones Spring Boot pueden usar directamente el marco de registro integrado Logback, que se implementa de acuerdo con el estándar API SLF4J.

6. El registro de excepciones debe imprimir la información de excepción completa.

Contador de ejemplo:

try {    //读文件操作    readFile();} catch (IOException e) {    // 只保留了异常消息,栈没有记录    log.error("文件读取错误, {}", e.getMessage());}

Ejemplo positivo:

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

7. Evite imprimir registros capa por capa.

Por ejemplo: el método 1 llama al método 2, el método 2 genera un error e imprime el registro de errores y el método 1 también imprime el registro de errores, lo que equivale a imprimir un registro de errores dos veces.

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]

聊一聊责任链模式:

juejin.cn/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…

Supongo que te gusta

Origin juejin.im/post/7233961132540428349
Recomendado
Clasificación