设计模式:享元模式

欢迎你来读这篇博客,这篇博客主要是关于享元模式
其中包括享元模式的核心思想、适用场景、内部状态与外部状态、享元工厂、优缺点、与缓存/对象池/单例模式的区别,以及 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,属于结构型设计模式。

它的定义是:

运用共享技术有效地支持大量细粒度对象的复用。

这句话听起来有点抽象。

换成大白话:

如果系统中有大量对象,并且这些对象有很多重复的、不变的状态,就可以把这部分状态抽出来共享,避免重复创建。

享元模式通常包含几个角色:

  1. Flyweight 抽象享元:定义共享对象的公共接口。
  2. ConcreteFlyweight 具体享元:保存可共享的内部状态。
  3. UnsharedConcreteFlyweight 非共享享元:不适合共享的对象,某些场景可选。
  4. FlyweightFactory 享元工厂:负责创建和复用享元对象。
  5. 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:享元模式解决什么问题

享元模式主要解决的是:

大量重复对象造成的内存浪费问题。

它适合以下场景:

  1. 对象数量非常多;
  2. 对象之间存在大量重复状态;
  3. 重复状态可以被抽离出来共享;
  4. 共享对象最好是不可变的;
  5. 外部变化状态可以由客户端传入。

例如:

  • 字符串常量池;
  • 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, 用户管理)

这里真正变化的是:

1
userId

权限定义本身没有变化。

如果系统规模很大,这种重复对象会造成:

  • 内存浪费;
  • GC 压力增加;
  • 对象创建成本增加;
  • 缓存占用变大;
  • 序列化数据变大;
  • 数据结构不够清晰。

享元模式会把权限定义拆出去:

1
PermissionFlyweight permission = PermissionFlyweightFactory.get("user:add");

每个用户权限关系只保存:

1
userId + permissionCode + 外部状态

权限元数据由享元对象共享。

chapter 5:享元模式的核心思想

享元模式的核心思想可以总结为四步:

  1. 找出对象中可以共享的内部状态;
  2. 把内部状态封装成享元对象;
  3. 把不可共享的外部状态从享元对象中剥离;
  4. 使用享元工厂统一管理和复用享元对象。

例如:

1
2
权限定义 = 内部状态
用户授权关系 = 外部状态

享元对象:

1
PermissionFlyweight

外部上下文:

1
UserPermissionContext

享元工厂:

1
PermissionFlyweightFactory

客户端使用时:

1
2
3
PermissionFlyweight permission = factory.getPermission("user:add");

permission.check(context);

共享对象只保存稳定的权限定义。

用户、角色、租户等上下文信息由外部传入。

chapter 6:案例背景:权限资源元数据共享

下面用一个 Java 后端常见场景来讲享元模式:权限系统。

假设我们正在开发一个 RBAC 权限系统。

系统中有三类数据:

  1. 权限资源定义;
  2. 用户与权限关系;
  3. 角色与权限关系。

权限资源定义相对稳定,例如:

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, "订单管理");
}
}

真实项目中,这些权限定义可能来自数据库:

1
sys_permission

也可能来自配置文件、初始化脚本或权限中心。

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);

结果通常是:

1
true

因为字符串字面量 "hello" 会进入字符串常量池,多个引用可以共享同一个字符串对象。

这就是典型的共享思想。

2. Integer 缓存

1
2
3
4
Integer a = 100;
Integer b = 100;

System.out.println(a == b);

结果通常是:

1
true

因为 Java 对一定范围内的 Integer 做了缓存。

但是:

1
2
3
4
Integer x = 1000;
Integer y = 1000;

System.out.println(x == y);

结果通常是:

1
false

因为超过缓存范围后,可能会创建新对象。

3. Boolean 缓存

1
2
3
4
Boolean a = Boolean.TRUE;
Boolean b = Boolean.valueOf(true);

System.out.println(a == b);

通常是:

1
true

因为 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
order:refund

这时享元缓存需要刷新。

常见方式有几种。

1. 手动刷新

提供一个管理接口:

1
2
3
4
@PostMapping("/permissions/cache/refresh")
public void refreshPermissionCache() {
permissionCacheRefreshService.refresh();
}

2. 监听配置变更

如果权限定义来自配置中心,可以监听变更事件后刷新。

3. 数据变更后主动刷新

新增、修改、删除权限定义时,同步刷新工厂缓存。

4. 使用本地缓存组件

可以使用 Caffeine 之类本地缓存,并设置过期策略。

不过要注意:

享元工厂不一定等于缓存框架。

享元模式强调共享对象结构。

缓存框架强调数据访问优化。

两者可以结合,但不是一回事。

chapter 20:并发安全问题

享元对象通常会被多个线程共享,所以要考虑线程安全。

建议:

  1. 享元对象设计成不可变对象;
  2. 工厂内部使用线程安全 Map;
  3. 避免在享元对象中保存请求级状态;
  4. 缓存刷新时注意并发读写;
  5. 如果刷新频繁,可以使用整体替换 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。

但它内部管理很多个:

1
PermissionFlyweight

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;

不推荐:

1
2
setCode()
setName()

3. 工厂用线程安全容器

例如:

1
ConcurrentHashMap

或者读多写少时使用不可变 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.

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


设计模式:享元模式
https://allendericdalexander.github.io/2026/04/01/java/design/12flyweight-pattern-blog/
作者
AtLuoFu
发布于
2026年4月1日
许可协议