【基础数据篇】数据访问守卫:Accessor模式

1. 前言

在面向对象编程的世界里,封装(Encapsulation) 是第一大支柱。我们被告知要将数据(字段)声明为 private,然后通过公共方法来访问和修改它们。这听起来很简单,但你是否深入思考过这些问题:

  • 如果仅仅是为了读写一个值,为什么要大费周章地创建 getName()setName() 这种看似冗余的方法?
  • 直接使用 public 字段不是更简单、更直接吗?
  • 在简单的 getter/setter 之外,我们还能做什么?

Accessor(访问器)模式正是对"如何安全、可控地访问对象内部状态"这一问题的系统性回答。它远不止是自动生成的 get/set 方法,而是一种强大的设计思想,是对象边界的忠诚守卫,是保证数据完整性、实现懒加载、触发副作用、甚至管理访问权限的核心手段。

2. 定义

Accessor 模式是一种结构型设计模式,它提供了一种间接访问对象内部状态(字段)的方法。该模式通过定义专门的方法(即访问器)来读写字段值,从而将对象的内部表示与其外部访问方式分离开来。

该模式通常包含两种核心方法:

  1. Getter(获取器) :用于读取字段值的方法。通常以 getis(对于布尔值)为前缀。
  2. Setter(设置器) :用于修改字段值的方法。通常以 set 为前缀。
text 复制代码
Accessor 模式的核心思想:Accessor 模式的精髓不在于"访问"本身,而在于在访问的路径上建立了一个可控的关卡。这个关卡允许在数据进出对象时进行干预,从而实现以下目标:
1. 隐藏实现细节:外部无需知道数据是如何存储的。
2. 实施数据验证:在设置值之前确保其有效性。
3. 触发相关逻辑:值的变化可以自动引发通知、日志记录或状态更新。
4. 管理访问权限:可以对读写操作进行精细的权限控制。
5. 支持延迟加载:在第一次访问时才计算或获取耗时的数据。

3. 应用

Accessor 模式的应用场景极其广泛,从简单的数据载体到复杂的业务模型,无处不在。

3.1 场景一:数据验证与业务规则执行

这是 Setter 方法最经典的应用,解决了直接赋值无法保证数据的合法性问题。

java 复制代码
public class BankAccount {
    private double balance;

    // 简单的Getter
    public double getBalance() {
        return balance;
    }

    // 带有验证的Setter
    public void setBalance(double newBalance) {
        if (newBalance < 0) {
            throw new IllegalArgumentException("余额不能为负数");
        }
        this.balance = newBalance;
    }

    // 更业务化的方法,内部使用Setter进行验证
    public void withdraw(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("取款金额必须大于0");
        }
        setBalance(this.balance - amount); // 复用Setter的验证逻辑
    }
}

3.2 场景二:延迟加载(Lazy Initialization)

当对象的创建或数据的获取成本很高时,可以使用 Getter 来实现按需加载。

java 复制代码
public class HeavyResourceHolder {
    private HeavyResource resource;

    public HeavyResource getResource() {
        if (resource == null) {
            // 只有第一次调用getter时才会进行昂贵的初始化
            resource = ExpensiveObjectFactory.createHeavyResource();
        }
        return resource;
    }
}

3.3 场景三:派生属性与逻辑封装

Getter 方法返回的值不一定直接对应某个字段,可以是一个计算后的派生值。

java 复制代码
public class Order {
    private List<OrderItem> items;

    public BigDecimal getTotalPrice() {
        // 不存储总价,而是实时计算,保证与订单项一致
        return items.stream()
                   .map(item -> item.getPrice().multiply(new BigDecimal(item.getQuantity())))
                   .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}

3.4 场景四:通知与观察者模式

Setter 是触发状态变更通知的理想场所。

java 复制代码
public class ObservableModel {
    private String data;
    private List<ChangeListener> listeners = new ArrayList<>();

    public void setData(String newData) {
        String oldData = this.data;
        this.data = newData;
        // 数据变化后,自动通知所有监听器
        notifyListeners(oldData, newData);
    }

