Spring4完全教程(三):Spring AOP编程

AOP

对目标方法进行统一的日志、异常捕获等处理时,每个方法都要加上类似的代码。即繁琐也不利于重构。

代理模式解决

代理模式可以参考笔记“设计模式-代理模式”里面的说明。

下面提供使用代理模式提供输出日志的示例:

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
      
// 接口
public interface Calculator {
int add( int i, int j);
int sub( int i, int j);
int mul( int i, int j);
int div( int i, int j);
}
// 接口的实现类
public class CalculatorImpl implements Calculator {
@ Override
public int add( int i, int j) {
int result = i + j;
return result;
}
// 省略……
}
// 创建代理的核心类
public class CalculatorLoggingHandler implements InvocationHandler {
private Calculator target;
public CalculatorLoggingHandler( Calculator target) {
this.target = target;
}
@ Override
public Object invoke( Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
// 前置通知
System. out.println( "logging start...");
result = method.invoke(target, args);
// 返回通知
System. out.println( "logging end ...");
} catch ( Exception e) {
// 异常通知
} finally {
// 后置通知
}
return result;
}
public static Calculator getInstance( Calculator calculator) {
return ( Calculator) Proxy.newProxyInstance(calculator.getClass().getClassLoader(),
calculator.getClass().getInterfaces(),
new CalculatorLoggingHandler(calculator));
}
}

AOP简介

AOP主要实现的目的是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。AOP中经常使用到下面的术语:

  • aspect(切面):被模块化的特殊对象。例如最常见的logging功能,一个logging切面就可以使用AOP来实现相同的功能了。
  • jointpoint(连接点):进程执行的某个特定位置。连接点是应用进程提供给切面插入的地方。
  • pointcut(切点): AOP 通过切点定位到特定的连接点。可以有这样的类比:连接点相当于数据库中的记录,切点相当于查询条件

Spring AOP示例

Spring AOP使用AspectJ注解实现,具体步骤为:
(1):加入和AOP相关的jar包,包括:

      
1
2
3
4
      
com.springsource.net.sf.cglib- 2.2 .0.jar
com.springsource .org.aopalliance- 1.0 .0.jar
com.springsource .org.aspectj.weaver- 1.6 .8.RELEASE.jar
spring-aspects- 4.0 .0.RELEASE.jar

(2):配置切面类
切面类是AOP编程中的核心类,它是一个Java类,这个类中的方法就是各种前置、后置、异常通知等方法。示例:

      
1
2
3
4
5
6
7
8
9
10
11
      
@ Aspect
@ Component
public class LogginAspect {
@ Before( "execution(public int section04.aspect.Calculator.*(int, int))")
public void beforeMethod( JoinPoint joinpoint) {
String method = joinpoint.getSignature().getName();
Object[] args = joinpoint.getArgs();
System. out.println( "method[" + method + "(" + Arrays.asList(args) + ")]");
}
}

注意,一定要加上@Aspect和@Component两个注解,因为:

  • @Aspect声明这是一个切面类;
  • 声明为@Aspect的类会从自动代理排除(无法在IOC容器自动创建),所以需要加上@Component注解,以便加入到IOC容器中。

(3):配置通知方法
使用@Before@After@AfterReturning@AfterThrowing@Around等注解声明通知方法,后面还会详细介绍这些方法。示例:

      
1
2
3
4
5
6
      
@ Before( "execution(public int section04.aspect.Calculator.*(int, int))")
public void beforeMethod( JoinPoint joinpoint) {
String method = joinpoint.getSignature().getName();
Object[] args = joinpoint.getArgs();
System. out.println( "method[" + method + "(" + Arrays.asList(args) + ")]");
}

(4):配置AOP并执行
前面已经提供了切面类通知方法,首先下面是供测试使用的接口和接口的实现类:

      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
      
public interface {
int add( int i, int j);
int sub( int i, int j);
int mul( int i, int j);
int div( int i, int j);
}
@Component( "calculatorImpl")
public class implements Calculator {
@Override
public int add( int i, int j) {
int result = i + j;
return result;
}
}

注意,这里实现类用@Component标志,以便在IOC容器中自动创建。

此时可以使用XML配置文档打开AOP或者是零配置的注解方式,方法分别是:

  • 使用XML需要配置2项内容,分别是自动扫描包启动自动代理,所以此时的配置内容为:
      
1
2
3
4
      
