Spring源码剖析6:Spring AOP概述

原文出处: 五月的仓颉html

咱们为何要使用 AOP

前言
一年半前写了一篇文章Spring3:AOP,是当时学习如何使用Spring AOP的时候写的,比较基础。这篇文章最后的推荐以及回复认为我写的对你们有帮助的评论有不少,可是如今从我我的的角度来看,这篇文章写得并很差,甚至能够说是没有太多实质性的内容,所以这些推荐和评论让我以为受之有愧。程序员

基于以上缘由,更新一篇文章,从最基础的原始代码–>使用设计模式(装饰器模式与代理)–>使用AOP三个层次来说解一下为何咱们要使用AOP,但愿这篇文章能够对网友朋友们有益。面试

原始代码的写法
既然要经过代码来演示,那必需要有例子,这里个人例子为:spring

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

/**
 * @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,其实现为:express

/**
 * @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程序员面试指南等干货资源
相关文章
相关标签/搜索