[火眼速查] Spring 速查指南(三)- SpEL & 缓存

简介

Spring 是一款开源的 J2EE 框架,它有许多项目,为 Java 应用开发提供了一整套的工具,其中最核心的就是 Spring Framework 和 Spring Boot 项目。

文本是一个系列文章的第一篇,下面就这两个项目的核心内容做一些速查整理,同时辅以生产源码,便于理解。

相关文章

Spring 表达式语言(Spring Expression Language, SpEL)

Spring 表达式语言支持在运行时动态查询和操作对象,语法和 Jakarta Expression Language 类似,但提供了更强大的函数调用和字符串模版功能。SpEL 由解析器处理,除非我们需要单独使用,不必关心它的细节,它已经集成在 Spring 多个产品中了。

哪里会用到 SpEL 呢?

  • XML Bean 配置中(如属性 property)
  • Spring 提供的许多注解配置中(如 @Value、@Cacheable 等)
  • 独立使用

独立使用

一个简单的使用例子:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();

表达式中可以简单地调用方法,message 的值就是 "Hello World!"

ExpressionParser parser = new SpelExpressionParser();
// 调用 getBytes()
Expression exp = parser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) exp.getValue();

可以给表达式提供一个根对象,可以方便地获取它的属性。

// 创建根对象 Inventor(name)
Inventor inv = new Inventor("wwtg99");

ExpressionParser parser = new SpelExpressionParser();

Expression exp = parser.parseExpression("name"); // 获取 name 属性
String name = (String) exp.getValue(inv);
// name 的值就是 wwtg99

exp = parser.parseExpression("name == 'wwtg99'");
boolean result = exp.getValue(inv, Boolean.class);
// result 的值为 true

上下文

另外可以提供上下文(EvaluationContext 接口的实例)给解析器,可以提供更强大的能力。

有两个实现:

  • SimpleEvaluationContext:提供有限的 SpEL 功能和配置选项,主要用于数据绑定和属性过滤。
  • StandardEvaluationContext:提供完整的 SpEL 功能和配置选项。

解析器配置

可以使用 SpelParserConfiguration 对象来配置解析器。例如,默认的表达式不能超过 10000 个字符,可以配置 maxExpressionLength 选项。可以通过配置文件 spring.properties 来进行配置,配置项可参考官方文档

XML 中使用

在 Spring 的注解或 XML 配置中使用 SpEL 可以通过 #{表达式} 的语法。

Application Context 中注册的 Bean 可以用它们的 Bean 名字作为预定义变量。例如,获取 systemProperties Bean:

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
	<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

	<!-- 其他属性 -->
</bean>

也可以获取其他 Bean:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
	<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

	<!-- 其他属性 -->
</bean>

<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
	<property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>

	<!-- 其他属性 -->
</bean>

注解中使用

Spring 中的许多注解支持 SpEL 表达式,例如,注入 @Value 属性的值。

public class FieldValueTestBean {

	@Value("#{ systemProperties['user.region'] }")
	private String defaultLocale;

	public void setDefaultLocale(String defaultLocale) {
		this.defaultLocale = defaultLocale;
	}

	public String getDefaultLocale() {
		return this.defaultLocale;
	}
}

在 @Autowired 注解的方法或构造函数中也可使用 @Value 注入参数。

public class SimpleMovieLister {

	private MovieFinder movieFinder;
	private String defaultLocale;

	@Autowired
	public void configure(MovieFinder movieFinder,
			@Value("#{ systemProperties['user.region'] }") String defaultLocale) {
		this.movieFinder = movieFinder;
		this.defaultLocale = defaultLocale;
	}

	// ...
}

表达式语法

字面量

SpEL 支持以下的字面量:

  • 字符串:使用单引号或双引号包裹
  • 数值:整数或浮点数
  • 布尔值:true 或 false
  • 空值:null

属性

使用 . 来获取对象的属性。

int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);
String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context);

首字母的大小写是不敏感的。

数组、列表和字典

数组(Array)、列表(List)和字典(Map)使用方括号 [] 来获取值。

String name = parser.parseExpression("members[0].name").getValue(context, ieee, String.class);
Inventor pupin = parser.parseExpression("officers['president']").getValue(context, Inventor.class);

使用花括号 {} 来创建列表(List)。

List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);
List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);

{} 表示空列表。

使用 {key:value} 来创建字典(Map)。

Map inventorInfo = (Map) parser.parseExpression("{name:'wwtg99',age:30}").getValue(context);
Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'wwtg',last:'99'},dob:{day:10,month:6,year:2000}}").getValue(context);

{:} 表示空字典。

可以使用构造函数创建数组。

int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);
// 初始化数组
int[] numbers2 = (int[]) parser.parseExpression("new int[] {1, 2, 3}").getValue(context);
// 二维数组
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

方法

调用方法和 Java 的语法类似。

// 值为 "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(societyContext, Boolean.class);

运算符

关系运算符

支持 >, >=, <, <=, ==, !=,也支持字符方式:

  • lt (<)
  • gt (>)
  • le (<=)
  • ge (>=)
  • eq (==)
  • ne (!=)

还支持 between(两边包含)、instanceof、和正则表达式 matches 运算。

// true
result = parser.parseExpression("1 between {1, 5}").getValue(Boolean.class);
// true
result = parser.parseExpression("'elephant' between {'aardvark', 'zebra'}").getValue(Boolean.class);
// true
result = parser.parseExpression("123 instanceof T(Integer)").getValue(Boolean.class);
// true
result = parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);
逻辑运算符
  • and (&&)
  • or (||)
  • not (!)
// -- AND --
// false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

// -- OR --
// true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

// -- NOT --
// false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);
字符串操作符
  • +:字符串连接
  • -:单字符的字符串“减法”
  • *:重复
// -- 连接 --
// "hello world"
String helloWorld = parser.parseExpression("'hello' + ' ' + 'world'").getValue(String.class);

// -- 字符“减法” --
// 'a'
char ch = parser.parseExpression("'d' - 3").getValue(char.class);

// -- 重复 --
// "abcabc"
String repeated = parser.parseExpression("'abc' * 2").getValue(String.class);
数学运算符

对于数值支持数学运算符。

  • +:加法
  • -:减法
  • ++:自增
  • –:自减
  • *:乘法
  • /:除法
  • %:取模
  • ^:指数
Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

// -- 加法 --
int two = parser.parseExpression("1 + 1").getValue(int.class);  // 2

// -- 减法 --
int four = parser.parseExpression("1 - -3").getValue(int.class);  // 4
double d = parser.parseExpression("1000.00 - 1e4").getValue(double.class);  // -9000

// -- 自增 --
two = parser.parseExpression("counter++ + 2").getValue(context, inventor, int.class);

// -- 自减 --
int six = parser.parseExpression("counter-- + 4").getValue(context, inventor, int.class);

// -- 乘法 --
six = parser.parseExpression("-2 * -3").getValue(int.class);  // 6
double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(double.class);  // 24.0

// -- 除法 --
int minusTwo = parser.parseExpression("6 / -3").getValue(int.class);  // -2
double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(double.class);  // 1.0

// -- 取模 --
int three = parser.parseExpression("7 % 4").getValue(int.class);  // 3
int oneInt = parser.parseExpression("8 / 5 % 2").getValue(int.class);  // 1

// -- 指数 --
int maxInt = parser.parseExpression("(2^31) - 1").getValue(int.class);  // Integer.MAX_VALUE
int minInt = parser.parseExpression("-2^31").getValue(int.class);  // Integer.MIN_VALUE

// -- 标准的运算符优先级 --
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(int.class);  // -21
赋值运算符

使用 = 来赋值。

String name = parser.parseExpression("name = 'wwtg99'").getValue(context, inventor, String.class);
三元操作符

SpEL 也有类似于 Java 的三元操作符 ? :,判断 ? 为真则 : 否则。

// 值为 falseExp
String falseString = parser.parseExpression("false ? 'trueExp' : 'falseExp'").getValue(String.class);
Elvis 操作符

SpEL 也提供了类似于 Groovy 和 Kotlin 的 Elvis 操作符 ?:,用于值为空时提供默认值。

ExpressionParser parser = new SpelExpressionParser();
String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name);  // 'Unknown'

SpEL 对字符串检查是为 null 或者空字符串,都会取后面的值。

空安全操作符

SpEL 也提供了类似于 Groovy 和 Kotlin 的空安全操作符(?),对于可能为 null 的变量使用空安全操作符获取其属性,可避免空指针异常。

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor inv = new Inventor("wwtg99", "");
tesla.setPlaceOfBirth(new PlaceOfBirth("China"));

// 值为 China
String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);

inv.setPlaceOfBirth(null);

// 值为 null - 不会抛出 NullPointerException
city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class);
运算符重载

数值的加减乘除等运算符可以通过实现 OperatorOverloader 接口来重载,提供给其他类型使用。

例如,实现一个列表相加的运算符:

pubic class ListConcatenation implements OperatorOverloader {

	@Override
	public boolean overridesOperation(Operation operation, Object left, Object right) {
		return (operation == Operation.ADD && left instanceof List && right instanceof List);
	}

	@Override
	public Object operate(Operation operation, Object left, Object right) {
		if (operation == Operation.ADD && left instanceof List list1 && right instanceof List list2) {
			List result = new ArrayList(list1);
			result.addAll(list2);
			return result;
		}
		throw new UnsupportedOperationException("不支持的重载操作符 %s 给 [%s] 和 [%s]".formatted(operation, left, right));
	}
}

然后把 ListConcatenation 注册到 StandardEvaluationContext 上下文中,就可以像这样 {1, 2, 3} + {4, 5} 使用了。

StandardEvaluationContext context = new StandardEvaluationContext();
context.setOperatorOverloader(new ListConcatenation());

// 获得新的 List: [1, 2, 3, 4, 5]
parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List.class);

类型

使用 T() 操作符来表示类型,需要填类的全限定名(java.lang 包下的类可以省略)。

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);
Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);
boolean trueValue = parser.parseExpression(
		"T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
		.getValue(Boolean.class);

也可以使用 new 来实例化类。

Inventor einstein = p.parseExpression(
		"new org.spring.samples.spel.inventor.Inventor('wwtg99', 'China')")
		.getValue(Inventor.class);

变量

变量使用 #variableName 来引用,变量可以通过 EvaluationContext 接口的 setVariable() 方法注入。

Inventor inv = new Inventor("wwtg99", "China");

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "New Name");
// 赋值新名字
parser.parseExpression("name = #newName").getValue(context, inv);
System.out.println(inv.getName());  // "New Name"

有两个特殊的变量 #this#root

#this 总是指向表达式当前对象,#root 则指向根对象,例如根上下文对象。#this 在不同的表达式中不同,而 #root 则总是根对象。

函数

SpEL 支持注册自定义函数,并以 #functionName(…​) 的方式调用。可以使用上下文的 setVariable() 方法或者专用的 registerFunction() 方法注册。

假设有如下工具类方法:

public abstract class StringUtils {

	public static String reverseString(String input) {
		return new StringBuilder(input).reverse().toString();
	}
}

注册并使用

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString", StringUtils.class.getMethod("reverseString", String.class));
// 值为 "olleh"
String helloWorldReversed = parser.parseExpression("#reverseString('hello')").getValue(context, String.class);

Bean 引用

如果上下文配置了 Bean 解析器,则可以使用 @ 引用 Bean。

集合筛选

集合筛选可以帮助我们从一个集合中选择部分满足条件的元素组成新的集合。集合筛选的语法是 .?[selectionExpression],会创建一个新的集合。

List<Inventor> list = (List<Inventor>) parser.parseExpression("members.?[nationality == 'China']").getValue(context);

集合筛选适用于数组或其他实现 java.lang.Iterable 或 java.util.Map 接口的对象。对于数组和 Iterable 对象,筛选的是每个元素,而对于 Map,则筛选的是 Map.Entry,有 key 和 value 属性。

// #map 是一个 Map 对象
Map newMap = parser.parseExpression("#map.?[value < 27]").getValue(Map.class);

集合投影

集合投影允许我们对集合元素进行解析后获取一个新的集合。集合投影的语法是 .![projectionExpression],例如我们要从一个成员对象集合中获取所有成员的出生城市。

List placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]").getValue(contetxt, List.class);

表达式模版

表达式模版可以混合文本和代码解析块,使用 #{} 分隔(可自行定义)。

String randomPhrase = parser.parseExpression(
		"random number is #{T(java.lang.Math).random()}",
		new TemplateParserContext()).getValue(String.class);

// 值为 "random number is 0.7038186818312008"

上面是常用的使用方式,更多详细的语法可查阅官方语言参考

缓存

Spring 提供了一个缓存抽象层,可屏蔽不同缓存实现的差别,对外提供统一的门面。核心的接口是 CacheManager 和 Cache。CacheManager 用来获取 Cache 实例,而 Cache 实例与对应的缓存实现进行交互。

和其他功能类似,要启用缓存,需要先添加 @EnableCaching 注解。

缓存实现

Spring 提供了一系列的缓存实现,包括基于 ConcurrentMap、Gemfire、Redis、Caffeine,JSR-107 标准兼容的缓存系统(如 Ehcache 3.x)等。

如果使用了 SpringBoot,则可以在配置文件中快速配置,基本上的缓存系统都有实现。

spring:
  cache:
    # 指定类型,默认可根据环境自动识别
    type: REDIS
    # caffeine 配置
    caffeine:
      spec:
    # ehcache 配置
    ehcache:
      config:
    # jcache 配置
    jcache:
      config:
      provider:
    # redis 配置
    rediis:

更多配置可以参考官方文档

为方法缓存返回值

Spring 的缓存层可以为方法缓存返回值,对于调用方来说,没有任何变化,但是可以避免多次执行方法。

Spring 提供了几个注解来方便地使用这个功能。

@Cacheable 注解

@Cacheable 用来标记方法的返回值可以被缓存。

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

参数是缓存名称(支持多个),这个方法被调用时,会首先检查缓存键在所有的缓存中是否存在,如果存在则不执行,直接返回缓存值。

缓存键

缓存的键通过一些策略来生成。默认的缓存键生成策略是这样的:

  • 如果方法没有参数,则使用 SimpleKey.EMPTY
  • 如果有一个参数,则使用这个实例
  • 如果有多个参数,则返回一个 SimpleKey 对象包含了所有的参数实例

会根据参数实例的 hashCode() 和 equals() 方法来生成。

有时候需要自定义不同的策略,例如某些参数对返回结果没有影响,需要排除在缓存键之外。可以使用 key 参数,也支持 SpEL 表达式。

// 使用 isbn 实例做键
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

// 使用 isbn 实例的 rawNumber 属性做键
@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 名称。

key 和 keyGenerator 参数是互斥的,同时使用两个参数会抛出异常。

缓存解析器

缓存抽象层使用 CacheResolver 接口来为 CacheManager 解析缓存。默认的缓存解析器为单个 CacheManager 实例工作基本没有问题。如果有多个 CacheManager 实例,则可以通过 cacheManager 参数指定。

@Cacheable(cacheNames="books", cacheManager="anotherCacheManager")
public Book findBook(ISBN isbn) {...}

当然也可以通过 cacheResolver 参数指定 CacheResolver。

cacheManager 和 cacheResolver 参数也是互斥的。

同步锁

在多线程的环境中,如果多个线程同时调用方法,还是可能会被执行多次。这时可以使用 sync 参数加上同步锁,这样当一个线程进入方法时,其他线程会等待。

@Cacheable(cacheNames="foos", sync=true)
public Foo executeExpensiveOperation(String id) {...}

这个特性需要缓存实现的支持,官方的实现都支持了这个参数,但第三方的不一定。

条件缓存

有时候并不是所有调用都需要缓存,可以使用 condition 参数(支持 SpEL 表达式)来判断,如果表达式返回 true 则尝试使用缓存,否则就执行方法。

// 当 name 的长度小于 32 时使用缓存
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)

另一个 unless 参数类似,在方法执行结束后决定是否要缓存结果。

// 只缓存 hardback 为 true 的结果
@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback")
public Book findBook(String name)

缓存也支持 Optional 包装的返回值,会缓存 Optional 包装的对象或 null,SpEL 表达式中的 #result 总是指向被包装的对象,因此需要考虑 null(使用空安全操作符),上面的例子就要改成这样:

@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback")
public Optional<Book> findBook(String name)
SpEL 上下文

SpEL 上下文中支持以下的元数据,SpEL 语法请参考上文。

名称说明例子
方法名被调用的方法名#root.methodName
方法被调用的方法#root.method.name
目标对象实例目标对象的实例#root.target
目标对象类型目标对象的类型#root.targetClass
参数目标对象的参数
缓存对象缓存对象集合#root.caches[0].name
方法参数方法的参数(使用参数名或索引)#iban 或 #a0 或 #p0 或 #p<#arg>
返回值方法的返回值(被缓存的对象)#result

@CachePut 注解

如果方法被用来更新缓存,但不需要获取缓存结果(也就是方法总是会被调用),例如更新数据,则可以使用 @CachePut 注解,它与 @Cacheable 有相同的参数。

@CachePut(cacheNames="book", key="#isbn")
public Book updateBook(ISBN isbn, BookDescriptor descriptor)

不要在同一个方法上同时使用 @Cacheable 和 @CachePut 注解,可能产生意想不到的行为。

@CacheEvict 注解

@CacheEvict 注解用来清除缓存,例如删除数据的方法,需要把对应的缓存也清除。它有一个额外的参数 allEntries,用来清除所有的数据,而不只是缓存键的数据。

@CacheEvict(cacheNames="books", allEntries=true)
public void loadBooks(InputStream batch)

另外一个参数 beforeInvocation,用来指定清除缓存的动作是否在执行方法前执行。默认情况下,先执行方法,再清除缓存,如果执行抛出异常,则不会执行缓存清除。而指定 beforeInvocation=true 先清除缓存则不会有这个问题。

@CacheEvict 标记的方法可以返回 void,因为清除缓存不依赖返回值。

@Caching 注解

有时候需要使用多个缓存注解,例如清除或更新多个缓存,可以使用 @Caching 注解来组合。

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") })
public Book importBooks(String deposit, Date date)

@CacheConfig 注解

@CacheConfig 注解标记在类上,用来做统一的配置。

例如指定所有方法都使用缓存 books:

@CacheConfig("books")
public class BookRepositoryImpl implements BookRepository {

	@Cacheable
	public Book findBook(ISBN isbn) {...}
}

当有多个缓存配置时,它们的优先级(由低到高)如下:

  • 全局配置的,例如 CachingConfigurer
  • 类上 @CacheConfig 配置的
  • 方法注解上配置的

自定义注解

在某些场景可能需要反复配置相同的缓存注解,可以使用上面的缓存注解作为元注解,自定义一个注解来使用。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(cacheNames="books", key="#isbn")
public @interface SlowService {
}

定义这个注解后,下面两种使用方式等价。

@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

直接使用

要直接使用缓存接口,可以注入 CacheManager 实例(参考指南一的注入 Bean),然后获取 Cache 实例。

public class CacheTest {

    @Autowired
    private CacheManager cacheManager;

    public Cache getCache(String name) {
        // 获取缓存实例
        return this.cacheManager.getCache(name);
    }

}

获取 Cache 实例后,就可以对缓存进行操作了,主要的操作有:

  • get:获取缓存
  • put:设置缓存
  • putIfAbsent:如果键不存在则设置缓存
  • evict:清除缓存
  • evictIfPresent:如果键存在则清除缓存

缓存失效时间

Spring 的缓存抽象层不支持缓存失效时间,因为并不是所有的缓存实现都支持。但我们可以通过包装对象来实现缓存失效时间的功能。

public class CachePackage implements Serializable {

    /** 数据 */
    @Getter private final Object data;

    /** 创建时间(毫秒) */
    @Getter private final long createTime;

    /** 存活时间(毫秒) */
    @Getter private final long ttl;

    /**
     * @param data 数据
     * @param ttl 存活时间(毫秒)
     */
    public CachePackage(Object data, long ttl) {
        this.data = data;
        this.createTime = System.currentTimeMillis();
        this.ttl = ttl;
    }

    /** 是否存活 */
    public boolean isAlive() {
        if (this.ttl == 0) {
        return true;
        }
        return System.currentTimeMillis() < this.createTime + this.ttl;
    }
}

然后,包装一下缓存抽象层接口,提供缓存失效时间参数。

public void put(String name, String key, @Nullable Object value, long ttl) {
    Cache cache = getCache(name);
    if (Objects.nonNull(cache)) {
        CachePackage cachePackage = new CachePackage(value, ttl);
        cache.put(key, cachePackage);
    }
}

同时要在获取缓存时判断一下是否失效。

直达源码

(未完待续)

如果觉得有用,请多多支持,点赞收藏吧!

  • 29
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值