锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

Spring之AOP

时间:2022-09-16 10:30:00 6tx酸度变送器

一、AOP概述

1、什么是AOP

OOP(Object-Oriented Programming),面向对象编程。

AOP(Aspect-Oriented Programming),面向切面编程。

在AOP切面是最重要的概念。能否理解切面的概念,决定了能否掌握AOP技术。

2.生活中的面向切面案例
1)案例一 丰巢快递柜

丰巢智能快递柜起源于2015年,是所有快递公司和电子商务物流的24小时自助开放平台。最初的出现是为快递行业提供最后一公里的解决方案服务。

整个物品的寄送过程大致如下:

【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】
【寄件】->【集散】->【跨省】->【分拣】->【快递员】->【送货上门】

在上述过程中,前几步几乎都是程式化的。只有最后一个环节最容易出现问题。例如,社区禁止快递员进入,收货人不在家,收货人无法打电话……而且,这种现象并非个案,但所有的快递公司和街道上的快递员都会面临这些问题。

丰巢很好地解决了这个问题。在社区门口设置智能快递柜,对所有快递公司开放。旧金山快递、圆通、云达、中通等快递均可存放在丰巢,用户可统一到丰巢取快递。

不同的快递公司有不同的业务流程。例如,旧金山快递选择长途运输,而其他公司选择陆路运输。或者,从同一地址发送到同一目的地,旧金山快递可能会转移三次,圆通会转移四次,云达会转移两次……然而,丰巢并不在乎。它所关心的只是所有快递公司都会面临的问题——最后一公里,送货上门。

最后一公里相当于整个快递行业的一个切面。丰巢是针对这一切面存在的问题提出的解决方案。

我们可以理解生活中面向切面的问题,用于解决某一环节不同领域或相似领域的相同问题。

2)银行排号系统

张三到工行办理开户手续:

前往工行-> 排队-> 复印身份证-> 填表-> 柜台办理-> 签字-> 离开银行

李四到农行办理存款:

前往农行-> 排队-> 提供卡和钱-> 柜台存入-> 回执单签字-> 离开银行

王五到建行办理预留手机号变更:

前往建行-> 排队-> 填表-> 柜台办理-> 验证码-> 确认-> 离开银行

无论去哪家银行,办理什么业务,都要排队。在早期的银行中,人们必须站成一个长队,通过队列中的位置来决定业务的顺序。所以这种方式给人们带来了极大的不便。排队是一件很无聊的事情。每个人站久了都会腰酸腿痛。而且排队的时候不能离队。如果有人辛辛苦苦排了一个小时的队,突然内心焦虑,回来后要站到队尾再排队。……

针对这种情况,银行排号系统出现在人们的视线中。每个人都可以先去银行取号纸,然后在银行设立的等候区休息,等待系统广播依次打电话。

在这种情况下,排队就像我们从所有银行业务中抽离出来的一个切面。每家银行的业务流程略有不同,客户到银行需要处理的业务因人而异。但在整个过程中,排队业务是常见的。然后我们可以把它拉出来,作为一个切面单独处理。

3)食堂、快餐、外卖、菜鸟驿站、自动售货机……

A公司员工-> 上班-> 讲课-> 吃饭-> 买饮料-> 回办公室-> 继续上班

B公司员工 -> 上班-> 研究销售机会-> 吃饭 -> 买饮料 -> 回办公室 ->上班

二、程序中的面向切面

程序中也存在许多切面问题,即与完整的业务逻辑执行过程无关。

问题只是出现了多个类似的过程同一个环节中。

例如,在传统的三层结构中,我们需要Service在执行开始和执行结束时行结束时。

浏览器 >> 登录 >> LoginServlet >> CustomerService >> CustomerDao >> 数据库

浏览器 >> 注册 >> RegisterServlet >> CustomerService >> CustomerDao >> 数据库

浏览器 >> 查询书籍 >> ViewBookServlet >> BookService >> BookDao >> 数据库

想要实现Service植入日志的记录很简单,直接在每一个service方法的开始和结束可以调用日志模块。然而,这种做法会带来许多问题:大量代码重复、系统需求和业务需求耦合等……

三、为什么要用?AOP

1、AOP是OOP思想的补充和完善

传统的OOP(面向对象编程)编程在解决实际问题时仍存在一些不足和困难。

而AOP正是为了解决这些问题。

2、AOP能做什么

在OOP正是由于分散在各地且与业务相关的代码(横切代码)的存在,模块。

AOP则将封装好的对象剖开,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块,这个模块被命名为“切面”(Aspect)。该截面提取并包装了与业务无关但由业务模块共同调用的逻辑,减少了系统中的重复代码,减少了模块之间的耦合,提高了系统的可维护性。

