我们到底为什么要用 IoC 和 AOP
作为一名 Java 开发,对 Spring 框架是再熟悉不过的了。Spring 支持的控制反转(Inversion of Control,缩写为IoC)和面向切面编程(Aspect-oriented programming,缩写为AOP)早已成为我们的开发习惯,仿佛 Java 开发天生就该如此。人总是会忽略习以为常的事物,所有人都熟练使用 IoC 和 AOP,却鲜有人说得清楚到底为什么要用 IoC 和 AOP。
技术肯定是为了解决某个问题而诞生,要弄清楚为什么使用 IoC 和 AOP,就得先弄清楚不用它们会碰到什么问题。
IoC
我们现在假设回到了没有 IoC 的时代,用传统的 Servlet 进行开发。
传统开发模式的弊端
三层架构是经典的开发模式,我们一般将视图控制、业务逻辑和数据库操作分别抽离出来单独形成一个类,这样各个职责就非常清晰且易于复用和维护。大致代码如下:
@WebServlet("/user")
public class UserServlet extends HttpServlet {
// 用于执行业务逻辑的对象
private UserService userService = new UserServiceImpl();
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// ...省略其他代码
// 执行业务逻辑
userService.doService();
// ...返回页面视图
}
}
public class UserServiceImpl implements UserService{
// 用于操作数据库的对象
private UserDao userDao = new UserDaoImpl();
@Override
public void doService() {
// ...省略业务逻辑代码
// 执行数据库操作
userDao.doUpdate();
// ...省略业务逻辑代码
}
}
public class UserDaoImpl implements UserDao{
@Override
public void doUpdate() {
// ...省略JDBC代码
}
}
上层依赖下层的抽象,代码就分为了三层:
业界普遍按这种分层方式组织代码,其核心思想是职责分离。层次越低复用程度越高,比如一个 DAO 对象往往会被多个 Service 对象使用,一个 Service 对象往往也会被多个 Controller 对象使用:
条理分明,井然有序。这些被复用的对象就像一个个的组件,供多方使用。
虽然这个倒三角看上去非常漂亮,然而我们目前的代码有一个比较大的问题,那就是我们只做到了逻辑复用,并没有做到资源复用。
上层调用下一层时,必然会持有下一层的对象引用,即成员变量。目前我们每一个成员变量都会实例化一个对象,如下图所示:
每一个链路都创建了同样的对象,造成了极大的资源浪费。本应多个 Controller 复用同一个 Service,多个 Service 复用同一个 DAO。现在变成了一个 Controller创建多个重复的 Service,多个 Service 又创建了多个重复的 DAO,从倒三角变成了正三角。
许多组件只需要实例化一个对象就够了,创建多个没有任何意义。针对对象重复创建的问题,我们自然而然想到了单例模式。只要编写类时都将其写为单例,这样就避免了资源浪费。但是,引入设计模式必然会带来复杂性,况且还是每一个类都为单例,每一个类都会有相似的代码,其弊端不言自明。
有人可能会说,那我不在意“这点”资源浪费了,我服务器内存大无所谓,我只求开发便捷痛快不想写额外的代码。
确实,三层架构达到逻辑复用已经非常方便了,还奢求其他的干什么呢。但就算不管资源问题,目前代码还有一个致命缺陷,那就是变化的代价太大。
假设有 10 个 Controller 依赖了 UserService,最开始实例化的是 UserServiceImpl
,后面需要换一个实现类 OtherUserServiceImpl
,我就得逐个修改那 10 个 Controller,非常麻烦。更换实现类的需求可能不会太多,没多大说服力。那咱们看另一个情况。
之前咱们演示的组件创建过程非常简单,new
一下就完了,可很多时候创建一个组件没那么容易。比如 DAO 对象要依赖一个这样的数据源组件:
public class UserDaoImpl implements UserDao{
private MyDataSource dataSource;
public UserDaoImpl() {
// 构造数据源
dataSource = new MyDataSource("jdbc:mysql://localhost:3306/test", "root", "password");
// 进行一些其他配置
dataSource.setInitiaSize(10);
dataSource.setMaxActive(100);
// ...省略更多配置项
}
}
该数据源组件要想真正生效需要对其进行许多配置,这个创建和配置过程是非常麻烦的。而且配置可能会随着业务需求的变化经常更改,这时候你就需要修改每一个依赖该组件的地方,牵一发而动全身。这还只是演示了一个数据源的创建配置过程,真实开发中可有太多组件和太多配置需要编码了,其麻烦程度堪称恐怖。
当然,这些问题都可以引入设计模式来解决,不过这样一来又绕回去了:设计模式本身也会带来复杂性。这就像一种死循环:传统开发模式编码复杂,要想解决这种复杂却得陷入另一种复杂。难道没有办法解决了吗?当然不是的,在讲优秀解决方案前,我们先来梳理一下目前出现的问题:
创建了许多重复对象,造成大量资源浪费; 更换实现类需要改动多个地方; 创建和配置组件工作繁杂,给组件调用方带来极大不便。
控制反转和依赖注入
@Component
public class UserServiceImpl implements UserService{
@Autowired // 获取 Bean
private UserDao userDao;
}
public class UserServiceImpl implements UserService{
...
}
// 将该实现类声明为 Bean
@Component
public class OtherUserServiceImpl implements UserService{
...
}
AOP
面向对象的局限性
public class UserServiceImpl implements UserService {
@Override
public void doService() {
System.out.println("---安全校验---");
System.out.println("---性能统计 Start---");
System.out.println("---日志打印 Start---");
System.out.println("---事务管理 Start---");
System.out.println("业务逻辑");
System.out.println("---事务管理 End---");
System.out.println("---日志打印 End---");
System.out.println("---性能统计 End---");
}
}
面向切面编程
@Aspect // 声明一个切面
@Component
public class MyAspect {
// 原业务方法执行前
@Before("execution(public void com.rudecrab.test.service.*.doService())")
public void methodBefore() {
System.out.println("===AspectJ 方法执行前===");
}
// 原业务方法执行后
@AfterReturning("execution(* com.rudecrab.test.service..doService(..))")
public void methodAddAfterReturning() {
System.out.println("===AspectJ 方法执行后===");
}
}
总结
创建了许多重复对象,造成大量资源浪费; 更换实现类需要改动多个地方; 创建和配置组件工作繁杂,给组件调用方带来极大不便。
切面逻辑编写繁琐,有多少个业务方法就需要编写多少次。
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️