0

0

为什么要使用AOP

巴扎黑

巴扎黑

发布时间:2017-06-26 11:48:01

|

2072人浏览过

|

来源于php中文网

原创

原文地址,转载请注明出处,谢谢

 

前言

一年半前写了一篇文章Spring3:AOP,是当时学习如何使用Spring AOP的时候写的,比较基础。这篇文章最后的推荐以及回复认为我写的对大家有帮助的评论有很多,但是现在从我个人的角度来看,这篇文章写得并不好,甚至可以说是没有太多实质性的内容,因此这些推荐和评论让我觉得受之有愧。

基于以上原因,更新一篇文章,从最基础的原始代码-->使用设计模式(装饰器模式与代理)-->使用AOP三个层次来讲解一下为什么我们要使用AOP,希望这篇文章可以对网友朋友们有益。

 

原始代码的写法

既然要通过代码来演示,那必须要有例子,这里我的例子为:

有一个接口Dao有insert、delete、update三个方法,在insert与update被调用的前后,打印调用前的毫秒数与调用后的毫秒数

首先定义一个Dao接口:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public interface Dao { 5  6     public void insert(); 7      8     public void delete(); 9     10     public void update();11     12 }

然后定义一个实现类DaoImpl:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class DaoImpl implements Dao { 5  6     @Override 7     public void insert() { 8         System.out.println("DaoImpl.insert()"); 9     }10 11     @Override12     public void delete() {13         System.out.println("DaoImpl.delete()");14     }15 16     @Override17     public void update() {18         System.out.println("DaoImpl.update()");19     }20     21 }

最原始的写法,我要在调用insert()与update()方法前后分别打印时间,就只能定义一个新的类包一层,在调用insert()方法与update()方法前后分别处理一下,新的类我命名为ServiceImpl,其实现为:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class ServiceImpl { 5  6     private Dao dao = new DaoImpl(); 7      8     public void insert() { 9         System.out.println("insert()方法开始时间:" + System.currentTimeMillis());10         dao.insert();11         System.out.println("insert()方法结束时间:" + System.currentTimeMillis());12     }13     14     public void delete() {15         dao.delete();16     }17     18     public void update() {19         System.out.println("update()方法开始时间:" + System.currentTimeMillis());20         dao.update();21         System.out.println("update()方法结束时间:" + System.currentTimeMillis());22     }23     24 }

这是最原始的写法,这种写法的缺点也是一目了然:

  1. 方法调用前后输出时间的逻辑无法复用,如果有别的地方要增加这段逻辑就得再写一遍

  2. 如果Dao有其它实现类,那么必须新增一个类去包装该实现类,这将导致类数量不断膨胀

 

使用装饰器模式

接着我们使用上设计模式,先用装饰器模式,看看能解决多少问题。装饰器模式的核心就是实现Dao接口并持有Dao接口的引用,我将新增的类命名为LogDao,其实现为:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class LogDao implements Dao { 5  6     private Dao dao; 7      8     public LogDao(Dao dao) { 9         this.dao = dao;10     }11 12     @Override13     public void insert() {14         System.out.println("insert()方法开始时间:" + System.currentTimeMillis());15         dao.insert();16         System.out.println("insert()方法结束时间:" + System.currentTimeMillis());17     }18 19     @Override20     public void delete() {21         dao.delete();22     }23 24     @Override25     public void update() {26         System.out.println("update()方法开始时间:" + System.currentTimeMillis());27         dao.update();28         System.out.println("update()方法结束时间:" + System.currentTimeMillis());29     }30 31 }

在使用的时候,可以使用"Dao dao = new LogDao(new DaoImpl())"的方式,这种方式的优点为:

  1. 透明,对调用方来说,它只知道Dao,而不知道加上了日志功能

  2. 类不会无限膨胀,如果Dao的其它实现类需要输出日志,只需要向LogDao的构造函数中传入不同的Dao实现类即可

不过这种方式同样有明显的缺点,缺点为:

  1. 输出日志的逻辑还是无法复用

  2. 输出日志的逻辑与代码有耦合,如果我要对delete()方法前后同样输出时间,需要修改LogDao

但是,这种做法相比最原始的代码写法,已经有了很大的改进。

 

使用代理模式

接着我们使用代理模式尝试去实现最原始的功能,使用代理模式,那么我们就要定义一个InvocationHandler,我将它命名为LogInvocationHandler,其实现为:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class LogInvocationHandler implements InvocationHandler { 5  6     private Object obj; 7      8     public LogInvocationHandler(Object obj) { 9         this.obj = obj;10     }11     12     @Override13     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {14         String methodName = method.getName();15         if ("insert".equals(methodName) || "update".equals(methodName)) {16             System.out.println(methodName + "()方法开始时间:" + System.currentTimeMillis());17             Object result = method.invoke(obj, args);18             System.out.println(methodName + "()方法结束时间:" + System.currentTimeMillis());19             20             return result;21         }22         23         return method.invoke(obj, args);24     }25     26 }

