원본 텍스트 주소, 재인쇄 시 출처를 표기해주세요. 감사합니다
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로 지정하고 구현은 다음과 같습니다. 가장 독창적인 작성 방법입니다. 이 작성 방법의 단점도 한눈에 알 수 있습니다.
메서드 호출 전후의 시간 출력 논리는 이 논리를 다른 곳에 추가하려는 경우 재사용할 수 없습니다. , 다시 작성해야 합니다
Dao에 다른 구현 클래스가 있는 경우 새 클래스를 추가하여 구현 클래스를 래핑해야 하며 이로 인해 클래스 수가 계속 늘어납니다
그런 다음 디자인 모드를 사용해 먼저 데코레이터 모드를 사용해 보겠습니다. 얼마나 많은 문제를 해결할 수 있는지 볼까요? 데코레이터 패턴의 핵심은 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의 다른 구현 클래스가 로그를 출력해야 하는 경우 LogDao의 생성자에 다른 Dao 구현 클래스만 전달하면 됩니다.
하지만 이렇게 하면 됩니다. 단점도 있습니다.
출력 로그의 논리는 재사용할 수 없습니다.
출력 로그의 논리는 이전과 동일한 시간을 출력하려는 경우 코드와 결합됩니다. delete() 메서드 이후에는 LogDao를 수정해야 합니다
그러나 이 접근 방식은 가장 독창적인 코드 작성 방법에 비해 크게 개선되었습니다.
프록시 모드 사용
그런 다음 프록시 모드를 사용하여 가장 독창적인 기능을 구현하려고 합니다. 그런 다음 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 }
结果就不演示了,这种方式的优点为:
输出日志的逻辑被复用起来,如果要针对其他接口用上输出日志的逻辑,只要在newProxyInstance的时候的第二个参数增加Class>数组中的内容即可
这种方式的缺点为:
JDK提供的动态代理只能针对接口做代理,不能针对类做代理
代码依然有耦合,如果要对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两种方式。
从目前看来代码又更好了一些,但是我认为还有两个缺点:
无论使用Java代理还是使用CGLIB,编写这部分代码都稍显麻烦
代码之间的耦合还是没有解决,像要针对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获取调用方法的类、方法签名等信息。由于这里我用的
接着看一下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的几个优点:
切面的内容可以复用,比如TimeHandler的printTime方法,任何地方需要打印方法执行前的时间与方法执行后的时间,都可以使用TimeHandler的printTime方法
避免使用Proxy、CGLIB生成代理,这方面的工作全部框架去实现,开发者可以专注于切面内容本身
代码与代码之间没有耦合,如果拦截的方法有变化修改配置文件即可
下面用一张图来表示一下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,可以实现两个功能:
insert、update、delete操作事物自动提交
对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 태그가
Postscript
이 글에서는 네이티브 코드부터 AOP를 사용하기까지의 과정을 보여주고, 각 진화의 장단점을 조금씩 소개하고, 마지막으로 실제 예제를 통해 AOP가 할 수 있는 일을 분석합니다.
이전 AOP 소개 기사 Spring3: AOP가 이 기사와 결합되어 네티즌과 친구들에게 정말 유익할 수 있기를 바랍니다.
위 내용은 AOP를 사용하는 이유의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!