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"; } }
现在考虑下边这个例子
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); } } }
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可以添加到类上,意味着这个类的所有方法都会被缓存。
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>
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>
请注意,这个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); } }
下边来执行:
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); } }
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); } }
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); }
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 }
@Override @CacheEvict(value = "members", allEntries = true) public void saveMember(final Member member) { // code removed for brevity }
原文:http://www.javacreed.com/caching-made-easy-with-spring/