浅聊Spring AOP

浅聊Spring AOP_aop
AOP是Spring框架除了IOC之外的另一个核心概念。

AOP:Aspect Oriented Programming,意为面向切面编程

这是一个新的概念,我们知道Java是面向对象编程(OOP):指将所有的一切都看做对象,通过对象与对象之间相互作用来解决问题的一种编程思想。

AOP是对OOP的一个补充,在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。将不同方法的同一位置抽象成一个切面对象,对该切面对象进行编程就是AOP。
浅聊Spring AOP_打印日志_02
AOP的优点:

1.降低模块之间的耦合度。
2.使系统容易扩展。
3.更好的代码复用。
4.非业务代码更加集中,不分散,便于统一管理。
5.业务代码更简洁纯粹,没有其他代码的影响。
说概念太空泛,不好理解,我们还是通过代码来直观感受什么是AOP。

1.创建一个计算器接口 Cal。

定义四个方法:加,减,乘,除。

public interface Cal {
    public int add(int num1,int num2);
    public int sub(int num1,int num2);
    public int mul(int num1,int num2);
    public int div(int num1,int num2);
}

2.创建接口实现类CalImpl。

实现四个方法。

public class CalImpl implements Cal{

    @Override
    public int add(int num1, int num2) {
        // TODO Auto-generated method stub
        int result = num1+num2;
        return result;
    }

    @Override
    public int sub(int num1, int num2) {
        // TODO Auto-generated method stub
        int result = num1-num2;
        return result;
    }

    @Override
    public int mul(int num1, int num2) {
        // TODO Auto-generated method stub
        int result = num1*num2;
        return result;
    }

    @Override
    public int div(int num1, int num2) {
        // TODO Auto-generated method stub
        int result = num1/num2;
        return result;
    }

}

3.在测试方法中创建CalImpl对象,调用方法。

public class Test {
    public static void main(String[] args) {
        Cal cal = new CalImpl();
        cal.add(10, 3);
        cal.sub(10, 3);
        cal.mul(10, 3);
        cal.div(10, 3);
    }
}

以上这段代码很简单,现添加功能:在每一个方法执行的同时,打印日志信息:该方法的参数列表和该方法的计算结果。

这个需求很简单,我们只需要在每一个方法体中,运算执行之前打印参数列表,运算结束之后打印计算结果即可,对代码做出如下修改。

public class CalImpl implements Cal{

    @Override
    public int add(int num1, int num2) {
        // TODO Auto-generated method stub
        System.out.println("add方法的参数是["+num1+","+num2+"]");
        int result = num1+num2;
        System.out.println("add方法的结果是"+result);
        return result;
    }

    @Override
    public int sub(int num1, int num2) {
        // TODO Auto-generated method stub
        System.out.println("sub方法的参数是["+num1+","+num2+"]");
        int result = num1-num2;
        System.out.println("sub方法的结果是"+result);
        return result;
    }

    @Override
    public int mul(int num1, int num2) {
        // TODO Auto-generated method stub
        System.out.println("mul方法的参数是["+num1+","+num2+"]");
        int result = num1*num2;
        System.out.println("mul方法的结果是"+result);
        return result;
    }

    @Override
    public int div(int num1, int num2) {
        // TODO Auto-generated method stub
        System.out.println("div方法的参数是["+num1+","+num2+"]");
        int result = num1/num2;
        System.out.println("div方法的结果是"+result);
        return result;
    }

}

再次运行代码,成功打印日志信息。
浅聊Spring AOP_aop_03

功能已经实现了,但是我们会发现这种方式业务代码打印日志代码耦合性非常高,不利于代码后期的维护。

如果需求改变,需要对打印的日志内容作出修改,那么我们就必须修改4个方法中的所有相关代码,如果是100个方法呢?每次就需要手动去改100个方法中的代码。

换个角度去分析,会发现4个方法中打印日志信息的代码基本相同,那么有没有可能将这部分代码提取出来进行封装,统一维护呢?同时也可以将日志代码和业务代码完全分离开,解耦和。

