概述

拦截器模式通常用于在应用程序中添加新的跨切面功能或逻辑,并且在许多库中得到了广泛支持。本文将对比两个主要库:CDI拦截器和Spring的AspectJ。

2. CDI拦截器项目设置

CDI官方支持Jakarta EE,但有些实现提供了在Java SE环境中使用CDI的支持。Weld可以被视为一个在Java SE环境中可用的CDI实现示例。为了使用CDI,我们需要在项目的pom.xml中导入Weld库:

<dependency>
    <groupId>org.jboss.weld.se</groupId>
    <artifactId>weld-se-core</artifactId>
    <version>3.1.6.Final</version>
</dependency>

最新的Weld库可以在Maven仓库中找到。

现在我们来创建一个简单的拦截器。

3. 引入CDI拦截器

为了指定需要拦截的类,我们首先创建拦截器绑定:

@InterceptorBinding
@Target( { METHOD, TYPE } )
@Retention( RUNTIME )
public @interface Audited {
}

定义了拦截器绑定后,我们需要定义实际的拦截器实现:

@Audited
@Interceptor
public class AuditedInterceptor {
    public static boolean calledBefore = false;
    public static boolean calledAfter = false;

    @AroundInvoke
    public Object auditMethod(InvocationContext ctx) throws Exception {
        calledBefore = true;
        Object result = ctx.proceed();
        calledAfter = true;
        return result;
    }
}

每个@AroundInvoke方法接受一个javax.interceptor.InvocationContext参数,返回一个java.lang.Object,并可能抛出异常。

因此,当我们给方法添加新的@Audit接口注解时,会先调用auditMethod,然后才执行目标方法。

4. 应用CDI拦截器

让我们将创建的拦截器应用到一些业务逻辑上:

public class SuperService {
    @Audited
    public String deliverService(String uid) {
        return uid;
    }
}

我们创建了一个简单的服务,并将我们想要拦截的方法注解为@Audited

为了启用CDI拦截器,需要在META-INF目录下的beans.xml文件中指定完整的类名:

<beans xmlns="http://java.sun.com/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
      http://java.sun.com/xml/ns/javaee/beans_1_2.xsd">
    <interceptors>
        <class>com.baeldung.interceptor.AuditedInterceptor</class>
    </interceptors>
</beans>

为了验证拦截器是否正常工作,现在运行以下测试

public class TestInterceptor {
    Weld weld;
    WeldContainer container;

    @Before
    public void init() {
        weld = new Weld();
        container = weld.initialize();
    }

    @After
    public void shutdown() {
        weld.shutdown();
    }

    @Test
    public void givenTheService_whenMethodAndInterceptorExecuted_thenOK() {
        SuperService superService = container.select(SuperService.class).get();
        String code = "123456";
        superService.deliverService(code);
        
        Assert.assertTrue(AuditedInterceptor.calledBefore);
        Assert.assertTrue(AuditedInterceptor.calledAfter);
    }
}

在这个快速测试中,我们首先从容器中获取SuperServicebean,然后在其上调用业务方法deliverService,并通过检查拦截器的状态变量验证它确实被调用了。此外,我们还有带有@Before@After注解的方法,分别用于初始化和关闭Weld容器。

5. CDI考虑因素

CDI拦截器的优点包括:

  • 它是Jakarta EE规范的标准特性
  • 一些CDI实现库可用于Java SE环境
  • 当项目对第三方库有严格限制时,可以使用

CDI拦截器的缺点是:

  • 业务逻辑类与拦截器之间的紧密耦合
  • 在项目中难以看到哪些类被拦截
  • 缺乏灵活机制来将拦截器应用到一组方法上

6. Spring AspectJ

Spring也支持使用类似AspectJ语法实现类似的功能。

首先,我们需要在pom.xml中添加Spring和AspectJ的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.20.1</version>
</dependency>

Spring上下文(spring-context)、AspectJweaver(aspectjweaver)的最新版本可以在Maven仓库找到。

现在我们可以使用AspectJ注解语法创建一个简单的切面:

@Aspect
public class SpringTestAspect {
    @Autowired
    private List accumulator;

    @Around("execution(* com.baeldung.spring.service.SpringSuperService.*(..))")
    public Object auditMethod(ProceedingJoinPoint jp) throws Throwable {
        String methodName = jp.getSignature().getName();
        accumulator.add("Call to " + methodName);
        Object obj = jp.proceed();
        accumulator.add("Method called successfully: " + methodName);
        return obj;
    }
}

我们创建了一个针对SpringSuperService类所有方法的切面,这个类(为了简化)看起来像这样:

public class SpringSuperService {
    public String getInfoFromService(String code) {
        return code;
    }
}

7. Spring AspectJ切面应用

为了验证切面是否真正应用到服务上,我们编写如下单元测试:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = { AppConfig.class })
public class TestSpringInterceptor {
    @Autowired
    SpringSuperService springSuperService;

    @Autowired
    private List accumulator;

    @Test
    public void givenService_whenServiceAndAspectExecuted_thenOk() {
        String code = "123456";
        String result = springSuperService.getInfoFromService(code);
        
        Assert.assertThat(accumulator.size(), is(2));
        Assert.assertThat(accumulator.get(0), is("Call to getInfoFromService"));
        Assert.assertThat(accumulator.get(1), is("Method called successfully: getInfoFromService"));
    }
}

在这个测试中,我们注入服务,调用方法并检查结果。

配置如下:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    @Bean
    public SpringSuperService springSuperService() {
        return new SpringSuperService();
    }

    @Bean
    public SpringTestAspect springTestAspect() {
        return new SpringTestAspect();
    }

    @Bean
    public List getAccumulator() {
        return new ArrayList();
    }
}

这里的关键点是@EnableAspectJAutoProxy注解,它启用了处理带有AspectJ@Aspect注解的组件的功能,类似于Spring XML元素中的功能。

8. Spring AspectJ考虑因素

使用Spring AspectJ的优点包括:

  • 拦截器与业务逻辑解耦
  • 拦截器可以从依赖注入中受益
  • 拦截器自身包含所有配置信息
  • 添加新拦截器无需修改现有代码
  • 拦截器具有灵活的机制来选择拦截哪些方法
  • 不依赖Jakarta EE即可使用

当然,也有一些缺点:

  • 需要了解AspectJ语法来开发拦截器
  • 学习AspectJ拦截器的曲线比CDI拦截器更高

9. CDI拦截器与Spring AspectJ比较

如果你的项目使用Spring,那么选择Spring AspectJ是个不错的选择。

如果你正在使用完整的应用服务器,或者你的项目不使用Spring(或其他框架,如Google Guice),并且严格遵循Jakarta EE标准,那么CDI拦截器将是唯一的选择。

10. 结论

本文对比了CDI拦截器和Spring的AspectJ两种拦截器模式的实现。我们探讨了它们各自的优缺点。本文示例的源代码可以在我们的GitHub仓库中找到:GitHub