面向切面编程 控制反转
1. Aspect Oriented Program#
1.1. 基础概念#
面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用
但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来
也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程
一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的
AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充
1.2. 举例解释#
假设有一个大的项目,包含多个服务类(如UserService、OrderService、ProductService),每个类有多个方法,都需要日志
直接写日志的方式
public class UserService {
public void saveUser(String username) {
System.out.println("Before saving user");
System.out.println("Saving user: " + username);
System.out.println("After saving user");
}
public void deleteUser(String username) {
System.out.println("Before deleting user");
System.out.println("Deleting user: " + username);
System.out.println("After deleting user");
}
}
public class OrderService {
public void createOrder(String orderId) {
System.out.println("Before creating order");
System.out.println("Creating order: " + orderId);
System.out.println("After creating order");
}
// 更多方法...
}
- 如果要改日志格式(比如加时间戳),得改动所有方法
- 如果要加新功能(比如记录方法执行时间),又得在每个方法里加代码
用AOP的方式
// 业务逻辑
public class UserService {
public void saveUser(String username) {
System.out.println("Saving user: " + username);
}
public void deleteUser(String username) {
System.out.println("Deleting user: " + username);
}
}
public class OrderService {
public void createOrder(String orderId) {
System.out.println("Creating order: " + orderId);
}
}
// 切面
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.*Service.*(..))")
public void logBefore() {
System.out.println("Before method execution");
}
@After("execution(* com.example.*Service.*(..))")
public void logAfter() {
System.out.println("After method execution");
}
}
-
代码简洁:业务代码只关注核心逻辑(如保存用户、创建订单),日志逻辑集中在 LoggingAspect
-
一处修改,处处生效:改日志格式只需改 LoggingAspect,无需动业务代码
2. IoC#
2.1. Dependency Inject#
ASP.NET Core 通过其内置的依赖注入容器(DI 容器)来管理依赖注入
2.2. 通过构造函数注入#
- ASP.NET Core 的 DI 容器会检查类的构造函数,判断构造函数中是否有需要注入的依赖
- 如果构造函数的参数类型(如 UserService)已经在 DI 容器中注册,容器会在实例化类时自动解析这些依赖并传入
public UserController(UserManageService userManageService, IMapper mapper)
{
_mapper = mapper;
_userManageService = userManageService;
}
当 ASP.NET Core 需要实例化 UserController(例如处理 HTTP 请求时),DI 容器会检查构造函数的参数类型:
- UserService:一个自定义服务,可能用于处理用户管理逻辑
- 如果这些类型已经在 DI 容器中注册,容器会自动解析并提供对应的实例,注入到
UserController
中
2.3. DI容器注册#
依赖注入的前提是服务必须在 DI 容器中注册, 通常在 Startup.cs
或 Program.cs
的 ConfigureServices
方法中完成注册
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<UserManageService>();
services.AddSingleton<IMapper>(new MapperConfiguration(cfg => { /* 配置 AutoMapper */ }).CreateMapper());
services.AddControllers();
}
AddScoped<UserManageService>()
:注册UserService
,指定其生命周期为Scoped
(每个 HTTP 请求一个实例)AddSingleton<IMapper>()
:注册IMapper
,指定为Singleton
(整个应用程序共享一个实例)- DI 容器会记录这些服务及其生命周期,并在需要时解析
ASP.NET Core 的依赖注入不依赖注解(如 Java 中的 @Inject 或 @Autowired), 它通过构造函数的签名和 DI 容器中的服务注册来自动识别和注入依赖
只要服务在容器中注册,并且构造函数中声明了这些服务作为参数,ASP.NET Core 就会自动进行依赖注入
2.4. 控制反转(IoC)是什么意思?#
控制反转(Inversion of Control, IoC) 是一种设计原则,用于降低代码之间的耦合度, 它的核心思想是将对象的创建和管理的控制权从类本身转移到外部容器
Inversion of Control (IoC) is a design principle aimed at decreasing coupling among software components. The fundamental concept involves shifting the responsibility for object creation and lifecycle management away from the components themselves and delegating it to an external container or framework.
依赖注入(DI)是实现 IoC 的一种具体方式,通过构造函数、属性或方法将依赖传递给类
在
UserController
中,构造函数注入是 ASP.NET Core 实现 IoC 的方式,DI 容器负责解析和提供UserService
和IMapper