22、方法拦截与面向切面编程(AOP)

方法拦截与面向切面编程(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 的概念、应用和实现方式,在实际项目中灵活运用这些技术。

内容概要:本文设计了一种基于PLC的全自动洗衣机控制系统内容概要:本文设计了一种,采用三菱FX基于PLC的全自动洗衣机控制系统,采用3U-32MT型PLC作为三菱FX3U核心控制器,替代传统继-32MT电器控制方式,提升了型PLC作为系统的稳定性自动化核心控制器,替代水平。系统具备传统继电器控制方式高/低水,实现洗衣机工作位选择、柔和过程的自动化控制/标准洗衣模式切换。系统具备高、暂停加衣、低水位选择、手动脱水及和柔和、标准两种蜂鸣提示等功能洗衣模式,支持,通过GX Works2软件编写梯形图程序,实现进洗衣过程中暂停添加水、洗涤、排水衣物,并增加了手动脱水功能和、脱水等工序蜂鸣器提示的自动循环控制功能,提升了使用的,并引入MCGS组便捷性灵活性态软件实现人机交互界面监控。控制系统通过GX。硬件设计包括 Works2软件进行主电路、PLC接梯形图编程线关键元,完成了启动、进水器件选型,软件、正反转洗涤部分完成I/O分配、排水、脱、逻辑流程规划水等工序的逻辑及各功能模块梯设计,并实现了大形图编程。循环小循环的嵌; 适合人群:自动化套控制流程。此外、电气工程及相关,还利用MCGS组态软件构建专业本科学生,具备PL了人机交互C基础知识和梯界面,实现对洗衣机形图编程能力的运行状态的监控操作。整体设计涵盖了初级工程技术人员。硬件选型、; 使用场景及目标:I/O分配、电路接线、程序逻辑设计及组①掌握PLC在态监控等多个方面家电自动化控制中的应用方法;②学习,体现了PLC在工业自动化控制中的高效全自动洗衣机控制系统的性可靠性。;软硬件设计流程 适合人群:电气;③实践工程、自动化及相关MCGS组态软件PLC的专业的本科生、初级通信联调工程技术人员以及从事;④完成PLC控制系统开发毕业设计或工业的学习者;具备控制类项目开发参考一定PLC基础知识。; 阅读和梯形图建议:建议结合三菱编程能力的人员GX Works2仿真更为适宜。; 使用场景及目标:①应用于环境MCGS组态平台进行程序高校毕业设计或调试运行验证课程项目,帮助学生掌握PLC控制系统的设计,重点关注I/O分配逻辑、梯形图实现方法;②为工业自动化领域互锁机制及循环控制结构的设计中类似家电控制系统的开发提供参考方案;③思路,深入理解PL通过实际案例理解C在实际工程项目PLC在电机中的应用全过程。控制、时间循环、互锁保护、手动干预等方面的应用逻辑。; 阅读建议:建议结合三菱GX Works2编程软件和MCGS组态软件同步实践,重点理解梯形图程序中各环节的时序逻辑互锁机制,关注I/O分配硬件接线的对应关系,并尝试在仿真环境中调试程序以加深对全自动洗衣机控制流程的理解。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值