Spring中的5种Aop常见应用方式(扫描注解方式)

转载于:https://zhuanlan.zhihu.com/p/103236714

 

提到Aop,不得不提的那就是动态代理;关于动态代理,可以参考前面写过的文章

加耀:浅谈动态代理​zhuanlan.zhihu.com图标

一个完整的AOP是由多个元素组成的,AOP由切面、切点、连接点、目标对象、回调 五个元素构成;就好比

aspect:切面,通俗的讲可以理解为一个功能,比如具备某项能力(如:帮助他人是一种能力)),定义为一个切面;
pointCut:切点,可以理解为一种匹配规则,比如哪些人需要被帮助,通过一些规则进行分组筛选;
Target Object:目标对象,比如某种能力需要对某个人使用,这个某个人就是目标对象;
joinpoint:连接点,具体的需要做的事情,可以理解为需要使用某项能力帮助某人做什么事情的时候提供帮助;这个做什么事情就是连接点了;
Advice:回调的通知,比如:在什么时间点去帮助他们,在什么时间点提供某种能力帮助别人;

 

AOP在Spring框架中应用非常广泛,我们平时最常见的事务,就是使用AOP来实现的;在方法前开启事务,方法后提交事务,检测到有异常时,回滚事务...

在Spring中的AOP有6种增强方式,分别是:

1、Before 前置增强

2、After 后置增强

3、Around 环绕增强

4、AfterReturning 最终增强

5、AfterThrowing 异常增强

6、DeclareParents 引入增强

 

前面的5种增强方式相信很多人都是已经应用过的,使用代码进行演示,新建一个SpringBoot工程方便演示

 

创建服务类CustomerService

package com.aop.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class CustomerService {
    /**
     * 添加收货地址
     */
    public void addCustomer(Long customerId, String userName, String address) {
        log.info("调用成功addCustomer,当前请求参数customerId={},userName={},address={}", customerId, userName, address);
    }
}

 

测试类主方法

package com.aop;

import com.aop.service.CustomerService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class SpringAopApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringAopApplication.class, args);
        CustomerService customerService = (CustomerService) context.getBean("customerService");
        customerService.addCustomer(1234L, "冯宝宝", "一人之下");
    }
}

 

如上所示,当我们启动项目时,当IOC容器加载完毕后,从容器中获取刚刚创建的服务类,然后调用服务类的业务方法;此时代码是可以完好运行的;

 

 

 

 

测试前置增强

如果需要在执行方法addCustomer之前,我们进行一些其它的业务操作,比如校验参数是否为空,这时候就可以使用前置增强Before来实现了;获取请求参数信息的代码如下

public class AopHelper {
    /**
     *  获取请求方法的参数
     * @param joinPoint
     */
    public  static Map getMethodParams(JoinPoint joinPoint){
        String[] names = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        Map params =new HashMap();
        if (ArrayUtils.isEmpty(names)) return params;
        Object[] values = joinPoint.getArgs();
        for (int i = 0; i < names.length; i++) {
            params.put(names[i],values[i]);
        }
        return params;
    }
}

 

 

定义一个AOP,在AOP中配置前置增强及拦截规则

@Slf4j
@Aspect
@Component
public class MyAop {
    /**
     * 测试前置增强,测试参数非空校验,此方法还可完善为携带有注定注解的参数则校验非空校验,不携带则不校验
     * 测试通过 正则匹配 的方式使用AOP
     */
    @Before(value ="execution(public * com.aop.service.*.*(..))")
    public void before1(JoinPoint joinPoint){
        Map params =  AopHelper.getMethodParams(joinPoint);
        params.forEach((key,value)->{
            if (Objects.isNull(value)) throw new RuntimeException("参数" + key + "不能为空");
        });
        log.info("【测试前置增强:】"+joinPoint.getTarget().getClass().getName()+"."+joinPoint.getSignature().getName());
    }
}

 

在上述代码中,定义好一个前置增强,并且为它配置一定的拦截规则,使其可以拦截到我们的业务方法;

 

