zoukankan      html  css  js  c++  java
  • 《Spring in action 4》(四)面向切面的Spring

    面向切面的Spring

    Aop 的概念

    Aop :Aspect oriented Programming 面向切面编程,面向切面编程是面向对象编程的补充,而不是替代品。在运行时,动态地将代码切入到类的指定方法,指定位置上的编程思想就是面向切面编程。

    Aop中的术语

    通知(Advice)

    ​ 通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前或是之后都调用?还是只在方法抛出异常时调用?

    Spring切面可以应用的切面有五种:

    • 前置通知(Before):在目标方法被调用之前调用通知方法。
    • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么。
    • 返回通知(After-Returning):在目标方法成功执行之后调用通知。
    • 异常通知(After-Throwing):在目标方法抛出异常之后调用通知。
    • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

    切面(Aspect)

    ​ Aspect声明类似与Java中类的声明,在Aspect中包含着一些Pointcut以及相应的 Advice。

    连接点(Joint Point)

    ​ 表示在程序中明确定义的点,典型的包括方法的调用,属性的修改,对类成员的访问以及异常处理程序块的执行等。它自身还可以嵌套其他的Joint Point。

    切点(PointCut)

    ​ 表示一组符合要求的Joint Point, 这些Joint Point 或是通过逻辑关系组合起来,或是通过通配,正则表达式等方法集中起来,它定义了相应的Advice将要发生的地方。

    目标对象(Target)

    ​ 织入Advice的目标对象。

    织入(Weaving)

    ​ 将Apsect和其他对象连接起来,并创建Adviced Object的过程。

    案例解释术语

    ​ 看到上面的术语其实非常的头痛,不知所云,那么下面用一个比较容易理解的例子来说明上述概念:
    (摘自网上 https://blog.csdn.net/q982151756/article/details/80513340

    **下面我以一个简单的例子来比喻一下 AOP 中 Aspect, Joint point, Pointcut 与 Advice之间的关系. **
    ​ 让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来.

    **来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系. **

    ​ 首先我们知道, 在 Spring AOP 中 Joint point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 Joint point, 通过 point cut, 我们就可以确定哪些 Joint point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, Joint point 就相当于 爪哇的小县城里的百姓,pointcut 就相当于 老王所做的指控, 即凶手是个男性, 身高约七尺五寸, 而 Advice 则是施加在符合老王所描述的嫌疑人的动作: 抓过来审问.
    为什么可以这样类比呢?

    • Join point : 爪哇的小县城里的百姓: 因为根据定义, Joint point 是所有可能被织入 Advice 的候选的点, 在 Spring AOP中, 则可以认为所有方法执行点都是 Joint point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人.

    • Pointcut :男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 Advice, 但是我们并不希望在所有方法上都织入 Advice, 而 Pointcut 的作用就是提供一组规则来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据凶手是个男性, 身高约七尺五寸, 把符合条件的人抓起来. 在这里 凶手是个男性, 身高约七尺五寸 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问.

    • Advice :抓过来审问, Advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 Joint point 上的. 同理, 对比到我们的例子中, 抓过来审问 这个动作就是对作用于那些满足 男性, 身高约七尺五寸 的爪哇的小县城里的百姓.

    • Aspect:Aspect 是 point cut 与 Advice 的组合, 因此在这里我们就可以类比: “根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问” 这一整个动作可以被认为是一个 Aspect.

    AspectJ指示器

    可参考官方文档:

    https://docs.spring.io/spring/docs/5.1.9.RELEASE/spring-framework-reference/core.html#aop-aspectj-support

    切入点表达式解释:

    AspectJ注解

    注解 通知
    @After 通知方法会在目标方法返回或抛出异常后调用
    @AfterReturning 通知方法会在目标方法返回后调用
    @AfterThrowing 通知方法会在目标方法抛出异常后调用
    @Around 通知方法会将目标方法包裹起来
    @Before 通知方法会再目标方法调用之前执行

    Aop配置元素

    spring的Aop配置元素能够以非侵入性的方式声明切面

    Aop配置元素 用途
    <aop:advisor> 定义Aop通知器
    <aop:after> 定义Aop后置通知(不管被通知的方法是否成功执行)
    <aop:after-returning> 定义Aop返回通知
    <aop:after-throwing> 定义Aop异常通知
    <aop:around> 定义Aop环绕通知
    <aop:aspect> 定义一个切面
    <aop:aspectj-autopoxy> 启用@Aspect注解驱动的切面
    <aop:before> 定义Aop前置通知
    <aop:config> 顶层的Aop配置元素,大多数的aop:*元素必须包含在aop:config元素类
    <aop:pointcut> 定义一个切点

    Java注解方式实现Aop

    表演接口:

    public interface Performance {
        String perform();
    }
    

    音乐表演:

    /**音乐表演*/
    @Component
    public class MusicPerformance implements Performance {
        public String perform() {
            System.out.println(">>>>>演员正在表演进行音乐演唱<<<<<");
            //int i = 1/0;
            return "MusicPerformance";
        }
    }
    

    切面定义:

    package com.ooyhao.spring.aop;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    /**
     * 描述:
     * 类【PerformanceAspect】
     *
     * @author 阳浩
     * @create 2019-08-29 17:55
     */
    /*使用注解版*/
    @Aspect
    @Component
    public class PerformanceAspect {
    
        @Pointcut("execution(* *.perform(..))")
        public void pointCut(){}
    
    
        @Before("pointCut()")
        public void offPhone(){
            System.out.println("将手机关机或调为静音");
        }
    
        @After("pointCut()")
        public void clean(){
            System.out.println("清理座位旁边的垃圾");
        }
    
        @Around(value = "pointCut()")
        public Object writeInfo(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("记录表演人员信息和歌曲名称");
            Object result = joinPoint.proceed();
            System.out.println("记录表演时间!");
            return result;
        }
    
        @AfterThrowing(value = "pointCut()", throwing = "exception")
        public void refund(JoinPoint joinPoint, Exception exception){
            System.out.println(exception.getMessage());
            System.out.println("观看不满意,要求退款");
        }
    
        @AfterReturning(value = "pointCut()" , returning = "result")
        public void applause(JoinPoint joinPoint,Object result){
            System.out.println("result:"+result);
            System.out.println("起身并鼓掌");
        }
    }
    

    测试类以测试结果:

    @Test
    public void testJavaConfigAop(){
      AnnotationConfigApplicationContext context
        = new AnnotationConfigApplicationContext(AopConfig.class);
      Performance bean = context.getBean(Performance.class);
      System.out.println(bean);
      bean.perform();
    }
    
    /*
    记录表演人员信息和歌曲名称
    将手机关机或调为静音
    >>>>>演员正在表演进行音乐演唱<<<<<
    记录表演时间!
    清理座位旁边的垃圾
    result:MusicPerformance
    起身并鼓掌
    */
    

    由上述的测试结果可以看出,通知的执行流程是:

    当出现异常时:

    目标方法执行时出现异常:

    @Component
    public class MusicPerformance implements Performance {
    
        public void perform() {
            System.out.println(">>>>>演员正在表演进行音乐演唱<<<<<");
            int i = 1/0;
        }
    }
    

    异常时执行结果:

    记录表演人员信息和歌曲名称
    将手机关机或调为静音
    >>>>>演员正在表演进行音乐演唱<<<<<
    清理座位旁边的垃圾
    观看不满意,要求退款
    
    java.lang.ArithmeticException: / by zero
    

    由上述两个流程图可以看出:

    正常情况时:

    环绕通知目标方法前-->前置通知-->目标方法-->环绕通知目标方法后-->后置通知-->返回通知。
    异常情况时:

    环绕通知目标方法前-->前置通知-->目标方法-->后置通知-->异常通知。

    总结:

    ​ 正常情况下,不会执行异常通知(AfterTrowing),异常情况下,不会执行环绕通知目标方法后的代码(Around after),也不会执行返回通知(AfterReturning)。

    Xml配置方式实现Aop

    切面:使用Xml方式,切面就是一个普通的Java类

    package com.ooyhao.spring.aop;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    /**
     * 描述:
     * 类【PerformanceAspect】
     *
     * @author 阳浩
     * @create 2019-08-29 17:55
     */
    /*使用XML版*/
    public class PerformanceAspect {
    
    
        //before
        public void offPhone(JoinPoint joinPoint){
            System.out.println("将手机关机或调为静音");
        }
    
        //after
        public void clean(JoinPoint joinPoint){
            System.out.println("清理座位旁边的垃圾");
        }
    
        //around
        public Object writeInfo(ProceedingJoinPoint joinPoint) throws Throwable {
            System.out.println("记录表演人员信息和歌曲名称");
            Object result = joinPoint.proceed();
            System.out.println("记录表演时间!");
            return result;
        }
    
        //afterTrowing
        public void refund(JoinPoint joinPoint, Exception exception){
            System.out.println(exception.getMessage());
            System.out.println("观看不满意,要求退款");
        }
    
        //afterReturning
        public void applause(JoinPoint joinPoint, Object result) {
            System.out.println("AfterReturning :result "+result);
            System.out.println("起身并鼓掌");
        }
    }
    

    Xml配置文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/aop
            https://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!--目标对象-->
        <bean class="com.ooyhao.spring.bean.MusicPerformance"/>
    
        <!--开启aop的自动代理-->
        <aop:aspectj-autoproxy/>
    
        <!--将切面定义为一个Bean-->
        <bean id="performanceAspect" class="com.ooyhao.spring.aop.PerformanceAspect"/>
    
        <!--通知定义-->
        <aop:config>
            <aop:aspect ref="performanceAspect">
                <aop:pointcut id="pointCut" expression="execution(* *.perform(..))"/>
                <aop:before method="offPhone" pointcut-ref="pointCut"/>
                <aop:after method="clean" pointcut-ref="pointCut"/>
                <aop:around method="writeInfo" pointcut-ref="pointCut"/>
                <aop:after-returning method="applause" pointcut-ref="pointCut" returning="result" />
                <aop:after-throwing method="refund" pointcut-ref="pointCut" throwing="exception"/>
            </aop:aspect>
        </aop:config>
    </beans>
    

    测试及结果:

    @Test
    public void testXmlAop(){
      ClassPathXmlApplicationContext context
        = new ClassPathXmlApplicationContext("springAop.xml");
      Performance performance = context.getBean(Performance.class);
      performance.perform();
    }
    /**
    将手机关机或调为静音
    记录表演人员信息和歌曲名称
    >>>>>演员正在表演进行音乐演唱<<<<<
    起身并鼓掌
    记录表演时间!
    清理座位旁边的垃圾
    */
    

    注意:可以看出,使用Java配置的方式和Xml配置的方式,通知执行顺序有差异。

    JoinPoint 对象

    JoinPoint

    JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法的JoinPoint对象。

    常用API

    方法名 功能
    Signature getSignature() 获取封装了署名信息的对象,在该对象中可以获取目标方法的方法名,所属类的Class等信息。
    Object[] getArgs() 获取传入目标方法的参数对象
    Object[] getTarget() 获取被代理的对象
    Object[] getThis() 获取代理对象

    ProceedingJoinPoint

    ProceedingJoinPoint 对象是JoinPoint的子接口,该对象只用在@Around的切面方法中,添加了两个方法:

    Object proceed() trows Trowable //执行目标方法

    Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法

    案例说明

    User类:

    public class User {
        private String name;
        private Integer age;
    }
    

    UserService类:

    @Component
    public class UserService {    
      public void Login(User user,String authCode){        
        System.out.println("user: "+user+" authCode: "+authCode);
        }
    }
    

    切面类:

    @Component
    @Aspect
    public class UserAspect {
    
    
        @Pointcut("execution(* *Login(..))")
        public void pointCut(){}
    
        /**
         * 目标方法:
         * public class UserService {
         *
         *     public void Login(User user,String authCode){
         *         System.out.println("user: "+user+" authCode: "+authCode);
         *     }
         * }
         * */
        @Around("pointCut()")
        public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
    //=================joinPoint.getArgs()==============================
            //目标方法的入参 [User{name='张三', age=23}, 123456]
            Object[] args = joinPoint.getArgs();
            System.out.println(Arrays.toString(args));
    // ================joinPoint.getSignature()=========================
            Signature signature = joinPoint.getSignature();
            //方法名 Login
            String name = signature.getName();
            System.out.println(name);
    
            //目标方法所在类的Class对象 class com.ooyhao.spring.service.UserService
            Class aClass = signature.getDeclaringType();
            System.out.println(aClass);
    
            //目标方法所在类的类的权限类名 com.ooyhao.spring.service.UserService
            String typeName = signature.getDeclaringTypeName();
            System.out.println(typeName);
    
            //目标方法的修饰符
            int modifiers = signature.getModifiers();
            System.out.println(modifiers);
    
    //=====================joinPoint.getTarget()===================
            //被代理的目标对象 com.ooyhao.spring.service.UserService@1ba9117e
            Object target = joinPoint.getTarget();
            System.out.println(target);
    //=====================joinPoint.getThis()===================
            //代理对象
            Object aThis = joinPoint.getThis();
            System.out.println(aThis);
            //可以将原有调用时传入的参数进行修改
            // 调用无参的方法,即表示使用调用者传入的参数。
            Object obj = joinPoint.proceed(new Object[]{new User("李四",24),"123abc"});
            return obj;
        }
    }
    

    配置类:

    @ComponentScan(basePackages = "com.ooyhao.spring")
    @EnableAspectJAutoProxy
    public class UserAopConfig {}
    

    对现有类增加方法

    ​ 至此,SpringAop的JavaConfig配置类和Xml配置文件形式都已经学完,但是Aop中 @Before、@After、@Around、@AfterReturning、@AfterTrowing这几种通知都是只对目标类的目标方法进行增强,但是无法向目标方法注入新的方法。这么强大的Spring,肯定有相应的解决办法啦!那就是使用@DeclareParents 注解实现。

    Java配置类方式

    学生接口:

    public interface Student {
        void readBook();
    }
    

    学生实现类:

    @Component
    public class CollegeStudent implements Student {
        public void readBook() {
            System.out.println("我在阅读大学必修书籍!");
        }
    }
    

    教师接口:

    /*教师接口*/
    public interface Teacher  {
        void speak();
    }
    

    教师实现类:

    public class EnglishTeacher implements Teacher {
        public void speak() {
            System.out.println("我会说英语!");
        }
    }
    

    切面:

    @Aspect
    @Component
    public class StudentAspect {
        @DeclareParents(value = "com.ooyhao.spring.bean.Student+",defaultImpl = EnglishTeacher.class)
        private Teacher teacher;
    }
    
    

    配置类:

    @ComponentScan(basePackages = "com.ooyhao.spring")
    @EnableAspectJAutoProxy
    public class AopConfig {}
    

    单元测试:

    @Test
    public void testAop(){
      AnnotationConfigApplicationContext context
        = new AnnotationConfigApplicationContext(AopConfig.class);
      Student bean = context.getBean(Student.class);
      bean.readBook();
      Teacher t = (Teacher)bean;
      t.speak();
    }
    

    结果:

    解释:首先教师和学生都是一个普通的java类,切面类中依旧使用@Aspect注解来定义其为一个切面类,使用@Component标注为一个Spring组件。而在配置类中使用@ComponentScan注解用来对组件进行扫描。使用@EnableAspectJAutoProxy 开启AspectJ自动代理。

    需要研究的是切面中的内容:

    @DeclareParents(value = "com.ooyhao.spring.bean.Student+",
                defaultImpl = EnglishTeacher.class)
    private Teacher teacher;
    

    属性teacher表示将哪种类型声明为增加类。而使用@DeclareParents注解来声明需要增加和实际定义了增加方法的实际类。其中value表示向所有Student类及其子类增加方法,增加的方法的实际来源是在defaultImpl中定义的,即:增加的方法在EnglishTeacher中定义。并且在实际类型转化的时候,不能将测试代码中的bean强转为EnglishTeacher,只能强转为Teacher类型。

    解释:@DeclareParents 注解由三部分组成:

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

    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:aop="http://www.springframework.org/schema/aop"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:tx="http://www.springframework.org/schema/tx"
           xmlns:mvc="http://www.springframework.org/schema/mvc"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd
           http://www.springframework.org/schema/mvc
           http://www.springframework.org/schema/mvc/spring-mvc.xsd">
        <!--开启包的扫描-->
        <context:component-scan base-package="com.ooyhao.spring"/>
        <!--声明为一个Bean,即定义了增加方法的一个类-->
        <bean id="englishTeacher"
              class="com.ooyhao.spring.bean.EnglishTeacher"/>
        <!--切面-->
        <aop:config>
            <aop:aspect>
                <aop:declare-parents 
                          types-matching="com.ooyhao.spring.bean.Student+"
                          implement-interface="com.ooyhao.spring.bean.Teacher"
                          delegate-ref="englishTeacher"/>
            </aop:aspect>
        </aop:config>
        <!--开启AspectJ的自动代理-->
        <aop:aspectj-autoproxy/>
    </beans>
    

    单元测试:在获取Bean的时候,下列代码中只能获取Student类型,不能获取Student实现类CollegeStudent类型的Bean。

    @Test
        public void testXmlAop(){
            ClassPathXmlApplicationContext context
                    = new ClassPathXmlApplicationContext("AopConfig.xml");
            Student bean = context.getBean(Student.class);
            bean.readBook();
            Teacher teacher = (Teacher)bean;
            teacher.speak();
        }
    

    本节主要是学习SpringAop的基于Java配置和Xml配置两种方式的使用方法,以及SpringAop中五种通知做不到的,就是在目标类中添加方法,SpringAop中的五种通知只能增强方法,而不能添加方法到目标类中,SpringAop提供了另外一种解决方案:@DeclareParents.

    源码地址:

    https://gitee.com/ooyhao/JavaRepo_Public/tree/master/Spring-in-Action/spring-in-action-04

    最后

    如果觉得不错的话,那就关注一下小编哦!一起交流,一起学习

    程序yuan
  • 相关阅读:
    挂载在snap的/dev/loop占用100%问题
    机器学习3- 一元线性回归+Python实现
    机器学习-2 模拟评估与选择
    机器学习-1 绪论
    Java面试系列第4篇-HashMap相关面试题
    Java面试系列第3篇-类的加载及Java对象的创建
    Java面试系列第2篇-Object类中的方法
    Java面试系列第1篇-基本类型与引用类型
    第3篇-如何编写一个面试时能拿的出手的开源项目?
    第2篇-如何编写一个面试时能拿的出手的开源项目?
  • 原文地址:https://www.cnblogs.com/ooyhao/p/11537117.html
Copyright © 2011-2022 走看看