Spring集成apollo源码分析

本文涉及的产品
可观测可视化 Grafana 版,10个用户账号 1个月
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: Spring集成apollo源码分析

引言

为了在项目中用好框架,以及出现问题时候能够快速定位、分析、优化,文章尝试从源码角度分析Spring集成apollo的过程。期望文章能够把以下几个事情描述清楚:

  • apollo通过使用Spring哪些扩展点,完成了与Spring的集成;
  • apollo中的配置如何融入到Spring Environment;
  • apollo中的配置项如何赋值给Spring Bean相关字段、方法;
  • 在应用运行过程中,当修改apollo中的配置,配置如何在Spring Bean相关字段、方法上生效的

由于Spring Framework和Spring Boot集成apollo的方式有些许不同,分别进行分析。

Spring Framework

示例

Spring XML配置

<?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:apollo="http://www.ctrip.com/schema/apollo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
      http://www.springframework.org/schema/beans/spring-beans.xsd
      http://www.springframework.org/schema/context
      http://www.springframework.org/schema/context/spring-context.xsd
      http://www.ctrip.com/schema/apollo
      http://www.ctrip.com/schema/apollo.xsd"
>
    <apollo:config namespaces="application,public_namespace"/>
    <context:component-scan base-package="test.springframework"/>
    <bean id="test" class="test.springframework.Test">
        <property name="test1" value="${test1}"/>
    </bean>
</beans>

Spring Bean

package test.springframework;
public class Test {
    @ApolloConfig
    private Config config;
    @Value("${test2:}")
    private String test2;
    private String test1;
    public String getTest1() {
        return test1;
    }
    public void setTest1(String test1) {
        this.test1 = test1;
    }
    @ApolloConfigChangeListener
    public void onChange(ConfigChangeEvent changeEvent){
        System.out.println(changeEvent);
    }
}

集成分析

图中概括地描述了refresh过程,其中标识黄颜色的地方是这次分析的重点,下面分别进行描述。

解析apollo:config

当解析xml文件apollo:config标记的时候调用BeanDefinitionParserDelegate.parseCustomElement(…),主要流程如下:

图中流程主要完成了两件事:

  • 将xml中配置的apollo命名空间存储到了PropertySourcesProcessor(属于apollo jar包)类中的NAMESPACE_NAMES字段;
  • 将ConfigPropertySourcesProcessor(属于apollo jar包)作为BeanDefinition注册到了Spring容器中;ConfigPropertySourcesProcessor实现了Spring接口BeanDefinitionRegistryPostProcessor、BeanFactoryPostProcessor。

调用BeanFactoryPostProcessor

apollo中ConfigPropertySourcesProcessor实现了Spring接口BeanDefinitionRegistryPostProcessor、BeanFactoryPostProcessor,ConfigPropertySourcesProcessor实现如下:

public class ConfigPropertySourcesProcessor extends PropertySourcesProcessor
    implements BeanDefinitionRegistryPostProcessor {
  //使用java SPI机制,ConfigPropertySourcesProcessorHelper对应的实现类是DefaultConfigPropertySourcesProcessorHelper,这段代码完成了apollo client初始化
  private ConfigPropertySourcesProcessorHelper helper = ServiceBootstrap.loadPrimary(ConfigPropertySourcesProcessorHelper.class);
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    helper.postProcessBeanDefinitionRegistry(registry);
  }
}

调用BeanDefinitionRegistryPostProcessor.postProcessBeanDefinitionRegistry

实际调用的是DefaultConfigPropertySourcesProcessorHelper.postProcessBeanDefinitionRegistry方法,该方法主要完成以下几件事:

  • 将PropertySourcesPlaceholderConfigurer注册到spring容器中,该类实现了接口BeanFactoryPostProcessor,用于占位符的转换处理;
  • 将ApolloAnnotationProcessor注册到spring容器中,该类实现了接口BeanPostProcessor;
  • 将SpringValueProcessor注册到spring容器中,该类实现了接口BeanFactoryPostProcessor和BeanPostProcessor;
  • 将ApolloJsonValueProcessor注册到spring容器中,该类实现了接口BeanPostProcessor;
  • 将BeanDefinition中带有占位符的所有属性存储到Map<BeanDefinitionRegistry, Multimap<String, SpringValueDefinition>>结构中

调用BeanFactoryPostProcessor.postProcessBeanFactory

