拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Spring高手之路19——Spring AOP注解指南

文章目录*1. 背景* 2. 基于AspectJ注解来实现AOP* 3. XML实现和注解实现AOP的代码对比* 4. AOP通知讲解* 5. AOP时序图* 6. 实战手写JDK动态代理实现AOP1. 背景------在现代软件开发中面向切面编程AOP是一种强大的编程范式允许开发者跨越应用程序的多个部分定义横切关注点如日志记录、事务管理等。本文将介绍如何在Spring框架中通过AspectJ注解以及对应的XML配置来实现AOP在不改变主业务逻辑的情况下增强应用程序的功能。2. 基于AspectJ注解来实现AOP---------------------对于一个使用Maven的Spring项目需要在pom.xml中添加以下依赖 org.springframework spring-context 5.3.10 org.aspectj aspectjrt 1.9.6 org.aspectj aspectjweaver 1.9.6 确保版本号与使用的Spring版本相匹配可以自行调整。1.创建业务逻辑接口MyService package com.example.demo.aop; public interface MyService { void performAction(); } 2.创建业务逻辑类MyServiceImpl.java package com.example.demo.aop; import org.springframework.stereotype.Service; Service public class MyServiceImpl implements MyService { Override public void performAction() { System.out.println(“Performing an action in MyService”); } } 3.定义切面创建切面类MyAspect.java并使用注解定义切面和通知 package com.example.demo.aop; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; Aspect Component public class MyAspect { Before(“execution(* com.example.demo.aop.MyServiceImpl.performAction(…))”) public void logBeforeAction() { System.out.println(“Before performing action”); } }Aspect注解是用来标识一个类作为AspectJ切面的一种方式这在基于注解的AOP配置中是必需的。它相当于XML配置中定义切面的方式但使用注解可以更加直观和便捷地在类级别上声明切面而无需繁琐的XML配置。4.配置Spring以启用注解和AOP创建一个Java配置类来代替XML配置使用Configuration注解标记为配置类并通过ComponentScan注解来启用组件扫描通过EnableAspectJAutoProxy启用AspectJ自动代理 package com.example.demo.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; Configuration ComponentScan(“com.example”) EnableAspectJAutoProxy public class AppConfig { }EnableAspectJAutoProxy注解在Spring中用于启用对AspectJ风格的切面的支持。它告诉Spring框架去寻找带有Aspect注解的类并将它们注册为Spring应用上下文中的切面以便在运行时通过代理方式应用这些切面定义的通知Advice和切点Pointcuts。如果不写EnableAspectJAutoProxySpring将不会自动处理Aspect注解定义的切面则定义的那些前置通知Before、后置通知After、AfterReturning、AfterThrowing和环绕通知Around将不会被自动应用到目标方法上。这意味着定义的AOP逻辑不会被执行失去了AOP带来的功能增强。Before注解定义了一个前置通知Advice它会在指定方法执行之前运行。切点表达式execution(* com.example.demo.aop.MyServiceImpl.performAction(..))精确地定义了这些连接点的位置。在这个例子中切点表达式指定了MyServiceImpl类中的performAction方法作为连接点而Before注解标识的方法logBeforeAction将在这个连接点之前执行即logBeforeAction方法前置通知会在performAction执行之前被执行。5.创建主类和测试AOP功能主程序如下 package com.example.demo; import com.example.demo.aop.MyService; import com.example.demo.config.AppConfig; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); MyService service context.getBean(MyService.class); service.performAction(); } } 运行结果如下3. XML实现和注解实现AOP的代码对比----------------------对于上面的代码我们将原有基于注解的AOP配置改写为完全基于XML的形式方便大家对比。首先需要移除切面类和业务逻辑类上的所有Spring相关注解然后在XML文件中配置相应的bean和AOP逻辑。移除注解首先我们移除业务逻辑类和切面类上的所有注解。MyService.java(无变化接口保持原样): package com.example.demo.aop; public interface MyService { void performAction(); }MyServiceImpl.java (移除Service注解): package com.example.demo.aop; public class MyServiceImpl implements MyService { Override public void performAction() { System.out.println(“Performing an action in MyService”); } }MyAspect.java (移除Aspect和Component注解同时去掉方法上的Before注解): package com.example.demo.aop; public class MyAspect { public void logBeforeAction() { System.out.println(“Before performing action”); } }XML配置接下来删除AppConfig配置类在Spring的XML配置文件中定义beans和AOP配置。applicationContext.xml: ?xml version1.0 encodingUTF-8? aop:config aop:aspect id“aspect” ref“myAspect” aop:pointcut id“serviceOperation” expression“execution(* com.example.demo.aop.MyService.performAction(…))”/ aop:before pointcut-ref“serviceOperation” method“logBeforeAction”/ /aop:aspect /aop:config 在这个XML配置中我们手动注册了MyServiceImpl和MyAspect作为beans并通过aop:config元素定义了AOP逻辑。我们创建了一个切点serviceOperation用于匹配MyService.performAction(..)方法的执行并定义了一个前置通知当匹配的方法被调用时MyAspect的logBeforeAction方法将被执行。主类和测试AOP功能主类DemoApplication的代码不需要改变只是在创建ApplicationContext时使用XML配置文件而不是Java配置类 package com.example.demo; import com.example.demo.aop.MyService; import org.springframework.context.support.ClassPathXmlApplicationContext; public class DemoApplication { public static void main(String[] args) { ClassPathXmlApplicationContext context new ClassPathXmlApplicationContext(“applicationContext.xml”); MyService service context.getBean(MyService.class); service.performAction(); } } 运行结果是一样的4. AOP通知讲解-----------在Spring AOP中通知Advice定义了切面Aspect在目标方法调用过程中的具体行为。Spring AOP支持五种类型的通知它们分别是前置通知Before、后置通知After、返回通知After Returning、异常通知After Throwing和环绕通知Around。通过使用这些通知开发者可以在目标方法的不同执行点插入自定义的逻辑。* **Before前置通知*前置通知是在目标方法执行之前执行的通知通常用于执行一些预处理任务如日志记录、安全检查等。 Before(execution(com.example.demo.aop.MyServiceImpl.performAction(…))) public void logBeforeAction() { System.out.println(“Before performing action”); } *AfterReturning返回通知返回通知在目标方法成功执行之后执行可以访问方法的返回值。 AfterReturning(pointcut execution(com.example.demo.aop.MyServiceImpl.performAction(…)), returning “result”) public void logAfterReturning(Object result) { System.out.println(Method returned value is : result); } 这里在AfterReturning注解中指定returning result时Spring AOP框架将目标方法的返回值传递给切面方法的名为result的参数因此切面方法需要有一个与之匹配的参数类型兼容目标方法的返回类型。如果两者不匹配Spring在启动时会抛出异常因为它无法将返回值绑定到切面方法的参数。AfterThrowing异常通知异常通知在目标方法抛出异常时执行允许访问抛出的异常。 AfterThrowing(pointcut execution(com.example.demo.aop.MyServiceImpl.performAction(…))“, throwing “ex”) public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) { String methodName joinPoint.getSignature().getName(); System.out.println(”AfterThrowing: Exception in method: methodName ; Exception: ex.toString()); } 在AfterThrowing注解的方法中包含JoinPoint参数是可选的当想知道哪个连接点即方法引发了异常的详细信息时非常有用假设有多个方法可能抛出相同类型的异常而我们想在日志中明确指出是哪个方法引发了异常。通过访问JoinPoint提供的信息可以记录下引发异常的方法名称和其他上下文信息从而使得日志更加清晰和有用。After后置通知后置通知在目标方法执行之后执行无论方法执行是否成功即便发生异常仍然会执行。它类似于finally块。 After(execution(com.example.demo.aop.MyServiceImpl.performAction(…))) public void logAfter() { System.out.println(“After performing action”); } *Around环绕通知环绕通知围绕目标方法执行可以在方法调用前后执行自定义逻辑同时决定是否继续执行目标方法。环绕通知提供了最大的灵活性和控制力。 Around(execution(com.example.demo.aop.MyServiceImpl.performAction(…))“) public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println(“Before method execution”); Object result joinPoint.proceed(); // 继续执行目标方法 System.out.println(“After method execution”); return result; } 接下来我们来演示一下全部代码如下服务接口MyService.java: package com.example.demo.aop; public interface MyService { String performAction(String input); }服务实现MyServiceImpl.java:修改performAction方法使其在接收到特定输入时抛出异常 package com.example.demo.aop; import org.springframework.stereotype.Service; Service public class MyServiceImpl implements MyService { Override public String performAction(String input) { System.out.println(“Performing action with: input); if (“error”.equals(input)) { throw new RuntimeException(“Simulated error”); } return “Processed input; } }完整的切面类包含所有通知类型切面类MyAspect.java - 保持不变确保包含所有类型的通知 package com.example.demo.aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.; import org.springframework.stereotype.Component; Aspect Component public class MyAspect { Before(execution(com.example.demo.aop.MyService.performAction(…))“) public void beforeAdvice(JoinPoint joinPoint) { System.out.println(”Before: Before calling performAction); } After(execution(com.example.demo.aop.MyService.performAction(…))“) public void afterAdvice(JoinPoint joinPoint) { System.out.println(”After: After calling performAction); } AfterReturning(pointcut execution(com.example.demo.aop.MyService.performAction(…))“, returning “result”) public void afterReturningAdvice(JoinPoint joinPoint, Object result) { System.out.println(”AfterReturning: Method returned value is : result); } AfterThrowing(pointcut execution(com.example.demo.aop.MyService.performAction(…))“, throwing “ex”) public void afterThrowingAdvice(JoinPoint joinPoint, Throwable ex) { String methodName joinPoint.getSignature().getName(); System.out.println(”AfterThrowing: Exception in method: methodName ; Exception: ex.toString()); } Around(execution(com.example.demo.aop.MyService.performAction(…))”) public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println(”Around: Before method execution”); Object result null; try { result proceedingJoinPoint.proceed(); } catch (Throwable throwable) { // 如果执行方法出现异常打印这里 System.out.println(“Around: Exception in method execution”); throw throwable; } // 如果执行方法正常打印这里 System.out.println(“Around: After method execution”); return result; } } 这里要强调几点1.Around环绕通知常见用例是异常捕获和重新抛出。在这个例子中我们通过ProceedingJoinPoint的proceed()方法调用目标方法。如果目标方法执行成功记录执行后的消息并返回结果。如果在执行过程中发生异常在控制台上打印出异常信息然后重新抛出这个异常。这样做可以确保异常不会被吞没而是可以被上层调用者捕获或由其他异常通知处理。 2.AfterThrowing注解标明这个通知只有在目标方法因为异常而终止时才会执行。throwing属性指定了绑定到通知方法参数上的异常对象的名称。这样当异常发生时异常对象会被传递到afterThrowingAdvice方法中方法中可以对异常进行记录或处理。 3.AfterThrowing和AfterReturning通知不会在同一个方法调用中同时执行。这两个通知的触发条件是互斥的。AfterReturning通知只有在目标方法成功执行并正常返回后才会被触发这个通知可以访问方法的返回值。AfterThrowing通知只有在目标方法抛出异常时才会被触发这个通知可以访问抛出的异常对象。 4. 假设想要某个逻辑总是在方法返回时执行不管是抛出异常还是正常返回则考虑放在After或者Around通知里执行。配置类package com.example.demo.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; Configuration ComponentScan(“com.example”) EnableAspectJAutoProxy public class AppConfig { } 测试不同情况为了测试所有通知类型的触发在主类中执行performAction方法两次一次传入正常参数一次传入会导致异常的参数。主程序如下 package com.example.demo; import com.example.demo.aop.MyService; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class DemoApplication { public static void main(String[] args) { AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); MyService service context.getBean(MyService.class); try { // 正常情况 System.out.println(“Calling performAction with ‘test’”); service.performAction(“test”); // 异常情况 System.out.println( Calling performAction with ‘error’); service.performAction(“error”); } catch (Exception e) { System.out.println(Exception caught in DemoApplication: e.getMessage()); } } } 在这个例子中当performAction方法被第二次调用并传入error作为参数时将会抛出异常从而触发AfterThrowing通知。运行结果如下5. AOP时序图----------这里展示在Spring AOP框架中一个方法调用的典型处理流程包括不同类型的通知Advice的执行时机。1.客户端调用方法客户端Client发起对某个方法的调用。这个调用首先被AOP代理AOP Proxy接收这是因为在Spring AOP中代理负责在真实对象Target和外界之间进行中介。2.环绕通知开始 (Around)AOP代理首先调用切面Aspect中定义的环绕通知的开始部分。环绕通知可以在方法执行前后执行代码并且能决定是否继续执行方法或直接返回自定义结果。这里的“开始部分”通常包括方法执行前的逻辑。3.前置通知 (Before)在目标方法执行之前执行前置通知。这用于在方法执行前执行如日志记录、安全检查等操作。4.执行目标方法如果环绕通知和前置通知没有中断执行流程代理会调用目标对象Target的实际方法。5.方法完成方法执行完成后控制权返回到AOP代理。这里的“完成”可以是成功结束也可以是抛出异常。6.返回通知或异常通知返回通知 (AfterReturning)如果方法成功完成即没有抛出异常执行返回通知。这可以用来处理方法的返回值或进行某些后续操作。异常通知 (AfterThrowing)如果方法执行过程中抛出异常执行异常通知。这通常用于异常记录或进行异常处理。7.后置通知 (After)独立于方法执行结果后置通知总是会执行。这类似于在编程中的finally块常用于资源清理。8.环绕通知结束 (Around)在所有其他通知执行完毕后环绕通知的结束部分被执行。这可以用于执行清理工作或者在方法执行后修改返回值。9.返回结果最终AOP代理将处理的结果返回给客户端。这个结果可能是方法的返回值或者通过环绕通知修改后的值或者是异常通知中处理的结果。6. 实战手写JDK动态代理实现AOP---------------------首先定义一个接口和一个实现类 package com.example.demo.service; public interface UserService { void addUser(String user); } package com.example.demo.service.impl; import com.example.demo.service.UserService; public class UserServiceImpl implements UserService { Override public void addUser(String user) { System.out.println(Adding user: user); // 模拟异常 if (“error”.equals(user)) { throw new RuntimeException(“User error”); } } } 定义一个InvocationHandler来实现前置通知、后置通知、返回通知、异常通知和环绕通知 package com.example.demo.handler; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class UserServiceInvocationHandler implements InvocationHandler { private Object target; public UserServiceInvocationHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result null; try { // 环绕通知前半部分逻辑 System.out.println(Around before method: method.getName()); // 前置通知逻辑 System.out.println(Before method: method.getName()); // 执行目标方法 result method.invoke(target, args); // 返回通知逻辑 System.out.println(Returning from method: method.getName()); } catch (Exception e) { // 捕获目标方法抛出的实际异常 Throwable cause e.getCause(); // 异常通知逻辑 System.out.println(Exception in method: method.getName() , cause: cause); throw cause; } finally { // 后置通知逻辑 System.out.println(“After method: method.getName()); // 环绕通知后半部分逻辑 System.out.println(“Around after method: method.getName()); } return result; } } 创建动态代理实例并使用代理对象主程序如下 package com.example.demo; import com.example.demo.handler.UserServiceInvocationHandler; import com.example.demo.service.UserService; import com.example.demo.service.impl.UserServiceImpl; import java.lang.reflect.Proxy; public class DemoApplication { public static void main(String[] args) { // 创建目标对象 UserService target new UserServiceImpl(); // 创建InvocationHandler UserServiceInvocationHandler handler new UserServiceInvocationHandler(target); // 创建代理对象 UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); // 使用代理对象 proxy.addUser(“John Doe”); System.out.println(””); // 测试发生异常的情况 try { proxy.addUser(“error”); } catch (Exception e) { System.out.println(Caught exception: e.getMessage()); } } } 运行结果如下对比第4节可以通过日志打印顺序可以看出这和直接用注解实现是一样的顺序。 欢迎一键三连~ 有问题请留言大家一起探讨学习 ----------------------Talk is cheap, show me the code-----------------------
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门