Статьи

Применение аспектно-ориентированного программирования

1. Введение

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

  • логирование
  • Управление транзакциями
  • Обработка ошибок
  • Мониторинг
  • Безопасность

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

В этом примере у нас есть приложение Spring MVC, которое обращается к запрашиваемым данным (клиентам и заказам) и показывает страницу с их информацией. Мы можем взглянуть на разные слои:

AOP1

На приведенном выше графике мы можем оценить, что существуют функциональные возможности, распределенные по разным классам (мониторинг реализован в каждой службе), и некоторые классы содержат различные проблемы (например, класс ClientController содержит ведение журнала и обработку исключений). Чтобы это исправить, мы напишем аспекты для реализации наших сквозных задач. Цель состоит в том, чтобы реализовать следующую модель:

AOP2

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

Давайте посмотрим на это на примере.

  • Исходный код можно найти на github .

2. Проверка кода контроллера

ClientController:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
public class ClientController {
    @Autowired
    private ClientService clientService;
    private static Logger mainLogger = LoggerFactory.getLogger("generic");
    private static Logger errorLogger = LoggerFactory.getLogger("errors");
 
    @RequestMapping("/getClients")
    public String getClients(Model model, @RequestParam("id") int id) {
        mainLogger.debug("Executing getClients request");
         
        try {
            Client client = clientService.getClient(id);
            model.addAttribute("client", client);
        } catch (DataAccessException e) {
            errorLogger.error("error in ClientController", e);
            NotificationUtils.sendNotification(e);
            return "errorPage";
        }
         
        return "showClient";
    }
}

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

Это правда, что мы могли бы использовать аннотацию @ControllerAdvice для централизации обработки исключений, но цель этого поста — увидеть, как это сделать с помощью Spring AOP.

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

3. Проверка кода услуг

Обслуживание клиентов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Service("clientService")
public class ClientServiceImpl implements ClientService {
    @Autowired
    private ClientRepository clientRepository;
    private static Logger mainLogger = LoggerFactory.getLogger("generic");
    private static Logger monitorLogger = LoggerFactory.getLogger("monitoring");
     
    @Override
    @Transactional(readOnly = true)
    public Client getClient(int id) {
        mainLogger.debug("Accessing client service");
        long startTime = System.currentTimeMillis();
        Client client = clientRepository.getClient(id);
        long totalTime = System.currentTimeMillis() - startTime;
        monitorLogger.info("Invocation time {}ms ", totalTime);
         
        return client;
    }
}

В дополнение к вызову службы он также содержит создание журнала и мониторинг времени выполнения в каждом вызове.

Мы также могли бы использовать аспекты для модульного управления транзакциями, если нам нужно было использовать программное управление транзакциями, но в этом примере это не так.

4. Уровень доступа к данным

ClientRepositoryImpl:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Repository
public class ClientRepositoryImpl implements ClientRepository {
    private JdbcTemplate template;
    private RowMapper<Client> rowMapper = new ClientRowMapper();
    private static final String SEARCH = "select * from clients where clientId = ?";
    private static final String COLUMN_ID = "clientId";
    private static final String COLUMN_NAME = "name";
     
    public ClientRepositoryImpl() {}
     
    public ClientRepositoryImpl(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }
     
    public Client getClient(int id) {
        return template.queryForObject(SEARCH, rowMapper, id);
    }
     
    private class ClientRowMapper implements RowMapper<Client> {
        public Client mapRow(ResultSet rs, int i) throws SQLException {
            Client client = new Client();
            client.setClientId(rs.getInt(COLUMN_ID));
            client.setName(rs.getString(COLUMN_NAME));
             
            return client;
        }
    }
}

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

5. Активация АОП

Для настройки AOP необходимо импортировать следующие зависимости:

01
02
03
04
05
06
07
08
09
10
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>3.2.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.6.8</version>
</dependency>

В конфигурационном файле Spring нам нужно добавить следующие теги:

1
2
<context:component-scan base-package="xpadro.spring.mvc.aop"/>
<aop:aspectj-autoproxy/>

