Статьи

Чистый код с аспектами

В моем предыдущем посте я описал преобразование алфавита и упомянул, что мы использовали AspectJ для решения этой задачи, но я не упомянул, как работает AspectJ и каковы аспекты в целом. Итак, в следующих нескольких строках я объясню:

  • что такое Аспектно-ориентированное программирование и зачем оно нам нужно
  • что такое AspectJ
  • использование AspectJ с Spring (настройка AspectJ и Spring для совместной работы)
  • и я объясню аспекты на примере из предыдущего поста.

Что такое аспектно-ориентированное программирование и зачем оно нам нужно

Во время разработки программного обеспечения мы можем использовать различные парадигмы программирования, такие как ООП (объектно-ориентированное программирование) или POP (процедурно-ориентированное программирование).
Сегодня большинство из нас использует методологии объектно-ориентированного программирования для решения реальных проблем в процессе разработки программного обеспечения.

Но во время нашей работы мы постоянно встречаемся с некоторым кодом, который пересекает нашу кодовую базу, ломает ее модульность и делает ее грязной.

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

Транзакции очень важны для нашего программного обеспечения, потому что они заботятся о согласованности данных. Код, который запускает и обрабатывает транзакции, очень важен для нашего приложения, но он используется для технических вещей (запуск, фиксация и откат транзакций). Это затрудняет понимание реального значения кода (чтобы увидеть реальную ценность кода для бизнеса).

Конечно, я не буду приводить пример того, как обрабатывать транзакции с использованием аспектов, потому что существует множество платформ, которые позаботятся о транзакциях вместо нас. Я только что упомянул транзакции, потому что вы, вероятно, знаете, как вставить данные в базу данных, используя простой JDBC API.

Поэтому, чтобы сделать наш код чище, мы будем использовать шаблоны проектирования, которые являются хорошим подходом для решения проблем. Но иногда использование шаблонов проектирования не приводит нас к простому решению, и большинство из нас прибегают к более простому решению, которое будет создавать «грязный» код.

В этой ситуации мы должны дать шанс Аспектно-ориентированному подходу к решению проблемы. Когда мы думаем об АОП, мы не должны думать о чем-то совершенно новом для нас, мы должны думать о АОП как о дополнении ООП. AOP призван упростить модульность кода, сделать его более чистым и дать нам возможность легче и быстрее понять, что должна делать какая-то часть приложения.

АОП вводит несколько новых концепций, которые позволят нам упростить модуляцию кода. Если мы хотим эффективно использовать Аспекты, нам нужно знать его основы, принципы и терминологию.
Когда мы начнем с использования AOP, мы встретим новые термины:

  • Перекрестные проблемы, это код, который должен быть перемещен в отдельный модуль (т.е. код для обработки транзакций).
  • Аспект, это модуль, который содержит проблемы.
  • Pointcut, мы можем рассматривать его как указатель, который будет указывать, когда должен выполняться соответствующий код
  • Совет, он содержит код, который должен выполняться при достижении некоторой точки соединения.
  • Объявление внутреннего типа, позволяет модифицировать структуру класса.
  • Аспект-ткачество — это механизм, который координирует интеграцию с остальной частью системы.

В конце я покажу, как они и как их использовать в примере.

Что такое AspectJ

AspectJ — это расширение языка программирования Java, которое позволяет использовать концепции AOP в языке программирования Java.

Когда вы используете AspectJ, вам не нужно вносить какие-либо изменения в существующий код.

AspectJ расширяет Java новой конструкцией под названием аспект, и после AspectJ 5 вы можете использовать стиль разработки, основанный на аннотациях.

AspectJ и весна

Spring Framework уже обеспечивает собственную реализацию АОП. Spring AOP является более простым решением, чем AspectJ, но он не такой надежный, как AspectJ. Поэтому, если вы хотите использовать аспекты в своем приложении Spring, вы должны быть знакомы с возможностями Spring AOP, прежде чем выбирать AspectJ для работы.

Прежде чем мы рассмотрим пример использования аспекта, я покажу вам, как интегрировать AspectJ с Spring и как настроить Tomcat для запуска приложения AspectJ с Spring. В этом примере я использовал LTW (время загрузки) аспектов.

Поэтому я начну сначала объяснять, как это сделать с весны. Это просто, просто добавьте следующую строку в файл конфигурации вашего приложения:

1
<context:load-time-weaver aspectj-weaving="autodetect"/>

Это все, что нужно сделать в весенней конфигурации.

Следующим шагом является настройка Tomcat. Нам нужно определить новый загрузчик классов для приложения. Этот загрузчик классов должен уметь выполнять загрузку времени, поэтому мы используем:

1
<loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader" />

