1.前言
开发中最常见就是使用Aspect完成AOP功能的实现,下面将简单使用@Aspect实现AOP的面向切面编程。关于Spring原生AOP的用法可参考之前的一篇博客文章 Spring5框架之AOP-ProxyFactory底层实现(五)
在使用AspectJ之前让我先温习一下几个比较重要的概念如下所示:
- before:在某个连接点之前执行程序逻辑。
- after returning:连接点正常后执行的程序逻辑,需要注意的是如果程序抛出异常该通知并不会执行。
- after throwing :当程序出现异常时候执行的程序逻辑。
- after:当连接点结束执行的程序逻辑(无论是否出现异常都会执行)
- around:spring中最强大的通知功能,它可以完成并实现上面4种功能的实现。
2.使用Aspect实现AOP功能
如果使用@Aspect注解需要在项目中支持引入下述Aspect的依赖:
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
下面我们依然使用Calculator这个计算器接口演示AspectJ方法的使用如下所示:
- 新增接口及其实现
首先AspectJ为我们提供了如下几个通知注解:
@Before
、 @AfterReturning
、 @After
、@Around
、 @AfterThrowing
接下来将使用一个简单的案例演示这几个注解的使用。
public interface Calculator {
int add(int a, int b);
int sub(int a, int b);
double divide(int a, int b);
}
@Service
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
@AdviceRequired
public int sub(int a, int b) {
return a - b;
}
@Override
public double divide(int a, int b) {
return a / b;
}
}
- 定义切面类
@Aspect
@Component
public class LogAspect {
/**
* 定义切点,其中execution定义切入点表达式
*/
@Pointcut(value = "execution(* *..aop..*(..))")
public void logPoint() {
}
/**
* 前置通知在方法执行前执行
*/
@Before(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))")
public static void logStart(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
// 拿到执行方法的源对象-----即切入表达式实际切入的地方并运行的地方
System.out.println(joinPoint.getTarget().getClass());
// System.out.println(joinPoint.getStaticPart()); 打印详细切入点表达式
System.out.println("LogAspect-普通通知方法@before:" + joinPoint.getSignature().getName() + "日志开始了....方法参数:" + Arrays.asList(args));
}
/**
* 方法执行后返回结果后执行该通知
* @param result
*/
@AfterReturning(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))", returning = "result")
public static void logRun(Object result) {
System.out.println("LogAspect-普通通知@AfterReturning" + "运行结果为:" + result);
}
/**
*
* 如果方法执行出现异常将执行此通知
*/
@AfterThrowing(value = "execution(public * com.codegeek.aop.day1.Calculator.*(..))", throwing = "e")
public static void logException(Exception e) {
System.out.println("LogAspect-普通通知@AfterThrowing出异常啦:" + e);
}
/**
* 后置通知切入点执行后
*/
@After(value = "execution(public * com.codegeek.aop.day1.Calculator.*(..))")
public void logEnd() {
System.out.println("LogAspect-普通通知@After日志结束了");
}
@Around(value = "logPoint()")
public Object logAround(ProceedingJoinPoint proceedingJoinPoint) {
Object proceed = null;
try {
// @Before
System.out.println("环绕前通知.....当前执行的方法:" + proceedingJoinPoint.getSignature().getName());
proceed = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
// @AfterReturn
System.out.println("环绕后通知.....");
} catch (Throwable throwable) {
// @AfterThrowing
System.out.println("环绕异常通知.......");
// 如果不抛出异常----则就将异常catch掉了,普通通知异常将无法感知到异常对象,所以认为是正常执行的
throw new RuntimeException(throwable);
} finally {
// @After
System.out.println("环绕结束通知.....");
}
return proceed;
}
}
在我们定义完切面类后有几个重要的点需要注意一下:
- 使用了@Component和@Aspect注解声明一个切面类对象,且可以被Spring IOC容器扫描到此对象。
- 使用了 @Pointcut注解定义了方法的切点,所谓的切点的作用就是决定通知能够在那些目标类的目标方法执行。
- @Before、@Around、@After、@AfterReturning、@AfterThrowing 中可以引入切点的值,亦可自定义切点的值
接下来我们还需要定义配置类以使Spring可以扫描到切面类并成功可以执行通知。
@Configuration
@ComponentScan(basePackages = {"com.codegeek.aop.day1"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AOPConfig {
}
- 测试方法:
需要注意的是在测试类上可以引入此配置类如下所示:
@Test
public void testAspect() {
// 配置切面类IOC就生成接口的代理类否则就是基本类
System.out.println();
Calculator bean = applicationContext.getBean(Calculator.class);
System.out.println(bean.add(1, 5));
System.out.println("-------------------");
}
这里我们使用了正常的方法测试其运行结果如下:
环绕前通知.....当前执行的方法:add
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:add日志开始了....方法参数:[1, 5]
环绕后通知.....
环绕结束通知.....
LogAspect-普通通知@After日志结束了
LogAspect-普通通知@AfterReturning运行结果为:6
6
-------------------
接下来我们测试一个异常的情况如下:
@Test
public void testAspect() {
// 配置切面类IOC就生成接口的代理类否则就是基本类
System.out.println();
Calculator bean = applicationContext.getBean(Calculator.class);
System.out.println(bean.add(1, 5));
System.out.println("-------------------");
System.out.println(bean.divide(5, 0));
System.out.println(bean);
System.out.println(bean.getClass());
}
运行测试方法后输出结果如下所示:
环绕前通知.....当前执行的方法:add
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:add日志开始了....方法参数:[1, 5]
环绕后通知.....
环绕结束通知.....
LogAspect-普通通知@After日志结束了
LogAspect-普通通知@AfterReturning运行结果为:6
6
-------------------
环绕前通知.....当前执行的方法:divide
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:divide日志开始了....方法参数:[5, 0]
环绕异常通知.......
环绕结束通知.....
LogAspect-普通通知@After日志结束了
LogAspect-普通通知@AfterThrowing出异常啦:java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
我们发现了异常通知执行了,但是程序运行正常情况下异常通知将不会执行,于此同时可以发现@AfterReturning注解通知并没有执行,它只有在程序运行正常下才会执行通知。关于通知执行顺序可以很形象表示如下:
[普通前置]
try {
环绕通知前置
环绕执行
环绕返回
} catch(Exception e) {
环绕异常通知
} finally {
环绕后置通知
}
[普通后置]
[普通方法返回/普通异常通知]
3.使用xml实现AOP实现
我们首先将LogAspect类上的@Aspect注解进行注释,然后新建立一个切面类OrderAspect如下:
@Component
//@Aspect
@Order(1)
public class OrderAspect {
// @Before(value = "execution(* *..aop..*(..))")
public static void logStart(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
// System.out.println(joinPoint.getStaticPart()); 打印详细切入点表达式
System.out.println("OrderAspect-普通通知方法@before:" + joinPoint.getSignature().getName() + "日志开始了....方法参数:" + Arrays.asList(args));
}
// @Pointcut(value = "execution(* *..aop..*(..))")
public void logPoint() {
}
// returning 告诉方法执行完毕后返回的值
// @AfterReturning(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))", returning = "result")
public static void logRun(Object result) {
System.out.println("OrderAspect-普通通知@AfterReturning" + "运行结果为:" + result);
}
//@AfterThrowing(value = "execution(public * com.codegeek.aop.day1.Calculator.*(..))", throwing = "e")
public static void logException(Exception e) {
System.out.println("OrderAspect-普通通知@AfterThrowing出异常啦:" + e);
}
// @After(value = "execution(public * com.codegeek.aop.day1.Calculator.*(..))")
public void logEnd() {
System.out.println("OrderAspect-普通通知@After日志结束了");
}
}
然后在xml中配置如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- false使用jdk代理否则使用CGlib-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<context:component-scan base-package="com.codegeek.aop.day1"/>
<aop:config>
<aop:pointcut id="logPoint" expression="execution(* *..aop..*(..))"/>
<aop:aspect id="myAspect" ref="orderAspect">
<aop:before method="logStart" pointcut-ref="logPoint" arg-names="joinPoint"/>
<aop:after method="logEnd" pointcut-ref="logPoint" />
<aop:after-returning method="logRun" pointcut-ref="logPoint" returning="result"/>
<aop:after-throwing method="logException" pointcut-ref="logPoint" throwing="e"/>
</aop:aspect>
</aop:config>
<bean id="orderAspect" class="com.codegeek.aop.day1.OrderAspect"/>
</beans>
我们再次运行测试方法如下所示:
@Test
public void testAspect() {
// 配置切面类IOC就生成接口的代理类否则就是基本类
System.out.println();
Calculator bean = applicationContext.getBean(Calculator.class);
System.out.println(bean.add(1, 5));
System.out.println("-------------------");
System.out.println(bean.divide(5, 0));
System.out.println(bean);
运行结果如下所示:
OrderAspect-普通通知方法@before:add日志开始了....方法参数:[1, 5]
OrderAspect-普通通知@After日志结束了
OrderAspect-普通通知@AfterReturning运行结果为:6
6
-------------------
OrderAspect-普通通知方法@before:divide日志开始了....方法参数:[5, 0]
OrderAspect-普通通知@After日志结束了
OrderAspect-普通通知@AfterThrowing出异常啦:java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
4. 如何选择AOP类型
在上面我们演示了一个基于Aspect风格注解的AOP实现,也看到了一个基于XML配置的AOP实现。这些受到多种因素的影响,例如程序需求、开发工具、开发团队对AOP熟悉的程度等等。由于Spring AOP与AspectJ都使用了相同的Aspect风格,如果有额外的需要AspectJ的功能需求,可以将现有的Aspect风格注解的代码迁移到Aspect上。