Heim  >  Artikel  >  Java  >  Warum AOP verwenden?

Warum AOP verwenden?

巴扎黑
巴扎黑Original
2017-06-26 11:48:011796Durchsuche

Ursprüngliche Adresse, bitte geben Sie die Quelle für den Nachdruck an, vielen Dank

Vorwort

Ich habe vor anderthalb Jahren einen Artikel geschrieben, als ich den Umgang mit Spring AOP lernte. Die Empfehlungen und Antworten am Ende dieses Artikels enthalten viele Kommentare, die meiner Meinung nach für alle hilfreich sind. Aus meiner persönlichen Sicht ist dieser Artikel jedoch nicht gut geschrieben, und man kann sogar sagen, dass dies nicht der Fall ist Viel Substanz, daher fühle ich mich mit diesen Empfehlungen und Rezensionen wohlverdient.

Aus den oben genannten Gründen habe ich einen Artikel aktualisiert, beginnend mit dem grundlegendsten Originalcode –> unter Verwendung von Designmustern (Dekorationsmuster und Proxy) –> von AOP Lassen Sie uns erklären, warum wir AOP verwenden. Ich hoffe, dass dieser Artikel für Internetnutzer von Nutzen sein kann.

So schreiben Sie den Originalcode

Da Sie dies durch Code demonstrieren möchten, Es muss sein. Es gibt Beispiele, hier ist mein Beispiel:

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

Definieren Sie zunächst eine Dao-Schnittstelle:

 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 }

Dann definieren Sie eine Implementierungsklasse 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 }

Die originellste Schreibweise besteht darin, insert( ) und der update()-Methode vor und nach dem Drucken der Zeit können Sie nur eine neue Klassenpaketebene definieren und diese vor und nach dem Aufruf der insert()-Methode und der update()-Methode verarbeiten. Ich habe die neue Klasse ServiceImpl genannt und sie Die Implementierung lautet:

 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 }

Dies ist die originellste Schreibweise, und die Mängel dieser Schreibweise sind ebenfalls offensichtlich:

  1. Die Logik der Zeitausgabe vor und nach dem Methodenaufruf kann nicht wiederverwendet werden. Wenn Sie diese Logik an anderer Stelle hinzufügen möchten, müssen Sie sie erneut schreiben

  2. Wenn Dao über andere Implementierungsklassen verfügt, muss eine neue Klasse hinzugefügt werden, um die Implementierungsklasse zu umschließen, wodurch die Anzahl der Klassen weiter zunimmt

Dekorationsmodus verwenden

Dann verwenden wir den Designmodus. Verwenden Sie zunächst den Dekorationsmodus Sehen Sie, wie viele Probleme gelöst werden können. Der Kern des Dekorationsmusters besteht darin, die Dao-Schnittstelle zu implementieren und einen Verweis auf die Dao-Schnittstelle zu halten. Ich habe die neue Klasse LogDao genannt, und ihre Implementierung ist:

 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 }

Bei der Verwendung können Sie die Methode „Dao dao = new LogDao(new DaoImpl())“ verwenden sind:

  1. Transparent, für den Anrufer kennt er nur Dao, weiß aber nicht, dass die Protokollierungsfunktion hinzugefügt wurde

  2. Die Klasse wird nicht unbegrenzt erweitert. Wenn andere Dao-Implementierungsklassen Protokolle ausgeben müssen, müssen Sie nur andere Dao-Implementierungsklassen an den LogDao-Konstruktor übergeben

Diese Methode weist jedoch auch offensichtliche Mängel auf. Die Nachteile sind:

  1. Die Logik des Ausgabeprotokolls kann nicht wiederverwendet werden

  2. Die Logik des Ausgabeprotokolls ist mit dem Code gekoppelt. Wenn ich vor und nach der delete()-Methode gleichzeitig ausgeben möchte, muss ich LogDao ändern

Dieser Ansatz wurde jedoch im Vergleich zur ursprünglichsten Methode zum Schreiben von Code erheblich verbessert.

Verwendung des Proxy-Modus

Dann versuchen wir, dies mit dem Proxy-Modus zu erreichen Die originellste Funktion verwendet den Proxy-Modus. Dann müssen wir einen InvocationHandler definieren. Ich habe ihn LogInvocationHandler genannt Die Aufrufmethode ist sehr einfach. Ich schreibe eine Hauptfunktion:

 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 }

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

  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 }

Es besteht kein Zweifel, dass das Aop-Tag, das wir in diesem Szenario verwenden möchten, ist. Was ich hier geschrieben habe, ist sehr einfach. Bestimmen Sie anhand dieser beiden, ob der Benutzer berechtigt ist, auf die Anfrage zuzugreifen.

Postscript

Der Artikel zeigt den Prozess vom nativen Code bis zur Verwendung von AOP It stellt Stück für Stück die Vor- und Nachteile jeder Evolution vor und analysiert abschließend anhand praktischer Beispiele, was AOP leisten kann.

Der vorherige AOP-Einführungsartikel Spring3: AOP kombiniert mit diesem Artikel, ich hoffe, dass er für Internetnutzer und Freunde wirklich nützlich sein kann.

Das obige ist der detaillierte Inhalt vonWarum AOP verwenden?. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Vorheriger Artikel:c3p0-Nutzungsdatensatz in JavaNächster Artikel:c3p0-Nutzungsdatensatz in Java