浅谈aop【原创】
参数书籍《spring实战第四版》
何为aop
想起大三实习前去面试的时候总会 被问到:你了解spring框架吗?这个时候总按照固定格式回答,了解,spring两大核心,ioc,aop,面试官再问ioc和aop分别是什么?aop作用是干嘛的?这个时候又是固定格式,既然面试这么喜欢问,那咋今天就来聊一聊aop,以及使用aop实现一些具体的功能。
aop就是我们通常所所说的面向切面编程,举个最简单栗子,比如说你有个class A,class B,A里面有个方法a,B里面有个方法b,a调用了b,这个时候你想知道a给b传递了那些值,以及那些参数,简单办法直接在b里面加上代码打印输出完事,,但是这个时候你的项目负责人告诉你说这样不行,不能直接在写在方法b里面,而且还需要知道入参的名称以及类型巴拉巴拉一堆的要求,最后还要你记录到日志里面作为监控,这个时候咋办呢?莫慌,aop来救你脱离苦海。
下面做一个功能,获取接口中的入参参数
-
首先定义一个注解
这个没啥好说的,我直接上代码,有不明白的看下我的上一篇文章 自定义java注解
@Target(METHOD)
@Retention(RUNTIME)
public @interface MarkFunction {
}
上面的注解的作用就是用来标记方法的,也只能用来标记方法
-
定义一个切面类和切点
@Aspect
@Component
public class CheckParam {
@Pointcut("@annotation(注解全路径)")
public void point() {
}
}
- 上面@Aspect注解作用是表明这是一个切面类
- @Componet单纯的在项目启动的时候去加载这个bin
- point 方法在这里的作用就是定义一个切点,如果不采取这种方法就需要使用切点表达式,如果切面类里面切点的使用地方多的话比较麻烦,因此这里采用这种形式来定义统一切点
关于切点表达式
spring 的切面最小力度是方法,但是切点表达式可以指定是哪个方法,说白了就是切点表达式可以指定哪个方法只作为切入点
- execution
@Pointcut("execution(*com.lightkits.mes.domain.controller.AndonController.**(..))")
public void point1() {
}
首先这里表明的是在AndonController这个类里面的任意个参数的方法都是可以作为切入点的,也就是说个表达式匹配的是AndonControlle这里里面的所有的function(ps:特殊方法除外),当然在匹配到类之后你也可以匹配以某个自字母开头或者结束,或者返回接收参数个数等等具体语法如下
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
语法解释如下:
这里问号表示当前项可以有也可以没有,其中各项的语义如下:
modifiers-pattern:方法的可见性,如public,protected;
ret-type-pattern:方法的返回值类型,如int,void等;
declaring-type-pattern:方法所在类的全路径名,如com.spring.Aspect;
name-pattern:方法名类型,如buisinessService();
param-pattern:方法的参数类型,如java.lang.String;
throws-pattern:方法抛出的异常类型,如java.lang.Exception;
关于表达式中的通配符解释如下
*通配符,该通配符主要用于匹配单个单词,或者是以某个词为前缀或后缀的单词
如下示例表示返回值为任意类型,在com.spring.service.BusinessObject类中,并且参数个数为零的方法
execution(* com.spring.service.BusinessObject.*())
..通配符,该通配符表示0个或多个项,主要用于declaring-type-pattern和param-pattern中,如果用于declaring-type-pattern中,则表示匹配当前包及其子包,如果用于param-pattern中,则表示匹配0个或多个参数
如下示例表示匹配返回值为任意类型,并且是com.spring.service包及其子包下的任意类的名称为businessService的方法,而且该方法不能有任何参数
execution(* com.spring.service..*.businessService())
这里需要说明的是,包路径service..*.businessService()中的..应该理解为延续前面的service路径,表示到service路径为止,或者继续延续service路径,从而包括其子包路径;后面的*.businessService(),这里的*表示匹配一个单词,因为是在方法名前,因而表示匹配任意的类。
如下示例是使用..表示任意个数的参数的示例,需要注意,表示参数的时候可以在括号中事先指定某些类型的参数,而其余的参数则由..进行匹配:
execution(* com.spring.service.BusinessObject.businessService(java.lang.String,..))
- annotation
示例如下
@Pointcut("@annotation(com.lightkits.mes.domain.common.annotation.MarkFunction)")
public void point() {
}
这里就是说的是只要被@MarkFunction注解标注了的方法就是一个 切点,当然某些没法代理的方法标注了是不起作用的,记住一点就是如果某个function没有办法做代理,那么就没有办法做aop操作
- witch
@Pointcut("within(com.lightkits.mes.domain.controller.AndonController)")
public void point2(){
}
within表达式的粒度为类,其参数为全路径的类名(可使用通配符),表示匹配当前表达式的所有类都将被当前方法环绕。within表达式只能指定到类级别,通配符写法,表示的是controller下面所有类里面所有可以被代理的function
@Pointcut("within(com.lightkits.mes.domain.controller.*)")
public void point2(){
}
- args
@Pointcut("args(java.lang.String)")
public void point3(){
}
这里表示的是任何可被代理的方法中的参数有且只有一个,且参数类型是String就可以被作为切点。
当然那也可以有多个参数
@Pointcut("args(java.lang.String,..,java.util.List)")
public void point3(){
}
但这里通配符只能使用..,而不能使用*。如下是使用通配符的实例,该切点表达式将匹配第一个参数为java.lang.String,最后一个参数为java.util.List,并且中间可以有任意个数和类型参数的方法
- this和target
this和target需要放在一起进行讲解,主要目的是对其进行区别。this和target表达式中都只能指定类或者接口,在面向切面编程规范中,this表示匹配调用当前切点表达式所指代对象方法的对象,target表示匹配切点表达式指定类型的对象。比如有两个类A和B,并且A调用了B的某个方法,如果切点表达式为this(B),那么A的实例将会被匹配,也即其会被使用当前切点表达式的Advice环绕;如果这里切点表达式为target(B),那么B的实例也即被匹配,其将会被使用当前切点表达式的Advice环绕。
在讲解Spring中的this和target的使用之前,首先需要讲解一个概念:业务对象(目标对象)和代理对象。对于切面编程,有一个目标对象,也有一个代理对象,目标对象是我们声明的业务逻辑对象,而代理对象是使用切面逻辑对业务逻辑进行包裹之后生成的对象。如果使用的是Jdk动态代理,那么业务对象和代理对象将是两个对象,在调用代理对象逻辑时,其切面逻辑中会调用目标对象的逻辑;如果使用的是Cglib代理,由于是使用的子类进行切面逻辑织入的,那么只有一个对象,即织入了代理逻辑的业务类的子类对象,此时是不会生成业务类的对象的。
在Spring中,其对this的语义进行了改写,即如果当前对象生成的代理对象符合this指定的类型,那么就为其织入切面逻辑。简单的说就是,this将匹配代理对象为指定类型的类。target的语义则没有发生变化,即其将匹配业务对象为指定类型的类。如下是使用this和target表达式的简单示例
this(com.spring.service.BusinessObject)
target(com.spring.service.BusinessObject)
通过上面的讲解可以看出,this和target的使用区别其实不大,大部分情况下其使用效果是一样的,但其区别也还是有的。Spring使用的代理方式主要有两种:Jdk代理和Cglib代理(关于cjlb和jdk的区别这里推荐一篇文章代理模式实现方式及优缺点对比)。针对这两种代理类型,关于目标对象与代理对象,理解如下两点是非常重要的:
- 如果目标对象被代理的方法是其实现的某个接口的方法,那么将会使用Jdk代理生成代理对象,此时代理对象和目标对象是两个对象,并且都实现了该接口;
- 如果目标对象是一个类,并且其没有实现任意接口,那么将会使用Cglib代理生成代理对象,并且只会生成一个对象,即Cglib生成的代理类的对象。
结合上述两点说明,这里理解this和target的异同就相对比较简单了。我们这里分三种情况进行说明: - this(SomeInterface)或target(SomeInterface):这种情况下,无论是对于Jdk代理还是Cglib代理,其目标对象和代理对象都是实现SomeInterface接口的(Cglib生成的目标对象的子类也是实现了SomeInterface接口的),因而this和target语义都是符合的,此时这两个表达式的效果一样;
- this(SomeObject)或target(SomeObject),这里SomeObject没实现任何接口:这种情况下,Spring会使用Cglib代理生成SomeObject的代理类对象,由于代理类是SomeObject的子类,子类的对象也是符合SomeObject类型的,因而this将会被匹配,而对于target,由于目标对象本身就是SomeObject类型,因而这两个表达式的效果一样;
- this(SomeObject)或target(SomeObject),这里SomeObject实现了某个接口:对于这种情况,虽然表达式中指定的是一种具体的对象类型,但由于其实现了某个接口,因而Spring默认会使用Jdk代理为其生成代理对象,Jdk代理生成的代理对象与目标对象实现的是同一个接口,但代理对象与目标对象还是不同的对象,由于代理对象不是SomeObject类型的,因而此时是不符合this语义的,而由于目标对象就是SomeObject类型,因而target语义是符合的,此时this和target的效果就产生了区别;这里如果强制Spring使用Cglib代理,因而生成的代理对象都是SomeObject子类的对象,其是SomeObject类型的,因而this和target的语义都符合,其效果就是一致的
关于区别代码演示如下
// 目标类
public class Apple {
public void eat() {
System.out.println("Apple.eat method invoked.");
}
}
// 切面类
@Aspect
public class MyAspect {
@Around("this(com.business.Apple)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("this is before around advice");
Object result = pjp.proceed();
System.out.println("this is after around advice");
return result;
}
}
<!-- bean声明文件 -->
<bean id="apple" class="chapter7.eg1.Apple"/>
<bean id="aspect" class="chapter7.eg6.MyAspect"/>
<aop:aspectj-autoproxy/>\
// 驱动类
public class AspectApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Apple fruit = (Apple) context.getBean("apple");
fruit.eat();
}
}
执行结果如下
this is before around advice
Apple.eat method invoked.
this is after around advice
上述示例中,Apple没有实现任何接口,因而使用的是Cglib代理,this表达式会匹配Apple对象。这里将切点表达式更改为target,还是执行上述代码,会发现结果还是一样的
target(com.business.Apple)
public class Apple implements IApple {
public void eat() {
System.out.println("Apple.eat method invoked.");
}
}
public class AspectApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Fruit fruit = (Fruit) context.getBean("apple");
fruit.eat();
}
}
this 结果
Apple.eat method invoked.
对于target表达式
this is before around advice
Apple.eat method invoked.
this is after around advice
可以看到,这种情况下this和target表达式的执行结果是不一样的,这正好符合我们前面讲解的第三种情况
- 有witch 自然就有@witch
这里的@within表示匹配带有指定注解的类,其使用语法如下所示
表示匹配使用com.spring.annotation.BusinessAspect注解标注的类
@within(com.spring.annotation.BusinessAspect)
- @args
表示使用指定注解标注的类作为某个方法的参数时该方法将会被匹配
@args(annotation-type)
- @DeclareParents
@DeclareParents也称为Introduction(引入),表示为指定的目标类引入新的属性和方法。关于@DeclareParents的原理其实比较好理解,因为无论是Jdk代理还是Cglib代理,想要引入新的方法,只需要通过一定的方式将新声明的方法织入到代理类中即可,因为代理类都是新生成的类,因而织入过程也比较方便。如下是@DeclareParents的使用语法
@DeclareParents(value = "TargetType", defaultImpl = WeaverType.class)
private WeaverInterface attribute;
这里TargetType表示要织入的目标类型(带全路径),WeaverInterface中声明了要添加的方法,WeaverType中声明了要织入的方法的具体实现
9 . perthis和pertarget
在Spring AOP中,切面类的实例只有一个,比如前面我们一直使用的MyAspect类,假设我们使用的切面类需要具有某种状态,以适用某些特殊情况的使用,比如多线程环境,此时单例的切面类就不符合我们的要求了。在Spring AOP中,切面类默认都是单例的,但其还支持另外两种多例的切面实例的切面,即perthis和pertarget,需要注意的是perthis和pertarget都是使用在切面类的@Aspect注解中的。这里perthis和pertarget表达式中都是指定一个切面表达式,其语义与前面讲解的this和target非常的相似,perthis表示如果某个类的代理类符合其指定的切面表达式,那么就会为每个符合条件的目标类都声明一个切面实例;pertarget表示如果某个目标类符合其指定的切面表达式,那么就会为每个符合条件的类声明一个切面实例。从上面的语义可以看出,perthis和pertarget的含义是非常相似的。
语法
perthis(pointcut-expression)
pertarget(pointcut-expression)
相对来说 execution 和annotion用的比较多,其余的用的不怎么常见
-
使用切点
@Before("point()")
public void check(JoinPoint joinPoint){
这里可以写你想要执行的代码逻辑
}
这里的before注解表示的在调用切点之前进行切面逻辑代码操作
关于切面增强注解如下
@Before:该注解标注的方法在业务模块代码执行之前执行,其不能阻止业务模块的执行,除非抛出异常;
@AfterReturning:该注解标注的方法在业务模块代码执行之后执行;
@AfterThrowing:该注解标注的方法在业务模块抛出指定异常后执行;
@After:该注解标注的方法在所有的Advice执行完成后执行,无论业务模块是否抛出异常,类似于finally的作用;
@Around:该注解功能最为强大,其所标注的方法用于编写包裹业务模块执行的代码,其可以传入一个ProceedingJoinPoint用于调用业务模块的代码,无论是调用前逻辑还是调用后逻辑,都可以在该方法中编写,甚至其可以根据一定的条件而阻断业务模块的调用;
@DeclareParents:其是一种Introduction类型的模型,在属性声明上使用,主要用于为指定的业务模块添加新的接口和相应的实现。
@Aspect:严格来说,其不属于一种Advice,该注解主要用在类声明上,指明当前类是一个组织了切面逻辑的类,并且该注解中可以指定当前类是何种实例化方式,主要有三种:singleton、perthis和pertarget,具体的使用方式后面会进行讲解。
这里需要说明的是,@Before是业务逻辑执行前执行,与其对应的是@AfterReturning,而不是@After,@After是所有的切面逻辑执行完之后才会执行,无论是否抛出异常。
说下JoinPoint
如果切点是是一个function,在这里面可以拿到方法的入参参数以及一些其他东西
代码如下
// 获取入参参数列表
Object[] args = joinPoint.getArgs();
// 获取接口所有参数注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
其中涉及到java的反射以及其他的操作具体参考请看我的另外一篇文章java反射获取对象属性以及子对象属性
总结
aop非常的强大,虽然这东西大部分的开发不会去写,但是个人认为还是需要会,因为这个是非常的重要的基础知识,尤其是使用spring框架,使用这些可以减轻非常的工作量,但是用的不好会非常的麻烦,aop在实际开发中可以用来进行日志监控,可以使用环绕增强将code转换为对应的语言等等,aop结合java反射可以说是非常强大的组合