聊聊防御式编程

前言

有些小伙伴在工作中,可能经常遇到这样的场景:线上系统突然崩溃,排查发现是因为一个预料之外的输入参数;或者用户反馈某个功能异常,最终定位到是外部服务返回了异常数据。

这些问题往往不是核心逻辑的错误,而是因为我们没有做好充分的防御。

作为一名老司机,我见证过太多因为缺乏防御意识导致的线上事故。

今天我就从浅入深,带你彻底掌握防御式编程的精髓,希望对你会有所帮助。

1. 什么是防御式编程?

在深入具体技术之前,我们先明确防御式编程的概念。

防御式编程不是一种具体的技术,而是一种编程哲学和思维方式

核心理念

防御式编程的核心思想是:程序应该能够在面对非法输入、异常环境或其他意外情况时,依然能够保持稳定运行,或者以可控的方式失败。

有些小伙伴在工作中可能会说:"我的代码已经处理了所有正常情况,为什么还需要防御?"

这是因为在复杂的生产环境中,我们无法预知所有可能的情况:

  • 用户可能输入我们未曾预料的数据
  • 外部服务可能返回异常响应
  • 网络可能突然中断
  • 磁盘可能空间不足
  • 内存可能耗尽

为什么需要防御式编程?

从架构师的角度看,防御式编程的价值体现在:

  1. 提高系统稳定性:减少因边缘情况导致的系统崩溃
  2. 提升可维护性:清晰的错误处理让问题定位更简单
  3. 增强用户体验:优雅的降级比直接崩溃更好
  4. 降低维护成本:预防性代码减少线上紧急修复

让我们通过一个简单的例子来感受防御式编程的差异:

// 非防御式编程
public class UserService {
    public void updateUserAge(User user, int newAge) {
        user.setAge(newAge);  // 如果user为null,这里会抛出NPE
        userRepository.save(user);
    }
}

// 防御式编程
public class UserService {
    public void updateUserAge(User user, int newAge) {
        if (user == null) {
            log.warn("尝试更新年龄时用户对象为空");
            return;
        }
        
        if (newAge < 0 || newAge > 150) {
            log.warn("无效的年龄输入: {}", newAge);
            throw new IllegalArgumentException("年龄必须在0-150之间");
        }
        
        user.setAge(newAge);
        userRepository.save(user);
    }
}

看到区别了吗?

防御式编程让我们提前发现问题,而不是等到异常发生。

好了,让我们开始今天的主菜。

我将从最基本的参数校验,逐步深入到复杂的系统级防御,确保每个知识点都讲透、讲懂。

2.防御式编程的核心原则

防御式编程不是随意地添加if判断,而是有章可循的。

下面我总结了几大核心原则。

原则一:对输入保持怀疑态度

这是防御式编程的第一原则:永远不要信任任何外部输入

无论是用户输入、外部API响应、还是配置文件,都应该进行验证。

示例代码

public class UserRegistrationService {
    
    // 非防御式写法
    public void registerUser(String username, String email, Integer age) {
        User user = new User(username, email, age);
        userRepository.save(user);
    }
    
    // 防御式写法
    public void registerUserDefensive(String username, String email, Integer age) {
        // 1. 检查必需参数
        if (StringUtils.isBlank(username)) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        
        if (StringUtils.isBlank(email)) {
            throw new IllegalArgumentException("邮箱不能为空");
        }
        
        if (age == null) {
            throw new IllegalArgumentException("年龄不能为空");
        }
        
        // 2. 检查参数格式
        if (username.length() < 3 || username.length() > 20) {
            throw new IllegalArgumentException("用户名长度必须在3-20个字符之间");
        }
        
        if (!isValidEmail(email)) {
            throw new IllegalArgumentException("邮箱格式不正确");
        }
        
        // 3. 检查业务规则
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年龄必须在0-150之间");
        }
        
        if (userRepository.existsByUsername(username)) {
            throw new IllegalArgumentException("用户名已存在");
        }
        
        // 4. 执行业务逻辑
        User user = new User(username.trim(), email.trim().toLowerCase(), age);
        userRepository.save(user);
    }
    
    private boolean isValidEmail(String email) {
        String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$";
        return email != null && email.matches(emailRegex);
    }
}

深度剖析

有些小伙伴在工作中可能会觉得这些校验很繁琐,但其实它们各自有重要作用:

  1. 空值检查:防止NPE,这是Java中最常见的异常
  2. 格式验证:确保数据符合预期格式,避免后续处理出错
  3. 业务规则验证:在数据进入核心业务前就发现问题
  4. 数据清理:去除前后空格、统一格式等

重要原则:校验要尽早进行,在数据进入系统边界时就完成验证。

为了更直观理解输入验证的层次,我画了一个验证流程图:

image

原则二:善用断言和异常

断言和异常是防御式编程的重要工具,但它们的使用场景有所不同。

断言(Assertions)

断言用于检查在代码正确的情况下永远不应该发生的条件。

public class Calculator {
    
    public double divide(double dividend, double divisor) {
        // 使用断言检查内部不变性
        assert divisor != 0 : "除数不能为0,这应该在调用前被检查";
        
        // 但对外部输入,我们仍然需要正常检查
        if (divisor == 0) {
            throw new IllegalArgumentException("除数不能为0");
        }
        
        return dividend / divisor;
    }
    
    public void processPositiveNumber(int number) {
        // 这个断言表达:这个方法只应该处理正数
        // 如果传入负数,说明调用方有bug
        assert number > 0 : "输入必须为正数: " + number;
        
        // 业务逻辑
    }
}

注意:Java断言默认是关闭的,需要通过-ea参数启用。在生产环境中,通常不建议依赖断言。

异常处理

异常应该根据情况分层处理:

public class FileProcessor {
    
    // 不好的异常处理
    public void processFile(String filePath) {
        try {
            String content = Files.readString(Path.of(filePath));
            // 处理内容...
        } catch (Exception e) {  // 捕获过于宽泛
            e.printStackTrace(); // 在生产环境中无效
        }
    }
    
    // 好的异常处理
    public void processFileDefensive(String filePath) {
        // 1. 参数校验
        if (StringUtils.isBlank(filePath)) {
            throw new IllegalArgumentException("文件路径不能为空");
        }
        
        try {
            // 2. 读取文件
            String content = Files.readString(Path.of(filePath));
            
            // 3. 处理内容
            processContent(content);
            
        } catch (NoSuchFileException e) {
            log.error("文件不存在: {}", filePath, e);
            throw new BusinessException("文件不存在: " + filePath, e);
        } catch (AccessDeniedException e) {
            log.error("没有文件访问权限: {}", filePath, e);
            throw new BusinessException("没有文件访问权限: " + filePath, e);
        } catch (IOException e) {
            log.error("读取文件失败: {}", filePath, e);
            throw new BusinessException("读取文件失败: " + filePath, e);
        }
    }
    
    private void processContent(String content) {
        if (StringUtils.isBlank(content)) {
            log.warn("文件内容为空");
            return;
        }
        
        try {
            // 解析JSON等可能抛出异常的操作
            JsonObject json = JsonParser.parseString(content).getAsJsonObject();
            // 处理JSON...
        } catch (JsonSyntaxException e) {
            log.error("文件内容不是有效的JSON", e);
            throw new BusinessException("文件格式不正确", e);
        }
    }
}

深度剖析

有些小伙伴在工作中可能会混淆检查型异常和非检查型异常的使用场景:

  • 检查型异常:调用方应该能够预期并处理的异常(如IOException)
  • 非检查型异常:通常是编程错误,调用方不应该捕获(如IllegalArgumentException)

最佳实践

  • 使用具体的异常类型,而不是通用的Exception
  • 提供有意义的错误信息
  • 保持异常链,不要丢失根因
  • 在系统边界处统一处理异常

原则三:资源管理和清理

资源泄漏是系统不稳定的常见原因。

防御式编程要求我们确保资源被正确释放。

示例代码

public class ResourceService {
    
    // 不好的资源管理
    public void copyFileUnsafe(String sourcePath, String targetPath) throws IOException {
        FileInputStream input = new FileInputStream(sourcePath);
        FileOutputStream output = new FileOutputStream(targetPath);
        
        byte[] buffer = new byte[1024];
        int length;
        while ((length = input.read(buffer)) > 0) {
            output.write(buffer, 0, length);
        }
        
        // 如果中间抛出异常,资源不会被关闭!
        input.close();
        output.close();
    }
    
