乐趣区

Spring源码剖析6Spring-AOP概述

原文出处:五月的仓颉

我们为什么要使用 AOP

前言
一年半前写了一篇文章 Spring3:AOP,是当时学习如何使用 Spring AOP 的时候写的,比较基础。这篇文章最后的推荐以及回复认为我写的对大家有帮助的评论有很多,但是现在从我个人的角度来看,这篇文章写得并不好,甚至可以说是没有太多实质性的内容,因此这些推荐和评论让我觉得受之有愧。

基于以上原因,更新一篇文章,从最基础的原始代码–> 使用设计模式(装饰器模式与代理)–> 使用 AOP 三个层次来讲解一下为什么我们要使用 AOP,希望这篇文章可以对网友朋友们有益。

原始代码的写法
既然要通过代码来演示,那必须要有例子,这里我的例子为:

1
有一个接口 Dao 有 insert、delete、update 三个方法,在 insert 与 update 被调用的前后,打印调用前的毫秒数与调用后的毫秒数
首先定义一个 Dao 接口:

/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public interface Dao {public void insert();
     
    public void delete();
     
    public void update();}

然后定义一个实现类 DaoImpl:

/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class DaoImpl implements Dao {
 
    @Override
    public void insert() {System.out.println("DaoImpl.insert()");
    }
 
    @Override
    public void delete() {System.out.println("DaoImpl.delete()");
    }
 
    @Override
    public void update() {System.out.println("DaoImpl.update()");
    }
     
}

最原始的写法,我要在调用 insert()与 update()方法前后分别打印时间,就只能定义一个新的类包一层,在调用 insert()方法与 update()方法前后分别处理一下,新的类我命名为 ServiceImpl,其实现为:

/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class ServiceImpl {private Dao dao = new DaoImpl();
     
    public void insert() {System.out.println("insert()方法开始时间:" + System.currentTimeMillis());
        dao.insert();
        System.out.println("insert()方法结束时间:" + System.currentTimeMillis());
    }
     
    public void delete() {dao.delete();
    }
     
    public void update() {System.out.println("update()方法开始时间:" + System.currentTimeMillis());
        dao.update();
        System.out.println("update()方法结束时间:" + System.currentTimeMillis());
    }
     
}

这是最原始的写法,这种写法的缺点也是一目了然:

方法调用前后输出时间的逻辑无法复用,如果有别的地方要增加这段逻辑就得再写一遍
如果 Dao 有其它实现类,那么必须新增一个类去包装该实现类,这将导致类数量不断膨胀
使用装饰器模式
接着我们使用上设计模式,先用装饰器模式,看看能解决多少问题。装饰器模式的核心就是实现 Dao 接口并持有 Dao 接口的引用,我将新增的类命名为 LogDao,其实现为:

/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class LogDao implements Dao {
 
    private Dao dao;
     
    public LogDao(Dao dao) {this.dao = dao;}
 
    @Override
    public void insert() {System.out.println("insert()方法开始时间:" + System.currentTimeMillis());
        dao.insert();
        System.out.println("insert()方法结束时间:" + System.currentTimeMillis());
    }
 
    @Override
    public void delete() {dao.delete();
    }
 
    @Override
    public void update() {System.out.println("update()方法开始时间:" + System.currentTimeMillis());
        dao.update();
        System.out.println("update()方法结束时间:" + System.currentTimeMillis());
    }
 
}

在使用的时候,可以使用”Dao dao = new LogDao(new DaoImpl())”的方式,这种方式的优点为:

透明,对调用方来说,它只知道 Dao,而不知道加上了日志功能
类不会无限膨胀,如果 Dao 的其它实现类需要输出日志,只需要向 LogDao 的构造函数中传入不同的 Dao 实现类即可
不过这种方式同样有明显的缺点,缺点为:

输出日志的逻辑还是无法复用
输出日志的逻辑与代码有耦合,如果我要对 delete()方法前后同样输出时间,需要修改 LogDao
但是,这种做法相比最原始的代码写法,已经有了很大的改进。

使用代理模式
接着我们使用代理模式尝试去实现最原始的功能,使用代理模式,那么我们就要定义一个 InvocationHandler,我将它命名为 LogInvocationHandler,其实现为:


