欢迎你来读这篇博客,这篇博客主要是关于享元模式。
其中包括享元模式的核心思想、适用场景、内部状态与外部状态、享元工厂、优缺点、与缓存/对象池/单例模式的区别,以及 Java 后端开发中权限资源元数据共享的完整案例。
序言
在软件开发中,我们经常会遇到一种问题:
系统中需要创建大量相似对象,但这些对象内部有很多重复数据。
例如一个后台权限系统中,有很多用户、很多角色、很多权限关系。
权限资源本身可能只有几百个:
1 2 3 4 5 6 7
| user:add user:update user:delete role:add role:assign order:view order:export
|
但是用户和角色的授权关系可能有几十万甚至几百万条。
如果每一条授权关系里都保存一份完整的权限对象:
1 2 3
| new Permission("user:add", "新增用户", "BUTTON", "用户管理") new Permission("user:add", "新增用户", "BUTTON", "用户管理") new Permission("user:add", "新增用户", "BUTTON", "用户管理")
|
那就会出现大量重复对象。
这些对象内容一样,却在内存中重复存在。
这就像每个人进电影院都自己带一块 IMAX 银幕,场面很豪华,服务器先崩。
享元模式就是为了解决这类问题:
通过共享大量细粒度对象中相同的部分,减少对象数量,降低内存开销。
简单说:
享元模式就是把可共享的对象缓存起来,大家共用一份。
正文
chapter 1:什么是享元模式
享元模式,英文是 Flyweight Pattern,属于结构型设计模式。
它的定义是:
运用共享技术有效地支持大量细粒度对象的复用。
这句话听起来有点抽象。
换成大白话:
如果系统中有大量对象,并且这些对象有很多重复的、不变的状态,就可以把这部分状态抽出来共享,避免重复创建。
享元模式通常包含几个角色:
- Flyweight 抽象享元:定义共享对象的公共接口。
- ConcreteFlyweight 具体享元:保存可共享的内部状态。
- UnsharedConcreteFlyweight 非共享享元:不适合共享的对象,某些场景可选。
- FlyweightFactory 享元工厂:负责创建和复用享元对象。
- Client 客户端:使用享元对象,并传入外部状态。
结构如下:
1 2 3 4 5 6 7 8
| Client │ ▼ FlyweightFactory │ ├── key1 -> ConcreteFlyweight ├── key2 -> ConcreteFlyweight └── key3 -> ConcreteFlyweight
|
享元工厂通常内部维护一个缓存:
1
| private final Map<String, Flyweight> cache = new ConcurrentHashMap<>();
|
当客户端需要某个对象时,先从缓存中取。
如果存在,直接复用。
如果不存在,再创建并放入缓存。
chapter 2:享元模式解决什么问题
享元模式主要解决的是:
大量重复对象造成的内存浪费问题。
它适合以下场景:
- 对象数量非常多;
- 对象之间存在大量重复状态;
- 重复状态可以被抽离出来共享;
- 共享对象最好是不可变的;
- 外部变化状态可以由客户端传入。
例如:
- 字符串常量池;
- Integer 缓存;
- 数据库中权限资源元数据;
- 地图系统中的图标样式;
- 游戏中的子弹类型、树木类型、怪物类型;
- 文档编辑器中的字符字形;
- 报表系统中的单元格样式;
- 订单系统中的状态定义、币种定义、地区定义;
- 大量规则对象中相同的规则模板。
享元模式不是为了让代码看起来高级,而是为了省内存。
它的本质非常务实:
重复的东西不要重复创建。
chapter 3:内部状态和外部状态
理解享元模式,最重要的是理解两个概念:
这是享元模式的灵魂。
1. 内部状态
内部状态指的是:
可以被多个对象共享,且通常不会变化的状态。
例如权限资源:
1 2 3 4
| 权限编码:user:add 权限名称:新增用户 权限类型:BUTTON 所属模块:用户管理
|
这些信息对所有用户都是一样的。
用户 A 拥有 user:add 权限。
用户 B 也拥有 user:add 权限。
权限本身的名称、类型、模块并不会因为用户不同而不同。
所以这部分可以作为内部状态共享。
2. 外部状态
外部状态指的是:
不能共享,依赖具体上下文变化的状态。
例如:
1 2 3 4 5 6
| 用户 ID 角色 ID 租户 ID 授权时间 是否启用 数据范围
|
这些信息和具体用户、角色、租户有关,不能放进共享对象里。
如果把用户 ID 放进享元对象,那这个享元对象就不能共享了。
3. 一句话区分
可以这样记:
内部状态属于对象本身,可以共享。
外部状态属于使用场景,由客户端传入。
享元模式的关键就是:
把内部状态放到享元对象里,把外部状态放到上下文里。
chapter 4:不用享元模式会怎样
假设系统里有 100 万条用户权限关系。
每条关系都保存一个完整权限对象。
1 2 3 4 5 6 7 8 9 10 11 12
| public class UserPermission {
private Long userId;
private String permissionCode;
private String permissionName;
private String permissionType;
private String moduleName; }
|
如果很多用户都拥有同一个权限:
1
| user:add 新增用户 BUTTON 用户管理
|
那这些字段会被重复保存很多次。
1 2 3 4
| UserPermission(1, user:add, 新增用户, BUTTON, 用户管理) UserPermission(2, user:add, 新增用户, BUTTON, 用户管理) UserPermission(3, user:add, 新增用户, BUTTON, 用户管理) UserPermission(4, user:add, 新增用户, BUTTON, 用户管理)
|
这里真正变化的是:
权限定义本身没有变化。
如果系统规模很大,这种重复对象会造成:
- 内存浪费;
- GC 压力增加;
- 对象创建成本增加;
- 缓存占用变大;
- 序列化数据变大;
- 数据结构不够清晰。
享元模式会把权限定义拆出去:
1
| PermissionFlyweight permission = PermissionFlyweightFactory.get("user:add");
|
每个用户权限关系只保存:
1
| userId + permissionCode + 外部状态
|
权限元数据由享元对象共享。
chapter 5:享元模式的核心思想
享元模式的核心思想可以总结为四步:
- 找出对象中可以共享的内部状态;
- 把内部状态封装成享元对象;
- 把不可共享的外部状态从享元对象中剥离;
- 使用享元工厂统一管理和复用享元对象。
例如:
1 2
| 权限定义 = 内部状态 用户授权关系 = 外部状态
|
享元对象:
外部上下文:
享元工厂:
1
| PermissionFlyweightFactory
|
客户端使用时:
1 2 3
| PermissionFlyweight permission = factory.getPermission("user:add");
permission.check(context);
|
共享对象只保存稳定的权限定义。
用户、角色、租户等上下文信息由外部传入。
chapter 6:案例背景:权限资源元数据共享
下面用一个 Java 后端常见场景来讲享元模式:权限系统。
假设我们正在开发一个 RBAC 权限系统。
系统中有三类数据:
- 权限资源定义;
- 用户与权限关系;
- 角色与权限关系。
权限资源定义相对稳定,例如:
1 2 3 4 5
| user:add 新增用户 BUTTON 用户管理 user:update 修改用户 BUTTON 用户管理 user:delete 删除用户 BUTTON 用户管理 role:add 新增角色 BUTTON 角色管理 role:assign 分配权限 BUTTON 角色管理
|
这些权限定义在系统中只有一份。
但是授权关系很多。
例如:
1 2 3 4
| 用户 1 拥有 user:add 用户 2 拥有 user:add 用户 3 拥有 user:add 用户 4 拥有 user:add
|
如果每个授权关系都创建完整权限对象,就很浪费。
所以我们使用享元模式:
PermissionFlyweight:共享权限定义;
PermissionFlyweightFactory:缓存权限定义;
UserPermissionContext:保存用户、租户、授权状态等外部状态;
PermissionChecker:使用享元对象进行权限判断。
chapter 7:定义权限类型枚举
先定义权限类型。
1 2 3 4 5 6 7 8
| public enum PermissionType {
MENU,
BUTTON,
API }
|
权限可能是:
chapter 8:定义抽象享元 PermissionFlyweight
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public interface PermissionFlyweight {
String code();
String name();
PermissionType type();
String module();
boolean match(String permissionCode);
void print(UserPermissionContext context); }
|
这个接口代表享元对象。
它暴露的是权限定义相关能力。
注意 print() 方法中传入了:
1
| UserPermissionContext context
|
这是外部状态。
享元对象本身不保存用户 ID、租户 ID、授权时间。
chapter 9:定义具体享元 BasicPermissionFlyweight
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| public class BasicPermissionFlyweight implements PermissionFlyweight {
private final String code;
private final String name;
private final PermissionType type;
private final String module;
public BasicPermissionFlyweight(String code, String name, PermissionType type, String module) { if (code == null || code.isBlank()) { throw new IllegalArgumentException("code can not be blank"); }
if (name == null || name.isBlank()) { throw new IllegalArgumentException("name can not be blank"); }
if (type == null) { throw new IllegalArgumentException("type can not be null"); }
if (module == null || module.isBlank()) { throw new IllegalArgumentException("module can not be blank"); }
this.code = code; this.name = name; this.type = type; this.module = module; }
@Override public String code() { return code; }
@Override public String name() { return name; }
@Override public PermissionType type() { return type; }
@Override public String module() { return module; }
@Override public boolean match(String permissionCode) { return this.code.equals(permissionCode); }
@Override public void print(UserPermissionContext context) { System.out.println("用户 " + context.userId() + " 在租户 " + context.tenantId() + " 拥有权限:" + name + " [" + code + "]" + ",模块:" + module + ",类型:" + type); } }
|
这个类只保存权限定义。
这些字段都是 final,对象创建后不可变。
这是享元对象很重要的实践建议:
享元对象最好设计成不可变对象。
因为它会被多个地方共享。
如果共享对象可变,某个地方一改,所有使用方都受影响,系统就会变成“共享事故现场”。
chapter 10:定义外部状态 UserPermissionContext
外部状态单独放在上下文对象里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import java.time.LocalDateTime;
public class UserPermissionContext {
private final Long userId;
private final Long tenantId;
private final String permissionCode;
private final boolean enabled;
private final LocalDateTime grantedAt;
public UserPermissionContext(Long userId, Long tenantId, String permissionCode, boolean enabled, LocalDateTime grantedAt) { this.userId = userId; this.tenantId = tenantId; this.permissionCode = permissionCode; this.enabled = enabled; this.grantedAt = grantedAt; }
public Long userId() { return userId; }
public Long tenantId() { return tenantId; }
public String permissionCode() { return permissionCode; }
public boolean enabled() { return enabled; }
public LocalDateTime grantedAt() { return grantedAt; } }
|
这里保存的是用户授权关系中的个性化数据:
- 哪个用户;
- 哪个租户;
- 哪个权限编码;
- 是否启用;
- 什么时候授权。
这些不能放进享元对象里。
chapter 11:定义享元工厂 PermissionFlyweightFactory
享元工厂负责缓存和复用享元对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
public class PermissionFlyweightFactory {
private final Map<String, PermissionFlyweight> cache = new ConcurrentHashMap<>();
public PermissionFlyweight getPermission(String code, String name, PermissionType type, String module) { return cache.computeIfAbsent( code, key -> new BasicPermissionFlyweight(code, name, type, module) ); }
public PermissionFlyweight getPermission(String code) { PermissionFlyweight permission = cache.get(code);
if (permission == null) { throw new IllegalArgumentException("Permission not found, code = " + code); }
return permission; }
public int size() { return cache.size(); } }
|
核心代码是:
1
| cache.computeIfAbsent(...)
|
这行代码的含义是:
如果缓存中已经有这个权限对象,就直接返回;如果没有,就创建一个并放入缓存。
这就是享元模式的核心。
chapter 12:初始化权限享元对象
可以在系统启动时加载权限定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class PermissionBootstrap {
public static void init(PermissionFlyweightFactory factory) { factory.getPermission("user:add", "新增用户", PermissionType.BUTTON, "用户管理"); factory.getPermission("user:update", "修改用户", PermissionType.BUTTON, "用户管理"); factory.getPermission("user:delete", "删除用户", PermissionType.BUTTON, "用户管理");
factory.getPermission("role:add", "新增角色", PermissionType.BUTTON, "角色管理"); factory.getPermission("role:assign", "分配权限", PermissionType.BUTTON, "角色管理");
factory.getPermission("order:view", "查看订单", PermissionType.API, "订单管理"); factory.getPermission("order:export", "导出订单", PermissionType.BUTTON, "订单管理"); } }
|
真实项目中,这些权限定义可能来自数据库:
也可能来自配置文件、初始化脚本或权限中心。
chapter 13:使用享元对象进行权限判断
定义权限检查器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import java.util.List;
public class PermissionChecker {
private final PermissionFlyweightFactory factory;
public PermissionChecker(PermissionFlyweightFactory factory) { this.factory = factory; }
public boolean hasPermission(List<UserPermissionContext> contexts, String requiredPermissionCode) { for (UserPermissionContext context : contexts) { if (!context.enabled()) { continue; }
PermissionFlyweight permission = factory.getPermission(context.permissionCode());
if (permission.match(requiredPermissionCode)) { return true; } }
return false; }
public void printPermissions(List<UserPermissionContext> contexts) { for (UserPermissionContext context : contexts) { PermissionFlyweight permission = factory.getPermission(context.permissionCode());
permission.print(context); } } }
|
注意这里:
1
| PermissionFlyweight permission = factory.getPermission(context.permissionCode());
|
上下文中只保存权限编码。
真正的权限元数据从享元工厂中获取。
这样多个上下文可以共享同一个权限定义对象。
chapter 14:客户端使用案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import java.time.LocalDateTime; import java.util.List;
public class FlyweightDemo {
public static void main(String[] args) { PermissionFlyweightFactory factory = new PermissionFlyweightFactory();
PermissionBootstrap.init(factory);
List<UserPermissionContext> permissions = List.of( new UserPermissionContext(1L, 100L, "user:add", true, LocalDateTime.now()), new UserPermissionContext(1L, 100L, "user:update", true, LocalDateTime.now()), new UserPermissionContext(1L, 100L, "order:view", true, LocalDateTime.now()),
new UserPermissionContext(2L, 100L, "user:add", true, LocalDateTime.now()), new UserPermissionContext(2L, 100L, "role:assign", false, LocalDateTime.now()) );
PermissionChecker checker = new PermissionChecker(factory);
checker.printPermissions(permissions);
boolean hasUserAdd = checker.hasPermission(permissions, "user:add");
boolean hasRoleAssign = checker.hasPermission(permissions, "role:assign");
System.out.println("是否拥有 user:add 权限:" + hasUserAdd); System.out.println("是否拥有 role:assign 权限:" + hasRoleAssign); System.out.println("享元对象数量:" + factory.size()); System.out.println("授权关系数量:" + permissions.size()); } }
|
输出类似:
1 2 3 4 5 6 7 8 9
| 用户 1 在租户 100 拥有权限:新增用户 [user:add],模块:用户管理,类型:BUTTON 用户 1 在租户 100 拥有权限:修改用户 [user:update],模块:用户管理,类型:BUTTON 用户 1 在租户 100 拥有权限:查看订单 [order:view],模块:订单管理,类型:API 用户 2 在租户 100 拥有权限:新增用户 [user:add],模块:用户管理,类型:BUTTON 用户 2 在租户 100 拥有权限:分配权限 [role:assign],模块:角色管理,类型:BUTTON 是否拥有 user:add 权限:true 是否拥有 role:assign 权限:false 享元对象数量:7 授权关系数量:5
|
这里有 5 条授权关系,但权限定义对象只有 7 个。
如果授权关系是 500 万条,而权限定义只有 500 个,享元模式的价值就非常明显。
chapter 15:享元模式中的共享对象必须谨慎设计
享元对象最重要的要求是:
尽量不可变。
例如:
1 2 3 4 5 6 7 8 9 10
| public class BasicPermissionFlyweight implements PermissionFlyweight {
private final String code;
private final String name;
private final PermissionType type;
private final String module; }
|
这里所有字段都是 final。
对象创建后不能被修改。
如果提供 setter:
1 2 3
| public void setName(String name) { this.name = name; }
|
那就危险了。
因为多个用户上下文都共享同一个权限对象。
一旦某个地方修改了权限名称,所有使用这个享元对象的地方都会受影响。
共享对象可变,基本就是在代码里放了一只没关笼子的猴子。
chapter 16:内部状态不能依赖外部上下文
享元对象不应该保存用户信息。
错误示例:
1 2 3 4 5 6 7 8
| public class BadPermissionFlyweight {
private String code;
private String name;
private Long userId; }
|
这里的 userId 是外部状态。
如果把它放进享元对象,这个对象就不能被多个用户共享。
正确做法是:
1 2 3 4 5 6
| public class BasicPermissionFlyweight {
private final String code;
private final String name; }
|
用户信息放在:
1 2 3 4 5 6
| public class UserPermissionContext {
private final Long userId;
private final String permissionCode; }
|
享元对象保存共享状态。
上下文保存变化状态。
这是享元模式最核心的边界。
chapter 17:Java 中的享元思想
Java 里有很多享元思想的影子。
1. String 常量池
1 2 3 4
| String a = "hello"; String b = "hello";
System.out.println(a == b);
|
结果通常是:
因为字符串字面量 "hello" 会进入字符串常量池,多个引用可以共享同一个字符串对象。
这就是典型的共享思想。
2. Integer 缓存
1 2 3 4
| Integer a = 100; Integer b = 100;
System.out.println(a == b);
|
结果通常是:
因为 Java 对一定范围内的 Integer 做了缓存。
但是:
1 2 3 4
| Integer x = 1000; Integer y = 1000;
System.out.println(x == y);
|
结果通常是:
因为超过缓存范围后,可能会创建新对象。
3. Boolean 缓存
1 2 3 4
| Boolean a = Boolean.TRUE; Boolean b = Boolean.valueOf(true);
System.out.println(a == b);
|
通常是:
因为 Boolean 只有两个值,天然适合共享。
4. 枚举对象
枚举实例天然是固定的、可共享的。
1
| PermissionType type = PermissionType.BUTTON;
|
枚举也体现了共享固定对象的思想。
当然,这些不一定都严格等同于 GoF 享元模式,但它们都体现了同一个原则:
可复用的对象不要重复创建。
chapter 18:Spring Boot 中如何落地享元模式
在 Spring Boot 项目中,享元工厂可以作为一个单例 Bean。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import org.springframework.stereotype.Component;
import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
@Component public class PermissionFlyweightFactory {
private final Map<String, PermissionFlyweight> cache = new ConcurrentHashMap<>();
public PermissionFlyweight getPermission(String code) { PermissionFlyweight permission = cache.get(code);
if (permission == null) { throw new IllegalArgumentException("Permission not found, code = " + code); }
return permission; }
public void register(String code, String name, PermissionType type, String module) { cache.put(code, new BasicPermissionFlyweight(code, name, type, module)); }
public void refresh(Map<String, PermissionDefinitionDTO> definitions) { cache.clear();
for (PermissionDefinitionDTO definition : definitions.values()) { register( definition.getCode(), definition.getName(), definition.getType(), definition.getModule() ); } } }
|
权限定义 DTO:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class PermissionDefinitionDTO {
private String code;
private String name;
private PermissionType type;
private String module;
public String getCode() { return code; }
public String getName() { return name; }
public PermissionType getType() { return type; }
public String getModule() { return module; } }
|
应用启动时加载权限定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component;
import java.util.List;
@Component public class PermissionFlyweightLoader implements CommandLineRunner {
private final PermissionRepository permissionRepository;
private final PermissionFlyweightFactory permissionFlyweightFactory;
public PermissionFlyweightLoader(PermissionRepository permissionRepository, PermissionFlyweightFactory permissionFlyweightFactory) { this.permissionRepository = permissionRepository; this.permissionFlyweightFactory = permissionFlyweightFactory; }
@Override public void run(String... args) { List<PermissionDefinitionDTO> definitions = permissionRepository.findAllDefinitions();
for (PermissionDefinitionDTO definition : definitions) { permissionFlyweightFactory.register( definition.getCode(), definition.getName(), definition.getType(), definition.getModule() ); } } }
|
Repository 示例:
1 2 3 4 5 6
| import java.util.List;
public interface PermissionRepository {
List<PermissionDefinitionDTO> findAllDefinitions(); }
|
权限服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import org.springframework.stereotype.Service;
import java.util.List;
@Service public class PermissionService {
private final PermissionRepository permissionRepository;
private final PermissionFlyweightFactory permissionFlyweightFactory;
public PermissionService(PermissionRepository permissionRepository, PermissionFlyweightFactory permissionFlyweightFactory) { this.permissionRepository = permissionRepository; this.permissionFlyweightFactory = permissionFlyweightFactory; }
public boolean hasPermission(Long userId, String requiredPermissionCode) { List<UserPermissionContext> contexts = permissionRepository.findUserPermissions(userId);
for (UserPermissionContext context : contexts) { if (!context.enabled()) { continue; }
PermissionFlyweight permission = permissionFlyweightFactory.getPermission( context.permissionCode() );
if (permission.match(requiredPermissionCode)) { return true; } }
return false; } }
|
这个结构下:
- 数据库保存权限定义和授权关系;
- Spring 启动时加载权限定义为享元对象;
- 用户授权关系只保存外部状态;
- 权限判断时通过 code 获取共享对象。
chapter 19:享元缓存如何刷新
真实项目中,权限定义可能会变化。
例如管理员新增了一个权限:
这时享元缓存需要刷新。
常见方式有几种。
1. 手动刷新
提供一个管理接口:
1 2 3 4
| @PostMapping("/permissions/cache/refresh") public void refreshPermissionCache() { permissionCacheRefreshService.refresh(); }
|
2. 监听配置变更
如果权限定义来自配置中心,可以监听变更事件后刷新。
3. 数据变更后主动刷新
新增、修改、删除权限定义时,同步刷新工厂缓存。
4. 使用本地缓存组件
可以使用 Caffeine 之类本地缓存,并设置过期策略。
不过要注意:
享元工厂不一定等于缓存框架。
享元模式强调共享对象结构。
缓存框架强调数据访问优化。
两者可以结合,但不是一回事。
chapter 20:并发安全问题
享元对象通常会被多个线程共享,所以要考虑线程安全。
建议:
- 享元对象设计成不可变对象;
- 工厂内部使用线程安全 Map;
- 避免在享元对象中保存请求级状态;
- 缓存刷新时注意并发读写;
- 如果刷新频繁,可以使用整体替换 Map 的方式。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference;
public class SafePermissionFlyweightFactory {
private final AtomicReference<Map<String, PermissionFlyweight>> cacheRef = new AtomicReference<>(Map.of());
public PermissionFlyweight getPermission(String code) { PermissionFlyweight permission = cacheRef.get().get(code);
if (permission == null) { throw new IllegalArgumentException("Permission not found, code = " + code); }
return permission; }
public void refresh(Map<String, PermissionFlyweight> newCache) { cacheRef.set(Map.copyOf(newCache)); } }
|
这种方式适合读多写少的场景。
刷新时直接替换整个不可变 Map。
读线程永远读到一份完整快照。
chapter 21:享元模式和缓存的区别
享元模式和缓存很像,但不是一回事。
| 对比项 |
享元模式 |
缓存 |
| 目的 |
共享对象,减少内存占用 |
提高访问速度,减少重复计算或 IO |
| 关注点 |
对象复用 |
数据读取性能 |
| 对象状态 |
通常强调内部/外部状态分离 |
不一定 |
| 是否要求对象可共享 |
是 |
不一定 |
| 示例 |
权限定义共享、字符对象共享 |
Redis 缓存用户信息、Caffeine 缓存查询结果 |
缓存可能缓存任何数据。
享元强调的是:
多个上下文共享同一个细粒度对象。
缓存可以用来实现享元工厂,但缓存本身不等于享元模式。
chapter 22:享元模式和对象池的区别
对象池也会复用对象,比如连接池、线程池。
但对象池和享元模式不同。
| 对比项 |
享元模式 |
对象池 |
| 复用方式 |
多个客户端同时共享一个不可变对象 |
客户端借用对象,用完归还 |
| 对象状态 |
通常不可变 |
通常可变或有资源状态 |
| 目的 |
减少大量重复对象 |
避免频繁创建昂贵资源 |
| 示例 |
String 常量池、权限元数据 |
数据库连接池、线程池 |
| 是否同时共享 |
可以同时共享 |
通常不能同时被多个客户端使用 |
数据库连接池不是享元模式。
因为连接对象是有状态资源,通常一个连接同一时刻只能被一个线程使用,用完要归还。
享元对象一般是无状态或不可变状态,可以被多个地方同时共享。
chapter 23:享元模式和单例模式的区别
单例模式保证一个类只有一个实例。
享元模式是缓存多个可共享对象。
| 对比项 |
单例模式 |
享元模式 |
| 对象数量 |
一个 |
多个 |
| 管理方式 |
类自己或容器管理唯一实例 |
工厂按 key 管理多个共享实例 |
| 目的 |
控制实例唯一性 |
共享大量细粒度对象 |
| 典型接口 |
getInstance() |
getFlyweight(key) |
| 示例 |
全局配置管理器 |
按权限 code 共享权限定义 |
享元工厂本身可以是单例。
但享元模式不是单例模式。
例如:
1
| PermissionFlyweightFactory
|
可以是 Spring 单例 Bean。
但它内部管理很多个:
chapter 24:享元模式和原型模式的区别
原型模式通过复制对象创建新对象。
享元模式通过共享对象减少对象数量。
| 对比项 |
原型模式 |
享元模式 |
| 核心动作 |
copy |
share |
| 目的 |
快速创建相似对象 |
减少重复对象 |
| 对象关系 |
新对象和原型通常是不同实例 |
多个使用方共享同一实例 |
| 状态处理 |
复制状态 |
区分内部状态和外部状态 |
| 示例 |
复制报表模板 |
共享权限定义 |
一句话区分:
原型模式是复制已有对象。
享元模式是共享已有对象。
chapter 25:享元模式和静态常量的区别
有些场景可以直接用静态常量。
例如:
1 2
| public static final PermissionFlyweight USER_ADD = new BasicPermissionFlyweight("user:add", "新增用户", PermissionType.BUTTON, "用户管理");
|
如果对象数量很少、固定不变,这样也可以。
但如果对象是动态加载的,比如从数据库读取权限定义,那么享元工厂更合适。
| 对比项 |
静态常量 |
享元工厂 |
| 对象来源 |
编码固定 |
可动态加载 |
| 扩展性 |
较差 |
较好 |
| 管理方式 |
分散在类中 |
集中管理 |
| 适合场景 |
少量固定值 |
大量可共享对象 |
chapter 26:享元模式的优点
1. 减少对象数量
共享相同对象,避免重复创建。
2. 降低内存占用
当对象数量巨大、重复状态很多时,节省非常明显。
3. 减少 GC 压力
对象少了,垃圾回收压力也会降低。
4. 统一管理共享对象
通过享元工厂集中创建和复用对象。
5. 适合细粒度对象复用
例如字符、样式、权限定义、规则模板、状态定义等。
chapter 27:享元模式的缺点
1. 增加系统复杂度
需要区分内部状态和外部状态。
如果分不清,代码会更绕。
2. 外部状态需要额外传递
享元对象不保存外部状态,因此客户端需要传入上下文。
3. 共享对象如果可变,会很危险
一个地方修改共享对象,所有地方都会受影响。
4. 调试成本可能增加
对象被共享后,排查问题时需要知道哪些地方使用了同一个对象。
5. 不适合对象差异很大的场景
如果对象之间没有多少重复状态,享元模式价值不大。
chapter 28:适用场景
享元模式适合以下场景。
1. 系统中存在大量相似对象
对象数量非常多,创建和持有成本明显。
2. 对象有大量可共享状态
例如权限定义、样式定义、规则模板。
3. 外部状态可以被剥离
例如用户 ID、租户 ID、坐标、授权时间可以单独传入。
4. 共享对象最好不可变
内部状态稳定,不会频繁修改。
5. 读多写少
享元对象通常适合频繁读取,不适合频繁修改。
chapter 29:不适合使用的场景
以下场景不建议使用享元模式。
1. 对象数量不多
如果系统只有几十个对象,没必要为了省这点内存增加复杂度。
2. 对象之间差异很大
没有可共享状态,就没有享元价值。
3. 对象包含大量可变状态
可变对象共享风险很高。
4. 外部状态难以剥离
如果对象状态高度绑定上下文,就不适合共享。
5. 共享对象生命周期复杂
如果共享对象频繁变更、频繁失效、频繁刷新,管理成本可能超过收益。
chapter 30:真实项目中的实践建议
1. 先确认对象数量真的大
不要看到重复对象就立刻上享元。
先问自己:
这个对象数量会不会真的非常多?
如果只有几百个对象,普通对象也没什么问题。
2. 享元对象尽量不可变
推荐:
1 2
| private final String code; private final String name;
|
不推荐:
3. 工厂用线程安全容器
例如:
或者读多写少时使用不可变 Map + 原子替换。
4. 明确内部状态和外部状态边界
这是享元模式成败的关键。
例如:
1 2
| 权限名称、权限类型、模块名称 = 内部状态 用户 ID、租户 ID、授权时间 = 外部状态
|
5. 不要把享元工厂写成万能缓存
享元工厂只负责共享对象管理。
不要把数据库查询、业务判断、权限决策全塞进去。
6. 注意缓存刷新策略
如果共享对象来自数据库,要考虑:
- 启动加载;
- 定时刷新;
- 手动刷新;
- 变更事件刷新;
- 多实例一致性。
7. 大对象共享前要确认线程安全
如果共享对象里有集合,最好使用不可变集合:
1 2 3
| List.copyOf(list) Map.copyOf(map) Set.copyOf(set)
|
8. 分布式系统中本地享元只在当前 JVM 生效
每个服务实例都有自己的本地享元工厂。
如果权限定义在多个实例间变化,需要配合:
- 配置中心;
- MQ 广播;
- Redis 发布订阅;
- 手动刷新接口;
- 服务重启加载。
本地享元不是全局唯一,不要把它想成分布式宇宙共享对象。
chapter 31:完整案例代码汇总
权限类型
1 2 3 4 5 6 7 8
| public enum PermissionType {
MENU,
BUTTON,
API }
|
抽象享元
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public interface PermissionFlyweight {
String code();
String name();
PermissionType type();
String module();
boolean match(String permissionCode);
void print(UserPermissionContext context); }
|
具体享元
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| public class BasicPermissionFlyweight implements PermissionFlyweight {
private final String code;
private final String name;
private final PermissionType type;
private final String module;
public BasicPermissionFlyweight(String code, String name, PermissionType type, String module) { if (code == null || code.isBlank()) { throw new IllegalArgumentException("code can not be blank"); }
if (name == null || name.isBlank()) { throw new IllegalArgumentException("name can not be blank"); }
if (type == null) { throw new IllegalArgumentException("type can not be null"); }
if (module == null || module.isBlank()) { throw new IllegalArgumentException("module can not be blank"); }
this.code = code; this.name = name; this.type = type; this.module = module; }
@Override public String code() { return code; }
@Override public String name() { return name; }
@Override public PermissionType type() { return type; }
@Override public String module() { return module; }
@Override public boolean match(String permissionCode) { return this.code.equals(permissionCode); }
@Override public void print(UserPermissionContext context) { System.out.println("用户 " + context.userId() + " 在租户 " + context.tenantId() + " 拥有权限:" + name + " [" + code + "]" + ",模块:" + module + ",类型:" + type); } }
|
外部状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import java.time.LocalDateTime;
public class UserPermissionContext {
private final Long userId;
private final Long tenantId;
private final String permissionCode;
private final boolean enabled;
private final LocalDateTime grantedAt;
public UserPermissionContext(Long userId, Long tenantId, String permissionCode, boolean enabled, LocalDateTime grantedAt) { this.userId = userId; this.tenantId = tenantId; this.permissionCode = permissionCode; this.enabled = enabled; this.grantedAt = grantedAt; }
public Long userId() { return userId; }
public Long tenantId() { return tenantId; }
public String permissionCode() { return permissionCode; }
public boolean enabled() { return enabled; }
public LocalDateTime grantedAt() { return grantedAt; } }
|
享元工厂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
public class PermissionFlyweightFactory {
private final Map<String, PermissionFlyweight> cache = new ConcurrentHashMap<>();
public PermissionFlyweight getPermission(String code, String name, PermissionType type, String module) { return cache.computeIfAbsent( code, key -> new BasicPermissionFlyweight(code, name, type, module) ); }
public PermissionFlyweight getPermission(String code) { PermissionFlyweight permission = cache.get(code);
if (permission == null) { throw new IllegalArgumentException("Permission not found, code = " + code); }
return permission; }
public int size() { return cache.size(); } }
|
初始化权限定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class PermissionBootstrap {
public static void init(PermissionFlyweightFactory factory) { factory.getPermission("user:add", "新增用户", PermissionType.BUTTON, "用户管理"); factory.getPermission("user:update", "修改用户", PermissionType.BUTTON, "用户管理"); factory.getPermission("user:delete", "删除用户", PermissionType.BUTTON, "用户管理");
factory.getPermission("role:add", "新增角色", PermissionType.BUTTON, "角色管理"); factory.getPermission("role:assign", "分配权限", PermissionType.BUTTON, "角色管理");
factory.getPermission("order:view", "查看订单", PermissionType.API, "订单管理"); factory.getPermission("order:export", "导出订单", PermissionType.BUTTON, "订单管理"); } }
|
权限检查器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import java.util.List;
public class PermissionChecker {
private final PermissionFlyweightFactory factory;
public PermissionChecker(PermissionFlyweightFactory factory) { this.factory = factory; }
public boolean hasPermission(List<UserPermissionContext> contexts, String requiredPermissionCode) { for (UserPermissionContext context : contexts) { if (!context.enabled()) { continue; }
PermissionFlyweight permission = factory.getPermission(context.permissionCode());
if (permission.match(requiredPermissionCode)) { return true; } }
return false; }
public void printPermissions(List<UserPermissionContext> contexts) { for (UserPermissionContext context : contexts) { PermissionFlyweight permission = factory.getPermission(context.permissionCode());
permission.print(context); } } }
|
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import java.time.LocalDateTime; import java.util.List;
public class FlyweightDemo {
public static void main(String[] args) { PermissionFlyweightFactory factory = new PermissionFlyweightFactory();
PermissionBootstrap.init(factory);
List<UserPermissionContext> permissions = List.of( new UserPermissionContext(1L, 100L, "user:add", true, LocalDateTime.now()), new UserPermissionContext(1L, 100L, "user:update", true, LocalDateTime.now()), new UserPermissionContext(1L, 100L, "order:view", true, LocalDateTime.now()),
new UserPermissionContext(2L, 100L, "user:add", true, LocalDateTime.now()), new UserPermissionContext(2L, 100L, "role:assign", false, LocalDateTime.now()) );
PermissionChecker checker = new PermissionChecker(factory);
checker.printPermissions(permissions);
boolean hasUserAdd = checker.hasPermission(permissions, "user:add");
boolean hasRoleAssign = checker.hasPermission(permissions, "role:assign");
System.out.println("是否拥有 user:add 权限:" + hasUserAdd); System.out.println("是否拥有 role:assign 权限:" + hasRoleAssign); System.out.println("享元对象数量:" + factory.size()); System.out.println("授权关系数量:" + permissions.size()); } }
|
chapter 32:一句话总结
享元模式的本质是:
把大量对象中可共享、不可变的内部状态抽出来复用,把不可共享的外部状态交给客户端传入,从而减少对象数量和内存开销。
它适合对象数量巨大、重复状态明显、共享对象稳定的场景。
在 Java 后端开发中,享元模式常见于:
- 权限资源定义;
- 字典项定义;
- 状态定义;
- 规则模板;
- 报表样式;
- 消息模板;
- 地区编码;
- 固定元数据对象。
享元模式最难的不是写 Map 缓存,而是判断:
哪些状态可以共享,哪些状态必须留在外部。
如果这个边界判断错了,享元模式就会从“省内存神器”变成“共享状态事故制造机”。
好的享元设计像公共图书馆:书共享,借阅记录归个人。
坏的享元设计像共用一张身份证:省是省了,但人也乱了。
参考资料
- Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design Patterns: Elements of Reusable Object-Oriented Software.
- Robert C. Martin. Agile Software Development, Principles, Patterns, and Practices.
- Joshua Bloch. Effective Java.
- Brian Goetz. Java Concurrency in Practice.
- Oracle Java Documentation: String and Wrapper Classes.
- Spring Framework Documentation: Core Technologies - The IoC Container.
- Refactoring Guru: Flyweight Pattern.
- SourceMaking: Flyweight Design Pattern.
启示录
富贵岂由人,时会高志须酬。
能成功于千载者,必以近察远。