    // 传统的防御式资源管理
    public void copyFileTraditional(String sourcePath, String targetPath) throws IOException {
        FileInputStream input = null;
        FileOutputStream output = null;
        
        try {
            input = new FileInputStream(sourcePath);
            output = new FileOutputStream(targetPath);
            
            byte[] buffer = new byte[1024];
            int length;
            while ((length = input.read(buffer)) > 0) {
                output.write(buffer, 0, length);
            }
            
        } finally {
            // 确保资源被关闭
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    log.error("关闭输入流失败", e);
                }
            }
            if (output != null) {
                try {
                    output.close();
                } catch (IOException e) {
                    log.error("关闭输出流失败", e);
                }
            }
        }
    }
    
    // 使用try-with-resources(推荐)
    public void copyFileModern(String sourcePath, String targetPath) throws IOException {
        try (FileInputStream input = new FileInputStream(sourcePath);
             FileOutputStream output = new FileOutputStream(targetPath)) {
            
            byte[] buffer = new byte[1024];
            int length;
            while ((length = input.read(buffer)) > 0) {
                output.write(buffer, 0, length);
            }
        } // 资源会自动关闭,即使抛出异常
    }
    
    // 复杂资源管理场景
    public void processWithMultipleResources() {
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        
        try {
            conn = dataSource.getConnection();
            stmt = conn.prepareStatement("SELECT * FROM users WHERE age > ?");
            stmt.setInt(1, 18);
            rs = stmt.executeQuery();
            
            while (rs.next()) {
                // 处理结果
            }
            
        } catch (SQLException e) {
            log.error("数据库操作失败", e);
            throw new BusinessException("数据查询失败", e);
        } finally {
            // 按创建顺序的逆序关闭资源
            closeQuietly(rs);
            closeQuietly(stmt);
            closeQuietly(conn);
        }
    }
    
    private void closeQuietly(AutoCloseable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (Exception e) {
                log.warn("关闭资源时发生异常", e);
                // 静默处理,不掩盖主要异常
            }
        }
    }
}

深度剖析

有些小伙伴在工作中可能没有意识到,资源管理不当会导致:

  1. 文件句柄泄漏:最终导致"Too many open files"错误
  2. 数据库连接泄漏:连接池耗尽,系统无法响应
  3. 内存泄漏:对象无法被GC回收

防御式资源管理要点

  • 使用try-with-resources(Java 7+)
  • 在finally块中关闭资源
  • 关闭资源时处理可能的异常
  • 按创建顺序的逆序关闭资源

为了理解资源管理的正确流程,我画了一个资源生命周期图:
image

防御式编程的高级技巧

掌握了基本原则后,我们来看看一些高级的防御式编程技巧。

使用Optional避免空指针

Java 8引入的Optional是防御空指针的利器。

public class OptionalExample {
    
    // 不好的做法:多层null检查
    public String getUserEmailBad(User user) {
        if (user != null) {
            Profile profile = user.getProfile();
            if (profile != null) {
                Contact contact = profile.getContact();
                if (contact != null) {
                    return contact.getEmail();
                }
            }
        }
        return null;
    }
    
    // 使用Optional的防御式写法
    public Optional<String> getUserEmailGood(User user) {
        return Optional.ofNullable(user)
                .map(User::getProfile)
                .map(Profile::getContact)
                .map(Contact::getEmail)
                .filter(email -> !email.trim().isEmpty());
    }
    
    // 使用方法
    public void processUser(User user) {
        Optional<String> emailOpt = getUserEmailGood(user);
        
        // 方式1:如果有值才处理
        emailOpt.ifPresent(email -> {
            sendNotification(email, "欢迎使用我们的服务");
        });
        
        // 方式2:提供默认值
        String email = emailOpt.orElse("default@example.com");
        
        // 方式3:如果没有值,抛出特定异常
        String requiredEmail = emailOpt.orElseThrow(() -> 
            new BusinessException("用户邮箱不能为空")
        );
    }
}

不可变对象的防御价值

不可变对象天生具有防御性,因为它们的状态在创建后就不能被修改。

// 可变对象 - 容易在不知情的情况下被修改
public class MutableConfig {
    private Map<String, String> settings = new HashMap<>();
    
    public Map<String, String> getSettings() {
        return settings; // 危险!调用方可以修改内部状态
    }
    
    public void setSettings(Map<String, String> settings) {
        this.settings = settings; // 危险!外部map的修改会影响内部
    }
}

// 不可变对象 - 防御式设计
public final class ImmutableConfig {
    private final Map<String, String> settings;
    
    public ImmutableConfig(Map<String, String> settings) {
        // 防御性拷贝
        this.settings = Collections.unmodifiableMap(new HashMap<>(settings));
    }
    
