美文网首页
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