四、Spring AOP和AOP的关系

注意:AOP只是一种设计理念,不是特定的技术,更不用说Spring专属技术。

针对AOP许多公司开发自己的想法AOP实现框架的方式也不同。

例如:AspectJ、JBoss AOP、Nanning Aspects、Spring AOP……

Spring AOP只是其中之一,也是最好的。

五、代理模式

学习Spring AOP先了解代理模式。Spring中的AOP技术,就是通过动态代理来实现的。

1.生活中的代理案例
  • 请明星唱歌、跳舞、拍戏,找经纪人谈价格

  • 通过中介租房

  • 找律师打官司

  • 通过销售点购买火车票和机票

代理思想的原则:
  • 目标对象被隐藏

    代理模式是隐藏委托对象的控制权,改为访问代理对象。

  • 代理和本尊具有相同的功能

    刘德华技能:唱歌、跳舞、拍摄

    刘德华的经纪人:唱歌、跳舞、拍戏

  • 代理只做一些预处理和善后工作,但真正的业务还是要靠原对象来完成。

    我们邀请刘德华通过经纪人唱歌,但这首歌仍然由刘德华自己唱。

    我们通过中介租房,但房子最终还是要自己住。

    我们委托律师帮忙提起诉讼,庭审还是要自己出庭。

    如果诉讼输了,你必须自己坐牢,而不是律师帮你坐牢。

2.程序中的代理模式

代理模式是OOAD在不修改原始代码的基础上,设计模式之一可以扩展程序的功能。

例如,一个类需要在现有的基础上扩展一些功能,但直接在自己的类中写代码并不方便,所以你可以找到一个类来做它的代理类。

使代理对象完成需要扩展的功能,而核心功能仍然依赖于原始对象的实现。

代理模式的一般做法是为某一对象提供代理对象,并由代理对象控制引用原对象。

需要扩展功能的类别:委托类——委托他人为自己工作的人——刘德华

  • 委托对象也称为[目标对象]target

帮助委托扩展功能的类别:代理-代理他人处理事情的人-刘德华的经纪人

  • 代理对象也称为[代理对象]proxy
3.代理模式分类

静态代理:在程序运行前,代理.clas文件就已经存在。

动态代理:在程序运行时,代理类是运用了反射技术或字节码技术动态创建而成的。

静态代理需要程序员手动地为每一个委托类编写对应的代理类代码,仅仅为了方便理解动态代理,不可能在生产中使用。

4、静态代理

业务逻辑层接口:

public interface CustomerService { 
        
    // 用户登录方法
    public void login(String username, String password);

    // 用户注册方法
    public void register(String username, String password, String gender, int age);
}

业务逻辑层实现类:

public class CustomerServiceImpl implements CustomerService { 
        
    @Override
    public void login(String username, String password) { 
        
        // 处理登录业务逻辑的代码
        System.out.println("处理用户登录的代码...");	
    }

    @Override
    public void register(String username, String password, String gender, int age) { 
        
        // 处理注册业务逻辑的代码
        System.out.println("处理用户注册的代码...");
    }
}

日志类:

public class Logger { 
        
	public void info(String msg) { 
        
		System.out.println("[日志输出]" + msg);
	}
}

业务逻辑层代理类:

public class CustomerServiceProxy implements CustomerService { 
        

    // 目标对象
    private CustomerService target;

    // 日志对象
    Logger logger = new Logger();

    // 目标对象通过构造器传入
    public CustomerServiceProxy(CustomerService target) { 
        
        this.target = target;
    }

    @Override
    public void login(String username, String password) { 
        
        // 日志输出
        logger.info("login方法被调用了...");

        // 让目标对象执行它真正处理业务逻辑的方法
        target.login(username, password);

        // 日志输出
        logger.info("login方法调用结束了...");
    }

    @Override
    public void register(String username,String password,
                             String gender,int age){ 
        
        // 日志输出
        logger.info("register方法被调用了...");

        // 让目标对象执行它真正处理业务逻辑的方法
        target.register(username, password, gender, age);

        // 日志输出
        logger.info("register方法调用结束了...");
    }
}

测试类:

public class Test { 
        
	public static void main(String[] args) { 
        
		// 创建目标对象
		CustomerService service = new CustomerServiceImpl();

		// 创建代理对象,注入目标对象
		CustomerServiceProxy proxy = new CustomerServiceProxy(service);

		// 访问代理对象方法
		proxy.login("tom", "123");
		proxy.register("tom","123","男",23);
	}
}

