高性能IoC(控制反转)容器设计与Aop(面向切面编程)一:太“亲密”也不好,“距离”产生美(DI、IoC与Aop的概念)

前言

笔者梦开始的地方,在笔者开发的过程中,为了减少项目的耦合程度,曾考虑引入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 缓存吧!"

你需要做什么?

  1. 修改 PlayerRepository 类,把 MySQLDatabase 换成 RedisDatabase
  2. 如果还有 ItemRepositoryGuildRepository 等其他仓库类,每个都要改
  3. 如果某些地方需要 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  │
                    └─────────────┘

手动管理这些依赖关系,你需要:

  1. 搞清楚创建顺序(先创建 Database,再创建 Repository,最后创建 Service)
  2. 确保每个依赖都被正确传递
  3. 处理循环依赖等特殊情况
  4. 就你开发的时候,越往后依赖关系越复杂,史山代码就是这样形成的

而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 横切关注点问题

让我们继续扩展我们的游戏服务器。现在,你老板提出了新需求:

  1. 日志:记录每个方法的调用,方便排查问题
  2. 性能监控:统计每个方法的执行时间
  3. 权限检查:某些操作需要检查玩家权限

你可能会这样写:

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()  ✗     ✗     ✗   │ │
│  └─────────────────────────────────┘ │
└──────────────────────────────────────┘

✗ = 需要添加相同的代码

这种情况带来的问题:

  1. 代码重复:相同的日志/监控代码复制粘贴到处都是
  2. 难以维护:想修改日志格式?需要改几十个地方
  3. 职责混乱:业务代码和非业务代码混在一起
  4. 容易遗漏:新加一个方法,可能忘记加日志

这就是著名的 "霰弹式修改(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 本章小结

在本章中,我们学习了:

  1. 横切关注点问题:日志、监控、权限等代码散落在各处,导致代码重复和维护困难

  2. AOP 核心概念

    • 切面(Aspect):横切关注点的模块化
    • 连接点(JoinPoint):程序执行的某个点
    • 切入点(Pointcut):匹配连接点的表达式
    • 通知(Advice):在切入点执行的动作
    • 织入(Weaving):将切面应用到目标对象
  3. AOP 实现方式

    • 编译时织入(AspectJ)
    • 类加载时织入(Java Agent)
    • 运行时织入(动态代理)—— 我们的选择
  4. AOP 的好处

    • 消除重复代码
    • 业务逻辑与横切关注点分离
    • 集中管理,易于维护

在接下来的章节中,我们将开始设计和实现自己的 IoC 容器和 AOP 框架。

游客

全部评论 (0)

暂无评论,快来抢沙发吧~