PropertySourcesProcessor
CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_PROPERTY_SOURCE_NAME);
ImmutableSortedSet<Integer> orders = ImmutableSortedSet.copyOf(NAMESPACE_NAMES.keySet());
Iterator<Integer> iterator = orders.iterator();
while (iterator.hasNext()) {
    int order = iterator.next();
    for (String namespace : NAMESPACE_NAMES.get(order)) {
        Config config = ConfigService.getConfig(namespace);
        composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
    }
}
NAMESPACE_NAMES.clear();
environment.getPropertySources().addFirst(composite);
  • NAMESPACE_NAMES中存储的是应用配置的apollo命名空间;
  • 通过Config config = ConfigService.getConfig(namespace)获取每个命名空间的配置对象,将Config对象封装成ConfigPropertySource,接着将所有ConfigPropertySource放入CompositePropertySource,最后将CompositePropertySource加入到spring ConfigurableEnvironment中,此时spring容器的ConfigurableEnvironment已经拥有了apollo命令空间的配置;
AutoUpdateConfigChangeListener autoUpdateConfigChangeListener = new AutoUpdateConfigChangeListener(environment, beanFactory);
List<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();
for (ConfigPropertySource configPropertySource : configPropertySources) {
  configPropertySource.addChangeListener(autoUpdateConfigChangeListener);
}
  • 为apollo命名空间对象添加监听AutoUpdateConfigChangeListener,当改变命名空间中配置的时候,该监听完成Bean对象属性值得更新,及方法的调用。
PropertySourcesPlaceholderConfigurer
  • 将ConfigurableEnvironment构造成自身的PropertySource;
new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
  public String getProperty(String key) {
    return this.source.getProperty(key);
  }
}
  • 通过自身的PropertySource构造PropertySourcesPropertyResolver,PropertySourcesPropertyResolver中完成解析的类是PropertyPlaceholderHelper;
new PropertySourcesPropertyResolver(this.propertySources)
  • 构造StringValueResolver,然后调用PropertySourcesPropertyResolver来进行解析;
StringValueResolver valueResolver = strVal -> {
    String resolved = (ignoreUnresolvablePlaceholders ?
                       propertyResolver.resolvePlaceholders(strVal) :
                       propertyResolver.resolveRequiredPlaceholders(strVal));
    if (trimValues) {
        resolved = resolved.trim();
    }
    return (resolved.equals(nullValue) ? null : resolved);
};
  • 构造BeanDefinitionVisitor,用于解析BeanDefinition包含的所有String值(属性、构造方法参数、元数据),解析找到的任何占位符。
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
public void visitBeanDefinition(BeanDefinition beanDefinition) {
  visitParentName(beanDefinition);
  visitBeanClassName(beanDefinition);
  visitFactoryBeanName(beanDefinition);
  visitFactoryMethodName(beanDefinition);
  visitScope(beanDefinition);
  if (beanDefinition.hasPropertyValues()) {
    visitPropertyValues(beanDefinition.getPropertyValues());
  }
  if (beanDefinition.hasConstructorArgumentValues()) {
    ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
    visitIndexedArgumentValues(cas.getIndexedArgumentValues());
    visitGenericArgumentValues(cas.getGenericArgumentValues());
  }
}
@Nullable
protected String resolveStringValue(String strVal) {
  if (this.valueResolver == null) {
    throw new IllegalStateException("No StringValueResolver specified - xxx");
  }
    // 最终调用的是PropertySourcesPropertyResolver中的resolveXXX方法
  String resolvedValue = this.valueResolver.resolveStringValue(strVal);
  return (strVal.equals(resolvedValue) ? strVal : resolvedValue);
}

调用BeanPostProcessor

ApolloAnnotationProcessor

用于解析apollo注解ApolloConfig、ApolloConfigChangeListener。

ApolloConfig

为注解为ApolloConfig的字段赋值对应的namespace Config对象。

protected void processField(Object bean, String beanName, Field field) {
  ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class);
  if (annotation == null) {
    return;
  }
  Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()),
    "Invalid type: %s for field: %s, should be Config", field.getType(), field);
  String namespace = annotation.value();
  Config config = ConfigService.getConfig(namespace);
  ReflectionUtils.makeAccessible(field);
  ReflectionUtils.setField(field, bean, config);
}
ApolloConfigChangeListener

