如何使用 Java 配置手动添加 Spring CacheInterceptor?

How can I manually add a Spring CacheInterceptor using Java Config?

我正在尝试研究如何将缓存添加到第三方的方法调用中 Java class。我正在为我的应用程序使用 Spring Boot

我在尝试让缓存工作时想出了这个 class。

package test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheOperation;
import org.springframework.cache.interceptor.CacheProxyFactoryBean;
import org.springframework.cache.interceptor.CacheableOperation;
import org.springframework.cache.interceptor.NameMatchCacheOperationSource;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Collection;
import java.util.Date;
import java.util.HashSet;

@SpringBootApplication
@EnableCaching
@Configuration
public class MyApp {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(MyApp.class, args);
        Greeter greeter = context.getBean(Greeter.class);

        System.out.println(new Date() + " : " + greeter.getGreeting("Bob"));
        System.out.println(new Date() + " : " +greeter.getGreeting("Fred"));

        System.out.println(new Date() + " : " +greeter.getGreeting("Bob"));
        System.out.println(new Date() + " : " +greeter.getGreeting("Fred"));

        System.out.println(new Date() + " : " +greeter.getGreeting("Bob"));
        System.out.println(new Date() + " : " +greeter.getGreeting("Fred"));
    }

    @Bean
    public Greeter greeter() {
        final NameMatchCacheOperationSource nameMatchCacheOperationSource = new NameMatchCacheOperationSource();
        Collection<CacheOperation> cacheOperations = new HashSet<CacheOperation>();
        cacheOperations.add(new CacheableOperation.Builder().build());
        nameMatchCacheOperationSource.addCacheMethod("*", cacheOperations);

        CacheProxyFactoryBean cacheProxyFactoryBean = new CacheProxyFactoryBean();
        cacheProxyFactoryBean.setTarget(new MySlowGreeter());
        cacheProxyFactoryBean.setProxyInterfaces(new Class[] {Greeter.class});
        cacheProxyFactoryBean.setCacheOperationSources(nameMatchCacheOperationSource);
        cacheProxyFactoryBean.afterPropertiesSet();
        return (Greeter) cacheProxyFactoryBean.getObject();
    }

    interface Greeter {
        String getGreeting(String name);
    }

    class MySlowGreeter implements Greeter {
        public String getGreeting(String name) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello " + name;
        }
    }
}

我希望能够在我的 Spring 配置中创建一个 bean 来包装对 Greeter.getGreeting(..) 和 returns 的调用缓存结果(如果存在)。但是没有缓存发生。

有什么想法吗?

您可以简单地创建一个 Adapter/Wrapper class 委托给您想要启用缓存的基础第 3 方 class 类型。然后,将缓存行为添加到您的应用程序 Adapter/Wrapper 类型中。

这比尝试确定适当的 AOP 切入点来拦截第 3 方 class 上您想建议缓存的方法要容易得多。当然,如果你没有对要在其中引入缓存的对象的引用,这种方法将不起作用,在这种情况下你将不得不求助于 AOP。

从您的示例中,我的印象是您可能引用了这个第 3 方对象实例???

如果不是,请说明,然后我可以帮助您处理 AOP。

好的,我有更多信息要告诉你。但首先,我想解决上面代码的一些问题。

1) 第一个问题涉及您在应用程序 "greeter" @Bean 定义中使用 o.s.cache.interceptor.CacheProxyFactoryBean @Configuration class(即 "MyApp").

任何时候你使用 Spring 的 FactoryBeans 之一(例如 CacheProxyFactoryBean)或实现你自己的,你 return 来自 @Bean 方法的是 FactoryBean 本身, 不是 FactoryBean 的产物。所以,而不是 return factoryBean.getObject(),你会 return FactoryBean,像这样...

@Bean
GreeterFactoryBean greeter() {
  GreeterFactoryBean factoryBean = new GreeterFactoryBean();
  factoryBean.set...
  return factoryBean;
}

这里,GreeterFactoryBean实现了o.s.beans.factory.FactoryBean

正如Spring的 Reference Documentation指出的那样,Spring容器知道return FactoryBean 的产品(例如 [Singleton] Greeter 实例)和 不是 FactoryBean 本身,作为此 @Bean 方法的 Spring 容器中的 "defined" bean。如果未使用 @Bean(例如 @Bean("Greeter"))明确定义,则 bean 的名称将是 @Bean 方法的名称。

