从代理到 AOP,如何手写一个 AOP 框架?

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 背景AOP(Aspect-oriented Programming) 作为 OOP(Object-oriented Programming) 的补充,提供了另外一种思考程序结构的编程思想。OOP 的模块化的单位是 Class,而 AOP 模块化的单位则为 Aspect。Aspect 将跨越多个 Class 的关注点模块化,如日志、事务等等。本篇从代理谈起,逐步过渡到 AOP,并实现一个简单的 AOP 框架,旨在加深自己和大家对 AOP 的理解。

背景


AOP(Aspect-oriented Programming) 作为 OOP(Object-oriented Programming) 的补充,提供了另外一种思考程序结构的编程思想。OOP 的模块化的单位是 Class,而 AOP 模块化的单位则为 Aspect。Aspect 将跨越多个 Class 的关注点模块化,如日志、事务等等。本篇从代理谈起,逐步过渡到 AOP,并实现一个简单的 AOP 框架,旨在加深自己和大家对 AOP 的理解。


场景

日常开发中,我们通常会有打印日志的需求,日志上我们可以对方法的参数和返回值进行打印,并统计方法的执行时长。最简单的方式,我们可能会使用下述类似的代码。


@Slf4j
public interface IService {
    String doSomething(String param);
}
@Slf4j
public class ServiceImpl implements IService{
    @Override
    public String doSomething(String param) {
        long startTime = System.currentTimeMillis();
        log.info("Service#doSomething 开始执行,参数为:{}", param);
        // 具体业务逻辑
        String result = detailBiz();
        long costTime = System.currentTimeMillis() - startTime;
        log.info("Service#doSomething 结束执行,结果为:{},执行时长:{} ms", result, costTime);
        return result;
    }
    private String detailBiz() {
        return null;
    }
}


如果需要打印日志的方法比较少,我们在每个方法业务代码执行前后添加打印日志的代码是没有问题的,然而如果有很多方法都需要打印日志,还按照这种方式毫无疑问将大大增加我们的工作量,并且如果需要对日志打印的逻辑做出修改将十分费时费力。


在上述代码中打印日志就是我们的非业务功能的关注点,这些关注点通常情况下会遍布项目各处。为了减少重复的代码,增加可扩展性、可维护性,我们可以使用代理,将业务代码和非业务代码进行隔离。Java 中实现代理的方式有多种,具体可参见我前面文章《Java 中创建代理的几种方式》。如果业务类实现了接口,我们可以选择 JDK 动态代理的方式,以避免手动创建较多的静态代理类。使用 JDK 动态代理的方式处理日志打印代码如下。


public interface IService {
    String doSomething(String param);
}
public class ServiceImpl implements IService{
    @Override
    public String doSomething(String param) {
        // 具体业务逻辑
        String result = detailBiz();
        return result;
    }
    private String detailBiz() {
        return null;
    }
}
@Slf4j
public class LogProxy {
    private Object obj;
    public LogProxy(Object obj) {
        this.obj = obj;
    }
    public Object createProxy() {
        ClassLoader classLoader = obj.getClass().getClassLoader();
        Class<?>[] interfaces = obj.getClass().getInterfaces();
        return Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                // 查找方法
                if (method.getDeclaringClass() == IService.class &&
                        "doSomething".equals(method.getName()) &&
                        method.getParameterCount() == 1 &&
                        method.getParameterTypes()[0] == String.class) {
                    // 方法执行前
                    long startTime = System.currentTimeMillis();
                    log.info("Service#doSomething 开始执行,参数为:{}", args[0]);
                    // 执行方法
                    String result = (String) method.invoke(obj, args);
                    // 方法执行后
                    long costTime = System.currentTimeMillis() - startTime;
                    log.info("Service#doSomething 结束执行,结果为:{},执行时长:{} ms", result, costTime);
                    return result;
                }
                return null;
            }
        });
    }
}


上述代码创建了给定实例的代理对象,并筛选出我们感兴趣的方法,在方法执行前后添加了我们自己的日志打印代码。


AOP 术语


