注解版的springaop实操讲解(赋完整测试代码)

aop是个很强的东西,我们可以用来实现日志收集,鉴权,敏感词过滤等等功能。在说注解版的springaop使用之前,一些专业术语我用大白话来复述一遍,希望大家不要嫌弃。

  • 切面:切入点+通知
  • 连接点:目标对象中被增强的某个方法
  • 切点:连接点的集合
  • 目标对象:被增强的对象
  • 织入:把代理逻辑加入到目标对象的过程
    好了先来定义一个切面写法1:
 @Component
@Aspect
public class aspect1 {
    
    
    @Pointcut("execution(* com.zzh.service.*.*(..))")
    public void pointCut() {
    
    
    }

    @Before("pointCut()")
    public void a(JoinPoint joinPoint) throws Throwable {
    
    
        System.out.println("前置通知");
    }
}

写法二:
这俩个写法无非就是一气呵成与分步骤写而已的区别了,我个人推荐写法一,排查起来方便,切点还可以复用

@Component
@Aspect
public class aspect1 {
    
    
    
    @Before("execution(* com.zzh.service.*.*(..))")
    public void a(JoinPoint joinPoint) throws Throwable {
    
    
        System.out.println("前置通知");
    }

  
}

execution里面是我们的切入点表达式,表示哪些类要被增强
@Pointcut就是切点了,

启动类

注意这里要加@EnableAspectJAutoProxy注解,作用是开启动态代理,默认是cglib还是jdk忘了,等本文说到这再来调试

@EnableAspectJAutoProxy
@Configuration
@ComponentScan({
    
    "com.zzh"})
public class DemoApplication {
    
    

    public static void main(String[] args) {
    
    
        AnnotationConfigApplicationContext c = new 		AnnotationConfigApplicationContext(DemoApplication.class);
        IService bean = (IService)c.getBean("serviceImpl");
    
        bean.eat("肉");

    }

}

被增强的类

@Component
public class Service extends iService {
    
    
    public void eat(String food) {
    
    
        System.out.println("吃: " + food);
    }
}

这样一个基础的aop就搭建完成了。每次调用我们的service对象都会打印“前置通知这几个字了”
在这里插入图片描述基本的注解aop完了,接下来开始撸重点了。切面这里面有很大的学问呢,其中@before()这里面就可以加 this、whthin、@annotation、target,这些东西粗讲就是可以指定目标对象,细讲后文说。还可以加args、return等东西。而切面上面更是可以加@Aspect(“perthis(this(aop.a))”)、@Scope(“prototype”)。

this

作用:指定代理对象的类型,只有代理对象是我们指定的类型aop才会生效。在说这个之前先来把启动类改造一下,也就是把getBean的类型换成了ServiceImpl,诶这样一看好像是不是感觉没什么问题呢,拿的就是serviceImpl这个bean,类型转换成这样也没错吧。

@EnableAspectJAutoProxy
@Configuration
@ComponentScan({
    
    "com.zzh"})
public class DemoApplication {
    
    

    public static void main(String[] args) {
    
    
        AnnotationConfigApplicationContext c = new AnnotationConfigApplicationContext(DemoApplication.class);
        IService bean = (IService)c.getBean("serviceImpl");
        //ServiceImpl bean = (ServiceImpl)c.getBean("serviceImpl");
        bean.eat("肉");

    }

}

但是结果确是不能,说到这里还得提一下jdk代理底层的逻辑。为什么jdk动态代理是通过接口来实现而不是用继承的呢?假设a实现了接口b,我获取名字为a的bean这个ben取名叫il吧,在jdk代理下,我们的il extends proxy implents b,代理对象il已经继承了proxy,java中是单继承的,所以只能通过接口来实现目标方法咯,而且这里还衍生出一个问题,那就是我们获取到的il不能转换成a,只能转换成b或者proxy类型的。
正确写法:
IService bean = (IService)c.getBean(“serviceImpl”);
错误写法:
ServiceImpl bean = (ServiceImpl)c.getBean(“serviceImpl”);
在这里插入图片描述

this精讲

好了扯了这么多开始说this。一个demo带大家分析原因。启动类还是上面这个,下面这么写能不能使aop生效呢?

@Aspect
@Component
public class aspect2 {
    
    
    //代理对象为ServiceImpl类型aop才生效
    @Pointcut("this(com.zzh.service.bean1.ServiceImpl)")
    public void point() {
    
    
    }

    @Before("point()")
    public void test() {
    
    
        System.out.println("before");
    }
}

答案是不能,getBean(“serviceImpl”)这个代理对象只会等于IService与proxy,不会等于serviceImpl。但是我们的切面中是这么写的@Pointcut(“this(com.zzh.service.bean1.ServiceImpl)”)代理对象为ServiceImpl类型aop才生效

搞来搞去,头都晕了,那怎么才会aop生效呢,

方法一

那就是修改成这个咯,再看看this的作用,指定代理对象为***类型的aop才生效,是不是一下子就明白了this原来是这样用的啊。