/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class LogInvocationHandler implements InvocationHandler {
 
    private Object obj;
     
    public LogInvocationHandler(Object obj) {this.obj = obj;}
     
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName();
        if ("insert".equals(methodName) || "update".equals(methodName)) {System.out.println(methodName + "()方法开始时间:" + System.currentTimeMillis());
            Object result = method.invoke(obj, args);
            System.out.println(methodName + "()方法结束时间:" + System.currentTimeMillis());
             
            return result;
        }
         
        return method.invoke(obj, args);
    }
     
}

其调用方式很简单,我写一个 main 函数:


/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public static void main(String[] args) {Dao dao = new DaoImpl();
         
    Dao proxyDao = (Dao)Proxy.newProxyInstance(LogInvocationHandler.class.getClassLoader(), new Class<?>[]{Dao.class}, new LogInvocationHandler(dao));
         
    proxyDao.insert();
    System.out.println("---------- 分割线 ----------");
    proxyDao.delete();
    System.out.println("---------- 分割线 ----------");
    proxyDao.update();}

结果就不演示了,这种方式的优点为:

输出日志的逻辑被复用起来,如果要针对其他接口用上输出日志的逻辑,只要在 newProxyInstance 的时候的第二个参数增加 Class<?> 数组中的内容即可
这种方式的缺点为:

JDK 提供的动态代理只能针对接口做代理,不能针对类做代理
代码依然有耦合,如果要对 delete 方法调用前后打印时间,得在 LogInvocationHandler 中增加 delete 方法的判断
使用 CGLIB
接着看一下使用 CGLIB 的方式,使用 CGLIB 只需要实现 MethodInterceptor 接口即可:


/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class DaoProxy implements MethodInterceptor {
 
    @Override
    public Object intercept(Object object, Method method, Object[] objects, MethodProxy proxy) throws Throwable {String methodName = method.getName();
         
        if ("insert".equals(methodName) || "update".equals(methodName)) {System.out.println(methodName + "()方法开始时间:" + System.currentTimeMillis());
            proxy.invokeSuper(object, objects);
            System.out.println(methodName + "()方法结束时间:" + System.currentTimeMillis());
             
            return object;
        }
         
        proxy.invokeSuper(object, objects);
        return object;
    }
 
}

代码调用方式为:


/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public static void main(String[] args) {DaoProxy daoProxy = new DaoProxy();
     
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(DaoImpl.class);
    enhancer.setCallback(daoProxy);
         
    Dao dao = (DaoImpl)enhancer.create();
    dao.insert();
    System.out.println("---------- 分割线 ----------");
    dao.delete();
    System.out.println("---------- 分割线 ----------");
    dao.update();}

使用 CGLIB 解决了 JDK 的 Proxy 无法针对类做代理的问题,但是这里要专门说明一个问题:使用装饰器模式可以说是对使用原生代码的一种改进,使用 Java 代理可以说是对于使用装饰器模式的一种改进,但是使用 CGLIB 并不是对于使用 Java 代理的一种改进。

前面的可以说改进是因为使用装饰器模式比使用原生代码更好,使用 Java 代理又比使用装饰器模式更好,但是 Java 代理与 CGLIb 的对比并不能说改进,因为使用 CGLIB 并不一定比使用 Java 代理更好,这两种各有优缺点,像 Spring 框架就同时支持 Java Proxy 与 CGLIB 两种方式。

从目前看来代码又更好了一些,但是我认为还有两个缺点:

无论使用 Java 代理还是使用 CGLIB,编写这部分代码都稍显麻烦
代码之间的耦合还是没有解决,像要针对 delete()方法加上这部分逻辑就必须修改代码

使用 AOP

最后来看一下使用 AOP 的方式,首先定义一个时间处理类,我将它命名为 TimeHandler:


/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class TimeHandler {public void printTime(ProceedingJoinPoint pjp) {Signature signature = pjp.getSignature();
        if (signature instanceof MethodSignature) {MethodSignature methodSignature = (MethodSignature)signature;
            Method method = methodSignature.getMethod();
            System.out.println(method.getName() + "()方法开始时间:" + System.currentTimeMillis());
             
            try {pjp.proceed();
                System.out.println(method.getName() + "()方法结束时间:" + System.currentTimeMillis());
            } catch (Throwable e) {}}
    }
     
}