<context:component-scan base-package="section04.aspect"/>
<!-- 打开自动代理 -->
<aop:aspectj-autoproxy />

测试类为:

      
1
2
3
4
5
6
7
      
public class XmlConfigTest {
public static void main(String[] args){
ApplicationContext ctx = new ClassPathXmlApplicationContext( "spring-aop.xml");
Calculator calculator = ctx.getBean( "calculatorImpl", CalculatorImpl.class);
System. out.println(calculator.add( 1, 2));
}
}

可以看到这是前面常见的做法,即使用ClassPathXmlApplicationContext获取IOC容器,而IOC容器通过扫描的方式创建Bean对象。这里CalculatorImpl实现类已经加上了@Component注解,所以可以从IOC容器中获取。

  • 使用零配置的方式,需要用注解类来代替上面XML中的2项配置,所以是这样的:
      
1
2
3
4
5
6
7
8
9
10
11
12
      
@Configuration
@ComponentScan(basePackages = "section04.aspect")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AspectTest {
public static void main(String[] args){
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AspectTest.class);
Calculator calculator = ctx.getBean( "calculatorImpl", CalculatorImpl.class);
System.out.println(calculator.add( 1, 3));
}
}

这里简单说明一下:

+ @Configuration声明当前类是注解类,对应XML配置文档;
+ @ComponentScan声明扫描的包,对应XML配置中的`<context:component-scan base-package=""/>`;
+ @EnableAspectJAutoProxy(proxyTargetClass = true):开启自动代理,对应`<aop:aspectj-autoproxy /> `,注意proxyTargetClass 一定要设置为true,因为默认为false;
+ 最后用`AnnotationConfigApplicationContext`获取当前的类,创建IOC容器。在前面的笔记中介绍了这个类的使用。

通知种类

AspectJ 支持 5 种类型的通知注解:

  • @Before:前置通知,在方法执行之前执行。通知方法可以接收JoinPoint类型参数,下同。
  • @After:后置通知,在方法执行之后执行。
    后置通知的特点是 无论目标方法是正常返回还是抛出异常,后置通知都会执行。所以后置通知 不能获取目标方法的返回值,因为不知道是否会执行异常;
  • @AfterRunning:返回通知。
    特点是正常执行结束后调用, 返回通知可以获取目标方法的返回值
    @AfterRunning 可以使用returning属性声明返回值,作为通知方法的入参。
