Spring系列(五)、Spring中AOP面向切面的编程(动态代理)

不管现实多么惨不忍睹,都要持之以恒地相信,这只是黎明前短暂的黑暗而已。不要惶恐眼前的难关迈不过去,不要担心此刻的付出没有回报,别再花时间等待天降好运。真诚做人,努力做事!你想要的,岁月都会给你。Spring系列(五)、Spring中AOP面向切面的编程(动态代理),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

5 什么是AOP面向切面编程?

AOP(Aspect Oriented Programming,面向方面编程)

  • 我们知道OOP(Object-Oriented Programming,面向对象编程),OOP引入了封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一种集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。简单来说,就是OOP定义了从上到下的关系,但不适合从左到右的关系。eg:日志功能,日志业务往往水平的分布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系;类似的还有核心业务代码的预处理、异常处理以及结束信息处理等操作,是完全与核心业务没有关联的存在。然而这些散布到各处的无关的代码我们称为 “横切(cross-cutting)代码”,这写无关核心的业务代码会被大量引用,从而就导致了代码的耦合性,且不利于各个模块之间的重用。

  • 而AOP技术恰好是对OOP面向对象的补充和增强处理,它利用一种 “横切(cross-cutting)技术”,横贯在封装的对象内部,使那些影响了多个类的公共行为通过 代理机制 封装在一个可重用的模块中,并将其命名为 “Aspect”,即方面。简单的来说,就是将那些与核心业务无关的代码,却被核心业务模块共同调用的逻辑代码封装起来,便于减少系统的重用代码,降低模块之间的耦合度,并有利于。未来的 可维护性和可扩展性

5.1 AOP目标和原理

AOP目标:

  • 让我们可以“专心做事”;

AOP原理:

  • 将复杂的需求分解出不同的方面,将散布在系统中的公共功能集中解决;
  • 采用代理机制将公共功能组装起来运行,在不改变原程序的基础上对代码段进行增强处理,增加新的功能。

5.2 实现AOP的技术,主要分为两大类:

  • 一是采用动态代理技术,利用截取消息的方式,对该消息进行封装,以取代原有对象行为的执行;
  • 二是采用静态织入的方式,引入特定的语法创建“方面”,从而使编译器可以在编译期间织入有关“方面”的代码。

5.3 AOP技术常用场景:

  • Authentication 权限
  • Caching 缓存
  • Context passing 内容传递
  • Error handling 错误处理
  • Lazy loading 懒加载
  • Debugging  调试
  • logging, tracing, profiling and monitoring 记录跟踪 优化 校准
  • Performance optimization 性能优化
  • Persistence  持久化
  • Resource pooling 资源池
  • Synchronization 同步
  • Transactions 事务