Загрузчик должен быть в classpath Tomcat, прежде чем вы сможете его использовать.
Конечно, чтобы это работало, нам нужно создать файл aop.xml. Этот файл содержит инструкцию, которая будет использоваться загрузчиком классов в процессе преобразования класса.
Вот пример файла aop.xml, который я использовал для преобразования алфавита.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
<aspectj>
 <weaver options="-Xset:weaveJavaxPackages=true">
  <!-- only weave classes in our application-specific packages -->
  <include within="ba.codecentric.medica.model.*" />
  <include within="ba.codecentric.medica..*.service.*" />
  <include within="ba.codecentric.medica.controller..*" />
  <include within="ba.codecentric.medica.utils.ModelMapper" />
  <include within="ba.codecentric.medica.utils.RedirectHelper" />
  <include within="ba.codecentric.medica.aop.aspect.CharacterConvertionAspect" />
  <include within="ba.codecentric.medica.security.UserAuthenticationProvider" />
  <include within="ba.codecentric.medica.wraper.MedicaRequestWrapper"/>
 </weaver>
 <aspects>
  <!-- weave in just this aspect -->
  <aspect name="ba.codecentric.medica.aop.aspect.CharacterConversionAspect" />
 </aspects>
</aspectj>

Этот последний XML-файл наиболее интересен для всех вас, кто хочет попробовать AspectJ. Он инструктирует процесс плетения AspectJ.
Раздел ткача содержит информацию о том, что должно быть соткано. Так что этот файл будет включать все классы внутри:

  • ba.codecentric.medica.model. *
  • ba.codecentric.medica .. *. обслуживание. *
  • ba.codecentric.medica.controller .. *
  • ba.codecentric.medica.utils.ModelMapper
  • ba.codecentric.medica.utils.RedirectHelper
  • ba.codecentric.medica.aop.aspect.CharacterConvertionAspect
  • ba.codecentric.medica.security.UserAuthenticationProvider
  • ba.codecentric.medica.wraper.MedicaRequestWrapper

Итак, первая строка, включающая все классы внутри пакета модели. Второй включает все классы, которые являются частью субпакетов сервисов внутри пакета ba.codecentric.medica (то есть ba.codecentric.medica.hospitalisation.service). Третий включает в себя все, что находится ниже пакета контроллера. А остальные строки включают указанные классы.
Атрибут опций определяет опцию сложения, которую следует использовать в процессе плетения. Так что в этом примере -Xset: weaveJavaxPackages = true указывает AspectJ также на создание пакетов Java.
Раздел Аспекты содержит список аспектов, которые будут использоваться в процессе плетения.
Для получения дополнительной информации о конфигурации с xml вы можете увидеть документацию AspectJ .

Пример использования AspectJ


Я предпочитаю использовать Annotation, поэтому следующий пример покажет вам, как использовать AspectJ с аннотацией. Программирование на основе аннотаций с AspectJ возможно из версии AspectJ 5. Вот некоторый код полного аспекта, который содержит проблемы, используемые для преобразования алфавита.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
package ba.codecentric.medica.aop.aspect;
  
import java.util.List;
import java.util.Map;
  
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
  
import ba.codecentric.medica.utils.CharacterConverter;
import ba.codecentric.medica.utils.ContextHelper;
import ba.codecentric.medica.utils.LanguageHelper;
  
/**
 * Aspect used for transformation characters from one alphabet to another.
 *
 * @author igor
 *
 */
@Aspect
public class CharacterConvertionAspect {
   
 private static Log LOG = LogFactory.getLog(CharacterConvertionAspect.class);
   
 public int getConvertTo() {
  return getLanguageHelper().getConvertTo();
 }
  
 protected LanguageHelper getLanguageHelper() {
  return ContextHelper.getBean("languageHelper");
 }
  
 public CharacterConvertionAspect() {
  LOG.info("Character converter aspect created");
 }
   
 @SuppressWarnings("rawtypes")
 @Around("execution(public java.lang.String ba.codecentric.medica.model..*.get*(..)) && !cflow(execution(* ba.codecentric.medica.controller..*.*(..))) && !cflow(execution(public void ba.codecentric.medica..*.service..*.*(..))) && !cflow(execution(* ba.codecentric.medica.security.UserAuthenticationProvider.*(..)))")
 public Object convertCharacters(ProceedingJoinPoint pjp) throws Throwable {
  LOG.info("Character conversion trigered");
  Object value = pjp.proceed();
  if (value instanceof String) {
   LOG.info("Convert:" + value);
   Signature signature = pjp.getSignature();
   Class type = signature.getDeclaringType();
   String methodName = signature.getName();
   Map<Class, List<string&lgt;&lgt; skipConvertionMap = getBlackList();
   if(skipConvertionMap.containsKey(type)){
    List<string&lgt; list = skipConvertionMap.get(type);
    if(list == null || list.contains(methodName)){
     LOG.info("Value will not be converted because it is on blacklist");
     return value;
    }
      
   }
   return getConverter().convertCharacters((String) value, getConvertTo());
  }
  LOG.info("Convertion will not be performed (" + value + ")");
  return value;
 }
   