    public Map<String, String> getSettings() {
        // 返回不可修改的视图
        return settings;
    }
    
    // 没有setter方法,对象创建后不可变
}

// 使用Builder模式创建复杂不可变对象
public final class User {
    private final String username;
    private final String email;
    private final int age;
    
    private User(String username, String email, int age) {
        this.username = username;
        this.email = email;
        this.age = age;
    }
    
    // getters...
    
    public static class Builder {
        private String username;
        private String email;
        private int age;
        
        public Builder username(String username) {
            this.username = Objects.requireNonNull(username, "用户名不能为空");
            return this;
        }
        
        public Builder email(String email) {
            this.email = Objects.requireNonNull(email, "邮箱不能为空");
            if (!isValidEmail(email)) {
                throw new IllegalArgumentException("邮箱格式不正确");
            }
            return this;
        }
        
        public Builder age(int age) {
            if (age < 0 || age > 150) {
                throw new IllegalArgumentException("年龄必须在0-150之间");
            }
            this.age = age;
            return this;
        }
        
        public User build() {
            // 在build时进行最终验证
            if (username == null || email == null) {
                throw new IllegalStateException("用户名和邮箱必须设置");
            }
            return new User(username, email, age);
        }
    }
}

深度剖析

有些小伙伴在工作中可能觉得不可变对象创建麻烦,但它们带来的好处是巨大的:

  1. 线程安全:无需同步,可以在多线程间安全共享
  2. 易于缓存:可以安全地缓存,因为状态不会改变
  3. 避免意外的状态修改:不会被其他代码意外修改
  4. 简化推理:对象的状态是确定的

系统级的防御式编程

除了代码层面的防御,我们还需要在系统架构层面考虑防御措施。

断路器模式

在微服务架构中,断路器是重要的防御机制。

// 简化的断路器实现
public class CircuitBreaker {
    private final String name;
    private final int failureThreshold;
    private final long timeout;
    
    private State state = State.CLOSED;
    private int failureCount = 0;
    private long lastFailureTime = 0;
    
    enum State { CLOSED, OPEN, HALF_OPEN }
    
    public CircuitBreaker(String name, int failureThreshold, long timeout) {
        this.name = name;
        this.failureThreshold = failureThreshold;
        this.timeout = timeout;
    }
    
    public <T> T execute(Supplier<T> supplier) {
        if (state == State.OPEN) {
            // 检查是否应该尝试恢复
            if (System.currentTimeMillis() - lastFailureTime > timeout) {
                state = State.HALF_OPEN;
                log.info("断路器 {} 进入半开状态", name);
            } else {
                throw new CircuitBreakerOpenException("断路器开启,拒绝请求");
            }
        }
        
        try {
            T result = supplier.get();
            
            // 请求成功,重置状态
            if (state == State.HALF_OPEN) {
                state = State.CLOSED;
                failureCount = 0;
                log.info("断路器 {} 恢复关闭状态", name);
            }
            
            return result;
            
        } catch (Exception e) {
            handleFailure();
            throw e;
        }
    }
    
    private void handleFailure() {
        failureCount++;
        lastFailureTime = System.currentTimeMillis();
        
        if (state == State.HALF_OPEN || failureCount >= failureThreshold) {
            state = State.OPEN;
            log.warn("断路器 {} 开启,失败次数: {}", name, failureCount);
        }
    }
}

// 使用示例
public class UserServiceWithCircuitBreaker {
    private final CircuitBreaker circuitBreaker;
    private final RemoteUserService remoteService;
    
    public UserServiceWithCircuitBreaker() {
        this.circuitBreaker = new CircuitBreaker("UserService", 5, 60000);
        this.remoteService = new RemoteUserService();
    }
    
    public User getUser(String userId) {
        return circuitBreaker.execute(() -> remoteService.getUser(userId));
    }
}

限流和降级

// 简单的令牌桶限流
public class RateLimiter {
    private final int capacity;
    private final int tokensPerSecond;
    private double tokens;
    private long lastRefillTime;
    
    public RateLimiter(int capacity, int tokensPerSecond) {
        this.capacity = capacity;
        this.tokensPerSecond = tokensPerSecond;
        this.tokens = capacity;
        this.lastRefillTime = System.nanoTime();
    }
    
    public synchronized boolean tryAcquire() {
        refill();
        if (tokens >= 1) {
            tokens -= 1;
            return true;
        }
        return false;
    }
    