上面使用代理打印日志的代码中,虽然没有提出 AOP 的概念,事实上我们已经将 AOP 应用于我们的日志打印功能中,下面结合上面的样例对 AOP 的术语加以解释。


Joint Point


Joint Point 是程序执行的一个点,包括构造器执行、方法执行、字段赋值等等,是我们执行额外代码的拦截点,如上述样例中的IService#doSomething方法的执行。Java 作为静态语言,结构一经定义便无法修改,在运行时只能拦截方法的执行,这可以帮我们完成绝大多数功能。如果需要拦截结构造器或者字段等,则需要特殊的编译器,在编译期间就生成相关字节码。


Pointcut


Pointcut 用于过滤出我们感兴趣的 Joint Point。Joint Point 包含众多的方法,那么具体哪个方法才是我们感兴趣并需要做额外逻辑的呢?这需要 Pointcut 进行筛选,如上述样例中,我们筛选出IService#doSomething方法,只有这个方法执行时才会打印日志。


Advice


Advice 是在特定 Joint Point 执行的动作,可以再细分为 Around Advice、Before Advice、After Advice,样例中在业务代码执行前后打印日志的代码即为 Advice。


Introduction


Introduction 也被称为 inter-type declaration,它用于向代理对象中添加附加的接口。日常开发使用相对较少。


Aspect


Aspect 是跨域多个类的关注点的模块化,如样例中,对多个类执行方法的参数及返回值的日志打印是我们的关注点。它和 OOP 中的 Class 很相似,Class 用于对代码进行模块化,包含成员变量、构造器、方法等,而 Aspect 包含 Introduction、Pointcut、Advice。样例中,我们并未提供 Aspect类。


手写一个 AOP 框架


既然使用代理能够实现我们通用的非业务功能,那为什么还要抽取出一个 AOP 框架呢?样例中提供了一个代理工厂用于创建代理,然而这个代理工厂相对定制化,对拦截方法的筛选、筛选后执行的动作都是固定的,如果有其他的需求这个代理工厂则无法满足。AOP 框架提供了相对通用、易扩展的功能,满足我们不同的非业务需求。下面对样例中的代理工厂进行改造,实现一个较为简单的 AOP 框架。


目标对象


代理工厂创建的代理对象通常情况下会对应一个目标对象,如果当前调用的方法不是 Advice 关心的方法,则会用目标对象直接调用方法。除了目标对象自身的接口,创建的代理类还可以灵活的实现自定义的接口,因此可以改造 ProxyFactory 如下。


public class ProxyFactory {
    // 目标对象
    private Object target;
    // 自定义实现的接口
    private List<Class<?>> interfaces = new ArrayList<>();
    public ProxyFactory(Object target) {
        this.target = target;
    }
    public void addInterface(Class<?> ifc) {
        this.interfaces.add(ifc);
    }
    public Object createProxy() {
        ClassLoader classLoader = this.getClass().getClassLoader();
        List<Class<?>> interfaces = new ArrayList<>(this.interfaces);
        interfaces.addAll(Arrays.asList(this.target.getClass().getInterfaces()));
        return Proxy.newProxyInstance(classLoader, interfaces.toArray(new Class[0]), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                return null;
            }
        });
    }
}


方法筛选


由于代理是在运行时创建,因此当前只有方法作为 Joint Point 。由 Pointcut 对方法进行过滤,如果满足条件则进行拦截。代码如下。

public interface Pointcut {
    // 是否匹配方法,仅拦截匹配的方法
    boolean match(Method method);  
}


执行动作


执行的动作为 Advice,这里我们暂时只考虑 Before Advice、After Advice。代码如下。

public interface Advice {
}
public interface BeforeAdvice extends Advice {
    // 目标方法执行前进行执行
    void before(Object proxy, Method method, Object[] args);
}
public interface AfterAdvice extends Advice {
    // 目标方法执行后执行
    void after(Object result, Object proxy, Method method, Object[] args);
}


由于不同的拦截方法需要执行不同的动作,因此还需要把 Pointcut 和 Advice 进行整合。


public interface PointcutBeforeAdvice extends Pointcut, BeforeAdvice {
}
public interface PointcutAfterAdvice extends Pointcut, AfterAdvice {
}


