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觉得不错的点个赞吧