做为一名 Java 开发,对 Spring 框架是再熟悉不过的了。Spring 支持的控制反转(Inversion of Control,缩写为IoC)和面向切面编程(Aspect-oriented programming,缩写为AOP)早已成为咱们的开发习惯,仿佛 Java 开发天生就该如此。人老是会忽略习觉得常的事物,全部人都熟练使用 IoC 和 AOP,却鲜有人说得清楚到底为何要用 IoC 和 AOP。java
技术确定是为了解决某个问题而诞生,要弄清楚为何使用 IoC 和 AOP,就得先弄清楚不用它们会碰到什么问题。mysql
咱们如今假设回到了没有 IoC 的时代,用传统的 Servlet 进行开发。sql
三层架构是经典的开发模式,咱们通常将视图控制、业务逻辑和数据库操做分别抽离出来单独造成一个类,这样各个职责就很是清晰且易于复用和维护。大体代码以下:数据库
@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 会怎样。
面向对象编程(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 解决了如下问题:
本文到这里就结束了,我是「RudeCrab」,一只粗鲁的螃蟹,咱们下篇文章再见。
关注「RudeCrab」微信公众号,和螃蟹一块儿横行霸道。