共计 5272 个字符,预计需要花费 14 分钟才能阅读完成。
作为一名 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);
// ... 省略更多配置项
}
}
复制代码
该数据源组件要想真正失效须要对其进行许多配置,这个创立和配置过程是十分麻烦的。而且配置可能会随着业务需要的变动常常更改,这时候你就须要批改每一个依赖该组件的中央,牵一发而动全身。这还只是演示了一个数据源的创立配置过程,实在开发中可有太多组件和太多配置须要编码了,其麻烦水平堪称恐怖。
当然,这些问题都能够引入设计模式来解决,不过这样一来又绕回去了:设计模式自身也会带来复杂性。这就像一种死循环:传统开发模式编码简单,要想解决这种简单却得陷入另一种简单。难道没有方法解决了吗?当然不是的,在讲优良解决方案前,咱们先来梳理一下目前呈现的问题:
- 创立了许多反复对象,造成大量资源节约;
- 更换实现类须要改变多个中央;
- 创立和配置组件工作繁冗,给组件调用方带来极大不便。
透过景象看实质,这些问题的呈现都是同一个起因:组件的调用方参加了组件的创立和配置工作。
其实调用方只需关注组件如何调用,至于这个组件如何创立和配置又与调用方有什么关系呢?就好比我去餐馆只需点菜,饭菜并不需要我亲自去做,餐馆天然会做好给我送过来。如果咱们编码时,有一个「货色」能帮忙咱们创立和配置好那些组件,咱们只负责调用该多好。这个「货色」就是容器。
容器这一概念咱们已接触过,Tomcat 就是 Servlet 的容器,它帮咱们创立并配置好 Servlet,咱们只需编写业务逻辑即可。试想一下,如果 Servlet 要咱们本人创立,HttpRequest、HttpResponse 对象也须要咱们本人配置,那代码量得有多恐怖。
Tomcat 是 Servlet 容器,只负责管理 Servlet。咱们平时应用的组件则须要另一种容器来治理,这种容器咱们称之为 IoC 容器。
管制反转和依赖注入
管制反转,是指对象的创立和配置的控制权从调用方转移给容器。好比在家本人做菜,菜的滋味全副由本人管制;去餐馆吃饭,菜的滋味则是交由餐馆管制。IoC 容器就负责了餐馆的角色。
有了 IoC 容器,咱们能够将对象交由容器治理,交由容器治理后的对象称之为 Bean。调用方不再负责组件的创立,要应用组件时间接获取 Bean 即可:
@Component
public class UserServiceImpl implements UserService{
@Autowired // 获取 Bean
private UserDao userDao;
}
复制代码
调用方只需依照约定申明依赖项,所须要的 Bean 就主动配置结束了,就如同在调用方内部注入了一个依赖项给其应用,所以这种形式称之为 依赖注入(Dependency Injection,缩写为 DI)。管制反转和依赖注入是一体两面,都是同一种开发模式的表现形式。
IoC 轻而易举地解决了咱们刚刚总结的问题:
对象交由容器治理后,默认是单例的,这就解决了资源节约问题。
若要更换实现类,只需更改 Bean 的申明配置,即可达到无感知更换:
public class UserServiceImpl implements UserService{...}
// 将该实现类申明为 Bean
@Component
public class OtherUserServiceImpl implements UserService{...}
复制代码
当初组件的应用和组件的创立与配置齐全拆散开来。调用方只需调用组件而无需关怀其余工作,这极大进步了咱们的开发效率,也让整个利用充斥了灵活性、扩展性。
这样看来,咱们如此中意 IoC 不是没有情理的。
AOP
咱们再来假如没有 AOP 会怎么。
面向对象的局限性
面向对象编程(Object-oriented programming,缩写:OOP)的三大个性:封装、继承、多态,咱们早已用得炉火纯青。OOP 的益处已无需赘言,置信大家都有领会。这里咱们来看一下 OOP 的局限性。
当有反复代码呈现时,能够就将其封装进去而后复用。咱们通过分层、分包、分类来布局不同的逻辑和职责,就像之前解说的三层架构。但这里的复用的都是 外围业务逻辑 ,并不能复用一些 辅助逻辑,比方:日志记录、性能统计、平安校验、事务管理,等等。这些边缘逻辑往往贯通你整个外围业务,传统 OOP 很难将其封装:
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---");
}
}
复制代码
为了不便演示,这里只用了打印语句,就算如此这代码看着也很好受,而且这些逻辑是所有业务办法都要加上,想想都恐怖。
OOP 是至上而下的编程形式,犹如一个树状图,A 调用 B、B 调用 C,或者 A 继承 B、B 继承 C。这种形式对于业务逻辑来说是适合的,通过调用或继承以复用。而辅助逻辑就像一把闸刀横向贯通所有办法,如图 2 - 4 所示:
这一条条横线好像切开了 OOP 的树状构造,犹如一个大蛋糕被切开多层,每一层都会执行雷同的辅助逻辑,所以大家将这些辅助逻辑称为层面或者切面。
代理模式用来减少或加强原有性能再适宜不过了,但切面逻辑的难点不是 不批改原有业务 ,而是 对所有业务失效。对一个业务类加强就得新建一个代理类,对所有业务加强,每个类都要新建代理类,这无疑是一场劫难。而且这里只是演示了一个日志打印的切面逻辑,如果我再加一个性能统计切面,就得新建一个切面代理类来代理日志打印的代理类,一旦切面多起来这个代理类嵌套就会十分深。
面向切面编程(Aspect-oriented programming,缩写为 AOP)正是为了解决这一问题而诞生的技术。
面向切面编程
AOP 不是 OOP 的对立面,它是对 OOP 的一种补充。OOP 是纵向的,AOP 是横向的,两者相结合方能构建出良好的程序结构。AOP 技术,让咱们可能不批改原有代码,便能让切面逻辑在所有业务逻辑中失效。
咱们只需申明一个切面,写上切面逻辑:
@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 办法执行后 ===");
}
}
复制代码
无论你有一个业务办法,还是一万个业务办法,对咱们开发者来说只需编写一次切面逻辑,就能让所有业务办法失效,极大进步了咱们的开发效率。
总结
IoC 解决了以下问题:
- 创立了许多反复对象,造成大量资源节约;
- 更换实现类须要改变多个中央;
- 创立和配置组件工作繁冗,给组件调用方带来极大不便。
AOP 解决了以下问题:
- 切面逻辑编写繁琐,有多少个业务办法就须要编写多少次。
参考:《2020 最新 Java 根底精讲视频教程和学习路线!》
链接:https://juejin.cn/post/692484…