03-依赖注入与AOP

3 阅读5分钟

依赖注入与 AOP

1. 依赖注入(Dependency Injection)

1.1 什么是依赖注入?

定义:依赖注入是一种设计模式,将类所依赖的对象从外部注入,而不是在类内部创建。

1.2 为什么需要依赖注入?

传统方式的问题

<?php
class UserController
{
    private $userService;
    private $logService;
    private $cache;
    
    public function __construct()
    {
        // 在类内部创建依赖,高耦合
        $this->userService = new UserService();
        $this->logService = new LogService();
        $this->cache = new RedisCache();
    }
}

问题

  • ❌ 高耦合:Controller 直接依赖具体实现
  • ❌ 难以测试:无法注入 Mock 对象
  • ❌ 难以替换:切换实现需要修改代码
  • ❌ 依赖管理复杂:需要手动创建所有依赖

依赖注入方式

<?php
class UserController
{
    private $userService;
    private $logService;
    private $cache;
    
    // 从外部注入依赖
    public function __construct(
        UserService $userService,
        LogService $logService,
        CacheInterface $cache
    ) {
        $this->userService = $userService;
        $this->logService = $logService;
        $this->cache = $cache;
    }
}

优势

  • ✅ 低耦合:依赖接口而非具体实现
  • ✅ 易于测试:可以注入 Mock 对象
  • ✅ 易于替换:切换实现无需修改代码
  • ✅ 自动管理:容器自动创建和注入依赖

1.3 Hyperf 的依赖注入

Hyperf 提供了强大的 DI 容器,支持多种注入方式。

方式一:构造函数注入
<?php
namespace App\Controller;

use App\Service\UserService;

class UserController
{
    private $userService;
    
    // 自动注入
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }
    
    public function index()
    {
        return $this->userService->getList();
    }
}
方式二:属性注入(推荐)
<?php
namespace App\Controller;

use App\Service\UserService;
use Hyperf\Di\Annotation\Inject;

class UserController
{
    #[Inject]
    private UserService $userService;
    
    public function index()
    {
        return $this->userService->getList();
    }
}
方式三:从容器获取
<?php
use Hyperf\Context\ApplicationContext;

$userService = ApplicationContext::getContainer()->get(UserService::class);
$result = $userService->getList();

1.4 DI 容器原理

容器的核心功能

  1. 绑定:将接口绑定到具体实现
  2. 解析:根据类型创建对象
  3. 注入:自动注入依赖
  4. 管理生命周期:单例、多例

示例

<?php
// 1. 定义接口
interface CacheInterface
{
    public function get(string $key);
    public function set(string $key, $value);
}

// 2. 实现接口
class RedisCache implements CacheInterface
{
    public function get(string $key) { /* ... */ }
    public function set(string $key, $value) { /* ... */ }
}

// 3. 绑定到容器(config/autoload/dependencies.php)
return [
    CacheInterface::class => RedisCache::class,
];

// 4. 使用时注入接口
class UserService
{
    #[Inject]
    private CacheInterface $cache;  // 自动注入 RedisCache 实例
}

1.5 作用域

Hyperf 支持两种作用域:

单例模式(Singleton)
<?php
use Hyperf\Di\Annotation\Inject;

#[Inject]
private UserService $userService;  // 默认是单例,整个应用共享一个实例
多例模式
<?php
use Hyperf\Di\Annotation\Inject;

#[Inject(lazy: true)]
private UserService $userService;  // 每次使用时创建新实例

注意:在协程环境下,单例模式需要确保类是协程安全的。

1.6 延迟加载

<?php
use Hyperf\Di\Annotation\Inject;

class UserController
{
    // 不会立即创建对象,而是在首次使用时才创建
    #[Inject(lazy: true)]
    private UserService $userService;
    
    public function index()
    {
        // 这里才真正创建 UserService 对象
        return $this->userService->getList();
    }
}

2. AOP(面向切面编程)

2.1 什么是 AOP?

定义:AOP(Aspect Oriented Programming)是一种编程范式,允许在不修改原有代码的情况下,对方法进行增强。

核心概念

  • 切面(Aspect):横切关注点的模块化
  • 切点(Pointcut):定义在哪些方法上应用切面
  • 通知(Advice):切面的具体行为

2.2 AOP 的应用场景

  • ✅ 日志记录
  • ✅ 性能监控
  • ✅ 权限校验
  • ✅ 事务管理
  • ✅ 缓存
  • ✅ 异常处理

2.3 创建切面

示例一:记录方法执行时间
<?php
namespace App\Aspect;