5.4 AOP相关概念:

  • 切面(Aspect):

    一个关注点的模块,这个关注点的实心可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子(eg:典型案例:银行转款,事务前操作和后操作,但是要保证整个事务机制同时成功或同时失败),方面用Spring的Advisor(顾问)或interceptor(拦截器)来实现。

  • 切入点(PointCut):

    指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点:例如使用正则表达式。Spring定义了Pointcut接口,用来组合MethodMatcher和ClassFilter,可用通过名字清晰的理解;
    MethidMatcher:是用来检查目标类的方法是否可以被应用次通知;
    ClassFilter:是用来检查Poincut是否应该应用到目标类上。

  • 连接点(Join Point):

    程序执行过程中明确的点,如方法的调用或特定的异常被抛出。

  • 增强处理(Advice):

    在特定的连接点,AOP执行的动作,包括一下各种类型的通知。
    前置增强(Before)
    后置增强(AfterReturning)
    环绕增强(Around)
    异常抛出增强(AfterThrowing)
    最终增强(After)

  • 目标对象(Target Object):

    包含连接点的对象,也被称作被代理通知被代理对象,POJO。

  • AOP代理(AOP Proxy):

    AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或者是CGLIB动态代理

  • 织入(Weaving):

    组装方面来创建一个被通知对象,这可以在编译时完成(eg:使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,也是在运行时完成织入。

5.5 如何使用Spring AOP

可以通过配置xml文件或编程的方式实现。
通过配置xml文件,可以概括为四种方式:

  • 1 配置ProxyFactoryBean,显示的配置advisors、advice、target等属性;
  • 2 配置AutoProxyCreator,这种方式,还是如以前一样使用定义的Bean,但是从容器中获取到的其实已经是代理对象了;
  • 3 通过< aop-config >来配置;
  • 4 通过 < aop:aspectj-autoproxy > 来配置,使用AspectJ的注解来标识通知及切入点(开发中常用)

5.6 废话少说,还是举个例子,先来演示第一种实现Spring AOP的方式,配置ProxyFactoryBean

上章节Spring系列(四)、设计模式之代理模式中,我们已经简单的讲解了java动态代理的功能及原理,那仍然是在通过代码硬性编程实现的动态代理,如何在Spring的AOP中实现,看如下分析,AOP中的增强处理(Advice):

创建一个人类共同接口Human(创建说话speak方法和睡觉sleep方法),有其实现类Chinese,通过Spring的Advice增强处理,对speak和sleep方法执行前后做一定的业务处理(LogBeforeAdvice和LogAfterAdvice),eg:预处理、记录日志、结束信息处理等。

  • 创建Human接口类,包含speak和sleep方法:
    package com.dao;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-14 10:52:07
     */
    public interface Human {
        /**
         * 说话
         * @param name
         */
        void speak(String name);
    
        /**
         * 睡觉
         */
        void sleep(String name);
    
    }
    
  • 创建Chinese实现类:
    package com.dao.impl;
    
    import com.dao.Human;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-14 10:58:51
     */
    public class Chinese implements Human {
        @Override
        public void speak(String name) {
            System.out.println("你好:" + name);
        }
    
        @Override
        public void sleep(String name) {
            System.out.println("你好:" + name + "该睡了");
        }
    }
    
  • 创建前置增强通知LogBeforeAdvice类,实现MethodBeforeAdvice接口:
    package com.advice;
    
    import org.springframework.aop.MethodBeforeAdvice;
    
    import java.lang.reflect.Method;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-15 09:54:58
     */
    public class LogBeforeAdvice implements MethodBeforeAdvice {
        @Override
        public void before(Method method, Object[] objects, Object o) throws Throwable {
            System.out.println("方法执行前!");
        }
    }
    
  • 创建后置增强通知LogAfterAdvice类,实现AfterReturningAdvice接口:
    package com.advice;
    
    import org.springframework.aop.AfterReturningAdvice;
    
    import java.lang.reflect.Method;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-15 09:54:58
     */
    public class LogAfterAdvice implements AfterReturningAdvice {
    
        @Override
        public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
            System.out.println("方法执行后!");
        }
    }
    
  • applicationContext.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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!--创建Chinese的Bean实例-->
        <bean id="ch" class="com.dao.impl.Chinese"/>
        <!--创建LogBeforeAdvice的Bean实例-->
        <bean id="logBeforeAdvice" class="com.advice.LogBeforeAdvice"/>
        <!--创建LogAfterAdvice的Bean实例-->
        <bean id="logAfterAdvice" class="com.advice.LogAfterAdvice"/>
    	
      
        <!--增量式配置-->
        <bean id="humanProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <!--setter方法注入:目标对象-->
            <property name="target" ref="ch"/>
            <!--面向接口-->
            <property name="interfaces">
                <value>com.dao.Human</value>
            </property>
            <!--代理方法:要要执行业务的拦截方法-->
            <property name="interceptorNames">
                <list>
                    <value>logBeforeAdvice</value>
                    <value>logAfterAdvice</value>
                </list>
            </property>
        </bean>
    </beans>
    

    在这里插入图片描述

通过上述配置我们看出,Spring中的配置文件的原理与Proxy代理类的newInstance方法的实现原理是一模一样的。

  • TestAdvice测试类:

    package com.test;
    
    import com.dao.Human;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-15 10:04:17
     */
    public class TestAdvice {
        public static void main(String[] args) {
            ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
            Human human1 = (Human)applicationContext.getBean("humanProxy");
            human1.speak("狗腿子");
            System.out.println("------------------------");
            human1.sleep("二狗子");
        }
    }
    
  • 控值台结果:
    在这里插入图片描述
    我们知道代理类值是将一些公共的业务集中在一起,为被代理类的方法进行业务增强处理。,但是有时候并不是所有的核心业务方法都需要进行增强业务处理,所以我们就需要一个业务顾问(Advisor)来询问哪些核心业务需要进行哪写增强操作

  • applicationContext.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"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    
        <!--创建Chinese的Bean实例-->
        <bean id="ch" class="com.dao.impl.Chinese"/>
        <!--创建LogBeforeAdvice的Bean实例-->
        <bean id="logBeforeAdvice" class="com.advice.LogBeforeAdvice"/>
        <!--创建LogAfterAdvice的Bean实例-->
        <bean id="logAfterAdvice" class="com.advice.LogAfterAdvice"/>
    
        <!--通过日志顾问,允许哪些操作-->
        <!--日志顾问(前置)-->
        <bean id="logAdvisorBefore" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
            <property name="mappedName"><!--切入点:拦截方法名(以前缀开头的方法)-->
                <value>sp*</value>
            </property>
            <!--放行的通知Advice-->
            <property name="advice">
                <ref bean="logBeforeAdvice"/>
            </property>
        </bean>
    
        <!--日志顾问(后置)-->
        <bean id="logAdvisorAfter" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
            <property name="mappedName"><!--切入点:拦截方法名(以前缀开头的方法)-->
                <value>sp*</value>
            </property>
            <!--放行的通知Advice-->
            <property name="advice">
                <ref bean="logAfterAdvice"/>
            </property>
        </bean>
    
    
        <!--增量式配置-->
        <bean id="humanProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <!--setter方法注入:目标对象-->
            <property name="target" ref="ch"/>
            <!--面向接口-->
            <property name="interfaces">
                <value>com.dao.Human</value>
            </property>
            <!--代理方法:要要执行业务的拦截方法-->
            <property name="interceptorNames">
                <list>
                    <!--<value>logBeforeAdvice</value>
                    <value>logAfterAdvice</value>-->
                    <value>logAdvisorBefore</value>
                    <value>logAdvisorAfter</value>
                </list>
            </property>
        </bean>
    
    </beans>
    

    在这里插入图片描述

  • 控制台结果:
    在这里插入图片描述