为注解为ApolloConfigChangeListener的方法添加namespace的监听。

ApolloConfigChangeListener annotation = AnnotationUtils
        .findAnnotation(method, ApolloConfigChangeListener.class);
String[] namespaces = annotation.value();
ConfigChangeListener configChangeListener = new ConfigChangeListener() {
  @Override
  public void onChange(ConfigChangeEvent changeEvent) {
  ReflectionUtils.invokeMethod(method, bean, changeEvent);
  }
};
for (String namespace : namespaces) {
  Config config = ConfigService.getConfig(namespace);
  config.addChangeListener(configChangeListener);
}

SpringValueProcessor

  • 将Bean中含有Value注解的字段、方法注册到SpringValueRegistry(Map<BeanFactory, Multimap<String, SpringValue>>)中,SpringValue保存了Bean实例,对应的key,Field或Method;
protected void processField(Object bean, String beanName, Field field) {
  Value value = field.getAnnotation(Value.class);
  Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
  for (String key : keys) {
    SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, field, false);
    springValueRegistry.register(beanFactory, key, springValue);
  }
}
protected void processMethod(Object bean, String beanName, Method method) {
  Value value = method.getAnnotation(Value.class);
  Set<String> keys = placeholderHelper.extractPlaceholderKeys(value.value());
  for (String key : keys) {
    SpringValue springValue = new SpringValue(key, value.value(), bean, beanName, method, false);
    springValueRegistry.register(beanFactory, key, springValue);
  }
}
  • 将DefaultConfigPropertySourcesProcessorHelper中解析的Map<BeanDefinitionRegistry, Multimap<String, SpringValueDefinition>>转换为SpringValue保存在SpringValueRegistry中;
  • 前面在PropertySourcesProcessor类中对每个namespace Config对象注册了监听AutoUpdateConfigChangeListener,AutoUpdateConfigChangeListener类含有SpringValueRegistry的引用;
  • 当namespace Config配置改变的时候会通知到AutoUpdateConfigChangeListener,AutoUpdateConfigChangeListener通过配置key找到对应的SpringValue对象,通过SpringValue改变Bean对应的属性,或调用Bean对应的方法。

ApolloJsonValueProcessor

  • 将Bean中含有ApolloJsonValue注解的字段、方法注册到SpringValueRegistry(Map<BeanFactory, Multimap<String, SpringValue>>)中,SpringValue保存了Bean实例,对应的key,Field或Method;
  • 对于注解在Field的情况,获取ApolloJsonValue key的配置,将配置转换为Field对应类型的对象并完成对Field的赋值;
protected void processField(Object bean, String beanName, Field field) {
  ApolloJsonValue apolloJsonValue = AnnotationUtils.getAnnotation(field, ApolloJsonValue.class);
  String placeholder = apolloJsonValue.value();
  Object propertyValue = placeholderHelper
    .resolvePropertyValue(beanFactory, beanName, placeholder);
  boolean accessible = field.isAccessible();
  field.setAccessible(true);
  ReflectionUtils
    .setField(field, bean, parseJsonValue((String)propertyValue, field.getGenericType()));
  field.setAccessible(accessible);
  if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
    Set<String> keys = placeholderHelper.extractPlaceholderKeys(placeholder);
    for (String key : keys) {
    SpringValue springValue = new SpringValue(key, placeholder, bean, beanName, field, true);
    springValueRegistry.register(beanFactory, key, springValue);
    }
  }
}
  • 对于注解在Method的情况,获取ApolloJsonValue key的配置,将配置转换为Method入参对应类型的对象并完成对Method的调用
protected void processMethod(Object bean, String beanName, Method method) {
  ApolloJsonValue apolloJsonValue = AnnotationUtils.getAnnotation(method, ApolloJsonValue.class);
  String placeHolder = apolloJsonValue.value();
  Object propertyValue = placeholderHelper
    .resolvePropertyValue(beanFactory, beanName, placeHolder);
  boolean accessible = method.isAccessible();
  method.setAccessible(true);
  ReflectionUtils.invokeMethod(method, bean, parseJsonValue((String)propertyValue, types[0]));
  method.setAccessible(accessible);
  if (configUtil.isAutoUpdateInjectedSpringPropertiesEnabled()) {
    Set<String> keys = placeholderHelper.extractPlaceholderKeys(placeHolder);
    for (String key : keys) {
    SpringValue springValue = new SpringValue(key, apolloJsonValue.value(), bean, beanName,method, true);
    springValueRegistry.register(beanFactory, key, springValue);
    }
  }
}

