Использование удаленного мониторинга Java-программ на уровне метода Metrics (оптимизация)

Java задняя часть Spring API

Эта статья продолжает предыдущуюИспользуйте удаленный мониторинг Java-программ на уровне метода Metrics.Говоря о. В приведенном выше примере мы просто реализовали функцию, но если ее превратить в библиотеку и использовать для нескольких проектов, будет хуже. Поэтому я сделал некоторые оптимизации в этой библиотеке.

1 недостаток

  1. Мертвые точки @RestController, @Controller и @Service не являются гибкими. Однако некоторые сервисы в нашем проекте также используют свои собственные пользовательские аннотации, такие как @XXController и @XXService (конкретные названия здесь писать не будем). В нашей бизнес-логике он принадлежит слою контроллера и слою службы. Если это предыдущий метод кодирования, методы в этих классах не могут отслеживаться. Поэтому я надеюсь, что аспект мониторинга можно настроить.
  2. Имя пакета прописано насмерть.Если я сменю бизнес и использую другое имя пакета, я не могу его контролировать. Поэтому я надеюсь, что имя пакета также можно настроить.

2 Реализация настраиваемых аспектов

2.1 Желаемая поза использования

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

import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
// 这是我们自定义的Controller注解
import com.sinafenqi.cashloan.annotations.XXXControler;  

@SpringBootApplication
//通过在Application上加一个注解,配置切点
@MonitorEnable({Controller.class, Service.class, XXXControler.class})  
public class MonitorApplication {

    public static void main(String[] args) {
        SpringApplication.run(MonitorApplication.class, args);
    }

}

Я хотел бы иметь возможность использовать аннотацию в приложении (например, MonitorEnable), а затем указать в нем pointcut (или даже пользовательскую аннотацию). Таким образом, мы можем произвольно выбирать код бизнес-уровня, который хотим отслеживать. Итак, давайте посмотрим, как это сделать дальше.

2.2 Получение конфигурационных точек из пользовательских аннотаций

2.2.1 Аннотации, используемые в пользовательской конфигурации

Первая пользовательская аннотацияMonitorEnableи определитьvalueМассив классов для аннотации. Здесь мы ограничиваем типы pointcut для простоты. Впоследствии, если вам нужно расширить функцию, отпустите ее.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MonitorEnable {

    Class<? extends Annotation>[] value();

}

Это позволяет другим проектам использовать эту аннотацию в классе. Далее мы получаем значение Value в аннотации.

2.2.2 Получение срезов конфигурации из аннотаций

MonitorEnableРоль заключается в предоставлении данных конфигурации. Итак, если мы хотим получить информацию, содержащуюся в нем, нам нужноMonitorEnableплюс@ImportАннотируем и указываем для него класс конфигурации, здесь мы указываемMonitorConfigдля класса конфигурации.

измененныйMonitorEnableФайл аннотации:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MonitorConfig.class)
public @interface MonitorEnable {

    Class<? extends Annotation>[] value();

}

хочу быть вMonitorConfigПолучено из класса конфигурацииMonitorEnableИнформация о конфигурации в , должна быть реализованаImportAwareинтерфейс, так что Spring загружаетсяMetaDataперезвонит, когдаsetImportMetadataметод. Мы можем получить содержимое в аннотации здесь.

@Configuration
@Log
public class MonitorConfig implements ImportAware{

    // MonitorProperty类中包装了监控属性。用来存储配置的切点
    public MonitorProperty monitorProperty = new MonitorProperty();  

    // 原来的内容不变,这里省略,详情请参考上一篇文章

    // 这里把MonitorProperty装载到Spring容器。以供其他人使用
    @Bean
    public MonitorProperty monitorProperty() {
        return monitorProperty;
    }

    // 这里获取配置的切点,并设置到monitorProperty中
    @Override
    public void setImportMetadata(AnnotationMetadata annotationMetadata) {
        Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(MonitorEnable.class.getName(), false);
        AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(attributes);

        Class<? extends Annotation>[] aopClasses = (Class<? extends Annotation>[]) annotationAttributes.getClassArray("value");
        if (aopClasses == null || aopClasses.length == 0) {
            throw new RuntimeException("monitor cannot get aop annotation classes. nothing to monitor. Please use MonitorEnable annotation on your application.");
        }
        monitorProperty.setAopAnnotationClasses(aopClasses);
    }
}