5.7 通过< aop:config >来配置AOP事务机制

在使用Spring框架配置AOP的时候,不管是通过XML配置文件还是注解方式,都需要配置“Pointcut”切入点。
AspectJ中定义切入点表达式:execution( com.aop….(…))*
execution()是常用的切点函数,整个表达式可以分为5部分,其语法如下:

  • 1 execution():表达式主体;
  • 2 第一个*号:表示任意类型的返回值;
  • 3 包名com.aop. . :表示需要拦截的包名,后面的两个点表示当前包和当前包下的所有子包都可以扫描到;
  • 4 第二个*号:表示要扫描的类名,*表示所有的类;
  • 5 第三个*(…)号:表示要扫描的所有类中的所有方法,后面括号代表参数列表,括号里的两个点,代表任意类型的参数均可访问到。
    在这里插入图片描述

我们通过一个用户实例来演示AOP的AspectJ的切面编程思想:

  • 创建UserAction类(目标对象):

    package com.aop.action;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-15 11:10:29
     */
    public class UserAction {
    
        private String name;
    
        public String getName() {
            System.out.println("get方法执行了!");
            return name;
        }
    
        public void setName(String name) {
            System.out.println("set方法执行了!");
            this.name = name;
        }
    }
    
  • 创建Aop类(切面):

    package com.aop;
    
    /**
     * @author 一宿君(CSDN : qq_52596258)
     * @date 2021-07-15 11:09:09
     */
    public class Aop {
        public void beforeInvoke(){
            System.out.println("前置通知!");
        }
    
        public void afterInvoke(){
            System.out.println("后置操作!");
        }
    }
    
  • applicationAop.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"
           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">
    
    
        <!--引入切点(目标对象)-->
        <bean id="userAction" class="com.aop.action.UserAction"/>
    
        <!--引入切面(声明式)-->
        <bean id="aop" class="com.aop.Aop"/>
    
        <!--配置切面-->
        <aop:config>
            <!--execution(* com.aop.action.*.*(..))-->
            <!--表达式   *:函数的返回值       第一个.*:包下所有类     第二个.*:类下所有方法     (..):函数的参数列表(任意)  -->
            <!--execution(* com.aop..*.*(..))-->
            <!--第一个..*是代表所有类及同级包下的子类都可访问到-->
            
            <!--配置全局切入点-->
            <aop:pointcut id="pointcut1" expression="execution(* com.aop..*.*(..))"/>
            <!--此处是只对set开头的方法切入-->
            <aop:pointcut id="pointcut2" expression="execution(* com.aop.action.*.set*(..))"/>
            <!--配置切入面中的方法-->
            <aop:aspect ref="aop">
                <aop:before method="beforeInvoke" pointcut-ref="pointcut1"/>
                <aop:after-returning method="afterInvoke" pointcut-ref="pointcut2"/>
            </aop:aspect>
        </aop:config>
    
    </beans>
    
  • 控制台结果:
    在这里插入图片描述
    在这里插入图片描述

5.8 基于Annotation(注解)的装配方式实现AOP事务机制

在Spring中,尽管使用XML配置文件可以实现Bean的装配工作,但如果应用中有很多Bean时也有很多增强事务通知时会到值XMl配置文件过于臃肿,不利于后续的升级维护和扩展,因此,Spring就根据这种情况提供了基于Annotation注解技术的全面支持

Spring中常用的注解方法:

注解名称 注解描述
@Component 描述Spring中的Bean,是一个泛化的概念,仅仅表示一个组件(Bean),并且可以作用在任何层次,使用时只需将该注解标注在相应类上即可。
@Repository 用于将数据访问层(Dao层)的实现类标识为Spring中的Bean,其功能与@Component相同
@Service 用于将业务层(Service层)的实现类标识为Spring中的Bean,其功能与@Component相同
@Controller 用于将控制层(Controller层)的类标识为Spring中的Bean,其功能与@Component相同
@Autowired 用于对Bean的属性变量、属性的setter方法以及构造方法进行标注,配合对应的注解处理器完成Bean的自动装配工作,默认按照Bean的类型进行装配。
@Resource 其作用与Autowired一样。其区别在于@Autowired默认按照Bean类型装配,而@Resource默认按照Bean实例名称进行装配
@Qualifier 与@Autowired注解配合使用,会将默认的按Bean类型装配修改为按Bean实例名称装配,Bean的实例名称由@Qualifier的参数指定
@Aspect 一般作用在一个共用模块类上,该模块中有多个影响类的公共行为,我们称之为切面,该注解的作用就是把当前的切面类标识为一个切面共容易读取
@Pointcut Pointcut是织入Advice的触发条件,每个Pointcut的定义包括2两部分,一是:表达式;二是:方法签名。方法签名必须是public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码
@Around 环绕增强,相当于MethodInterceptor
@Before 前置增强,相当于BeforeAdvice的功能
@AfterReturning 后置增强,想当于AfterReturningAdvice,方法正常退出时执行
@AfterThrowing 异常抛出增强,相当于ThrowsAdvice
@After final最终增强,不管是抛出异常还是正常退出都会执行

UserAction.javaAop.java依然是上述的;

我们把上述在xml配置文件使用的< aop:config > 的相关配置以及Aop的Bean实例注释掉,并开启注解开启注解扫描包
在这里插入图片描述

  • applicationAop.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"
           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">
    
    
        <!--引入切点(目标对象)-->
        <bean id="userAction" class="com.aop.action.UserAction"/>
    
        <!--开启自动扫描com包下的所有类(包括切面类)-->
        <context:component-scan base-package="com.*"/>
    
        <!--AOP自动代理(开启注解)-->
        <aop:aspectj-autoproxy/>
    </beans>
    

    在这里插入图片描述

  • 定义基于注解的AopAnnotation类:
    package com.aop;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;/** * @author 一宿君(CSDN : qq_52596258) * @date 2021-07-15 12:43:47 */@Component //组件 bean@Aspect //定义为切面(公共行为的集合类)public class AopAnnotations {    @Pointcut("execution(* com.aop..*.*(..))") //此处是定义全局切入点    public void MyPoint(){};    @Before("MyPoint()")    public void beforeInvoke(){        System.out.println("前置通知!");    }    @AfterReturning(value = "MyPoint()",returning = "result")    public void afterReturning(Object result){        System.out.println("后置通知!");            System.out.println("-----------" + result + "-----------");    }    @After("MyPoint()")    public void finalAdvice(){        System.out.println("最终通知!");    }    @AfterThrowing("MyPoint()")    public void exceptionInvoke(){        System.out.println("异常通知!");    }    @Around("MyPoint()")    public Object aroundInvoke(ProceedingJoinPoint joinPoint) {        Object obj = null;        System.out.println("环绕通知-前!");        try {            obj = joinPoint.proceed();//执行业务代码(注意此处如果直接try——catch,会使上述异常通知失效,即无法执行)        } catch (Throwable throwable) {            throwable.printStackTrace();        }        System.out.println("环绕通知-后!");        return obj;    }    }
  • 对各种事务增强通知的简要解释:
    在这里插入图片描述
    在这里插入图片描述
  • 执行TestAop测试类:
    public class TestAop {    public static void main(String[] args) {        ApplicationContext context = new ClassPathXmlApplicationContext("applicationAop.xml");        UserAction userAction = (UserAction)context.getBean("userAction");        userAction.setName("zs");        userAction.getName();    }}
  • 看控制台结果:
    在这里插入图片描述
  • 上述执行时无异常的结果,如果核心业务在执行时出现异常,结果会如何?

    在这里插入图片描述

  • 再次运行TestAop类,查看控制台信息:
    在这里插入图片描述
  • 对于上述使用try——catch捕获异常当然是不可取得,所以我们将其声明异常并抛出Throwable:

在这里插入图片描述

  • 再次执行TestAop类,查看控制台结果:
    在这里插入图片描述

最后应注意一点,上述通知的执行顺序

如果没有异常:(自上而下)

  • 前环绕通知
  • 前置通知
  • 核心业务方法
  • 后环绕通知
  • 最终通知
  • 后置通知

如果程序出现异常:(自上而下)

  • 前环绕通知
  • 前置通知
  • 核心业务方法
  • 最终通知
  • 异常通知

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/189455.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之家——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!