前言
笔者梦开始的地方,在笔者开发的过程中,为了减少项目的耦合程度,曾考虑引入SpringBoot,但由于Spring框架太过重了,而市面上的其他框架并不能满足笔者的需求(它们很多都是面向Web编程),故笔者考虑自己设计IoC/Aop以满足需要,并进行深度的定制化
本系列笔记共四章,大量参考Spring文档和CSDN上的各种Spring框架介绍,因为太杂故不一一引用,再次集体感谢这些知识的分享者们!
注意:我们在这一个系列地介绍中只关心设计,笔者不会给出实际的实现,敬请谅解!
第一部分:概念篇 - 理解控制反转与依赖注入
第1章:从一个简单的问题开始
1.1 传统编程中的依赖问题
让我们从一个简单的场景开始:假设你正在开发一个游戏服务器,需要实现一个玩家数据管理系统。
最直观的写法可能是这样的:
kotlin
// 数据库连接类
class MySQLDatabase {
fun query(sql: String): List<Map<String, Any>> {
println("执行 MySQL 查询: $sql")
// 实际的数据库查询逻辑...
return listOf()
}
fun execute(sql: String) {
println("执行 MySQL 命令: $sql")
}
}
// 玩家数据仓库
class PlayerRepository {
// 直接在类内部创建依赖对象
private val database = MySQLDatabase()
fun findByUUID(uuid: String): PlayerData? {
val results = database.query("SELECT * FROM players WHERE uuid = '$uuid'")
return results.firstOrNull()?.let { parsePlayerData(it) }
}
fun save(player: PlayerData) {
database.execute("INSERT INTO players ...")
}
private fun parsePlayerData(map: Map<String, Any>): PlayerData {
// 解析逻辑...
return PlayerData(map["uuid"] as String, map["name"] as String)
}
}
// 玩家服务类
class PlayerService {
// 直接在类内部创建依赖对象
private val repository = PlayerRepository()
fun getPlayer(uuid: String): PlayerData? {
return repository.findByUUID(uuid)
}
fun createPlayer(uuid: String, name: String): PlayerData {
val player = PlayerData(uuid, name)
repository.save(player)
return player
}
}
data class PlayerData(val uuid: String, val name: String)
这段代码看起来很正常,对吧?但是,让我们思考几个问题:
问题1:如果我想换一个数据库怎么办?
假设有一天,你的老板说:"MySQL 太慢了,我们换 Redis 缓存吧!"
你需要做什么?
- 修改
PlayerRepository类,把MySQLDatabase换成RedisDatabase - 如果还有
ItemRepository、GuildRepository等其他仓库类,每个都要改 - 如果某些地方需要 MySQL,某些地方需要 Redis,代码会变得更复杂
问题2:如何为这段写单元测试?
kotlin
class PlayerServiceTest {
fun testGetPlayer() {
val service = PlayerService()
// 问题:这会真的去连接数据库!
// 我只想测试 PlayerService 的逻辑,不想测试数据库
val player = service.getPlayer("test-uuid")
// ...
}
}
当你创建 PlayerService 时,它会自动创建 PlayerRepository,而 PlayerRepository 又会自动创建 MySQLDatabase。你无法阻止这个连锁反应,也无法用一个"假的"数据库来替代真实数据库进行Mock测试。
问题3:依赖关系不清晰
看这张依赖图:
┌─────────────────┐
│ PlayerService │
└────────┬────────┘
│ 内部创建
▼
┌─────────────────┐
│PlayerRepository │
└────────┬────────┘
│ 内部创建
▼
┌─────────────────┐
│ MySQLDatabase │
└─────────────────┘
每个类都在内部"偷偷"创建自己的依赖。从外部看 PlayerService,你根本不知道它依赖了什么。这种隐藏的依赖关系会让代码难以理解和维护。
这就是我们所说的 紧耦合(Tight Coupling) 问题,也就是我们说:代码太“亲密了”。
1.2 什么是控制反转(IoC)
"控制"指的是什么?
在上面的例子中,PlayerService 控制着 PlayerRepository 的创建:
kotlin
class PlayerService {
// PlayerService 控制着 repository 的创建
private val repository = PlayerRepository()
}
PlayerService 决定了:
- 何时创建
PlayerRepository(在自己被创建时) - 如何创建
PlayerRepository(直接 new) - 创建哪个具体的实现(写死
PlayerRepository)
这就是传统编程中的"控制"——对象自己控制其依赖的创建。
"反转"反转了什么?
控制反转(Inversion of Control,IoC) 的核心思想是:
不要让对象自己创建依赖,而是由外部来提供依赖。
反转后的代码:
kotlin
class PlayerService(
// 依赖从外部传入,而不是内部创建
private val repository: PlayerRepository
) {
fun getPlayer(uuid: String): PlayerData? {
return repository.findByUUID(uuid)
}
}
现在,PlayerService 不再控制 PlayerRepository 的创建了。它只是声明自己需要一个 PlayerRepository,至于这个对象从哪来、怎么创建,它不关心。PlayerRepository是一个接口,它的实现可以是MySQL、PGSql、Sqlite、Redis等等等等,Who cares?
控制权从 PlayerService 转移到了外部——这就是"反转"的含义。
好莱坞原则
IoC 有一个著名的别名叫 好莱坞原则(Hollywood Principle):
"Don't call us, we'll call you."
(不要打电话给我们,我们会打电话给你。)
这句话来自好莱坞的试镜场景:演员去试镜后,不应该主动打电话问结果,而是等待导演来通知。
在编程中:
- 传统方式:对象主动去获取(创建)自己需要的依赖
- IoC 方式:对象被动等待,依赖会被"注入"进来
传统方式: IoC 方式:
┌──────────────┐ ┌──────────────┐
│PlayerService │ │PlayerService │
│ │ │ │
│ 我要创建 │ │ 我需要一个 │
│ Repository! │ │ Repository │
│ │ │ │ ▲ │
│ ▼ │ │ │ │
│ new Repo() │ │ (等待注入) │
└──────────────┘ └──────────────┘
│
┌──────┴──────┐
│ IoC 容器 │
│ 给你一个 │
│ Repository │
└─────────────┘
1.3 什么是依赖注入(DI)
依赖注入(Dependency Injection,DI) 是实现 IoC 的一种具体方式。
简单来说:DI 就是把依赖"注入"到对象中,而不是让对象自己创建依赖。
有三种常见的注入方式:
1. 构造函数注入(Constructor Injection)
通过构造函数传入依赖:
kotlin
class PlayerService(
private val repository: PlayerRepository // 通过构造函数注入
) {
fun getPlayer(uuid: String) = repository.findByUUID(uuid)
}
// 使用时
val repository = PlayerRepository(database)
val service = PlayerService(repository) // 注入依赖
当然上面写的是立即注入,在实际的开发中repository的声明和service的声明并不在一个地方,甚至你可以不需要自己声明,IoC容器会自动帮你实例化Repository和Service,而你只需要关心内部实现即可
优点:
- 依赖关系清晰,一眼就能看出类需要什么
- 可以保证依赖不为 null(Kotlin 的非空类型)
- 便于单元测试
是的,距离才能产生美!
2. Setter 注入(Setter Injection)
通过 setter 方法传入依赖:
kotlin
class PlayerService {
private lateinit var repository: PlayerRepository
// 通过 setter 注入
fun setRepository(repository: PlayerRepository) {
this.repository = repository
}
fun getPlayer(uuid: String) = repository.findByUUID(uuid)
}
// 使用时
val service = PlayerService()
service.setRepository(repository) // 注入依赖,这句在实际的开发中也不会用到,IoC容器会自动帮你干
优点:
- 可以在对象创建后再注入依赖
- 支持可选依赖
缺点:
- 对象可能处于"未完全初始化"状态
- 依赖关系不够清晰
3. 字段注入(Field Injection)
直接设置字段值(通常通过反射):
kotlin
class PlayerService {
@Autowired // 标记需要注入的字段,之后IoC容器会通过反射设置此字段的值
private lateinit var repository: PlayerRepository
fun getPlayer(uuid: String) = repository.findByUUID(uuid)
}
优点:
- 代码简洁,不需要写构造器或 setter
缺点:
- 依赖关系隐藏在类内部
- 必须使用反射,有一定性能开销
- 不利于单元测试(需要使用反射或特殊框架来注入 mock 对象)
代码对比:手动注入 vs 自动注入
手动注入(没有 IoC 容器):
kotlin
fun main() {
// 手动创建所有对象,手动组装依赖关系
val database = MySQLDatabase()
val repository = PlayerRepository(database)
val service = PlayerService(repository)
// 如果有更多的类,这会变得非常繁琐...
val itemDatabase = MySQLDatabase()
val itemRepository = ItemRepository(itemDatabase)
val itemService = ItemService(itemRepository)
val guildDatabase = MySQLDatabase()
val guildRepository = GuildRepository(guildDatabase)
val guildService = GuildService(guildRepository, service) // 还依赖 PlayerService
// 想象一下有 50 个服务类,逼疯你哦!...
}
自动注入(使用 IoC 容器):
kotlin
// 只需要标记注解,IoC 容器会自动处理一切
@Service
class MySQLDatabase { ... }
@Service
class PlayerRepository(
@Autowired private val database: MySQLDatabase
) { ... }
@Service
class PlayerService(
@Autowired private val repository: PlayerRepository
) { ... }
// 使用时,直接从容器获取
fun main() {
val service = IoCContainer.getBean(PlayerService::class.java)
// IoC 容器自动创建了 Database、Repository,并注入到 Service 中
}
1.4 为什么需要 IoC 容器
你可能会问:我手动注入不也可以吗?为什么需要一个"容器"?
原因1:手动管理依赖太复杂
当项目变大时,依赖关系会变成一张复杂的网:
┌─────────────┐
│ConfigService│
└──────┬──────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ PlayerService │ │ ItemService │ │ GuildService │
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│PlayerRepository│ │ItemRepository │ │GuildRepository│
└───────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ │ │
└──────────────────┼──────────────────┘
│
▼
┌─────────────┐
│ Database │
└─────────────┘
手动管理这些依赖关系,你需要:
- 搞清楚创建顺序(先创建 Database,再创建 Repository,最后创建 Service)
- 确保每个依赖都被正确传递
- 处理循环依赖等特殊情况
- 就你开发的时候,越往后依赖关系越复杂,史山代码就是这样形成的
而IoC 容器可以自动分析依赖关系,按正确的顺序创建对象。
原因2:对象生命周期管理
不同的对象可能有不同的生命周期需求:
- 单例(Singleton):整个应用只有一个实例(如数据库连接池)
- 原型(Prototype):每次请求都创建新实例(如请求处理器)
- 作用域(Scoped):在特定范围内共享实例(如一次请求内)
kotlin
@Service
@Singleton // 标记为单例
class DatabasePool { ... }
@Service
@Prototype // 标记为原型,每次获取都是新实例
class RequestHandler { ... }
IoC 容器可以根据配置,自动管理对象的创建和销毁。
原因3:便于测试和扩展
使用 IoC 容器后,替换实现变得非常简单:
kotlin
// 生产环境使用真实数据库
@Service
@Profile("production")
class MySQLDatabase : Database { ... }
// 测试环境使用内存数据库
@Service
@Profile("test")
class InMemoryDatabase : Database { ... }
// PlayerRepository 不需要任何修改
@Service
class PlayerRepository(
private val database: Database // 依赖接口,而不是具体实现
) { ... }
IoC 容器会根据当前环境,自动注入正确的实现。
原因4:支持 AOP 等高级特性
IoC 容器是 AOP(面向切面编程)的基础。因为容器控制着对象的创建,它可以在创建时"偷偷"包装一层代理,实现日志、事务、缓存等横切功能。
kotlin
@Service
class PlayerService(private val repository: PlayerRepository) {
@Cacheable("players") // 自动缓存
fun getPlayer(uuid: String) = repository.findByUUID(uuid)
@Transactional // 自动事务管理
fun transferMoney(from: String, to: String, amount: Int) { ... }
}
这些"魔法"都是 IoC 容器在背后实现的,我们将在下一章详细讲解。
第2章:什么是面向切面编程(AOP)
2.1 横切关注点问题
让我们继续扩展我们的游戏服务器。现在,你老板提出了新需求:
- 日志:记录每个方法的调用,方便排查问题
- 性能监控:统计每个方法的执行时间
- 权限检查:某些操作需要检查玩家权限
你可能会这样写:
kotlin
@Service
class PlayerService(private val repository: PlayerRepository) {
private val logger = LogManager.getLogger()
fun getPlayer(uuid: String): PlayerData? {
// 日志
logger.info("开始获取玩家数据: $uuid")
val startTime = System.currentTimeMillis()
try {
// 实际业务逻辑
val player = repository.findByUUID(uuid)
// 日志 + 性能监控
val duration = System.currentTimeMillis() - startTime
logger.info("获取玩家数据完成: $uuid, 耗时: ${duration}ms")
return player
} catch (e: Exception) {
// 异常日志
logger.error("获取玩家数据失败: $uuid", e)
throw e
}
}
fun deletePlayer(uuid: String) {
// 权限检查
if (!PermissionManager.hasPermission(currentUser, "player.delete")) {
throw PermissionDeniedException("没有删除玩家的权限")
}
// 日志
logger.info("开始删除玩家: $uuid")
val startTime = System.currentTimeMillis()
try {
// 实际业务逻辑
repository.delete(uuid)
// 日志 + 性能监控
val duration = System.currentTimeMillis() - startTime
logger.info("删除玩家完成: $uuid, 耗时: ${duration}ms")
} catch (e: Exception) {
logger.error("删除玩家失败: $uuid", e)
throw e
}
}
// 还有更多方法,每个都要写这些重复代码...
}
现在看看 ItemService:
kotlin
@Service
class ItemService(private val repository: ItemRepository) {
private val logger = LogManager.getLogger()
fun getItem(id: String): Item? {
// 又是一样的日志代码...
logger.info("开始获取物品: $id")
val startTime = System.currentTimeMillis()
try {
val item = repository.findById(id)
val duration = System.currentTimeMillis() - startTime
logger.info("获取物品完成: $id, 耗时: ${duration}ms")
return item
} catch (e: Exception) {
logger.error("获取物品失败: $id", e)
throw e
}
}
fun deleteItem(id: String) {
// 又是一样的权限检查...
if (!PermissionManager.hasPermission(currentUser, "item.delete")) {
throw PermissionDeniedException("没有删除物品的权限")
}
// 又是一样的日志代码...
logger.info("开始删除物品: $id")
// ...
}
}
发现问题了吗?
日志、性能监控、权限检查这些代码,散落在每个服务类的每个方法中!
现在假设你测试完了,想把这些日志删掉,吼吼!删吧,一删一个不吱声
这就是所谓的 横切关注点(Cross-Cutting Concerns):
日志 性能监控 权限检查
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────┐
│ PlayerService │
│ ┌─────────────────────────────────┐ │
│ │ getPlayer() ✗ ✗ │ │
│ │ deletePlayer() ✗ ✗ ✗ │ │
│ │ updatePlayer() ✗ ✗ ✗ │ │
│ └─────────────────────────────────┘ │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ ItemService │
│ ┌─────────────────────────────────┐ │
│ │ getItem() ✗ ✗ │ │
│ │ deleteItem() ✗ ✗ ✗ │ │
│ │ updateItem() ✗ ✗ ✗ │ │
│ └─────────────────────────────────┘ │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ GuildService │
│ ┌─────────────────────────────────┐ │
│ │ getGuild() ✗ ✗ │ │
│ │ deleteGuild() ✗ ✗ ✗ │ │
│ │ createGuild() ✗ ✗ ✗ │ │
│ └─────────────────────────────────┘ │
└──────────────────────────────────────┘
✗ = 需要添加相同的代码
这种情况带来的问题:
- 代码重复:相同的日志/监控代码复制粘贴到处都是
- 难以维护:想修改日志格式?需要改几十个地方
- 职责混乱:业务代码和非业务代码混在一起
- 容易遗漏:新加一个方法,可能忘记加日志
这就是著名的 "霰弹式修改(Shotgun Surgery)" 反模式。
2.2 AOP 核心概念
面向切面编程(Aspect-Oriented Programming,AOP) 就是为了解决横切关注点问题而诞生的。
AOP 的核心思想是:
把横切关注点从业务代码中分离出来,集中到一个地方管理。
让我们先了解 AOP 的几个核心概念:
1. 切面(Aspect)
切面是横切关注点的模块化。简单说,就是把日志、权限检查等功能封装成一个独立的模块。
kotlin
@Aspect // 标记这是一个切面
object LoggingAspect {
// 这个切面负责所有的日志功能
}
@Aspect
object PermissionAspect {
// 这个切面负责所有的权限检查
}
2. 连接点(JoinPoint)
连接点是程序执行过程中的某个点,比如:
- 方法调用
- 方法执行
- 异常抛出
- 字段访问
在我们的实现中,连接点主要是方法执行。
kotlin
class PlayerService {
fun getPlayer(uuid: String): PlayerData? { // ← 这是一个连接点
return repository.findByUUID(uuid)
}
fun deletePlayer(uuid: String) { // ← 这也是一个连接点
repository.delete(uuid)
}
}
3. 切入点(Pointcut)
切入点是用来匹配连接点的表达式。它定义了"在哪些地方"应用切面逻辑。
kotlin
// 传统方式(Spring):使用字符串表达式(不推荐,混淆后会失效)
@Around("execution(* com.example.service.*.*(..))")
// 我们现在的设计方式:使用注解匹配(混淆安全)
@Around(Loggable::class) // 匹配所有标记了 @Loggable 的方法
4. 通知(Advice)
通知是在切入点执行的具体动作。有五种类型:
| 通知类型 | 说明 | 执行时机 |
|---|---|---|
@Before |
前置通知 | 目标方法执行前 |
@After |
后置通知 | 目标方法执行后(无论是否异常) |
@AfterReturning |
返回通知 | 目标方法正常返回后 |
@AfterThrowing |
异常通知 | 目标方法抛出异常后 |
@Around |
环绕通知 | 包裹目标方法,可以控制是否执行 |
kotlin
@Aspect
object LoggingAspect {
@Before(Loggable::class)
fun logBefore(joinPoint: JoinPoint) {
println("方法开始: ${joinPoint.method.name}")
}
@AfterReturning(Loggable::class)
fun logAfterReturning(joinPoint: JoinPoint, result: Any?) {
println("方法返回: ${joinPoint.method.name}, 结果: $result")
}
@AfterThrowing(Loggable::class)
fun logAfterThrowing(joinPoint: JoinPoint, exception: Throwable) {
println("方法异常: ${joinPoint.method.name}, 异常: $exception")
}
}
5. 织入(Weaving)
织入是将切面应用到目标对象的过程。织入后,调用目标方法时会自动执行切面逻辑。
织入前: 织入后:
┌─────────────────┐ ┌─────────────────┐
│ PlayerService │ │ 代理对象 │
│ │ │ ┌───────────┐ │
│ getPlayer() ───┼───────────▶ │ │ 日志切面 │ │
│ │ │ └─────┬─────┘ │
└─────────────────┘ │ ▼ │
│ ┌───────────┐ │
│ │ 权限切面 │ │
│ └─────┬─────┘ │
│ ▼ │
│ ┌───────────┐ │
│ │getPlayer()│ │
│ └───────────┘ │
└─────────────────┘
2.3 AOP 的实现方式
AOP 有三种主要的实现方式:
1. 编译时织入(Compile-Time Weaving)
在编译阶段,直接修改字节码,把切面逻辑编译进目标类。
代表:AspectJ、Google Dagger2
java
// AspectJ 会在编译时通过ASM魔法修改 PlayerService.class
// 把日志代码直接插入到 getPlayer() 方法中
优点:性能最好,运行时没有额外开销
缺点:需要特殊的编译器,配置复杂
2. 类加载时织入(Load-Time Weaving)
在类加载到 JVM 时,通过 Java Agent 修改字节码。
代表:AspectJ LTW、Spring Instrumentation
┌─────────────┐ 加载类 ┌─────────────┐ 修改字节码 ┌─────────────┐
│ .class 文件 │ ──────────▶ │ Java Agent │ ──────────────▶ │ JVM │
└─────────────┘ └─────────────┘ └─────────────┘
优点:不需要特殊编译器
缺点:需要配置 Java Agent,启动参数复杂
3. 运行时织入(Runtime Weaving)
在运行时,通过动态代理创建目标对象的代理类。
代表:Spring AOP、Google Guice、我们以后的实现
kotlin
// 原始对象
val playerService = PlayerService(repository)
// 创建代理对象
val proxy = AopProxyFactory.createProxy(playerService, interceptors)
// 调用代理对象的方法时,会自动执行切面逻辑
proxy.getPlayer("uuid")
优点:
- 不需要特殊编译器或 Agent
- 配置简单,易于理解
- 可以在运行时动态添加/移除切面
缺点:
- 有一定的性能开销(创建代理对象、方法调用转发)、但是在优化后在我们的场景基本上可以忽略
- 只能拦截 public 方法,但是通过JVM魔法可以绕过限制
我们选择运行时织入,因为它最灵活、最易于实现和理解。
2.4 AOP 的实战:重构我们的“史山”代码
现在,让我们用 AOP 重构之前那段充满重复代码的服务类。
第一步:定义注解
kotlin
// 标记需要记录日志的方法
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Loggable
// 标记需要性能监控的方法
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Monitored
// 标记需要权限检查的方法
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RequirePermission(val value: String)
第二步:定义切面
kotlin
@Aspect(order = 1) // order 越小越先执行
object LoggingAspect {
private val logger = LogManager.getLogger()
@Before(Loggable::class)
fun logBefore(joinPoint: JoinPoint) {
val methodName = joinPoint.method.name
val args = joinPoint.args.joinToString()
logger.info("方法开始: $methodName, 参数: [$args]")
}
@AfterReturning(Loggable::class, returning = "result")
fun logAfterReturning(joinPoint: JoinPoint, result: Any?) {
logger.info("方法返回: ${joinPoint.method.name}, 结果: $result")
}
@AfterThrowing(Loggable::class, throwing = "ex")
fun logAfterThrowing(joinPoint: JoinPoint, ex: Throwable) {
logger.error("方法异常: ${joinPoint.method.name}", ex)
}
}
@Aspect(order = 2)
object MonitoringAspect {
private val logger = LogManager.getLogger()
@Around(Monitored::class)
fun monitor(pjp: ProceedingJoinPoint): Any? {
val startTime = System.currentTimeMillis()
try {
return pjp.proceed() // 执行目标方法
} finally {
val duration = System.currentTimeMillis() - startTime
logger.info("方法 ${pjp.method.name} 执行耗时: ${duration}ms")
}
}
}
@Aspect(order = 0) // 权限检查最先执行
object PermissionAspect {
@Before(RequirePermission::class)
fun checkPermission(joinPoint: JoinPoint) {
val annotation = joinPoint.getAnnotation(RequirePermission::class.java)
val permission = annotation?.value ?: return
if (!PermissionManager.hasPermission(currentUser, permission)) {
throw PermissionDeniedException("没有权限: $permission")
}
}
}
第三步:重构业务代码
kotlin
@Service
class PlayerService(private val repository: PlayerRepository) {
@Loggable
@Monitored
fun getPlayer(uuid: String): PlayerData? {
// 只剩下纯粹的业务逻辑!
return repository.findByUUID(uuid)
}
@Loggable
@Monitored
@RequirePermission("player.delete")
fun deletePlayer(uuid: String) {
// 只剩下纯粹的业务逻辑!
repository.delete(uuid)
}
@Loggable
@Monitored
@RequirePermission("player.update")
fun updatePlayer(uuid: String, data: PlayerData) {
// 只剩下纯粹的业务逻辑!
repository.update(uuid, data)
}
}
@Service
class ItemService(private val repository: ItemRepository) {
@Loggable
@Monitored
fun getItem(id: String): Item? {
return repository.findById(id)
}
@Loggable
@Monitored
@RequirePermission("item.delete")
fun deleteItem(id: String) {
repository.delete(id)
}
}
对比效果
重构前:每个方法 20+ 行,大部分是重复的日志/监控代码
重构后:每个方法只有 1-2 行,纯粹的业务逻辑
重构前: 重构后:
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ fun getPlayer(uuid: String) │ │ @Loggable │
│ { │ │ @Monitored │
│ logger.info("开始...") │ │ fun getPlayer(uuid: String) │
│ val start = now() │ │ { │
│ try { │ ──▶ │ return repo.find(uuid) │
│ val p = repo.find(uuid) │ │ } │
│ logger.info("完成...") │ └─────────────────────────────┘
│ return p │
│ } catch (e) { │ 日志、监控逻辑集中在切面中:
│ logger.error("失败...") │
│ throw e │ ┌─────────────────────────────┐
│ } │ │ @Aspect │
│ } │ │ object LoggingAspect { ... }│
└─────────────────────────────┘ │ object MonitorAspect { ... }│
└─────────────────────────────┘
执行流程
当调用 playerService.deletePlayer("uuid") 时,实际执行流程如下:
调用 deletePlayer("uuid")
│
▼
┌─────────────────────────────────┐
│ 1. PermissionAspect (order=0) │
│ 检查 "player.delete" 权限 │
└─────────────┬───────────────────┘
│ 权限通过
▼
┌─────────────────────────────────┐
│ 2. LoggingAspect (order=1) │
│ @Before: 记录方法开始 │
└─────────────┬───────────────────┘
│
▼
┌─────────────────────────────────┐
│ 3. MonitoringAspect (order=2) │
│ @Around: 记录开始时间 │
└─────────────┬───────────────────┘
│
▼
┌─────────────────────────────────┐
│ 4. 执行目标方法 │
│ repository.delete(uuid) │
└─────────────┬───────────────────┘
│
▼
┌─────────────────────────────────┐
│ 5. MonitoringAspect │
│ @Around: 计算并记录耗时 │
└─────────────┬───────────────────┘
│
▼
┌─────────────────────────────────┐
│ 6. LoggingAspect │
│ @AfterReturning: 记录返回 │
└─────────────┬───────────────────┘
│
▼
返回结果
2.5 本章小结
在本章中,我们学习了:
-
横切关注点问题:日志、监控、权限等代码散落在各处,导致代码重复和维护困难
-
AOP 核心概念:
- 切面(Aspect):横切关注点的模块化
- 连接点(JoinPoint):程序执行的某个点
- 切入点(Pointcut):匹配连接点的表达式
- 通知(Advice):在切入点执行的动作
- 织入(Weaving):将切面应用到目标对象
-
AOP 实现方式:
- 编译时织入(AspectJ)
- 类加载时织入(Java Agent)
- 运行时织入(动态代理)—— 我们的选择
-
AOP 的好处:
- 消除重复代码
- 业务逻辑与横切关注点分离
- 集中管理,易于维护
在接下来的章节中,我们将开始设计和实现自己的 IoC 容器和 AOP 框架。
全部评论 (0)
暂无评论,快来抢沙发吧~