最后修改 ProxyFactory 如下。


public class ProxyFactory {
    // 目标对象
    private Object target;
    // 自定义实现的接口
    private List<Class<?>> interfaces = new ArrayList<>();
    // 目标方法执行前执行
    private List<PointcutBeforeAdvice> beforeAdviceList = new ArrayList<>();
    // 目标方法执行后执行
    private List<PointcutAfterAdvice> afterAdviceList = new ArrayList<>();
    public ProxyFactory(Object target) {
        this.target = target;
    }
    public void addInterface(Class<?> ifc) {
        this.interfaces.add(ifc);
    }
    public void addAdvice(Advice advice) {
        if (advice instanceof PointcutBeforeAdvice) {
            beforeAdviceList.add((PointcutBeforeAdvice) advice);
        } else if (advice instanceof PointcutAfterAdvice) {
            afterAdviceList.add((PointcutAfterAdvice) advice);
        }
    }
    public Object createProxy() {
        ClassLoader classLoader = this.getClass().getClassLoader();
        List<Class<?>> interfaces = new ArrayList<>(this.interfaces);
        interfaces.addAll(Arrays.asList(this.target.getClass().getInterfaces()));
        return Proxy.newProxyInstance(classLoader, interfaces.toArray(new Class[0]), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                beforeAdviceList.stream().filter(advice -> advice.match(method)).forEach(advice -> advice.before(proxy, method, args));
                Object result = method.invoke(target, args);
                afterAdviceList.stream().filter(advice -> advice.match(method)).forEach(advice -> advice.after(result, proxy, method, args));
                return result;
            }
        });
    }
}


切面


到当前,AOP 代理框架已经基本完成,这是通过 API 的方式使用框架,那么还需要做的是将 Pointcut、Advice 整合到 Aspect 中。常用的做法是通过注解或者 XML 读取配置,然后创建代理。对于注解,Spring 是使用的 AspectJ 的注解,感兴趣的小伙伴可以自行查阅相关资料。


总结


本篇从代理引入 AOP,除了对 AOP 中的术语进行解读,还实现了一个极其简单的 AOP 框架。事实上这个框架也极度不完善,例如如果代理的目标对象未实现接口我们需要切换到 cglib 创建代理、没有实现 Around Advice、没有实现 Throwing Advice 等等。本篇主要起到带领大家理解 AOP 的目的,对于更详细的 AOP 内容,可以参考 Spring AOP 或 AspectJ 的实现。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
37 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
2月前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
3月前
|
缓存 安全 Java
Spring AOP 中两种代理类型的限制
【8月更文挑战第22天】
31 0
|
3月前
|
Java Spring
|
4月前
|
缓存 安全 Java
Spring高手之路21——深入剖析Spring AOP代理对象的创建
本文详细介绍了Spring AOP代理对象的创建过程,分为三个核心步骤:判断是否增强、匹配增强器和创建代理对象。通过源码分析和时序图展示,深入剖析了Spring AOP的工作原理,帮助读者全面理解Spring AOP代理对象的生成机制及其实现细节。
48 0
Spring高手之路21——深入剖析Spring AOP代理对象的创建
|
4月前
|
分布式计算 Java MaxCompute
详解 Java 限流接口实现问题之在Spring框架中使用AOP来实现基于注解的限流问题如何解决
详解 Java 限流接口实现问题之在Spring框架中使用AOP来实现基于注解的限流问题如何解决
|
5月前
|
存储 算法 Java
ASM字节码操纵框架实现AOP
ASM字节码操纵框架实现AOP
51 0
|
5月前
|
设计模式 SQL Java
Spring框架第四章(AOP概念及相关术语)
Spring框架第四章(AOP概念及相关术语)
|
6月前
|
安全 Java 开发者
在Spring框架中,IoC和AOP是如何实现的?
【4月更文挑战第30天】在Spring框架中,IoC和AOP是如何实现的?
81 0
|
6月前
|
设计模式 安全 Java
深入理解Spring Boot AOP:CGLIB代理与JDK动态代理的完全指南
深入理解Spring Boot AOP:CGLIB代理与JDK动态代理的完全指南
1778 1