其调用方式很简单,我写一个main函数:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public static void main(String[] args) { 5     Dao dao = new DaoImpl(); 6          7     Dao proxyDao = (Dao)Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), new Class[]{Dao.class}, new LogInvocationHandler(dao)); 8          9     proxyDao.insert();10     System.out.println("----------分割线----------");11     proxyDao.delete();12     System.out.println("----------分割线----------");13     proxyDao.update();14 }

结果就不演示了,这种方式的优点为:

  1. 输出日志的逻辑被复用起来,如果要针对其他接口用上输出日志的逻辑,只要在newProxyInstance的时候的第二个参数增加Class>数组中的内容即可

这种方式的缺点为:

  1. JDK提供的动态代理只能针对接口做代理,不能针对类做代理

  2. 代码依然有耦合,如果要对delete方法调用前后打印时间,得在LogInvocationHandler中增加delete方法的判断

 

使用CGLIB

接着看一下使用CGLIB的方式,使用CGLIB只需要实现MethodInterceptor接口即可:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class DaoProxy implements MethodInterceptor { 5  6     @Override 7     public Object intercept(Object object, Method method, Object[] objects, MethodProxy proxy) throws Throwable { 8         String methodName = method.getName(); 9         10         if ("insert".equals(methodName) || "update".equals(methodName)) {11             System.out.println(methodName + "()方法开始时间:" + System.currentTimeMillis());12             proxy.invokeSuper(object, objects);13             System.out.println(methodName + "()方法结束时间:" + System.currentTimeMillis());14             15             return object;16         }17         18         proxy.invokeSuper(object, objects);19         return object;20     }21 22 }

代码调用方式为:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public static void main(String[] args) { 5     DaoProxy daoProxy = new DaoProxy(); 6      7     Enhancer enhancer = new Enhancer(); 8     enhancer.setSuperclass(DaoImpl.class); 9     enhancer.setCallback(daoProxy);10         11     Dao dao = (DaoImpl)enhancer.create();12     dao.insert();13     System.out.println("----------分割线----------");14     dao.delete();15     System.out.println("----------分割线----------");16     dao.update();17 }

使用CGLIB解决了JDK的Proxy无法针对类做代理的问题,但是这里要专门说明一个问题:使用装饰器模式可以说是对使用原生代码的一种改进,使用Java代理可以说是对于使用装饰器模式的一种改进,但是使用CGLIB并不是对于使用Java代理的一种改进

前面的可以说改进是因为使用装饰器模式比使用原生代码更好,使用Java代理又比使用装饰器模式更好,但是Java代理与CGLIb的对比并不能说改进,因为使用CGLIB并不一定比使用Java代理更好,这两种各有优缺点,像Spring框架就同时支持Java Proxy与CGLIB两种方式。

从目前看来代码又更好了一些,但是我认为还有两个缺点:

  1. 无论使用Java代理还是使用CGLIB,编写这部分代码都稍显麻烦

  2. 代码之间的耦合还是没有解决,像要针对delete()方法加上这部分逻辑就必须修改代码

 

使用AOP

最后来看一下使用AOP的方式,首先定义一个时间处理类,我将它命名为TimeHandler:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class TimeHandler { 5      6     public void printTime(ProceedingJoinPoint pjp) { 7         Signature signature = pjp.getSignature(); 8         if (signature instanceof MethodSignature) { 9             MethodSignature methodSignature = (MethodSignature)signature;10             Method method = methodSignature.getMethod();11             System.out.println(method.getName() + "()方法开始时间:" + System.currentTimeMillis());12             13             try {14                 pjp.proceed();15                 System.out.println(method.getName() + "()方法结束时间:" + System.currentTimeMillis());16             } catch (Throwable e) {17                 18             }19         }20     }21     22 }

到第8行的代码与第12行的代码分别打印方法开始执行时间与方法结束执行时间。我这里写得稍微复杂点,使用了的写法,其实也可以拆分为两种,这个看个人喜好。

这里多说一句,切面方法printTime本身可以不用定义任何的参数,但是有些场景下需要获取调用方法的类、方法签名等信息,此时可以在printTime方法中定义JointPoint,Spring会自动将参数注入,可以通过JoinPoint获取调用方法的类、方法签名等信息。由于这里我用的,要保证方法的调用,这样才能在方法调用前后输出时间,因此不能直接使用JoinPoint,因为JoinPoint没法保证方法调用。此时可以使用ProceedingJoinPoint,ProceedingPointPoint的proceed()方法可以保证方法调用,但是要注意一点,ProceedingJoinPoint只能和搭配,换句话说,如果aop.xml中配置的是,然后printTime的方法参数又是JoinPoint的话,Spring容器启动将报错。

