Добавление зависимости микрометра вызывает странную проблему прокси-сервера Spring

У меня есть простое приложение Spring Boot с частным методом @Scheduled:

@SpringBootApplication
@EnableScheduling
public class DemoApplication {

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

    @Scheduled(fixedRate = 1000)
    private void scheduledTask() {
        System.out.println("Scheduled task");
    }
}

пом.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--<dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-spring-legacy</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>1.1.1</version>
        </dependency>-->

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.8.11</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.11</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

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

Caused by: java.lang.IllegalStateException: Need to invoke method 'scheduledTask' found on proxy for target class 'DemoApplication' but cannot be delegated to target bean. Switch its visibility to package or protected.
    at org.springframework.aop.support.AopUtils.selectInvocableMethod(AopUtils.java:133) ~[spring-aop-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.processScheduled(ScheduledAnnotationBeanPostProcessor.java:343) ~[spring-context-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor.postProcessAfterInitialization(ScheduledAnnotationBeanPostProcessor.java:326) ~[spring-context-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:423) ~[spring-beans-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1633) ~[spring-beans-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555) ~[spring-beans-4.3.12.RELEASE.jar:4.3.12.RELEASE]
    ... 15 common frames omitted

У кого-нибудь есть идеи, что происходит внизу?

Изменить: я думаю, что существует какой-то конфликт между зависимостями mictometer и aspectJ, проблема возникает только в том случае, если оба присутствуют в пути к классам.


person dev123    schedule 14.12.2018    source источник
comment
да, есть конфликт, или вы пытались изменить видимость частного метода на защищенный или общедоступный, не комментируя зависимость.   -  person DHARMENDRA SINGH    schedule 14.12.2018
comment
да, изменение видимости методов работает, но я бы хотел этого избежать. можно как-то?   -  person dev123    schedule 14.12.2018


Ответы (1)


Совет Spring обычно реализуется, когда это возможно, с прокси-серверами JDK, для которых требуется интерфейс для динамической реализации (который выполняет интересную логику, а затем делегирует ваш бизнес-код). В таком случае у вас есть фактический класс, поэтому лучшее, что может сделать Spring, — это создать его подкласс.

Однако здесь есть два противоречащих друг другу требования: Spring хочет применить совет, который собирает метрики для вашего кода, но, поскольку метод private, он недоступен из подкласса. (На самом деле я умеренно удивлен, что он обнаруживает и вызывает запланированную задачу с помощью частного метода.)

Изменение вашего метода на protected позволяет Spring делать это во время выполнения (не фактический код Java, а эквивалентный сгенерированный байт-код):

class DemoApplicationWithAdvice extends DemoApplication {
    @Override
    protected void scheduledTask() {
        // record start time
        super.scheduledTask();
        // write metric with execution time
    }
}
person chrylis -cautiouslyoptimistic-    schedule 14.12.2018
comment
Спасибо за ответ. Если я меняю уровень доступа на частный пакет, он также работает правильно — создает ли Spring прокси-класс с тем же пакетом, что и мой класс? - person dev123; 14.12.2018
comment
@tutnhamon Лучший способ определить это — указать System.err.println(this.getClass()) в методе задачи. Я предполагаю, что да, но критический фактор заключается в том, что когда метод является закрытым, компилятор точно знает, что он никогда не будет использоваться вне классов, компилируемых прямо сейчас, поэтому он может обрабатывать его особым образом, который может мешать подклассы. Доступ по умолчанию и выше означают, что есть шанс, что внешний класс должен взаимодействовать с ним. - person chrylis -cautiouslyoptimistic-; 14.12.2018
comment
вы правы, имя класса, напечатанное в консоли, — class com.example.demo.DemoApplication$$EnhancerBySpringCGLIB$$ee930494. Можете ли вы сказать мне, почему без микрометра или зависимости от аспекта J он работает, и Spring может запускать этот частный метод? - person dev123; 14.12.2018
comment
@tutnhamon Потому что, как и многие Java-фреймворки, Spring обманывает: он использует setAccessible для управления вещами, к которым у него тоже нет доступа, например, @PostConstruct методы и @Autowired поля (не используйте их). В Java 9 и более поздних версиях первое, что вы обычно видите в консоли, — это предупреждение о незаконном рефлексивном доступе, и если вы установите строгий менеджер безопасности, ваше приложение не запустится. - person chrylis -cautiouslyoptimistic-; 14.12.2018
comment
У вас есть идеи, почему эти две зависимости, присутствующие в пути к классам, заставляют Spring больше не обманывать? - person dev123; 14.12.2018
comment
Возможность вызвать метод и возможность переопределить этот метод — это очень разные вещи; см., например, stackoverflow.com/a/23161648/1189885 - person chrylis -cautiouslyoptimistic-; 14.12.2018
comment
Но мне любопытно, что именно происходит и почему Spring не может вызвать частный метод @Scheduled, когда микрометр и аспект J присутствуют в пути к классам - можете ли вы объяснить этот конкретный случай? - person dev123; 14.12.2018
comment
Без Micrometer и AspectJ он не пытается применить рекомендации по метрикам к методу, а просто вызывает его. - person chrylis -cautiouslyoptimistic-; 14.12.2018
comment
Когда присутствует только микрометр (приложение работает правильно), оно не применяет рекомендации по метрике к методу? - person dev123; 14.12.2018
comment
@tutnhamon Это мое лучшее предположение. Попробуйте сами и посмотрите, будет ли об этом сообщено в этом случае. - person chrylis -cautiouslyoptimistic-; 14.12.2018
comment
Вы были правы, когда присутствует AspectJ, bean-компонент ScheduledMethodMetrics регистрируется (см. io.micrometer.spring.autoconfigure.MetricsAutoConfiguration.AopRequiredConfiguration, если вам интересно), и это вызывает проблему. Спасибо за беседу :) - person dev123; 14.12.2018
comment
На самом деле часть этого ответа, в которой упоминаются динамические прокси JDK, в этом контексте неверна. Правда, прокси-серверы JDK используются по умолчанию, но они работают только для классов, реализующих интерфейсы, и только для общедоступных методов (поскольку методы интерфейса всегда общедоступны). DemoApplication не реализует какой-либо интерфейс, поэтому Spring AOP будет использовать здесь прокси-сервер CGLIB, что также является причиной того, что он вообще работает с защищенными и пакетными методами, потому что CGLIB их поддерживает. - person kriegaex; 15.12.2018
comment
@kriegaex В таком случае у вас есть реальный класс, поэтому лучшее, что может сделать Spring, - это создать его подкласс. - person chrylis -cautiouslyoptimistic-; 15.12.2018
comment
Прокси-сервер CGLIB является подклассом, и этот CGLIB действительно используется, как я уже сказал, даже упоминалось в комментарии выше: имя класса, напечатанное на консоли, будет DemoApplication$$EnhancerBySpringCGLIB$$ee930494 - person kriegaex; 15.12.2018