5

I have a simple Spring Boot application with private @Scheduled method:

@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");
    }
}

pom.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>

When micrometer dependencies and commented out everything works as expected, but when I uncomment them, following exception occurs:

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

Does anyone have an idea what's going on underneath?

Edit: I think there is some kind of conflict between mictometer and aspectJ dependencies, the issue occurs only if both are present on classpath.

talex
  • 17,973
  • 3
  • 29
  • 66
dev123
  • 477
  • 8
  • 20
  • 1
    yes there is conflict or have you tried change visibility of private method to protected or public with out commenting the dependency. – DHARMENDRA SINGH Dec 14 '18 at 07:23
  • yes, changing methods visibility works, but I would like avoid it. is it possible somehow? – dev123 Dec 14 '18 at 07:26

1 Answers1

6

Spring advice is usually implemented when possible with JDK proxies, which require an interface to implement dynamically (which does the interesting logic and then delegates to your business code). In a case like this, you have an actual class, so the best Spring can do is to subclass it.

However, there are two conflicting requirements here: Spring wants to apply advice that captures metrics for your code, but since the method is private, it isn't accessible from a subclass. (I'm actually moderately surprised it's detecting and calling your scheduled task with a private method.)

Changing your method to protected allows Spring to do this at runtime (not actual Java code, but the equivalent generated bytecode):

class DemoApplicationWithAdvice extends DemoApplication {
    @Override
    protected void scheduledTask() {
        // record start time
        super.scheduledTask();
        // write metric with execution time
    }
}
chrylis -cautiouslyoptimistic-
  • 75,269
  • 21
  • 115
  • 152
  • Thanks for reply. If I change access level to package private, it also works correctly - does Spring create a proxy class with the same package as my class has? – dev123 Dec 14 '18 at 07:29
  • 1
    @tutnhamon The best way to determine that is to put `System.err.println(this.getClass())` in your task method. My guess is yes, but the critical factor is that when a method is private, the compiler knows for a fact that it will "never" be used from outside the classes being compiled right then, so it can handle it in special ways that may interfere with subclassing. Default access and up mean that there's a chance an outside class needs to interact with it. – chrylis -cautiouslyoptimistic- Dec 14 '18 at 07:31
  • you're right, the class name printed on console is "class com.example.demo.DemoApplication$$EnhancerBySpringCGLIB$$ee930494". Can you tell me why without either micrometer or aspectJ dependency it works and Spring can run this private method? – dev123 Dec 14 '18 at 07:36
  • @tutnhamon Because like many Java frameworks, Spring cheats: It uses `setAccessible` to manipulate things it's not supposed to have access too, like `@PostConstruct` methods and `@Autowired` fields (don't use these). On Java 9 and up, the first thing you'll usually see on the console is a warning about illegal reflective access, and if you put in a strict security manager, your application will fail to start. – chrylis -cautiouslyoptimistic- Dec 14 '18 at 07:40
  • Have you got any idea why those two dependencies present on classpath causes Spring cannot "cheat" anymore? – dev123 Dec 14 '18 at 07:46
  • Being able to call a method and being able to override that method are very different things; see, for example https://stackoverflow.com/a/23161648/1189885 – chrylis -cautiouslyoptimistic- Dec 14 '18 at 07:48
  • But I'm curious what exactly happens and causes Spring cannot call private @Scheduled method when micrometer and aspectJ are both present on classpath - are you able to explain this specific case? – dev123 Dec 14 '18 at 07:56
  • Without Micrometer and AspectJ, it's not trying to apply metrics advice to the method, it's just calling it. – chrylis -cautiouslyoptimistic- Dec 14 '18 at 07:57
  • When only micrometer is present (application is running correctly), it doesn't apply metrics advice to the method? – dev123 Dec 14 '18 at 08:03
  • 1
    @tutnhamon That's my best guess. Try it for yourself and see if it gets reported in that case. – chrylis -cautiouslyoptimistic- Dec 14 '18 at 08:12
  • You were right, when AspectJ is present, ScheduledMethodMetrics bean is registered (see io.micrometer.spring.autoconfigure.MetricsAutoConfiguration.AopRequiredConfiguration if you're interested), and that causes the issue. Thanks for conversation :) – dev123 Dec 14 '18 at 08:49
  • Actually, the part of this answer mentioning JDK dynamic proxies is incorrect in this context. True, JDK proxies are the default, but they only work for classes implementing interfaces and only on public methods (because interface methods are always public). `DemoApplication` does not implement any interface, so Spring AOP will use a CGLIB proxy here, which is also the reason why it works with protected and package-scoped methods at all because CGLIB supports them. – kriegaex Dec 15 '18 at 01:24
  • @kriegaex "In a case like this, you have an actual class, so the best Spring can do is to subclass it." – chrylis -cautiouslyoptimistic- Dec 15 '18 at 04:33
  • A CGLIB proxy **is** a subclass, and that CGLIB is indeed used as I said was even mentioned in a comment above: "_class name printed on console is_ `DemoApplication$$EnhancerBySpringCGLIB$$ee930494`" – kriegaex Dec 15 '18 at 05:43