Статьи

Простое аспектно-ориентированное программирование (AOP) с использованием CDI в JavaEE

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

01
02
03
04
05
06
07
08
09
10
11
12
13
public class MyService{
   public ServiceModel service1(){
      isAuthorized();
      //execute business logic.
   }
}
 
public class MyAnotherService{
  public ServiceModel service1(){
    isAuthorized():
    //execute business logic.
  }
}

Вышеупомянутый подход будет работать, но не без шума кода, смешивая сквозные проблемы с бизнес-логикой. Существует другой подход для решения вышеуказанных требований, который заключается в использовании аспекта, и этот подход называется аспектно-ориентированным программированием (AOP). Существует несколько способов использования AOP — с помощью Spring AOP, JavaEE AOP. В этом примере я попытаюсь использовать AOP с использованием CDI в приложениях Java EE. Чтобы объяснить это, я выбрал очень простой пример создания веб-приложения для извлечения нескольких записей из базы данных и отображения в браузере.

Создание слоя доступа к данным

Структура таблицы:

1
2
3
4
5
create table people(
    id INT NOT NULL AUTO_INCREMENT,
    name varchar(100) NOT NULL,
    place varchar(100),
    primary key(id));

Давайте создадим класс Model для хранения информации о человеке.

01
02
03
04
05
06
07
08
09
10
11
12
package demo.model;
public class Person{
  private String id;
  private String name;
  private String place;
  public String getId(){ return id; }
  public String setId(String id) { this.id = id;}
  public String getName(){ return name; }
  public String setName(String name) { this.name = name;}
  public String getPlace(){ return place; }
  public String setPlace(String place) { this.place = place;}
}

Давайте создадим объект доступа к данным, который предоставляет два метода:

  1. чтобы узнать подробности всех людей
  2. получить данные одного человека с данным идентификатором
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package demo.dao;
 
import demo.common.DatabaseConnectionManager;
import demo.model.Person;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
 
public class PeopleDAO {
 
    public List<Person> getAllPeople() throws SQLException {
        String SQL = "SELECT * FROM people";
        Connection conn = DatabaseConnectionManager.getConnection();
        List<Person> people = new ArrayList<>();
        try (Statement statement = conn.createStatement();
                ResultSet rs = statement.executeQuery(SQL)) {
            while (rs.next()) {
                Person person = new Person();
                person.setId(rs.getString("id"));
                person.setName(rs.getString("name"));
                person.setPlace(rs.getString("place"));
                people.add(person);
            }
        }
        return people;
    }
 
    public Person getPerson(String id) throws SQLException {
        String SQL = "SELECT * FROM people WHERE id = ?";
        Connection conn = DatabaseConnectionManager.getConnection();
        try (PreparedStatement ps = conn.prepareStatement(SQL)) {
            ps.setString(1, id);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    Person person = new Person();
                    person.setId(rs.getString("id"));
                    person.setName(rs.getString("name"));
                    person.setPlace(rs.getString("place"));
                    return person;
                }
            }
        }
 
        return null;
    }
}

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

Создание перехватчиков

Создание перехватчиков включает в себя 2 этапа:

  1. Создать привязку перехватчика, которая создает аннотацию с аннотацией @InterceptorBinding которая используется для привязки кода перехватчика и целевого кода, который необходимо перехватить.
  2. Создайте класс с аннотацией @Interceptor который содержит код перехватчика. Он будет содержать методы, аннотированные @AroundInvoke , различные аннотации жизненного цикла, @AroundTimeout и другие.

Давайте создадим привязку Interceptor по имени @LatencyLogger

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package demo;
 
import java.lang.annotation.Target;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.*;
import static java.lang.annotation.ElementType.*;
import javax.interceptor.InterceptorBinding;
 
@InterceptorBinding
@Retention(RUNTIME)
@Target({METHOD, TYPE})
public @interface LatencyLogger {
     
}

Теперь нам нужно создать код Interceptor, который аннотируется @Interceptor а также аннотируется привязкой Interceptor, которую мы создали выше, т.е. @LatencyLogger :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package demo;
import java.io.Serializable;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
 
@Interceptor
@LatencyLogger
public class LatencyLoggerInterceptor implements Serializable{
   
  @AroundInvoke
    public Object computeLatency(InvocationContext invocationCtx) throws Exception{
        long startTime = System.currentTimeMillis();
        //execute the intercepted method and store the return value
        Object returnValue = invocationCtx.proceed();
        long endTime = System.currentTimeMillis();
        System.out.println("Latency of " + invocationCtx.getMethod().getName() +": " + (endTime-startTime)+"ms");
        return returnValue;
         
    }
}