< 大专栏   Spring4完全教程(三):Spring AOP编程ul>
  • @AfterThrowing:异常通知,在方法抛出异常之后。
    @AfterThrowing可以使用throwing声明捕获的异常,作为通知方法的入参。
    • @Around:环绕通知,可以自由选择实现前面的4个通知类型。
      环绕通知方法的参数可以是ProceedingJoinPoint类型,它是JoinPoint的子类,提供了proceed()方法执行目标方法,并且通知方法一定要有返回值。

    示例:

          
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
          
    @ Aspect
    @ Component
    public class LogginAspect {
    // 前置通知
    @ Before(value = "execution(public int section04.aspect.Calculator.*(int, int))")
    public void beforeMethod( JoinPoint joinPoint) {
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "before method[" + method + "(" + Arrays.asList(args) + ")]");
    }
    // 后置通知
    @ After(value = "execution(public int section04.aspect.Calculator.*(int, int))")
    public void afterMethod( JoinPoint joinPoint) {
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "after method[" + method + "(" + Arrays.asList(args) + ")]");
    }
    // 返回通知
    @ AfterReturning(value = "execution(public int section04.aspect.Calculator.*(int, int))",
    returning = "result")
    public void afterReturning( JoinPoint joinPoint, Object result) {
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "afterReturning method[" + method + "("
    + Arrays.asList(args) + ")] return:" + result);
    }
    // 异常通知
    @ AfterThrowing(value = "execution(public int section04.aspect.Calculator.*(int, int))",
    throwing = "e")
    public void afterThrowing( JoinPoint joinPoint, Exception e) {
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "afterThrowing method[" + method + "("
    + Arrays.asList(args) + ")] exception:" + e.getMessage());
    }
    // 环绕通知
    @ Around(value = "execution(public int section04.aspect.Calculator.*(int, int))")
    public Object aroundMethod( ProceedingJoinPoint joinPoint) {
    Object result = null;
    try {
    // 前置通知
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "前置通知");
    // 执行目标方法
    result = joinPoint.proceed();
    // 返回通知
    System. out.println( "返回通知");
    } catch ( Throwable e) {
    // 异常通知
    System. out.println( "异常通知");
    } finally {
    // 后置通知
    System. out.println( "后置通知");
    }
    return result;
    }
    }

    切点表达式(pointcut expression)

    前面说过,切点就是连接点的查询条件。在配置通知方法时,通过切点表达式匹配具体的目标方法,例如前面示例中的:

          
    1
          
    execution( public int section04.aspect.Calculator.*( int, int))

    其中*号表示匹配任意的public方法,*号 还可以对访问权限返回值类型进行通配。例如:

          
    1
          
    execution(* int section04.aspect.Calculator.*( int, int))

    或者

          
    1
          
    execution( * * section04.aspect.Calculator. *(int, int))

    如果要匹配任意数目任意类型的参数,可以使用..例如:

          
    1
          
    execution( public int section04.aspect.Calculator.*(..))

    其实Spring AOP支持以下的标志符:

    • execution - for matching method execution join points, this is the primary pointcut designator you will use when working with Spring AOP
    • within - limits matching to join points within certain types (simply the execution of a method declared within a matching type when using Spring AOP)
    • this - limits matching to join points (the execution of methods when using Spring AOP) where the bean reference (Spring AOP proxy) is an instance of the given type
    • target - limits matching to join points (the execution of methods when using Spring AOP) where the target object (application object being proxied) is an instance of the given type
    • args - limits matching to join points (the execution of methods when using Spring AOP) where the arguments are instances of the given types
    • @target - limits matching to join points (the execution of methods when using Spring AOP) where the class of the executing object has an annotation of the given type
    • @args - limits matching to join points (the execution of methods when using Spring AOP) where the runtime type of the actual arguments passed have annotations of the given type(s)
    • @within - limits matching to join points within types that have the given annotation (the execution of methods declared in types with the given annotation when using Spring AOP)
    • @annotation - limits matching to join points where the subject of the join point (method being executed in Spring AOP) has the given annotation

    具体可以参考Supported Pointcut Designators

    合并切点表达式

    切点表达式可以直接在通知方法的注解中书写,但同一个切点表达式可能会在多个通知中重复出现。
    此时可以通过@Pointcut注解将一个切点声明成简单的方法,切点的方法体通常是空的。后面使用同样的该切点表达式的通知方法直接引用@Pointcut注解的方法名即可。

    示例:

          
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
          
    @ Aspect
    @ Component
    public class PointCutTest {
    @ Pointcut(value = "execution(public int section04.aspect.Calculator.*(int, int))")
    public void pointCutMethod() {}
    @ Before(value = "pointCutMethod()")
    public void before( JoinPoint joinPoint) {
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "PointCutTest before method[" + method
    + "(" + Arrays.asList(args) + ")]");
    }
    @ After(value = "pointCutMethod()")
    public void afterMethod( JoinPoint joinPoint) {
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "PointCutTest after method[" + method + "(" + Arrays.asList(args) + ")]");
    }
    }

    示例中用@Pointcut(value = "execution(public int section04.aspect.Calculator.*(int, int))")声明了切点方法,所以前置通知和后置通知中可以直接引用@Before(value = "pointCutMethod()")

    切面的优先级

    多个切面对同一个方法生效,这时候就有优先级问题。
    使用注解@Order声明切面的优先级,属性value的值越小,则优先级越高。
    示例,下面的LoggingAspect的前置通知的优先级比LoggingAspect2 高:

          
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
          
    @ Order(value = 1)
    @ Aspect
    @ Component
    public class LoggingAspect {
    // 前置通知
    @ Before(value = "execution(public int section04.aspect.Calculator.*(int, int))")
    public void beforeMethod( JoinPoint joinPoint) {
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "LoggingAspect before method[" + method + "(" + Arrays.asList(args) + ")]");
    }
    }
    @ Order(value = 2)
    @ Aspect
    @ Component
    public class LoggingAspect2 {
    @ Before(value = "execution(public int section04.aspect.Calculator.*(int, int))")
    public void beforeMethod( JoinPoint joinPoint) {
    String method = joinPoint.getSignature().getName();
    Object[] args = joinPoint.getArgs();
    System. out.println( "LoggingAspect2 before method[" + method
    + "(" + Arrays.asList(args) + ")]");
    }
    }

    猜你喜欢

    转载自www.cnblogs.com/liuzhongrong/p/11986016.html