到第 8 行的代码与第 12 行的代码分别打印方法开始执行时间与方法结束执行时间。我这里写得稍微复杂点,使用了 <aop:around> 的写法,其实也可以拆分为 <aop:before> 与 <aop:after> 两种,这个看个人喜好。

这里多说一句,切面方法 printTime 本身可以不用定义任何的参数,但是有些场景下需要获取调用方法的类、方法签名等信息,此时可以在 printTime 方法中定义 JointPoint,Spring 会自动将参数注入,可以通过 JoinPoint 获取调用方法的类、方法签名等信息。由于这里我用的 <aop:around>,要保证方法的调用,这样才能在方法调用前后输出时间,因此不能直接使用 JoinPoint,因为 JoinPoint 没法保证方法调用。此时可以使用 ProceedingJoinPoint,ProceedingPointPoint 的 proceed()方法可以保证方法调用,但是要注意一点,ProceedingJoinPoint 只能和 <aop:around> 搭配,换句话说,如果 aop.xml 中配置的是 <aop:before>,然后 printTime 的方法参数又是 ProceedingJoinPoint 的话,Spring 容器启动将报错。

接着看一下 aop.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:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
 
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
 
 
http://www.springframework.org/schema/aop
 
 
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
 
    <bean id="daoImpl" class="org.xrq.spring.action.aop.DaoImpl" />
    <bean id="timeHandler" class="org.xrq.spring.action.aop.TimeHandler" />
 
    <aop:config>
        <aop:pointcut id="addAllMethod" expression="execution(* org.xrq.spring.action.aop.Dao.*(..))" />
        <aop:aspect id="time" ref="timeHandler">
            <aop:before method="printTime" pointcut-ref="addAllMethod" />
            <aop:after method="printTime" pointcut-ref="addAllMethod" />
        </aop:aspect>
    </aop:config>
     
</beans>

我不大会写 expression,也懒得去百度了,因此这里就拦截 Dao 下的所有方法了。测试代码很简单:

=
/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class AopTest {
 
    @Test
    @SuppressWarnings("resource")
    public void testAop() {ApplicationContext ac = new ClassPathXmlApplicationContext("spring/aop.xml");
         
        Dao dao = (Dao)ac.getBean("daoImpl");
        dao.insert();
        System.out.println("---------- 分割线 ----------");
        dao.delete();
        System.out.println("---------- 分割线 ----------");
        dao.update();}
     
}

AOP 总结

结果就不演示了。到此我总结一下使用 AOP 的几个优点:

切面的内容可以复用,比如 TimeHandler 的 printTime 方法,任何地方需要打印方法执行前的时间与方法执行后的时间,都可以使用 TimeHandler 的 printTime 方法
避免使用 Proxy、CGLIB 生成代理,这方面的工作全部框架去实现,开发者可以专注于切面内容本身
代码与代码之间没有耦合,如果拦截的方法有变化修改配置文件即可
下面用一张图来表示一下 AOP 的作用:

我们传统的编程方式是垂直化的编程,即 A–>B–>C–>D 这么下去,一个逻辑完毕之后执行另外一段逻辑。但是 AOP 提供了另外一种思路,它的作用是在业务逻辑不知情(即业务逻辑不需要做任何的改动)的情况下对业务代码的功能进行增强,这种编程思想的使用场景有很多,例如事务提交、方法执行之前的权限检测、日志打印、方法调用事件等等。

AOP 使用场景举例
上面的例子纯粹为了演示使用,为了让大家更加理解 AOP 的作用,这里以实际场景作为例子。

第一个例子,我们知道 MyBatis 的事务默认是不会自动提交的,因此在编程的时候我们必须在增删改完毕之后调用 SqlSession 的 commit()方法进行事务提交,这非常麻烦,下面利用 AOP 简单写一段代码帮助我们自动提交事务(这段代码我个人测试过可用):


