Maison  >  Article  >  Java  >  Pourquoi utiliser l'AOP

Pourquoi utiliser l'AOP

巴扎黑
巴扎黑original
2017-06-26 11:48:011796parcourir

Adresse d'origine, merci d'indiquer la source de réimpression, merci

Avant-propos

J'ai écrit un article Spring3 : AOP il y a un an et demi. Je l'ai écrit quand j'apprenais à utiliser Spring AOP. C'était relativement basique. Les recommandations et réponses à la fin de cet article incluent de nombreux commentaires que je pense utiles à tout le monde, mais maintenant de mon point de vue personnel, cet article n'est pas bien écrit, et on peut même dire qu'il n'a pas beaucoup de contenu, donc ces recommandations et critiques me font me sentir bien méritées.

Sur la base des raisons ci-dessus, j'ai mis à jour un article, en commençant par le code original le plus basique --> en utilisant des modèles de conception (modèle de décorateur et proxy) --> d'AOP Expliquons pourquoi nous utilisons AOP. J'espère que cet article pourra être bénéfique aux internautes.

Comment écrire le code original

Puisque vous souhaitez démontrer à travers le code, ça doit être Il y a des exemples, voici mon exemple :

<span style="color: #000000">有一个接口Dao有insert、delete、update三个方法,在insert与update被调用的前后,打印调用前的毫秒数与调用后的毫秒数<br></span>

Définissez d'abord une interface 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 }

Définissez ensuite une classe d'implémentation 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 }

La façon la plus originale de l'écrire est d'appeler insert( ) et la méthode update() avant et après l'impression de l'heure, vous pouvez uniquement définir une nouvelle couche de package de classe et la traiter avant et après avoir appelé la méthode insert() et la méthode update() que j'ai nommée la nouvelle classe ServiceImpl, et sa méthode. la mise en œuvre est :

 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 }

C'est la façon d'écrire la plus originale, et les défauts de cette façon d'écrire sont également évidents :

  1. La logique de sortie du temps avant et après l'appel de méthode ne peut pas être réutilisée. Si vous souhaitez ajouter cette logique ailleurs, vous devez la réécrire

  2. Si Dao a d'autres classes d'implémentation, alors une nouvelle classe doit être ajoutée pour envelopper la classe d'implémentation, ce qui entraînera une augmentation continue du nombre de classes

Utiliser la décoration Mode Décorateur

Ensuite, nous utilisons le mode design, utilisons d'abord le mode décorateur pour voyez combien de problèmes peuvent être résolus. Le cœur du modèle de décorateur est d'implémenter l'interface Dao et de contenir une référence à l'interface Dao. J'ai nommé la nouvelle classe LogDao, et son implémentation est :

 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 }
Lors de son utilisation, vous pouvez utiliser la méthode "

Dao dao = new LogDao(new DaoImpl())" Les avantages de cette méthode. sont :

  1. Transparent, pour l'appelant, il ne connaît que Dao, mais ne sait pas que la fonction de journalisation a été ajoutée

  2. La classe ne s'étendra pas à l'infini. Si d'autres classes d'implémentation Dao doivent générer des journaux, il vous suffit de transmettre différentes classes d'implémentation Dao au constructeur LogDao

Cependant, cette méthode présente également des inconvénients évidents :

  1. La logique du journal de sortie ne peut pas être réutilisée

  2. La logique du journal de sortie est couplée au code. Si je veux sortir en même temps avant et après la méthode delete(), je dois modifier LogDao

  3. Cependant, cette approche a été grandement améliorée par rapport à la méthode d'écriture de code la plus originale.

Utiliser le mode proxy

Ensuite on utilise le mode proxy pour tenter d'atteindre la fonction la plus originale, utilisant le mode proxy, alors il faut définir un InvocationHandler, je l'ai nommé LogInvocationHandler, et son implémentation est :

 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 }
Le la méthode d'appel est très simple, j'écris une fonction principale :

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

  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 <?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          8          9         ">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 五月的仓颉 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 }

Il ne fait aucun doute que la balise aop que nous souhaitons utiliser dans ce scénario est . Ce que j'ai écrit ici est très simple. Obtenez l'ID utilisateur actuel et le chemin de la demande. Sur la base de ces deux éléments, déterminez si l'utilisateur est autorisé à accéder à la demande. Vous pouvez comprendre la signification.

Postscript

L'article démontre le processus depuis le code natif jusqu'à l'utilisation d'AOP It. présente petit à petit les avantages et les inconvénients de chaque évolution, et enfin analyse ce que peut faire AOP avec des exemples pratiques.

Le précédent article d'introduction à l'AOP Spring3 : AOP combiné à cet article, j'espère qu'il pourra être vraiment bénéfique aux internautes et aux amis.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn