8.5.2 使用@Cacheable执行缓存

8.5.2 使用@Cacheable执行缓存

@Cacheable注解可用于修饰类或修饰方法,

  • 当使用@Cacheable注解修饰类时,用于告诉Spring在类级别上进行缓存,此时程序调用该类的实例的任何方法时都需要缓存,而且共享同一个缓存区;
  • 当使用@Cacheable注解修饰方法时,用于告诉Spring在方法级别上进行缓存,此时只有当程序调用该方法时才需要缓存。

1. 类级别的缓存

使用@Cacheable注解修饰类时,就可控制Spring在类级别进行缓存,这样当程序调用该类的任意方法时,只要传入的参数相同, Spring就会使用缓存。

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\EhCache
└─src\
├─beans.xml
├─ehcache.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─domain\
│ └─User.java
└─service\
├─impl\
│ └─UserServiceImpl.java
└─UserService.java

假设本示例有如下UserServiceImpl组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
@Service("userService")
// 指定将数据放入users缓存区
@Cacheable(value = "users")
public class UserServiceImpl implements UserService
{
public User getUsersByNameAndAge(String name, int age)
{
System.out.println("--正在执行findUsersByNameAndAge()查询方法--");
return new User(name, age);
}
public User getAnotherUser(String name, int age)
{
System.out.println("--正在执行findAnotherUser()查询方法--");
return new User(name, age);
}
}

上面程序中的粗体字代码指定对UserServiceImpl进行类级别的缓存,这样程序调用该类的任意方法时,只要传入的参数相同, Spring就会使用缓存。
此处所指的缓存的意义是:当程序第一次调用该类的实例的某个方法时, Spring缓存机制会将该方法返回的数据放入指定缓存区——就是@Cacheable注解的value属性值所指定的缓存区(注意此处指定将数据放入users缓存区,这就要求前面为缓存管理器配置过名为users的缓存区)。以后程序调用该类的实例的任何方法时,只要传入的参数相同, Spring将不会真正执行该方法,而是直接利用缓存区中的数据
例如如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 第一次调用us对象的方法时会执行该方法,并缓存方法的结果
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
// 第二次调用us对象的方法时直接利用缓存的数据,并不真正执行该方法
User u2 = us.getAnotherUser("孙悟空", 500);
System.out.println(u1 == u2); // 输出true
}
}

上面程序中的两行粗体字代码先后调用了UserServiceImpl的两个不同方法,但由于程序传入的方法参数相同,因此Spring不会真正执行第二次调用的方法,而是直接复用缓存区中的数据。
编译、运行该程序,可以看到如下输出:

1
2
--正在执行findUsersByNameAndAge()查询方法--
true

从上面输出结果可以看出,程序并未真正指定第二次调用getAnotherUser方法。

类级别缓存默认以方法的参数作为key来缓存方法返回的数据

由此可见,类级别的缓存默认以所有方法参数作为key来缓存方法返回的数据——同一个类不管调用哪个方法,只要调用方法时传入的参数相同, Spring都会直接利用缓存区中的数据。

@Cacheable注解的属性

使用@Cacheable时可指定如下属性。

属性 描述
value 必需属性。该属性可指定多个缓存区的名字,用于指定将方法返回值放入指定的缓存区内。
key 通过SpEL表达式显式指定缓存的key
condition 该属性指定一个返回boolean值的SpEL表达式,只有当该表达式返回true时, Spring才会缓存方法返回值
unless 该属性指定一个返回boolean值的spEL表达式,当该表达式返回true时, Spring不缓存方法返回值

@CachePut注解

@Cacheable注解功能类似的还有一个@CachePut注解,@CachePut注解同样会让Spring将方法返回值放入缓存区。与@Cacheable不同的是,@CachePut修饰的方法不会读取缓存区中的数据——这意味着不管缓存区是否已有数据,@CachePut总会告诉Spring要重新执行这些方法,并再次将方法返回值放入缓存区.

@Cacheable的key属性详解

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\key
└─src\
├─beans.xml
├─ehcache.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─domain\
│ └─User.java
└─service\
├─impl\
│ └─UserServiceImpl.java
└─UserService.java

例如,将上面程序中UserServiceImpl的注解改为如下形式

1
2
3
4
5
6
7
8
9
10
@Service("userService")
...
@Cacheable(
key = "#name",
value = "users"
)
public class UserServiceImpl implements UserService
{
...
}

上面的粗体字代码显式指定以name参数作为缓存的key,这样只要调用的方法具有相同的name参数, Spring缓存机制就会生效。使用如下主程序来测试它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 第一次调用us对象的方法时会执行该方法,并缓存方法的结果
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
// 指定使用name作为缓存key,因此主要两次调用方法的name参数相同
// 缓存机制就会生效
User u2 = us.getAnotherUser("孙悟空", 400);
System.out.println(u1 == u2); // 输出true
}
}

