远程和网络服务
企业JavaBeans(EJB)集成
JMS(Java消息服务)
JMX
JCA CCI
任务执行和调度
Spring Framework提供异步执行和任务调度抽象,分别是TaskExecutor和TaskScheduler接口。Spring也实现了在程序服务器环境中支持线程池或CommonJ代理的特性。最后,这些通用接口背后的实现抽象掉了Java SE 5,Java SE 6和Java EE环境的不同。
Spring也使集成类支持用Timer(JDK1.3开始)和Quartz调度器(https://www.quartz-scheduler.org/)来调度。可以通过使用各自携带一个Timer或Trigger引用实例的FactoryBean来构建两种调度器。此外,有一个同时使用Quartz调度器和Timer的方便类(类似正常的MethodInvokingFactoryBean操作)。
Spring TaskExecutor抽象
执行器是JDK线程池概念的名称。“executor”名字的由来是因为没有保证底层实现真正是一个池。一个执行器可以是单线程的,设置同步的。Spring的抽象隐藏了Java SE和JavaEE环境的实现细节。
Spring的TaskExecutor接口等价于java.util.concurrent.Executor接口。事实上,原先,它存在的最重要原因是要抽象掉使用线程池时对Java 5的的依赖。这个接口有一个方法(execute(Runnable task))接收一个语义上的执行任务并且配置线程池。
TaskExecutor原先被创建来给其他Spring组件一个线程池化的抽象,在需要的时候。组件例如ApplicationEventMulticaster,JMS的AbstractMessageListenerContainer和Quartz集成都使用TaskExecutor作为池化线程的抽象。然而,自定义bean需要线程池化行为,可以按需使用这个抽象。
TaskExcutor类型
Spring包含一系列的预置TaskExecutor实现。在所有可能性中,应该从不需要实现自定义的。Spring提供的变体如下:
- SyncTaskExecutor:不异步执行。相反,每个调用在调用线程发生。
- SimpleAsyncTaskExecutor:不复用线程。每次调用开启一个新的线程。
- ConcurrentTaskExecutor:java.util.concurrent.Executor实例的适配器
- ThreadPoolTaskExecutor:用的最多的。暴露了配置java.util.concurrent.ThreadPoolExecutor的属性并包装成TaskExecutor。
- WorkManagerTaskExecutor:使用CommonJ的WorkManager作为背后服务器提供者。
- DefaultManagedTaskExecutor:使用一个JSR-236兼容运行时环境的获得JNDI的ManagedExecutorService,来代替CommonJ的WorkManager。
使用TaskExecutor
Spring的TaskExecutor实现和简单JavaBeans一样使用。在下面例子中,定义了一个bean使用ThreadPoolTaskExecutor来异步打印一系列消息。
import org.springframework.core.task.TaskExecutor;
public class TaskExecutorExample {
private class MessagePrinterTask implements Runnable {
private String message;
public MessagePrinterTask(String message) {
this.message = message;
}
public void run() {
System.out.println(message);
}
}
private TaskExecutor taskExecutor;
public TaskExecutorExample(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public void printMessages() {
for(int i = 0; i < 25; i++) {
taskExecutor.execute(new MessagePrinterTask("Message" + i));
}
}
}
可以看到,不是亲自从池中检索一个线程并执行任务,而是添加Runnable到队列。然后TaskExecutor使用内部规则来决定任务什么时候被执行。
为配置TaskExecutor使用的规则,暴露了简单属性:
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="10"/>
<property name="queueCapacity" value="25"/>
</bean>
<bean id="taskExecutorExample" class="TaskExecutorExample">
<constructor-arg ref="taskExecutor"/>
</bean>
Spring TaskScheduler抽象
除了TaskExecutor抽象,Spring 3.0引入TaskScheduler,有各种方法来调度任务在未来某个时间点运行。下面列出了TaskScheduler接口定义:
public interface TaskScheduler {
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture schedule(Runnable task, Instant startTime);
ScheduledFuture schedule(Runnable task, Date startTime);
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, long period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}
最简单的方法叫做schedule携带一个Runnable和Date参数。这导致任务在指定时间后运行一次。其他所有的方法都能调度任务重复地运行。固定频率和固定延迟方法是简单,周期地执行,但是方法接收一个Trigger会更加灵活。
Trigger接口
Trigger接口本质上受JSR-236启发,在Spring 3.0时,还没有官方地实现。Trigger的基本概念是执行时间可能由过去执行结果或任意的条件决定。如果这些决定考虑先前执行的结果,这个信息通过TriggerContext获得。Trigger接口本身很简单,如下面清单显示:
public interface Trigger {
Date nextExecutionTime(TriggerContext triggerContext);
}
Trigger实现
Spring提供Trigger接口的俩个实现。最有趣的是CronTrigger。它基于cron表达式开启任务调度。比如,下面的任务被调度来运行,每个小时过去15分钟,但是只在周末的“工作时间”9到5点时运行:
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
另一个实现是PeriodicTrigger,接收一个固定间隔,一个可选的初始延迟值,一个布尔值来表明间隔是被解释为固定频率或固定间隔。因为TaskScheduler接口已经定义了在固定频率或固定延迟调度任务的方法,这些方法可以直接使用。PeriodicTrigger可以用在依赖Trigger抽象的组件上。比如,交替使用周期的,基于cron的,设置自定义的Trigger可能更方便的情形。这样的组件可以利用依赖注入的优势,来在外部配置Triggers,所以,可以轻松地修改或继承它们。
TaskScheduler实现
TaskScheduler抽象级别和服务器环境是紧密相关地。不同的场景,Spring提供TimerManagerTaskScheduler代理到WebCommonJ的TimerManager。可以用ConcurrentTaskScheduler来适配ScheduledExecutorService。Spring提供ThreadPoolTaskScheduler,内部代理一个ScheduledExecutorService,类似ThreadPoolTaskExecutor能够配置。这些变体在嵌入式服务器环境特别是Tomcat和Jetty工作得很好。
调度和异步执行的注解支持
Spring提供注解支持任务调度和异步方法执行。
开启调度注解
为打开@Scheduled和@Async注解支持,可以添加@EnableScheduling和@EnableAsync到一个@Configuration类上,如下面例子所示:
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
按需只选择添加一个@Scheduled或@EnableAsync。为更加细腻的控制,可以继承SchedulingConfigurer接口、AsyncConfigurer接口或同时继承。
@Scheduled注解
每次执行延迟5秒
@Scheduled(fixedDelay=5000)
public void doSomething() {
// something that should execute periodically
}
每5秒执行一次
@Scheduled(fixedRate=5000)
public void doSomething() {
// something that should execute periodically
}
初始延迟和固定间隔
@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
// something that should execute periodically
}
cron表达式
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// something that should execute on weekdays only
}
也可以用zone属性来指定cron表达式解析时的time zone。
注意被调度的方法必须是void返回并且不能有参数。如果方法需要和程序范围的其他对象交互,这些对象通常通过依赖注入提供。
版本4.3开始,@Scheduled方法可以在任何范围的bean上支持。
确保运行时没有在多个实例上初始化相同的@Scheduled注解,除非想在每个实例上调度回调。相关地,也不要在注解了@Scheduled的类并注册为普通Spring beans上使用@Configurable。否则,会得到两次初始化(一次通过容器,一起通过@Configurable切面),导致每个@Scheduled方法被执行两次。
@Async注解
在方法上提供@Async注解,这样的话,方法的调用会异步发生。换句话说,调用者马上返回,而实际的执行发生在任务提交到Spring TaskExecutor上。最简单的使用场景,应用注解到返回void的方法:
@Async
void doSomething() {
// this will be executed asynchronously
}
和@Scheduled不同,这些方法可携带参数,因为这些方法以正常的方式在运行时被调用者执行,而不是一个被容器管理的调度任务。例如,下面的代码是合法的:
@Async
void doSomething(String s) {
// this will be executed asynchronously
}
甚至返回值的方法也可以异步执行。然而,这样的方法需要Future-类型的返回值。这个异步执行带来了方便,这样的话,调用者可以进行比调用Future的get()优先级更高的任务。下面的例子展示了如何在返回值的方法上使用@Async。
@Async
Future<String> returnSomething(int i) {
// this will be executed asynchronously
}
@Aync不仅可以声明java.util.concurrent.Future返回值,还可以是Spring的org.springframework.util.concurrent.ListenableFuture或版本4.2之后的java.util.concurrent.CompletableFuture。
可以使用@Async配合声明周期回调比如@PostConstruct。为异步初始化Spring beans,目前必须使用单独的初始化Spring bean来执行那个对象用@Async注解的方法,如下所示:
public class SampleBeanImpl implements SampleBean {
@Async
void doSomething() {
// ...
}
}
public class SampleBeanInitializer {
private final SampleBean bean;
public SampleBeanInitializer(SampleBean bean) {
this.bean = bean;
}
@PostConstruct
public void initialize() {
bean.doSomething();
}
}
@Async没有XML等效物,因为首先是方法应该被设计为异步执行,而不是外部重声明为异步。然而,可以用Spring AOP手动建立Spring的AsyncExecutionInterceptor,结合自定义切点。
@Async指定Executor
使用@Async的value属性来指定executor而不是默认的来执行给定方法。如下面例子所示:
@Async("otherExecutor")
void doSomething(String s) {
// this will be executed asynchronously by "otherExecutor"
}
这种情况下,“otherExecutor”是Spring容器的一个Executor名称。也可能是用@Qualifier关联的Executor。
@Async异常管理
返回值是Future-类型,管理异常比较简单。返回值是void,将不能捕捉和传输异常。可以提供AsyncUncaughtExceptionHandler来处理这种异常。如下例所示:
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}
默认,异常只是被打印。可以使用AsyncConfigurer或task:annotation-driven/>
定义AsyncUncaughtExceptionHandler。
task命名空间
scheduler
executor
scheduled-tasks
使用Quartz调度器
Quartz使用Trigger,Job和JobDetail概念来表示各种任务的调度。
使用JobDetailFactoryBean
<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="example.ExampleJob"/>
<property name="jobDataAsMap">
<map>
<entry key="timeout" value="5"/>
</map>
</property>
</bean>
package example;
public class ExampleJob extends QuartzJobBean {
private int timeout;
/**
* Setter called after the ExampleJob is instantiated
* with the value from the JobDetailFactoryBean (5)
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
// do the actual work
}
}
使用MethodInvokingJobDetailFactoryBean
如果很少需要在一个对象上执行方法,可使用MethodInvokingJobDetailFactoryBean。
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
</bean>
public class ExampleBusinessObject {
// properties and collaborators
public void doIt() {
// do the actual work
}
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>
非并发方式执行任务,设置concurrent标志为false。
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
<property name="concurrent" value="false"/>
</bean>
默认情况下,jobs以并发方式运行
使用Trigger和SchedulerFactoryBean接线任务
Spring提供两个Quartz的FactoryBean实现:CronTriggerFactoryBean和SimpleTriggerFactoryBean。
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<!-- see the example of method invoking job above -->
<property name="jobDetail" ref="jobDetail"/>
<!-- 10 seconds -->
<property name="startDelay" value="10000"/>
<!-- repeat every 50 seconds -->
<property name="repeatInterval" value="50000"/>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="exampleJob"/>
<!-- run every morning at 6 AM -->
<property name="cronExpression" value="0 0 6 * * ?"/>
</bean>
上面建立了两个trigger。一个延迟10秒每隔50秒,另一个每早6点。为最终定案,需要建立SchedulerFactoryBean:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
<ref bean="simpleTrigger"/>
</list>
</property>
</bean>
SchedulerFactoryBean还有更多的属性可用,比如日期,配置Quartz和其他。
缓存抽象
从3.1版开始,Spring框架就提供了对向现有Spring应用程序透明添加缓存的支持。与事务支持类似,缓存抽象允许一致使用各种缓存解决方案,对代码的影响最小。
从Spring 4.1开始,缓存抽象得到了显著的扩展,支持JSR-107注释和更多的定制选项。
理解缓存抽象
在其核心,缓存抽象将缓存应用于Java方法,从而减少基于缓存中可用信息的执行次数。也就是说,每次调用一个目标方法时,抽象应用一个缓存行为来检查是否已经为给定的参数调用了该方法。如果已经调用了它,则无需调用实际的方法就会返回缓存的结果。如果还没有调用该方法,则调用它,并缓存结果并返回给用户,以便下次调用该方法时,返回缓存的结果。这样,对于给定的参数集和重用的结果,昂贵的方法(无论是CPU绑定还是io绑定)只能调用一次,而不必实际再次调用该方法。缓存逻辑是透明应用的,不会对调用程序造成任何干扰。
这种方法只适用于那些无论调用多少次都保证为给定的输入(或参数)返回相同输出(结果)的方法。
缓存抽象提供了其他与缓存相关的操作,比如更新缓存内容或删除一个或所有条目的能力。如果缓存处理在应用程序过程中可能发生更改的数据,这些选项就很有用。
与其他服务在Spring框架中,缓存服务是一个抽象(不是缓存实现)和需要使用实际的存储来存储缓存数据——也就是说,抽象使你不必写缓存逻辑但不提供实际的数据存储。这种抽象是通过org.springframework.cache.Cache
和org.springframework.cache.CacheManager
接口实现的。
Spring提供了这种抽象的几个实现:基于JDK java.util.concurrent.ConcurrentMap
的缓存、Ehcache 2.x、Gemfire缓存、Caffeine和符合JSR-107的缓存(如Ehcache 3.x)。有关插入其他缓存存储和提供程序的更多信息,请参见插入不同的后端缓存。
缓存抽象对于多线程和多进程环境没有特殊的处理,因为这些特性是由缓存实现来处理的。
如果您有一个多进程环境(即部署在多个节点上的应用程序),则需要相应地配置缓存提供程序。根据您的用例,在几个节点上复制相同的数据就足够了。但是,如果在应用程序过程中更改了数据,则可能需要启用其他传播机制。
缓存特定项与通过编程式缓存交互找到的典型get-if-not-found-then-proceed-and-put-finally
代码块直接等价。没有应用锁,多个线程可能试图同时加载同一项。清除也是如此。如果多个线程试图同时更新或清除数据,则可以使用陈旧数据。某些缓存提供程序提供了这方面的高级功能。有关更多细节,请参阅缓存提供程序的文档。
要使用缓存抽象,你需要注意两个方面:
- 缓存声明:标识需要缓存的方法及其策略。
- 缓存配置:存储数据并从其中读取数据的后备缓存。
声明基于注解的缓存
对于缓存声明,Spring的缓存抽象提供了一组Java注释:
- @Cacheable:触发缓存填充。
- @CacheEvict:触发缓存驱逐。
- @CachePut:在不干扰方法执行的情况下更新缓存。
- @Caching:将应用于一个方法上的多个缓存操作重新分组。
- @CacheConfig:在类级别共享一些与缓存相关的设置。
@Cacheable注释
顾名思义,可以使用@Cacheable
来划分可缓存的方法,即结果存储在缓存中的方法,以便在后续调用(使用相同的参数)时,无需实际调用该方法就可以返回缓存中的值。在最简单的形式中,注释声明需要与注释方法相关联的缓存的名称,如下面的示例所示:
@Cacheable("books")
public Book findBook(ISBN isbn) {...}
在前面的代码片段中,findBook
方法与名为books
的缓存相关联。每次调用该方法时,都会检查缓存,以查看调用是否已经运行,并且不需要重复。虽然在大多数情况下,只声明了一个缓存,但注释允许指定多个名称,这样就可以使用多个缓存。在本例中,在调用方法之前检查每个缓存,如果至少命中一个缓存,则返回关联的值。
所有其他不包含该值的缓存也会被更新,即使缓存的方法实际上没有被调用。
下面的例子在findBook
方法上使用了@Cacheable
:
@Cacheable({"books", "isbns"})
public Book findBook(ISBN isbn) {...}
默认的键生成
由于缓存本质上是键值存储,所以每次对缓存方法的调用都需要转换为适合缓存访问的键。缓存抽象使用了一个简单的基于以下算法的键生成器:
- 如果没有给出参数,则返回
SimpleKey.EMPTY
。 - 如果只给出一个参数,则返回该实例。
- 如果给出多个参数,则返回一个包含所有参数的
SimpleKey
。
只要参数具有自然键并实现有效的hashCode()
和equals()
方法,这种方法对于大多数用例都很有效。如果情况并非如此,你就需要改变策略。
要提供一个不同的默认键生成器,您需要实现org.springframework.cache.interceptor.KeyGenerator
接口。
默认的密钥生成策略随着Spring 4.0的发布而改变。Spring的早期版本使用了一种键生成策略,对于多个键参数,只考虑参数的
hashCode()
而不考虑equals()
。这可能会导致意外的键冲突(背景信息请参见SPR-10237)。新的SimpleKeyGenerator
为此类场景使用复合键。
如果您想继续使用前面的键策略,您可以配置已弃用的org.springframework.cache.interceptor.DefaultKeyGenerator
类,或者创建一个自定义的基于散列的KeyGenerator
实现。
自定义键生成声明
由于缓存是通用的,所以目标方法很可能有各种签名,这些签名不能很容易地映射到缓存结构的顶部。当目标方法有多个参数,其中只有一些适合缓存(而其余的仅供方法逻辑使用)时,这一点就变得很明显。考虑下面的例子:
@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
乍一看,虽然这两个布尔参数影响找到书的方式,但它们对缓存没有用处。此外,如果两者中只有一个是重要的,而另一个不是呢?
对于这种情况,@Cacheable
注释允许您指定如何通过key
属性生成键。您可以使用SpEL来选择感兴趣的参数(或它们的嵌套属性)、执行操作,甚至无需编写任何代码或实现任何接口就可以调用任意方法。这是相对于默认生成器的推荐方法,因为随着代码库的增长,签名中的方法往往会有很大的不同。虽然默认策略可能适用于某些方法,但它很少适用于所有方法。
下面的例子使用了各种SpEL声明(如果您不熟悉SpEL,请自己阅读Spring Expression Language):
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
前面的代码片段显示了选择某个参数、某个属性,甚至是任意(静态)方法是多么容易。
如果负责生成键的算法过于特定,或者需要共享密钥,则可以在操作上定义一个自定义键生成器。为此,指定要使用的KeyGenerator
bean实现的名称,如下面的示例所示:
@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
键和键生成器参数是互斥的,同时指定这两个参数的操作会导致异常。
默认的缓存解决方案
缓存抽象使用一个简单的CacheResolver
,它通过使用配置的CacheManager
检索在操作级别定义的缓存。
要提供一个不同的默认缓存解析器,您需要实现org.springframework.cache.interceptor.CacheResolver
接口。
自定义缓存解决方案
默认的缓存分辨率非常适合使用单个缓存管理器的应用程序,并且没有复杂的缓存分辨率要求。
对于使用多个缓存管理器的应用程序,可以设置cacheManager
为每个操作使用,示例如下:
@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
public Book findBook(ISBN isbn) {...}
- 指定anotherCacheManager。
您还可以以类似于替换键生成的方式完全替换CacheResolver
。每个缓存操作都会请求解析,让实现根据运行时参数实际解析要使用的缓存。下面的例子展示了如何指定一个CacheResolver
:
@Cacheable(cacheResolver="runtimeCacheResolver")
public Book findBook(ISBN isbn) {...}
- 指定CacheResolver。
从Spring 4.1开始,缓存注释的
value
属性就不再是强制性的了,因为不管注释的内容是什么,CacheResolver
都可以提供这个特定的信息。与
key
和keyGenerator
类似,cacheManager
和cacheResolver
参数是互斥的,同时指定这两个参数的操作会导致异常。因为自定义的CacheManager
被CacheResolver
实现忽略。这可能不是你所期望的。
同步缓存
在多线程环境中,某些操作可能会因为相同的参数而被并发调用(通常在启动时)。默认情况下,缓存抽象不会锁定任何东西,相同的值可能会被计算多次,从而破坏缓存的目的。
对于这些特殊情况,可以使用sync
属性指示底层缓存提供程序在计算值时锁定缓存条目。因此,只有一个线程在忙于计算该值,而其他线程则被阻塞,直到该条目在缓存中更新为止。以下示例演示如何使用sync
属性:
@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}
- 使用
sync
属性。
这是一个可选特性,您最喜欢的缓存库可能不支持它。核心框架提供的所有
CacheManager
实现都支持它。有关更多细节,请参阅缓存提供程序的文档。
有条件的缓存
有时,一个方法可能不适合一直缓存(例如,它可能依赖于给定的参数)。缓存注释通过condition
参数支持这样的用例,该参数接受一个被计算为true或false的SpEL
表达式。如果为真,则缓存该方法。如果没有,它的行为就像该方法没有被缓存一样(也就是说,无论缓存中的值是什么或使用了什么参数,每次都会调用该方法)。例如,下面的方法只有当参数name的长度小于32时才会被缓存:
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
- 在
@Cacheable
上设置条件。
除了condition
参数,您还可以使用unless
参数来否决向缓存添加值。与condition
不同,unless
在调用方法之后求表达式。为了扩展前面的例子,也许我们只想缓存平装书,就像下面的例子一样:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
public Book findBook(String name)
- 使用
unless
属性阻止精装书。
缓存抽象支持java.util.Optional
,只有当java.util.Optional
存在时才使用它的内容作为缓存值。#result
总是指向业务实体,而不是受支持的包装器,因此可以将前面的示例重写为如下所示:
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
注意,result
仍然指向Book
,而不是Optional
。因为它可能是null
,所以我们应该使用安全导航操作符。
可用的缓存SpEL计算上下文
每个SpEL
表达式根据一个专用context
计算。除了内置参数之外,框架还提供专用的与缓存相关的元数据,比如参数名。下表描述了上下文可用的项,以便您可以使用它们进行键值和条件计算:
名称 | 位置 | 描述 | 例子 |
---|---|---|---|
methodName | Root object | 被调用的方法的名称 | #root.methodName |
method | Root object | 被调用的方法 | #root.method.name |
target | Root object | 被调用的目标对象 | #root.target |
targetClass | Root object | 被调用的目标的类 | #root.targetClass |
args | Root object | 用于调用目标的参数(作为数组) | #root.args[0] |
caches | Root object | 当前方法所针对的缓存的集合 | #root.caches[0].name |
参数名称 | Evaluation context | 任何方法参数的名称。如果名称不可用(可能由于没有调试信息),参数名称也可以在#a<#arg> 下可用,其中#arg 表示参数索引(从0 开始)。 | #iban or #a0 (你也可以使用#p0 或#p<#arg> 作为别名). |
result | Evaluation context | 方法调用的结果(要缓存的值)。仅在unless 表达式、cache put 表达式(用于计算key )或cache evict 表达式(当beforeInvocation 为false 时)中可用。对于所支持的包装器(比如Optional ),#result 引用实际对象,而不是包装器。 | #result |
@CachePut注释
当需要在不影响方法执行的情况下更新缓存时,可以使用@CachePut
注释。也就是说,总是调用该方法,并将其结果放入缓存中(根据@CachePut
选项)。它支持与@Cacheable
相同的选项,应该用于缓存填充,而不是方法流优化。下面的例子使用了@CachePut
注释:
@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)
在同一方法上使用
@CachePut
和@Cacheable
注释通常是非常不鼓励的,因为它们有不同的行为。后者通过使用缓存导致跳过方法调用,而前者则强制调用,以便运行缓存更新。这将导致意想不到的行为,并且,除了特定的边缘情况(例如具有排除它们彼此的条件的注释)之外,应该避免这样的声明。还要注意,这样的条件不应该依赖于result对象(即#result
变量),因为这些条件是预先验证的,以确认排除。
@CacheEvict注释
缓存抽象不仅允许填充缓存存储,还允许回收。此过程用于从缓存中删除过时或未使用的数据。与@Cacheable
相反,@CacheEvict
界定了执行缓存回收的方法(即,作为从缓存中删除数据的触发器的方法)。类似于它的兄弟姐妹,@CacheEvict
需要指定一个或多个缓存所影响的行动,允许自定义缓存和关键的决议或指定一个条件,和额外的特性参数(allEntries
)表明一个cache-wide驱逐是否需要执行,而不仅仅是一个条目驱逐(基于)的关键。下面的示例将从books
缓存中删除所有条目:
@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)
- 使用
allEntries
属性从缓存中删除所有条目。
当需要清除整个缓存区域时,这个选项很方便。正如前面的示例所示,所有条目都是在一个操作中删除的,而不是删除每个条目(这将花费很长时间,因为这是低效的)。注意,框架会忽略在这个场景中指定的任何键,因为它不适用(整个缓存会被清除,而不仅仅是一个条目)。
通过使用beforeInvocation
属性,还可以指示应该在方法调用之后(默认情况)还是之前执行收回操作。前者提供了与其他注释相同的语义:一旦方法成功完成,就会在缓存上运行一个操作(在本例中是清除操作)。如果方法没有运行(因为它可能被缓存)或者抛出异常,则不会发生驱逐。后者(beforeInvocation=true
)导致总是在调用方法之前执行收回操作。在不需要将驱逐与方法结果绑定的情况下,这是非常有用的。
注意,void
方法可以与@CacheEvict
一起使用——因为这些方法充当触发器,所以返回值被忽略(因为它们不与缓存交互)。@Cacheable
则不是这样,它向缓存中添加数据或更新缓存中的数据,因此需要一个结果。
@Caching注释
例如,有时需要指定多个相同类型的注释(例如@CacheEvict
或@CachePut
),因为不同缓存之间的条件或键表达式是不同的。@Caching
允许在同一方法上使用多个嵌套的@Cacheable
、@CachePut
和@CacheEvict
注释。下面的示例使用了两个@CacheEvict
注释:
@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)
@CacheConfig注释
到目前为止,我们已经看到缓存操作提供了许多自定义选项,并且可以为每个操作设置这些选项。但是,如果一些定制选项应用于类的所有操作,那么它们的配置可能会很繁琐。例如,为类的每个缓存操作指定要使用的缓存名称,可以由单个类级定义代替。这就是@CacheConfig
发挥作用的地方。下面的例子使用@CacheConfig
来设置缓存的名称:
@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {
@Cacheable
public Book findBook(ISBN isbn) {...}
}
- 使用
@CacheConfig
设置缓存的名称。
@CacheConfig
是一个类级注释,它允许共享缓存名称、自定义keyGenerator
、自定义CacheManager
和自定义CacheResolver
。将此注释放在类上并不会开启任何缓存操作。
操作级定制总是覆盖@CacheConfig
上的定制设置。因此,这为每个缓存操作提供了三个级别的自定义:
- 全局配置,可用于
CacheManager
,KeyGenerator
。 - 在类级别,使用
@CacheConfig
。 - 在操作层面。
启用缓存注释
重要的是要注意,虽然声明缓存注释并不会自动触发他们的行为——就像很多东西在Spring,该特性必须声明启用(这意味着如果你怀疑缓存是罪魁祸首,您可以禁用它只通过移除一个配置而不是所有的注释代码行)。
要启用缓存注释,请将注释@EnableCaching
添加到你的@Configuration
类中:
@Configuration
@EnableCaching
public class AppConfig {
}
另外,对于XML配置,你可以使用cache:annotation-driven
元素:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd">
<cache:annotation-driven/>
</beans>
cache:annotation-driven
元素和@EnableCaching
注释都允许指定各种选项,这些选项影响通过AOP将缓存行为添加到应用程序的方式。该配置故意与[@Transactional](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-annotation-driven-settings)
的配置类似。
处理缓存注释的默认建议模式是
proxy
,它允许仅通过代理截取调用。同一个类中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到aspectj
模式。
有关实现
CachingConfigurer
所需的高级定制(使用Java配置)的更多细节,请参阅javadoc。
<cache:annotation-driven/>
只在定义它的同一个应用程序上下文中的bean上查找@Cacheable/@CachePut/@CacheEvict/@Caching
。这意味着,如果您将<cache:annotation-driven/>
放在一个DispatcherServlet
的WebApplicationContext
中,它只检查控制器中的bean,而不是服务中的bean。有关更多信息,请参阅MVC部分。
方法可见性和缓存注释
在使用代理时,应该只将缓存注释应用于具有公共可见性的方法。如果使用这些注释注释受保护的、私有的或包可见的方法,不会引发错误,但是注释的方法不会显示配置的缓存设置。如果需要注释非公共方法,考虑使用AspectJ(请参阅本节的其余部分),因为它会更改字节码本身。
Spring建议只使用
@Cache*
注释具体类(以及具体类的方法),而不是注释接口。当然,您可以在接口(或接口方法)上放置@Cache*
注释,但只有在使用基于接口的代理时,才会像您预期的那样工作。Java注释的事实并不意味着继承接口,如果使用基于类的代理(proxy-target-class = "true"
)或基于编织的切面(mode=“aspectj”
),代理和编织基础设施无法识别缓存设置,对象也没有封装在缓存代理中。
在代理模式(默认)中,只拦截通过代理传入的外部方法调用。这意味着自调用(实际上,目标对象中的方法调用目标对象的另一个方法)不会导致运行时的实际缓存,即使被调用的方法被标记为
@Cacheable
。考虑在这种情况下使用aspectj
模式。此外,代理必须完全初始化才能提供预期的行为,因此您不应该在初始化代码中依赖此特性(即@PostConstruct
)。
使用自定义注释
自定义注释和AspectJ
该特性仅适用于基于代理的方法,但可以通过使用AspectJ进行一点额外的工作来启用。
spring-aspects
模块仅为标准注释定义了一个方面。如果您已经定义了自己的注释,那么还需要为它们定义一个方面。看看AnnotationCacheAspect
的例子。
缓存抽象允许您使用自己的注释来确定触发缓存填充或回收的方法。作为模板机制,这非常方便,因为它消除了重复缓存注释声明的需要,如果指定了键或条件,或者在您的代码库中不允许外部导入(org.springframework
),缓存注释声明特别有用。与原型注释的其他部分类似,您可以使用@Cacheable
、@CachePut
、@CacheEvict
和@CacheConfig
作为元注释(也就是说,可以注释其他注释的注释)。在下面的例子中,我们用自己的自定义注释替换了一个通用的@Cacheable
声明:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}
在前面的示例中,我们定义了自己的
SlowService
注释,它本身用@Cacheable
注释。现在我们可以替换下面的代码:
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
下面的例子展示了我们可以替换前面代码的自定义注释:
@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
尽管@SlowService
不是Spring注释,但容器会在运行时自动获取其声明并理解其含义。注意,正如前面提到的,需要启用注释驱动的行为。
JCache (JSR-107)注释
从4.1版开始,Spring的缓存抽象就完全支持JCache标准注释:@CacheResult
, @CachePut
, @CacheRemove
和@CacheRemoveAll
,以及@CacheDefaults
, @CacheKey
和@CacheValue
。即使不将缓存存储迁移到JSR-107,也可以使用这些注释。内部实现使用Spring的缓存抽象,并提供与规范兼容的默认CacheResolver
和KeyGenerator
实现。换句话说,如果您已经在使用Spring的缓存抽象,那么您可以切换到这些标准注释,而无需更改缓存存储(或相应的配置)。
功能概括
开启JSR-107的支持
声明性xml缓存
配置Cache存储
缓存抽象提供了几个存储集成选项。要使用它们,您需要声明一个适当的CacheManager
(一个控制和管理Cache
实例的实体,可以使用它来检索缓存实例以进行存储)。
基于JDK ConcurrentMap的缓存
基于Ehcache缓存
Caffeine缓存
基于GemFire缓存
JSR-107高速缓存
处理没有后备存储的缓存
有时,在切换环境或测试时,您可能会有缓存声明,而不需要配置实际的支持缓存。由于这是一个无效的配置,因此在运行时抛出一个异常,因为缓存基础设施无法找到合适的存储。在这样的情况下,而不是删除缓存声明(这可能被证明是乏味的),您可以在一个没有缓存的简单的虚拟缓存中连接——也就是说,它会强制每次调用的缓存方法。下面的例子展示了如何做到这一点:
<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
<property name="cacheManagers">
<list>
<ref bean="jdkCache"/>
<ref bean="gemfireCache"/>
</list>
</property>
<property name="fallbackToNoOpCache" value="true"/>
</bean>
前面的CompositeCacheManager
链接了多个CacheManager
实例,并通过fallbackToNoOpCache
标志为所有未被配置的缓存管理器处理的定义添加了一个无操作缓存。也就是说,在jdkCache
或gemfireCache
(在本例前面配置)中找不到的每个缓存定义都由no-op缓存处理,该缓存不存储任何信息,导致每次都调用目标方法。
插入不同的后端缓存
显然,有很多缓存产品可以用作后备存储。要插入它们,您需要提供一个CacheManager
和一个Cache
实现,因为不幸的是,没有可用的标准可以替代。这听起来可能更难,因为在实践中,类往往是简单的适配器,映射存储API之上的缓存抽象框架,正如ehcache
类所做的。大多数CacheManager
类可以使用org.springframework.cache.support
包中的类(例如AbstractCacheManager
,它负责处理样板代码,只留下实际的映射完成)。我们希望,提供与Spring集成的库能够及时填补这个小小的配置空白。
如何设置TTL/TTI/Eviction policy/XXX特性?
直接通过缓存提供程序。缓存抽象是一个抽象,而不是一个缓存实现。您使用的解决方案可能支持其他解决方案不支持的各种数据策略和不同的拓扑(例如,JDK ConcurrentHashMap
——在缓存抽象中公开这些将是无用的,因为没有后备支持)。这样的功能应该直接通过后备缓存(配置时)或通过其本机API来控制。