zoukankan      html  css  js  c++  java
  • [spring学习3] AOP

    简介

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

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

    概念

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

    通知

    切面的工作被称为通知。

    通知以日志为例,就是想要插入到业务代码的日志程序。

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

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

    在什么时候执行通知。

    连接点

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

    这个点就是触发执行通知的时机:如调用方法时,抛出异常时,修改字段时。

    切点

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

    相对于连接点而言,连接点是所有可以供通知切入的地方,切点就是满足设定条件的连接点。

    切面

    切面 = 通知 + 切点

    引入

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

    织入

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

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

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

    AOP支持

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

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

    如需更负责的AOP需求,如构造器和属性拦截,需要使用AspectJ实现。

    Spring的AspectJ自动代理仅仅使用@AspectJ作为创建切面的指导,切面依然是基于代理的。在本质上,它依然是Spring基于代理的切面。这意味着尽管使用的是@AspectJ注解,但仍然限于代理方法的调用。(如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并且不依赖Spring来创建基于代理的切面)

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

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

    Spring在运行时通知对象

    不使用AOP:

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

    使用AOP:

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

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

    通过切点选择连接点

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

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

    在Spring中尝试使用AspectJ其它指示器时,将会抛出IllegalArgument-Exception异常。

    上述指示器,只有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)操作
    

    &在xml中由特殊含义,所以spring在xml的配置中可以使用and替代&&,同理or,not替代||,!

    在切点中选择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("<---");
        }
    }
    

    @Component直接将其注册为Bean,给AspectJ代理使用。

    ProceedingJoinPoint可以不调用proceed(),以阻塞对被通知方法的调用。

    启用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;
    }
    

    @DeclareParents注解由三部分组成:

    • value属性指定了哪种类型的bean要引入该接口。(此处是所有实现了Person接口的类型)。标记符后面的加号+表示是Person的所有子类型,而不是Person本身。
    • defaultImpl属性指定了为引入功能提供实现的类。
    • @Declarearents注解所标注的静态属性指明了要引入的接口。

    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规定的那样在该对象上执行依赖注入。

  • 相关阅读:
    JSOI 2008 火星人prefix
    OI 中的 FFT
    浅谈最大化子矩阵问题
    qq空间答案
    若瑟夫问题
    [颓废] 改某人的WebGL light mapping demo并9xSSAA
    Codeforces Round #402 (Div. 2) C. Dishonest Sellers
    Codeforces Round #402 (Div. 2) D. String Game
    Codeforces Round #401 (Div. 2) E. Hanoi Factory
    Codeforces Round #401 (Div. 2) D. Cloud of Hashtags
  • 原文地址:https://www.cnblogs.com/maplesnow/p/11759832.html
Copyright © 2011-2022 走看看