1. 前言
在面向对象编程的世界里,封装(Encapsulation) 是第一大支柱。我们被告知要将数据(字段)声明为 private
,然后通过公共方法来访问和修改它们。这听起来很简单,但你是否深入思考过这些问题:
- 如果仅仅是为了读写一个值,为什么要大费周章地创建
getName()
和setName()
这种看似冗余的方法? - 直接使用
public
字段不是更简单、更直接吗? - 在简单的 getter/setter 之外,我们还能做什么?
Accessor(访问器)模式正是对"如何安全、可控地访问对象内部状态"这一问题的系统性回答。它远不止是自动生成的 get/set 方法,而是一种强大的设计思想,是对象边界的忠诚守卫,是保证数据完整性、实现懒加载、触发副作用、甚至管理访问权限的核心手段。
2. 定义
Accessor 模式是一种结构型设计模式,它提供了一种间接访问对象内部状态(字段)的方法。该模式通过定义专门的方法(即访问器)来读写字段值,从而将对象的内部表示与其外部访问方式分离开来。
该模式通常包含两种核心方法:
- Getter(获取器) :用于读取字段值的方法。通常以
get
或is
(对于布尔值)为前缀。 - 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() 方法
}
深度解析:
- Lombok 消除了编写模板代码的负担,使代码更简洁。它并没有改变 Accessor 模式的本意,而是将其元编程化。开发者通过声明意图("这个字段需要getter和setter"),由工具自动实现,这保证了封装性,同时提升了开发效率。
- 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 通过内省知道 UserService
有 userDao
和 timeout
两个属性。 核心类: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)
时会获取 PropertyHandler
,BeanWrapperImpl
会为每个属性缓存一个 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 的依赖注入之所以能将从配置文件(全是字符串)读取的值注入到 int
、boolean
甚至自定义类型的属性中,全靠 BeanWrapper
和 ConversionService
在 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); /
整个过程串联起来就是:
- 解析配置:容器解析 XML 或注解,生成一个
BeanDefinition
,其中包含一个PropertyValues
对象,它保存了需要注入的属性名和值(可能是未解析的字符串)。 - 实例化:通过反射创建 Bean 的实例。
- 包装:用
BeanWrapperImpl
包装这个实例。 - 内省:
BeanWrapperImpl
通过内省获取该 Bean 的所有PropertyDescriptor
。 - 注入(依赖解析与设置):遍历
PropertyValues
,对于每个属性:- 依赖查找:如果值是引用(如
ref="userDao"
),先根据名称从容器中获取对应的 Bean(userDao
实例)。 - 类型转换:如果需要,使用
ConversionService
将值转换为属性所需的类型。 - 调用 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
实体时,如果 customer
是 LAZY
,它不会执行 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)的本质远不止是访问字段。其核心设计思想是:通过方法调用来替代对字段的直接访问,从而在数据读写的路径上建立一个"守卫"或"代理点",实现对数据访问行为的控制、管理和增强。
它深刻体现了以下几个经典设计模式的思想:
-
代理模式: Accessor 方法充当了代理的角色。客户端意图访问数据,但必须通过 Accessor 这个"门卫"。这个门卫可以决定是直接放行,还是进行额外操作(如验证、通知、延迟加载)。
-
模板方法模式: 一个 Accessor 方法定义了一个算法骨架。例如,一个 Setter 的"模板"可能是:
[验证] -> [触发前置通知] -> [赋值] -> [触发后置通知]
。具体的验证规则和通知逻辑则是可变的子步骤。 -
装饰器模式: 原始的字段访问是基础功能,而 Accessor 方法是在这个基础功能上"装饰"了额外的行为(如日志、权限检查),而不改变客户端调用方式。
核心价值:将"数据访问"这个动作,从一个简单的内存操作,提升为一个可扩展、可控制的"行为"。
在下一篇《【基础数据篇】数据格式化妆师:Formatter模式》中,将探讨如何将数据的内部表示与对外展示格式解耦,实现日期、数字、货币等信息的美观、本地化与人性化呈现。