如果FactoryBean也实现了Spring的生命周期回调接口(例如o.s.beans.factory.InitializingBean or o.s.beans.factory.DisposableBean等),Spring 容器将知道在 "appropriate" 时间调用那些生命周期回调,在 Spring 容器的初始化过程中。

因此,无需在 "greeter" @Bean 定义中调用 CacheProxyFactoryBean.afterPropertiesSet()CacheProxyFactoryBean.getObject()。这样做实际上违反了 Spring 容器的初始化合同,并且您可能 运行 过早 "initialization" 出现问题,尤其是如果提供 FactoryBean实现其他 Spring 容器接口(例如 BeanClassLoaderAware,或 EnvironmentAware,等等)。

小心!

2) 其次,这不是 issue/problem 你的例子,而是需要注意的事情。您在此 SO post 中声明您正在尝试将 "caching" 行为添加到第 3 方库 classes.

您上面使用的方法仅适用于您能够自己在应用程序中实例化第 3 方 class(例如 Greeter)的情况。

但是,如果第 3 方库或框架代表您实例化 class,作为配置 library/framework 的结果(例如认为 JDBC Driver 和 Hibernate),您失去在您的应用程序中为此 class 引入缓存行为的能力,除非您求助于 Load-Time Weaving (LTW)。阅读文档了解更多详情。

好的,进入解决方案

我写了一个测试来重现这个问题并更好地理解 Spring 框架 内部发生的事情。你可以找到我完成的测试 here.

TestConfigurationOne 实际上与您采用的方法相同 以编程方式创建缓存代理,根据我上面讨论的内容进行修改,并解决我认为是 bug 核心 Spring Framework(注意:我在测试中使用了 Spring Framework 5.0.1.RELEASE)。

为了使您使用 CacheProxyFactoryBean 的配置方法起作用,我需要 extend the CacheProxyFactoryBean class. In addition to extending the CacheProxyFactoryBean, I also needed to implement the SmartInitializingSingleton interface and the BeanFactoryAware interface for reasons what will become apparent in a moment. See 9 for the complete implementation

在内部,Spring Framework 的 o.s.cache.interceptor.CacheProxyFactoryBeanmaking use of a o.s.cache.interceptor.CacheInterceptor. It also goes onto "initialize" this CacheInterceptor instance, here and here. However, this does not complete the initialization since the CacheInterceptor also, indirectly, implements the SmartInitializingSingleton interface, by extending CacheAspectSupport. If the SmartInitializingSingleton implemented CacheInterceptor.afterSingletonsInstantiated() method is never called, then the initialized bit is never tripped, and any cacheable operations will not be cached, resulting in the cacheable operation being invoked every single time(因此,忽略任何引入的缓存行为)。

这就是我在 Spring 期间的适当时刻将测试 class 中的 CacheProxyFactoryBean 扩展到 capture the "mainInterceptor" (i.e. the CacheInterceptor) and then call the afterSingletonsInstantiated() method 的确切原因 容器的初始化阶段,这就是为什么我的 SmartCacheProxyFactoryBean 扩展实现 SmartInitializingSingleton,以委托给 CacheInterceptor.afterSingletonsInstantiated() 方法。

此外,CacheInterceptorBeanFactoryAwarerequires the Spring BeanFactory to carry out its function, hence the reason I inspect this "mainInterceptor" and set the BeanFactory appropriately, here

我推荐的另一种方法是使用 TestConfigurationTwo.

在这个配置中,我直接configuringSpringAOP Advice(即CacheInterceptor),return从 @Bean 定义方法 "cacheInterceptor" 中调用它,它允许 Spring 容器适当地调用生命周期回调。

然后,我进入第3方class的use this Advice in the cache proxy creation(即“Greeter”)。

您应该小心地将“cacheInterceptor”bean 定义创建的 bean 传递给“greeter”bean 定义,like so. If you were to invoke the "cacheInterceptor" bean definition method from within your bean "greeter" bean definition, like so many users inappropriately do (for example), then you would forgo the Spring container lifecycle callbacks! Don't do this! The reasons for this are explained here

此外,要阅读有关代理程序创建的更多信息,您应该阅读 this

好的,差不多就这些了。

我提供的测试class(即“ProgrammaticCachingWithSpringIntegrationTests”)。请随意试用它,如果您有任何后续问题,请告诉我。

希望对您有所帮助!

干杯!