方法拦截与面向切面编程(AOP)
1. 方法拦截与 AOP 概述
在传统的事务处理逻辑中,我们可能会编写大量重复的代码来管理事务的开始、提交和回滚。例如:
public class Brokerage {
private final TransactionManager txn;
public void placeOrder(Order order) {
txn.beginTransaction();
try {
...
} catch(DataException e) {
txn.rollback();
} finally {
if(!txn.rolledBack())
txn.commit();
}
}
}
这种方式会导致代码冗余,并且每个需要事务处理的方法都要重复这些逻辑。而使用声明式事务,我们可以简化代码:
public class Brokerage {
@Transactional
public void placeOrder(Order order) {
...
}
}
通过声明
placeOrder()
方法为
@Transactional
,我们可以去除大部分包裹订单的样板代码,同时消除了
Brokerage
类对
TransactionManager
的依赖,使代码更易于编写和测试,并且将所有事务相关的逻辑集中在一个地方。
@Transactional
是一种元数据,它允许我们在
placeOrder()
方法周围声明一个事务。实际上,
placeOrder()
方法会被拦截,并在正常执行之前被包裹在一个事务中。这是通过一种称为面向切面编程(AOP)的技术实现的。
方法拦截可以通过多种不同的方式实现,具体取决于所使用的 AOP 库:
- 在编译时,通过专门的编译器实现。
- 在加载时,直接修改类定义。
- 在运行时,通过动态代理实现。
这个过程被称为织入(weaving),引入的行为被称为通知(advice)。由于我们关注的是依赖注入,因此将重点放在运行时的织入方式,即使用动态代理。
2. 使用 Guice 实现跟踪拦截器
2.1 实现拦截器类
我们希望通过拦截方法来跟踪某个类中每个方法的执行情况,而不是在每个方法中添加打印语句。Guice 允许我们通过绑定一个拦截器来实现这一点:
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class TracingInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation mi) throws Throwable {
System.out.println("enter " + mi.getMethod().getName());
try {
return mi.proceed();
} finally {
System.out.println("exit " + mi.getMethod().getName());
}
}
}
这个类很简单,它包含一个
invoke()
方法,每次拦截发生时都会调用该方法。该方法会执行以下操作:
- 在进入被拦截的方法之前打印一条提示信息。
- 继续执行被拦截的方法并返回结果。
- 在将控制权返回给调用者之前打印另一条提示信息。
2.2 绑定拦截器
在任何
Module
类中,我们可以使用 Guice 的绑定 API 来应用这个拦截器:
import static com.google.inject.matcher.Matchers.*;
public class MyModule extends AbstractModule {
@Override
protected void configure() {
...
bindInterceptor(any(), any(), new TracingInterceptor());
}
}
bindInterceptor(any(), any(), new TracingInterceptor())
中的前两个参数
any()
分别表示任何类和任何方法。Guice 使用这些匹配器来测试要拦截的类和方法。它自带了几个这样的匹配器,也可以自定义匹配器。
2.3 测试拦截器
假设我们有一个
Chef
类:
public class Chef {
public void cook() {
...
}
public void clean() {
...
}
}
当我们调用
Chef
类的方法时:
Chef chef = Guice.createInjector(new MyModule())
.getInstance(Chef.class);
chef.cook();
chef.clean();
会产生以下输出:
enter cook
exit cook
enter clean
exit clean
Chef
类并不知道如何向控制台打印信息,而且在单元测试中,方法不会被拦截,测试代码可以专注于验证相关的业务逻辑。
3. 使用 Spring 实现跟踪拦截器
3.1 实现拦截器类
Spring 使用不同的 AOP 库(虽然也支持 AopAlliance),但工作原理类似。我们使用 Spring 和 AspectJ 来拦截
Chef
类的方法:
import org.aspectj.lang.ProceedingJoinPoint;
public class TracingInterceptor {
public Object trace(ProceedingJoinPoint call) throws Throwable {
System.out.println("enter " + call.toShortString());
try {
return call.proceed();
} finally {
System.out.println("exit " + call.toShortString());
}
}
}
这个拦截器类与 Guice 中的
TracingInterceptor
类似,但不需要实现任何接口,而是直接告诉注入器
trace()
方法。
3.2 配置拦截器
我们需要在 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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
<bean id="chef" class="example.Chef"/>
<bean id="tracer" class="example.TracingInterceptor"/>
<aop:config>
<aop:aspect ref="tracer">
<aop:pointcut id="pointcuts.anyMethod"
expression="execution(* example.*.*(..))" />
<aop:around pointcut-ref="pointcuts.anyMethod" method="trace"/>
</aop:aspect>
</aop:config>
</beans>
这个配置文件看起来复杂,但实际上很简单:
- 首先,我们在
<bean>
标签中声明
TracingInterceptor
,并命名为
"tracer"
。
- 然后,使用 Spring 提供的
<aop:pointcut>
标签声明一个切入点。表达式
"execution(* example.*.*(..))"
用 AspectJ 切入点语言编写,告诉 Spring 拦截
example
包中任何可见性、名称或参数的方法的执行。这个切入点表达式相当于 Guice 中的匹配器。
-
<aop:aspect ref="tracer">
是一个切面的声明,它本质上是一个匹配器(或切入点)和一个拦截器(通知)之间的绑定。
3.3 测试拦截器
运行以下代码:
BeanFactory injector = new FileSystemXmlApplicationContext("myAspect.xml");
Chef chef = (Chef) injector.getBean("chef");
chef.cook();
chef.clean();
会产生以下输出:
enter execution(cook)
exit execution(cook)
enter execution(clean)
exit execution(clean)
4. 代理机制的工作原理
4.1 静态代理示例
动态代理是在运行时生成的子类。我们可以透明地用代理替换原始实现,并根据需要装饰其行为。以
Chef
类为例,一个静态代理可能如下所示:
public class ChefProxy extends Chef {
private final MethodInterceptor interceptor;
private final Chef chef;
public ChefProxy(MethodInterceptor interceptor, Chef chef) {
this.interceptor = interceptor;
this.chef = chef;
}
public void cook() {
interceptor.invoke(new MethodInvocation() { ... });
}
public void clean() {
interceptor.invoke(new MethodInvocation() { ... });
}
}
这个代理类不会直接将调用委托给被拦截的
chef
实例,而是使用一个控制对象
MethodInvocation
调用拦截器。
MethodInterceptor
可以使用这个控制对象来决定何时以及如何将调用传递给原始的
chef
实例。
4.2 使用 Java 代理代理接口
Java 核心库提供了动态生成代理的工具,它实现了与我们刚刚看到的代理和拦截器对相同的设计模式,并作为
java.lang.reflect
中的反射工具集的一部分提供。它仅限于代理接口,但对于大多数用例来说已经足够。
假设
Chef
是一个接口:
public interface Chef {
public void cook();
public void clean();
}
我们可以通过以下方式创建
Chef
接口的动态子类:
import java.lang.reflect.Proxy;
...
Chef proxy = (Chef) Proxy.newProxyInstance(Chef.class.getClassLoader(),
new Class[] { Chef.class },
invocationHandler);
Proxy.newProxyInstance()
方法的参数说明如下:
- 第一个参数是定义新代理的类加载器。
- 第二个参数是一个
Class
对象数组,表示要拦截的所有接口。
- 第三个参数是代理要使用的调用处理程序,它必须实现
java.lang.reflect
包中的
InvocationHandler
接口。
以下是使用 JDK 代理和
InvocationHandler
重新实现的方法跟踪拦截器:
public class TracingInterceptor implements InvocationHandler {
private final Chef chef;
public TracingInterceptor(Chef originalChef) {
this.chef = originalChef;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("enter " + method.getName());
try {
return method.invoke(chef, args);
} finally {
System.out.println("exit " + method.getName());
}
}
}
运行代码:
import java.lang.reflect.Proxy;
...
Chef proxy = (Chef) Proxy.newProxyInstance(Chef.class.getClassLoader(),
new Class[] { Chef.class },
new TracingInterceptor(originalChef));
proxy.cook();
proxy.clean();
会产生以下输出:
enter cook
exit cook
enter clean
exit clean
4.3 使用 CGLIB 代理类
Java 提供的代理机制无法代理类(抽象类或具体类),这时第三方字节码操作工具就派上用场了。Guice 和 Spring 都使用一个流行的库 CGLib 来在底层生成代理。
如果我们有一个具体类
FrenchChef
,可以通过以下方式为它生成代理:
import net.sf.cglib.proxy.Enhancer;
...
Chef chef = (Chef) Enhancer.create(FrenchChef.class,
new TracingInterceptor());
对应的
TracingInterceptor
如下:
public class TracingInterceptor implements MethodInterceptor {
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
System.out.println("enter " + method.getName());
try {
return methodProxy.invokeSuper(proxy, args);
} finally {
System.out.println("exit " + method.getName());
}
}
}
CGLib 允许我们直接在超类上调用相应的方法,而不需要持有原始的
chef
实例。
5. 过度使用通知的风险
5.1 性能问题
每次拦截都会产生在原始方法前后运行通知方法的开销。如果只对少数方法应用几个拦截器,通常不会有问题。但当拦截链变长,并且穿越应用程序的关键路径时,可能会降低性能。
5.2 程序语义问题
拦截顺序可能会影响程序的语义。例如,我们有一个
Template
类:
public class Template {
private final String template = "Hello, :name!";
public String process(String name) {
return template.replaceAll(":name", name);
}
}
使用方法拦截,我们可以通过将问候语加粗来增强它:
import org.aopalliance.intercept.MethodInterceptor;
public class BoldDecoratingInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation mi) throws Throwable {
String processed = (String)mi.proceed();
return "<b>" + processed + "</b>";
}
}
绑定这个拦截器:
import static com.google.inject.matcher.Matchers.*;
...
bindInterceptor(subclassesOf(Template.class), any(), new
BoldDecoratingInterceptor());
当客户端处理模板时,输出会被加粗:
Guice.createInjector(...)
.getInstance(Template.class)
.process("Bob");
输出:
<b>Hello, Bob!</b>
现在,如果我们想将整个内容包装在 HTML 中以便在网站上呈现,再添加一个拦截器:
import org.aopalliance.intercept.MethodInterceptor;
public class HtmlDecoratingInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation mi) throws Throwable {
String processed = (String) mi.proceed();
return "<html><body>" + processed + "</body></html>";
}
}
如果随意绑定这个拦截器,可能会导致非常意外的行为。因此,在使用拦截器时,需要谨慎考虑拦截顺序和性能问题。
综上所述,方法拦截和 AOP 是强大的技术,但在使用时需要权衡利弊,确保代码的性能和可维护性。在实际应用中,应根据具体需求选择合适的 AOP 库和代理方式,并合理使用拦截器,避免过度使用导致的问题。
6. 不同 AOP 库和代理方式对比
6.1 功能特性对比
| AOP 库/代理方式 | 支持的代理类型 | 匹配器/切入点定义 | 拦截器实现方式 | 配置方式 |
|---|---|---|---|---|
| Guice | 接口、类(借助 CGLib) |
使用
Matchers
类定义匹配器
|
实现
MethodInterceptor
接口
| Java 代码配置 |
| Spring + AspectJ | 接口、类(借助 CGLib) | 使用 AspectJ 切入点语言定义切入点 | 可以不实现接口,定义特定方法 | XML 配置或注解配置 |
| Java 代理 | 接口 | 无(通过接口指定) |
实现
InvocationHandler
接口
| Java 代码配置 |
| CGLib | 类 | 无(通过类指定) |
实现
MethodInterceptor
接口
| Java 代码配置 |
6.2 性能对比
一般来说,Java 代理由于是 JDK 原生支持,在代理接口时性能相对稳定,但功能相对单一。CGLib 在代理类时功能强大,但由于需要动态生成字节码,在创建代理对象时可能会有一定的性能开销。Guice 和 Spring 在使用代理时,性能会受到具体配置和使用场景的影响。如果拦截链过长,无论使用哪种方式,都会增加额外的性能开销。
6.3 使用场景对比
- Java 代理 :适用于只需要代理接口的场景,并且对性能要求较高,代码结构相对简单的情况。
- CGLib :适用于需要代理具体类的场景,例如在依赖注入框架中增强类的行为。
- Guice :适用于使用 Guice 进行依赖注入的项目,希望通过 Java 代码简洁地配置 AOP 功能。
- Spring + AspectJ :适用于使用 Spring 框架的项目,支持更复杂的切入点定义和配置方式,包括 XML 和注解。
7. 方法拦截和 AOP 的应用场景
7.1 日志记录
在前面的例子中,我们已经看到了如何使用方法拦截来实现日志记录。通过在方法执行前后添加日志输出,可以方便地跟踪方法的执行过程,帮助调试和监控系统。
7.2 事务管理
在事务处理中,AOP 可以将事务管理的逻辑从业务逻辑中分离出来。例如,在方法执行前开始事务,在方法执行成功后提交事务,在方法抛出异常时回滚事务。以下是一个简单的示例:
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class TransactionInterceptor implements MethodInterceptor {
private TransactionManager txn;
public TransactionInterceptor(TransactionManager txn) {
this.txn = txn;
}
public Object invoke(MethodInvocation mi) throws Throwable {
txn.beginTransaction();
try {
Object result = mi.proceed();
if (!txn.rolledBack()) {
txn.commit();
}
return result;
} catch (Exception e) {
txn.rollback();
throw e;
}
}
}
7.3 权限验证
在方法执行前进行权限验证,只有具有相应权限的用户才能调用该方法。例如:
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
public class PermissionInterceptor implements MethodInterceptor {
private PermissionChecker permissionChecker;
public PermissionInterceptor(PermissionChecker permissionChecker) {
this.permissionChecker = permissionChecker;
}
public Object invoke(MethodInvocation mi) throws Throwable {
if (permissionChecker.checkPermission()) {
return mi.proceed();
} else {
throw new PermissionDeniedException();
}
}
}
7.4 缓存处理
在方法执行前检查缓存中是否存在结果,如果存在则直接返回缓存结果,否则执行方法并将结果存入缓存。例如:
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.util.HashMap;
import java.util.Map;
public class CacheInterceptor implements MethodInterceptor {
private Map<String, Object> cache = new HashMap<>();
public Object invoke(MethodInvocation mi) throws Throwable {
String methodKey = mi.getMethod().getName();
if (cache.containsKey(methodKey)) {
return cache.get(methodKey);
} else {
Object result = mi.proceed();
cache.put(methodKey, result);
return result;
}
}
}
8. 总结
方法拦截和 AOP 是强大的编程技术,它们可以帮助我们将横切关注点(如日志记录、事务管理、权限验证等)从核心业务逻辑中分离出来,提高代码的可维护性和可复用性。
在使用方法拦截和 AOP 时,需要注意以下几点:
- 选择合适的 AOP 库和代理方式,根据项目的具体需求和场景进行选择。
- 合理使用拦截器,避免过度使用导致性能问题和程序语义混乱。
- 注意拦截顺序,确保拦截器的执行顺序符合业务需求。
通过掌握方法拦截和 AOP 的原理和应用,我们可以编写出更加高效、灵活和可维护的代码。以下是一个简单的流程图,展示了方法拦截的基本流程:
graph TD;
A[调用方法] --> B{是否有拦截器};
B -- 是 --> C[执行拦截器前置逻辑];
C --> D[执行原始方法];
D --> E[执行拦截器后置逻辑];
E --> F[返回结果];
B -- 否 --> D;
希望本文能够帮助你更好地理解方法拦截和 AOP 的概念、应用和实现方式,在实际项目中灵活运用这些技术。
超级会员免费看
4239

被折叠的 条评论
为什么被折叠?