按照这思路继续向下走,我们希望做的事情是把这4个方法的相同位置(业务方法执行前后)提取出来,形成一个横切面,并且将这个横切面封装成一个对象,将所有的打印日志代码写到这个对象中,以实现与业务代码的分离。

这就是AOP的思想。

如何实现?
使用动态代理的方式来实现。

我们希望CalImpl只进行业务运算,不进行打印日志的工作,那么就需要有一个对象来替代CalImpl进行打印日志的工作,这就是代理对象

代理对象首先应该具备CalImpl的所有功能,并在此基础上,扩展出打印日志的功能。

1.删除CalImpl方法中所有的打印日志代码,只保留业务代码。

public class CalImpl implements Cal{

    @Override
    public int add(int num1, int num2) {
        // TODO Auto-generated method stub
        int result = num1+num2;
        return result;
    }

    @Override
    public int sub(int num1, int num2) {
        // TODO Auto-generated method stub
        int result = num1-num2;
        return result;
    }

    @Override
    public int mul(int num1, int num2) {
        // TODO Auto-generated method stub
        int result = num1*num2;
        return result;
    }

    @Override
    public int div(int num1, int num2) {
        // TODO Auto-generated method stub
        int result = num1/num2;
        return result;
    }

}

2.创建MyInvocationHandlerl类,并实现InvocationHandler接口,成为一个动态代理类

public class MyInvocationHandler implements InvocationHandler{
    //被代理对象
    private Object obj = null;

    //返回代理对象
    public Object bind(Object obj){
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // TODO Auto-generated method stub
        System.out.println(method.getName()+"的参数是:"+Arrays.toString(args));
        Object result = method.invoke(this.obj, args);
        System.out.println(method.getName()+"的结果是:"+result);
        return result;
    }

}

bind方法是MyInvocationHandlerl类提供给外部调用的方法,传入需要被代理的对象,bind方法会返回一个代理对象。

bind方法完成了两项工作:

1.将外部传进来的被代理对象保存到成员变量中,因为业务方法调用时需要用到被代理对象。

2.通过Proxy.newProxyInstance方法创建一个代理对象。

解释一下Proxy.newProxyInstance方法的参数:

(1)我们知道对象是JVM根据运行时类来创建的,此时需要动态创建一个代理对象,可以使用被代理对象的运行时类来创建代理对象:obj.getClass().getClassLoader()获取被代理对象的运行时类

(2)同时代理对象需要具备被代理对象的所有功能,即需要拥有被代理对象的所有接口,所以传入obj.getClass().getInterfaces()

(3)this指当前MyInvocationHandler对象。

以上全部是反射(reflect)的知识点。

invoke方法method是描述被代理对象的所有方法的对象,agrs是描述被代理对象方法的参数列表的对象

method.invoke(this.obj, args)是通过反射机制来调用被代理对象的方法,即业务方法。

所以在method.invoke(this.obj, args)前后添加打印日志信息,就等同于在被代理对象的业务方法前后添加打印日志信息,并且已经做到了分类,业务方法在被代理对象中,打印日志信息在代理对象中。

测试方法中执行代码。

public class Test {
    public static void main(String[] args) {
        //被代理对象
        Cal cal = new CalImpl();
        MyInvocationHandler mh = new MyInvocationHandler();
        //代理对象
        Cal cal2 = (Cal) mh.bind(cal);
        cal2.add(10, 3);
        cal2.sub(10, 3);
        cal2.mul(10, 3);
        cal2.div(10, 3);
    }
}

浅聊Spring AOP_打印日志_04
成功,并且现在已经做到了代码分离,CalImpl类中只有业务代码,打印日志的代码写在MyInvocationHandler类中。

以上就是通过动态代理实现AOP的过程,我们在使用Spring框架的AOP时,并不需要这么复杂,Spring已经对这个过程进行了封装,让开发者可以更加便捷的使用AOP进行开发。

接下来我们就来学习Spring框架的AOP如何使用。

在Spring框架中,我们不需要创建动态代理类,只需要创建一个切面类,Spring底层会自动根据切面类以及目标类生成一个代理对象。

1.创建切面类 LoggerAspect