Класс MonitorProperty:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MonitorProperty {

    private Class<? extends Annotation>[] aopAnnotationClasses;

}

Таким образом, мы можем получить его при запуске программыMonitorEnableЗначение, настроенное пользователем и сохраненное вMonitorPropertyсередина.

2.3 Подготовьте таймер к методу мониторинга в соответствии с точкой отсечки

Теперь, когда pointcut настроен,为监控方法准备TimerЭтот шаг также следует изменить соответствующим образом. Этот шаг относительно прост.MethodMonitorCenterКод повышения класса выглядит следующим образом: отMonitorPropertyПолучите pointcut в , и замените ранее написанную логику:

@Log
public class MethodMonitorCenter implements ApplicationContextAware {

    // 将MonitorProperty注入进来
    @Autowired
    private MonitorProperty monitorProperty;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, Object> monitorBeans = new HashMap<>();
        // 这里从monitorProperty中获取切点
        Class<? extends Annotation>[] classes = monitorProperty.getAopAnnotationClasses();
        if (classes == null || classes.length == 0) {
            return;
        }
        for (Class<? extends Annotation> aClass : classes) {
            // 这里使用获取的切点获取要监控的类,下面的筛选逻辑与之前相同,省略
            monitorBeans.putAll(applicationContext.getBeansWithAnnotation(aClass));
        }

        // 之后和以前一摸一样,这里省略。
    }

}

2.4 Создание касательных плоскостей на основе касательных точек

Читатели, не знакомые со Spring AOP, могут поискать в Интернете. Есть много статей об этом. Я не буду повторять это.

Наши общие положения использования Spring AOP жестко запрограммированы. Так называемый жестко закодированный способ относится к аннотациям Java (метод, который я использовал в своей последней статье) и методам конфигурации XML. Теперь наш pointcut настроен. Тогда это не может быть достигнуто путем жесткого кодирования. Однако и динамические прокси Java, и AspectJ должны знать целевой класс прокси. Явно не подходит для нашего сценария. Но я считаю, что жесткое кодирование можно сделать, мягкое кодирование определенно можно сделать, но это может быть более проблематично. Итак, я пролистал исходный код Spring. Нашел способ. В этой статье не хочу включать исходный код и принципы, только реализацию.

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

2.4.1 Подготовка к созданию класса огранки

пользовательский классMonitorAdviceвыполнитьMethodInterceptorинтерфейс, из нихinvokeМетод эквивалентен методу обертывания среза.

@Log
public class MonitorAdvice implements MethodInterceptor {

    MetricRegistry metricRegistry;

    public MonitorAdvice(MetricRegistry metricRegistry) {
        this.metricRegistry = metricRegistry;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String methodName = invocation.getMethod().toString();
        log.info("monitor invoke. method: " + methodName);
        boolean contains = metricRegistry.getNames().contains(methodName);
        if (!contains) {
            return invocation.proceed();
        }
        log.info("monitor start method = [" + methodName + "]");
        Timer timer = metricRegistry.timer(methodName);
        Timer.Context context = timer.time();
        try {
            return invocation.proceed();
        } finally {
            context.stop();
        }
    }

}

2.4.2 Использование программного кода для создания срезов из точек касания

MonitorConfigДобавьте код в класс, см. пояснения в комментариях:

@Configuration
@Log
// 让MonitorConfig实现BeanFactoryPostProcessor接口,
// 在其postProcessBeanFactory方法中我们可以软代码向Spring装载Bean
public class MonitorConfig implements ImportAware, BeanFactoryPostProcessor {

