Перейти к основному содержимому

Перехватчик CDI против Spring AspectJ

· 5 мин. чтения

1. Введение

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

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

2. Настройка проекта перехватчика CDI

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 и может генерировать Exception .

Итак, когда мы аннотируем метод с новым интерфейсом @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.foreach.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. Весенний аспектJ

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.foreach.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 Применение

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

@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 .