    private void refill() {
        long now = System.nanoTime();
        double seconds = (now - lastRefillTime) / 1e9;
        tokens = Math.min(capacity, tokens + seconds * tokensPerSecond);
        lastRefillTime = now;
    }
}

// 使用限流和降级
public class OrderService {
    private final RateLimiter rateLimiter;
    private final PaymentService paymentService;
    
    public OrderService() {
        this.rateLimiter = new RateLimiter(100, 10); // 100容量,每秒10个令牌
        this.paymentService = new PaymentService();
    }
    
    public PaymentResult processPayment(Order order) {
        // 限流检查
        if (!rateLimiter.tryAcquire()) {
            log.warn("系统繁忙,触发限流");
            return PaymentResult.rateLimited();
        }
        
        try {
            return paymentService.process(order);
        } catch (Exception e) {
            log.error("支付服务异常,触发降级", e);
            // 降级策略:返回排队中状态
            return PaymentResult.queued();
        }
    }
}

防御式编程的陷阱与平衡

防御式编程不是越多越好,过度防御也会带来问题。

避免过度防御

// 过度防御的例子
public class OverDefensiveExample {
    
    // 过多的null检查,使代码难以阅读
    public String processData(String data) {
        if (data == null) {
            return null;
        }
        
        String trimmed = data.trim();
        if (trimmed.isEmpty()) {
            return "";
        }
        
        // 即使知道trimmed不为空,还是继续检查...
        if (trimmed.length() > 1000) {
            log.warn("数据过长: {}", trimmed.length());
            // 但仍然继续处理...
        }
        
        // 更多的检查...
        return trimmed.toUpperCase();
    }
}

// 适度防御的例子
public class BalancedDefensiveExample {
    
    public String processData(String data) {
        // 在方法入口处统一验证
        if (StringUtils.isBlank(data)) {
            return "";
        }
        
        String trimmed = data.trim();
        
        // 核心业务逻辑,假设数据现在是有效的
        return transformData(trimmed);
    }
    
    private String transformData(String data) {
        // 内部方法可以假设输入是有效的
        if (data.length() > 1000) {
            // 记录日志但继续处理
            log.info("处理长数据: {}", data.length());
        }
        
        return data.toUpperCase();
    }
}

性能考量

防御性检查会带来性能开销,需要在关键路径上权衡。

public class PerformanceConsideration {
    
    // 在性能敏感的方法中,可以延迟验证
    public void processBatch(List<String> items) {
        // 先快速处理,最后统一验证
        List<String> results = new ArrayList<>();
        
        for (String item : items) {
            // 最小化的必要检查
            if (item != null) {
                results.add(processItemFast(item));
            }
        }
        
        // 批量验证结果
        validateResults(results);
    }
    
    private String processItemFast(String item) {
        // 假设这个方法很快,不做详细验证
        return item.toUpperCase();
    }
}

总结

经过以上详细剖析,相信你对防御式编程有了更深入的理解。

下面是我总结的一些实用建议:

核心原则

  1. 对输入保持怀疑:验证所有外部输入
  2. 明确失败:让失败尽早发生,提供清晰的错误信息
  3. 资源安全:确保资源被正确释放
  4. 优雅降级:在部分失败时保持系统可用
  5. 适度防御:避免过度防御导致的代码复杂

实践指南

场景 防御措施 示例
方法参数 入口验证 Objects.requireNonNull()
外部调用 异常处理 try-catch特定异常
资源操作 自动清理 try-with-resources
并发访问 不可变对象 final字段、防御性拷贝
系统集成 断路器 失败阈值、超时控制
高并发 限流降级 令牌桶、服务降级

我的建议

有些小伙伴在工作中,可能一开始觉得防御式编程增加了代码量,但从长期来看,它的收益是巨大的:

  1. 投资思维:前期的一点投入,避免后期的大规模故障
  2. 团队共识:在团队中建立防御式编程的文化和规范
  3. 工具支持:使用静态分析工具发现潜在问题
  4. 代码审查:在CR中重点关注防御性措施

记住,防御式编程的目标不是创建"完美"的代码。

而是创建健壮的代码——能够在面对意外时依然保持稳定的代码。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

更多项目实战在我的技术网站:http://www.susan.net.cn/project

posted @ 2025-11-04 10:48  苏三说技术  阅读(323)  评论(1)    收藏  举报