【转】我们为什么要使用AOP?

原文作者:五月的仓颉

原文地址:http://www.cnblogs.com/xrq730/p/7003082.html 转载请注明出处,谢谢!!!

前言

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

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

 

原始代码的写法

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

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

首先定义一个Dao接口:

复制代码
 1 /**
 2  * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  3 */  4 public class DaoImpl implements Dao {  5  6  @Override  7 public void insert() {  8 System.out.println("DaoImpl.insert()");  9  } 10 11  @Override 12 public void delete() { 13 System.out.println("DaoImpl.delete()"); 14  } 15 16  @Override 17 public void update() { 18 System.out.println("DaoImpl.update()"); 19  } 20 21 }
复制代码

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

复制代码
 1 /**
 2  * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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  @Override 13 public void insert() { 14 System.out.println("insert()方法开始时间:" + System.currentTimeMillis()); 15  dao.insert(); 16 System.out.println("insert()方法结束时间:" + System.currentTimeMillis()); 17  } 18 19  @Override 20 public void delete() { 21  dao.delete(); 22  } 23 24  @Override 25 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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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  @Override 13 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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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行的代码分别打印方法开始执行时间与方法结束执行时间。我这里写得稍微复杂点,使用了<aop:around>的写法,其实也可以拆分为<aop:before>与<aop:after>两种,这个看个人喜好。

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

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

复制代码
 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"  3  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  4  xmlns:aop="http://www.springframework.org/schema/aop"  5  xmlns:tx="http://www.springframework.org/schema/tx"  6  xsi:schemaLocation="http://www.springframework.org/schema/beans  7  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd  8  http://www.springframework.org/schema/aop  9  http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"> 10 11 <bean id="daoImpl" class="org.xrq.spring.action.aop.DaoImpl" /> 12 <bean id="timeHandler" class="org.xrq.spring.action.aop.TimeHandler" /> 13 14 <aop:config> 15 <aop:pointcut id="addAllMethod" expression="execution(* org.xrq.spring.action.aop.Dao.*(..))" /> 16 <aop:aspect id="time" ref="timeHandler"> 17 <aop:before method="printTime" pointcut-ref="addAllMethod" /> 18 <aop:after method="printTime" pointcut-ref="addAllMethod" /> 19 </aop:aspect> 20 </aop:config> 21 22 </beans>
复制代码

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

复制代码
 1 /**
 2  * @author 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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标签为<aop:after>,即切在方法调用之后。

这里我做了一个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 五月的仓颉http://www.cnblogs.com/xrq730/p/7003082.html  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 // 获取用户id 25 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标签为<aop:before>。这里我写得很简单,获取当前用户id与请求路径,根据这两者,判断该用户是否有权限访问该请求,大家明白意思即可。

 

后记

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

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

转载于:https://www.cnblogs.com/sxc1991/p/7048460.html

### 如何在 Spring Boot 中使用 AOP 实现参数换 #### 创建基础项目结构 为了实现参数换功能,首先需要构建一个标准的 Spring Boot 应用程序。这通常涉及创建一个新的 Java 类作为应用程序入口点,并标注 `@SpringBootApplication` 注解来启用自动配置和其他特性。 ```java package com.example.paramconversion; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ParamConversionApplication { public static void main(String[] args) { SpringApplication.run(ParamConversionApplication.class, args); } } ``` #### 添加必要的依赖项 确保项目的 `pom.xml` 或者 `build.gradle` 文件中包含了对 AspectJ 和 Spring AOP 支持的相关依赖: 对于 Maven 用户,在 pom.xml 中加入如下片段[^3]: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` #### 定义切面类处理入参换逻辑 接下来定义一个切面 (Aspect),用于拦截目标方法调用并执行自定义行为——即在此处完成参数类型的换工作。下面是一个简单的例子展示如何利用环绕通知 (`@Around`) 来改变传给服务层的方法参数值: ```java package com.example.paramconversion.aspect; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Slf4j @Aspect @Component public class ParameterConversionAspect { @Around("@annotation(com.example.paramconversion.annotation.ParamConvert)") public Object convertParameter(ProceedingJoinPoint joinPoint) throws Throwable { log.info("Before method execution"); // 获取原始参数列表 Object[] originalArgs = joinPoint.getArgs(); // 假设第一个参数是要被换的对象实例 if(originalArgs != null && originalArgs.length > 0){ String inputString = (String)originalArgs[0]; // 进行字符串到整数的简单换为例 Integer convertedValue = Integer.parseInt(inputString); // 替换原参数数组中的对应位置元素为新值 originalArgs[0] = convertedValue; } try{ // 继续执行连接点所代表的目标对象上的方法调用 return joinPoint.proceed(originalArgs); } finally { log.info("After method execution"); } } } ``` 上述代码展示了当某个带有特定注解的方法被执行时,会触发此方面内的业务逻辑;在这里实现了将输入的第一个参数由字符串形式成整形数值的功能。 #### 使用自定义注解标记需化参数的方法 为了让上面编写的切面能够识别哪些地方应该发生参数变换,则可以设计一套自己的元数据标签(Annotation)。例如: ```java package com.example.paramconversion.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ParamConvert {} ``` 最后一步就是在实际的服务接口里运用这个新的注释了: ```java package com.example.paramconversion.service.impl; import com.example.paramconversion.annotation.ParamConvert; import com.example.paramconversion.service.DemoService; import org.springframework.stereotype.Service; @Service public class DemoServiceImpl implements DemoService { @Override @ParamConvert public int addOne(Integer num) { System.out.println("Received number after conversion:" + num); return ++num; } } ``` 在这个例子中,每当调用了 `addOne()` 方法的时候,如果传递了一个可解析为数字的字符串进去的话,那么它将会先经过我们之前设置好的切面前置处理器来进行类型变再继续往下走正常的流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值