Spring - How to cache in self-invocation with aspectJ?

Viewed 2024

Thank you to click my question. I want to call a caching method in self-invocation, so I need to use AspectJ. (cache's config is okay)

  1. add AspectJ dependencies
implementation 'org.springframework.boot:spring-boot-starter-aop'
  1. add @EnableCaching(mode = AdviceMode.ASPECTJ) to my application.java
@EnableJpaAuditing
@EnableCaching(mode = AdviceMode.ASPECTJ) // <-- here 
@SpringBootApplication
public class DoctorAnswerApplication {

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

}
  1. my service.java
@Service
public class PredictionService {

    @Cacheable(value = "findCompletedRecordCache")
    public HealthCheckupRecord getRecordComplete(Long memberId, String checkupDate) {
        Optional<HealthCheckupRecord> recordCheckupData;
        recordCheckupData = healthCheckupRecordRepository.findByMemberIdAndCheckupDateAndStep(memberId, checkupDate, RecordStep.COMPLETE);

        return recordCheckupData.orElseThrow(NoSuchElementException::new);
    }
}
  1. my test code
    @Test
    public void getRecordCompleteCacheCreate() {
        // given
        Long memberId = (long)this.testUserId;
        List<HealthCheckupRecord> recordDesc = healthCheckupRecordRepository.findByMemberIdAndStepOrderByCheckupDateDesc(testUserId, RecordStep.COMPLETE);
        String checkupDate = recordDesc.get(0).getCheckupDate();
        String checkupDate2 = recordDesc.get(1).getCheckupDate();

        // when
        HealthCheckupRecord first = predictionService.getRecordComplete(memberId,checkupDate);
        HealthCheckupRecord second = predictionService.getRecordComplete(memberId,checkupDate);
        HealthCheckupRecord third = predictionService.getRecordComplete(memberId,checkupDate2);

        // then
        assertThat(first).isEqualTo(second);
        assertThat(first).isNotEqualTo(third);
    }

What did I don't...? I didn't make any class related with aspectJ. I think @EnableCaching(mode = AdviceMode.ASPECTJ) make @Cacheable work by AspectJ instead Spring AOP(proxy).

3 Answers

Did you read the Javadoc for EnableCaching?

Note that if the mode() is set to AdviceMode.ASPECTJ, then the value of the proxyTargetClass() attribute will be ignored. Note also that in this case the spring-aspects module JAR must be present on the classpath, with compile-time weaving or load-time weaving applying the aspect to the affected classes. There is no proxy involved in such a scenario; local calls will be intercepted as well.

So please check if you

  1. have spring-aspects on the class path and
  2. started your application with the parameter java -javaagent:/path/to/aspectjweaver.jar.

There is an alternative to #2, but using the Java agent is the easiest. I am not a Spring user, so I am not an expert in Spring configuration, but even a Spring noob like me succeeded with the Java agent, so please give that a shot first.

With thanks to @kriegaex, he fixed me by pointing out the spring-aspects dependency and the load-time-weaving javaagent requirement. For the convenience of others, the configuration snippets for Spring Boot and Maven follow.

(Note: In the end, I didn't feel all this (and the side-effects) were worth it for my project. See my other answer for a simple, if somewhat ugly, workaround.)

POM:

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

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
    </dependency>

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

Application Config:

@Configuration
@EnableCaching(mode = AdviceMode.ASPECTJ)
public class ApplicationConfig { ... }

Target Method:

@Cacheable(cacheNames = { "cache-name" })
public Thingy fetchThingy(String identifier) { ... }

Weaving mechanism:

Option 1: Load Time Weaving (Spring default)

Use JVM javaagent argument or add to your servlet container libs

-javaagent:<path-to-jar>/aspectjweaver-<version>.jar

Option 2: Compile Time Weaving

(This supposedly works, but I found a lack of coherent examples for use with Spring Caching - see further reading below)

Use aspectj-maven-plugin: https://www.mojohaus.org/aspectj-maven-plugin/index.html

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.11</version>
    <dependencies>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>${aspectj.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>${aspectj.version}</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <outxml>true</outxml>
        <showWeaveInfo>false</showWeaveInfo>
        <Xlint>warning</Xlint>
        <verbose>false</verbose>
        <aspectLibraries>
            <aspectLibrary>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aspects</artifactId>
            </aspectLibrary>
        </aspectLibraries>
        <complianceLevel>${java.version}</complianceLevel>
        <source>${java.version}</source>
        <target>${java.version}</target>
    </configuration>
</plugin>

For reference/search purposes, here is the error that started all this:

Caused by: java.io.FileNotFoundException: class path resource [org/springframework/cache/aspectj/AspectJCachingConfiguration.class] cannot be opened because it does not exist

More reading:

TL;DR: If AspectJ is giving you headaches and you don't really need it other than to work around Spring Caching self-invocation, it might actually be cleaner/lighter/easier to add a simple "cache delegate" bean that your service layers can re-use. No extra dependencies, no performance impacts, no unintended side-effects to changing the way spring proxies work by default.

Code:

@Component
public class CacheDelegateImpl implements CacheDelegate {
    @Override @Cacheable(cacheNames = { "things" })
    public Object getTheThing(String id) { ... }
}

@Service
public class ThingServiceImpl implements ThingService {
    @Resource
    private CacheDelegate cacheDelegate;

    public Object getTheThing(String id) {
        return cacheDelegate.getTheThing(id);
    }

    public Collection<Object> getAllTheThings() {
        return CollectionUtils.emptyIfNull(findAllTheIds())
                .parallelStream()
                .map(this::getTheThing)
                .collect(Collectors.toSet());
    }
}

Adding another answer, because to solve this same issue for myself I ended up changing direction. The more direct solutions are noted by @kriegaex and myself earlier, but this is a different option for people that have issues getting AspectJ to work when you don't fundamentally need it.

For my project, adding AspectJ only to allow cacheable same-bean references was a disaster that caused 10 new headaches instead of one simple (but annoying) one.

A brief non-exhaustive rundown is:

  • Introduction of multiple new dependencies
  • Introduction of complex POM plugins to either compile-time weave (which never worked quite right for me) OR marshal the run-time byte-weaving jar into the correct place
  • Adding a runtime javaagent JVM argument to all our deployments
  • Much poorer performance at either build-time or start-time (to do the weaving)
  • AspectJ picking up and failing on Spring Transactional annotations in other areas of the codebase (where I was otherwise happy to use Spring proxies)
  • Java versioning issues
    • Somehow a dependency on the ancient Sun Microsystems tools.jar (which is not present in later OpenJDK versions)
  • Generally poor and scattershot doc on how to implement the caching use case in isolation from Spring Transactions and/or without full-blown AOP with AspectJ (which I don't need/want)

In the end, I just reverted to Spring Caching by proxy and introduced a "cache delegate" that both my service methods could refer to instead. This workaround is not the prettiest, but for me was preferable to all the AspectJ hoops I was jumping through when I really didn't need AspectJ. I just want seamless caching and DRY service code, which this workaround achieves.

Related