美文网首页
Spring参考手册 4 面向切面编程

Spring参考手册 4 面向切面编程

作者: liycode | 来源:发表于2017-03-12 17:45 被阅读26次

    翻译自Spring官方文档 4.1.2版本

    相关文章:

    一、简介

    面向切面编程英文全称:Aspect-Oriented Programming (AOP)是面向对象编程(OOP)的补充。具体概念就不赘述。

    1.1 AOP概念

    我们首先学习一些AOP的核心概念和专业术语。这些并不是Spring特有的,不幸的是AOP的术语不是特别直观,如果Spring用它自己的术语会更让人难以理解。

    • Aspect 切面。事务管理是一个很好的例子。在Spring里切面实现特殊类或者使用@Aspect注解的类。
    • Join point 在Spring AOP中join point代表一个方法的执行。
    • Advice 特定Join point产生的通知。例如:"around","before","after",就像拦截器。
    • Pointcut 一种匹配Join point的描述。Advice将会作用于所有符合Pointcut描述的Join point
    • Introduction 声明一些方法或者成员变量在某方面为一个类型。

    Advice的类型:

    • Before advice:在一个Join point(以后都说方法)前执行,但是它没有能力阻止不执行这个方法,除非它发生异常。
    • After returning advice:当一个方法正常完成后执行。
    • After throwing advice:当一个方法抛出异常后执行。
    • After (finally) advice:当一个方法完成后执行,不管正常还是异常。
    • Around advice:在一个方法执行前和执行后调用。

    二、@AspectJ支持

    Spring的注解和AspectJ 5的一样,但是没有任何依赖AspectJ编译器。

    2.1 启用@AspectJ支持

    通过Java配置

    @Configuration
    @EnableAspectJAutoProxy
    public class AppConfig {
    
    }
    

    通过XML配置

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
           http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd
           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd">
    
        <aop:aspectj-autoproxy />
    

    2.2 声明一个切面

    首先创建一个类:

    package org.xyz;
    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    public class NotVeryUsefulAspect {
    
    }
    

    将它声明为一个bean:

    <bean id="myAspect" class="org.xyz.NotVeryUsefulAspect">
        <!-- configure properties of aspect here as normal -->
    </bean>
    

    如果配置了自动扫描发现bean的方式也可以这样:

    package org.xyz;
    import org.aspectj.lang.annotation.Aspect;
    
    @Aspect
    @Component
    public class NotVeryUsefulAspect {
    
    }
    

    切面可以有其他的方法和成员变量,和其他的类一样,只是多了@Aspect注解。

    2.3 声明一个切点

    Spring AOP只支持在Spring beans的方法执行声明一个切点。有一个切点的声明包含两部分:一个方法签名和一个切点表达式,准确的匹配哪些方法是我们感兴趣的。在@AspectJ注解风格的切面编程里,@Pointcut注解表示切点(这个注解相关的方法必须使用void返回类型)。

    下面是一个例子:

    @Pointcut("execution(* transfer(..))")// 切点表达式
    private void anyOldTransfer() {}// 切点方法签名
    

    这个切点将会匹配所有名字为transfer的方法(当然是在Spring beans里)。完整的切点语法规则参见AspectJ Programming Guide

    支持的切点标识符

    切点标识符就是上面例子中:execution部分,下面是支持的列表:

    • execution 用来匹配方法执行,这个将会是你在Spring AOP里主要用到的标识符
    • within 只匹配指定类型(不太好理解,后面有例子,就是根据包名匹配)
    • this 只匹配引用是给定类型的一个实例的方法(还是看后面的例子吧)
    • target 只匹配目标对象是给定类型的一个实例的方法
    • args 只匹配参数是给定类型实例的方法

    @target@args@within@annotation这些都是根据注解来匹配的。

    注意,Spring AOP是一个基于代理的框架,被保护的方法不会被拦截,所以上面声明的切点只会对public方法起作用。

    联合切点表达式

    可以使用&&||!这些符号来联合切点表达式。还可以通过切点的名字来引用切点。请看下面的例子:

    @Pointcut("execution(public * (..))")
    private void anyPublicOperation() {}
    
    @Pointcut("within(com.xyz.someapp.trading..)")
    private void inTrading() {}
    
    @Pointcut("anyPublicOperation() && inTrading()")
    private void tradingOperation() {}
    

    anyPublicOperation切点匹配所有public的方法。

    inTrading切点匹配所有com.xyz.someapp.trading包下的方法。

    tradingOperation切点是上面两个切点的交集。

    最佳实践是用小的切点表达式来构成复杂的切点表达式,就像上面的例子。

    一些通用的切点定义

    在企业环境中,你会经常引用应用程序的各个模块。我们推荐定义一个"系统架构"切面,捕获通用的切点。一个典型的切面看起来像下面这个类:

    package com.xyz.someapp;
    
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    
    @Aspect
    public class SystemArchitecture {
    
        /
         * 一个web层的切点,匹配任何在com.xyz.someapp.web包下的方法
         */
        @Pointcut("within(com.xyz.someapp.web..)")
        public void inWebLayer() {}
    
        /
         * 一个service层的切点,匹配任何在com.xyz.someapp.service包下的方法
         */
        @Pointcut("within(com.xyz.someapp.service..)")
        public void inServiceLayer() {}
    
        /
         * 一个数据访问层的切点,匹配任何在com.xyz.someapp.dao包下的方法
         */
        @Pointcut("within(com.xyz.someapp.dao..)")
        public void inDataAccessLayer() {}
    
        /
         * 匹配在service包下的方法,例如com.xyz.someapp.user.service.addUser()这个方法
         */
        @Pointcut("execution( com.xyz.someapp..service..(..))")
        public void businessService() {}
    
        /*
         * 匹配在dao包下的方法
         */
        @Pointcut("execution( com.xyz.someapp.dao..(..))")
        public void dataAccessOperation() {}
    
    }
    

    2.4 一些列子

    Spring AOP的用户可能和切点表达式交往最频繁。一个方法执行表达式的格式为:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? 
        name-pattern(param-pattern) throws-pattern?)
    

    ?表示是可选的。

    • modifiers-pattern? 修饰符部分
    • ret-type-pattern 返回类型部分
    • declaring-type-pattern? 声明部分
    • name-pattern 名称部分
    • param-pattern 参数部分
    • throws-pattern? 异常抛出部分

    *表示匹配某任意部分。(..)表示任意数量的参数。(*,String)表示匹配参数数量是2,第一个参数是任意类型,第二个参数必须是String的方法。

    下面是一些常见的切点表达式:

    • 任意公开方法的执行
    execution(public * *(..))
    
    • 以set开头的任意方法
    execution(* set*(..))
    
    • 定义在service包下的任意方法
    execution(* com.xyz.service..(..))
    
    • 定义在service包以及子包下的任意方法
    execution(* com.xyz.service...(..))
    
    • service包里的任意切点 (method execution only in Spring AOP)
    within(com.xyz.service.*)
    
    • 一个参数是Serializable的任意切点 (method execution only in Spring AOP)
    args(java.io.Serializable)
    
    • 切点匹配的方法上有@Transactional的注解
    @annotation(org.springframework.transaction.annotation.Transactional)
    

    译者注:很多我也没实践过,以后实践了再加些更通俗的解释吧。

    2.5 声明通知

    通知(Advice)是和切点表达式关联的。在切点表达式匹配的方法之前、之后或者前后执行。

    前置通知

    前置通知在一个切面里使用@Before注解:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    @Aspect
    public class BeforeExample {
    
        @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
        public void doAccessCheck() {
            // ...
        }
    
    }
    

    com.xyz.myapp.SystemArchitecture是上面的一个例子:

    @Pointcut("execution( com.xyz.someapp.dao..(..))")
    public void dataAccessOperation() {}
    

    或者直接用切点的表达式:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    
    @Aspect
    public class BeforeExample {
    
        @Before("execution(* com.xyz.myapp.dao..(..))")
        public void doAccessCheck() {
            // ...
        }
    
    }
    

    方法返回后通知

    如其字面意思,当一个匹配的方法执行并正常返回后执行。它使用@AfterReturning注解声明:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.AfterReturning;
    
    @Aspect
    public class AfterReturningExample {
    
        @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
        public void doAccessCheck() {
            // ...
        }
    
    }
    

    注意:通知和切点完全可以写在一个切面里。

    有时你需要访问方法返回的值。你可以使用下面这种形式绑定返回值:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.AfterReturning;
    
    @Aspect
    public class AfterReturningExample {
    
        @AfterReturning(
            pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
            returning="retVal")
        public void doAccessCheck(Object retVal) {
            // ...
        }
    
    }
    

    returning属性的名字必须与通知方法的参数名字保持一致。returning项目还会约束方法返回值的类型(Object在这里将会匹配任何返回值,如果是String,将只会匹配String类型的返回值)

    方法抛出异常后通知

    当切点匹配的方法抛出异常后执行的通知。使用@AfterThrowing注解:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.AfterThrowing;
    
    @Aspect
    public class AfterThrowingExample {
    
        @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
        public void doRecoveryActions() {
            // ...
        }
    
    }
    

    如果想只获得指定类型的异常,可以使用throwing属性,和上面的returning类似:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.AfterThrowing;
    
    @Aspect
    public class AfterThrowingExample {
    
        @AfterThrowing(
            pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
            throwing="ex")
        public void doRecoveryActions(DataAccessException ex) {
            // ...
        }
    
    }
    

    后置通知

    只要是一个切点匹配的方法退出了就会执行方法后通知,不管是否成功返回。它使用@After注解。你必须同时考虑到正常和异常的情况。一个典型的应用就是释放资源等:

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.After;
    
    @Aspect
    public class AfterFinallyExample {
    
        @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
        public void doReleaseLock() {
            // ...
        }
    
    }
    

    环绕通知

    环绕通知经常被用在需要在一个线程安全的方法执行前后共享状态。总是使用能满足你需求的能力最弱的切面(例如,如果前置通知就可以完成的不要使用环绕通知)。

    环绕通知使用@Around注解来声明。通知方法的第一个参数必须是ProceedingJoinPoint类型。在通知方法体内调用ProceedingJoinPointproceed()方法会执行切点所匹配的方法。proceed()方法也可以接收一个Object[]类型的参数给切点所匹配的方法。

    译者注:proceed()方法之前相当于前置通知处理的范围,proceed()方法之后相当于后置通知处理的范围。

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.ProceedingJoinPoint;
    
    @Aspect
    public class AroundExample {
    
        @Around("com.xyz.myapp.SystemArchitecture.businessService()")
        public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
            // retVal是方法的返回值
            Object retVal = pjp.proceed();
    
            return retVal;
        }
    
    }
    

    访问当前的切点

    任何通知的方法都可以声明一个org.aspectj.lang.JoinPoint类型的参数作为它的第一个参数(环绕通知中的ProceedingJoinPointJoinPoint的子类,所以可以作为环绕通知的第一个参数)。JoinPoint接口提供了很多实用的方法例如:

    • getArgs() 返回切点匹配方法的参数
    • getThis() 返回代理对象
    • getTarget() 返回目标对象
    • getSignature() 返回通知关联的切点方法的签名

    未完,待续...

    相关文章

      网友评论

          本文标题:Spring参考手册 4 面向切面编程

          本文链接:https://www.haomeiwen.com/subject/rhqxnttx.html