上面程序两次调用方法时传入的参数并不完全相同,只有name参数相同,但由于前面使用@Cacheable注解时显式指定了key="#name",这就意味着缓存使用name参数作为缓存的key,因此上面两次调用方法将依然只执行第一次调用,第二次调用将直接使用缓存的数据,不会真正执行该方法。

@Cacheable注解的condition属性unless属性详解

condition属性与unless属性的功能基本相似,但规则恰好相反:

  • condtion指定的条件为true时,Spring缓存机制才会执行缓存;
  • unless指定的条件为true时, Spring缓存机制就不执行缓存。

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
E:\workspace_QingLiangJiJavaEEQiYeYingYongShiZhang5\condition
└─src\
├─beans.xml
├─ehcache.xml
├─lee\
│ └─SpringTest.java
└─org\
└─crazyit\
└─app\
├─domain\
│ └─User.java
└─service\
├─impl\
│ └─UserServiceImpl.java
└─UserService.java

例如,将程序中UserServiceImpl类中的注解改为如下形式

1
2
3
4
5
6
7
...
@Service("userService")
@Cacheable(value = "users" , condition="#age<100")
public class UserServiceImpl implements UserService
{
...
}

上面代码显式指定Spring缓存生效的条件是#age<100,这样只要调用方法时age参数小于, Spring缓存机制就会生效。使用如下主程序来测试它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 调用方法时age参数不小于100,因此不会缓存,
// 所以下面两次方法调用都会真正执行这些方法。
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
User u2 = us.getAnotherUser("孙悟空", 500);
System.out.println(u1 == u2); // 输出false
// 调用方法时age参数小于100,因此会缓存,
// 所以下面第二次方法调用时不会真正执行该方法,而是直接使用缓存数据。
User u3 = us.getUsersByNameAndAge("孙悟空", 50);
User u4 = us.getAnotherUser("孙悟空", 50);
System.out.println(u3 == u4); // 输出true
}
}

上面程序中前两行代码调用方法时age参数大于100,因此前两行代码不会使用缓存。但程序后面两行代码调用方法时age参数小于100,因此后面两行代码会使用缓存。编译、运行该示例,可以看到如下输出:

1
2
3
4
5
--正在执行findUsersByNameAndAge()查询方法--
--正在执行findAnotherUser()查询方法--
false
--正在执行findUsersByNameAndAge()查询方法--
true

2. 方法级别的缓存

使用@Cacheable修饰方法时,就可控制Spring在方法级别进行缓存,这样当程序调用该方法时,只要传入的参数相同, Spring就会使用缓存

例如,将前面的UserDaoImpl改为如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
@Service("userService")
public class UserServiceImpl implements UserService
{
@Cacheable(value = "users1")
public User getUsersByNameAndAge(String name, int age)
{
System.out.println("--正在执行findUsersByNameAndAge()查询方法--");
return new User(name, age);
}
@Cacheable(value = "users2")
public User getAnotherUser(String name, int age)
{
System.out.println("--正在执行findAnotherUser()查询方法--");
return new User(name, age);
}
}

上面两行粗体字代码指定getUsersByNameAndAgegetAnotherUser这两个方法分别使用不同的缓存区,这意味着这两个方法都会缓存,但由于它们使用了不同的缓存区,因此它们不能共享缓存数据。
上面程序需要分别使用users1users2两个缓存区,因此还需要在ehcache.xml文件中配置这两个缓存区,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="gbk"?>
<ehcache>
<diskStore path="java.io.tmpdir" />
<!-- 配置默认的缓存区 -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU"/>
<!-- 配置名为users1的缓存区 -->
<cache name="users1"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600" />
<cache name="users2"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="300"
timeToLiveSeconds="600" />
</ehcache>

使用如下主程序来测试它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
public class SpringTest
{
public static void main(String[] args)
{
@SuppressWarnings("resource")
ApplicationContext ctx = new ClassPathXmlApplicationContext(
"beans.xml");
UserService us = ctx.getBean("userService", UserService.class);
// 第一次调用us对象的方法时会执行该方法,并缓存方法的结果
//代码1
User u1 = us.getUsersByNameAndAge("孙悟空", 500);
// 由于getAnotherUser()方法使用另一个缓存区,
// 因此无法使用getUsersByNameAndAge()方法缓存区的数据。
//代码2
User u2 = us.getAnotherUser("孙悟空", 500);
System.out.println(u1 == u2); // 输出false
// getAnotherUser("孙悟空", 500)已经执行过一次,故下面代码使用缓存
//代码3
User u3 = us.getAnotherUser("孙悟空", 500);
System.out.println(u2 == u3); // 输出true
}
}

上面程序中的代码1和代码2分别调用了不同的方法,由于这两个方法分别使用不同的缓存区,因此它们不能共享缓存,所以代码2也需要真正执行。代码3与代码2调用的是同一个方法,而且方法参数相同,因此代码3会直接使用缓存中保存的方法返回值.
运行上面程序,将看到如下输出:

1
2
3
4
--正在执行findUsersByNameAndAge()查询方法--
--正在执行findAnotherUser()查询方法--
false
true