>  기사  >  Java  >  AOP를 사용하는 이유

AOP를 사용하는 이유

巴扎黑
巴扎黑원래의
2017-06-26 11:48:011810검색

원본 텍스트 주소, 재인쇄 시 출처를 표기해주세요. 감사합니다

Foreword

1년 반 전에 Spring3: AOP라는 글을 썼고, 배우고 있었습니다. 당시 Spring AOP 사용법은 당시에 작성된 내용으로 비교적 기본적인 내용이다. 이 글 마지막에 있는 추천과 답글에는 모두에게 도움이 될 것 같은 댓글이 많이 포함되어 있지만, 지금 개인적인 관점에서 볼 때 이 글은 잘 쓰여지지 않았고, 심지어는 없다고 할 수도 있습니다. 콘텐츠가 많기 때문에 이러한 추천과 리뷰를 보면 자격이 있다고 느껴집니다.

위의 이유를 바탕으로 가장 기본적인 원본 코드-> 디자인 패턴(데코레이터 패턴 및 프록시) 사용-> AOP의 세 가지 수준을 사용하여 AOP를 사용하는 이유를 설명하는 기사를 업데이트하겠습니다. , 이 기사가 네티즌과 친구들에게 도움이되기를 바랍니다.

원본 코드 작성 방법

코드를 통해 시연하려면 예제가 있어야 합니다.

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

먼저 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. Dao에 다른 구현 클래스가 있는 경우 새 클래스를 추가하여 구현 클래스를 래핑해야 하며 이로 인해 클래스 수가 계속 늘어납니다

  2. 데코레이터 모드를 사용해 보세요

그런 다음 디자인 모드를 사용해 먼저 데코레이터 모드를 사용해 보겠습니다. 얼마나 많은 문제를 해결할 수 있는지 볼까요? 데코레이터 패턴의 핵심은 Dao 인터페이스를 구현하고 Dao 인터페이스에 대한 참조를 유지하는 것입니다. 저는 새 클래스 이름을 LogDao로 지정했으며 그 구현은 다음과 같습니다.

 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 }

사용할 때 "

Dao dao = new LogDao(new DaoImpl())

" 메소드, 이 메소드의 장점은 다음과 같습니다. 투명합니다. 호출자에게는 Dao만 알지만 로그에 기능이 추가되었습니다

  1. 클래스는 무한히 확장되지 않습니다. Dao의 다른 구현 클래스가 로그를 출력해야 하는 경우 LogDao의 생성자에 다른 Dao 구현 클래스만 전달하면 됩니다.

  2. 하지만 이렇게 하면 됩니다. 단점도 있습니다.

출력 로그의 논리는 재사용할 수 없습니다.

  1. 출력 로그의 논리는 이전과 동일한 시간을 출력하려는 ​​경우 코드와 결합됩니다. delete() 메서드 이후에는 LogDao를 수정해야 합니다

  2. 그러나 이 접근 방식은 가장 독창적인 코드 작성 방법에 비해 크게 개선되었습니다.

프록시 모드 사용

그런 다음 프록시 모드를 사용하여 가장 독창적인 기능을 구현하려고 합니다. 그런 다음 InvocationHandler를 정의하고 이름을 LogInvocationHandler로 지정해야 합니다. 구현은 다음과 같습니다.

 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 }

호출 방법은 매우 간단합니다. 주요 함수를 작성합니다.

 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 }

이 시나리오에서 사용하려는 aop 태그가 라는 것은 의심의 여지가 없습니다. 여기서 작성한 내용은 매우 간단합니다. 현재 사용자 ID와 요청 경로를 가져옵니다. 이 두 가지를 기반으로 사용자에게 요청에 대한 액세스 권한이 있는지 확인하면 의미를 이해할 수 있습니다.

Postscript

이 글에서는 네이티브 코드부터 AOP를 사용하기까지의 과정을 보여주고, 각 진화의 장단점을 조금씩 소개하고, 마지막으로 실제 예제를 통해 AOP가 할 수 있는 일을 분석합니다.

이전 AOP 소개 기사 Spring3: AOP가 이 기사와 결합되어 네티즌과 친구들에게 정말 유익할 수 있기를 바랍니다.

위 내용은 AOP를 사용하는 이유의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.