Java 后端防御性编程实践大全
一、什么是防御性编程
防御性编程(Defensive Programming)是一种编码习惯:假设所有外部输入都不可信、所有依赖都可能失败,在代码中主动预防各种异常情况,而不是等问题发生后再修复。
核心原则: 不信任任何输入,不假设任何前提,让代码在任何异常情况下都不崩溃或产生脏数据。
二、涉及的技术知识点
2.1 空值防护
| 知识点 | 说明 |
|---|---|
| NullPointerException | Java 最常见的运行时异常 |
| Optional | Java 8 引入的空值容器,强制调用方处理空值 |
| Objects.equals() | 避免 null.equals() 导致 NPE |
| 集合空安全 | 空集合 vs null 的区别处理 |
| 链式调用空值短路 | a.getB().getC() 中任何一环为 null 都会 NPE |
2.2 输入校验
| 知识点 | 说明 |
|---|---|
| 参数前置校验 | 方法入口处校验参数合法性 |
| Bean Validation(JSR 380) | @NotNull/@Size/@Pattern 注解校验 |
| 白名单校验 | 只允许预期范围内的值 |
| 边界值处理 | 0、负数、超大值、空字符串等 |
2.3 异常处理
| 知识点 | 说明 |
|---|---|
| 受检异常 vs 非受检异常 | 强制处理 vs 可选处理 |
| 全局异常处理 | @ControllerAdvice 统一兜底 |
| 异常不吞没 | catch 后记日志或重抛 |
| 快速失败(Fail-Fast) | 发现错误立即终止,不让错误扩散 |
2.4 并发安全
| 知识点 | 说明 |
|---|---|
| 线程安全集合 | ConcurrentHashMap / CopyOnWriteArrayList |
| 原子操作 | AtomicInteger / AtomicReference |
| 不可变对象 | 对象创建后状态不可变,天然线程安全 |
| 防重复提交 | 幂等性设计 |
2.5 数据边界防护
| 知识点 | 说明 |
|---|---|
| SQL 注入防护 | 参数化查询 |
| XSS 防护 | 输出转义 |
| 数据截断 | 超长字符串写入数据库时截断 |
| 精度丢失 | BigDecimal 替代 double |
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
三、完整代码示例
3.1 空值防护
3.1.1 Objects.equals() 代替 .equals()
java
// ❌ 危险:如果 orderStatus 为 null → NPE
if (orderStatus.equals("PAID")) { ... }
// ✅ 安全:即使 orderStatus 为 null 也不会 NPE
if (Objects.equals(orderStatus, "PAID")) { ... }
// ✅ 常量在前(也能避免 NPE,但可读性略差)
if ("PAID".equals(orderStatus)) { ... }
3.1.2 集合空安全操作
java
// ❌ 危险:如果 getItemList() 返回 null → NPE
for (ItemDto item : order.getItemList()) { ... }
// ✅ 安全方式1:判空
if (order.getItemList() != null && !order.getItemList().isEmpty()) {
for (ItemDto item : order.getItemList()) { ... }
}
// ✅ 安全方式2:使用工具类
if (CheckEmptyUtil.isNotEmpty(order.getItemList())) {
order.getItemList().forEach(item -> { ... });
}
// ✅ 安全方式3:空集合兜底(Commons Collections)
for (ItemDto item : ListUtils.emptyIfNull(order.getItemList())) { ... }
// ✅ 安全方式4:Optional + Stream
Optional.ofNullable(order.getItemList())
.orElse(Collections.emptyList())
.stream()
.filter(Objects::nonNull)
.forEach(item -> { ... });
3.1.3 链式调用防护
java
// ❌ 危险:任何一环为 null 都会 NPE
String cityName = order.getAddress().getCity().getName();
// ✅ 安全方式1:逐层判空
String cityName = null;
if (order != null && order.getAddress() != null && order.getAddress().getCity() != null) {
cityName = order.getAddress().getCity().getName();
}
// ✅ 安全方式2:Optional 链式
String cityName = Optional.ofNullable(order)
.map(Order::getAddress)
.map(Address::getCity)
.map(City::getName)
.orElse("");
// ✅ 安全方式3:封装空安全 getter
public String getCityNameSafe() {
return Optional.ofNullable(this.address)
.map(Address::getCity)
.map(City::getName)
.orElse(null);
}
3.1.4 Map 取值防护
java
// ❌ 危险:key 不存在返回 null,后续操作 NPE
Integer count = countMap.get(itemId);
int total = count + 1; // NPE if count is null
// ✅ 安全:getOrDefault
int total = countMap.getOrDefault(itemId, 0) + 1;
// ✅ 安全:computeIfAbsent
countMap.computeIfAbsent(itemId, k -> new AtomicInteger(0)).incrementAndGet();
3.2 输入参数校验
3.2.1 方法入口前置校验(快速失败)
java
/**
* 确认发货.
* 入口处校验所有必填参数,不合法立即失败.
*/
public void confirmDelivery(ConfirmDeliveryDto dto) {
// === 前置校验(快速失败) ===
if (dto == null) {
throw new IllegalArgumentException("入参不能为空");
}
if (dto.getMemberId() == null) {
throw new BusinessException("会员ID不能为空");
}
if (dto.getOrderId() == null) {
throw new BusinessException("订单ID不能为空");
}
if (CheckEmptyUtil.isEmpty(dto.getOrderItemDetail())) {
throw new BusinessException("商品明细不能为空");
}
// 校验通过后执行业务逻辑
doConfirmDelivery(dto);
}
3.2.2 Bean Validation 注解校验
java
/**
* 使用 JSR 380 注解在 DTO 上声明校验规则.
*/
@Data
public class CreateOrderDto {
@NotNull(message = "会员ID不能为空")
private Integer memberId;
@NotBlank(message = "订单编码不能为空")
@Size(max = 50, message = "订单编码最长50个字符")
private String orderCode;
@NotNull(message = "订单金额不能为空")
@DecimalMin(value = "0.01", message = "订单金额必须大于0")
private BigDecimal amount;
@NotEmpty(message = "商品列表不能为空")
@Size(max = 100, message = "单次最多100个商品")
private List<@Valid OrderItemDto> itemList;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Email(message = "邮箱格式不正确")
private String email;
}
// Controller 中使用 @Valid 触发校验
@PostMapping("/create-order")
public Result createOrder(@Valid @RequestBody CreateOrderDto dto) {
// 校验不通过会自动抛 MethodArgumentNotValidException
// 由全局异常处理器捕获并返回友好错误信息
return orderService.createOrder(dto);
}
3.2.3 枚举值/白名单校验
java
/**
* 只接受预定义的合法值,拒绝一切非法输入.
*/
public void updateOrderStatus(Integer orderId, String status) {
// ✅ 白名单校验
Set<String> validStatuses = Set.of("CREATED", "PAID", "SHIPPED", "COMPLETED", "CANCELLED");
if (!validStatuses.contains(status)) {
throw new BusinessException("非法的订单状态: " + status);
}
orderRepository.updateStatus(orderId, status);
}
// 或用枚举
public void updateOrderStatus(Integer orderId, OrderStatusEnum status) {
// 编译期保证只能传合法值
orderRepository.updateStatus(orderId, status.getCode());
}
3.2.4 数值边界校验
java
/**
* 数值型参数的边界防护.
*/
public void deductStock(Integer itemId, Integer qty) {
// 防空
if (itemId == null || qty == null) {
throw new BusinessException("参数不能为空");
}
// 防负数/零
if (qty <= 0) {
throw new BusinessException("扣减数量必须大于0");
}
// 防超大值(业务上单次扣减不可能超过10000)
if (qty > 10000) {
throw new BusinessException("单次扣减数量超限");
}
stockRepository.deduct(itemId, qty);
}
3.3 外部调用防护
3.3.1 HTTP 调用超时 + 异常兜底
java
/**
* 调用外部接口的防御性模板.
*/
public DeliveryTimeResult getDeliveryTime(String gbCode) {
DeliveryTimeResult defaultResult = new DeliveryTimeResult(); // 兜底默认值
try {
String response = httpUtil.postForString(url, params);
// 防止返回空
if (CheckEmptyUtil.isEmpty(response)) {
log.warn("xxx返回为空, gbCode={}", gbCode);
return defaultResult;
}
// 防止 JSON 解析失败
ResultDto result = JSON.parseObject(response, ResultDto.class);
if (result == null || result.getData() == null) {
log.warn("xxx返回数据解析为空, response={}", response);
return defaultResult;
}
// 防止业务状态码异常
if (!Objects.equals(result.getCode(), 200)) {
log.warn("xxx返回业务失败, code={}, msg={}", result.getCode(), result.getMsg());
return defaultResult;
}
return convertResult(result.getData());
} catch (SocketTimeoutException e) {
log.error("调用xxx超时, gbCode={}", gbCode, e);
return defaultResult; // 超时不阻断业务
} catch (Exception e) {
log.error("调用xxx异常, gbCode={}", gbCode, e);
return defaultResult; // 任何异常不阻断业务
}
}
3.3.2 Feign 调用结果校验
java
/**
* Feign 调用后的防御性校验模板.
*/
public List<OrderInfoDto> getOrderInfo(Integer orderId) {
try {
RestControllerResult<List<OrderInfoDto>> result =
orderFeign.getOrderStatusInfo(params);
// 防 Feign 返回 null
if (result == null) {
log.warn("调用订单服务返回null, orderId={}", orderId);
return Collections.emptyList();
}
// 防业务失败
if (!Boolean.TRUE.equals(result.getSuccess())) {
log.warn("调用订单服务失败, errorMsg={}", result.getErrorMsg());
return Collections.emptyList();
}
// 防 data 为 null
if (CheckEmptyUtil.isEmpty(result.getData())) {
log.info("订单服务返回数据为空, orderId={}", orderId);
return Collections.emptyList();
}
return result.getData();
} catch (Exception e) {
log.error("调用订单服务异常, orderId={}", orderId, e);
return Collections.emptyList();
}
}
3.4 并发安全防护
3.4.1 防重复提交(幂等性)
java
/**
* 基于 Redis 的防重复提交.
*/
@Service
public class IdempotentService {
@Resource
private StringRedisTemplate redisTemplate;
/**
* 执行幂等操作.
* 同一个 key 在 expireSeconds 内只能执行一次.
*/
public boolean tryExecute(String idempotentKey, int expireSeconds) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", expireSeconds, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
}
// 使用示例
public void confirmDelivery(ConfirmDeliveryDto dto) {
String key = "confirm_delivery_" + dto.getOrderCode();
if (!idempotentService.tryExecute(key, 60)) {
throw new BusinessException("请勿重复提交");
}
try {
doConfirmDelivery(dto);
} catch (Exception e) {
// 失败时删除幂等键,允许重试
redisTemplate.delete(key);
throw e;
}
}
3.4.2 并发安全的集合操作
java
/**
* 并发环境下的集合防护.
*/
public class ConcurrentSafeDemo {
// ❌ 危险:多线程同时操作 HashMap 会导致死循环或数据丢失
private Map<String, String> cache = new HashMap<>();
// ✅ 安全:ConcurrentHashMap
private Map<String, String> safeCache = new ConcurrentHashMap<>();
// ❌ 危险:遍历时修改集合 → ConcurrentModificationException
public void removeExpired(List<Order> orders) {
for (Order order : orders) {
if (order.isExpired()) {
orders.remove(order); // 异常!
}
}
}
// ✅ 安全:使用 Iterator 或 removeIf
public void removeExpiredSafe(List<Order> orders) {
orders.removeIf(Order::isExpired);
}
// ✅ 安全:收集后批量删除
public void removeExpiredSafe2(List<Order> orders) {
List<Order> toRemove = orders.stream()
.filter(Order::isExpired)
.collect(Collectors.toList());
orders.removeAll(toRemove);
}
}
3.5 数据类型安全
3.5.1 BigDecimal 替代 double
java
// ❌ 危险:浮点精度丢失
double price = 0.1 + 0.2; // = 0.30000000000000004
// ✅ 安全:BigDecimal
BigDecimal price = new BigDecimal("0.1").add(new BigDecimal("0.2")); // = 0.3
// ❌ 危险:BigDecimal 构造方式
BigDecimal bad = new BigDecimal(0.1); // = 0.1000000000000000055511151231257827021181583404541015625
// ✅ 安全:字符串构造
BigDecimal good = new BigDecimal("0.1"); // = 0.1
// ✅ 金额比较
if (amount.compareTo(BigDecimal.ZERO) > 0) { // 而不是 amount > 0
// ...
}
3.5.2 类型转换防护
java
/**
* 安全的类型转换.
*/
public Integer safeParseInteger(Object value) {
if (value == null) {
return null;
}
if (value instanceof Integer) {
return (Integer) value;
}
try {
return Integer.valueOf(value.toString().trim());
} catch (NumberFormatException e) {
log.warn("数值转换失败: {}", value);
return null;
}
}
// 日期转换防护
public Date safeParseDate(String dateStr) {
if (dateStr == null || dateStr.trim().isEmpty()) {
return null;
}
try {
return DateUtil.convertToDate(dateStr);
} catch (Exception e) {
log.warn("日期解析失败: {}", dateStr);
return null;
}
}
3.6 数据库操作防护
3.6.1 批量操作分片
java
/**
* 批量查询分片,避免 IN 条件过多导致 SQL 过长.
*/
public List<ItemBase> batchQueryItems(List<Integer> itemSkuIds) {
if (CheckEmptyUtil.isEmpty(itemSkuIds)) {
return Collections.emptyList();
}
List<ItemBase> result = new ArrayList<>();
// 每 200 个一批查询,避免 MySQL IN 超限
int batchSize = 200;
for (int i = 0; i < itemSkuIds.size(); i += batchSize) {
int end = Math.min(i + batchSize, itemSkuIds.size());
List<Integer> batch = itemSkuIds.subList(i, end);
List<ItemBase> batchResult = itemBaseRepository.findByItemSkuIdIn(batch);
if (CheckEmptyUtil.isNotEmpty(batchResult)) {
result.addAll(batchResult);
}
}
return result;
}
3.6.2 乐观锁重试
java
/**
* 乐观锁冲突时自动重试.
*/
public void updateWithRetry(Integer id, int maxRetries) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
doUpdate(id);
return; // 成功退出
} catch (OptimisticLockException e) {
if (attempt == maxRetries) {
throw new BusinessException("操作冲突,请稍后重试");
}
log.warn("乐观锁冲突,第{}次重试, id={}", attempt, id);
try {
Thread.sleep(100 * attempt); // 退避等待
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new BusinessException("操作被中断");
}
}
}
}
3.7 字符串安全处理
3.7.1 截断防止数据库溢出
java
/**
* 字符串截断,防止超过数据库字段长度.
*/
public String safeTruncate(String value, int maxLength) {
if (value == null) {
return null;
}
if (value.length() <= maxLength) {
return value;
}
return value.substring(0, maxLength);
}
// 使用示例
order.setRemark(safeTruncate(dto.getRemark(), 500)); // varchar(500)
order.setAddress(safeTruncate(dto.getAddress(), 200));
3.7.2 日志输出脱敏
java
/**
* 日志中敏感信息脱敏.
*/
public String maskPhone(String phone) {
if (phone == null || phone.length() < 7) {
return "***";
}
return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
}
// 使用
log.info("发货给客户, phone={}, address={}",
maskPhone(dto.getPhone()),
safeTruncate(dto.getAddress(), 20) + "...");
3.8 返回值防护
3.8.1 永远不返回 null 集合
java
/**
* 方法约定:集合类返回值永远不返回 null.
*/
public List<OrderDto> listOrders(Integer memberId) {
List<Order> orders = orderRepository.findByMemberId(memberId);
if (CheckEmptyUtil.isEmpty(orders)) {
return Collections.emptyList(); // ✅ 返回空集合,不返回 null
}
return orders.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
}
// 调用方不需要判 null
List<OrderDto> orders = orderService.listOrders(memberId);
orders.forEach(order -> { ... }); // 安全,即使没有数据也不会 NPE
3.8.2 Optional 表达可能为空的返回值
java
/**
* 单条查询可能为空时用 Optional.
*/
public Optional<OrderDto> getOrder(Integer orderId) {
return orderRepository.findById(orderId)
.map(this::convertToDto);
}
// 调用方被强制处理空值
OrderDto order = orderService.getOrder(orderId)
.orElseThrow(() -> new BusinessException("订单不存在"));
四、防御性编程检查清单
在写每个方法时,对照此清单检查:
| # | 检查项 | 要求 |
|---|---|---|
| 1 | 入参是否可能为 null? | 方法入口判空或用 @NotNull |
| 2 | 集合参数是否可能为空? | isEmpty 检查后再遍历 |
| 3 | 外部调用是否可能失败? | try-catch + 日志 + 兜底值 |
| 4 | 返回的集合是否可能为 null? | 返回 emptyList 而非 null |
| 5 | 数值是否可能溢出/为负? | 边界值校验 |
| 6 | 字符串是否可能超长? | 截断处理 |
| 7 | 类型转换是否安全? | try-catch NumberFormatException |
| 8 | 并发是否安全? | 线程安全集合/锁/原子变量 |
| 9 | 日志是否泄露敏感信息? | 脱敏处理 |
| 10 | 异常是否被正确处理? | 不吞没、不暴露堆栈给前端 |