首页 > 应用开发 > IT综合 > 防御性编程:让系统坚不可摧
面对复杂多变的运行环境、不可预测的用户输入以及潜在的编程错误,如何确保软件在遭遇异常情况时依然能够稳定运行,是每位开发者必须面对的挑战。防御性编程(Defensive Programming)正是为解决这一问题而生的一种编程范式,它强调在编程过程中预见并防范潜在的错误和异常情况,从而增强软件的健壮性和稳定性。 作为一种细致、谨慎的编程方法,通过提前考虑并防范可能出现的错误,从而有效减少软件漏洞和故障。本文将详细介绍防御性编程的基本概念、关键策略,并通过实际案例展示其在实际项目中的应用。
防御性编程的核心思想在于 承认程序总会存在问题和需要修改,因此聪明的程序员会提前考虑并防范可能的错误。它强调在编程过程中不仅要实现功能,还要 确保程序在面对错误输入、异常情况和并发操作时能够稳定运行。
非系统性风险:只影特定场景下的响单次调用,不对系统整体稳定性产生影响。比如空指针异常、数据越界等。
系统性风险:导致整个服务不可用的风险。比如 死循环,分页查询pageSize过大等。
用户输入数据到Web表单中,系统需要处理这些数据以进行后续操作。
风险识别:系统性风险,可能导致系统整体不可用。
防御策略:
分页参数防御式编程案例
下面以分页参数防御式编程为案例进行举例说明:
场景描述: 假设开发一个Web API,该API需要根据用户请求返回特定数据的分页结果。分页请求包含以下参数:
pageSize:每页应显示的记录数。pageNumber:用户请求的当前页码。防御性编程措施:
示例代码(伪代码):
public class PaginationService {
private static final int MAX_PAGE_SIZE = 100;
/**
* 获取分页信息并进行参数校验
*
* @param totalRecords 总记录数
* @param pageSize 每页记录数
* @param pageNumber 当前页码
* @return 分页信息,包括总页数、当前页码等
*/
public PaginationInfo getPaginationInfo(int totalRecords, int pageSize, int pageNumber) {
// 校验pageSize
if (pageSize <= 0 || pageSize > MAX_PAGE_SIZE) {
throw new IllegalArgumentException("pageSize必须为正整数且不超过" + MAX_PAGE_SIZE);
}
// 校验pageNumber
if (pageNumber <= 0) {
pageNumber = 1; // 默认为第一页
}
// 计算总页数
int totalPages = (totalRecords + pageSize - 1) / pageSize;
// 确保pageNumber不超过总页数
if (pageNumber > totalPages) {
pageNumber = totalPages;
}
// 计算当前页的数据起始索引(可选,根据具体需求)
int startIndex = (pageNumber - 1) * pageSize;
// 返回分页信息
return new PaginationInfo(totalPages, pageNumber, startIndex);
}
// PaginationInfo 是一个简单的类,用于封装分页信息
...
在这个例子中,
getPaginationInfo 方法首先验证了
pageSize 和
pageNumber 参数的有效性,确保了它们符合预期的约束条件。如果参数无效,方法会抛出一个
IllegalArgumentException 异常,这有助于调用者识别并处理错误情况。然后,方法计算了总页数,并根据需要调整了
pageNumber 以确保它不会超出范围。最后,方法返回了一个包含分页信息的
PaginationInfo 对象。
这种防御性编程策略有助于防止因无效的分页参数而导致的程序错误,提高了API的健壮性和用户体验。
在循环或者遍历场景中,没有明确的退出机制。
风险识别:系统性风险,可能导致系统整体不可用。
防御策略:
示例代码(Java):
/**
* 生成时间段。
*
* @param startMinutes 开始时间
* @param endMinutes 结束时间
* @param interval 时间段间隔
* @param duration 时间的时长
* @return 时间段列表
*/ public List<String> generateList(int startMinutes, int endMinutes, int interval, int duration) {
List<String> result = new ArrayList<>();
int nextStartTime = startMinutes;
while (nextStartTime == endMinutes) {
int currentStartMinutes = nextStartTime;
int currentEndMinutes = currentStartMinutes + duration;
result.add(currentStartMinutes + "-" + currentEndMinutes);
nextBatchStartTime += interval;
}
return result;}
针对以上代码,我们可以添加一些防御式编程的元素来确保代码的健壮性和可靠性。防御式编程侧重于预防错误的发生,包括输入验证、错误处理和边界条件检查。以下是修改后的代码,包含了防御式编程的改进:
/**
* 生成时间段。
*
* @param startMinutes 开始时间
* @param endMinutes 结束时间
* @param interval 时间段间隔
* @param duration 时间的时长
* @return 时间段列表
*/ public List<String> generateList(int startMinutes, int endMinutes, int interval, int duration) {
// 改进点1:校验 interval,以保证循环中的步长能够正向增长
// 一般情况下,还需要对步长,和 endMinutes与startMinutes的区间大小做限制,避免生成“巨大”的列表。
if (interval <= 0) {
throw new IllegalArgumentException("Invalid parameters: interval must be positive integers.");
}
List<String> result = new ArrayList<>();
int nextStartTime = startMinutes;
//改进点2:避免使用等号做循环终止条件,以防跳过循环终止点。
while (nextStartTime <= endMinutes) {
int currentStartMinutes = nextStartTime;
int currentEndMinutes = currentStartMinutes + duration;
result.add(currentStartMinutes + "-" + currentEndMinutes);
nextBatchStartTime += interval;
}
return result;}
程序在读取文件、进行网络请求或执行其他可能失败的操作时,需要处理潜在的异常。
风险识别:非系统性风险, 影响单次请求。
防御策略:
• 使用try-except语句:将可能抛出异常的代码块放在try语句中,并在except语句中捕获并处理这些异常。
• 区分异常类型:根据实际需要捕获特定的异常类型,或捕获所有异常(使用Exception作为异常类型)。
• 记录错误信息:在捕获异常后,记录详细的错误信息(如异常类型、错误消息、堆栈跟踪等),以便后续分析和调试。
示例代码(Java):
/**
* 读取文件内容。
*
* @param filePath 文件路径
* @return 文件内容,如果文件不存在或读取失败则返回null
*/
public static String readFile(String filePath) {
try {
byte[] encoded = Files.readAllBytes(Paths.get(filePath));
return new String(encoded);
} catch (FileNotFoundException e) {
log.info("文件未找到:" + filePath);
return null;
} catch (Exception e) {
log.info("读取文件时发生错误:" + e.getMessage());
return null;
}
}
在循环、条件判断或数组访问等操作中,需要确保不会超出预期的范围或边界。
风险识别:非系统性风险,影响单次请求。
防御策略:
示例代码(Java):
public class ArrayAccess {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
int index = getIndexFromUser(); // 假设这是从用户那里获取的索引
if (index >= 0 && index < numbers.length) {
log.info(numbers[index]);
} else {
log.info("索引超出数组范围");
}
}
// 假设这个方法从用户那里获取索引值,并进行基本的验证
private static int getIndexFromUser() {
// 为了示例,我们直接返回一个示例值
return 2; // 假设用户输入了有效的索引值2
} }
在代码的关键路径上,需要确保某些条件始终为真,否则程序将无法正确执行。
示例代码(Java):
/**
* 计算年龄。
*
* @param birthYear 出生年份
* @return 年龄,如果输入无效则返回-1。
*/ public static int calculateAge(int birthYear) {
// 输入验证:确保出生年份是一个合理的值
if (birthYear <= 0 || birthYear > java.time.Year.now().getValue()) {
// 抛出IllegalArgumentException来指示方法接收到了非法参数
throw new IllegalArgumentException("出生年份必须是一个大于0且小于当前年份的整数");
}
// 计算年龄
int currentYear = java.time.Year.now().getValue();
return currentYear - birthYear; }
public static void main(String[] args) {
try {
// 假设我们从某个地方(如用户输入)获取了出生年份
int birthYear = 1990; // 这里直接赋值作为示例
int age = calculateAge(birthYear);
if (age != -1) { // 注意:这个例子中calculateAge实际上不会返回-1,但为了展示如何处理可能的异常情况,我们可以这样设计
log.info("年龄是:" + age);
}
} catch (IllegalArgumentException e) {
// 捕获并处理IllegalArgumentException
log.info("错误:" + e.getMessage());
}
// 如果需要从用户输入中获取出生年份,你可以添加相应的逻辑来处理字符串到整数的转换和验证
}
// 注意:在这个例子中,我们没有直接使用assert,因为Java的assert主要用于调试,且默认是禁用的。
// 而是通过显式的条件检查和异常抛出来实现防御性编程。
No,过度的防御式编程会使程序会变得臃肿而缓慢,增加软件的复杂度。
要考虑好什么地方需要进行防御,然后因地制宜地调整进行防御式编程的优先级。
一般在入口处或者接入层做通用性防御性编程,比如数据准入校验;但 对于循环类逻辑,应始终在使用处做细节性防御 。
例如对于网络请求,一般是统一处理超时、鉴权、各种错误code,而不是在业务层个别处理
如项目内部使用的utils函数和公开发布的package,后者防御要求更高
防御性编程是一种积极主动的编程策略,它要求开发者在编写代码时,不仅要关注功能的实现,更要关注代码的健壮性和稳定性。通过预见并防范潜在的错误和异常情况,防御性编程能够显著提升软件的质量,减少因外部因素导致的程序崩溃,提升系统稳定性。
文章中难免会有不足之处,希望读者能给予宝贵的意见和建议。谢谢!
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70037994/viewspace-3023355/ ,如需转载,请注明出处,否则将追究法律责任。