    private void notifyListeners(String oldData, String newData) {
        for (ChangeListener listener : listeners) {
            listener.onDataChanged(oldData, newData);
        }
    }
}

3.5 场景五:访问控制

在不同上下文中,可以对访问器进行精细控制。

java 复制代码
public class UserProfile {
    private String email;
    private String passwordHash;

    // 电子邮件可以被读取
    public String getEmail() { return email; }

    // 但密码哈希绝不应该被直接获取!
    // 因此,不提供 getPasswordHash() 方法。

    // 只提供验证密码的方法
    public boolean validatePassword(String inputPassword) {
        return hashFunction.hash(inputPassword).equals(passwordHash);
    }
}

4. 开源代码解析

Accessor 模式在开源项目中的应用已经超越了简单的手写方法,呈现出更高级的形态。

4.1 Lombok:自动化 Accessor 的典范

Project Lombok 是一个 Java 库,通过注解在编译时自动生成代码,其中 @Getter@Setter 是其最著名的功能。

代码示例:

java 复制代码
import lombok.Getter;
import lombok.Setter;

public class User {
    private @Getter @Setter String name;
    private @Getter @Setter int age;
    // Lombok 会在编译时生成 getName(), setName(), getAge(), setAge() 方法
}

深度解析:

  1. Lombok 消除了编写模板代码的负担,使代码更简洁。它并没有改变 Accessor 模式的本意,而是将其元编程化。开发者通过声明意图("这个字段需要getter和setter"),由工具自动实现,这保证了封装性,同时提升了开发效率。
  2. Lombok 还支持访问级别控制(如 @Setter(AccessLevel.PROTECTED))、懒加载(@Getter(lazy = true)),展示了 Accessor 模式的多样化应用。

4.2 PropertyHandler :Spring 属性注入

将 Java 对象(JavaBean)的属性(通过其 Accessor 方法)暴露为一个可以通过标准方式(内省)发现和操作的抽象接口。 Spring 容器利用这个标准接口,将配置元数据(如 XML、注解)动态地注入到 Bean 的属性中,从而实现控制反转(IoC)依赖注入(DI)


第一步:JavaBeans 标准 - 内省(Introspection)

假设有一个简单的 JavaBean:

java 复制代码
public class UserService {
    private UserDao userDao; // 属性
    private int timeout;

    // Accessor 方法
    public UserDao getUserDao() { return userDao; }
    public void setUserDao(UserDao userDao) { this.userDao = userDao; }
    public int getTimeout() { return timeout; }
    public void setTimeout(int timeout) { this.timeout = timeout; }
}

Spring 通过内省知道 UserServiceuserDaotimeout 两个属性。 核心类:java.beans.Introspector

java 复制代码
// Spring 内部使用 Spring 自封装的工具,但原理与 JDK 内省相同
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(userService);
// 底层会调用类似 JDK Introspector 的逻辑:

// 1. 获取 BeanInfo
BeanInfo beanInfo = Introspector.getBeanInfo(UserService.class, Object.class);
// 2. 获取属性描述符
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();

for (PropertyDescriptor pd : pds) {
    String propertyName = pd.getName(); // "userDao", "timeout" (但不会有 "class")
    Method readMethod = pd.getReadMethod(); // getter: getUserDao(), getTimeout()
    Method writeMethod = pd.getWriteMethod(); // setter: setUserDao(...), setTimeout(...)
    Class<?> propertyType = pd.getPropertyType(); // UserDao.class, int.class
}

PropertyDescriptor 是关键:它封装了一个属性的所有元数据:名字、类型、读方法(Getter)、写方法(Setter)。Spring 拿到这个数组,就相当于拿到了这个类的"配置清单"。


第二步:Spring 的核心 - BeanWrapper 与数据绑定

java 复制代码
public interface BeanWrapper extends ConfigurablePropertyAccessor {
    // 设置一个属性的值(核心方法)
    void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException;
    // 获取一个属性的值
    Object getPropertyValue(String propertyName) throws BeansException;
    // 获取属性描述符(基于内省)
    PropertyDescriptor[] getPropertyDescriptors() throws BeansException;
    // 获取特定属性的描述符
    PropertyDescriptor getPropertyDescriptor(String propertyName) throws BeansException;
}

当我们调用 bw.setPropertyValue("userDao", someDaoImpl) 时会获取 PropertyHandlerBeanWrapperImpl 会为每个属性缓存一个 PropertyHandler,它封装了对应的 Getter 和 Setter 的Method 对象:

java 复制代码
// org.springframework.beans.BeanWrapperImpl
private abstract class PropertyHandler {
    final Method readMethod;
    final Method writeMethod;
    final Class<?> propertyType;

