• Spring之旅第五篇-AOP详解


    一、什么是AOP?

    Aspect oritention programming(面向切面编程),AOP是一种思想,高度概括的话是“横向重复,纵向抽取”,如何理解呢?举个例子:访问页面时需要权限认证,如果每个页面都去实现方法显然是不合适的,这个时候我们就可以利用切面编程。

    每个页面都去实现这个方法就是横向的重复,我们直接从中切入,封装一个与主业务无关的权限验证的公共方法,这样可以减少系统的重复代码,降低模块之间的耦合度,简单的示意图如下:

    二、应用场景

    AOP用来封装横切关注点,具体可以在下面的场景中使用:

    Authentication 权限

    Caching 缓存

    Context passing 内容传递

    Error handling 错误处理

    Lazy loading 懒加载

    Debugging  调试 l

    ogging, tracing, profiling and monitoring 记录跟踪 优化 校准

    Performance optimization 性能优化

    Persistence  持久化

    Resource pooling 资源池

    Synchronization 同步

    Transactions 事务

    三、相关概念

    1.连接点(Joinpoint) 所谓连接点是指那些可能被拦截到的方法。例如:所有可以增加的方法

    2.切点(Pointcut) 已经被增强的连接点

    3.增强(Advice) 增强的代码

    4.目标对象(Target) 目标类,需要被代理的类

    5.织入(Weaving) 是指把增强advice应用到目标对象target来创建新的代理对象proxy的过程

    6.代理(Proxy) 一个类被AOP织入增强后,就产生出了一个结果类,它是融合了原类和增强逻辑的代理类。

    7.切面(Aspect)切入点+通知

    通知类型:Spring按照通知Advice在目标类方法的连接点位置,可以分为5类

    • 前置通知 (在目标方法执行前实施增强)

    • 后置通知(在目标方法执行后实施增强)

    • 环绕通知(在目标方法执行前后实施增加)

    • 异常抛出通知(在方法跑出异常时通知)

    • 引介通知(在目标类中添加一些新的方法和属性)

    四、实现原理

    AOP的实现关键在于AOP框架自动创建的AOP代理。AOP代理主要分为两大类:

    静态代理:使用AOP框架提供的命令进行编译,从而在编译阶段就可以生成AOP代理类,因此也称为编译时增强;静态代理一Aspectj为代表。

    动态代理:在运行时借助于JDK动态代理,CGLIB等在内存中临时生成AOP动态代理类,因此也被称为运行时增强,Spring AOP用的就是动态代理。

    4.1 静态代理

    例子:在增加员工和删除员工时增加事务处理

    //员工类
    public class Employee {
        private Integer uid;
    
        public void setUid(Integer uid) {
            this.uid = uid;
        }
    
        public Integer getUid() {
    
            return uid;
        }
    
        private Integer age;
    
        private String name;
    
        public Integer getAge() {
            return age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }

    员工接口:

    //员工接口
    public interface EmployeeService {
        //新增方法
        void addEmployee(Employee employee);
        //删除方法
        void deleteEmployee(Integer uid);
    }

    员工实现:

    //员工方法实现
    public class EmployeeServiceImpl implements EmployeeService {
        @Override
        public void addEmployee(Employee employee) {
            System.out.println("新增员工");
        }
    
        @Override
        public void deleteEmployee(Integer uid) {
            System.out.println("删除员工");
    
        }
    }

    事务类:

    //事务类
    public class MyTransaction {
        //开启事务
        public void before(){
            System.out.println("开启事务");
        }
        //提交事务
        public void after(){
            System.out.println("提交事务");
        }
    }

    代理类:

    //代理类
    public class ProxyEmployee implements EmployeeService {
        //
        private EmployeeService employeeService;
    
        private MyTransaction myTransaction;
    
        public ProxyEmployee(EmployeeService employeeService,MyTransaction myTransaction)
        {
           this.employeeService=employeeService;
           this.myTransaction=myTransaction;
        }
        @Override
        public void addEmployee(Employee employee) {
             myTransaction.before();
             employeeService.addEmployee(employee);
             myTransaction.after();
        }
    
        @Override
        public void deleteEmployee(Integer uid) {
             myTransaction.before();
             employeeService.deleteEmployee(uid);
             myTransaction.after();
        }
    }

    测试:

    @Test
        public void fun1(){
            MyTransaction transaction = new MyTransaction();
            EmployeeService EmployeeService = new EmployeeServiceImpl();
            //产生静态代理对象
            ProxyEmployee proxy = new ProxyEmployee(EmployeeService, transaction);
            proxy.addEmployee(null);
            proxy.deleteEmployee(0);
        }

    结果:

    这是静态代理的实现方式,静态代理有明显的缺点:

    1、代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。

    2、如果接口增加一个方法,比如 EmployeeService增加修改 updateEmployee()方法,则除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

    4.2 动态代理

    动态代理就不要自己手动生成代理类了,我们去掉 ProxyEmployee.java 类,增加一个 ObjectInterceptor.java 类

    public class ObjectInterceptor implements InvocationHandler {
    
        //目标类
        private Object target;
        //切面类(这里指事务类)
        private MyTransaction transaction;
    
        //通过构造器赋值
        public ObjectInterceptor(Object target,MyTransaction transaction){
            this.target = target;
            this.transaction = transaction;
        }
    
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            this.transaction.before();
            method.invoke(target,args);
            this.transaction.after();
            return  null;
        }
    }

    测试:

    @Test
    public void fun2(){
        //目标类
        Object target = new EmployeeServiceImpl ();
        //事务类
        MyTransaction transaction = new MyTransaction();
        ObjectInterceptor proxyObject = new ObjectInterceptor(target, transaction);
        /**
         * 三个参数的含义:
         * 1、目标类的类加载器
         * 2、目标类所有实现的接口
         * 3、拦截器
         */
        EmployeeService employeeService = (EmployeeService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), proxyObject);
        employeeService.addEmployee(null);
        employeeService.deleteEmployee(0);
    }

    结果:

     五、spring的处理AOP的方式

    spring 有两种方式实现AOP的:一种是采用声明的方式来实现(基于XML),一种是采用注解的方式来实现(基于AspectJ)

    5.1 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:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans.xsd
             http://www.springframework.org/schema/context
             http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
         <!--目标类-->
         <bean name="employeeService" class="com.yuanqinnan.aop.EmployeeServiceImpl"></bean>
         <bean name="transaction" class="com.yuanqinnan.aop.MyTransaction"></bean>
         <aop:config>
              <aop:aspect ref="transaction">
                   <aop:pointcut id="pointcut" expression="execution(* com.yuanqinnan.aop.EmployeeServiceImpl..*(..))"/>
                   <!-- 配置前置通知,注意 method 的值要和 对应切面的类方法名称相同 -->
                   <aop:before method="before" pointcut-ref="pointcut"></aop:before>
                   <!--配置后置通知,注意 method 的值要和 对应切面的类方法名称相同-->
                   <aop:after-returning method="after" pointcut-ref="pointcut"/>
              </aop:aspect>
         </aop:config>
    
    </beans>

    测试:

    @Test
    public void fun3(){
        ApplicationContext context = new ClassPathXmlApplicationContext("META-INF/applicationContext.xml");
        EmployeeService employeeService = (EmployeeService) context.getBean("employeeService");
        employeeService.addEmployee(null);
    }

    结果:

    补充:

    1.aop:pointcut如果位于aop:aspect元素中,则命名切点只能被当前aop:aspect内定义的元素访问到,为了能被整个aop:config元素中定义的所有增强访问,则必须在aop:config下定义切点。

    2.如果在aop:config元素下直接定义aop:pointcut,必须保证aop:pointcutaop:aspect之前定义。aop:config下还可以定义aop:advisor,三者在aop:config中的配置有先后顺序的要求:首先必须是aop:pointcut,然后是aop:advisor,最后是aop:aspect。而在aop:aspect中定义的aop:pointcut则没有先后顺序的要求,可以在任何位置定义。

    aop:pointcut:用来定义切入点,该切入点可以重用;

    aop:advisor:用来定义只有一个通知和一个切入点的切面;

    aop:aspect:用来定义切面,该切面可以包含多个切入点和通知,而且标签内部的通知和切入点定义是无序的;和advisor的区别就在此,advisor只包含一个通知和一个切入点。

    3.在使用spring框架配置AOP的时候,不管是通过XML配置文件还是注解的方式都需要定义pointcut"切入点" 例如定义切入点表达式 execution(* com.sample.service.impl...(..)) execution()是最常用的切点函数,其语法如下所示:

    整个表达式可以分为五个部分: (1)、execution(): 表达式主体。

    (2)、第一个号:表示返回类型,号表示所有的类型。

    (3)、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.sample.service.impl包、子孙包下所有类的方法。

    (4)、第二个号:表示类名,号表示所有的类。

    (5)、(..):最后这个星号表示方法名,号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。

    5.2 注解配置

    新建注解类:

    @Component
    @Aspect
    public class AopAspectJ {
        /**
         * 必须为final String类型的,注解里要使用的变量只能是静态常量类型的
         */
        public static final String EDP="execution(* com.yuanqinnan.aop.EmployeeServiceImpl..*(..))";
    
        /**
         * 切面的前置方法 即方法执行前拦截到的方法
         * 在目标方法执行之前的通知
         * @param jp
         */
        @Before(EDP)
        public void doBefore(JoinPoint jp){
    
            System.out.println("=========执行前置通知==========");
        }
    
    
        /**
         * 在方法正常执行通过之后执行的通知叫做返回通知
         * 可以返回到方法的返回值 在注解后加入returning
         * @param jp
         * @param result
         */
        @AfterReturning(value=EDP,returning="result")
        public void doAfterReturning(JoinPoint jp,String result){
            System.out.println("===========执行后置通知============");
        }
    
        /**
         * 最终通知:目标方法调用之后执行的通知(无论目标方法是否出现异常均执行)
         * @param jp
         */
        @After(value=EDP)
        public void doAfter(JoinPoint jp){
            System.out.println("===========执行最终通知============");
        }
    
        /**
         * 环绕通知:目标方法调用前后执行的通知,可以在方法调用前后完成自定义的行为。
         * @param pjp
         * @return
         * @throws Throwable
         */
        @Around(EDP)
        public Object doAround(ProceedingJoinPoint pjp) throws Throwable{
    
            System.out.println("======执行环绕通知开始=========");
            // 调用方法的参数
            Object[] args = pjp.getArgs();
            // 调用的方法名
            String method = pjp.getSignature().getName();
            // 获取目标对象
            Object target = pjp.getTarget();
            // 执行完方法的返回值
            // 调用proceed()方法,就会触发切入点方法执行
            Object result=pjp.proceed();
            System.out.println("输出,方法名:" + method + ";目标对象:" + target + ";返回值:" + result);
            System.out.println("======执行环绕通知结束=========");
            return result;
        }
    
        /**
         * 在目标方法非正常执行完成, 抛出异常的时候会走此方法
         * @param jp
         * @param ex
         */
        @AfterThrowing(value=EDP,throwing="ex")
        public void doAfterThrowing(JoinPoint jp,Exception ex) {
            System.out.println("===========执行异常通知============");
        }
    }

    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:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
             http://www.springframework.org/schema/beans/spring-beans.xsd
             http://www.springframework.org/schema/context
             http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
        <context:component-scan base-package="com.yuanqinnan.aop" ></context:component-scan>
        <!-- 声明spring对@AspectJ的支持 -->
        <aop:aspectj-autoproxy/>
        <!--目标类-->
        <bean name="employeeService" class="com.yuanqinnan.aop.EmployeeServiceImpl"></bean>
    </beans>

    测试:

    @Test
    public void fun4(){
        ApplicationContext act =  new ClassPathXmlApplicationContext("META-INF/applicationContext.xml");
        EmployeeService employeeService = (EmployeeService) act.getBean("employeeService");
        employeeService.addEmployee(null);
    }

    结果:

    pringAOP的知识就总结到这里

  • 相关阅读:
    equals()与=的区别
    HashTable和HashMap的区别
    shell高级用法——磁盘管理 创建虚拟的磁盘映射到一个文件
    shell妙用之——dd命令合并多个烧录文件为一个flash镜像
    自动解包ROM 文件获取uboot,uboot-spl ,uImage, rootfs.tar.gz 并烧写SD卡
    运用层通过shell脚本直接操控gpio
    shell脚本之位运算+for循环+返回值承接+shell小数运算
    shell函数递归调用实现目录的对比拷贝
    用debootstrip制作debian环境的rootfs
    shell命令的高级使用之---选择性copy
  • 原文地址:https://www.cnblogs.com/yuanqinnan/p/10507411.html
Copyright © 2020-2023  润新知