@Aspect
@Component
public class LoggerAspect {

    @Before("execution(public int com.kun.aspect.CalImpl.*(..))")
    public void before(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取参数列表
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println(name+"的参数是:"+args);
    }

    @After("execution(public int com.kun.aspect.CalImpl.*(..))")
    public void after(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法结束");
    }

    @AfterReturning(value="execution(public int com.kun.aspect.CalImpl.*(..))",returning="result")
    public void afterReturn(JoinPoint joinPoint,Object result){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法的结果是"+result);
    }

    @AfterThrowing(value="execution(public int com.kun.aspect.CalImpl.*(..))",throwing="ex")
    public void afterThrowing(JoinPoint joinPoint,Exception ex){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        System.out.println(name+"方法抛出异常:"+ex);
    }

}

LoggerAspect类名处添加两个注解:
1.@Aspect:表示该类是切面类。
2.@Component:将该类注入到IOC容器。

分别来说明类中的4个方法注解的含义。

 @Before("execution(public int com.kun.aspect.CalImpl.*(..))")
    public void before(JoinPoint joinPoint){
        //获取方法名
        String name = joinPoint.getSignature().getName();
        //获取参数列表
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println(name+"的参数是:"+args);
    }

1.@Before:表示before方法执行的时机。

2.execution(public int com.kun.aspect.CalImpl.*(…)):表示切入点是

com.kun.aspect包下CalImpl类中的所有方法。

即CalImpl所有方法在执行之前会首先执行LoggerAspect类中的before方法。

after方法同理,
表示CalImpl所有方法执行之后会执行LoggerAspect类中的after方法。

afterReturn方法表示CalImpl所有方法在return之后会执行LoggerAspect类中的afterReturn方法。

afterThrowing方法表示CalImpl所有方法在抛出异常时会执行LoggerAspect类中的afterThrowing方法。

所以我们就可以根据具体需求,选择在before,after,afterReturn,afterThrowing方法中添加相应代码。

2.目标类也需要添加@Component注解。

@Component
public class CalImpl implements Cal{

    @Override
    public int add(int num1, int num2) {
        // TODO Auto-generated method stub
        return num1+num2;
    }

    @Override
    public int sub(int num1, int num2) {
        // TODO Auto-generated method stub
        return num1-num2;
    }

    @Override
    public int mul(int num1, int num2) {
        // TODO Auto-generated method stub
        return num1*num2;
    }

    @Override
    public int div(int num1, int num2) {
        // TODO Auto-generated method stub
        return num1/num2;
    }

}

3.spring.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-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

    <!-- 自动扫描 -->
    <context:component-scan base-package="com.kun.aspect"></context:component-scan>

    <!-- 使Aspect注解生效,为目标类自动生成代理对象 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>

1.将com.kun.aspect包中的类扫描到IOC容器中。
2.添加aop:aspectj-autoproxy注解,Spring容器会结合切面类和目标类自动生成动态代理对象,Spring框架的AOP底层就是通过动态代理的方式完成AOP。

4.测试方法执行如下代码:
从IOC容器中获取代理对象,执行方法。

public class Test {
    public static void main(String[] args) {
        //加载spring.xml
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
        //获取代理对象
        Cal cal = (Cal) applicationContext.getBean("calLogger");
        //执行方法
        cal.add(10, 3);
        cal.sub(10, 3);
        cal.mul(10, 3);
        cal.div(10, 3);
    }
}

浅聊Spring AOP_spring_05
成功。结合代码,回过头来说几个概念更好理解。

切面:横切关注点被模块化的特殊对象。

CalImpl所有方法中需要加入日志的部分,抽象成一个切面对象LoggerAspect。

通知:切面对象完成的工作。

LoggerAspect对象打印日志的操作。

目标:被通知的对象,即被横切的对象。

CalImpl对象。

代理:切面通知目标混合之后的内容。
连接点:程序要执行的某个特定位置。

切面方法要插入业务代码的具体位置。

切点:AOP通过切点定位到连接点。


欢迎关注公众号Java技术大本营,会不定期分享BAT面试资料等福利。

浅聊Spring AOP_ide_06