Spring Boot

示例

// Spring Boot启动类
@SpringBootApplication
@EnableApolloConfig
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
//测试Bean见Spring Framework部分示例

上面代码中的@EnableApolloConfig是一个接入点,后面会分析到。

集成分析

图中概括地描述了SpringApplication启动过程(其中会将启动类加入到spring容器中),其中标识黄颜色的地方是这次分析的重点,下面分别进行描述。

加载apollo ApplicationContextInitializer

  • 通过SpringFactoriesLoader加载apollo jar包中META-INF/spring.factories文件,解析ApplicationContextInitializer实现类ApolloApplicationContextInitializer。
setInitializers((Collection) getSpringFactoriesInstances(
        ApplicationContextInitializer.class));
// 通过SpringFactoriesLoader加载、解析META-INF/spring.factories
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
    Class<?>[] parameterTypes, Object... args) {
  ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
  Set<String> names = new LinkedHashSet<>(
      SpringFactoriesLoader.loadFactoryNames(type, classLoader));
  List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
      classLoader, args, names);
  AnnotationAwareOrderComparator.sort(instances);
  return instances;
}

apollo ApplicationContextInitializer initialize

  • ApolloApplicationContextInitializer解析apollo.bootstrap.namespaces配置的命名空间名称,然后通过Config config = ConfigService.getConfig(namespace)获取每个命名空间的配置对象,将Config对象封装成ConfigPropertySource,接着将所有ConfigPropertySource放入CompositePropertySource,最后将CompositePropertySource加入到spring ConfigurableEnvironment中,此时spring容器的ConfigurableEnvironment已经拥有了apollo命令空间的配置。
protected void initialize(ConfigurableEnvironment environment) {
  String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
  List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces);
  CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
  for (String namespace : namespaceList) {
    Config config = ConfigService.getConfig(namespace);
    composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
  }
  environment.getPropertySources().addFirst(composite);
}

初始化BeanDefinitionLoader

我们关注BeanDefinitionLoader类中属性annotatedReader=new AnnotatedBeanDefinitionReader(registry),AnnotatedBeanDefinitionReader构造方法如下:

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
  this.registry = registry;
  this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);
  // 将Annotation相关的处理类注册到Spring容器中,如:
  // ConfigurationClassPostProcessor : 处理Configuration注解
  // AutowiredAnnotationBeanPostProcessor : 处理Value注解
  // RequiredAnnotationBeanPostProcessor : 处理Required注解
  // CommonAnnotationBeanPostProcessor : 处理PostConstruct/PreDestroy/Resource/Lazy注解
  // ... ...
  AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}

与我们这次分析相关的类是:ConfigurationClassPostProcessor,该类实现了BeanDefinitionRegistryPostProcessor、BeanFactoryPostProcessor接口。

ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry

  • 判断spring容器中BeanDefinition是否含有Configuration、Component、ComponentScan、Import

ImportResource、Bean注解,没有则直接返回;

  • 构造ConfigurationClassParser(Parses a Configuration class definition, populating a collection of ConfigurationClass objects (parsing a single Configuration class may result in any number of ConfigurationClass objects because one Configuration class may import another using the Import annotation)),接着对BeanDefinition上的Configuration、Component、ComponentScan、Import

ImportResource、Bean注解进行解析。

  • 解析完成后,通过ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClass)进行apollo相关BeanDefinition的加载:
  • apollo EnableApolloConfig注解的定义如下
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 在上面parse阶段会将Import封装在ConfigurationClass中
@Import(ApolloConfigRegistrar.class)
public @interface EnableApolloConfig {
  String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};
  int order() default Ordered.LOWEST_PRECEDENCE;
}
  • ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars会调用到ApolloConfigRegistrar类的registerBeanDefinitions方法
private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
  registrars.forEach((registrar, metadata) ->
      registrar.registerBeanDefinitions(metadata, this.registry));
}
  • ApolloConfigRegistrar最终将apollo中BeanDefinitionRegistryPostProcessor、BeanFactoryPostProcessor、BeanPostProcessor实现类的注册到spring容器中(具体实现类与Spring Framework部分基本是一致的),后续的逻辑与Spring Framework部分分析的是一致的了。
