Spring AOP 面向切面编程

       AOP全称Aspect Oriented Programming。在OOP
(面向对象程序设计)中,正是这种分散在各处且与对象核心功能无关的代码(横切代码)的存在,使得模块复用难度增加。AOP则将封装好的对象剖开,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),切面将那些与业务无关,却被业务模块共同调用的逻辑提取并封装起来,减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

       AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

一 AOP编程相关名词

  • 切面(Aspect):在代码体系中,对象与对象之间,方法与方法之间,模块与模块之间都可以认为是一个个的切面。Spring中通过@Aspect:来描述一个切面。

  • 连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是表示一个方法的执行。

  • 通知(Advice):在切面的某个特定的连接点上执行的动作。其中包括了“around”、“before”和“after”等不同类型的通知。许多AOP框架(包括Spring)都是以拦截器做通知模型,并维护一个以连接点为中心的拦截器链。

  • 切入点(Pointcut):定义在什么时候切人方法。匹配连接点的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。Spring里面通过@Pointcut来引入切入点。

  • 引入(Introduction):用来给一个类型声明额外的方法或属性(也被称为连接类型声明(inter-type declaration))。Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用引入来使一个bean实现IsModified接口,以便简化缓存机制。

  • 目标对象(Target Object):被一个或者多个切面所通知的对象。也被称做被通知(advised)对象。既然Spring AOP是通过运行时代理实现的,这个对象永远是一个被代理(proxied)对象。

  • AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(例如通知方法执行等等)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

  • 织入(Weaving):把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

二 连接点(Joinpoint)

       连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时,抛出异常时,甚至是修改一个字段时,切面代码可以利用这些连接点插入到应用的正常流程中,并添加新的行为,如日志、安全、事务、缓存等。具体代码中,连接点体现在每个通知方法的参数中。
比如如下前置@Before通知方法的第一个参数就是连接点,通过连接点我们可以获取到一些上下文的信息。

    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

       @Before、@After、@AfterReturning、@AfterThrowing都是使用的org.aspectj.lang.JoinPoint接口表示目标类连接点对象,@Around使用org.aspectj.lang.ProceedingJoinPoint表示连接点对象。两个接口里面的方法也不复杂。主要方法如下。

JoinPoint接口主要方法如下

public interface JoinPoint {

    /**
     * 获取代理对象本身
     */
    Object getThis();

    /**
     * 获取连接点所在的目标对象

     */
    Object getTarget();

    /**
     * 获取连接点方法运行时的入参列表
     */
    Object[] getArgs();

    /** 获取连接点的方法签名对象,进而可以获取方法的名字,方法修饰符这些
     */
    Signature getSignature();

    /**
     * 获取连接点方法在文件中的信息,比如文件中第几行啥的
     */
    SourceLocation getSourceLocation();

    /** 获取连接点的方法的类型
     */
    String getKind();

    /**
     * 获取封装连接点的一个对象
     */
    JoinPoint.StaticPart getStaticPart();


}

ProceedingJoinPoint主要方法如下,ProceedingJoinPoint继承自JoinPoint

public interface ProceedingJoinPoint extends JoinPoint {

    /**
     * 这个函数咱们不能直接调用,不管他
     */
    void set$AroundClosure(AroundClosure arc);

    /**
     * P通过反射执行目标对象的连接点处的方法
     */
    public Object proceed() throws Throwable;

    /**
     * 通过反射执行目标对象连接点处的方法,不过使用新的入参替换原来的入参
     */
    public Object proceed(Object[] args) throws Throwable;
}

三 通知(Advice)

       通知定义了切面是什么以及何时调用,何时调用。通知包含以下几种:

Advice Spring注解 解释
Before @Before 前置通知,在方法被调用之前调用
After @After 最终通知, 在方法完成之后调用,无论方法执行是否成功
After-returning @AfterReturning 后置通知,在方法成功执行之后调用
After-throwing @AfterThrowing 异常通知,在方法抛出异常后调用
Around @Around 环绕通知, 包围一个连接点的通知

       @Before、@After、@AfterReturning、@AfterThrowing这几个通知应该都还好理解。就@Around稍稍复杂一点。

       环绕通知:包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。

       环绕通知最麻烦,也最强大,其是一个对方法的环绕,具体方法会通过代理传递到切面中去,切面中可选择执行方法与否,执行方法几次等。

       环绕通知使用一个代理ProceedingJoinPoint类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJoinPoint类型,在通知体内,调用ProceedingJoinPoint的proceed()方法会导致后台的连接点方法执行。proceed 方法也可能会被调用并且传入一个Object[]对象-该数组中的值将被作为方法执行时的参数。

