«1. Введение

Шаблон Interceptor обычно используется для добавления новых сквозных функций или логики в приложение и имеет надежную поддержку в большом количестве библиотек.

В этой статье мы рассмотрим и сравним две из этих основных библиотек: перехватчики CDI и Spring AspectJ.

2. Настройка проекта CDI Interceptor

CDI официально поддерживается для Jakarta EE, но некоторые реализации поддерживают использование CDI в среде Java SE. Weld можно рассматривать как один из примеров реализации CDI, которая поддерживается в Java SE.

Чтобы использовать CDI, нам нужно импортировать библиотеку Weld в наш POM:

<dependency>
    <groupId>org.jboss.weld.se</groupId>
    <artifactId>weld-se-core</artifactId>
    <version>3.0.5.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, необходимо указать полное имя класса в файле beans.xml, расположенном в каталоге META-INF:

<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);
    }
}

В этом быстром тесте мы сначала получаем bean-компонент SuperService из контейнера, затем вызываем для него бизнес-метод deliveryService и проверяем, действительно ли был вызван перехватчик AuditedInterceptor, проверяя его переменные состояния.

Также у нас есть аннотированные методы @Before и @After, в которых мы инициализируем и выключаем контейнер Weld соответственно.

5. Рассмотрение CDI

Мы можем указать на следующие преимущества перехватчиков CDI:

    Это стандартная функция спецификации Jakarta EE Некоторые библиотеки реализации CDI могут использоваться в Java SE Может использоваться, когда наш проект жесткие ограничения на сторонние библиотеки

Недостатками перехватчиков CDI являются следующие:

    Тесная связь между классом с бизнес-логикой и перехватчиком Трудно увидеть, какие классы перехватываются в проекте Отсутствие гибкого механизма для применения перехватчиков к группа методов

6. Spring AspectJ

Spring также поддерживает аналогичную реализацию функциональности перехватчика с использованием синтаксиса AspectJ.

Сначала нам нужно добавить в POM следующие зависимости 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.2</version>
</dependency>

Самые последние версии контекста Spring, 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 Aspect Apply

Чтобы убедиться, что аспект действительно применим к сервису, давайте напишем следующий модульный тест:

@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, подобно функциональности, найденной в XML-элементе Spring.

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.