上述案例为我们展示了静态代理的基本实现过程,具有如下特点:

1)委托类对象(目标对象)被“隐藏”

代理模式的主要方式就是要将目标对象隐藏起来,控制外界对它的直接访问。而改为访问代理对象。

2)委托类和代理类实现相同的接口

目的是让代理类具有和委托类相同的行为。

刘德华:唱歌、跳舞、拍戏…

刘德华经纪人:接唱歌的活儿、接跳舞的活儿、接拍戏的活儿…

3)代理类中包含委托类对象(目标对象)

代理对象只是做预先工作和善后工作,真正的功能还要靠目标对象自己实现。

通过上述案例,我们可以发现,代理模式很大程度上降低了登录代码和日志代码的耦合。

首先,在业务逻辑层中看不到任何日志代码的存在。如果将来日志类发生了改变,对业务逻辑层却没有任何影响。

这样一来,降低了项目后期维护的难度,也易于进行功能扩展。而且不同分工的程序员就更专注地开发自己的组件,提升代码质量。

但是这样的静态代理也存在一定的缺点,程序员不得不为每一个有需求的类编写一个代理类出来。不仅费时费力,也不利于修改,更是使项目变得十分臃肿。

我们可以通过使用动态代理来解决这一问题。

5、动态代理

动态代理的特点是:在编译期间,不需要手动编写代理类。而是在程序运行期间动态生成一个代理类对象。

不仅简化了编程工作,而且提高了软件系统的可扩展性,因为使用它可以生成任意类型的动态代理类。

动态代理有两种实现方式:

  • JDK动态代理(要求掌握)

    通过Java反射机制动态创建代理对象。

  • CGLib代理(稍作了解)

    通过ASM字节码技术创建代理对象。

1)JDK动态代理案例

日志类、业务逻辑层接口、业务逻辑层实现类和静态代理案例中相同:

class Logger、interface CustomerService、class CustomerServiceImpl

使用JDK动态代理,首先需要提供一个InvocationHandler接口的实现类。

这个接口的实现类,类似我们后面在AOP中要提到的“切面类”概念。

简而言之,我们要对委托类进行扩展的功能,都在这个类中定义。

也就是说,在静态代理案例中,我们在代理类方法中执行的日志输出代码,现在全部都要定义在这个InvocationHandler的实现类中。


InvocationHandler实现类:

public class MyHandler implements InvocationHandler { 
        

    // 目标对象
    Object target;

    // 日志对象
    Logger logger = new Logger();

    // 构造器(传入目标对象)
    public MyHandler(Object target) { 
        
        this.target = target;
    }

    // 参数:
    // 1.proxy 将来要生成的代理对象
    // 2.method 目标对象中的目标方法
    // 3.args 调用代理方法时传入的参数列表
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
        
        // 目标方法被调用前执行的操作
        logger.info(method.getName() + "方法被调用了...");

        // 调用目标对象的目标方法,并获取返回值
        Object returnValue = method.invoke(target, args);	

        // 目标方法调用结束之后执行的操作
        logger.info(method.getName() + "方法调用结束了...");		
 
        // 将返回值返回
        return returnValue;
    }
}

测试类:

public class Test { 
        
    public static void main(String[] args) throws Exception { 
        
        // 获取委托类镜像对象
        Class cls = Class.forName("com.briup.CustomerServiceImpl");
		
        // 获取委托类的类加载器
        ClassLoader loader = cls.getClassLoader();

        // 获取委托类实现的所有接口
        // 返回值类型是装有所有实现接口镜像对象的Class[]数组
        Class[] interfaces = cls.getInterfaces();

        // 创建目标对象
        CustomerService target = new CustomerServiceImpl();

        // 创建InvocationHandler接口的实现类 
        InvocationHandler h = new MyHandler(target);

        // 动态代理的核心方法,返回值为动态生成的代理对象。
        // 参数:
        // 1.loader:委托类的类加载器 
        // 2.interfaces:委托类实现的所有接口
        // 3.h:invocation实现类对象,指定代理对象要做的事情
        CustomerService proxy = (CustomerService) Proxy.newProxyInstance(
            loader, interfaces, h);

        // 调用代理对象的方法
        proxy.login("username", "password");
        proxy.register("username", "password","男",24);
    }
}

这样的代码看起来十分复杂,类、接口和方法一时难以理解。