四 切点(Pointcut)

       切点定义了何处,切点的定义会匹配通知所要织入的一个或多个连接点,我们通常使用明确的类的方法名称来指定这些切点,或是利用正则表达式定义匹配的类和方法名称来指定这些切点。

       切点(Pointcut)的使用关键在切点表达式,一般由下列方式来定义或者通过 &&、 ||、 !、 的方式进行组合:

切入点表达式指示符 解释
execution 匹配子表达式(匹配方法执行)
within 匹配连接点所在的Java类或者包
this 用于向通知方法中传入代理对象的引用
target 用于向通知方法中传入目标对象的引用
args 用于将参数传入到通知方法中
@within 匹配在类一级使用了参数确定的注解的类,其所有方法都将被匹配(在类上添加注解)
@target 和@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME
@args 传入连接点的对象对应的Java类必须被@args指定的Annotation注解标注
@annotation 匹配当前执行方法持有指定注解的方法

咱们可以简单的认为,通知(Advice)定义了什么时候调用,切点(Pointcut)定义了哪个地方。一个指定了when,另一个指定了where。

4.1 execution

       "execution(方法表达式)":匹配方法执行的连接点。

       execution方法表达式语法如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern.?name-pattern(param-pattern)throws-pattern?) 
  • 修饰符匹配(modifier-pattern?): 可选。修饰符:public、private、protected。

  • 返回值匹配(ret-type-pattern): 必填。匹配返回值类类型。也可以为*表示任何返回值。

  • 类路径匹配(declaring-type-pattern.?): 可选。匹配类名。 (相当于某个类下面的哪个方法。注意:后面是有一个点号的,然后才接的方法名)

  • 方法名匹配(name-pattern): 必填。匹配方法名 也可以为表示代表所有方法, 还可以类似set的形式,代表以set开头的所有方法。

  • 参数匹配((param-pattern)): 必填。 匹配具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法;(,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(..)表示零个或多个任意参数。

  • 异常类型匹配(throws-pattern?): 可选。函数抛出异常类型匹配。

@Pointcut execution 表达式其实是很好理解的,咱们可以把他看成一个函数就好了,函数有修饰符、返回值、方法名、参数、异常。declaring-type-pattern? 和 name-pattern 共同组合匹配方法匹配方法。 如果写了declaring-type-pattern注意,后面要带一个点号。

       比如如下实例,匹配com.tuacy.microservice.framework.user.manage.controller包下面直接子类所有方法

/**
 * 日志AOP 切面类
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配com.tuacy.microservice.framework.user.manage.controller包下面直接子类所有方法。
     */
    @Pointcut("execution(* com.tuacy.microservice.framework.user.manage.controller.*.*(..))")
    public void operateLog() {
    }
    
    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }
    
    
}

4.2 within

       "within(类型表达式)":匹配连接点所在的Java类或者包。within()函数定义的连接点是针对目标类而言的。with所指定的连接点最小范围是类。所以within能实现的功能,execution也能实现。

/**
 * 日志AOP 切面类
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {


    /**
     * 匹配com.tuacy.microservice.framework.user.manage.controller包下直接子类所有方法。
     */
    @Pointcut("within(com.tuacy.microservice.framework.user.manage.controller.*)")
    public void operateLog() {
    }


    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }


}

匹配 com.tuacy.microservice.framework.user.manage.controller包下面,子类的所有方法。(不包含子孙包,如果想包含子孙包需要改为:within(com.tuacy.microservice.framework.user.manage.controller..*))。

       在比如如果A继承了接口B,则within("B")不会匹配到A,但是within("B+")可以匹配到A。

       在比如一个,匹配 所有添加了com.tuacy.microservice.framework.user.manage.annotation.LogginAnnotation注解的方法。

    @Pointcut("within(@com.tuacy.microservice.framework.user.manage.annotation.LogginAnnotation *)")
    public void operateLog() {
    }

4.3 this

       this 向通知方法中传入代理对象的引用。

