Java开发经验——日志治理经验
摘要
本文主要介绍了Java开发中的日志治理经验,包括系统异常日志、接口摘要日志、详细日志和业务摘要日志的定义和目的,以及错误码规范和异常处理规范。强调了日志治理的重要性和如何通过规范化错误码和日志格式来提高系统可观测性和问题排查效率。
1. 错误码规范
【强制】错误码的制定原则:快速溯源、沟通标准化。
说明: 错误码想得过于完美和复杂,就像康熙字典中的生僻字一样,用词似乎精准,但是字典不容易随身携带并且简单易懂。
正例: 错误码回答的问题是谁的错?错在哪?
- 错误码必须能够快速知晓错误来源,可快速判断是谁的问题。
- 错误码必须能够进行清晰地比对(代码中容易 equals)。
- 错误码有利于团队快速对错误原因达 到一致认知。
【强制】错误码不体现版本号和错误等级信息。
说明:错误码以不断追加的方式进行兼容。错误等级由日志和错误码本身的释义来决定。
【强制】全部正常,但不得不填充错误码时返回五个零:00000。
【强制】错误码为字符串类型,共5位,分成两个部分:错误产生来源+四位数字编号。
说明: 错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付 超时等问题;B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源 于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的步长间距预留 100。
【强制】编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统一平台上进行,审批生效,编号即被永久固定。
【强制】错误码使用者避免随意定义新的错误码。
说明: 尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。
假设系统已有的错误码表如下:
错误码 | 错误描述 |
| 用户名不存在 |
| 密码错误 |
| 用户未授权 |
| 请求参数缺失 |
| 参数格式错误 |
不建议的做法:当遇到一个新的错误情况时,比如“用户输入的邮箱地址格式错误”,如果开发者随意定义新的错误码:
public static final int ERROR_INVALID_EMAIL_FORMAT = 4001; // 随意定义错误码
推荐的做法:首先,检查现有的错误码表中是否有适合的错误码。如果已存在类似的错误码,如3002
(参数格式错误),可以在代码中使用已有的错误码,或者选择一个相关的错误码。
public static final int ERROR_INVALID_EMAIL_FORMAT = 3002; // 采用已有的错误码
如果系统的错误码表没有类似的错误码,可以考虑增加新的错误码,但应该尽量确保其语义清晰,避免误解:
public static final int ERROR_INVALID_EMAIL_FORMAT = 4002; // 增加新的错误码,避免与其他错误码冲突
【强制】错误码不能直接输出给用户作为提示信息使用。
说明: 堆栈(stack_trace)、错误信息(error_message)、错误码(error_code)、提示信息(user_tip) 是一个有效关联并互相转义的和谐整体,但是请勿互相越俎代庖。
错误码不应该直接作为提示信息输出给用户。错误码是开发人员用于定位问题、分析故障的工具,而不是直接与用户交互的内容。错误码应该与用户提示信息分开,避免让用户看到冗长或无意义的错误码,从而影响用户体验。
错误处理的正确做法:
// 错误码定义
public static final int ERROR_INVALID_EMAIL_FORMAT = 4002;// 错误处理方法
public void handleInvalidEmail(String email) {try {// 检查邮箱格式是否有效if (!isValidEmail(email)) {// 如果邮箱格式无效,抛出自定义异常throw new CustomException(ERROR_INVALID_EMAIL_FORMAT, "邮箱格式错误");}} catch (CustomException e) {// 输出堆栈信息给开发者e.printStackTrace();// 记录错误信息logError(e.getErrorCode(), e.getErrorMessage());// 给用户友好的提示信息showUserTip("请输入有效的邮箱地址,例如 user@example.com。");}
}
错误信息输出:
- 开发者:看到
ERROR_INVALID_EMAIL_FORMAT
和详细的错误信息(如邮箱格式错误
)以及堆栈信息,用于定位代码中的问题。 - 用户:看到简洁的提示信息
"请输入有效的邮箱地址,例如 user@example.com。"
,这是用户可以理解并采取相应行动的内容。
错误码与提示信息的区分:
- 错误码:
ERROR_INVALID_EMAIL_FORMAT
(开发者使用)。 - 错误信息:
邮箱格式错误
(开发者记录日志时使用)。 - 用户提示信息:
请输入有效的邮箱地址,例如 user@example.com。
(直接展示给用户)。
不推荐的做法:
public void handleInvalidEmail(String email) {try {if (!isValidEmail(email)) {// 错误码直接暴露给用户throw new CustomException(4002, "错误码 4002: 邮箱格式错误");}} catch (CustomException e) {// 将错误码直接展示给用户showUserTip("错误码 4002: 邮箱格式错误");}
}
【推荐】在获取第三方服务错误码时,向上抛出允许本系统转义,由C转为B,并且在错误信息上带上原有的第三方错误码。
在系统集成时,往往需要将第三方服务的错误码转化为本系统的错误码,这样既能让系统的错误码保持一致性,又能确保第三方错误信息不丢失,并提供给用户或开发者必要的上下文信息。为了实现这一目标,可以通过异常机制将第三方错误码捕获后转义为本系统的错误码,并且在错误信息中附带原有的第三方错误码,确保错误的追踪和调试不受影响。
第三方错误码转义
假设我们的系统调用了一个第三方支付服务,而该支付服务返回的错误码格式为 TS_ERR_1001
。我们希望将这个错误码转义为系统内部的错误码 5001
,并且将第三方错误码和错误信息一起传递到本系统。
定义第三方错误码和系统内部错误码
// 第三方错误码
public static final String THIRD_PARTY_ERR_1001 = "TS_ERR_1001";
public static final String THIRD_PARTY_ERR_1002 = "TS_ERR_1002";// 系统内部错误码
public static final int ERROR_PAYMENT_FAILED = 5001;
public static final int ERROR_INVALID_PAYMENT_METHOD = 5002;
封装第三方错误码的异常类
首先,定义一个自定义异常类,用于封装来自第三方服务的错误信息,并在其中记录第三方错误码。
public class ThirdPartyException extends Exception {private final String thirdPartyErrorCode;public ThirdPartyException(String message, String thirdPartyErrorCode) {super(message);this.thirdPartyErrorCode = thirdPartyErrorCode;}public String getThirdPartyErrorCode() {return thirdPartyErrorCode;}
}
服务调用和错误处理
在服务调用过程中,如果第三方服务返回错误,我们可以通过异常处理将第三方错误转义为本系统的错误,并在错误信息中附带第三方的错误码。
public class PaymentService {public void processPayment(String paymentDetails) throws CustomException {try {// 调用第三方支付服务并捕获其可能抛出的异常String thirdPartyErrorCode = callThirdPartyPaymentService(paymentDetails);if (thirdPartyErrorCode != null) {// 根据第三方错误码转义为系统内部的错误码throw new ThirdPartyException("支付失败,第三方服务错误", thirdPartyErrorCode);}} catch (ThirdPartyException e) {// 转义第三方错误码为本系统的错误码if (e.getThirdPartyErrorCode().equals(THIRD_PARTY_ERR_1001)) {throw new CustomException(ERROR_PAYMENT_FAILED, "支付失败,错误码: " + e.getThirdPartyErrorCode());} else if (e.getThirdPartyErrorCode().equals(THIRD_PARTY_ERR_1002)) {throw new CustomException(ERROR_INVALID_PAYMENT_METHOD, "无效支付方式,错误码: " + e.getThirdPartyErrorCode());} else {// 处理其他未知的错误码throw new CustomException(9999, "未知错误,错误码: " + e.getThirdPartyErrorCode());}}}private String callThirdPartyPaymentService(String paymentDetails) {// 模拟调用第三方支付服务,返回错误码return THIRD_PARTY_ERR_1001; // 假设返回的是第三方的错误码}
}
自定义异常类 (CustomException)
为了便于处理系统内部的错误,可以创建一个自定义异常类 CustomException
,将错误码和错误信息封装起来。
public class CustomException extends Exception {private final int errorCode;private final String errorMessage;public CustomException(int errorCode, String errorMessage) {super(errorMessage);this.errorCode = errorCode;this.errorMessage = errorMessage;}public int getErrorCode() {return errorCode;}public String getErrorMessage() {return errorMessage;}
}
调用并处理错误
调用服务时,通过异常捕获机制可以得到转义后的错误信息。
public class Main {public static void main(String[] args) {PaymentService paymentService = new PaymentService();try {paymentService.processPayment("paymentDetails");} catch (CustomException e) {// 打印系统内部错误码和错误信息System.out.println("系统错误码: " + e.getErrorCode());System.out.println("错误信息: " + e.getErrorMessage());}}
}
错误处理流程总结:
- 第三方服务错误:当第三方服务返回错误时,抛出
ThirdPartyException
异常,并带上第三方的错误码。 - 错误码转义:系统通过捕获该异常,根据第三方错误码转义为本系统的错误码,同时在错误信息中带上第三方的错误码,帮助开发者调试。
- 错误信息反馈给用户:系统向用户返回的错误提示信息不包含技术细节(如第三方错误码),而是使用系统错误码和友好的提示信息。
【推荐】在获取第三方服务错误码时,向上抛出允许本系统转义,由C转为B,并且在错误信 息上带上原有的第三方错误码。
【参考】错误码分为一级宏观错误码、二级宏观错误码、三级宏观错误码。 说明:在无法更加具体确定的错误场景中,可以直接使用一级宏观错误码,分别是:A0001(用户端错误)、 B0001(系统执行出错)、C0001(调用第三方服务出错)。
正例: 调用第三方服务出错是一级,中间件错误是二级,消息服务出错是三级。
【参考】错误码的后三位编号与 HTTP 状态码没有任何关系。
【参考】错误码有利于不同文化背景的开发者进行交流与代码协作。
说明:英文单词形式的错误码不利于非英语母语国家(如阿拉伯语、希伯来语、俄罗斯语等)之间的开发 者互相协作。
【参考】错误码即人性,感性认知+口口相传,使用纯数字来进行错误码编排不利于感性记忆和分类。
说明: 数字是一个整体,每位数字的地位和含义是相同的。
反例: 一个五位数字 12345,第 1 位是错误等级,第 2 位是错误来源,345 是编号,人的大脑不会主动地 拆开并分辨每位数字的不同含义。
2. 异常处理规范
【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不
通过catch NumberFormatException来实现。
正例:if(obj!=nul){}
反例:try{ obj.method(); } catch ( NullPointerException e){ . }
错误的做法:使用异常进行流程控制
public class OrderService {public void processOrder(int orderId) {try {// 用异常捕获条件控制的流程if (orderId <= 0) {throw new IllegalArgumentException("订单ID无效");}// 正常的订单处理逻辑System.out.println("处理订单 " + orderId);} catch (IllegalArgumentException e) {// 处理异常:用异常控制流程,这种做法是错误的System.out.println("捕获异常:订单ID无效");}}
}
问题:在上面的代码中,使用 throw
和 catch
来控制流程判断 orderId
是否有效,这明显是不合适的。orderId <= 0
只是一个普通的条件检查,完全可以使用条件判断来处理,而不需要抛出异常。
正确的做法:使用条件判断来控制流程
public class OrderService {public void processOrder(int orderId) {// 使用条件判断处理正常的逻辑流程if (orderId <= 0) {System.out.println("订单ID无效");return; // 直接返回,不继续处理}// 正常的订单处理逻辑System.out.println("处理订单 " + orderId);}
}
错误的异常用法会导致性能问题
假设我们在一个循环中每次都要检查条件,并且抛出异常:
public void processOrders(List<Integer> orderIds) {for (int orderId : orderIds) {try {// 通过异常进行流程控制if (orderId <= 0) {throw new IllegalArgumentException("订单ID无效");}// 处理订单System.out.println("处理订单 " + orderId);} catch (IllegalArgumentException e) {System.out.println("捕获异常:订单ID无效");}}
}
这种写法会导致:
- 每次循环都会进行异常处理,严重影响性能。
- 异常捕获会增加系统的资源消耗,特别是在异常发生频繁时。
public void processOrders(List<Integer> orderIds) {for (int orderId : orderIds) {// 使用条件判断进行流程控制if (orderId <= 0) {System.out.println("订单ID无效");continue; // 直接跳过这个订单,继续下一个}// 处理有效订单System.out.println("处理订单 " + orderId);}
}
【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。 对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例: 用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请 将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
【强制】事务场景中,抛出异常被catch后,如果需要回滚,一定要注意手动回滚事务。
在实际项目中,通常会使用 Spring 框架的事务管理来简化事务的控制。Spring 提供了 @Transactional
注解,它会自动处理事务的开始、提交、回滚等过程。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;@Service
public class BankService {@Autowiredprivate AccountRepository accountRepository;@Transactionalpublic void transferFunds(int fromAccountId, int toAccountId, double amount) {// 减少账户余额Account fromAccount = accountRepository.findById(fromAccountId);if (fromAccount.getBalance() < amount) {throw new IllegalArgumentException("余额不足");}fromAccount.setBalance(fromAccount.getBalance() - amount);accountRepository.save(fromAccount);// 增加目标账户余额Account toAccount = accountRepository.findById(toAccountId);if (toAccount == null) {throw new IllegalArgumentException("目标账户不存在");}toAccount.setBalance(toAccount.getBalance() + amount);accountRepository.save(toAccount);// 如果这里抛出异常,Spring 会自动回滚事务}
}
Spring 事务管理的特点:
- 自动管理事务:使用
@Transactional
注解时,Spring 会自动开启事务,并且在方法执行结束后自动提交或回滚事务。 - 异常回滚:如果方法执行过程中抛出了异常,Spring 会自动回滚事务。默认情况下,
RuntimeException
和Error
会导致回滚,而checked exception
默认不会回滚(可以通过@Transactional(rollbackFor = Exception.class)
指定回滚条件)。 - 简化代码:无需手动管理事务的开始、提交和回滚,极大简化了代码。
【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。
说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
finally
块关闭资源(传统做法)
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class ResourceManagement {public void readFile(String filePath) {InputStream inputStream = null;try {inputStream = new FileInputStream(filePath);// 执行文件读取操作System.out.println("读取文件内容");} catch (IOException e) {// 处理异常System.out.println("文件读取异常:" + e.getMessage());} finally {// 确保资源被关闭if (inputStream != null) {try {inputStream.close();} catch (IOException e) {System.out.println("关闭流时发生异常:" + e.getMessage());}}}}public static void main(String[] args) {ResourceManagement rm = new ResourceManagement();rm.readFile("test.txt");}
}
问题分析:
finally
块用于确保资源被关闭,即使在try
或catch
块中发生异常。- 但在
finally
块中,关闭流时仍然可能抛出异常,这时需要在finally
块内再嵌套一个try-catch
来捕获异常。 - 代码冗长,且资源关闭的逻辑分散在不同的地方,可能增加出错的风险。
JDK 7 及以上的 try-with-resources
(推荐做法)
从 JDK 7 开始,引入了 try-with-resources
语法,能够自动关闭实现了 AutoCloseable
接口(如 InputStream
、OutputStream
、Connection
等)的资源。它通过实现 AutoCloseable
接口的 close()
方法来确保资源的关闭,因此我们不再需要显式地在 finally
块中关闭资源。
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;public class ResourceManagement {public void readFile(String filePath) {// 使用 try-with-resources 语法自动关闭资源try (InputStream inputStream = new FileInputStream(filePath)) {// 执行文件读取操作System.out.println("读取文件内容");} catch (IOException e) {// 处理异常System.out.println("文件读取异常:" + e.getMessage());}// 无需显式关闭流,资源会自动关闭}public static void main(String[] args) {ResourceManagement rm = new ResourceManagement();rm.readFile("test.txt");}
}
try-with-resources
的优势:
- 自动关闭资源:当
try-with-resources
块执行完毕时,JVM 会自动调用资源的close()
方法,无需显式地调用close()
,避免了资源泄漏。 - 代码更简洁:代码更加简洁且易读,避免了冗长的
finally
块。 - 异常处理更简便:如果资源在关闭过程中抛出异常,它会被添加到原有异常的
suppressed
异常中,而不需要额外的try-catch
。
【强制】不要在 finally 块中使用 return。
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存 在return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
为什么不能在 finally
块中使用 return
:
- 覆盖返回值:
finally
中的return
会覆盖try
或catch
中的返回值,导致原本应该返回的值被替换。 - 影响程序逻辑:
finally
中的return
会导致无法清晰判断哪个返回值应该被执行。 - 可维护性差:其他开发者阅读代码时,会对
finally
中的return
产生困惑,不知道程序到底返回了什么。
错误示例:在 finally
块中使用 return
public class ReturnInFinallyExample {public int exampleMethod() {int result = 0;try {result = 10;// 模拟某种异常if (result == 10) {throw new Exception("Exception in try block");}} catch (Exception e) {result = 20;} finally {// 错误:在 finally 块中使用 returnreturn 30; // 这会覆盖 try 或 catch 中的返回值}// 这个返回值永远不会被执行return result; // 这个返回值将被覆盖}public static void main(String[] args) {ReturnInFinallyExample example = new ReturnInFinallyExample();System.out.println(example.exampleMethod()); // 输出 30,而不是 20 或 10}
}
正确的做法:避免在 finally
块中使用 return
通常,我们应该在 finally
块中执行必要的清理工作,而不是返回值。如果确实需要返回值,可以将返回值的处理放在 try
或 catch
中,确保 finally
块中不会有返回语句。
public class ReturnInFinallyExample {public int exampleMethod() {int result = 0;try {result = 10;// 模拟某种异常if (result == 10) {throw new Exception("Exception in try block");}} catch (Exception e) {result = 20;} finally {// 在 finally 中进行清理工作,但不返回值System.out.println("Finally block executed");}// 返回结果return result;}public static void main(String[] args) {ReturnInFinallyExample example = new ReturnInFinallyExample();System.out.println(example.exampleMethod()); // 输出 20}
}
【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明: 如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable类来进行拦截。
说明: 通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出 NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配, 或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代 码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
在调用 RPC (Remote Procedure Call)、二方包(即第三方库)或 动态生成类 的相关方法时,捕捉异常需要使用 Throwable
类来进行拦截,而不仅仅是捕捉 Exception
。原因是,Throwable
是 Java 中所有错误和异常的父类,包含了两种类型的对象:
Exception
:通常用于表示程序中能够预料到的异常情况。Error
:表示 JVM 或底层运行环境发生的严重错误(例如OutOfMemoryError
、StackOverflowError
等),通常不可恢复。
在调用这些方法时,尤其是在网络请求、动态类加载或不受控制的第三方库调用中,可能会遇到 Error
类型的错误,导致系统崩溃。因此,捕获 Throwable
可以确保所有可能的错误(无论是 Exception
还是 Error
)都能被捕获,并做出相应处理。
捕捉 Throwable
类的示例:假设我们有一个 RPC 调用或二方包的调用,可能会抛出未知的 Error
或 Exception
,我们希望在捕获异常时处理所有情况。
捕捉 Throwable
类来拦截所有异常和错误
public class RpcClient {public void makeRpcCall() {try {// 假设这是一个 RPC 调用,可能会抛出异常或错误callRemoteService();} catch (Throwable t) {// 捕捉所有异常和错误System.out.println("捕获到异常或错误: " + t.getMessage());// 这里可以做异常处理或者记录日志等}}private void callRemoteService() throws Exception {// 模拟远程调用抛出异常throw new Exception("远程服务调用失败");}public static void main(String[] args) {RpcClient client = new RpcClient();client.makeRpcCall();}
}
为什么需要捕捉 Throwable
:
- RPC 调用、二方包、动态生成类的操作可能引发
Error
:这些操作可能会遇到网络中断、服务不可用等导致的Error
,比如OutOfMemoryError
或UnknownHostException
,这些是我们通常不预期的严重错误,但它们仍然会影响程序的正常运行。 - 无法预见的异常和错误:调用外部服务时,可能会遇到一些底层错误(例如,JVM 内存问题等),这些错误是
Exception
类无法捕获的。 - 保证稳定性:通过捕获
Throwable
,可以确保程序能够稳健地处理异常和错误,而不至于因为未处理的Error
导致程序崩溃。
动态生成类的调用:假设我们使用反射或动态代理来调用一些方法,这些方法可能会抛出异常或错误,我们同样需要使用 Throwable
来捕获所有可能的异常和错误。
通过反射调用动态生成的类方法时捕捉 Throwable
import java.lang.reflect.Method;public class DynamicMethodInvocation {public void invokeMethod() {try {// 动态生成或反射调用某个类的方法Class<?> clazz = Class.forName("com.example.SomeService");Object instance = clazz.getDeclaredConstructor().newInstance();Method method = clazz.getMethod("someMethod");method.invoke(instance);} catch (Throwable t) {// 捕捉所有异常和错误System.out.println("捕获到异常或错误: " + t.getMessage());// 可以进一步处理或者记录错误}}public static void main(String[] args) {DynamicMethodInvocation invocation = new DynamicMethodInvocation();invocation.invokeMethod();}
}
二方包调用时捕捉 Throwable
假设我们使用第三方库来发送消息或执行某些操作,这些操作可能会抛出 Exception
或 Error
。
捕捉二方包的异常和错误
import org.apache.commons.lang3.StringUtils;public class ThirdPartyLibraryUsage {public void useLibraryMethod() {try {// 使用一个第三方库(例如 Apache Commons Lang)String input = null;String result = StringUtils.capitalize(input); // 可能会抛出异常System.out.println(result);} catch (Throwable t) {// 捕捉所有异常和错误System.out.println("捕获到异常或错误: " + t.getMessage());// 进行适当的错误处理}}public static void main(String[] args) {ThirdPartyLibraryUsage usage = new ThirdPartyLibraryUsage();usage.useLibraryMethod();}
}
捕捉 Throwable
的优缺点:
优点:
- 全局捕获异常和错误:可以保证程序不会因为未处理的
Error
崩溃,能够适当地记录、处理或者上报这些错误。 - 稳定性:通过捕获所有
Throwable
类型的对象,程序能够尽量保证即使出现无法预见的错误,也能稳定运行。
缺点:
- 过度捕获:捕获
Throwable
会捕获所有错误类型,包括Error
,例如OutOfMemoryError
等严重错误。如果这些错误发生时,程序可能已经处于无法恢复的状态,捕获并继续执行可能并不合适。对于严重错误,通常应该让程序终止并进行适当的资源释放。 - 异常处理不够精确:捕获
Throwable
可能会掩盖一些具体的异常类型,导致代码的异常处理逻辑不够明确。
【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说 明什么情况下会返回 null 值。
说明: 本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也 并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
- 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。反例: public int f() { return Integer 对象}, 如果为 null,自动解箱抛 NPE。
- 数据库的查询结果可能为 null。
- 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
- 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
- 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
- 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例: 使用 JDK8 的 Optional 类来防止 NPE 问题。
常见的导致 NPE 的场景
- 调用 null 对象的方法
- 访问 null 对象的属性
- 尝试对 null 数组元素进行操作
- 集合对象为 null 时进行操作
- 反射调用 null 对象
防止 NPE 的最佳实践:
- 空值检查:在操作对象之前总是进行空值检查。
Optional
使用:在不确定对象是否为null
时,可以使用Optional
来避免直接操作null
。- 使用工具类:使用
Objects.requireNonNull()
来验证传入对象不为null
,从而避免出现 NPE。 - 尽量避免返回
null
:如果可能,避免返回null
,使用空对象或者Optional
类型来代替。 - 使用现代 API:Java 8 引入的
Stream
、Optional
等 API 提供了更多处理空值的优雅方式,避免了直接与null
打交道。
- NullPointerException (NPE) 常发生在访问
null
对象的方法、属性、数组元素时。避免 NPE 是编程的基本修养。 - 预防 NPE 的方法:检查对象是否为
null
,使用Optional
类型,使用 Java 8 的流式操作,避免返回null
,以及使用Objects.requireNonNull()
来明确断言非空。 - 使用现代工具和 API:如
Optional
、Stream
、Objects
等工具类,可以大大减少由于null
导致的问题。
【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(), 更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。
推荐业界已定义过的自定义异常,如:DAOException/ServiceException等。
【参考】对于公司外的 http/api 开放接口必须使用errorCode; 而应用内部推荐异常抛出; 跨应用间 RPC 调用优先考虑使用 Result方式,封装 isSuccess()方法、errorCode、 errorMessage;而应用内部直接抛出异常即可。
说明: 关于 RPC 方法返回方式使用 Result 方式的理由:
- 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
- 如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题 的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
3. 日志规约
【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 (SLF4J、JCL--Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和 各个类的日志处理方式统一。
说明:日志框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)
// 使用 SLF4J:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class); // 使用 JCL:
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static final Log log = LogFactory.getLog(Test.class);
【强制】所有日志文件至少保存15天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下,过往日志 格式为: {logname}.log.{保存日期},日期格式:yyyy-MM-dd
正例: 以 aap 应用为例,日志保存在/home/admin/aapserver/logs/aap.log,历史日志名称为 app.log.2016-08-01
【强制】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存 的日志不少于六个月,并且进行网络多机备份。
【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType:日志类型,如 stats/monitor/access 等;logName:日志描 述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
说明: 推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
正例: mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log
【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明: 因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例: logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。
说明:虽然在 debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT)为真时(Slf4j 的常见实现
- Log4j 和 Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName())
- 这种参数内有 getName()方法调用,无谓浪费方法调用的开销。
正例:
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {logger.debug("Current ID is: {} and name is: {}", id, getName());
}
【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false。 正例:<logger name="com.taobao.dubbo.config" additivity="false">
【强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用 e.printStackTrace()打印异常堆栈。
说明: 标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易 造成文件大小超过操作系统大小限制。
错误示例:直接使用 System.out
或 System.err
public class BadLoggingExample {public static void main(String[] args) {try {// 模拟一些代码,可能会抛出异常int result = 10 / 0;} catch (Exception e) {// 错误做法:直接使用 System.err 打印异常堆栈System.err.println("An error occurred: " + e.getMessage());e.printStackTrace(); // 这会将堆栈信息打印到控制台}// 错误做法:直接使用 System.out 打印日志System.out.println("This is a log message");}
}
正确示例:使用日志框架(如 Logback)
<dependencies><!-- SLF4J API --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.0</version></dependency><!-- Logback 实现 --><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.4.7</version></dependency>
</dependencies>
配置 logback.xml
然后在项目的 src/main/resources
目录下创建 logback.xml
文件,配置日志输出到控制台和文件:
<configuration><!-- 控制台输出 --><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern></encoder></appender><!-- 文件输出 --><appender name="file" class="ch.qos.logback.core.FileAppender"><file>logs/app.log</file><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern></encoder></appender><!-- 根日志配置 --><root level="INFO"><appender-ref ref="console" /><appender-ref ref="file" /></root></configuration>
使用 SLF4J
记录日志
在代码中使用 SLF4J API 来记录日志,替换掉 System.out
和 System.err
。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class GoodLoggingExample {// 创建日志记录器private static final Logger logger = LoggerFactory.getLogger(GoodLoggingExample.class);public static void main(String[] args) {try {// 模拟一些代码,可能会抛出异常int result = 10 / 0;} catch (Exception e) {// 使用 SLF4J 记录异常信息logger.error("An error occurred: {}", e.getMessage(), e); // 记录异常及堆栈信息}// 使用 SLF4J 记录普通日志logger.info("This is a log message");}
}
【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。
正例: logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e);
【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。
说明: 如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流 程的执行。
正例: 打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。
在日志打印时,直接使用 JSON 工具将对象转换成 String
(例如,使用 JSON.toJSONString()
或 ObjectMapper.writeValueAsString()
等)并不推荐。原因如下:
- 性能问题:将对象转换为 JSON 字符串可能会导致性能问题,尤其是在高并发环境下,频繁进行对象的序列化会占用不必要的资源。
- 日志污染:如果对象包含大量字段或者嵌套对象,直接将整个对象序列化为 JSON 字符串会导致日志输出过长,增加阅读和排查问题的难度。
- 敏感信息泄露:直接序列化整个对象可能会泄露敏感信息,尤其是当对象包含密码、身份标识符或其他敏感数据时。
错误做法:直接使用 JSON 工具将对象转换成 String
错误做法 - 直接使用 JSON.toJSONString()
或 ObjectMapper.writeValueAsString()
import com.alibaba.fastjson.JSON;public class BadLoggingExample {public static void main(String[] args) {MyObject obj = new MyObject("Alice", 30, "Engineer");// 错误做法:直接使用 JSON.toJSONString 打印整个对象的 JSON 字符串System.out.println(JSON.toJSONString(obj)); // 直接打印整个对象的 JSON 字符串}
}class MyObject {private String name;private int age;private String job;public MyObject(String name, int age, String job) {this.name = name;this.age = age;this.job = job;}// getters and setters
}
正确做法:通过日志框架打印日志,并记录必要的字段
<dependencies><!-- SLF4J API --><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.0</version></dependency><!-- Logback 实现 --><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.4.7</version></dependency>
</dependencies>
配置 Logback: 在 src/main/resources/logback.xml
中配置日志输出格式:
<configuration><!-- 控制台输出 --><appender name="console" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss} - %msg%n</pattern></encoder></appender><!-- 根日志配置 --><root level="INFO"><appender-ref ref="console" /></root></configuration>
通过 SLF4J 记录日志,避免直接将对象转换为 JSON 字符串,输出必要的字段:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class GoodLoggingExample {// 创建日志记录器private static final Logger logger = LoggerFactory.getLogger(GoodLoggingExample.class);public static void main(String[] args) {MyObject obj = new MyObject("Alice", 30, "Engineer");// 正确做法:只记录对象的必要信息logger.info("User Info - Name: {}, Age: {}, Job: {}", obj.getName(), obj.getAge(), obj.getJob());}
}class MyObject {private String name;private int age;private String job;public MyObject(String name, int age, String job) {this.name = name;this.age = age;this.job = job;}// getterspublic String getName() {return name;}public int getAge() {return age;}public String getJob() {return job;}
}
防止敏感信息泄露
为了防止将敏感信息泄露到日志中,应该确保:
- 只记录对象的必要信息:在日志中记录关键信息,如用户的用户名、请求类型等,而非整个对象或敏感字段(如密码、Token 等)。
- 自定义日志输出格式:通过日志框架定制日志输出,避免将整个对象序列化为字符串输出。
- 脱敏处理:如果必须记录一些敏感信息,确保通过脱敏方法将敏感数据遮蔽或加密。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class SecureLoggingExample {private static final Logger logger = LoggerFactory.getLogger(SecureLoggingExample.class);public static void main(String[] args) {User user = new User("Alice", "secretPassword", 30);// 只记录用户的非敏感信息logger.info("User Info - Name: {}, Age: {}", user.getName(), user.getAge());// 如果必须记录敏感信息,进行脱敏处理logger.info("User Info with sensitive data: Name: {}, Password: {}", user.getName(), "[PROTECTED]");}
}class User {private String name;private String password;private int age;public User(String name, String password, int age) {this.name = name;this.password = password;this.age = age;}// getterspublic String getName() {return name;}public String getPassword() {return password;}public int getAge() {return age;}
}
总结:
- 避免直接使用 JSON 工具将对象转换为
String
。直接将对象序列化为 JSON 字符串会导致日志冗长、性能下降并可能泄露敏感信息。 - 使用日志框架(如 SLF4J 和 Logback),并只记录必要的字段或关键信息,而不是整个对象的 JSON 字符串。
- 敏感信息保护:避免在日志中记录敏感信息,如密码、Token 等,必要时可以进行脱敏处理。
【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑 爆,并记得及时删除这些观察日志。
说明: 大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些 日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
说明: 注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。
【推荐】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息。
博文参考
《阿里巴巴java开发规范》
相关文章:

Java开发经验——日志治理经验
摘要 本文主要介绍了Java开发中的日志治理经验,包括系统异常日志、接口摘要日志、详细日志和业务摘要日志的定义和目的,以及错误码规范和异常处理规范。强调了日志治理的重要性和如何通过规范化错误码和日志格式来提高系统可观测性和问题排查效率。 1. …...

使用复数类在C#中轻松绘制曼德布洛集分形
示例在 C# 中绘制曼德布洛特集分形解释了如何通过迭代以下方程来绘制曼德布洛特集: 其中 Z(n) 和 C 是复数。程序迭代此方程,直到 Z(n) 的大小至少为 2 或程序执行最大迭代次数。 该示例在单独的变量中跟踪数字的实部和虚部。此示例使用Complex类来更轻松…...

VSCode 启用免费 Copilot
升级VSCode到 1.96版本,就可以使用每个月2000次免费额度了,按照工作日每天近80次免费额度,满足基本需求。前两天一直比较繁忙,今天周六有时间正好体验一下。 引导插件安装GitHub Copilot - Visual Studio Marketplace Extension f…...

常见问题整理
DevOps 和 CICD DevOps 全称Development & Operation 一种实现开发和运维一体化的协同模式,提供快速交付应用和服务的能力 用于协作:开发,部署,质量测试 整体生命周期工作内容,最终实现持续继承,持续部…...

使用Vue创建前后端分离项目的过程(前端部分)
前端使用Vue.js作为前端开发框架,使用Vue CLI3脚手架搭建项目,使用axios作为HTTP库与后端API交互,使用Vue-router实现前端路由的定义、跳转以及参数的传递等,使用vuex进行数据状态管理,后端使用Node.jsexpress…...

【Springboot知识】Redis基础-springboot集成redis相关配置
文章目录 1. 添加依赖2. 配置Redis连接3. 配置RedisTemplate(可选)4. 使用RedisTemplate或StringRedisTemplate5. 测试和验证 集群配置在application.properties中配置在application.yml中配置 主从配置1. 配置Redis服务器使用配置文件使用命令行 2. 配置…...

网络安全概论——身份认证
一、身份证明 身份证明可分为以下两大类 身份验证——“你是否是你所声称的你?”身份识别——“我是否知道你是谁?” 身份证明系统设计的三要素: 安全设备的系统强度用户的可接受性系统的成本 实现身份证明的基本途径 所知:个…...

OpenHarmony-4.HDI 框架
HDI 框架 1.HDI介绍 HDI(Hardware Device Interface,硬件设备接口)是HDF驱动框架为开发者提供的硬件规范化描述性接口,位于基础系统服务层和设备驱动层之间,是连通驱动程序和系统服务进行数据流通的桥梁,是…...

leecode494.目标和
这道题目第一眼感觉就不像是动态规划,可以看出来是回溯问题,但是暴力回溯超时,想要用动态规划得进行一点数学转换 class Solution { public:int findTargetSumWays(vector<int>& nums, int target) {int nnums.size(),bagWeight0,s…...

在Spring中application 的配置属性(详细)
application 的配置属性。 这些属性是否生效取决于对应的组件是否声明为 Spring 应用程序上下文里的 Bean (基本是自动配置 的),为一个不生效的组件设置属性是没有用的。 multipart multipart.enabled 开启上传支持(默认&a…...

jvm符号引用和直接引用
在解析阶段中,符号引用和直接引用是Java类加载和内存管理中的重要概念,它们之间存在显著的区别。以下是对这两个概念的详细解析: 一、定义与特性 符号引用(Symbolic Reference) 定义:符号引用是编译器生成的用于表示类、方法、字段等的引用方式。特性: 独立性:符号引用…...

一文流:JVM精讲(多图提醒⚠️)
一文流系列是作者苦于技术知识学了-忘了,背了-忘了的苦恼,决心把技术知识的要点一笔笔✍️出来,一图图画出来,一句句讲出来,以求刻在🧠里。 该系列文章会把核心要点提炼出来,以求掌握精髓,至于其他细节,写在文章里,留待后续回忆。 目前进度请查看: :::info https:/…...

python 分段拟合笔记
效果图: 源代码: import numpy as np import cv2 import matplotlib.pyplot as plt from numpy.polynomial.polynomial import Polynomialdef nihe(x_points,y_points,p_id):# 按照 p_id 将 points 分成两组group_0_x = []group_0_y = []group_1_x = []group_1_y = []for i, …...

Mysql索引类型总结
按照数据结构维度划分: BTree 索引:MySQL 里默认和最常用的索引类型。只有叶子节点存储 value,非叶子节点只有指针和 key。存储引擎 MyISAM 和 InnoDB 实现 BTree 索引都是使用 BTree,但二者实现方式不一样(前面已经介…...

数据结构——队列的模拟实现
大家好,上一篇博客我带领大家进行了数据结构当中的栈的模拟实现 今天我将带领大家实现一个新的数据结构————队列 一:队列简介 首先来认识一下队列: 队列就像我们上学时的排队一样,有一个队头也有一个队尾。 有人入队的话就…...

在window环境下安装openssl生成钥私、证书和签名,nodejs利用express实现ssl的https访问和测试
在生成我们自己的 SSL 证书之前,让我们创建一个简单的 Express应用程序。 要创建一个新的 Express 项目,让我们创建一个名为node-ssl -server 的目录,用终端cmd中进入node-ssl-server目录。 cd node-ssl-server 然后初始化一个新的 npm 项目…...

Redis 最佳实践
这是以前写下来的文章,发出来备份一下 Redis 在企业中的最佳实践可以帮助提高性能、可用性和数据管理效率。以下是一些推荐的做法: 选择合适的数据结构: 根据需求选择适当的 Redis 数据结构(如 Strings、Lists、Sets、Hashes、So…...

网站灰度发布?Tomcat的8005、8009、8080三个端口的作用什么是CDNLVS、Nginx和Haproxy的优缺点服务器无法开机时
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默, 忍不住分享一下给大家。点击跳转到网站 学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……) 2、学会Oracle数据库入门到入土用法(创作中……) 3、手把…...

从客户跟进到库存管理:看板工具赋能新能源汽车销售
在新能源汽车市场日益扩张的今天,门店销售管理变得更加复杂和重要。从跟踪客户线索到优化订单流程,再到团队协作,效率低下常常成为许多门店的“隐形成本”。如果你曾为销售流程不畅、客户管理混乱而苦恼,那么一种简单直观的工具—…...

算法时间空间复杂度的计算
一、时间复杂度 :找循环最内层 二、空间复杂度: 1.找int float等变量 2.递归调用:空间复杂度递归调用的深度 int 型变量: 四个字节...

人才画像系统如何支撑企业的人才战略落地
在当今竞争激烈的商业环境中,企业的人才战略对于其长期发展至关重要。为了有效实施人才战略,企业需要一套精准、高效的人才管理工具,而人才画像系统正是满足这一需求的关键解决方案。本文将探讨人才画像系统如何支撑企业的人才战略落地&#…...

[数据结构] 链表
目录 1.链表的基本概念 2.链表的实现 -- 节点的构造和链接 节点如何构造? 如何将链表关联起来? 3.链表的方法(功能) 1).display() -- 链表的遍历 2).size() -- 求链表的长度 3).addFirst(int val) -- 头插法 4).addLast(int val) -- 尾插法 5).addIndex -- 在任意位置…...

三格电子——新品IE103转ModbusTCP网关
型号:SG-TCP-IEC103 产品概述 IE103转ModbusTCP网关型号SG-TCP-IEC103,是三格电子推出的工业级网关(以下简称网关),主要用于IEC103数据采集、DLT645-1997/2007数据采集,IEC103支持遥测和遥信,可…...

遥感影像目标检测:从CNN(Faster-RCNN)到Transformer(DETR
我国高分辨率对地观测系统重大专项已全面启动,高空间、高光谱、高时间分辨率和宽地面覆盖于一体的全球天空地一体化立体对地观测网逐步形成,将成为保障国家安全的基础性和战略性资源。未来10年全球每天获取的观测数据将超过10PB,遥感大数据时…...

深入详解神经网络基础知识——理解前馈神经网络( FNN)、卷积神经网络(CNN)和循环神经网络(RNN)等概念及应用
深入详解神经网络基础知识 深度学习作为人工智能(AI)的核心分支之一,近年来在各个领域取得了显著的成果。从图像识别、自然语言处理到自动驾驶,深度学习技术的应用无处不在。而深度学习的基础,神经网络,是理…...

react 项目打包二级目 使用BrowserRouter 解决页面刷新404 找不到路由
使用BrowserRouter package 配置 (这部分代码可以不做配置也能实现) {"homepage": "/admin",}vite.config 配置 export default defineConfig({base: /admin])BrowserRouter 添加配置项 <BrowserRouter basename/admin>&l…...

EasyPlayer.js播放器Web播放H.265要兼顾哪些方面?
在数字化时代,流媒体技术已经成为信息传播和娱乐消费的重要方式。随着互联网技术的飞速发展和移动设备的普及,流媒体服务正在重塑我们的生活和工作方式。从视频点播、在线直播到音乐流媒体,流媒体技术的广泛应用不仅改变了内容的分发和消费模…...

使用 acme.sh 申请域名 SSL/TLS 证书完整指南
使用 acme.sh 申请域名 SSL/TLS 证书完整指南 简介为什么选择 acme.sh 和 ZeroSSL?前置要求安装过程 步骤一:安装 acme.sh步骤二:配置 ZeroSSL 证书申请 方法一:手动 DNS 验证(推荐新手使用)方法二…...

睡岗和玩手机数据集,4653张原始图,支持YOLO,VOC XML,COCO JSON格式的标注
睡岗和玩手机数据集,4653张原始图,支持YOLO,VOC XML,COCO JSON格式的标注 数据集分割 训练组70% 3257图片 有效集20% 931图片 测试集10% 465图片 预处理 没有采用任何预处…...

[Unity] 【VR】【游戏开发】在VR中使用New Input System获取按键值的完整教程
在使用Unity开发VR项目时,推荐使用 New Input System 来处理输入操作。相比于旧的Input系统,New Input System更加灵活、功能强大,尤其在处理VR控制器的按键输入时具有明显优势。本文将详细介绍如何在VR项目中使用New Input System获取按键值,并通过代码示例和图文讲解,帮…...