public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
  private ApolloConfigRegistrarHelper helper = ServiceBootstrap.loadPrimary(ApolloConfigRegistrarHelper.class);
  @Override
  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
  // 该方法中将apollo相关类注册到spring容器中,如:
  // PropertySourcesProcessor
  // ApolloAnnotationProcessor
  // SpringValueProcessor
  // SpringValueDefinitionProcessor
  // ApolloJsonValueProcessor
    helper.registerBeanDefinitions(importingClassMetadata, registry);
  }
}

总结

文章主要介绍了apollo借助Spring扩展点完成了与Spring的集成:

Spring Framework集成方式使用到了loadBeanDefinitions阶段中apollo:config NamespaceHandler,BeanDefinitionRegistryPostProcessor,BeanFactoryPostProcessor,BeanPostProcessor;

Spring Boot集成方式使用到了ApplicationContextInitializer,BeanDefinitionRegistryPostProcessor,BeanFactoryPostProcessor,BeanPostProcessor。

目录
相关文章
|
7天前
|
监控 Java 应用服务中间件
Spring Boot整合Tomcat底层源码分析
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置和起步依赖等特性,大大简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是其与Tomcat的整合。
26 1
|
1月前
|
Java Maven Docker
gitlab-ci 集成 k3s 部署spring boot 应用
gitlab-ci 集成 k3s 部署spring boot 应用
|
3月前
|
资源调度 Java 调度
Spring Cloud Alibaba 集成分布式定时任务调度功能
定时任务在企业应用中至关重要,常用于异步数据处理、自动化运维等场景。在单体应用中,利用Java的`java.util.Timer`或Spring的`@Scheduled`即可轻松实现。然而,进入微服务架构后,任务可能因多节点并发执行而重复。Spring Cloud Alibaba为此发布了Scheduling模块,提供轻量级、高可用的分布式定时任务解决方案,支持防重复执行、分片运行等功能,并可通过`spring-cloud-starter-alibaba-schedulerx`快速集成。用户可选择基于阿里云SchedulerX托管服务或采用本地开源方案(如ShedLock)
126 1
|
23天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
31 1
|
24天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
25 1
|
1月前
|
缓存 JavaScript Java
Spring之FactoryBean的处理底层源码分析
本文介绍了Spring框架中FactoryBean的重要作用及其使用方法。通过一个简单的示例展示了如何通过FactoryBean返回一个User对象,并解释了在调用`getBean()`方法时,传入名称前添加`&`符号会改变返回对象类型的原因。进一步深入源码分析,详细说明了`getBean()`方法内部对FactoryBean的处理逻辑,解释了为何添加`&`符号会导致不同的行为。最后,通过具体代码片段展示了这一过程的关键步骤。
Spring之FactoryBean的处理底层源码分析
|
1月前
|
前端开发 Java 程序员
springboot 学习十五:Spring Boot 优雅的集成Swagger2、Knife4j
这篇文章是关于如何在Spring Boot项目中集成Swagger2和Knife4j来生成和美化API接口文档的详细教程。
102 1
|
20天前
|
前端开发 Java Spring
Spring MVC源码分析之DispatcherServlet#getHandlerAdapter方法
`DispatcherServlet`的 `getHandlerAdapter`方法是Spring MVC处理请求的核心部分之一。它通过遍历预定义的 `HandlerAdapter`列表,找到适用于当前处理器的适配器,并调用适配器执行具体的处理逻辑。理解这个方法有助于深入了解Spring MVC的工作机制和扩展点。
22 0
|
1月前
|
存储 前端开发 Java
Spring Boot 集成 MinIO 与 KKFile 实现文件预览功能
本文详细介绍如何在Spring Boot项目中集成MinIO对象存储系统与KKFileView文件预览工具,实现文件上传及在线预览功能。首先搭建MinIO服务器,并在Spring Boot中配置MinIO SDK进行文件管理;接着通过KKFileView提供文件预览服务,最终实现文档管理系统的高效文件处理能力。
275 11
|
1月前
|
Java Spring
springboot 学习十一:Spring Boot 优雅的集成 Lombok
这篇文章是关于如何在Spring Boot项目中集成Lombok,以简化JavaBean的编写,避免冗余代码,并提供了相关的配置步骤和常用注解的介绍。
98 0
下一篇
无影云桌面