java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Java防御性编程

Java中后端开发防御性编程实践大全

作者:霸道流氓气质

防御性编程(Defensive Programming)是一种编码习惯,本文将深入讲解防御性编程的核心原则和Java实现技巧,涵盖空值防护、输入校验、异常处理、并发安全等关键知识点,希望对大家有所帮助

一、什么是防御性编程

防御性编程(Defensive Programming)是一种编码习惯:假设所有外部输入都不可信、所有依赖都可能失败,在代码中主动预防各种异常情况,而不是等问题发生后再修复。

核心原则: 不信任任何输入,不假设任何前提,让代码在任何异常情况下都不崩溃或产生脏数据。

二、涉及的技术知识点

2.1 空值防护

知识点说明
NullPointerExceptionJava 最常见的运行时异常
OptionalJava 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

三、完整代码示例

3.1 空值防护

Objects.equals() 代替 .equals()

// ❌ 危险:如果 orderStatus 为 null → NPE
if (orderStatus.equals("PAID")) { ... }

// ✅ 安全:即使 orderStatus 为 null 也不会 NPE
if (Objects.equals(orderStatus, "PAID")) { ... }

// ✅ 常量在前(也能避免 NPE,但可读性略差)
if ("PAID".equals(orderStatus)) { ... }

集合空安全操作

// ❌ 危险:如果 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 -> { ... });

链式调用防护

// ❌ 危险:任何一环为 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);
}

Map 取值防护

// ❌ 危险: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 输入参数校验

方法入口前置校验(快速失败)

/**
 * 确认发货.
 * 入口处校验所有必填参数,不合法立即失败.
 */
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);
}

Bean Validation 注解校验

/**
 * 使用 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);
}

枚举值/白名单校验

/**
 * 只接受预定义的合法值,拒绝一切非法输入.
 */
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());
}

数值边界校验

/**
 * 数值型参数的边界防护.
 */
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 外部调用防护

HTTP 调用超时 + 异常兜底

/**
 * 调用外部接口的防御性模板.
 */
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;  // 任何异常不阻断业务
    }
}

Feign 调用结果校验

/**
 * 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 并发安全防护

防重复提交(幂等性)

/**
 * 基于 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;
    }
}

并发安全的集合操作

/**
 * 并发环境下的集合防护.
 */
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 数据类型安全

BigDecimal 替代 double

// ❌ 危险:浮点精度丢失
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
    // ...
}

类型转换防护

/**
 * 安全的类型转换.
 */
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 数据库操作防护

批量操作分片

/**
 * 批量查询分片,避免 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;
}

乐观锁重试

/**
 * 乐观锁冲突时自动重试.
 */
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 字符串安全处理

截断防止数据库溢出

/**
 * 字符串截断,防止超过数据库字段长度.
 */
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));

日志输出脱敏

/**
 * 日志中敏感信息脱敏.
 */
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 返回值防护

永远不返回 null 集合

/**
 * 方法约定:集合类返回值永远不返回 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

Optional 表达可能为空的返回值

/**
 * 单条查询可能为空时用 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异常是否被正确处理?不吞没、不暴露堆栈给前端

到此这篇关于Java中后端开发防御性编程实践大全的文章就介绍到这了,更多相关Java防御性编程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文