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

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 异常是否被正确处理? 不吞没、不暴露堆栈给前端
相关推荐
搬砖小匠2 小时前
SpringBoot 通过自定义注解 + AOP 实现数据字典自动翻译(通用方案)
java·后端
今天AI了吗2 小时前
【AI智能体】Hermes Agent 从部署到项目实战操作详解
java·人工智能·python·milvus
青山木2 小时前
Hot 100 --- 搜索二维矩阵
java·算法·leetcode·矩阵
CoderYanger3 小时前
Java EE:9.JVM(课件内容-上篇)
java·jvm·程序人生·面试·职场和发展·java-ee·程序员创富
xiaoqiMikko3 小时前
Spring Cloud Config 这个 CVE 的补丁,Maven Central 上根本没有
java·spring boot
残月心殇 请珍惜枸3 小时前
.NET陷阱之五:奇怪的OutOfMemoryException——大对象堆引起的问题与对策
java·jvm·.net
用户3126874877203 小时前
Spring Boot 配置体系到底怎么加载的?从 application.yml 到 @Conditional 的全链路拆解
java·spring boot
Bonnie_12153 小时前
08-JUC并发基础-AQS-补充共享锁
java·开发语言
Zane19943 小时前
线程池入门:7 大核心参数与 4 种拒绝策略
java·后端