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

我的梦境 提交于 2021-01-28 11:17:33

问题


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


回答1:


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.




回答2:


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 was worth it for my project. See my other answer for a simpler, 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:

  • AOP and Spring: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop
  • AspectJ tutorial (Baeldung): https://www.baeldung.com/aspectj
  • Complie-Time Weaving vs Load-Time Weaving: https://stackoverflow.com/a/23042793/631272
  • CTW vs LTW in spring brief: https://stackoverflow.com/a/41370471/631272
  • CTW vs LTW Tutorial: https://satenblog.wordpress.com/2017/09/22/spring-aspectj-compile-time-weaving/
  • Getting CTW to work in Eclipse M2e: https://stackoverflow.com/a/19616845/631272
  • CTW and Java 11 issues (may have been part of my struggles with it): https://www.geekyhacker.com/2020/03/28/how-to-configure-aspectj-in-spring-boot/



回答3:


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.

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 gross, but for me was preferable to all the AspectJ hoops I was jumping through when I really didn't need/want AspectJ. I just want seamless caching and DRY service code, which this workaround achieves.



来源:https://stackoverflow.com/questions/62932146/spring-how-to-cache-in-self-invocation-with-aspectj

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!