Spring 缓存注解解析过程
通过 SpringCacheAnnotationParser 的 parseCacheAnnotations 方法解析指定方法或类上的缓存注解,
@Cacheable 注解将被解析为 CacheableOperation 操作,
@CachePut 注解将被解析为 CachePutOperation 操作,
@CacheEvict 注解将被解析为 CacheEvictOperation 操作。
缓存注解
/**
* 启用Spring以注解驱动的缓存管理功能
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {
/**
* 是否启用 CGLIB 类代理,默认是 JDK 动态代理
*/
boolean proxyTargetClass() default false;
/**
* 指示如何应用缓存通知,默认是 JDK Proxy
*/
AdviceMode mode() default AdviceMode.PROXY;
/**
* 当多个 Adviser 将通知织入连接点时,缓存通知的优先级
*/
int order() default Ordered.LOWEST_PRECEDENCE;
}
/**
* 在类级别设置共享的缓存配置信息
* @since 4.1
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
/**
* 默认缓存名称
*/
String[] cacheNames() default {};
/**
* 默认的键生成器 bean 名称
* org.springframework.cache.interceptor.KeyGenerator
*/
String keyGenerator() default "";
/**
* 默认的缓存管理器 bean 名称
* org.springframework.cache.CacheManager
*/
String cacheManager() default "";
/**
* 默认的缓存解析器 bean 名称
* org.springframework.cache.interceptor.CacheResolver
*/
String cacheResolver() default "";
}
/**
* 表明类中单个方法【作用于方法】或所有方法的返回值【作用于类】能被缓存
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
/**
* cacheNames 别名
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 关联缓存名称数组
* @since 4.2
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 动态计算缓存键的 SpEL 表达式
*/
String key() default "";
/**
* 缓存键生成器 bean 名称
*/
String keyGenerator() default "";
/**
* 缓存管理器 bean 名称
*/
String cacheManager() default "";
/**
* 缓存解析器 bean 名称
*/
String cacheResolver() default "";
/**
* 缓存操作的生效条件【SpEL 表达式】,不指定默认生效
*/
String condition() default "";
/**
* 否决方法缓存的条件【SpEL 表达式】,默认不匹配【返回 true 表示不缓存】
*/
String unless() default "";
/**
* 多线程调用方法时,是否执行同步调用
* <ol>
* <li>{@link #unless()} is not supported</li>
* <li>Only one cache may be specified</li>
* <li>No other cache-related operation can be combined</li>
* </ol>
* @since 4.3
*/
boolean sync() default false;
}
/**
* 指定的方法调用或类中所有方法调用,需要执行 CachePut 操作将结果值进行缓存
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CachePut {
/**
* cacheNames 别名
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 关联缓存名称数组
* @since 4.2
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 动态计算缓存键的 SpEL 表达式
*/
String key() default "";
/**
* 缓存键生成器 bean 名称
*/
String keyGenerator() default "";
/**
* 缓存管理器 bean 名称
*/
String cacheManager() default "";
/**
* 缓存解析器 bean 名称
*/
String cacheResolver() default "";
/**
* 缓存操作的生效条件【SpEL 表达式】,不指定默认生效
*/
String condition() default "";
/**
* 否决方法缓存的条件【SpEL 表达式】,默认不匹配【返回 true 表示不缓存】
* @since 3.2
*/
String unless() default "";
}
/**
* 指定的方法调用或所有方法调用,需要执行一个缓存清除操作。
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CacheEvict {
/**
* cacheNames 别名
*/
@AliasFor("cacheNames")
String[] value() default {};
/**
* 关联缓存名称数组
* @since 4.2
*/
@AliasFor("value")
String[] cacheNames() default {};
/**
* 动态计算缓存键的 SpEL 表达式
*/
String key() default "";
/**
* 缓存键生成器 bean 名称
*/
String keyGenerator() default "";
/**
* 缓存管理器 bean 名称
*/
String cacheManager() default "";
/**
* 缓存解析器 bean 名称
*/
String cacheResolver() default "";
/**
* 缓存操作的生效条件【SpEL 表达式】,不指定默认生效
*/
String condition() default "";
/**
* 是否删除缓存中的所有条目,默认只删除缓存键关联的条目
*/
boolean allEntries() default false;
/**
* 缓存清除操作是否需要在方法调用前执行
*/
boolean beforeInvocation() default false;
}
缓存注解解析器
/**
* 用于解析缓存注解的策略接口
*/
public interface CacheAnnotationParser {
/**
* 基于注释类型解析指定类的缓存定义
*/
@Nullable
Collection<CacheOperation> parseCacheAnnotations(Class<?> type);
/**
* 基于注释类型解析指定方法的缓存定义
*/
@Nullable
Collection<CacheOperation> parseCacheAnnotations(Method method);
}
/**
* 用于解析 @Caching、@Cacheable、@CacheEvict、@CachePut 注解的解析器
*/
@SuppressWarnings("serial")
public class SpringCacheAnnotationParser implements CacheAnnotationParser, Serializable {
/**
* 缓存注解操作集合
*/
private static final Set<Class<? extends Annotation>> CACHE_OPERATION_ANNOTATIONS = new LinkedHashSet<>(8);
static {
CACHE_OPERATION_ANNOTATIONS.add(Cacheable.class);
CACHE_OPERATION_ANNOTATIONS.add(CacheEvict.class);
CACHE_OPERATION_ANNOTATIONS.add(CachePut.class);
CACHE_OPERATION_ANNOTATIONS.add(Caching.class);
}
/**
* 解析指定类上的缓存注解
*/
@Override
@Nullable
public Collection<CacheOperation> parseCacheAnnotations(Class<?> type) {
// 创建默认的缓存配置
final DefaultCacheConfig defaultConfig = new DefaultCacheConfig(type);
return parseCacheAnnotations(defaultConfig, type);
}
/**
* 解析指定方法上的缓存注解
*/
@Override
@Nullable
public Collection<CacheOperation> parseCacheAnnotations(Method method) {
// 创建默认的缓存配置
final DefaultCacheConfig defaultConfig = new DefaultCacheConfig(method.getDeclaringClass());
return parseCacheAnnotations(defaultConfig, method);
}
/**
* 基于默认的缓存配置解析缓存注解
*/
@Nullable
private Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
final Collection<CacheOperation> ops = parseCacheAnnotations(cachingConfig, ae, false);
if (ops != null && ops.size() > 1) {
// 如果发现多于 1 个缓存操作,则类中的缓存操作覆盖接口中的缓存操作
final Collection<CacheOperation> localOps = parseCacheAnnotations(cachingConfig, ae, true);
if (localOps != null) {
return localOps;
}
}
return ops;
}
/**
* 解析指定注解元素上的缓存注解
*
* @param cachingConfig 缓存配置
* @param ae 注解元素
* @param localOnly 是否只解析当前类中的缓存注解【缓存注解也可以在接口上使用】
*/
@Nullable
private Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae,
boolean localOnly) {
// 读取所有的缓存注解
final Collection<? extends Annotation> anns = localOnly
? AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS)
: AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS);
if (anns.isEmpty()) {
return null;
}
final Collection<CacheOperation> ops = new ArrayList<>(1);
// 解析 Cacheable 注解
anns.stream().filter(ann -> ann instanceof Cacheable)
.forEach(ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann)));
// 解析 CacheEvict 注解
anns.stream().filter(ann -> ann instanceof CacheEvict)
.forEach(ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann)));
// 解析 CachePut 注解
anns.stream().filter(ann -> ann instanceof CachePut)
.forEach(ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann)));
// 解析 Caching 注解
anns.stream().filter(ann -> ann instanceof Caching)
.forEach(ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops));
return ops;
}
private CacheableOperation parseCacheableAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig,
Cacheable cacheable) {
final CacheableOperation.Builder builder = new CacheableOperation.Builder();
// 操作名称为注解元素的字符串表示
builder.setName(ae.toString());
builder.setCacheNames(cacheable.cacheNames());
builder.setCondition(cacheable.condition());
builder.setUnless(cacheable.unless());
builder.setKey(cacheable.key());
builder.setKeyGenerator(cacheable.keyGenerator());
builder.setCacheManager(cacheable.cacheManager());
builder.setCacheResolver(cacheable.cacheResolver());
builder.setSync(cacheable.sync());
// 尝试写入默认值
defaultConfig.applyDefault(builder);
final CacheableOperation op = builder.build();
/**
* 验证缓存操作
* key 和 keyGenerator 不可同时配置
* cacheManager 和 cacheResolver 不可同时配置
*/
validateCacheOperation(ae, op);
return op;
}
private CacheEvictOperation parseEvictAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig,
CacheEvict cacheEvict) {
final CacheEvictOperation.Builder builder = new CacheEvictOperation.Builder();
builder.setName(ae.toString());
builder.setCacheNames(cacheEvict.cacheNames());
builder.setCondition(cacheEvict.condition());
builder.setKey(cacheEvict.key());
builder.setKeyGenerator(cacheEvict.keyGenerator());
builder.setCacheManager(cacheEvict.cacheManager());
builder.setCacheResolver(cacheEvict.cacheResolver());
builder.setCacheWide(cacheEvict.allEntries());
builder.setBeforeInvocation(cacheEvict.beforeInvocation());
defaultConfig.applyDefault(builder);
final CacheEvictOperation op = builder.build();
validateCacheOperation(ae, op);
return op;
}
private CacheOperation parsePutAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig,
CachePut cachePut) {
final CachePutOperation.Builder builder = new CachePutOperation.Builder();
builder.setName(ae.toString());
builder.setCacheNames(cachePut.cacheNames());
builder.setCondition(cachePut.condition());
builder.setUnless(cachePut.unless());
builder.setKey(cachePut.key());
builder.setKeyGenerator(cachePut.keyGenerator());
builder.setCacheManager(cachePut.cacheManager());
builder.setCacheResolver(cachePut.cacheResolver());
defaultConfig.applyDefault(builder);
final CachePutOperation op = builder.build();
validateCacheOperation(ae, op);
return op;
}
private void parseCachingAnnotation(AnnotatedElement ae, DefaultCacheConfig defaultConfig, Caching caching,
Collection<CacheOperation> ops) {
final Cacheable[] cacheables = caching.cacheable();
for (final Cacheable cacheable : cacheables) {
ops.add(parseCacheableAnnotation(ae, defaultConfig, cacheable));
}
final CacheEvict[] cacheEvicts = caching.evict();
for (final CacheEvict cacheEvict : cacheEvicts) {
ops.add(parseEvictAnnotation(ae, defaultConfig, cacheEvict));
}
final CachePut[] cachePuts = caching.put();
for (final CachePut cachePut : cachePuts) {
ops.add(parsePutAnnotation(ae, defaultConfig, cachePut));
}
}
/**
* Validates the specified {@link CacheOperation}.
* <p>Throws an {@link IllegalStateException} if the state of the operation is
* invalid. As there might be multiple sources for default values, this ensure
* that the operation is in a proper state before being returned.
* @param ae the annotated element of the cache operation
* @param operation the {@link CacheOperation} to validate
*/
private void validateCacheOperation(AnnotatedElement ae, CacheOperation operation) {
if (StringUtils.hasText(operation.getKey()) && StringUtils.hasText(operation.getKeyGenerator())) {
throw new IllegalStateException("Invalid cache annotation configuration on ‘" + ae.toString()
+ "‘. Both ‘key‘ and ‘keyGenerator‘ attributes have been set. "
+ "These attributes are mutually exclusive: either set the SpEL expression used to"
+ "compute the key at runtime or set the name of the KeyGenerator bean to use.");
}
if (StringUtils.hasText(operation.getCacheManager()) && StringUtils.hasText(operation.getCacheResolver())) {
throw new IllegalStateException("Invalid cache annotation configuration on ‘" + ae.toString()
+ "‘. Both ‘cacheManager‘ and ‘cacheResolver‘ attributes have been set. "
+ "These attributes are mutually exclusive: the cache manager is used to configure a"
+ "default cache resolver if none is set. If a cache resolver is set, the cache manager"
+ "won‘t be used.");
}
}
@Override
public boolean equals(Object other) {
return this == other || other instanceof SpringCacheAnnotationParser;
}
@Override
public int hashCode() {
return SpringCacheAnnotationParser.class.hashCode();
}
/**
* 为给定的缓存操作集提供默认配置
*/
private static class DefaultCacheConfig {
/**
* 目标类型
*/
private final Class<?> target;
/**
* 缓存名称
*/
@Nullable
private String[] cacheNames;
/**
* 键生成器名称
*/
@Nullable
private String keyGenerator;
/**
* 缓存管理器名称
*/
@Nullable
private String cacheManager;
/**
* 缓存解析器名称
*/
@Nullable
private String cacheResolver;
/**
* 是否已经初始化
*/
private boolean initialized = false;
public DefaultCacheConfig(Class<?> target) {
this.target = target;
}
/**
* Apply the defaults to the specified {@link CacheOperation.Builder}.
*/
public void applyDefault(CacheOperation.Builder builder) {
if (!initialized) {
// 查找目标类型上的 CacheConfig 注解配置,如果存在则写入默认配置
final CacheConfig annotation = AnnotatedElementUtils.findMergedAnnotation(target, CacheConfig.class);
if (annotation != null) {
cacheNames = annotation.cacheNames();
keyGenerator = annotation.keyGenerator();
cacheManager = annotation.cacheManager();
cacheResolver = annotation.cacheResolver();
}
initialized = true;
}
// 指定注解未设置缓存名称 && 默认配置不为 null && 写入默认配置
if (builder.getCacheNames().isEmpty() && cacheNames != null) {
builder.setCacheNames(cacheNames);
}
// 指定注解未指定键和键生成器 && 默认配置不为 null && 写入键生成器
if (!StringUtils.hasText(builder.getKey()) && !StringUtils.hasText(builder.getKeyGenerator())
&& StringUtils.hasText(keyGenerator)) {
builder.setKeyGenerator(keyGenerator);
}
// 未指定缓存管理器和缓存解析器,则默认的 cacheResolver 优先级高于 cacheManager
if (StringUtils.hasText(builder.getCacheManager()) || StringUtils.hasText(builder.getCacheResolver())) {
// One of these is set so we should not inherit anything
} else if (StringUtils.hasText(cacheResolver)) {
builder.setCacheResolver(cacheResolver);
} else if (StringUtils.hasText(cacheManager)) {
builder.setCacheManager(cacheManager);
}
}
}
}
缓存操作
/**
* 所有缓存操作必须实现的基本接口
*/
public interface BasicOperation {
/**
* 返回与此操作关联的所有缓存名称
*/
Set<String> getCacheNames();
}
/**
* 缓存操作基础类
*/
public abstract class CacheOperation implements BasicOperation {
/**
* 操作名称
*/
private final String name;
/**
* 关联缓存名称
*/
private final Set<String> cacheNames;
/**
* 缓存键
*/
private final String key;
/**
* 缓存键生成器名称
*/
private final String keyGenerator;
/**
* 缓存管理器名称
*/
private final String cacheManager;
/**
* 缓存解析器名称
*/
private final String cacheResolver;
/**
* 缓存条件
*/
private final String condition;
/**
* 操作的字符串表示
*/
private final String toString;
}
/**
* 对应于 @Cacheable 注解的操作
*/
public class CacheableOperation extends CacheOperation {
/**
* 缓存条件
*/
@Nullable
private final String unless;
/**
* 此操作是否是同步的
*/
private final boolean sync;
}
/**
* 对应于 @CacheEvict 注解的操作
*/
public class CacheEvictOperation extends CacheOperation {
/**
* 是否删除缓存中的所有条目
*/
private final boolean cacheWide;
/**
* 是否在目标方法调用之前执行
*/
private final boolean beforeInvocation;
}
/**
* 对应于 @CachePut 注解的操作
*/
public class CachePutOperation extends CacheOperation {
/**
* 缓存条件
*/
@Nullable
private final String unless;
}
缓存操作源
/**
* 被 CacheInterceptor 使用的缓存操作源
*/
public interface CacheOperationSource {
/**
* 获取目标类中指定方法的所有缓存操作
*/
@Nullable
Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass);
}
/**
* 带兜底策略的缓存操作源,缓存注解解析顺序如下
* 1. specific target method;
* 2. target class;
* 3. declaring method;
* 4. declaring class/interface.
*/
public abstract class AbstractFallbackCacheOperationSource implements CacheOperationSource {
/**
* Canonical value held in cache to indicate no caching attribute was
* found for this method and we don‘t need to look again.
*/
private static final Collection<CacheOperation> NULL_CACHING_ATTRIBUTE = Collections.emptyList();
protected final Log logger = LogFactory.getLog(getClass());
/**
* 缓存操作的缓存
*/
private final Map<Object, Collection<CacheOperation>> attributeCache = new ConcurrentHashMap<>(1024);
/**
* 确定此方法调用的缓存属性
*/
@Override
@Nullable
public Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass) {
if (method.getDeclaringClass() == Object.class) {
return null;
}
// 创建缓存键
final Object cacheKey = getCacheKey(method, targetClass);
// 读取操作缓存
final Collection<CacheOperation> cached = attributeCache.get(cacheKey);
// 1)如果存在则直接返回
if (cached != null) {
return cached != NULL_CACHING_ATTRIBUTE ? cached : null;
}
else {
// 2)解析指定方法上的缓存操作并缓存
final Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
if (cacheOps != null) {
if (logger.isTraceEnabled()) {
logger.trace("Adding cacheable method ‘" + method.getName() + "‘ with attribute: " + cacheOps);
}
attributeCache.put(cacheKey, cacheOps);
}
else {
attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
}
return cacheOps;
}
}
/**
* Determine a cache key for the given method and target class.
* <p>Must not produce same key for overloaded methods.
* Must produce same key for different instances of the same method.
* @param method the method (never {@code null})
* @param targetClass the target class (may be {@code null})
* @return the cache key (never {@code null})
*/
protected Object getCacheKey(Method method, @Nullable Class<?> targetClass) {
return new MethodClassKey(method, targetClass);
}
@Nullable
private Collection<CacheOperation> computeCacheOperations(Method method, @Nullable Class<?> targetClass) {
// 是否只解析 public 方法的缓存注解【默认 false】
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
/**
* The method may be on an interface, but we need attributes from the target class.
* If the target class is null, the method will be unchanged.
*/
final Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);
// 1)First try is the method in the target class.
Collection<CacheOperation> opDef = findCacheOperations(specificMethod);
if (opDef != null) {
return opDef;
}
// 2)Second try is the caching operation on the target class.
opDef = findCacheOperations(specificMethod.getDeclaringClass());
if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
return opDef;
}
if (specificMethod != method) {
// Fallback is to look at the original method.
opDef = findCacheOperations(method);
if (opDef != null) {
return opDef;
}
// Last fallback is the class of the original method.
opDef = findCacheOperations(method.getDeclaringClass());
if (opDef != null && ClassUtils.isUserLevelMethod(method)) {
return opDef;
}
}
return null;
}
@Nullable
protected abstract Collection<CacheOperation> findCacheOperations(Class<?> clazz);
@Nullable
protected abstract Collection<CacheOperation> findCacheOperations(Method method);
protected boolean allowPublicMethodsOnly() {
return false;
}
}
@SuppressWarnings("serial")
public class AnnotationCacheOperationSource extends AbstractFallbackCacheOperationSource implements Serializable {
/**
* 是否只解析 public 方法,默认为 true
*/
private final boolean publicMethodsOnly;
/**
* 缓存注解解析器
*/
private final Set<CacheAnnotationParser> annotationParsers;
public AnnotationCacheOperationSource() {
this(true);
}
public AnnotationCacheOperationSource(boolean publicMethodsOnly) {
this.publicMethodsOnly = publicMethodsOnly;
annotationParsers = Collections.singleton(new SpringCacheAnnotationParser());
}
public AnnotationCacheOperationSource(CacheAnnotationParser annotationParser) {
publicMethodsOnly = true;
Assert.notNull(annotationParser, "CacheAnnotationParser must not be null");
annotationParsers = Collections.singleton(annotationParser);
}
public AnnotationCacheOperationSource(CacheAnnotationParser... annotationParsers) {
publicMethodsOnly = true;
Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified");
this.annotationParsers = new LinkedHashSet<>(Arrays.asList(annotationParsers));
}
public AnnotationCacheOperationSource(Set<CacheAnnotationParser> annotationParsers) {
publicMethodsOnly = true;
Assert.notEmpty(annotationParsers, "At least one CacheAnnotationParser needs to be specified");
this.annotationParsers = annotationParsers;
}
@Override
@Nullable
protected Collection<CacheOperation> findCacheOperations(Class<?> clazz) {
return determineCacheOperations(parser -> parser.parseCacheAnnotations(clazz));
}
@Override
@Nullable
protected Collection<CacheOperation> findCacheOperations(Method method) {
return determineCacheOperations(parser -> parser.parseCacheAnnotations(method));
}
/**
* 解析缓存注解并转换为 CacheOperation
*/
@Nullable
protected Collection<CacheOperation> determineCacheOperations(CacheOperationProvider provider) {
Collection<CacheOperation> ops = null;
for (final CacheAnnotationParser annotationParser : annotationParsers) {
final Collection<CacheOperation> annOps = provider.getCacheOperations(annotationParser);
if (annOps != null) {
if (ops == null) {
ops = annOps;
}
else {
final Collection<CacheOperation> combined = new ArrayList<>(ops.size() + annOps.size());
combined.addAll(ops);
combined.addAll(annOps);
ops = combined;
}
}
}
return ops;
}
@Override
protected boolean allowPublicMethodsOnly() {
return publicMethodsOnly;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof AnnotationCacheOperationSource)) {
return false;
}
final AnnotationCacheOperationSource otherCos = (AnnotationCacheOperationSource) other;
return annotationParsers.equals(otherCos.annotationParsers) &&
publicMethodsOnly == otherCos.publicMethodsOnly;
}
@Override
public int hashCode() {
return annotationParsers.hashCode();
}
@FunctionalInterface
protected interface CacheOperationProvider {
/**
* 返回指定缓存解析解析成功的缓存操作
*/
@Nullable
Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser);
}
}
原文地址:https://www.cnblogs.com/zhuxudong/p/10322597.html
时间: 2024-10-12 21:35:04