/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class TransactionHandler {public void commit(JoinPoint jp) {Object obj = jp.getTarget();
        if (obj instanceof MailDao) {Signature signature = jp.getSignature();
            if (signature instanceof MethodSignature) {SqlSession sqlSession = SqlSessionThrealLocalUtil.getSqlSession();               
                 
                MethodSignature methodSignature = (MethodSignature)signature;
                Method method = methodSignature.getMethod();
                  
                String methodName = method.getName();
                if (methodName.startsWith("insert") || methodName.startsWith("update") || methodName.startsWith("delete")) {sqlSession.commit();
                }
                 
                sqlSession.close();}
        }
    }
     
}

这种场景下我们要使用的 aop 标签为 <aop:after>,即切在方法调用之后。

这里我做了一个 SqlSessionThreadLocalUtil,每次打开会话的时候,都通过 SqlSessionThreadLocalUtil 把当前会话 SqlSession 放到 ThreadLocal 中,看到通过 TransactionHandler,可以实现两个功能:

insert、update、delete 操作事务自动提交
对 SqlSession 进行 close(),这样就不需要在业务代码里面关闭会话了,因为有些时候我们写业务代码的时候会忘记关闭 SqlSession,这样可能会造成内存句柄的膨胀,因此这部分切面也一并做了
整个过程,业务代码是不知道的,而 TransactionHandler 的内容可以充分再多处场景下进行复用。

第二个例子是权限控制的例子,不管是从安全角度考虑还是从业务角度考虑,我们在开发一个 Web 系统的时候不可能所有请求都对所有用户开放,因此这里就需要做一层权限控制了,大家看 AOP 作用的时候想必也肯定会看到 AOP 可以做权限控制,这里我就演示一下如何使用 AOP 做权限控制。我们知道原生的 Spring MVC,Java 类是实现 Controller 接口的,基于此,利用 AOP 做权限控制的大致代码如下(这段代码纯粹就是一段示例,我构建的 Maven 工程是一个普通的 Java 工程,因此没有验证过):


/**
 * @author 五月的仓颉 http://www.cnblogs.com/xrq730/p/7003082.html
 */
public class PermissionHandler {public void hasPermission(JoinPoint jp) throws Exception {Object obj = jp.getTarget();
         
        if (obj instanceof Controller) {Signature signature = jp.getSignature();
            MethodSignature methodSignature = (MethodSignature)signature;
             
            // 获取方法签名
            Method method = methodSignature.getMethod();
            // 获取方法参数
            Object[] args = jp.getArgs();
             
            // Controller 中唯一一个方法的方法签名 ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
            // 这里对这个方法做一层判断
            if ("handleRequest".equals(method.getName()) && args.length == 2) {Object firstArg = args[0];
                if (obj instanceof HttpServletRequest) {HttpServletRequest request = (HttpServletRequest)firstArg;
                    // 获取用户 id
                    long userId = Long.parseLong(request.getParameter("userId"));
                    // 获取当前请求路径
                    String requestUri = request.getRequestURI();
                     
                    if(!PermissionUtil.hasPermission(userId, requestUri)) {throw new Exception("没有权限");
                    }
                }
            }
        }
         
    }
     
}

毫无疑问这种场景下我们要使用的 aop 标签为 <aop:before>。这里我写得很简单,获取当前用户 id 与请求路径,根据这两者,判断该用户是否有权限访问该请求,大家明白意思即可。

后记
文章演示了从原生代码到使用 AOP 的过程,一点一点地介绍了每次演化的优缺点,最后以实际例子分析了 AOP 可以做什么事情。

微信公众号【黄小斜】作者是蚂蚁金服 JAVA 工程师,专注于 JAVA
后端技术栈:SpringBoot、SSM 全家桶、MySQL、分布式、中间件、微服务,同时也懂点投资理财,坚持学习和写作,相信终身学习的力量!关注公众号后回复”架构师“即可领取
Java 基础、进阶、项目和架构师等免费学习资料,更有数据库、分布式、微服务等热门技术学习视频,内容丰富,兼顾原理和实践,另外也将赠送作者原创的 Java 学习指南、Java 程序员面试指南等干货资源

退出移动版