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
|
@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 просто примите во внимание его известные ограничения.