spring使缓存变得容易

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/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring 中,你可以使用本地缓存来提高应用程序的性能和响应速度。Spring 提供了一个抽象层,使得使用本地缓存变得非常简单。 Spring 的本地缓存抽象层基于注解来实现,你可以使用以下注解来声明和配置本地缓存: 1. @EnableCaching:在配置类上添加该注解,启用缓存功能。 2. @Cacheable:将方法的返回缓存起来。当相同的参数传递给被注解的方法时,Spring 会首先检查缓存中是否已经存在相应的结果,如果存在,则直接返回缓存中的值,不再执行方法体。 3. @CachePut:与 @Cacheable 注解类似,但它会每次都执行方法体,并将结果放入缓存中。适用于需要更新缓存内容的场景。 4. @CacheEvict:从缓存中移除指定的数据项。可以在方法执行前或执行后移除指定的缓存项。 5. @CacheConfig:用于在类级别上配置缓存的公共属性,如缓存名称、缓存管理器等。 使用本地缓存的步骤如下: 1. 配置缓存管理器:在 Spring 配置文件中配置一个缓存管理器,比如使用 ConcurrentMapCacheManager 实现。 2. 在需要进行缓存的方法上添加缓存注解:使用 @Cacheable、@CachePut 等注解来声明方法的缓存行为,包括缓存名称、缓存键等。 3. 启用缓存功能:在配置类上添加 @EnableCaching 注解,启用缓存功能。 4. 调用被缓存的方法:当调用被标记为缓存的方法时,Spring 会根据缓存注解的配置来处理方法的执行和缓存操作。 总结来说,Spring 的本地缓存抽象层使得使用本地缓存变得简单和灵活。你可以使用注解来声明和配置缓存行为,通过配置缓存管理器和启用缓存功能,实现对方法结果的缓存和访问。这样可以提高应用程序的性能,并减少对底层资源的访问频率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值