对于spring aop,我们除了需要知道它的原理,更需要知道它的使用方法,以便更方便地帮助我们完成日常工作上的开发。
概述
aop 就是面向切面编程,优点不必细说,这里着重说明使用方法,不废话。把需要了解的东西简明扼要的叙述一遍,心里有个大概的结构和框架,以后不论看代码还是使用aop 都可以比较容易。
对于aop 的使用,首先要理解execution 表达式的意义,然后才是具体的使用。
execution 表达式
首先我们需要了解execution 表达式的语法,这个其实就是定位到我们要执行的切面逻辑的切点。
语法:execution(修饰符 返回值 包.类.方法名(参数) throws异常)
其中*代表一个任意类型的参数,而…代表零个或多个任意类型的参数
再举几个例子:
修饰符,一般省略
public -> 公共方法
* -> 任意
返回值,不能省略
void -> 返回没有值
String -> 返回值字符串
* -> 任意
包,[省略]
com.gyf.crm -> 固定包
com.gyf.crm.*.service -> crm包下面子包任意 (例如:com.gyf.crm.staff.service)
com.gyf.crm.. -> crm包下面的所有子包(含自己)
com.gyf.crm.*.service.. -> crm包下面任意子包,固定目录service,service目录任意包
类,[省略]
UserServiceImpl -> 指定类
*Impl -> 以Impl结尾
User* -> 以User开头
* -> 任意
方法名,不能省略
addUser -> 固定方法
add* -> 以add开头
*Do -> 以Do结尾
* -> 任意
(参数)
() -> 无参
(int) -> 一个整型
(int ,int) -> 两个
(..) -> 参数任意
throws ,可省略,一般不写。
这里举几个例子,一看就明白了
execution(* *..service..*.*(..))
第一个* 表示任意返回值,这里没有“修饰符”
第二个* 表示任意包,后面加了俩.. 任意包及其子包
service 后面的.. 表示service 当前包及子包
倒数第二个* 表示service 包及其子包种的任意类
括号前的* 表示任意方法
括号内的.. 表示任意参数
这个表达式最终表示的就是/所有包中的/service包/中的/所有类中的/所有方法,参数任意,返回值任意
execution(* *..*ServiceImpl.trans(..))
这个表达式表示/所有包中的/任意以ServiceImpl结尾/的类中的/trans()方法,其参数任意,返回值任意
execution(* *..galaxy.*ServiceImpl.trans(..))
这个表示/所有包中的/galaxy 包中的/以SerivceImpl结尾的/类的/trans()方法,参数任意,返回值任意。
当然,如果有个具体的方法,可以直接写,如:
execution(void com.free.galaxy.bbqServiceImpl.trans(int num))
定义切面及使用
spring 定义了五个注解,表示五种通知时间。
@Before/@Around/@After/@AfterReturning/@AfterThrowing
依照上代码,写好注释,看一遍就明白了
// 首先定义一个接口,里面有个方法,设想一个考试的场景,那么方法就是exam()
public interface BbqSerivce {
void exam();
}
// 然后创建一个切面类
@Aspect
public class Student {
// 考试之前,收拾好桌面
@Before("execution(* com.free.BbqServiceImpl.exam(..))")
public void clearUpTable() {
System.out.println("clear up table");
}
// 考完试,交卷
@AfterReturning("execution(* com.free.BbqServiceImpl.exam(..))")
public void handInExaminationPaper() {
System.out.println("hand in examination paper");
}
// 考试过程出问题,重新考试
@AfterThrowing("execution(* com.free.BbqServiceImpl.exam(..))")
public void reExamine() {
System.out.println("reexamine");
}
}
如上,定义了一个待切入的方法,和切面逻辑。方法执行后,各个切面逻辑就会执行,打印出相应的结果。
但是例子中的切面逻辑中各个切点的execution 表达式都是一致的,发现每个通知时间的注解后的内容都一样,在java 编程中,遇到这种重复性的代码,下意识的想法就是将其抽象出一个方法。对于切面来讲,可以使用@PointCut 来定义切点。
@Pointcut("execution(* com.free.BbqServiceImpl.exam(..))")
public void point(){
};
@AfterThrowing("point()")
public void reExamine() {
System.out.println("reexamine");
}
在上面的切面逻辑中,发现没有使用到@Around 环绕通知,
对于环绕通知,可以理解为是“环绕被通知的方法”,可以包裹被通知的目标方法。
需要注意的是,它接受ProceedingJoinPoint作为参数。这个参数是必须要有的,因为而我们需要在通知中通过它来调用被通知的方法。
在环绕通知方法中,可以写任何逻辑,当需要执行到被通知方法时,需要调用ProceedingJoinPoint的proceed()方法。
如果不调proceed()这个方法,那么通知实际上会阻塞对被通知方法的调用。
@Around("point()")
public void takeExam(ProceedingJoinPoint point)throws Throwable{
System.out.println("before exam");
point.proceed();
System.out.println("after exam");
System.out.println("finish exam");
}
以上是aop 的简单实用方法,可以记一下具体的应用框架,之后会介绍一些比较有意思的高级实用方法。
下面的内容是在之前内容的基础上,介绍一些aop 的切点表达式的其他写法,可以选择在合适的地方使用合适的方法。
(以下一些比较长长句子会使用“/”来分割,便于理解)
切点表达式的写法
execution
execution(修饰符 返回值 包.类.方法名(参数) throws异常)
在上篇文章中使用了execution 表达式,将某个接口(某个方法)作为切点进行讲解。
后续使用了@Pointcut 注解,对代码进行了规整。
除了这些写法,对于切点表达式,还有很多其他写法,下文找几个常用的做说明。
within
照例参考代码,一看就懂
within(com.free.service.home.*)
切点为包“com.free.service.home” 中的/任何类的/任何方法,不包含子包中的方法。
即/拦截/包中的任意方法,不包含子包中的方法
whitin(com.free.service..*)
对于包“com.free.service” /及子包中的/任意类的/任意方法都会被拦截,作为切点。
即/拦截包/或者子包中/定义的方法。
具体使用时,举例如:
@Before("whitin(com.free.service..*)")
args
参考代码及介绍如下
args(com.free.request.SelfInfo)
这种是用来匹配只有一个参数,且类型为“com.free.request.SelfInfo” 的所有方法
args(xxx.xxx.SelfInfo, xxx.xxx.YoursInfo, xxx.xxx.ThemselvesInfo)
用来匹配有多个参数,且类型为那三种的所有方法
args(com.free.request.SelfInfo, ..)
匹配第一个参数类型为“com.free.request.SelfInfo” 的所有方法。方法中其他类型的参数可以有0 个或者多个。其中".." 表示任意个参数。
具体使用举例:
@Around("args(com.free.request.SelfInfo)")
@target
使用这个表达式,是匹配一个类,这个类的目标对象/有一个指定的注解
@target(com.free.anno.MyAnnotation)
对于这个表达式的理解这个注解分为以下两点:
(1)目标对象中/包含/com.free.anno.MyAnnotation 注解
(2)调用/这个目标对象的/任意方法/都会被拦截
判断/被调用目标对象中,是否/声明了/注解MyAnnotation,如果有,会被拦截。
关注的是被调用的对象。
@within
这个表达式,匹配这种连接点,参考代码和介绍
@within(com.free.anno.MyAnnotation)
声明有com.free.anno.MyAnnotation 注解的类/这个类中的所有方法/都会被拦截
判断被调用的方法,所属的类中,是否声明了注解MyAnnotation,如果有,会被拦截。
关注的是调用的方法所在的类。
@annotation
这个表达式/是匹配/有指定注解的/方法
注意,目标是:方法
@annotation(com.free.anno.MyAnnotation)
这种是对于被调用的方法,需要包含指定的注解MyAnnotation,这种方法会被拦截。
@args 表达式
args 本身是代表“参数”。
这个表达式是/对于方法参数/所属的类型上/有指定的注解,则会被匹配。
注意,这个的使用/是对于/方法参数/所属的类型上/有指定的注解,不是方法参数中有注解
@args(com.free.anno.MyAnnotation)
匹配1个参数/同时第1个参数所属的类中/有MyAnnotation 注解/的方法
@args(com.free.anno.MyAnnotation,com.free.anno.YourAnnotation)
匹配n个参数,且这n 个参数所属的类型上都有指定的注解/的方法
@args(com.free.anno.MyAnnotation,..)
匹配多个参数,且第一个参数/所属的类中/有MyAnnotation 注解
对于@annotation @args 和@target 和@within,只接受注解类名作为入参。
注解 + AOP 实现业务逻辑
之前内容大概介绍了注解的相关概念,以及如何将注解和反射两种技术进行配合使用。
我们日常在使用spring 框架进行后端业务逻辑开发的时候,经常会使用到aop,而“注解+aop”又是一个很好的组合,我们可以使用这个组合来完成很多自定义切面逻辑。下文做具体的介绍。
关于aop 的简单使用在之前这篇文章在前面内容种有所介绍,接下来的重点放在如何通过注解+AOP 实现特定业务的开发。
这里只做一个简单的介绍,重点放在实现二者组合的步骤上,帮助理解并记忆这个步骤是这篇文章的主要目的。没有太复杂的技术。
注解+AOP 实现方法
我们都知道,注解其实就是一个“标记”,所以在“注解+aop”的组合之中,注解可以很完美充当aop 切点的角色。
在aop 一般不写具体的业务逻辑代码(类似增删改查那种),一般都是比如“打日志”、“权限校验”这种。
对于具体的使用介绍,话不多说,一边看代码一边介绍。
(1)首先自定义一个注解,模拟打日志的功能。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLogAnnotation {
}
这个注解的作用其实就是作为一个标记,注解作用范围在方法上(ElementType.METHOD),在运行期生效。
之后可以将此注解标记在某个方法上,以此来达到成为aop 切点的角色。
(2)接下来定义切面逻辑,上代码(使用System.out.println 模拟日志效果):
@Aspect
@Component
public class MyLogAnnotationAspect {
@Pointcut("@annotation(com.free.anno.MyLogAnnotation)")
public void pointCut() {
}
@Around("pointCut()")
public void addPoint(ProceedingJoinPoint point) throws Throwable {
//执行调用者类中的方法
System.out.println("方法执行开始");
point.proceed();
System.out.println("方法执行完成");
}
}
其中,切入点是自定义注解类的地址,然后使用了@Around 环绕通知,在方法执行前后分别打印开始和完成,达到日志输出的功能。这样一个简单的“注解+Spring AOP” 的组合就完成了。之后就可以在自己需要使用方法上加上这个日志,就可以实现自己想要的效果了。