简谈AOP

想起以前有个需求,controller的一个方法需要传入起始时间和结束时间查询某段时间内的数据,其中结束时间限制在距当前时间一个月以前。但是经常容易忘记这个规定,使输入的结束时间查过了限制,查出了多余的数据,所以考虑通过增加切面,在方法被执行前获取请求参数,校验参数中的结束时间,如果超出时间限制则加以校正。最近在看《Spring实战》,正好复习一下。

AOP(Aspect Oriented Programming)是Spring的两大核心概念之一,在Spring编译期加入或者运行时将功能逻辑动态添加到目标对象的方法中,实现功能复用和业务流程的分离。

image

Spring AOP功能示意简图

先来看看AOP的原理。

​ 它基于接口的JDK动态代理或者面向类的CGLIB实现。这里主要讲JDK动态代理的一个简单实现。

​目标类实现了接口。代理类自身有一个Object的引用,通过构造函数指向目标对象(后面对Object对象的操作实际上是对目标对象的操作)。在程序运行期间,通过JDK的Proxy的newProxyInstance(省略参数)方法,重写invoke方法,在Object对象的方法执行前后,插入日志等非业务逻辑。可以看到,这里运用了反射来进行一系列操作

​ 目标对象的行为已得到了增强,在客户端内通过传入已实现接口的目标对象,获取代理对象,操作代理对象就能看到增强后的效果了。AOP的应用有日志、事务管理等。

image

JDK动态代理UML图

下面来看一个AOP的实例

UserDao没有动态代理之前,调用say()方法的控制台输出:

image

使用代理模式的方法后:

代理类DynamicProxy


@Slf4j
public class DynamicProxy   {
    private Object object;


    public DynamicProxy(Object object) {
        this.object = object;
    }

    public Object getProxyObject() {
        return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                log.info("开始say");
                Object value = method.invoke(object, args);
                log.info("结束say");
                return value;
            }
        });
    }
    
}


客户端ProxyInstance


public class ProxyInstance {

    public static void main(String[] args) {
        UserDao userDao = new UserDaoImpl();
        DynamicProxy dynamicProxy = new DynamicProxy(userDao);
        UserDao proxy = (UserDao) dynamicProxy.getProxyObject();
        proxy.say();
    }
}


运行ProxyInstance的控制台输出:

image

笔者在这里实现了动态添加日志功能,者看起来可能跟在方法执行前后添加逻辑差不多,但还是不一样的。代理模式不需要修改被代理类的内部实现,通过代理类动态添加功能,将业务调用和其他逻辑分离,使业务开发人员更专注于业务开发。

再来看AOP的实现,有基于JDK的Spring AOP和依赖外部实现的AspectJ 它们之间的区别如下:

类型 Spring AOP AspectJ
依赖 JDK 外部依赖
连接点支持时期 方法执行期 所有时期均可支持连接点
织入时期 运行时 编译器和类加载时

总结:Spring AOP比AspectJ构建方便因此使用更加便捷,但是Aspect在编译期将切面织入到代码中,效率更高。

再回到AOP, 它有以下几个概念:

通知(Advice):添加到切点的逻辑

切面(Aspect):切点和通知共同构成了切面

切点(Cut Point):通知发生在何处

织入(Weaving):将切面应用到目标对象并创建代理,切面在指定的连接点织入目标对象

连接点:切点表示一个区域,连接点表示这个区域能够插入切面的一个点

环绕:通知方法将目标方法环绕起来

通过前面对AOP的总结,我们选择AspectJ进行面向切面编程。

业务方

Performance.java

@Component
public class Performance {

    public void perform() {
        System.out.println("perform action");
    }
}

定义一个切面

Audience.java


@Aspect
@Component
public class Audience {

    @Pointcut("execution(* com.xingren.aspect.Performance.perform(..))")
    public void performance() {
    }

    @Before("performance()")
    public void silenceCellPhones() {
        System.out.println("silencing cell phones");
    }

    @Before("performance()")
    public void takeSeats() {
        System.out.println("Taking seats");
    }

    @AfterReturning("performance()")
    public void applause() {
        System.out.println("applause");
    }

    @AfterThrowing("performance()")
    public void demandRefund() {
        System.out.println("demand refund");
    }


重点在于@Pointcut,execution(* com.xingren.aspect.Performance.perform(..)定义了通知发生的地点,也可以传入参数,比如@Pointcut("execution(* com.xingren.aspect.ArgumentPerformance.perform(int)) && args(trackNumber)"),那么连接点上的注解也需要包含该参数。

测试类

AOPTest.java


public class AOPTest {
    @Autowired
    private Performance performance;
    
  @Test
    public void testPerform() {
       performance.perform();
    }
}


运行结果:

image

可以看到在业务方执行前后的连接点均得到了通知。

也可以使用环绕通知,它集成了前置通知和后置通知,更加灵活,但是需要额外额外的参数ProceedingJoinPoint。

下面是一个例子。


@Around("performance()")
    public void watchPerformance(ProceedingJoinPoint joinPoint) {
        System.out.println("silencing cell phones");
        System.out.println("Taking seats");
        try {
            joinPoint.proceed();
            System.out.println("clap clap");
        } catch (Throwable throwable) {
            System.out.println("demand refund");
        }
    }


运行测试类得到结果:

image

可以看到环绕通知实现了和前置、后置通知相似的功能,并且通过ProceedingJoinPoint可以增加更加丰富的功能,比如获取目标方法的注解等,这里就不详细实现了。

​文中可能存在不正确的地方,欢迎指正。

总结:本文主要讲了Spring AOP的JDK动态代理实现,Spring AOP和AspectJ的区别以及AspectJ的简单实用实例。实际上要实现增强对象的功能,还有一种选择,JDK静态代理,它和AspectJ一样也是在编译期生成字节码文件的,但静态代理只能为一个目标对象服务,如果目标对象过多,则会产生很多代理类。三种方法,可以根据自己的业务情况合理选择。我个人选择:如何目标对象较少,则选择JDK静态代理,否则AspectJ。

参考文章:

《Spring In Action Fourth Edition》 第四章面向切面的Spring

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。