1. Введение
Основной целью аспектно-ориентированного программирования является разделение сквозных задач. Когда мы говорим о сквозных проблемах, мы имеем в виду общую функциональность, которая используется в нескольких местах в нашей системе или приложении. Эти понятия, среди прочего:
- логирование
- Управление транзакциями
- Обработка ошибок
- Мониторинг
- Безопасность
Способ достижения этого разделения — модульность этих концепций. Это позволит нам поддерживать чистоту наших классов бизнес-логики, содержащих только код, для которого был разработан класс. Если мы не будем модульно решать эти проблемы, это приведет к запутыванию кода (класс содержит различные задачи) и рассеянию кода (одна и та же проблема будет распространяться по всей системе).
В этом примере у нас есть приложение Spring MVC, которое обращается к запрашиваемым данным (клиентам и заказам) и показывает страницу с их информацией. Мы можем взглянуть на разные слои:
На приведенном выше графике мы можем оценить, что существуют функциональные возможности, распределенные по разным классам (мониторинг реализован в каждой службе), и некоторые классы содержат различные проблемы (например, класс ClientController содержит ведение журнала и обработку исключений). Чтобы это исправить, мы напишем аспекты для реализации наших сквозных задач. Цель состоит в том, чтобы реализовать следующую модель:
Каждый класс содержит только код, связанный с бизнес-логикой, в то время как аспекты будут отвечать за перехват кода для внедрения сквозных проблем.
Давайте посмотрим на это на примере.
- Исходный код можно найти на 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
|
@Controllerpublic 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
|
@Repositorypublic 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@Aspectpublic 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
|
@Controllerpublic 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@Componentpublic 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@Componentpublic 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
|
@Controllerpublic 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 просто примите во внимание его известные ограничения.