use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Logger\LoggerFactory;

#[Aspect]
class TimeAspect extends AbstractAspect
{
    // 要切入的类
    public array $classes = [
        'App\Service\UserService',
    ];
    
    // 要切入的注解
    public array $annotations = [];
    
    public function __construct(private LoggerFactory $loggerFactory)
    {
    }
    
    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        $start = microtime(true);
        
        // 执行原方法
        $result = $proceedingJoinPoint->process();
        
        $time = microtime(true) - $start;
        $class = $proceedingJoinPoint->className;
        $method = $proceedingJoinPoint->methodName;
        
        $logger = $this->loggerFactory->get('performance');
        $logger->info("{$class}::{$method} 执行时间: {$time}秒");
        
        return $result;
    }
}
示例二:权限校验
<?php
namespace App\Aspect;

use App\Annotation\RequirePermission;
use App\Exception\PermissionDeniedException;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;

#[Aspect]
class PermissionAspect extends AbstractAspect
{
    // 切入带有 RequirePermission 注解的方法
    public array $annotations = [
        RequirePermission::class,
    ];
    
    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        // 获取注解
        $annotation = $proceedingJoinPoint->getAnnotationMetadata()
            ->method[RequirePermission::class] ?? null;
        
        if ($annotation) {
            $permission = $annotation->permission;
            
            // 检查权限
            if (!$this->hasPermission($permission)) {
                throw new PermissionDeniedException("缺少权限: {$permission}");
            }
        }
        
        // 权限验证通过,执行原方法
        return $proceedingJoinPoint->process();
    }
    
    private function hasPermission(string $permission): bool
    {
        // 实现权限检查逻辑
        return true;
    }
}

使用注解:

<?php
namespace App\Controller;

use App\Annotation\RequirePermission;

class AdminController
{
    #[RequirePermission(permission: 'admin.user.delete')]
    public function deleteUser(int $id)
    {
        // 只有有权限的用户才能执行这里的代码
    }
}
示例三:自动缓存
<?php
namespace App\Aspect;

use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Redis\Redis;

#[Aspect]
class CacheAspect extends AbstractAspect
{
    public array $annotations = [
        \App\Annotation\Cacheable::class,
    ];
    
    public function __construct(private Redis $redis)
    {
    }
    
    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        $annotation = $proceedingJoinPoint->getAnnotationMetadata()
            ->method[\App\Annotation\Cacheable::class] ?? null;
        
        if (!$annotation) {
            return $proceedingJoinPoint->process();
        }
        
        // 生成缓存 key
        $key = $annotation->prefix . ':' . md5(serialize($proceedingJoinPoint->arguments['keys']));
        
        // 检查缓存
        $cached = $this->redis->get($key);
        if ($cached !== null) {
            return unserialize($cached);
        }
        
        // 执行原方法
        $result = $proceedingJoinPoint->process();
        
        // 保存到缓存
        $this->redis->setex($key, $annotation->ttl, serialize($result));
        
        return $result;
    }
}

使用注解:

<?php
namespace App\Service;

use App\Annotation\Cacheable;

class UserService
{
    #[Cacheable(prefix: 'user', ttl: 3600)]
    public function getUserById(int $id)
    {
        // 方法结果会自动缓存 1 小时
        return Db::table('users')->find($id);
    }
}

2.4 切面优先级

当多个切面作用于同一个方法时,可以设置优先级。

<?php
#[Aspect(priority: 1)]  // 数字越小,优先级越高
class LogAspect extends AbstractAspect
{
}

#[Aspect(priority: 10)]
class CacheAspect extends AbstractAspect
{
}

执行顺序:

请求 → LogAspect → CacheAspect → 原方法 → CacheAspect → LogAspect → 响应

2.5 自定义注解

定义注解
<?php
namespace App\Annotation;

use Attribute;
use Hyperf\Di\Annotation\AbstractAnnotation;

#[Attribute(Attribute::TARGET_METHOD)]
class RateLimit extends AbstractAnnotation
{
    public function __construct(
        public int $limit = 100,      // 限制次数
        public int $period = 60,      // 时间周期(秒)
    ) {
    }
}
创建切面处理注解
<?php
namespace App\Aspect;

use App\Annotation\RateLimit;
use App\Exception\RateLimitException;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Redis\Redis;

#[Aspect]
class RateLimitAspect extends AbstractAspect
{
    public array $annotations = [
        RateLimit::class,
    ];
    
    public function __construct(private Redis $redis)
    {
    }
    
    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        $annotation = $proceedingJoinPoint->getAnnotationMetadata()
            ->method[RateLimit::class] ?? null;
        