但是这样做的好处也是显而易见的:

  • 可扩展性强

    如果需要扩展业务逻辑功能,那么只需要在service接口添加方法,并在实现类给出具体实现即可。

    测试代码则无需改动,执行时新添加的方法也会被代理。而在静态代理中,不仅要修改service接口和实现类,每一个受到影响的proxy类也要手动修改。

  • 代码复用率高

    在静态代理中,一个代理类只能服务于一个委托类。假如将来项目中有十个类需要进行日志输出,那么委托类也要编写十个。

    在动态代理中,我们编写的MyHandler(InvocationHandler接口的实现类),可以为任何一个需要代理的service服务。假设系统中所有的service都需要进行日志输出,那么也只需要编写一个MyHandler即可。

2)CGLib代理

JDK动态代理要求目标类实现接口,才能对其进行代理。

CGLib的好处就是在于对于没有实现接口的类,也可以对其进行动态代理。

CGLib采用了非常底层的ASM字节码技术,其原理是通过字节码技术为目标类创建一个子类对象,并在子类对象中拦截所有父类方法的调用。然后在方法调用前后调用后都可以加入自己想要执行的代码。

在CGLib代理中,我们同样需要将“横切代码”定义在某个类中,这个类是MethodInterceptor接口的子类(类似JDK动态代理中InvocationHandler接口的实现类),我们可以把它称之为“拦截器类”。将来目标方法被调用之前,就会先一步被拦截,调用到拦截器中预先定义好的横切代码。


MethodInterceptor实现类:

public class MyInterceptor implements MethodInterceptor { 
        

    // 日志对象
    Logger logger = new Logger();

    // 参数:
    // 1.proxyObj:将来生成的代理对象
    // 2.method:代理对象中被调用的方法
    // 3.args:调用方法传入的参数列表
    // 4.mProxy:该参数可以调用父类中的方法

    @Override
    public Object intercept(Object proxyObj, Method method, 
                            Object[] args, MethodProxy mProxy) throws Throwable { 
        

        // 日志输出
        logger.info(method.getName()+"方法被调用了...");

        // 子类对象调用父类方法
        Object returnValue = mProxy.invokeSuper(proxyObj, args);

        // 日志输出
        logger.info(method.getName()+"方法调用结束了...");
		
        // 将目标方法调用结果返回
        return returnValue;
    }
}

测试类:

public class Test { 
        
	public static void main(String[] args) { 
        
		
		// 创建拦截器对象
		MethodInterceptor ic = new MyInterceptor();
		
		// 创建Enhancer对象,用来生成某个类的子类对象
		Enhancer enhancer = new Enhancer();
		
		// 指定父类(要生成哪个类的子类)
		enhancer.setSuperclass(CustomerService.class);
		
		// 指定调用哪个拦截器
		enhancer.setCallback(ic);
		
		// 生成子类对象
		CustomerService proxy = (CustomerService) enhancer.create();
		
		// 访问代理对象
		proxy.login("tom", "123");
	}
}	

【面试题】1、请比较JDK动态代理和cglib?

1)JDK动态代理所需要使用的类和接口全部来自于原生API(java.lang.reflect)

​ cglib属于第三方技术,需要额外引入依赖(也可以直接从spring框架中使用它)。

2)底层的实现方式不同:

​ JDK动态代理的思路是使用委托类的镜像、类加载器,通过运行期间内存中的活动,动态生成一个代理对象。该代理对象和目标对象具有相同的行为(通过实现同一个接口实现)。代理对象和目标对象的关系是包含关系(一个类中维护另外一个类的引用),在设计上这种关系称之为弱耦合的聚合或组合关系。

​ cglib的实现思路是创建一个目标对象的子类对象,通过继承来实现和它用有相同的特性。在设计上这种关系可以看做强耦合的关系。

3)JDK动态代理要求被代理的类必须要实现接口,而cglib不做要求,实现也行不实现也行。

2、Spring中的AOP功能是通过哪种技术实现的?

默认是使用JDK动态代理实现,但是可以通过配置修改,改为使用cglib。

六、Spring中AOP的实现

1、AOP中的基本名词和概念
名词 解释
AOP 面向切面编程
Aspect 切面/切面类,比如调用日志类、日志模块、权限验证的代码
JoinPoint 连接点,即被拦截的方法。在Spring AOP中连接点就是java中的方法。
PointCut 切入点/切点,一组连接点的集合。
Advice 通知/拦截器,指定切面类代码织入到切入点之前、之后…… 类似动态代理中的InvocationHandler接口的实现类、MethodInterceptor接口的实现类
Advisor 增强器,指定类中的哪些方法需要被拦截
Target 目标对象、委托类对象,被代理的对象,本尊对象
Proxy 代理对象
Wave 织入
2、Advice通知类型

Advice通知/拦截器用来指定将切面类中的代码织入到连接点的什么位置。