Тег component-scan будет искать в базовом пакете, чтобы найти наши аспекты. Чтобы использовать автоматическое сканирование, вам нужно не только определить класс аспектов с помощью аннотации @Aspect, но также вам нужно будет включить аннотацию @Component. Если вы не включите @Component, вам нужно будет определить аспект в файле конфигурации xml.

6.Централизация обработки ошибок

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
@Component
@Aspect
public class CentralExceptionHandler {
    private static Logger errorLogger = LoggerFactory.getLogger("errors");
     
    @Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) && target(controller)")
    public String handleException(ProceedingJoinPoint jp, Object controller) throws Throwable {
        String view = null;
         
        try {
            view = (String) jp.proceed();
        } catch (DataAccessException e) {
            errorLogger.error("error in {}", controller.getClass().getSimpleName(), e);
            NotificationUtils.sendNotification(e);
            return "errorPage";
        }
         
        return view;
    }
}

Аннотация @Target позволяет нам ссылаться на перехваченный класс. Теперь у нас есть обработка исключений, обработанная аспектом, чтобы мы могли избавиться от этой логики в наших контроллерах.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
public class ClientController {
    @Autowired
    private ClientService clientService;
    private static Logger mainLogger = LoggerFactory.getLogger("generic");
    //private static Logger errorLogger = LoggerFactory.getLogger("errors");
     
    @RequestMapping("/getClients")
    public String getClients(Model model, @RequestParam("id") int id) {
        mainLogger.debug("Executing getClients request");
         
        //try {
            Client client = clientService.getClient(id);
            model.addAttribute("client", client);
        //} catch (DataAccessException e) {
            //errorLogger.error("error in ClientController", e);
            //NotificationUtils.sendNotification(e);
            //return "errorPage";
        //}
         
        return "showClient";
    }  
}

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

1
@AfterThrowing(pointcut="@annotation(org.springframework.web.bind.annotation.RequestMapping)", throwing="e")

Но имейте в виду, что этот совет не помешает распространению исключения.

7. Централизация ведения журнала

У аспекта регистрации будет два совета, один для регистрации контроллера и другой для регистрации сервиса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Aspect
@Component
public class CentralLoggingHandler {
    private static Logger mainLogger = LoggerFactory.getLogger("generic");
     
    @Before("@annotation(org.springframework.web.bind.annotation.RequestMapping) && @annotation(mapping)")
    public void logControllerAccess(RequestMapping mapping) {
        mainLogger.debug("Executing {} request", mapping.value()[0]);
    }
     
    @Before("execution(* xpadro.spring.mvc.*..*Service+.*(..)) && target(service)")
    public void logServiceAccess(Object service) {
        mainLogger.debug("Accessing {}", service.getClass().getSimpleName());
    }
}

8. Наконец, концерн мониторинга

Мы напишем еще один аспект для мониторинга беспокойства. Совет следующий:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Aspect
@Component
public class CentralMonitoringHandler {
    private static Logger monitorLogger = LoggerFactory.getLogger("monitoring");
     
    @Around("execution(* xpadro.spring.mvc.*..*Service+.*(..)) && target(service)")
    public Object logServiceAccess(ProceedingJoinPoint jp, Object service) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = jp.proceed();
        long totalTime = System.currentTimeMillis() - startTime;
        monitorLogger.info("{}|Invocation time {}ms ", service.getClass().getSimpleName(), totalTime);
         
        return result;
    }
}

9. Проверка окончательного кода

После того, как мы сконфигурировали все сквозные задачи, наши контроллеры и сервисы содержат только бизнес-логику:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Controller
public class ClientController {
    @Autowired
    private ClientService clientService;
     
    @RequestMapping("/getClients")
    public String getClients(Model model, @RequestParam("id") int id) {
        Client client = clientService.getClient(id);
        model.addAttribute("client", client);
         
        return "showClient";
    }  
}
 
 
@Service("clientService")
public class ClientServiceImpl implements ClientService {
    @Autowired
    private ClientRepository clientRepository;
     
    @Override
    @Transactional(readOnly = true)
    public Client getClient(int id) {
        return clientRepository.getClient(id);
    }
}

10.Conclusion

Мы увидели, как применять аспектно-ориентированное программирование, чтобы сохранить наш код чистым и сфокусированным на логике, для которой он был разработан. Перед использованием AOP просто примите во внимание его известные ограничения.