简介

典型的应用场景就是日志,我们需要在某段业务代码的前后做一些日志输出的话,如果我们将日志代码与业务代码混在一起,是非常难以解耦合的。

aop就是应对这种情况产生的技术。

概念

        通知
         |
         |切点
         ↓
    ——*——*——*——程序执行→
      ↑  ↑    ↑
       连接点

通知

切面的工作被称为通知。

Spring切面的5种类型的通知:

  • 前置通知(Before)
  • 后置通知(After)
  • 返回通知(After-returning)
  • 异常通知(After-throwing)
  • 环绕通知(Around)

连接点

连接点是在应用执行过程中能够插入切面的一个点。

切点

一个切面并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点的范围。

切面

切面 = 通知 + 切点

引入

向现有类添加新方法或属性。

织入

把切面应用到目标对象,并创建新的代理对象的过程。

在目标对象的生命周期里可织入的点:

  • 编译期
  • 类加载期
  • 运行期

AOP支持

Spring提供的4种类型的AOP支持:

  • 基于代理的经典Spring AOP(过于笨重复杂,直接使用ProxyFactory Bean。)
  • 纯POJO切面
  • @AspectJ注解驱动的切面
  • 注入式AspectJ切面

Spring只支持方法级别的连接点

因为Spring基于动态代理,所以Spring只支持方法连接点。

Spring在运行时通知对象

不使用AOP:

┌─────┐      ┌───────┐
│调用者│----->│目标对象│
└─────┘      └───────┘

使用AOP:

              ┌─────────┐
              │代理类    │
┌─────┐       │┌───────┐│
│调用者│-----> ││目标对象││
└─────┘       │└───────┘│
              └─────────┘ 

通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,再调用目标bean方法之前,会执行切面逻辑。

通过切点选择连接点

spring借助AspectJ的切点表达式语言来定义Spring切面

execution()用于匹配是连接点的执行方法
arg()限制连接点匹配参数为指定类型的执行方法
@args()限制连接点匹配参数由指定注解标注的执行方法
this()限制连接点匹配AOP代理的bean引用为指定类型的类
target限制连接点匹配目标对象为指定类型的类
@target()限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within()限制连接点匹配指定的类型
@within()限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在由指定的注解所标注的类里)
@annotation限定匹配带有指定注解的连接点

上述指示器,只有execution指示器是实际执行匹配的,而其它的都是用来限制的。

对于xml配置

采用注解和自动代理的方式创建切面,是十分便利的方式。

但面向注解的切面声明有一个明显的劣势:你必须能够为通知类添加注解。为了做到这一点,必须要有源码。

没有通知类的源码,只能采用xml配置文件声明切面。

详细

切点

编写切点

expression:

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:异常类型
  在方法执行时触发  方法所属的类  方法
    ┌───────┐    ┌─────────┐ ┌──┐
    execution( * com.yww.Log.info(..) )
              └─┘                └──┘
            返回任意类型         使用任意参数
            执行Log.info()方法              当com.yww包下的任意类的方法被调用时
    ┌─────────────────────────────────┐    ┌───────────────┐
    execution( * com.yww.Log.info(..) ) && within(com.yww.*)
                                       └──┘
                                  与(and)操作

在切点中选择bean

execution(* com.yww.Login.info()) and bean('work')

execution(* com.yww.Login.info()) and !bean('work')

切面

代码

示例几种使用:

  • 基本使用
  • 处理通知中的参数
  • 通过注解引入新功能

基本使用

目录结构

.
├── build.gradle
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── yww
    │   │           ├── Config.java
    │   │           ├── Log.java
    │   │           ├── Main.java
    │   │           └── Work.java
    │   └── resources
    └── test
        ├── java
        └── resources

build.gradle

build.gradle:引入的库.

plugins {
    id 'java'
}

group 'com.yww'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

ext{
    springVersion = '5.2.0.RELEASE'
}

dependencies {
    compile "org.springframework:spring-core:$springVersion"
    compile "org.springframework:spring-context:$springVersion"
    compile "org.springframework:spring-beans:$springVersion"
    compile "org.springframework:spring-expression:$springVersion"
    compile "org.springframework:spring-aop:$springVersion"
    compile "org.springframework:spring-aspects:$springVersion"

    testCompile "junit:junit:4.12"
    testCompile "org.springframework:spring-test:$springVersion"
}

jar {
    from {
        configurations.runtime.collect{zipTree(it)}
    }
    manifest {
        attributes 'Main-Class': 'com.yww.Main'
    }
}

业务代码

Work.java:将这个类的功能作为业务代码为例。就是一个普通的bean。

package com.yww;

import org.springframework.stereotype.Component;

@Component
public class Work {
    public void working(){
        System.out.println("-w-o-r-k-i-n-g-");
    }
}

切面

Log.java:使用@Aspect声明切面。

写通知,在通知方法上使用@Before@After等声明切点。

也可以使用@Pointcut声明切点位置,减少重复。