        if ($annotation) {
            $key = 'rate_limit:' . $proceedingJoinPoint->className . ':' . $proceedingJoinPoint->methodName;
            
            $count = $this->redis->incr($key);
            
            if ($count == 1) {
                $this->redis->expire($key, $annotation->period);
            }
            
            if ($count > $annotation->limit) {
                throw new RateLimitException('请求过于频繁,请稍后再试');
            }
        }
        
        return $proceedingJoinPoint->process();
    }
}
使用注解
<?php
namespace App\Controller;

use App\Annotation\RateLimit;

class ApiController
{
    #[RateLimit(limit: 10, period: 60)]  // 每分钟最多 10 次
    public function sendSms()
    {
        // 发送短信
    }
}

3. 实战案例

3.1 统一日志记录

<?php
namespace App\Aspect;

use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
use Hyperf\Logger\LoggerFactory;

#[Aspect]
class LogAspect extends AbstractAspect
{
    public array $classes = [
        'App\Controller\*',  // 所有控制器
    ];
    
    public function __construct(private LoggerFactory $loggerFactory)
    {
    }
    
    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        $logger = $this->loggerFactory->get('request');
        
        $class = $proceedingJoinPoint->className;
        $method = $proceedingJoinPoint->methodName;
        $arguments = $proceedingJoinPoint->arguments['keys'];
        
        $logger->info("调用 {$class}::{$method}", [
            'arguments' => $arguments,
        ]);
        
        try {
            $result = $proceedingJoinPoint->process();
            
            $logger->info("调用成功", [
                'result' => $result,
            ]);
            
            return $result;
        } catch (\Throwable $e) {
            $logger->error("调用失败", [
                'exception' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
            
            throw $e;
        }
    }
}

3.2 数据库事务

<?php
namespace App\Aspect;

use App\Annotation\Transaction;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;

#[Aspect]
class TransactionAspect extends AbstractAspect
{
    public array $annotations = [
        Transaction::class,
    ];
    
    public function process(ProceedingJoinPoint $proceedingJoinPoint)
    {
        return Db::transaction(function () use ($proceedingJoinPoint) {
            return $proceedingJoinPoint->process();
        });
    }
}

使用:

<?php
namespace App\Service;

use App\Annotation\Transaction;

class OrderService
{
    #[Transaction]
    public function createOrder(array $data)
    {
        // 创建订单
        $orderId = Db::table('orders')->insertGetId($data);
        
        // 扣减库存
        Db::table('products')->where('id', $data['product_id'])->decrement('stock');
        
        // 如果发生异常,自动回滚
        return $orderId;
    }
}

4. 注意事项

4.1 AOP 不生效的常见原因

  1. 类没有从容器获取

    ❌ 错误:

    $service = new UserService();  // AOP 不生效
    

    ✅ 正确:

    $service = ApplicationContext::getContainer()->get(UserService::class);
    
  2. 方法不是 public

    class UserService
    {
        private function getList() { }  // AOP 不生效
    }
    
  3. 类被标记为 final

    final class UserService { }  // AOP 不生效
    

4.2 性能考虑

  • AOP 会生成代理类,有一定的性能开销
  • 不要在所有方法上都使用 AOP
  • 合理设置切点范围

5. 要点

必须掌握

  • 什么是依赖注入,有什么优势
  • Hyperf 的依赖注入方式
  • 什么是 AOP,有什么应用场景
  • 如何创建切面

加分项

  • DI 容器的实现原理
  • 依赖注入和服务定位器的区别
  • AOP 的底层实现(代理模式)

高频题

1. 什么是依赖注入?有什么优势?

答:依赖注入是将类所依赖的对象从外部注入,而不是在类内部创建。

优势:

  • 降低耦合度,依赖接口而非具体实现
  • 便于单元测试,可以注入 Mock 对象
  • 便于替换实现,无需修改代码
  • 自动管理对象生命周期

2. Hyperf 的 AOP 有什么应用场景?

答:常见应用场景包括:

  • 日志记录:记录方法调用日志
  • 性能监控:统计方法执行时间
  • 权限校验:检查用户权限
  • 缓存:自动缓存方法结果
  • 事务管理:自动开启和提交事务
  • 限流:防止接口被频繁调用

3. 为什么 AOP 有时候不生效?

答:常见原因:

  1. 对象不是从容器获取的(new 创建的对象 AOP 不生效)
  2. 方法不是 public
  3. 类被标记为 final
  4. 切点配置不正确

下一步:阅读 04-常用组件.md 学习组件使用