@Aspect
@Component
public class aspect2 {
    
    
//    //代理对象为ServiceImpl类型aop才生效
//    @Pointcut("this(com.zzh.service.bean1.ServiceImpl)")
//    public void point() {
    
    
//    }
//    @Before("point()")
//    public void test() {
    
    
//        System.out.println("before");
//    }
    //代理对象为IService类型aop才生效
    @Pointcut("this(com.zzh.service.bean1.IService)")
    public void point2() {
    
    
    }

    @Before("point2()")
    public void test() {
    
    
        System.out.println("before");
    }
}

方法二开启cglib动态代理。

启动类改下这个标签@EnableAspectJAutoProxy(proxyTargetClass = true),就开启的是cglib代理了,因为cglib代理是通过继承目标对象来实现的。哦吼画画的baby,有的人修改后可能又会报下面这个错,这个错不详解,

在这里插入图片描述

解决办法

加上这个参数-noverify,跳过字节码的检查,然后就不会报错了,
在这里插入图片描述

target

现在来分析target,作用:指定目标对象的类型,只有目标对象是我们 指定的类型aop才会生效。还是同一个启动类,毫无疑问下面的写法aop都会生效,目标对象是啥,getBean(“serviceImpl”);双引号里面的这个就是目标对象,而serviceImpl实现了Iservice接口,所以aop都会生效。

//    //目标对象为IService类型aop才生效
//    @Pointcut("target(com.zzh.service.bean1.IService)")
//    public void point2() {
    
    
//    }
//
//    @Before("point2()")
//    public void test() {
    
    
//        System.out.println("before");
//    }


    //目标对象为ServiceImpl类型aop才生效
    @Pointcut("target(com.zzh.service.bean1.ServiceImpl)")
    public void point2() {
    
    
    }

    @Before("point2()")
    public void test() {
    
    
        System.out.println("before");
    }

@annotation

说完了我认为最难的this和target,接下来来点容易的吧,这个的作用就是:为指定的注解赋予aop的属性,以后只要是被这个注解标注的方法都会aop生效。说这个之前先说下自定义注解的使用吧。下面这个就是一个简单的自定义注解。

//表明这个注解只会在运行时才会生效
@Retention(RetentionPolicy.RUNTIME)
//表明这个注解是作用在方法上的
@Target(value = ElementType.METHOD)
public @interface BeforeCustom {
    
    
}

好了有了注解,那就来使用它吧。下面为注解注入灵魂

	//被此注解标注aop会生效
    @Pointcut("@annotation(com.zzh.annocation.BeforeCustom)")
    public void point2() {
    
    
    }

    @Before("point2()")
    public void test() {
    
    
        System.out.println("注解生效了");
    }

给注解分配工作的代码,给serviceImpl’中加个方法并且用我们的自定义注解标注

@Repository
public class ServiceImpl implements IService {
    
    

    public void eat(String food) {
    
    
        System.out.println("吃: " + food);
    }

    /**
     * @param
     * @method 自定义注解
     */
    @BeforeCustom
    public void methodA(String food) {
    
    
        System.out.println("吃: " + food);
    }
}

测试只需在启动类中调用bean.methodA(“肉”);
在这里插入图片描述

args

作用:就是精确级别到参数类型来匹配哪些方法能被aop作用。args中的映射类型从a()中的参数直接映射获取。

@Around("execution(* com.zzh.service.*.*(..))&&args(args1,args2)")
    public Object a( ProceedingJoinPoint proceedingJoinPoint,int args1,String args2) throws Throwable {
    
    
        Object proceed = proceedingJoinPoint.proceed();
        System.out.println("被代理的方法是:"+proceedingJoinPoint.getSignature().getName()+"的参数:" +proceedingJoinPoint.getArgs().toString()+"方法的返回值:"+proceed);
        return proceed;
    }

这段代码就是匹配com.zzh.service下的所有类的所有方法但是参数类型顺序依次为int,string类型的方法才会被aop生效。同时值得注意的是,我在测试中发现@Around只能仅与ProceedingJoinPoint配对才能使用否则报错。不太清除为什么。其实我感觉这些东西都和@Around有关,那干脆接着细聊下@Around好了

@Around

网上都是说环绕通知可以在方法的执行前后做点啥事情,那到底可以做啥事情呢?1:修改被代理方法的返回值。2:修改被代理方法的参数。3:一般被@Around这个通知标注的方法都带有返回值,且这个返回值就是被代理方法的返回值。如果你返回void,那么目标方法返回null,我们是搞代理,不是搞设计的,切记要加返回值。但是这里面还有需要注意的点,那就是我们的返回值的类型要>=目标方法的返回值的类型,不然类型转换不过来。4:就是在执行前后织入自己的逻辑。好了还是代码分析吧

  /**
     * @param
     * @method 带返回值的环绕通知
     */
    @Around("execution(* com.zzh.service.*.*(..))&&args(args1,args2)")
    public Object a(ProceedingJoinPoint proceedingJoinPoint, int args1, String args2) throws Throwable {
    
    
        //修改执行参数
        //Object[] objects = {111, "222"};
        //Object proceed = proceedingJoinPoint.proceed(objects);
        Object proceed = proceedingJoinPoint.proceed();
        for (int i = 0; i < proceedingJoinPoint.getArgs().length; i++) {
    
    
            System.out.println("被代理的方法是:" + proceedingJoinPoint.getSignature().getName() + "的参数:" + proceedingJoinPoint.getArgs()[i] + "方法的返回值:" + proceed);

        }
        return proceed;
    }