修改测试程序方法参数如下:

public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(SpringAopApplication.class, args);
    CustomerService customerService = (CustomerService) context.getBean("customerService");
    customerService.addCustomer(1234L, "冯宝宝", "一人之下");
    customerService.addCustomer(1234L, "冯宝宝", null);
}

 

运行程序后,在控制台输出如下:

2020-01-16 14:53:51.197  INFO 13028 --- [           main] com.aop.conf.MyAop                       : 【测试前置增强:】com.aop.service.CustomerService.addCustomer
2020-01-16 14:53:51.212  INFO 13028 --- [           main] com.aop.service.CustomerService          : 调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
Exception in thread "main" java.lang.RuntimeException: 参数address不能为空
at com.aop.conf.MyAop.lambda$before1$0(MyAop.java:51)
at java.util.HashMap.forEach(HashMap.java:1289)

 

通过控制台输出内容可以看到,方法addCustomer在所有的参数都有值时,可以正常执行,但是如果有参数的值是空的,此时就会抛出异常;结合这个实例,其实还可以换一种方式去实现,比如可以自定义一个注解,作用范围在形参上,被注解标记的参数不能为空,这样在AOP里就可以只判断必传参数即可;

 

 

后置增强

在上面我们实现了前置增强的应用,接下来我们看一下后置增强的应用,可以在方法运行之后进行一些逻辑处理,比如打印一个日志告知当前方法运行成功;在MyAop这个AOP类中,新增一个后置增强的代码

/**
 * 测试后置增强,测试使用target指定目标对象
 */
@After(value ="target(com.aop.service.CustomerService)")
public void after(JoinPoint joinPoint){
    log.info("后置增强AfterAop测试指定目标匹配 " + joinPoint.getTarget().getClass().getName()+"."+joinPoint.getSignature().getName() + "调用成功");
}

 

 

此时,在MyAop程序中,有两种增强方式,分别是前置增强和后置增强;然后执行程序后在控制台可以看到输出内容如下所示:

【测试前置增强:】com.aop.service.CustomerService.addCustomer
 调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
 后置增强AfterAop测试指定目标匹配 com.aop.service.CustomerService.addCustomer调用成功

 

可以看到,程序在运行时,先调用了前置增强,然后执行业务方法,后面再执行了后置增强;而此时,一旦在执行业务方法的时候抛出异常了,后置增强还是会被执行的;比如在业务方法中添加一个异常,然后运行程序

/**
 * 测试添加异常,核对后置增强是否被触发
 */
public void addCustomer(Long customerId, String userName, String address) {
    log.info("调用成功addCustomer,当前请求参数customerId={},userName={},address={}", customerId, userName, address);
    int a = 10 / 0;
}

----- 控制台输出如下:
2020-01-17 09:29:48.699  INFO 4324 --- [           main] com.aop.service.CustomerService          : 调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
Exception in thread "main" java.lang.ArithmeticException: / by zero
2020-01-17 09:29:51.792  INFO 4324 --- [           main] com.aop.conf.MyAop                       : 后置增强AfterAop测试指定目标匹配 com.aop.service.CustomerService.addCustomer调用成功
at com.aop.service.CustomerService.addCustomer(CustomerService.java:28)

 

从控制台可以看到,即使程序抛出异常了,后置增强一样会执行的;

 

 

 

环绕增强

环绕增强是在调用业务方法之前和调用业务方法之后都可以执行响应的增强语法,也就是说:一个前置增强和一个后置增强相当于是组成了一个环绕增强;不同的是,在前置增强和后置增强中,在AOP中前置增强和后置增强只能拿到JoinPoint类,而在环绕增强中,可以拿到ProceedingJoinPoint类;

 

ProceedingJoinPoint类继承了JoinPoint类,也继承了JoinPoint所有的非私有的方法;比如获取连接点相关信息、获取参数信息、获取方法等等,并且ProceedingJoinPoint类扩展了JoinPoint类的方法,ProceedingJoinPoint可以调用业务方法执行业务逻辑,而JoinPoint则不可以;

 