选择不同的通知类型,可以决定像日志输出这样的操作(切面代码)是发生在执行目标方法之前,还是之后,还是前后都有……具体有下列四种通知类型:

类型 描述
前置通知
(Before advice)
在某些连接点之前执行的通知
返回后通知
(After returning advice)
在某些连接点正常完成后执行的通知(方法正常结束,没有异常)
抛出异常后通知
(After throwing advice)
在某些连接点抛出异常退出时执行的通知
环绕通知
(Around Advice)
包围一个连接点的通知。例如事务的处理,就需要这样的通知。
因为事务需要在方法前开启,在方法后提交,以及方法抛出异常时候回滚。
注:在spring中,连接点(join point)指的就是类中的方法

在Spring中,通知是以类的形式体现的。而这个类属于哪一种通知,就要看它具体实现了哪一个接口,对应关系如下:

通知类型 实现的接口
前置通知 MethodBeforeAdvice
返回后通知 AfterReturningAdvice
抛出异常后通知 ThrowsAdvice
环绕通知 MethodInterceptor
3、五种通知类型案例

Spring AOP中配置通知有两种方式:

xml配置文件配置通知过程较为繁琐,实际项目中多使用aop:config的方式配置通知。但是xml配置的方式的使用能够帮助理解aop:config标签的原理和大致过程。

1)xml配置文件案例

注意:在如下案例中同时配置了多种通知,每种通知都可以单独配置和使用。


实体类

Account.java

public class Account { 
        
    private int id;
    private String name;
    private double balance;// 余额
    // getters&setters...
}

Dao层接口

AccountDao.java

public interface AccountDao { 
        
    // 取款 账号减去多少钱
    void withdraw(Account account, double amount);

    // 存款 账号加上多少钱
    void deposit(Account account, double amount);
}

Dao层实现类

AccountDaoImpl.java

public class AccountDaoImpl implements AccountDao { 
        

    @Override
    public void withdraw(Account account, double amount) { 
        
        System.out.println("[AccountDao]" + account.getName() + "取款" + amount + "元。");
    }

    @Override
    public void deposit(Account account, double amount) { 
        
        System.out.println("[AccountDao]" + account.getName()+"存款" + amount + "元。");
    } 
}

Service层接口

AccountService.java

public interface AccountService { 
        
	
	// 模拟某个业务
	void bankAction(Account account);

}

Service层实现类

AccountServiceImpl.java

public class AccountServiceImpl implements AccountService { 
        

    // Dao层对象
    private AccountDao accountDao;

    @Override
    public void bankAction(Account account) { 
        
        // 存取一百元
        accountDao.deposit(account, 100);

        // 模拟异常抛出,测试异常抛出类通知
        // 测试前置、后置、环绕通知时,请将异常抛出代码删除或注释
        int a = 0;
        if (a==0) 
            throw new RuntimeException("测试异常!");
		
        accountDao.withdraw(account, 100);
    }

    public AccountDao getAccountDao() { 
        
        return accountDao;
    }

    public void setAccountDao(AccountDao accountDao) { 
        
        this.accountDao = accountDao;
    }
}

日志类

Logger.java

public class Logger { 
        
    // 简单的日志输出方法
    public void info(String msg) { 
        
        System.out.println("[日志输出]"+msg);
    }
}

前置通知

Before.java

public class Before implements MethodBeforeAdvice { 
        

    Logger logger; // 日志对象

    @Override
    public void before(Method method, Object[] args, Object obj) throws Throwable { 
        
        // 注意,这里不要手动调用method.invoke()
        // Spring会自动调用一次目标方法
        // 我们只需要在这里调用目标方法前要做什么事情即可
        // 如果在这里调用了method.invoke(),会导致目标方法被调用两次
        logger.info("[前置通知]" + method.getName() + "方法开始执行了...");
        
        // 另外,这里不需要返回值,因为执行到前置通知时,
        // 目标方法还未被真正调用,所以不存在返回值
    }

    // 用于依赖注入
    public void setLogger(Logger logger) { 
        
        this.logger = logger;
    }
}

返回后通知

AfterReturn.java

public class AfterReturn implements AfterReturningAdvice { 
        

    Logger logger;

    @Override
    public void afterReturning(Object returnValue, Method method, 
                               Object[] args, Object target) throws Throwable { 
        
        // 不要手动调用method.invoke(),原因同上
        logger.info("[后置通知]" + method.getName() + "方法执行结束...");
    }

    public void setLogger(Logger logger) { 
        
        this.logger = logger;
    }
}

环绕通知

Around.java

public class Around implements MethodInterceptor { 
        

    Logger logger;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable { 
        

        logger.info("[环绕前通知]" 

相关文章