spring3.1引入一种新的和简单的方式来缓存结果,在这篇文章中我们我们将看到在项目中怎么使用过spring 缓存来避免执行已经产生结果的重复任务。这篇文章的读者要有基本的spring和依赖注入的知识。
这篇文章被分成三部分。第一部分我们将会看一个只是能够运行的简单的例子;第二部分我们看一下在递归上是如何缓存的。最后一部分我们来看一个现实世界的例子,来看看缓存是如何被运用的;最后一部分我们同样也会看到当缓存过期是是如何清空的。
考虑下边这个类
package com.javacreed.examples.sc.part1;
import org.springframework.stereotype.Component;
@Component
public class Worker {
public String longTask(final long id) {
System.out.printf("Running long task for id: %d...%n", id);
return "Long task for id " + id + " is done";
}
public String shortTask(final long id) {
System.out.printf("Running short task for id: %d...%n", id);
return "Short task for id " + id + " is done";
}
}
这里有一个包含两个方法的简单的spring组件类。一个名为longTask(),传递一个虚构的长任务,第二个名为shortTask(),运行的快一些。这两个方法的输出仅仅取决于这两个方法的输入。因此对于相同的输入我们能得到同样的输出。这非常重要,否则的话我们不能使用缓存。
现在考虑下边这个例子
package com.javacreed.examples.sc.part1;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(final String[] args) {
final String xmlFile = "META-INF/spring/app-context.xml";
try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(xmlFile)) {
final Worker worker = context.getBean(Worker.class);
worker.longTask(1);
worker.longTask(1);
worker.longTask(1);
worker.longTask(2);
worker.longTask(2);
}
}
}
这里我们创建了spring环境并获取了spring中Worker的实例,然后我们调用方法longTask()五次,这将会迅速的产生下边的输出结果:
Running long task for id: 1...
Running long task for id: 1...
Running long task for id: 1...
Running long task for id: 2...
Running long task for id: 2...
注意这个方法longTask()运行了5次,每一次请求运行一次。同样要注意这个方法只接受了2各不同的输入。参数1的时候这个方法调用3次,参数为2的时候调用了2次。因为这个假的任务方法的输出仅仅取决于输入我们可以为下一次的请求缓存这个输出,而不用再次运行这个假的方法。
为了应用缓存,我们需要做下边三件事:
1、标记将要被缓存的这个方法(或者是类)
spring3.1在方法上增加了新的注释来使用缓存。
package com.javacreed.examples.sc.part1;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@Component
public class Worker {
@Cacheable("task")
public String longTask(final long id) {
System.out.printf("Running long task for id: %d...%n", id);
return "Long task for id " + id + " is done";
}
public String shortTask(final long id) {
System.out.printf("Running short task for id: %d...%n", id);
return "Short task for id " + id + " is done";
}
}
通过简单的在方法上添加@Cacheable注解,有相同参数的值的请求会返回缓存的值。spring允许我们使用缓存的值而不写方法来获取这个值。注意这个注解也需要一个值,来作为缓存仓库的名字,我们一会讨论缓存仓库。
注意这个注解@Cacheable可以添加到类上,意味着这个类的所有方法都会被缓存。
2、激活spring缓存
在spring开始缓存我们的值之前,我们需要增加下边的说明:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<context:annotation-config />
<context:component-scan base-package="com.javacreed.examples.sc" />
<!-- Enables the caching through annotations -->
<span style="color:#ff6666;"> <cache:annotation-driven />
</span>
</beans>
有了这个声明,spring将会查找任何标注了cacheable的方法或者是类,并会执行必要的操作来提供缓存
3、配置需要使用的缓存仓库
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
<context:annotation-config />
<context:component-scan base-package="com.javacreed.examples.sc" />
<!-- Enables the caching through annotations -->
<cache:annotation-driven />
<!-- Generic cache manager based on the JDK ConcurrentMap -->
<span style="color:#ff0000;"> <bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="task" />
</set>
</property>
</bean></span>
</beans>
这个缓存仓库是被缓存对象存储的地方 ,spring支持两种仓库,一种是基于JDK的ConcurrentMap,另一种基于ehcache流行的库,也可能增加更多的库。这里我们使用的是JDK的ConcurrentMap作为我们的缓存仓库,仓库间的互相切换时很简单的。我们的对象将会在ConcurrentMap中被缓存,在这个例子中,我们增加了名为task的仓库。我们可以有多个仓库。注意这个仓库的名字和之前注释中仓库的名字是一样的。
请注意,这个JDK的ConcurrentMap类和spring3.1和spring3.2中的类是不一样的,这里我们使用的是spring3.2,在3.1中类的名字如下:
org.springframework.cache.concurrent.ConcurrentCacheFactoryBean
当我们使用缓存的时候,spring将会把我们标记缓存的对象给代理。调用者不会操作对象而是操作代理。
如果我们打印spring返回的类的名字,我们会看到以下的信息
Worker class: com.javacreed.examples.sc.part1.Worker$$EnhancerByCGLIB$$4fa6f80b
注意这个不是我创建的Worker类(com.javacreed.examples.sc.part1.Worker))。事实上,这个类是通过
spring的字节码技术生成的,我们在这里不做讨论。当我们调用Worker的任何方法的时候,我们将会调用代理的方法。这个代理有我们Worker类的实例。它将会推送任何请求给我们的对象并返回响应,如下图。如果这个方法被标记为可以缓存的,代理将会绕开这个请求而返回缓存的内容。如果这个代理没有为这个输入缓存值,它将会执行这个请求,并把响应缓存起来以备将来使用。
如果我们运行我们的main类,我们将会得到以下的输出结果
Running long task for id: 1...
Running long task for id: 2...
这里的假的方法实际上被调用两次,代理返回了其他请求的缓存结果。事实证明,使用springcache非常简单,我们需要做的就是我们之前列出来的东西。下一部分我们可以看一下怎么在递归上使用缓存。
缓存递归方法
package com.javacreed.examples.sc.part2_1;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@Component("fibonacci")
public class Fibonacci {
private int executions = 0;
public int getExecutions() {
return executions;
}
public void resetExecutions() {
this.executions = 0;
}
@Cacheable("fibonacci")
public long valueAt(final long index) {
executions++;
if (index < 2) {
return 1;
}
return valueAt(index - 1) + valueAt(index - 2);
}
}
这个类实现了fibonacci数列并根据index返回了计算的结果。fibonacci数字使用下边的函数做递归计算fib(n) = fib(n-1) + fib(n-2)。注意这个类同样保存了方法被调用的次数。我们可以通过get方法获取这个值。这个fibonacci类实现了重置值为0的方法,方便将来的调用。
下边来执行:
package com.javacreed.examples.sc.part2_1;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(final String[] args) {
final String xmlFile = "META-INF/spring/app-context.xml";
try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(xmlFile)) {
final long start = System.nanoTime();
final Fibonacci sequence = context.getBean("fibonacci", Fibonacci.class);
final long fibNumber = sequence.valueAt(5);
final int executions = sequence.getExecutions();
final long timeTaken = System.nanoTime() - start;
System.out.printf("The 5th Fibonacci number is: %d (%,d executions in %,d NS)%n", fibNumber, executions,
timeTaken);
}
}
}
执行上边的代码,我们得到以下的输出结果:
The 5th Fibonacci number is: 8 (15 executions in 17,762,022 NS)
这个缓存的vauleAt方法被调用了总共15次,这看起来不对,这个vauleAt方法应该被执行6次而不是15次,其他的9次应该使用缓存的值。
哪里出错了呢
在main()方法中,我们通过spring获取Fibonacci 类的实例,继而spring把对象包装到代理中,因此在main()方法中,我们只能访问代理,但是Fibonacci 中的valuesAt方法调用它自己(递归),而不是通过代理调用valueAt()方法,而是直接使用Fibonacci类,因此代理被略过了。这就是为什么我们不在递归层面使用缓存。
注意:如果我们调用sequence.vauleAt()方法(再一次使用相同的值),这个缓存的值将会被作为变量sequence代理的实例返回。
我们怎么解决这个问题呢
为了解决这个问题,我们需要修改Fibonacci 类,传递我们代理的一个引用。
package com.javacreed.examples.sc.part2_2;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@Component("fibonacci2")
public class Fibonacci {
private int executions = 0;
public int getExecutions() {
return executions;
}
public void resetExecutions() {
this.executions = 0;
}
@Cacheable("fibonacci")
public long valueAt(final long index, final Fibonacci callback) {
executions++;
if (index < 2) {
return 1;
}
return callback.valueAt(index - 1, callback) + callback.valueAt(index - 2, callback);
}
}
注意我们的vauleAt()方法有两个参数而不是一个参数,有Fibonacci 类的一个实例,作为callback的引用,所以不是再调用自己的vauleAt方法而是调用callback的vauleAt方法。对于main方法我们做如下修改:
package com.javacreed.examples.sc.part2_2;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(final String[] args) {
final String xmlFile = "META-INF/spring/app-context.xml";
try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(xmlFile)) {
final long start = System.nanoTime();
final Fibonacci sequence = context.getBean("fibonacci2", Fibonacci.class);
final long fibNumber = sequence.valueAt(5, sequence);
final int executions = sequence.getExecutions();
final long timeTaken = System.nanoTime() - start;
System.out.printf("The 5th Fibonacci number is: %d (%,d executions in %,d NS)%n", fibNumber, executions,
timeTaken);
}
}
}
这个将会产生如下的输出结果:
The 5th Fibonacci number is: 8 (6 executions in 18,320,003 NS)
注意:在这个例子中,花费的时间比没有缓存的要长。这是因为缓存增加了一些附加操作,这个例子中缓存的时间减少不足以抵消增加的消耗。如果我们尝试调用更大的Fibonacci 序列,这个优势就能体现出来了。请不要尝试太大的数,因为这将会花费大量的时间去计算。
真实世界的例子
package com.javacreed.examples.sc.part3;
public class Member {
private final int memberId;
private final String memberName;
public Member(final int memberId, final String memberName) {
this.memberId = memberId;
this.memberName = memberName;
}
// Getters removed for brevity
@Override
public String toString() {
return String.format("[%d] %s", memberId, memberName);
}
}
这个简单的类表示了一个只有id和name的类为了简单的表示,成员用如下的方式表示:
1,Albert Attard
2,Mary Borg
3,Tony White
4,Jane Black
下边是service的代码:
package com.javacreed.examples.sc.part3;
public interface MembersService {
Member getMemberWithId(int id);
void saveMember(Member member);
}
这个接口有两个方法一个用来根据id取回成员,另一个用来保存修改。下边的类向MembersService的实现发送了几个请求。注意实现是被缓存的,因此我们使用的是MembersService的代理。
package com.javacreed.examples.sc.part3;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(final String[] args) {
final String xmlFile = "META-INF/spring/app-context.xml";
try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(xmlFile)) {
final MembersService service = context.getBean(MembersService.class);
// Load member with id 1
Member member = service.getMemberWithId(1);
System.out.println(member);
// Load member with id 1 again
member = service.getMemberWithId(1);
System.out.println(member);
// Edit member with id 1
member = new Member(1, "Joe Vella");
service.saveMember(member);
// Load member with id 1 after it was modified
member = service.getMemberWithId(1);
System.out.println(member);
}
}
如果我们执行上边的代码,将会输出下边结果:
Retrieving the member with id: [1] from file: C:\javacreed\spring-cache\members.txt
[1] Albert Attard
[1] Albert Attard
Retrieving the member with id: [1] from file: C:\javacreed\spring-cache\members.txt
[1] Joe Vella
这里我们使用id 1发送了两个获取请求,但是方法实际就被调用了一次,在第二次请求后,缓存的值返回了。然后我们修改相同id的值,因为member已经修改了,缓存无效,因此使用相同的id获取member,我们再次回调用方法,加载这个对象。这个值会一直被存储直到失效。
我们来看一下是怎么被实现的,这个getMemberWithId方法和我们已经看的其他方法很像,被标注了@Cacheable注解
@Override
@Cacheable("members")
public Member getMemberWithId(final int id) {
System.out.printf("Retrieving the member with id: [%d] from file: %s%n", id, dataFile.getAbsolutePath());
// code removed for brevity
}
这个saveMember方法需要让cache无效,为了实现这个,spring提供了另一个名叫@CacheEvict的注解
@Override
@CacheEvict(value = "members", allEntries = true)
public void saveMember(final Member member) {
// code removed for brevity
}
当这个方法被调用的时候,缓存仓库的名字为members的所有member都会被清空(因为注解选项allEntries = true 已经说明)。因此下次再次调用getMemberWithId()方法的时候,读取新的变量,没有这个,这个getMemberWithId()方法,还会返回之前缓存的老值。
原文:http://www.javacreed.com/caching-made-easy-with-spring/