也就是在环绕增强中,可以执行业务方法,而在前置增强和后置增强中则不可以;这里可以通过环绕增强实现数据库事务的实现,也可以通过环绕增强实现程序运行时间的记录;

 

新建一个注解RunTimeLog,测试使用注解的方法建立匹配AOP规则

/**
 * @demand: 定义记录方法执行时间的注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RunTimeLog {
}

 

在MyAop中新增方法

/**
 * 测试环绕增强,环绕增强参数可以为ProceedingJoinPoint,可以调用业务方法
 * 通过注解的形式进行AOP测试
 */
@Around("@annotation(com.aop.conf.RunTimeLog)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    log.info("【进入环绕增强】触发环绕那个增强开始");
    long startTime = System.currentTimeMillis();
    Object result = null;
    // 通过代理类调用业务逻辑执行
    result = pjp.proceed();
    log.info("【测试环绕增强:】当前方法{}执行成功,调用者为:{},  此方法运行时间为:{} ms", pjp.getSignature().getName(), pjp.getTarget().getClass().getName(), (System.currentTimeMillis() - startTime));
    return result;
}

 

在类CustomerService的addCustomer方法上添加刚刚定义的注解@RunTimeLog,去掉业务方法中的10/0后再次运行程序;通过控制台输出可以观察到:

【进入环绕增强】触发环绕那个增强开始
【测试前置增强:】com.aop.service.CustomerService.addCustomer
 调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
【测试环绕增强:】当前方法addCustomer执行成功,调用者为:com.aop.service.CustomerService,  此方法运行时间为:15 ms
 后置增强AfterAop测试指定目标匹配 com.aop.service.CustomerService.addCustomer调用成功

 

由此可以看出,在同一个AOP中,前置增强和后置增强以及环绕增强的先后顺序为:

Around --> Before --> Around --> After

 

 

 

 

最终增强

关于最终增强,这个和后置增强有一点类似,都是在业务方法执行后执行,不过两者还是有差异的;通过代码案例测试一下,在MyAop这个类中新增一个方法,

/**
 * 测试最终增强,
 * @param joinPoint
 */
@AfterReturning("execution(public * com.aop.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint){
    log.info("测试最终增强" + joinPoint.getTarget().getClass().getName()+"."+joinPoint.getSignature().getName());
}

 

运行程序后,输出如下所示:

【进入环绕增强】触发环绕那个增强开始
【测试前置增强:】com.aop.service.CustomerService.addCustomer
调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
【测试环绕增强:】当前方法addCustomer执行成功,调用者为:com.aop.service.CustomerService,  此方法运行时间为:23 ms
后置增强AfterAop测试指定目标匹配 com.aop.service.CustomerService.addCustomer调用成功
测试最终增强com.aop.service.CustomerService.addCustomer

 

通过控制台输出的顺序,可以看出,最终增强的顺序比后置增强要小,也就是:Around --> Before --> Around --> After --> AfterReturning

 

但是,但从这里的控制台输出,好像并不能体现出后置增强与最终增强的差别在哪;此时,如果修改一下程序的业务方法

@RunTimeLog
public void addCustomer(Long customerId, String userName, String address) {
    log.info("调用成功addCustomer,当前请求参数customerId={},userName={},address={}", customerId, userName, address);
    int a = 10 /0;
}

 

在程序中,添加一个10/0,这样的话程序会抛出异常,再次运行程序在控制台可以观察到

【进入环绕增强】触发环绕那个增强开始
【测试前置增强:】com.aop.service.CustomerService.addCustomer
调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
Exception in thread "main" java.lang.ArithmeticException: / by zero
后置增强AfterAop测试指定目标匹配 com.aop.service.CustomerService.addCustomer调用成功
	at com.aop.service.CustomerService.addCustomer(CustomerService.java:29)
	at com.aop.service.CustomerService$$FastClassBySpringCGLIB$$ef7074c5.invoke(<generated>)

 

