依赖注入与 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 容器原理
容器的核心功能:
- 绑定:将接口绑定到具体实现
- 解析:根据类型创建对象
- 注入:自动注入依赖
- 管理生命周期:单例、多例
示例:
<?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 不生效的常见原因
-
类没有从容器获取
❌ 错误:
$service = new UserService(); // AOP 不生效✅ 正确:
$service = ApplicationContext::getContainer()->get(UserService::class); -
方法不是 public
class UserService { private function getList() { } // AOP 不生效 } -
类被标记为 final
final class UserService { } // AOP 不生效
4.2 性能考虑
- AOP 会生成代理类,有一定的性能开销
- 不要在所有方法上都使用 AOP
- 合理设置切点范围
5. 要点
必须掌握
- 什么是依赖注入,有什么优势
- Hyperf 的依赖注入方式
- 什么是 AOP,有什么应用场景
- 如何创建切面
加分项
- DI 容器的实现原理
- 依赖注入和服务定位器的区别
- AOP 的底层实现(代理模式)
高频题
1. 什么是依赖注入?有什么优势?
答:依赖注入是将类所依赖的对象从外部注入,而不是在类内部创建。
优势:
- 降低耦合度,依赖接口而非具体实现
- 便于单元测试,可以注入 Mock 对象
- 便于替换实现,无需修改代码
- 自动管理对象生命周期
2. Hyperf 的 AOP 有什么应用场景?
答:常见应用场景包括:
- 日志记录:记录方法调用日志
- 性能监控:统计方法执行时间
- 权限校验:检查用户权限
- 缓存:自动缓存方法结果
- 事务管理:自动开启和提交事务
- 限流:防止接口被频繁调用
3. 为什么 AOP 有时候不生效?
答:常见原因:
- 对象不是从容器获取的(new 创建的对象 AOP 不生效)
- 方法不是 public
- 类被标记为 final
- 切点配置不正确
下一步:阅读 04-常用组件.md 学习组件使用