package com.yww;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class Log {

    @Pointcut("execution(* com.yww.Work.working())")
    public void working(){}

    @Before("working()")
    public void infoStart(){
        System.out.println("start");
    }

    @After("working()")
    public void infoEnd(){
        System.out.println("end");
    }

    @Around("working()")
    public void infoAround(ProceedingJoinPoint jp){
        System.out.println("--->");
        try {
            jp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("<---");
    }
}

启用AspectJ代理

LogConfig.java:使用@EnableAspectJAutoProxy开启AspectJ自动代理。

package com.yww;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class LogConfig {
}

Main

Main.java:主函数,这是一个普通的应用,通过上下文加载java配置类启动组件扫描AspectJ自动代理功能。

执行业务代码后,定义在切面的通知也会在适当时机执行。

package com.yww;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(com.yww.LogConfig.class);
        Work work = context.getBean(Work.class);
        work.working();
    }
}

处理通知中的参数

作用:获取业务方法的参数,在通知中做额外处理。

需要:在切点表达式中声明参数,这个参数传入到通知方法中。

  在方法执行时触发  方法所属的类   方法
    ┌───────┐    ┌──────────┐ ┌───┐
    execution( * com.yww.Work.clock(String) ) && args(username)
              └─┘                   └────┘       └────────────┘
            返回任意类型       接受String类型的参数      指定参数

目录结构

.
├── build.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── yww
    │   │           ├── Counter.java
    │   │           ├── Config.java
    │   │           ├── Main.java
    │   │           └── Work.java
    │   └── resources
    └── test
        ├── java
        └── resources

业务代码

Work.java:业务代码。

package com.yww;

import org.springframework.stereotype.Component;

@Component
public class Work {

    public void clock(String username){
        System.out.println(username + " 打卡");
    }
}

切面

Counter.java:在业务代码执行clock()方法时,在通知里记录用户打卡次数。

package com.yww;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Aspect
@Component
public class Counter {

    private Map<String, Integer> counter = new HashMap<>();

    @Pointcut("execution(* com.yww.Work.clock(String)) && args(username)")
    public void clock(String username){}

    /**
     * 记录打卡次数
     */
    @Before("clock(username)")
    public void count(String username){
        int curCount = getCount(username);
        counter.put(username, curCount+1);
    }

    /**
     * 获取打卡次数
     */
    public int getCount(String username){
        return counter.containsKey(username) ? counter.get(username) : 0;
    }
}

启用AspectJ代理

LogConfig.java:使用@EnableAspectJAutoProxy开启AspectJ自动代理。

package com.yww;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class LogConfig {
}

Main

Main.java:主函数。

package com.yww;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(com.yww.Config.class);

        Work work = context.getBean(Work.class);
        Counter counter = context.getBean(Counter.class);

        work.clock("zhangsan");
        work.clock("lisi");
        work.clock("lisi");
        System.out.println(counter.getCount("lisi"));
    }
}

通过注解引入新功能

通过代理暴露新接口的方式,让切面所通知的bean看起来像是实现了新接口.

                      ┌─────────────┐
                      │代理         │
        现有方法       │┌───────────┐│
              ┌-----> ││被通知的Bean││
┌─────┐-------┘       │└───────────┘│
│调用者│               │             │
└─────┘-------┐       │┌───────────┐│
              └-----> ││ 引入的代理 ││
        被引入的方法    │└───────────┘│
                      └─────────────┘ 

目录结构

.
├── build.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── com
    │   │       └── yww
    │   │           ├── Config.java
    │   │           ├── EnhancePerson.java
    │   │           ├── Main.java
    │   │           ├── Man.java
    │   │           ├── Person.java
    │   │           ├── WalkImpl.java
    │   │           └── Walk.java
    │   └── resources
    └── test
        ├── java
        └── resources

业务类

Person.java:业务的接口.

package com.yww;

public interface Person {
    public void getName();
}

Man.java:业务的实现.

package com.yww;

import org.springframework.stereotype.Component;

@Component
public class Man implements Person {
    @Override
    public void getName() {
        System.out.println("a man");
    }
}

新增的功能

Walk.java:新增功能的接口.

package com.yww;

public interface Walk {
    void walk();
}

`WalkImpl.java:新增功能的实现.

package com.yww;

public class WalkImpl implements Walk {
    @Override
    public void walk() {
        System.out.println("新增 walk");
    }
}

切面配置

EnhancePerson.java:给业务类Person新增Walk功能.尽管没有真正的添加方法,但通过代理的方式,也可看成了功能的添加.

package com.yww;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.DeclareParents;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class EnhancePerson {
    @DeclareParents(value = "com.yww.Person+", defaultImpl = WalkImpl.class)
    public static Walk walk;
}

Main

Main.java:主函数,如何使用新增的功能.

package com.yww;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(com.yww.Config.class);

        // 方式一
        Person person = context.getBean(Person.class);
        person.getName();
        Walk w1 = (Walk) person;
        w1.walk();

        // 方式二
        Walk w2 = context.getBean("man", Walk.class);
        w2.walk();
    }
}

附:

AspectJ

AspectJ声明的切面将其声明为bean,与Spring中声明为bean的配置无太多区别,最大的不同在于使用了factory-method属性.

因为Spring bean由Spring容器初始化,但AspectJ切面是由AspectJ在运行期创建的.等到Spring有机会为其注入依赖时,该切面已实例化了.

所以Spring需要通过aspectOf()工厂方法获得切面的引用,然后像bean规定的那样在该对象上执行依赖注入.

01-10 12:19