接着看一下aop.xml的配置:

 1  2 10 11     12     13 14     15         16         17             18             19         20     21     22 

我不大会写expression,也懒得去百度了,因此这里就拦截Dao下的所有方法了。测试代码很简单:

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class AopTest { 5  6     @Test 7     @SuppressWarnings("resource") 8     public void testAop() { 9         ApplicationContext ac = new ClassPathXmlApplicationContext("spring/aop.xml");10         11         Dao dao = (Dao)ac.getBean("daoImpl");12         dao.insert();13         System.out.println("----------分割线----------");14         dao.delete();15         System.out.println("----------分割线----------");16         dao.update();17     }18     19 }

结果就不演示了。到此我总结一下使用AOP的几个优点:

  1. 切面的内容可以复用,比如TimeHandler的printTime方法,任何地方需要打印方法执行前的时间与方法执行后的时间,都可以使用TimeHandler的printTime方法

  2. 避免使用Proxy、CGLIB生成代理,这方面的工作全部框架去实现,开发者可以专注于切面内容本身

  3. 代码与代码之间没有耦合,如果拦截的方法有变化修改配置文件即可

下面用一张图来表示一下AOP的作用:

我们传统的编程方式是垂直化的编程,即A-->B-->C-->D这么下去,一个逻辑完毕之后执行另外一段逻辑。但是AOP提供了另外一种思路,它的作用是在业务逻辑不知情(即业务逻辑不需要做任何的改动)的情况下对业务代码的功能进行增强,这种编程思想的使用场景有很多,例如事物提交、方法执行之前的权限检测、日志打印、方法调用事件等等。

 

AOP使用场景举例

上面的例子纯粹为了演示使用,为了让大家更加理解AOP的作用,这里以实际场景作为例子。

第一个例子,我们知道MyBatis的事物默认是不会自动提交的,因此在编程的时候我们必须在增删改完毕之后调用SqlSession的commit()方法进行事物提交,这非常麻烦,下面利用AOP简单写一段代码帮助我们自动提交事物(这段代码我个人测试过可用):

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class TransactionHandler { 5  6     public void commit(JoinPoint jp) { 7         Object obj = jp.getTarget(); 8         if (obj instanceof MailDao) { 9             Signature signature = jp.getSignature();10             if (signature instanceof MethodSignature) {11                 SqlSession sqlSession = SqlSessionThrealLocalUtil.getSqlSession();                
12                 13                 MethodSignature methodSignature = (MethodSignature)signature;14                 Method method = methodSignature.getMethod();15                  16                 String methodName = method.getName();17                 if (methodName.startsWith("insert") || methodName.startsWith("update") || methodName.startsWith("delete")) {18                     sqlSession.commit();19                 }20                 21                 sqlSession.close();22             }23         }24     }25     26 }

这种场景下我们要使用的aop标签为,即切在方法调用之后。

这里我做了一个SqlSessionThreadLocalUtil,每次打开会话的时候,都通过SqlSessionThreadLocalUtil把当前会话SqlSession放到ThreadLocal中,看到通过TransactionHandler,可以实现两个功能:

  1. insert、update、delete操作事物自动提交

  2. 对SqlSession进行close(),这样就不需要在业务代码里面关闭会话了,因为有些时候我们写业务代码的时候会忘记关闭SqlSession,这样可能会造成内存句柄的膨胀,因此这部分切面也一并做了

整个过程,业务代码是不知道的,而TransactionHandler的内容可以充分再多处场景下进行复用。

