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

Posted 一宿君

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring系列Spring中AOP面向切面的编程(动态代理)相关的知识,希望对你有一定的参考价值。

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一般作用在一个共用模块类上,该模块中有多个影响类的公共行为,我们称之为切面,该注解的作用就是把当前的切面类标识为一个切面共容易读取
@PointcutPointcut是织入Advice的触发条件,每个Pointcut的定义包括2两部分,一是:表达式;二是:方法签名。方法签名必须是public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码
@Around环绕增强,相当于MethodInterceptor
@Before前置增强,相当于BeforeAdvice的功能
@AfterReturning后置增强,想当于AfterReturningAdvice,方法正常退出时执行
@AfterThrowing异常抛出增强,相当于ThrowsAdvice
@Afterfinal最终增强,不管是抛出异常还是正常退出都会执行

@Resource中有两个重要属性:name和type。Spring将name解析为Bean实例名称,type属性解析为Bean实例类型。如果指定name属性,则按Bean实例名称进行装配;如果指定type属性,则按照Bean类型进行装配;如果都不指定,则先按照Bean实例名称装配,如果不能匹配,再按照Bean类型进行装配;如果都无法匹配,则抛出NoSuchBeanDefinitionException异常。

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

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