    // 该类中其他代码保留不变,省略
    // 这里将上面自定义的MonitorAdvice类装载到Spring中
    @Bean
    public MonitorAdvice monitorAdvice(MetricRegistry metricRegistry) {
        return new MonitorAdvice(metricRegistry);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        DefaultListableBeanFactory factory = (DefaultListableBeanFactory) beanFactory;
        MonitorAdvice monitorAdvice = (MonitorAdvice) factory.getBean("monitorAdvice");
        // 获取配置的切点
        Class<? extends Annotation>[] classes = monitorProperty.getAopAnnotationClasses();
        if (classes == null || classes.length == 0) {
            return;
        }
        for (Class<? extends Annotation> aClass : classes) {
            // 软代码根据切点创建Pointcut
            AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(aClass);
            // 软代码创建Advisor(硬编码的方式也是转化成这个东西)
            AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(DefaultPointcutAdvisor.class.getName())
                    .addPropertyValue("pointcut", pointcut)
                    .addPropertyValue("advice", monitorAdvice)
                    .getBeanDefinition();
            // 然后将Advisor装载到Spring
            factory.registerBeanDefinition("monitorAdvisor" + aClass.getName(), beanDefinition);
        }
    }
}

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

3 Настраиваемые имена пакетов

Это намного проще, чем предыдущая оптимизация.

3.1 Желаемая поза использования

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

пройти черезapplication.propertiesконфигурация файла, например, вapplication.propertiesДобавьте в файл следующий код:

monitor.property.basePackages=com.xxx,com.yyy

или черезMonitorEnableАннотации настроены следующим образом:

@MonitorEnable(value = {/*这里是配置的切点们,省略*/}, basePackages = {"com.xxx","com.yyy"})

Для достижения имени пакета, разработанного с помощью мониторинга.

3.2 Реализовать настраиваемое имя пакета

Здесь только черезapplication.propertiesРешение реализовано в виде файла конфигурации. Реализация конфигурации через аннотации есть только по-разному.Если интересно, можете сразу перейти к исходникам.

3.2.1 MonitorProperty добавляет свойство имени пакета

Мы можем заметить, что несколько имен пакетов можно настроить в 3.1, затем вMonitorPropertyдобавить атрибутыbasePackages:

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MonitorProperty {

    private Class<? extends Annotation>[] aopAnnotationClasses;

    private String[] basePackages;

}

3.2.2 Присвоение значения MonitorProperty

Есть много методов для этого шага, мы используемConfigurationPropertiesАннотация присваивает ему:

@Configuration
@Log
public class MonitorConfig implements ImportAware, BeanFactoryPostProcessor {

    
    @Bean
    // 在装载MonitorProperty的地方加上ConfigurationProperties注解,为其赋值。
    @ConfigurationProperties("monitor.property")
    public MonitorProperty monitorProperty() {
        return monitorProperty;
    }
    
    
}

3.2.3 Логика изменения фильтра

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

@Log
public class MethodMonitorCenter implements ApplicationContextAware {

    @Autowired
    private MonitorProperty monitorProperty;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        Map<String, Object> monitorBeans = new HashMap<>();
        
        // 获取要监控的Bean过程省略
        
        monitorBeans.values().stream()
                .map(obj -> obj.getClass().getName())
                .map(this::trimString)
                .map(this::getClass)
                .filter(Objects::nonNull)
                .filter(this::isInPackages) // 这里根据包名过滤
                .forEach(this::getClzMethods);
    }


    private boolean isInPackages(Class<?> clazz) {
        // 根据用户配置的包名过滤想要监控的类
        String[] basePackages = monitorProperty.getBasePackages();
        if (basePackages == null || basePackages.length == 0) {
            return true;
        }
        return Stream.of(basePackages).anyMatch(basePackage -> clazz.getName().startsWith(basePackage));
    }
    
    // 其他代码不变,省略
    
}

Это позволяет пользователям настраивать собственные имена пакетов.

4 Эффект оптимизации

Пользователь добавляет в свой класс Application, добавляяMonitorEnableаннотацию, а затем вы можете настроить pointcut конфигурации:

@MonitorEnable({RestController.class, Controller.class, Service.class, XXControler.class})

затем черезapplication.propertiesконфигурация в файлеmonitor.property.basePackagesproperties настройте имя пакета, который вы хотите отслеживать:

monitor.property.basePackages=com.xxx,com.yyy

затем пройти/monitor/metricsЭтот интерфейс Restful получает данные метода мониторинга.

5 Резюме

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

Наконец, приглашаю обратить внимание на мой личный публичный номер. Задавайте вопросы, общайтесь, что угодно.

Исходный код этой статьи