• Spring之环境变量配置


    本地配置实现原理

    项目启动时准备环境

    1. public class SpringApplication {
    2. public ConfigurableApplicationContext run(String... args) {
    3. long startTime = System.nanoTime();
    4. DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
    5. ConfigurableApplicationContext context = null;
    6. this.configureHeadlessProperty();
    7. SpringApplicationRunListeners listeners = this.getRunListeners(args);
    8. listeners.starting(bootstrapContext, this.mainApplicationClass);
    9. try {
    10. ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    11. // 获取环境变量和配置数据
    12. ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
    13. this.configureIgnoreBeanInfo(environment);
    14. Banner printedBanner = this.printBanner(environment);
    15. context = this.createApplicationContext();
    16. context.setApplicationStartup(this.applicationStartup);
    17. this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
    18. this.refreshContext(context);
    19. this.afterRefresh(context, applicationArguments);
    20. Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
    21. if (this.logStartupInfo) {
    22. (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
    23. }
    24. listeners.started(context, timeTakenToStartup);
    25. this.callRunners(context, applicationArguments);
    26. } catch (Throwable var12) {
    27. this.handleRunFailure(context, var12, listeners);
    28. throw new IllegalStateException(var12);
    29. }
    30. try {
    31. Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
    32. listeners.ready(context, timeTakenToReady);
    33. return context;
    34. } catch (Throwable var11) {
    35. this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
    36. throw new IllegalStateException(var11);
    37. }
    38. }
    39. }

    获取系统环境变量和配置数据,加载为Environment

    1. private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
    2. ConfigurableEnvironment environment = this.getOrCreateEnvironment();
    3. this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
    4. ConfigurationPropertySources.attach((Environment)environment);
    5. listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment);
    6. DefaultPropertiesPropertySource.moveToEnd((ConfigurableEnvironment)environment);
    7. Assert.state(!((ConfigurableEnvironment)environment).containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");
    8. this.bindToSpringApplication((ConfigurableEnvironment)environment);
    9. if (!this.isCustomEnvironment) {
    10. environment = this.convertEnvironment((ConfigurableEnvironment)environment);
    11. }
    12. ConfigurationPropertySources.attach((Environment)environment);
    13. return (ConfigurableEnvironment)environment;
    14. }

    listeners.environmentPrepared(bootstrapContext, (ConfigurableEnvironment)environment);

    1. public interface PropertySourceLoader {
    2. String[] getFileExtensions();
    3. List> load(String name, Resource resource) throws IOException;
    4. }

    spring在5个路径下(classpath/*, classpath/config/*, /*, /config/*, /config/*/*)找application.properties和application.yml配置文件。

    注:默认先读取application.properties文件,如果application.properties和application.yml中有相同的属性,那么以application.properties中的值为准,一旦加载到对应的属性,后面就会跳过,不会覆盖前面加载的属性值

    针对application.yml,实现类是YamlPropertySourceLoader

    1. public class YamlPropertySourceLoader implements PropertySourceLoader {
    2. public YamlPropertySourceLoader() {}
    3. public String[] getFileExtensions() {
    4. return new String[]{"yml", "yaml"};
    5. }
    6. public List> load(String name, Resource resource) throws IOException {
    7. if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", this.getClass().getClassLoader())) {
    8. throw new IllegalStateException("Attempted to load " + name + " but snakeyaml was not found on the classpath");
    9. } else {
    10. List> loaded = (new OriginTrackedYamlLoader(resource)).load();
    11. if (loaded.isEmpty()) {
    12. return Collections.emptyList();
    13. } else {
    14. List> propertySources = new ArrayList(loaded.size());
    15. for(int i = 0; i < loaded.size(); ++i) {
    16. String documentNumber = loaded.size() != 1 ? " (document #" + i + ")" : "";
    17. propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber, Collections.unmodifiableMap((Map)loaded.get(i)), true));
    18. }
    19. return propertySources;
    20. }
    21. }
    22. }
    23. }

    针对application.properties,实现类是PropertiesPropertySourceLoader

    1. public class PropertiesPropertySourceLoader implements PropertySourceLoader {
    2. private static final String XML_FILE_EXTENSION = ".xml";
    3. public PropertiesPropertySourceLoader() {}
    4. public String[] getFileExtensions() {
    5. return new String[]{"properties", "xml"};
    6. }
    7. public List> load(String name, Resource resource) throws IOException {
    8. List> properties = this.loadProperties(resource);
    9. if (properties.isEmpty()) {
    10. return Collections.emptyList();
    11. } else {
    12. List> propertySources = new ArrayList(properties.size());
    13. for(int i = 0; i < properties.size(); ++i) {
    14. String documentNumber = properties.size() != 1 ? " (document #" + i + ")" : "";
    15. propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber, Collections.unmodifiableMap((Map)properties.get(i)), true));
    16. }
    17. return propertySources;
    18. }
    19. }
    20. private List> loadProperties(Resource resource) throws IOException {
    21. String filename = resource.getFilename();
    22. List> result = new ArrayList();
    23. if (filename != null && filename.endsWith(".xml")) {
    24. result.add(PropertiesLoaderUtils.loadProperties(resource));
    25. } else {
    26. List documents = (new OriginTrackedPropertiesLoader(resource)).load();
    27. documents.forEach((document) -> {
    28. result.add(document.asMap());
    29. });
    30. }
    31. return result;
    32. }
    33. }
    1. public final class ConfigurationPropertySources {
    2. private static final String ATTACHED_PROPERTY_SOURCE_NAME = "configurationProperties";
    3. public static void attach(Environment environment) {
    4. Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
    5. MutablePropertySources sources = ((ConfigurableEnvironment)environment).getPropertySources();
    6. PropertySource attached = getAttached(sources);
    7. if (attached == null || !isUsingSources((PropertySource)attached, sources)) {
    8. attached = new ConfigurationPropertySourcesPropertySource("configurationProperties", new SpringConfigurationPropertySources(sources));
    9. }
    10. sources.remove("configurationProperties");
    11. // 将加载了配置文件的PropertySource添加到Environment中
    12. sources.addFirst((PropertySource)attached);
    13. }
    14. }

    刷新上下文容器

    1. public class ServletWebServerApplicationContext extends GenericWebApplicationContext implements ConfigurableWebServerApplicationContext {
    2. private void refreshContext(ConfigurableApplicationContext context) {
    3. if (this.registerShutdownHook) {
    4. shutdownHook.registerApplicationContext(context);
    5. }
    6. this.refresh(context);
    7. }
    8. protected void refresh(ConfigurableApplicationContext applicationContext) {
    9. applicationContext.refresh();
    10. }
    11. }
    1. public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
    2. public void refresh() throws BeansException, IllegalStateException {
    3. synchronized(this.startupShutdownMonitor) {
    4. StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
    5. this.prepareRefresh();
    6. ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
    7. this.prepareBeanFactory(beanFactory);
    8. try {
    9. this.postProcessBeanFactory(beanFactory);
    10. StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
    11. this.invokeBeanFactoryPostProcessors(beanFactory);
    12. this.registerBeanPostProcessors(beanFactory);
    13. beanPostProcess.end();
    14. this.initMessageSource();
    15. this.initApplicationEventMulticaster();
    16. this.onRefresh();
    17. this.registerListeners();
    18. this.finishBeanFactoryInitialization(beanFactory);
    19. this.finishRefresh();
    20. } catch (BeansException var10) {
    21. if (this.logger.isWarnEnabled()) {
    22. this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
    23. }
    24. this.destroyBeans();
    25. this.cancelRefresh(var10);
    26. throw var10;
    27. } finally {
    28. this.resetCommonCaches();
    29. contextRefresh.end();
    30. }
    31. }
    32. }
    33. }

    注册BeanPostProcessors,通过BeanPostProcessor修改bean的属性值

    1. public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
    2. protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    3. PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
    4. }
    5. }
    1. public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    2. public Object getBean(String name) throws BeansException {
    3. return this.doGetBean(name, (Class)null, (Object[])null, false);
    4. }
    5. protected T doGetBean(String name, @Nullable Class requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
    6. if (mbd.isSingleton()) {
    7. sharedInstance = this.getSingleton(beanName, () -> {
    8. try {
    9. return this.createBean(beanName, mbd, args);
    10. } catch (BeansException var5) {
    11. this.destroySingleton(beanName);
    12. throw var5;
    13. }
    14. });
    15. beanInstance = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
    16. }
    17. }
    18. }
    1. public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
    2. protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
    3. beanInstance = this.doCreateBean(beanName, mbdToUse, args);
    4. return beanInstance;
    5. }
    6. protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) throws BeanCreationException {
    7. this.populateBean(beanName, mbd, instanceWrapper);
    8. }
    9. protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
    10. PropertyValues pvsToUse;
    11. for(Iterator var9 = this.getBeanPostProcessorCache().instantiationAware.iterator(); var9.hasNext(); pvs = pvsToUse) {
    12. InstantiationAwareBeanPostProcessor bp = (InstantiationAwareBeanPostProcessor)var9.next();
    13. pvsToUse = bp.postProcessProperties((PropertyValues)pvs, bw.getWrappedInstance(), beanName);
    14. if (pvsToUse == null) {
    15. if (filteredPds == null) {
    16. filteredPds = this.filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
    17. }
    18. pvsToUse = bp.postProcessPropertyValues((PropertyValues)pvs, filteredPds, bw.getWrappedInstance(), beanName);
    19. if (pvsToUse == null) {
    20. return;
    21. }
    22. }
    23. }
    24. }
    25. }

    通过BeanPostProcessor注入bean的属性

    1. public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
    2. public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    3. InjectionMetadata metadata = this.findAutowiringMetadata(beanName, bean.getClass(), pvs);
    4. try {
    5. metadata.inject(bean, beanName, pvs);
    6. return pvs;
    7. } catch (BeanCreationException var6) {
    8. throw var6;
    9. } catch (Throwable var7) {
    10. throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", var7);
    11. }
    12. }
    13. }
    1. public class InjectionMetadata {
    2. public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    3. Collection checkedElements = this.checkedElements;
    4. Collection elementsToIterate = checkedElements != null ? checkedElements : this.injectedElements;
    5. if (!((Collection)elementsToIterate).isEmpty()) {
    6. Iterator var6 = ((Collection)elementsToIterate).iterator();
    7. while(var6.hasNext()) {
    8. InjectionMetadata.InjectedElement element = (InjectionMetadata.InjectedElement)var6.next();
    9. element.inject(target, beanName, pvs);
    10. }
    11. }
    12. }
    13. }

    AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(bean, beanName, pvs) -> resolveFieldValue(field, bean, beanName) ->

    bean属性注入

    1. protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    2. Field field = (Field)this.member;
    3. Object value;
    4. if (this.cached) {
    5. try {
    6. value = AutowiredAnnotationBeanPostProcessor.this.resolvedCachedArgument(beanName, this.cachedFieldValue);
    7. } catch (NoSuchBeanDefinitionException var7) {
    8. value = this.resolveFieldValue(field, bean, beanName);
    9. }
    10. } else {
    11. // 获取字段@Value对应的值
    12. value = this.resolveFieldValue(field, bean, beanName);
    13. }
    14. if (value != null) {
    15. ReflectionUtils.makeAccessible(field);
    16. // 通过反射给目标字段赋值
    17. field.set(bean, value);
    18. }
    19. }

    获取字段的值

    1. @Nullable
    2. private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) {
    3. DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
    4. desc.setContainingClass(bean.getClass());
    5. Set autowiredBeanNames = new LinkedHashSet(1);
    6. Assert.state(AutowiredAnnotationBeanPostProcessor.this.beanFactory != null, "No BeanFactory available");
    7. TypeConverter typeConverter = AutowiredAnnotationBeanPostProcessor.this.beanFactory.getTypeConverter();
    8. Object value;
    9. try {
    10. // 获取字段依赖的属性值
    11. value = AutowiredAnnotationBeanPostProcessor.this.beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
    12. } catch (BeansException var12) {
    13. throw new UnsatisfiedDependencyException((String)null, beanName, new InjectionPoint(field), var12);
    14. }
    15. synchronized(this) {
    16. if (!this.cached) {
    17. Object cachedFieldValue = null;
    18. if (value != null || this.required) {
    19. cachedFieldValue = desc;
    20. AutowiredAnnotationBeanPostProcessor.this.registerDependentBeans(beanName, autowiredBeanNames);
    21. if (autowiredBeanNames.size() == 1) {
    22. String autowiredBeanName = (String)autowiredBeanNames.iterator().next();
    23. if (AutowiredAnnotationBeanPostProcessor.this.beanFactory.containsBean(autowiredBeanName) && AutowiredAnnotationBeanPostProcessor.this.beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
    24. cachedFieldValue = new AutowiredAnnotationBeanPostProcessor.ShortcutDependencyDescriptor(desc, autowiredBeanName, field.getType());
    25. }
    26. }
    27. }
    28. this.cachedFieldValue = cachedFieldValue;
    29. this.cached = true;
    30. }
    31. return value;
    32. }
    33. }

    获取注入依赖

    1. public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
    2. @Nullable
    3. public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    4. descriptor.initParameterNameDiscovery(this.getParameterNameDiscoverer());
    5. if (Optional.class == descriptor.getDependencyType()) {
    6. return this.createOptionalDependency(descriptor, requestingBeanName);
    7. } else if (ObjectFactory.class != descriptor.getDependencyType() && ObjectProvider.class != descriptor.getDependencyType()) {
    8. if (javaxInjectProviderClass == descriptor.getDependencyType()) {
    9. return (new DefaultListableBeanFactory.Jsr330Factory()).createDependencyProvider(descriptor, requestingBeanName);
    10. } else {
    11. Object result = this.getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(descriptor, requestingBeanName);
    12. if (result == null) {
    13. result = this.doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
    14. }
    15. return result;
    16. }
    17. } else {
    18. return new DefaultListableBeanFactory.DependencyObjectProvider(descriptor, requestingBeanName);
    19. }
    20. }
    21. }
    1. @Nullable
    2. public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
    3. InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
    4. Object var23;
    5. try {
    6. Object shortcut = descriptor.resolveShortcut(this);
    7. if (shortcut != null) {
    8. Object var20 = shortcut;
    9. return var20;
    10. }
    11. Class type = descriptor.getDependencyType();
    12. Object value = this.getAutowireCandidateResolver().getSuggestedValue(descriptor);
    13. Object var11;
    14. if (value == null) {
    15. Object multipleBeans = this.resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
    16. if (multipleBeans != null) {
    17. var23 = multipleBeans;
    18. return var23;
    19. }
    20. Map matchingBeans = this.findAutowireCandidates(beanName, type, descriptor);
    21. if (matchingBeans.isEmpty()) {
    22. if (this.isRequired(descriptor)) {
    23. this.raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
    24. }
    25. var11 = null;
    26. return var11;
    27. }
    28. Object instanceCandidate;
    29. Object result;
    30. String autowiredBeanName;
    31. if (matchingBeans.size() > 1) {
    32. autowiredBeanName = this.determineAutowireCandidate(matchingBeans, descriptor);
    33. if (autowiredBeanName == null) {
    34. if (!this.isRequired(descriptor) && this.indicatesMultipleBeans(type)) {
    35. result = null;
    36. return result;
    37. }
    38. result = descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
    39. return result;
    40. }
    41. instanceCandidate = matchingBeans.get(autowiredBeanName);
    42. } else {
    43. Entry entry = (Entry)matchingBeans.entrySet().iterator().next();
    44. autowiredBeanName = (String)entry.getKey();
    45. instanceCandidate = entry.getValue();
    46. }
    47. if (autowiredBeanNames != null) {
    48. autowiredBeanNames.add(autowiredBeanName);
    49. }
    50. if (instanceCandidate instanceof Class) {
    51. instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
    52. }
    53. result = instanceCandidate;
    54. if (instanceCandidate instanceof NullBean) {
    55. if (this.isRequired(descriptor)) {
    56. this.raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
    57. }
    58. result = null;
    59. }
    60. if (!ClassUtils.isAssignableValue(type, result)) {
    61. throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
    62. }
    63. Object var14 = result;
    64. return var14;
    65. }
    66. if (value instanceof String) {
    67. // 获取bean注入的属性值
    68. String strVal = this.resolveEmbeddedValue((String)value);
    69. BeanDefinition bd = beanName != null && this.containsBean(beanName) ? this.getMergedBeanDefinition(beanName) : null;
    70. value = this.evaluateBeanDefinitionString(strVal, bd);
    71. }
    72. TypeConverter converter = typeConverter != null ? typeConverter : this.getTypeConverter();
    73. try {
    74. var23 = converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
    75. } catch (UnsupportedOperationException var18) {
    76. var11 = descriptor.getField() != null ? converter.convertIfNecessary(value, type, descriptor.getField()) : converter.convertIfNecessary(value, type, descriptor.getMethodParameter());
    77. return var11;
    78. }
    79. } finally {
    80. ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
    81. }
    82. return var23;
    83. }

    AbstractBeanFactory.resolveEmbeddedValue(value)

    根据@Value注解的key获取对应的值

    1. public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
    2. @Nullable
    3. public String resolveEmbeddedValue(@Nullable String value) {
    4. if (value == null) {
    5. return null;
    6. } else {
    7. String result = value;
    8. Iterator var3 = this.embeddedValueResolvers.iterator();
    9. do {
    10. if (!var3.hasNext()) {
    11. return result;
    12. }
    13. StringValueResolver resolver = (StringValueResolver)var3.next();
    14. result = resolver.resolveStringValue(result);
    15. } while(result != null);
    16. return null;
    17. }
    18. }
    19. }

    最后通过反射给目标字段赋值

    1. public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
    2. private class AutowiredFieldElement extends InjectedElement {
    3. protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
    4. Field field = (Field)this.member;
    5. Object value;
    6. if (this.cached) {
    7. try {
    8. value = AutowiredAnnotationBeanPostProcessor.this.resolvedCachedArgument(beanName, this.cachedFieldValue);
    9. } catch (NoSuchBeanDefinitionException var7) {
    10. value = this.resolveFieldValue(field, bean, beanName);
    11. }
    12. } else {
    13. // 获取字段@Value对应的值
    14. value = this.resolveFieldValue(field, bean, beanName);
    15. }
    16. if (value != null) {
    17. ReflectionUtils.makeAccessible(field);
    18. // 通过反射给目标字段赋值
    19. field.set(bean, value);
    20. }
    21. }
    22. }
    23. }

    field.set(bean, value);

    1. public final class Field extends AccessibleObject implements Member {
    2. @CallerSensitive
    3. public void set(Object obj, Object value)
    4. throws IllegalArgumentException, IllegalAccessException
    5. {
    6. if (!override) {
    7. if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
    8. Class caller = Reflection.getCallerClass();
    9. checkAccess(caller, clazz, obj, modifiers);
    10. }
    11. }
    12. getFieldAccessor(obj).set(obj, value);
    13. }
    14. }

  • 相关阅读:
    低代码工具大比拼:哪个最适合你?
    Rust常用特型之From和Into特型
    如何安装 Elasticsearch
    ionic Execution failed for task ‘:processDebugResources‘.
    规则引擎go
    免费分享一套SpringBoot+Vue农产品在线销售(在线商城)管理系统【论文+源码+SQL脚本】,帅呆了~~
    神经网络做预测的原理,神经网络预测空气质量
    线性表顺序存储结构--(Java)
    MySQL主从复制
    Java高性能本地缓存框架Caffeine
  • 原文地址:https://blog.csdn.net/luciferlongxu/article/details/126474919