    // 核心方法:将值 value 设置到目标对象 target 的此属性上
    public void setValue(@Nullable Object value) throws Exception {
        // ... 类型转换等准备工作
        // 【最关键的一行】通过反射调用 Setter 方法!
        ReflectionUtils.invokeMethod(this.writeMethod, getWrappedInstance(), value);
    }
}

如果我们要设置 bw.setPropertyValue("timeout", "30"),但 timeout 属性是 int 类型,而传入的是 String "30",怎么办? BeanWrapperImpl 继承自 TypeConverterSupport,它在调用 Setter 前,会使用 ConversionService 进行类型转换:

java 复制代码
// 在 setValue 方法内部的简化逻辑
public void setValue(@Nullable Object newValue) throws Exception {
    Object convertedValue = newValue;
    if (this.typeConverter != null) {
        // 使用 ConversionService 将值转换为属性所需的类型
        convertedValue = this.typeConverter.convertIfNecessary(newValue, this.propertyType);
    }
    // 然后再用反射调用方法
    Method writeMethod = this.writeMethod;
    ReflectionUtils.invokeMethod(writeMethod, getWrappedInstance(), convertedValue);
}

这意味着,Spring 的依赖注入之所以能将从配置文件(全是字符串)读取的值注入到 intboolean 甚至自定义类型的属性中,全靠 BeanWrapperConversionService 在 Accessor 调用前做的这份"预处理"工作。


第三步:依赖注入 - 将一切连接起来

现在,我们看看 Spring IoC 容器(以 AbstractAutowireCapableBeanFactory 为例)在创建一个 Bean 并注入属性时的完整流程。源码关键路径:doCreateBean -> populateBean

java 复制代码
// org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
    // 1. 实例化对象(通过构造函数)
    BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
    Object bean = instanceWrapper.getWrappedInstance();

    // 2. 【核心】为实例填充属性(依赖注入发生在这里!)
    populateBean(beanName, mbd, instanceWrapper);
    return bean;
}

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    // ... 检查等代码省略

    // 获取 BeanDefinition 中定义的属性值(例如来自 XML 的 <property> 标签或 @Value 注解)
    PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

    if (pvs != null) {
        // 【核心】应用属性值到 BeanWrapper 包装的实例上
        applyPropertyValues(beanName, mbd, bw, pvs);
    }
}

// 在 applyPropertyValues 中,最终会调用:
bw.setPropertyValues(pvs); /

整个过程串联起来就是:

  1. 解析配置:容器解析 XML 或注解,生成一个 BeanDefinition,其中包含一个 PropertyValues 对象,它保存了需要注入的属性名和值(可能是未解析的字符串)。
  2. 实例化:通过反射创建 Bean 的实例。
  3. 包装:用 BeanWrapperImpl 包装这个实例。
  4. 内省:BeanWrapperImpl 通过内省获取该 Bean 的所有 PropertyDescriptor
  5. 注入(依赖解析与设置):遍历 PropertyValues,对于每个属性:
    1. 依赖查找:如果值是引用(如 ref="userDao"),先根据名称从容器中获取对应的 Bean(userDao 实例)。
    2. 类型转换:如果需要,使用 ConversionService 将值转换为属性所需的类型。
    3. 调用 Accessor:通过 PropertyHandler,使用反射调用对应的 Setter 方法,将最终处理好的值注入到目标 Bean 中。

4.3 Hibernate:延迟加载代理

Hibernate 是一个 ORM 框架,它的核心挑战是:将面向对象的领域模型与关系型数据库的表结构进行映射,并管理其生命周期的同步。Accessor(Getter/Setter)是 Hibernate 实现这一目标的核心钩子,并且有许多经典应用。这里主要分享一下Hibernate中的延迟加载。

java 复制代码
@Entity
public class Order {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY) // 延迟加载
    private Customer customer;

    public Customer getCustomer() { // 这个 Accessor 是关键!
        return customer;
    }
    public void setCustomer(Customer customer) {
        this.customer = customer;
    }
}