第二个例子是权限控制的例子,不管是从安全角度考虑还是从业务角度考虑,我们在开发一个Web系统的时候不可能所有请求都对所有用户开放,因此这里就需要做一层权限控制了,大家看AOP作用的时候想必也肯定会看到AOP可以做权限控制,这里我就演示一下如何使用AOP做权限控制。我们知道原生的Spring MVC,Java类是实现Controller接口的,基于此,利用AOP做权限控制的大致代码如下(这段代码纯粹就是一段示例,我构建的Maven工程是一个普通的Java工程,因此没有验证过):

 1 /** 2  * @author 五月的仓颉 3  */ 4 public class PermissionHandler { 5  6     public void hasPermission(JoinPoint jp) throws Exception { 7         Object obj = jp.getTarget(); 8          9         if (obj instanceof Controller) {10             Signature signature = jp.getSignature();11             MethodSignature methodSignature = (MethodSignature)signature;12             13             // 获取方法签名14             Method method = methodSignature.getMethod();15             // 获取方法参数16             Object[] args = jp.getArgs();17             18             // Controller中唯一一个方法的方法签名ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;19             // 这里对这个方法做一层判断20             if ("handleRequest".equals(method.getName()) && args.length == 2) {21                 Object firstArg = args[0];22                 if (obj instanceof HttpServletRequest) {23                     HttpServletRequest request = (HttpServletRequest)firstArg;24                     // 获取用户id25                     long userId = Long.parseLong(request.getParameter("userId"));26                     // 获取当前请求路径27                     String requestUri = request.getRequestURI();28                     29                     if(!PermissionUtil.hasPermission(userId, requestUri)) {30                         throw new Exception("没有权限");31                     }32                 }33             }34         }35         36     }37     38 }

毫无疑问这种场景下我们要使用的aop标签为。这里我写得很简单,获取当前用户id与请求路径,根据这两者,判断该用户是否有权限访问该请求,大家明白意思即可。

 

后记

文章演示了从原生代码到使用AOP的过程,一点一点地介绍了每次演化的优缺点,最后以实际例子分析了AOP可以做什么事情。

之前的那篇AOP入门的文章Spring3:AOP再结合上这篇文章,希望可以真正对网友朋友们有益。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
1688阿里巴巴货源平台入口与批发采购指南
1688阿里巴巴货源平台入口与批发采购指南

本专题整理了1688阿里巴巴批发进货平台的最新入口地址与在线采购指南,帮助用户快速找到官方网站入口,了解如何进行批发采购、货源选择以及厂家直销等功能,提升采购效率与平台使用体验。

50

2026.02.06

快手网页版入口与电脑端使用指南 快手官方短视频观看入口
快手网页版入口与电脑端使用指南 快手官方短视频观看入口

本专题汇总了快手网页版的最新入口地址和电脑版使用方法,详细提供快手官网直接访问链接、网页端操作教程,以及如何无需下载安装直接观看短视频的方式,帮助用户轻松浏览和观看快手短视频内容。

8

2026.02.06

C# 多线程与异步编程
C# 多线程与异步编程

本专题深入讲解 C# 中多线程与异步编程的核心概念与实战技巧,包括线程池管理、Task 类的使用、async/await 异步编程模式、并发控制与线程同步、死锁与竞态条件的解决方案。通过实际项目,帮助开发者掌握 如何在 C# 中构建高并发、低延迟的异步系统,提升应用性能和响应速度。

7

2026.02.06

Python 微服务架构与 FastAPI 框架
Python 微服务架构与 FastAPI 框架

本专题系统讲解 Python 微服务架构设计与 FastAPI 框架应用,涵盖 FastAPI 的快速开发、路由与依赖注入、数据模型验证、API 文档自动生成、OAuth2 与 JWT 身份验证、异步支持、部署与扩展等。通过实际案例,帮助学习者掌握 使用 FastAPI 构建高效、可扩展的微服务应用,提高服务响应速度与系统可维护性。

4

2026.02.06

JavaScript 异步编程与事件驱动架构
JavaScript 异步编程与事件驱动架构

本专题深入讲解 JavaScript 异步编程与事件驱动架构,涵盖 Promise、async/await、事件循环机制、回调函数、任务队列与微任务队列、以及如何设计高效的异步应用架构。通过多个实际示例,帮助开发者掌握 如何处理复杂异步操作,并利用事件驱动设计模式构建高效、响应式应用。

4

2026.02.06

java连接字符串方法汇总
java连接字符串方法汇总

本专题整合了java连接字符串教程合集,阅读专题下面的文章了解更多详细操作。

25

2026.02.05

java中fail含义
java中fail含义

本专题整合了java中fail的含义、作用相关内容,阅读专题下面的文章了解更多详细内容。

28

2026.02.05

控制反转和依赖注入区别
控制反转和依赖注入区别

本专题整合了控制反转和依赖注入区别、解释、实现方法相关内容。阅读专题下面的文章了解更多详细教程。

19

2026.02.05

钉钉脑图插图教程合集
钉钉脑图插图教程合集

本专题整合了钉钉脑图怎么插入图片、钉钉脑图怎么用相关教程,阅读专题下面的文章了解更多详细内容。

60

2026.02.05

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP自制框架
PHP自制框架

共8课时 | 0.6万人学习

PHP面向对象基础课程(更新中)
PHP面向对象基础课程(更新中)

共12课时 | 0.7万人学习

AI绘画教程
AI绘画教程

共2课时 | 0.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号