В приведенном выше коде есть две интересные вещи:

  1. использование @AroundInvoke
  2. параметр типа InvocationContext переданный методу

@AroundInvoke определяет метод как метод перехватчика. Класс Interceptor может иметь только ОДИН метод, аннотированный этой аннотацией. Когда целевой метод перехватывается, его контекст передается перехватчику. Используя InvocationContext можно получить детали метода, параметры, переданные методу.

Нам нужно объявить вышеупомянутый перехватчик в файле WEB-INF / beans.xml

01
02
03
04
05
06
07
08
09
10
<?xml version="1.0" encoding="UTF-8"?>
       bean-discovery-mode="all">
     
    <interceptors>
        <class>demo.LatencyLoggerInterceptor</class>
    </interceptors>
</beans>

Создание сервисных API с аннотациями перехватчиков

Мы уже создали привязку Interceptor и перехватчик, который выполняется. Теперь давайте создадим API-интерфейсы служб, а затем аннотируем их с помощью привязки Interceptor.

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
29
30
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package demo.service;
 
import demo.LatencyLogger;
import demo.dao.PeopleDAO;
import demo.model.Person;
import java.sql.SQLException;
import java.util.List;
import javax.inject.Inject;
 
public class PeopleService {
 
  @Inject
  PeopleDAO peopleDAO;
 
  @LatencyLogger
  public List<Person> getAllPeople() throws SQLException {
    return peopleDAO.getAllPeople();
  }
 
  @LatencyLogger
  public Person getPerson(String id) throws SQLException {
    return peopleDAO.getPerson(id);
  }
 
}

Мы аннотировали сервисные методы с помощью привязки Interceptor @LatencyLogger . Другим способом будет аннотирование на уровне класса, которое затем применяет аннотацию ко всем методам класса. @Inject также @Inject аннотацию @Inject которая внедряет экземпляр, т.е. вводит зависимость в класс.

Далее следует подключить контроллер и вид для отображения данных. Контроллер — это сервлет, а view — это простая JSP, использующая теги JSTL.

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
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package demo;
 
import demo.model.Person;
import demo.service.PeopleService;
import java.io.IOException;
import java.sql.SQLException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.inject.Inject;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
@WebServlet(name = "AOPDemo", urlPatterns = {"/AOPDemo"})
public class AOPDemoServlet extends HttpServlet {
 
  @Inject
  PeopleService peopleService;
 
  @Override
  public void doGet(HttpServletRequest request, HttpServletResponse response)
          throws ServletException, IOException {
    try {
      List<Person> people = peopleService.getAllPeople();
      Person person = peopleService.getPerson("2");
      request.setAttribute("people", people);
      request.setAttribute("person", person);
      getServletContext().getRequestDispatcher("/index.jsp").forward(request, response);
    } catch (SQLException ex) {
      Logger.getLogger(AOPDemoServlet.class.getName()).log(Level.SEVERE, null, ex);
    }
  }
}

Вышеуказанный сервлет доступен по адресу http: // localhost: 8080 /

/ AOPDemo. Он извлекает данные и перенаправляет на представление для отображения того же. Обратите внимание, что служба также была @Inject с использованием аннотации @Inject . Если зависимости не внедрены и вместо этого созданы с использованием new то перехватчики не будут работать. Это важный момент, который я понял, создавая этот образец.

JSP для визуализации данных будет

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
29
30
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@ taglib prefix="c"
           uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>AOP Demo</title>
  </head>
  <body>
    <h1>Hello World!</h1>
    <table>
      <tr>
        <th>Id</th>
        <th>Name</th>
        <th>Place</th>
      </tr>
      <c:forEach items="${requestScope.people}" var="person">
        <tr>
          <td><c:out value="${person.id}"/></td>
          <td><c:out value="${person.name}"/></td>
          <td><c:out value="${person.place}"/></td>
        </tr>
      </c:forEach>
    </table>
    <br/>
    Details for person with id=2
    <c:out value="Name ${person.name} from ${person.place}" />
  </body>
</html>

При этом вы бы создали очень простое приложение с использованием перехватчиков. Спасибо, что читаете и остаетесь со мной до конца. Пожалуйста, поделитесь своими вопросами / отзывами в качестве комментариев. А также поделитесь этой статьей с друзьями!