/**
 * 日志AOP 切面类
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配UserController类所有的方法
     */
    @Pointcut("execution(* com.tuacy.microservice.framework.user.manage.controller.UserController.*(..))")
    public void operateLog() {
    }

    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容, 通过this传入了代理对象的引用
     */
    @Before(value = "operateLog() && this(param)")
    public void beforeMethod(JoinPoint jp, UserController param) {
        System.out.println(param.toString());
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

4.4 target

       target 向通知方法中传入目标对象的引用。

/**
 * 日志AOP 切面类
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配UserController类所有的方法
     */
    @Pointcut("execution(* com.tuacy.microservice.framework.user.manage.controller.UserController.*(..))")
    public void operateLog() {
    }

    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容, 通过target传入了目标对象的引用
     */
    @Before(value = "operateLog() && target(param)")
    public void beforeMethod(JoinPoint jp, UserController param) {
        System.out.println(param.toString());
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

4.5 args

       args 将参数传入到通知方法中。如果有多个参数逗号隔开。

切面

/**
 * 日志AOP 切面类
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配UserController类所有的方法
     */
    @Pointcut("execution(* com.tuacy.microservice.framework.user.manage.controller.UserController.*(..))")
    public void operateLog() {
    }

    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容, 通过args传入了目标方法的参数
     */
    @Before(value = "operateLog() && args(param)")
    public void beforeMethod(JoinPoint jp, UserParam param) {
        System.out.println(param.toString());
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

目标

@RestController
public class UserController extends BaseController implements IUserControllerApi {

    private IUserService userService;

    @Autowired
    public void setUserService(IUserService userService) {
        this.userService = userService;
    }

    public ResponseDataEntity<UserInfoEntity> getUser(@RequestBody UserParam param) {
        ResponseDataEntity<UserInfoEntity> responseDataEntity = new ResponseDataEntity<>();
        try {
            responseDataEntity.setMsg(ResponseResultType.SUCCESS.getDesc());
            responseDataEntity.setStatus(ResponseResultType.SUCCESS.getValue());
            responseDataEntity.setData(userService.getUserInfo());
        } catch (Exception e) {
            e.printStackTrace();
        }

        return responseDataEntity;
    }
}

4.6 @within

       "@within(注解类型)":匹配类上添加了指定注解的该类的所有方法;注解类型也必须是全限定类型名。比如如下实例会匹配到所有添加了ClassAnnotation注解的类的所有方法。

/**
 * 日志AOP 切面类
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {


    /**
     * 匹配所有添加了ClassAnnotation注解的方法
     */
    @Pointcut("@within(com.tuacy.microservice.framework.user.manage.annotation.ClassAnnotation)")
    public void operateLog() {
    }

    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

4.7 @target

       "@target(注解类型)":@within的功能类似,但必须要指定注解接口的保留策略为RUNTIME。注解类型也必须是全限定类型名。

       关于 @target的使用,编写的实例代码一直会报错。不晓得为啥。代码如下:

/**
 * 日志AOP 切面类
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {


    /**
     * 匹配所有添加了ClassAnnotation注解的方法
     */
    @Pointcut("@target(com.tuacy.microservice.framework.user.manage.annotation.ClassAnnotation)")
    public void operateLog() {
    }

    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}
/**
 * 添加在类上的注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
public @interface ClassAnnotation {
    public String value() default "";
}

4.8 @args

       "@args(注解列表)":匹配当前执行的方法传入的参数持有指定注解。注解类型也必须是全限定类型名。

       关于@args的使用,代码也是报错,不晓得为啥。

/**
 * 日志AOP 切面类
 */

@Aspect
@Component("logAspect")
public class LoggingAspect {


    /**
     * 匹配所有方法参数添加了RequestBody注解的方法
     */
    @Pointcut("@args(org.springframework.web.bind.annotation.RequestBody)")
    public void operateLog() {
    }

    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}

4.9 @annotation

       "@annotation(注解类型)":匹配所有添加了指定注解类型的方法。注解类型也必须是全限定类型名。比如下面的实例会匹配到所有添加了OperateLogAnnotation注解的方法。

@Aspect
@Component("logAspect")
public class LoggingAspect {

    /**
     * 匹配所有添加了LoggingAnnotation注解的方法
     */
    @Pointcut("@annotation(com.tuacy.microservice.framework.user.manage.annotation.LoggingAnnotation)")
    public void operateLog() {
    }

    /**
     * 前置通知:目标方法执行之前执行以下方法体的内容
     */
    @Before(value = "operateLog()")
    public void beforeMethod(JoinPoint jp) {
        String methodName = jp.getSignature().getName();
        System.out.println("【前置通知】the method 【" + methodName + "】");
    }

}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容