当 Hibernate 从数据库加载一个 Order 实体时,如果 customerLAZY,它不会执行 JOIN 查询或单独的 SELECT 来获取 Customer。相反,在 DefaultLoadEventListener 中,它会创建一个 Customer 的代理对象,并将其赋给 order.customer 字段。

java 复制代码
// 伪代码,在 Hibernate 内部
Customer customerProxy = session.getFactory()
                                .getMetamodel()
                                .getEntityPersister(Customer.class)
                                .createProxy(customerId, session);
order.setCustomer(customerProxy); // 设置的是代理,不是真实对象

当代码第一次调用 order.getCustomer().getName() 时,魔法发生了:

java 复制代码
// 伪代码,模拟 Hibernate 生成的代理
public class CustomerProxy extends Customer {
    private LazyInitializer initializer; // 持有初始化器
    private Customer target; // 真实目标对象,初始为 null

    @Override
    public String getName() {
        // 【关键】在方法执行前,先初始化(加载真实数据)
        initialize();
        // 将调用委托给已加载的真实对象
        return target.getName();
    }

    private void initialize() {
        if (target == null) {
            synchronized (this) {
                if (target == null) {
                    // 通过 LazyInitializer 从数据库加载真实实体
                    target = (Customer) initializer.initialize();
                }
            }
        }
    }
}

LazyInitializer.initialize() 方法会执行真正的 SQL 查询(如 SELECT * FROM customer WHERE id = ?),将结果合成一个真正的 Customer 实例。

Hibernate 通过拦截 getCustomer() 这个 Accessor 返回的代理对象上的任何方法调用,实现了按需加载。这对业务代码是完全透明的,无需编写任何加载逻辑。

5. 总结

Accessor 模式(Getter/Setter)的本质远不止是访问字段。其核心设计思想是:通过方法调用来替代对字段的直接访问,从而在数据读写的路径上建立一个"守卫"或"代理点",实现对数据访问行为的控制、管理和增强。

它深刻体现了以下几个经典设计模式的思想:

  1. 代理模式: Accessor 方法充当了代理的角色。客户端意图访问数据,但必须通过 Accessor 这个"门卫"。这个门卫可以决定是直接放行,还是进行额外操作(如验证、通知、延迟加载)。

  2. 模板方法模式: 一个 Accessor 方法定义了一个算法骨架。例如,一个 Setter 的"模板"可能是:[验证] -> [触发前置通知] -> [赋值] -> [触发后置通知]。具体的验证规则和通知逻辑则是可变的子步骤。

  3. 装饰器模式: 原始的字段访问是基础功能,而 Accessor 方法是在这个基础功能上"装饰"了额外的行为(如日志、权限检查),而不改变客户端调用方式。

核心价值:将"数据访问"这个动作,从一个简单的内存操作,提升为一个可扩展、可控制的"行为"。


在下一篇《【基础数据篇】数据格式化妆师:Formatter模式》中,将探讨如何将数据的内部表示与对外展示格式解耦,实现日期、数字、货币等信息的美观、本地化与人性化呈现。

相关推荐
Cache技术分享1 天前
213. Java 函数式编程风格 - Java 中的简单 for 循环转换:从命令式到函数式
前端·后端
Mr_WangAndy1 天前
C++设计模式_行为型模式_观察者模式Observer(发布-订阅(Publish-Subscribe))
c++·观察者模式·设计模式
知其然亦知其所以然1 天前
面试官问:MySQL表损坏怎么修?不会这三招你就凉了!
后端·mysql·面试
北海道浪子1 天前
多模型Codex、ChatGPT、Claude、DeepSeek等顶级AI在开发中的使用
前端·后端·程序员
ConardLi1 天前
一个小技巧,帮你显著提高 AI 的回答质量!
前端·人工智能·后端
CrabXin1 天前
如何让你的前端应用像“永动机”一样保持登录状态?
前端·设计模式
阿无,1 天前
Java设计模式之装饰者模式
java·开发语言·设计模式
间彧1 天前
Java数值类型:Long、Double、Float、Integer边界值详解与应用实践
后端