spring aop 与注解混合使用以及基本知识介绍(注重实用,简洁介绍,不废话)

1,146 阅读8分钟

对于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” 的组合就完成了。之后就可以在自己需要使用方法上加上这个日志,就可以实现自己想要的效果了。