Статьи

Анализ снижения производительности / улучшения приложения Java EE с перехватчиками

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

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

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

Поэтому Java EE поставляется с простой в использовании альтернативой: перехватчиками. Это — то, где инверсия образца управления проигрывает его преимущества. Поскольку сервер приложений вызывает ваши методы bean-методов / вызовы веб-служб, он легко перехватывает эти вызовы и предоставляет вам способ добавления кода до и после каждого вызова.

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

1
2
3
4
@Interceptors(PerformanceInterceptor.class)
public class CustomerService {
...
}

Та же информация, что и в дескрипторе развертывания, выглядит следующим образом:

1
2
3
4
<interceptor-binding>
    <target-name>myapp.CustomerService</target-name>
    <interceptor-class>myapp.PerformanceInterceptor.class</interceptor-class>
</interceptor-binding>

Сам перехватчик может быть простым классом POJO с методом, аннотированным @AroundInvoke и одним аргументом:

01
02
03
04
05
06
07
08
09
10
11
12
@AroundInvoke
public Object measureExecutionTime(InvocationContext ctx) throws Exception {
    long start = System.currentTimeMillis();
    try {
        return ctx.proceed();
    } finally {
        long time = System.currentTimeMillis() - start;
        Method method = ctx.getMethod();
        RingStorage ringStorage = RingStorageFactory.getRingStorage(method);
        ringStorage.addMeasurement(time);
    }
}

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

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

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
public interface PerformanceResourceMXBean {
    long getMeanValue();
}
 
public class RingStorage implements PerformanceResourceMXBean {
    private String id;
 
    public RingStorage(String id) {
        this.id = id;
        registerMBean();
        ...
    }
     
    private void registerMBean() {
        try {
            ObjectName objectName = new ObjectName("performance" + id + ":type=" + this.getClass().getName());
            MBeanServer platformMBeanServer = ManagementFactory.getPlatformMBeanServer();
            try {
                platformMBeanServer.unregisterMBean(objectName);
            } catch (Exception e) {
            }
            platformMBeanServer.registerMBean(this, objectName);
        } catch (Exception e) {
            throw new IllegalStateException("Problem during registration:" + e);
        }
    }
     
    @Override
    public long getMeanValue() {
        ...
    }
    ...
}

Теперь мы можем запустить jconsole и запросить у выставленного MXBean среднее значение:

JConsole

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

Вывод

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