这个就不分析了给大家开下效果图,这是不修改参数的
在这里插入图片描述

在这里插入图片描述这个是修改参数的
在这里插入图片描述这个是修改返回值的
在这里插入图片描述到这@Around差不多应该差不多了吧,说到这还想提提@AfterReturning

@AfterReturning

一般与returning 连用字面意思就是方法执行后的通知,那么对应的连接点只能写JoinPoint而不是ProceedingJoinPoint,猜都猜到方法执行后的通知是不需要过程的,虽然看过aop源码但是不太清楚报错位置,可能看的不够仔细吧?直接代码分析

 /**
     * @param
     * @method 带返回值的AfterReturning,MyReturn就是方法执行之后的结果
     */
    @AfterReturning(value = "execution(* com.zzh.service.*.*(..))&&args(args1,args2)", returning = "MyReturn")
    public Object a(JoinPoint joinPoint, int args1, String args2, Object MyReturn) throws Throwable {
    
    
        System.out.println(MyReturn);
        return "返回值被修改了,嘿嘿你气不气";
    }

在这里插入图片描述都说是得到返回结果之后的通知,果然想试试修改目标方法的返回值还真的没用,但是与returning = "MyReturn"使用帅的一批,可以得到返回值。最后我好像还用过within,那就来说这个吧

within

我测试了一下,其实就是锁的粒度更大吧,直接到类aop都能生效,而execution锁的粒度更小,精确到参数级别,这个很简单就不再细讲了。接下来说下 perthis吧,
在这里插入图片描述

perthis

网上都说是啥生命周期啥的讲的不是很清楚,下面的话是我亲测出来的,可能与你以往的认知不同,管它呢本来有好多人写的博客就是有错的,但是我亲测的我就大胆的来说了,我就用大白话来说了,这个和this差不多,this是匹配符合条件代理对象使得通知生效,而这个就是匹配符合条件代理对象使得切面生效,且每次getbean都会生成一个全新的切面与之匹配。当然是这个bean是prototype的情况下,如果bean是单例的辣么无论get多少次这个bean,永远是同一个切面与之匹配。因而perthis与@Scope(“prototype”)是配套使用,不可拆散,下面直接代码来说
在这里插入图片描述

在这里插入图片描述在这里插入图片描述

在这里插入图片描述

在切面上加上perthis与@Scope(“prototype”),同时为service下的所有类开启aop,新建俩类MyPrototypea与MyPrototypeb俩个类差不多,我就只贴MyPrototypea的了,下面开始测试
在这里插入图片描述

由于我们是get MyPrototypea,且此时MyPrototypea是多例的,因此2次get都生成了一个全新的切面与之匹配,那么我们来接着试试get其他的bean吧,
在这里插入图片描述
我们发现切面压根都没有创建那么aop更就不会生效了,那如果把
MyPrototypea改成单例的呢?好的看下图
在这里插入图片描述
我们可以看到切面只有创建一次,好的说到这其实perthis差不多就说透了,其实与pertarget都是差不多的,就不说了额到这,好像应该差不多了吧,额还有类似于这种的的@within表示匹配带有指定注解的类。其实这种加@大部分都是匹配对应的注解的,额好像还有。@DeclareParents的使用。

@DeclareParents

作用为指定类,增加属性或者方法。代码来说,如果我们要为ImplPerson类植入别的方法可以这么写,如下图,这样就为ImplPerson类植入了Iservice中的全部方法且默认实现类为ServiceImpl
在这里插入图片描述效果
在这里插入图片描述

这里我摘抄它底层的一段源码,我也是研究aop源码时偶然发现的

 DeclareParents declareParents = (DeclareParents)introductionField.getAnnotation(DeclareParents.class);
        if (declareParents == null) {
    
    
            return null;
        } else if (DeclareParents.class == declareParents.defaultImpl()) {
    
    
            throw new IllegalStateException("'defaultImpl' attribute must be set on DeclareParents");
        } else {
    
    
            return new DeclareParentsAdvisor(introductionField.getType(), declareParents.value(), declareParents.defaultImpl());
        }

好了大概的东西应该都有涉及到,下面是我测试用的代码完整链接https://github.com/zhangzihang3/zzhSpringAop.git觉得不错的点个赞吧

猜你喜欢

转载自blog.csdn.net/qq_42875345/article/details/108429247
今日推荐