背景
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 的实现。