Spring - 如何使用 aspectJ 缓存自调用?

Posted

技术标签:

【中文标题】Spring - 如何使用 aspectJ 缓存自调用?【英文标题】:Spring - How to cache in self-invocation with aspectJ? 【发布时间】:2020-11-05 23:19:12 【问题描述】:

感谢您点击我的问题。 我想在自调用中调用缓存方法,所以需要使用AspectJ。 (缓存的配置没问题)

    添加 AspectJ 依赖项
implementation 'org.springframework.boot:spring-boot-starter-aop'
    将 @EnableCaching(mode = AdviceMode.ASPECTJ) 添加到我的 application.java
@EnableJpaAuditing
@EnableCaching(mode = AdviceMode.ASPECTJ) // <-- here 
@SpringBootApplication
public class DoctorAnswerApplication 

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


    我的服务.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);
    

    我的测试代码
    @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);
    

我没有...? 我没有开设任何与 aspectJ 相关的课程。 我认为 @EnableCaching(mode = AdviceMode.ASPECTJ) 使 @Cacheable 由 AspectJ 代替 Spring AOP(proxy) 工作。

【问题讨论】:

【参考方案1】:

你读过Javadoc for EnableCaching吗?

请注意,如果 mode() 设置为AdviceMode.ASPECTJ,则proxyTargetClass() 属性的值将被忽略。另请注意,在这种情况下,spring-aspects 模块 JAR 必须存在于类路径中,编译时编织或加载时编织将方面应用于受影响的类。这种情况下不涉及代理;本地调用也会被拦截。

所以请检查你是否

    在类路径上有spring-aspects,并且 使用参数java -javaagent:/path/to/aspectjweaver.jar 启动您的应用程序。

#2 有一个替代方案,但使用 Java 代理是最简单的。我不是 Spring 用户,所以我不是 Spring 配置方面的专家,但即使是像我这样的 Spring 菜鸟也能成功使用 Java 代理,所以请先试一试。

【讨论】:

感谢您的评论。我看到一个文件说 implementation 'org.springframework.boot:spring-boot-starter-aop' 包括 org.springframework:spring-aop 和 org.aspectj:aspectjweaver。我正在尝试从 ~aspectjweaver.jar 开始。我想我没有设置一些配置。 请在您尝试后报告,以便我们一起关闭此问题。 好的!我会回来的。 你是终结者吗? ?【参考方案2】:

感谢@kriegaex,他通过指出 spring-aspects 依赖和 load-time-weaving javaagent 要求来修复我。为了方便其他人,Spring Boot和Maven的配置sn-ps如下。

(注意:最后,我觉得这一切(以及副作用)对于我的项目来说并不值得。请参阅我的其他答案,了解一个简单但有点难看的解决方法。)

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>

应用配置:

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

目标方法:

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

编织机制:

选项 1:加载时间编织(Spring 默认)

使用 JVM javaagent 参数或添加到您的 servlet 容器库

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

选项 2:编译时间编织

(这应该可行,但我发现缺少用于 Spring 缓存的连贯示例 - 请参阅下面的进一步阅读)

使用 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>

出于参考/搜索目的,以下是引发这一切的错误:

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

更多阅读:

AOP 和 Spring:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop AspectJ 教程(Baeldung):https://www.baeldung.com/aspectj 编译时编织与加载时编织:https://***.com/a/23042793/631272 春季简报中的 CTW 与 LTW:https://***.com/a/41370471/631272 CTW 与 LTW 教程:https://satenblog.wordpress.com/2017/09/22/spring-aspectj-compile-time-weaving/ 让 CTW 在 Eclipse M2e 中工作:https://***.com/a/19616845/631272 CTW 和 Java 11 问题(可能是我与之斗争的一部分):https://www.geekyhacker.com/2020/03/28/how-to-configure-aspectj-in-spring-boot/

【讨论】:

再次感谢您。阅读您的答案后,我创建了新项目进行测试。如果您很有趣并且有时间提供帮助,可以检查一下我的 git 吗? link - 'aspectj' 分支:我正在尝试应用 AspjectJ。但@Cacheable 不工作 - '缓存' 分支:在应用 AspectJ 之前。 @Cacheable 效果很好【参考方案3】:

TL;DR:如果 AspectJ 让您头疼,而您除了解决 Spring Caching 自调用之外并不真正需要它,那么添加一个简单的“缓存委托”bean 实际上可能更清洁/更轻/更容易您的服务层可以重复使用。没有额外的依赖,没有性能影响,也没有意外的副作用来改变 spring 代理默认的工作方式。

代码:

@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());
    


添加另一个答案,因为为了自己解决同样的问题,我最终改变了方向。 @kriegaex 和我之前提到了更直接的解决方案,但对于那些在您根本不需要 AspectJ 时遇到问题的人来说,这是一个不同的选择。

对于我的项目,仅添加 AspectJ 以允许可缓存的相同 bean 引用是一场灾难,它导致了 10 个新问题,而不是一个简单(但令人讨厌)的问题。

一个简短的非详尽纲要是:

引入多个新依赖项 将复杂的 POM 插件引入编译时编织(这对我来说从来没有完全正确)或将运行时字节编织 jar 编组到正确的位置 为我们的所有部署添加运行时 javaagent JVM 参数 在构建时或开始时(进行编织)的性能要差得多 AspectJ 在代码库的其他区域(我很乐意使用 Spring 代理)中的 Spring Transactional 注释上使用并失败 Java 版本控制问题 不知何故依赖于古老的 Sun Microsystems tools.jar(在更高版本的 OpenJDK 中不存在) 关于如何独立于 Spring Transactions 和/或没有使用 AspectJ 的成熟 AOP(我不需要/不需要)实现缓存用例的一般较差且分散的文档

最后,我只是通过代理恢复到 Spring Caching,并引入了一个“缓存委托”,我的两个服务方法都可以引用它。这种解决方法不是最漂亮的,但对我来说,比我在真正不需要 AspectJ 时跳过的所有 AspectJ 箍更可取。我只想要无缝缓存和 DRY 服务代码,这种解决方法可以实现。

【讨论】:

感谢您分享您的经验和好文章。我通过代理使用 @EnableAspectJAutoProxy(exposeProxy = true) 注释而不是 AspectJ 应用了 Spring Cache(EhCache)。它可以直接访问类的AOP类(自调用)。我是 Spring 初学者,所以我只想学习和应用每种情况的最佳实践。没想到 AspectJ 出现了 10 次头疼?所以我会再次研究并阅读您链接的文章以备下次准备。 对不起,迟到的评论,但请允许我说,我认为您的问题不是 AspectJ,或者在 Spring 中使用 AspectJ 非常困难(实际上使用 LTW 非常简单),但那可能你只是遇到了问题,因为你是第一次这样做。例如,tools.jar 问题可能是由于您使用了仍然依赖于 Java 8 的过时版本的 AspectJ Maven 插件,参考 tools.jar,但您使用了更新的 JDK 进行编译。使用更新的 Maven 插件,这将不是问题。 实际上,加载时编织 (LTW) 根本不需要 AspectJ Maven 插件,仅用于编译时编织 (CTW)。也许您不小心将两者混合在一起,或者您在解决问题过程的不同迭代中都尝试了这两种方法。 @kriegaex 感谢您的 cmets。我确实提到有两种方法,CTW 或 LTW,两者的配置/插件不同。我没有同时做这两件事,但我在回答中都提到了。 另外,我同意第一次这样做很困难,你可以通过足够的努力让它工作。我的意思是,即使它确实有效,这种取舍也不值得(对我而言)。除了使缓存更加无缝之外,我不需要它在我的代码中执行任何功能。配置很痛苦,性能也不是微不足道的。工具依赖来自 spring-boot BOM 中的一个部门。与它大相径庭,首先使用 spring-boot 会带来很大的好处。我会一直使用最新的插件。从那时起,他们可能已经继续前进了。

以上是关于Spring - 如何使用 aspectJ 缓存自调用?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 JUnit 测试 Spring Boot aspectj 方法?

自定义注解解耦reids缓存使用

如果没有AspectJ,Spring中如何使用SpringAOP@Transactional?

[AOP] 2. AOP的两种实现-Spring AOP以及AspectJ

Spring AOP 与 AspectJ

SpringBoot 中的 Aop + 自定义注解