此时,可以观察到,程序进入业务方法后,有抛出异常,然后后置增强还是正常执行了,不过,此时最终增强是没有被触发的;这就是两者的区别之一;如果用代码来描述这种关系,更多的有点类似下面这种

// 前置增强
try {
    // TODO 业务方法
    // 最终增强
}catch (Exception e){
    e.printStackTrace();
    // 异常增强
}finally {
    // 后置增强
}

 

最终增强暂时还没有想到在哪些地方有应用场景;

 

 

 

异常增强

异常增强即在业务方法调用是程序出现异常并且异常在没有被捕获的前提下所触发的,我们可以使用最终增强来记录程序的错误日志,以便于我们进行排错等;

 

在MyAop类中新增方法

/**
 *  测试异常增强,
 */
@AfterThrowing(value = "execution(public * com.aop.service.*.*(..))", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
    Map params =  AopHelper.getMethodParams(joinPoint);
    log.info("触发异常增强,当前程序抛出异常的方法是:" + joinPoint.getSignature()
            + ", 请求参数为:" + params.toString() + ",异常信息为:" + e.getMessage());
    // TODO 后面可执行入库、入ELK、入mongoDB等等
}

 

程序后可看到

【进入环绕增强】触发环绕那个增强开始
【测试前置增强:】com.aop.service.CustomerService.addCustomer
 调用成功addCustomer,当前请求参数customerId=1234,userName=冯宝宝,address=一人之下
Exception in thread "main" java.lang.ArithmeticException: / by zero
	at com.aop.service.CustomerService.addCustomer(CustomerService.java:29)
后置增强AfterAop测试指定目标匹配 com.aop.service.CustomerService.addCustomer调用成功
	at com.aop.service.CustomerService$$FastClassBySpringCGLIB$$ef7074c5.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
触发异常增强,当前程序抛出异常的方法是:String com.aop.service.CustomerService.addCustomer(Long,String,String), 请求参数为:{address=一人之下, customerId=1234, userName=冯宝宝},异常信息为:/ by zero
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769)

 

异常增强被触发,可以获取到当前请求的参数信息、时间信息等,记录到数据库或者是ELK等,以便于排错使用,并且这些逻辑和业务代码是分开的互不影响,很大程度上的解除代码耦合;

 

同时,当业务方法中没有异常信息时,则不会触发异常增强;从这里可以看出在同一个AOP中的执行顺序 Around --> Before --> Around --> After --> AfterReturning --> AfterThrowing

 

 

 

 

引入增强

引入增强参考网上的案例,尝试了很多种,最终都被抛出类型转换异常;感兴趣的朋友可以网上查询一下@DeclareParents注解,其本意是可以实现 一个Java类,没有实现A接口,在不修改Java类的情况下,使其具备A接口的功能。

 

 

 

 

 

 

pointCut 表达式匹配规则

在上述的代码中,有通过好几种匹配规则去匹配应该拦截哪些类的哪些方法,有使用过execution,也有使用过annotation,还有使用过target等方式进行匹配,这些匹配规则统称为pointCut表达式;pointCut表达式有很多种,其中用的比较多的有execution、annotation、target、args等;

 

简单描述一下常用的这几种表达式的常见使用形式

1、execution 匹配方法签名需要满足execution中描述的方法签名,例如

1、public * *(..) 
任何公共方法的执行
​
2、* cn.javass..IPointcutService.*()
cn.javass包及所有子包下IPointcutService接口中的任何无参方法
​
3、* cn.javass..IPointcutService.*(*)
cn.javass包及所有子包下IPointcutService接口的任何只有一个参数方法    
​
4、* (!cn.javass..IPointcutService+).*(..)
非“cn.javass包及所有子包下IPointcutService接口及子类型”的任何方法
​
5、* cn.javass..IPointcutService+.*()
cn.javass包及所有子包下IPointcutService接口及子类型的的任何无参方法
​
6、* cn.javass..IPointcut*.test*(java.util.Date)
    cn.javass包及所有子包下IPointcut前缀类型的的以test开头的只有一个参数类型为java.util.Date的方法,注意该匹配是根据方法签名的参数类型进行匹配的,而不是根据执行时传入的参数类型决定的