 @Around("execution(public void ba.codecentric.medica.model..*.set*(java.lang.String))")
 public Object convertCharactersToLat(ProceedingJoinPoint pjp) throws Throwable {
  Object value = pjp.getArgs()[0];
  LOG.info("Converting value:" + value + ", before persisting");
  if (value instanceof String){
   value= getConverter().convertCharacters((String)value, CharacterConverter.TO_LAT);
  }
  return pjp.proceed(new Object[]{value});
 }
   
 /**
  * Convert parameter to Latin alphabet
  *
  * @param pjp
  * @return
  * @throws Throwable
  */
 @Around("execution(public * ba.codecentric.medica.wraper.MedicaRequestWrapper.getParameter*(..))")
 public Object convertParametersToLat(ProceedingJoinPoint pjp) throws Throwable {
  Object value = pjp.proceed();
  return getConverter().convert(value, CharacterConverter.TO_LAT);
 }
   
 /**
  * If result of the invocation is String, it should be converted to chosen alphabet.
  *
  * @param jp
  * @return converted value
  * @throws Throwable
  */
 @Around("execution(* ba.codecentric.medica.controller..*.*(..))")
 public Object procedWithControllerInvocation(ProceedingJoinPoint jp) throws Throwable {
  Object value = jp.proceed();
  return getConverter().convert(value, getConvertTo());
 }
   
 public CharacterConverter getConverter() {
  return ContextHelper.getBean("characterConverter");
 }
  
 @SuppressWarnings("rawtypes")
 public Map<Class,List<string&lgt;&lgt; getBlackList(){
  return ContextHelper.getBean("blackList");
 }
   
}

Прежде всего, мы видим, что класс помечен аннотацией @Aspect. Это указывает на то, что этот класс на самом деле является аспектом. Аспект — это конструкция, которая содержит аналогичные сквозные проблемы. Таким образом, мы можем рассматривать его как модуль, который содержит сквозной код и определять, какой код будет использоваться и как.

1
2
3
4
5
6
7
8
9
@Around("execution(public void ba.codecentric.medica.model..*.set*(java.lang.String))")
public Object convertCharactersToLat(ProceedingJoinPoint pjp) throws Throwable {
 Object value = pjp.getArgs()[0];
 LOG.debug("Converting value:" + value + ", before persisting");
 if (value instanceof String) {
  value = getConverter().convertCharacters((String) value, CharacterConverter.TO_LAT);
 }
 return pjp.proceed(new Object[] { value });
}

Это метод, который аннотируется аннотацией @Around. Вокруг аннотация используется для представления вокруг совета. Я уже упоминал, что совет — это место, которое содержит сквозной код. В этом примере я использовал только совет «вокруг», но за исключением того, что есть еще до, после, после возвращения и после броска совета. Все советы, кроме как вокруг, не должны иметь возвращаемого значения. Содержимое внутренней аннотации определяет, когда код из совета будет сплетен. Это также может быть сделано, когда мы определяем pointcut. В этом примере я не использовал pointcuts для определения точек соединения, потому что это простой аспект. С помощью аннотаций pointcut вы можете определить реальные надежные точки соединения. В этом случае совет будет выполняться во время установки значений бинов сущностей, которые имеют только один параметр типа String.
ProcidingJoinPoint pjp в приведенном выше примере представляет точку соединения, поэтому для этого примера это метод установки объекта управления данными. Значение объекта, отправляемого в метод установки объекта, будет сначала преобразовано, а затем будет вызван метод установки с преобразованным значением.
Если бы я не использовал аспекты, мой код мог бы выглядеть так:

1
2
3
public void setJmbg(String jmbg) {
 this.jmbg = getConverter().convertCharacters(jmbg, CharacterConverter.TO_LAT);
}

Я уже говорил, что для этого примера я использую LTW. Поэтому в следующих нескольких строках я постараюсь кратко объяснить процесс плетения. Плетение — это процесс, в котором класс трансформируется с определенным аспектом. На следующем рисунке вы видите иллюстрацию процесса ткачества.

В этом случае для лучшего понимания ткачества вы можете рассмотреть его как внедрение кода вокруг вызывающего метода.

Вывод


Итак, в этом примере я только что рассмотрел некоторые основные принципы аспектного программирования с AspectJ.
Этот аспект помог мне сохранить код в чистоте. Результатом использования аспекта является четкое разделение сквозного кода и кода, представляющего реальную ценность для бизнеса. Контроллеры, сервисы и сущностные компоненты остаются чистыми, а технический код извлекается в отдельный модуль, что позволяет вам легче понимать и поддерживать ваш код. Более подробную информацию об определении pointcut и общих сведениях о проекте AspectJ вы можете увидеть на странице проекта .

Приятного кодирования и не забудьте поделиться!

Ссылка: Чистый код с аспектами от нашего партнера JCG Игоря Маджерика в блоге Игоря Маджерика .