如定义方法:public void test(Object obj);即使执行时传入java.util.Date,也不会匹配的;
​
7、* cn.javass..IPointcut*.test*(..)  throws llegalArgumentException, ArrayIndexOutOfBoundsException
cn.javass包及所有子包下IPointcut前缀类型的的任何方法,且抛出IllegalArgumentException和ArrayIndexOutOfBoundsException异常
​
8、* (cn.javass..IPointcutService+&& java.io.Serializable+).*(..)
任何实现了cn.javass包及所有子包下IPointcutService接口和java.io.Serializable接口的类型的任何方法
​
9、@java.lang.Deprecated * *(..)
任何持有@java.lang.Deprecated注解的方法
​
10、@java.lang.Deprecated @cn.javass..Secure  * *(..)
任何持有@java.lang.Deprecated和@cn.javass..Secure注解的方法
​
11、@(java.lang.Deprecated || cn.javass..Secure) * *(..)
任何持有@java.lang.Deprecated或@ cn.javass..Secure注解的方法
​
12、(@cn.javass..Secure  *)  *(..)
任何返回值类型持有@cn.javass..Secure的方法
​
13、*  (@cn.javass..Secure *).*(..)
 任何定义方法的类型持有@cn.javass..Secure的方法
 
14、* *(@cn.javass..Secure (*) , @cn.javass..Secure (*))
任何签名带有两个参数的方法,且这个两个参数都被@ Secure标记了。
如public void test(@Secure String str1,@Secure String str1);
​
15、* *((@ cn.javass..Secure *))或* *(@ cn.javass..Secure *)
任何带有一个参数的方法,且该参数类型持有@ cn.javass..Secure;
如public void test(Model model);且Model类上持有@Secure注解
​
16、* *(@cn.javass..Secure (@cn.javass..Secure *) ,@ cn.javass..Secure (@cn.javass..Secure *))
任何带有两个参数的方法,且这两个参数都被@ cn.javass..Secure标记了;且这两个参数的类型上都持有@ cn.javass..Secure;
​
17、* *(java.util.Map<cn.javass..Model, cn.javass..Model>, ..)
任何带有一个java.util.Map参数的方法,且该参数类型是以< cn.javass..Model, cn.javass..Model >为泛型参数;注意只匹配第一个参数为java.util.Map,不包括子类型;
如public void test(HashMap<Model, Model> map, String str);将不匹配,必须使用“* *(java.util.HashMap<cn.javass..Model,cn.javass..Model>, ..)”进行匹配;
而public void test(Map map, int i);也将不匹配,因为泛型参数不匹配
​
18、* *(java.util.Collection<@cn.javass..Secure *>)
任何带有一个参数(类型为java.util.Collection)的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass..Secure注解;
如public void test(Collection<Model> collection);Model类型上持有@cn.javass..Secure

 

2、target 匹配目标对象(非代理实例)的类型满足target 中的描述的类型,表达式类型为全限定名,不支持通配符。例如

@Before(value = "target(org.aop.UserService)")
    public void testTarget(){
        System.out.println("target 规则命中");
}

 

3、@annotation 匹配方法上包含@MethodAnnotation注解的方法。例如

@Before(value = "@annotation(org.aop.MethodAnnotation)")
	public void testMthodAnnotation(){
		System.out.println("@annotation 规则命中");
}

 

4、args 匹配方法的参数满参数类型为args中描述的类型,同时也用于接收目标方法的参数。

// 匹配方法参数为String,Integer 所有方法。
@Before(value = "args(java.lang.String,java.lang.Integer)")
    public void testArgs(){
        System.out.println("args 规则命中");
}

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值