首页 > 其他分享 >springboot启动读取配置文件过程&自定义配置文件处理器

springboot启动读取配置文件过程&自定义配置文件处理器

时间:2022-12-10 18:35:16浏览次数:51  
标签:配置文件 自定义 springframework environment context org new config springboot

    最近看到看到spring的配置文件放在了resources/config/application.yal 文件内部,第一次见。就想的研究下,springboot启动读取配置文件的过程。

1. 启动过程

  1. org.springframework.boot.SpringApplication#run(java.lang.Class<?>[], java.lang.String[])
    public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return (new SpringApplication(primarySources)).run(args);
    }
  1. org.springframework.boot.SpringApplication#SpringApplication(java.lang.Class<?>...)
    public SpringApplication(Class<?>... primarySources) {
        this((ResourceLoader)null, primarySources);
    }

    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.sources = new LinkedHashSet();
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.addConversionService = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = Collections.emptySet();
        this.isCustomEnvironment = false;
        this.lazyInitialization = false;
        this.applicationContextFactory = ApplicationContextFactory.DEFAULT;
        this.applicationStartup = ApplicationStartup.DEFAULT;
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

这里会调用到org.springframework.core.io.support.SpringFactoriesLoader#loadSpringFactories 去读取相关的自动配置。

    private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            Map<String, List<String>> result = new HashMap();

            try {
                Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");

                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Map.Entry<?, ?> entry = (Map.Entry)var6.next();
                        String factoryTypeName = ((String)entry.getKey()).trim();
                        String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        String[] var10 = factoryImplementationNames;
                        int var11 = factoryImplementationNames.length;

                        for(int var12 = 0; var12 < var11; ++var12) {
                            String factoryImplementationName = var10[var12];
                            ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                                return new ArrayList();
                            })).add(factoryImplementationName.trim());
                        }
                    }
                }

                result.replaceAll((factoryType, implementations) -> {
                    return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
                });
                cache.put(classLoader, result);
                return result;
            } catch (IOException var14) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
            }
        }
    }

这里也就看到了我们熟悉的扫描classpath下META-INF/spring.factories 文件信息,然后缓存到map。

  1. 在springboot.xxx.jar META-INF/spring.factories 文件有下面配置:
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
  1. 注意这里不是自动配置

自动配置是在一个Spring的后置处理器中处理的。org.springframework.boot.autoconfigure.AutoConfigurationImportSelector。

2. 读取配置文件过程

springboot 启动过程中会调用到org.springframework.boot.SpringApplication#run(java.lang.String...)

    public ConfigurableApplicationContext run(String... args) {
        long startTime = System.nanoTime();
        DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
        ConfigurableApplicationContext context = null;
        this.configureHeadlessProperty();
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);

        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            Banner printedBanner = this.printBanner(environment);
            context = this.createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
            this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
            }

            listeners.started(context, timeTakenToStartup);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var12) {
            this.handleRunFailure(context, var12, listeners);
            throw new IllegalStateException(var12);
        }

        try {
            Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
            listeners.ready(context, timeTakenToReady);
            return context;
        } catch (Throwable var11) {
            this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var11);
        }
    }

1. getRunListeners 方法

​ 这个方法源码如下:

    private SpringApplicationRunListeners getRunListeners(String[] args) {
        Class<?>[] types = new Class[]{SpringApplication.class, String[].class};
        return new SpringApplicationRunListeners(logger, this.getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args), this.applicationStartup);
    }

方法返回一个SpringApplicationRunListeners 对象。 getSpringFactoriesInstances 方法会从springboot 的自动配置读取SpringApplicationRunListener 实现类。也就是上面的EventPublishingRunListener。

2. prepareEnvironment

开始准备环境

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
        ConfigurableEnvironment environment = this.getOrCreateEnvironment();
        this.configureEnvironment(environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach(environment);
        listeners.environmentPrepared(bootstrapContext, environment);
        DefaultPropertiesPropertySource.moveToEnd(environment);
        Assert.state(!environment.containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");
        this.bindToSpringApplication(environment);
        if (!this.isCustomEnvironment) {
            EnvironmentConverter environmentConverter = new EnvironmentConverter(this.getClassLoader());
            environment = environmentConverter.convertEnvironmentIfNecessary(environment, this.deduceEnvironmentClass());
        }

        ConfigurationPropertySources.attach(environment);
        return environment;
    }
  1. 创建一个环境对象
  2. 对环境进行配置
  3. listeners.environmentPrepared(bootstrapContext, environment); 准备环境,发布事件
    void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        this.doWithListeners("spring.boot.application.environment-prepared", (listener) -> {
            listener.environmentPrepared(bootstrapContext, environment);
        });
    }
  1. 继续调用到org.springframework.boot.context.event.EventPublishingRunListener#environmentPrepared
    public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        this.initialMulticaster.multicastEvent(new ApplicationEnvironmentPreparedEvent(bootstrapContext, this.application, this.args, environment));
    }

这里就是广播事件,接下来就是看事件处理器的处理。

  1. 继续调用到org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent)
    public void multicastEvent(ApplicationEvent event) {
        this.multicastEvent(event, this.resolveDefaultEventType(event));
    }

    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
        Executor executor = this.getTaskExecutor();
        Iterator var5 = this.getApplicationListeners(event, type).iterator();

        while(var5.hasNext()) {
            ApplicationListener<?> listener = (ApplicationListener)var5.next();
            if (executor != null) {
                executor.execute(() -> {
                    this.invokeListener(listener, event);
                });
            } else {
                this.invokeListener(listener, event);
            }
        }

    }

这里获取到的变量var5包含如下listener:

接着遍历6个listener,然后invokeListener 内部调用doInvokeListener 方法。也就是调用到 listener.onApplicationEvent(event); 方法。

4. 6个listener的inApplicationEvent 方法(环境后置处理器用法)

1. org.springframework.boot.env.EnvironmentPostProcessorApplicationListener#onApplicationEvent

    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationEnvironmentPreparedEvent) {
            this.onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event);
        }

        if (event instanceof ApplicationPreparedEvent) {
            this.onApplicationPreparedEvent();
        }

        if (event instanceof ApplicationFailedEvent) {
            this.onApplicationFailedEvent();
        }

    }
    private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment environment = event.getEnvironment();
        SpringApplication application = event.getSpringApplication();
        Iterator var4 = this.getEnvironmentPostProcessors(application.getResourceLoader(), event.getBootstrapContext()).iterator();

        while(var4.hasNext()) {
            EnvironmentPostProcessor postProcessor = (EnvironmentPostProcessor)var4.next();
            postProcessor.postProcessEnvironment(environment, application);
        }

    }
  1. 这里是获取到环境的后置处理器,然后进行处理。获取到的7个后置处理器如下:

2. 核心的后置环境后置处理器是:ConfigDataEnvironmentPostProcessor

其逻辑如下:

    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        this.postProcessEnvironment(environment, application.getResourceLoader(), application.getAdditionalProfiles());
    }

    void postProcessEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
        try {
            this.logger.trace("Post-processing environment to add config data");
            ResourceLoader resourceLoader = resourceLoader != null ? resourceLoader : new DefaultResourceLoader();
            this.getConfigDataEnvironment(environment, (ResourceLoader)resourceLoader, additionalProfiles).processAndApply();
        } catch (UseLegacyConfigProcessingException var5) {
            this.logger.debug(LogMessage.format("Switching to legacy config file processing [%s]", var5.getConfigurationProperty()));
            this.configureAdditionalProfiles(environment, additionalProfiles);
            this.postProcessUsingLegacyApplicationListener(environment, resourceLoader);
        }

    }

    ConfigDataEnvironment getConfigDataEnvironment(ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
        return new ConfigDataEnvironment(this.logFactory, this.bootstrapContext, environment, resourceLoader, additionalProfiles, this.environmentUpdateListener);
    }

1.org.springframework.boot.context.config.ConfigDataEnvironment#ConfigDataEnvironment 创建对象和初始化

    ConfigDataEnvironment(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles, ConfigDataEnvironmentUpdateListener environmentUpdateListener) {
        Binder binder = Binder.get(environment);
        UseLegacyConfigProcessingException.throwIfRequested(binder);
        this.logFactory = logFactory;
        this.logger = logFactory.getLog(this.getClass());
        this.notFoundAction = (ConfigDataNotFoundAction)binder.bind("spring.config.on-not-found", ConfigDataNotFoundAction.class).orElse(ConfigDataNotFoundAction.FAIL);
        this.bootstrapContext = bootstrapContext;
        this.environment = environment;
        this.resolvers = this.createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
        this.additionalProfiles = additionalProfiles;
        this.environmentUpdateListener = environmentUpdateListener != null ? environmentUpdateListener : ConfigDataEnvironmentUpdateListener.NONE;
        this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext, resourceLoader.getClassLoader());
        this.contributors = this.createContributors(binder);
    }

(1). this.createContributors(binder) 是创建属性描述者:

    private ConfigDataEnvironmentContributors createContributors(Binder binder) {
        this.logger.trace("Building config data environment contributors");
        MutablePropertySources propertySources = this.environment.getPropertySources();
        List<ConfigDataEnvironmentContributor> contributors = new ArrayList(propertySources.size() + 10);
        PropertySource<?> defaultPropertySource = null;
        Iterator var5 = propertySources.iterator();

        while(var5.hasNext()) {
            PropertySource<?> propertySource = (PropertySource)var5.next();
            if (DefaultPropertiesPropertySource.hasMatchingName(propertySource)) {
                defaultPropertySource = propertySource;
            } else {
                this.logger.trace(LogMessage.format("Creating wrapped config data contributor for '%s'", propertySource.getName()));
                contributors.add(ConfigDataEnvironmentContributor.ofExisting(propertySource));
            }
        }

        contributors.addAll(this.getInitialImportContributors(binder));
        if (defaultPropertySource != null) {
            this.logger.trace("Creating wrapped config data contributor for default property source");
            contributors.add(ConfigDataEnvironmentContributor.ofExisting(defaultPropertySource));
        }

        return this.createContributors((List)contributors);
    }

(2). 继续调用到:

    private List<ConfigDataEnvironmentContributor> getInitialImportContributors(Binder binder) {
        List<ConfigDataEnvironmentContributor> initialContributors = new ArrayList();
        this.addInitialImportContributors(initialContributors, this.bindLocations(binder, "spring.config.import", EMPTY_LOCATIONS));
        this.addInitialImportContributors(initialContributors, this.bindLocations(binder, "spring.config.additional-location", EMPTY_LOCATIONS));
        this.addInitialImportContributors(initialContributors, this.bindLocations(binder, "spring.config.location", DEFAULT_SEARCH_LOCATIONS));
        return initialContributors;
    }

从静态代码块可以看到默认的配置文件路径如下:

    static {
        List<ConfigDataLocation> locations = new ArrayList();
        locations.add(ConfigDataLocation.of("optional:classpath:/;optional:classpath:/config/"));
        locations.add(ConfigDataLocation.of("optional:file:./;optional:file:./config/;optional:file:./config/*/"));
        DEFAULT_SEARCH_LOCATIONS = (ConfigDataLocation[])locations.toArray(new ConfigDataLocation[0]);
        EMPTY_LOCATIONS = new ConfigDataLocation[0];
        CONFIG_DATA_LOCATION_ARRAY = Bindable.of(ConfigDataLocation[].class);
        STRING_LIST = Bindable.listOf(String.class);
        ALLOW_INACTIVE_BINDING = new ConfigDataEnvironmentContributors.BinderOption[0];
        DENY_INACTIVE_BINDING = new ConfigDataEnvironmentContributors.BinderOption[]{BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE};
    }

(3).....继续后面的初始化

  1. org.springframework.boot.context.config.ConfigDataEnvironment#processAndApply 处理
    void processAndApply() {
        ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers, this.loaders);
        this.registerBootstrapBinder(this.contributors, (ConfigDataActivationContext)null, DENY_INACTIVE_BINDING);
        ConfigDataEnvironmentContributors contributors = this.processInitial(this.contributors, importer);
        ConfigDataActivationContext activationContext = this.createActivationContext(contributors.getBinder((ConfigDataActivationContext)null, new ConfigDataEnvironmentContributors.BinderOption[]{BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE}));
        contributors = this.processWithoutProfiles(contributors, importer, activationContext);
        activationContext = this.withProfiles(contributors, activationContext);
        contributors = this.processWithProfiles(contributors, importer, activationContext);
        this.applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(), importer.getOptionalLocations());
    }

(1). this.processInitial 会根据上面生成的属性描述符去找文件,经过多层调用,调用到org.springframework.boot.context.config.StandardConfigDataLocationResolver#resolve(org.springframework.boot.context.config.ConfigDataLocationResolverContext, org.springframework.boot.context.config.ConfigDataLocation)

    public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) throws ConfigDataNotFoundException {
        return this.resolve(this.getReferences(context, location.split()));
    }

参数如下:

解析到的结果如下:

class path resource [config/application.properties]

(2). this.createActivationContext(contributors.getBinder 会根据文件解析配置。找到文件开始调用propertySourceLoaders 解析文件,比如yaml和properties 对应的解析器分别为:PropertiesPropertySourceLoader、YamlPropertySourceLoader。 真正的解析方法也是在loadProperties 内部。调用连如下:

(3). 调用org.springframework.boot.context.config.ConfigDataEnvironment#applyToEnvironment 应用到environment 对象

private void applyToEnvironment(ConfigDataEnvironmentContributors contributors, ConfigDataActivationContext activationContext, Set<ConfigDataLocation> loadedLocations, Set<ConfigDataLocation> optionalLocations) {
    this.checkForInvalidProperties(contributors);
    this.checkMandatoryLocations(contributors, activationContext, loadedLocations, optionalLocations);
    MutablePropertySources propertySources = this.environment.getPropertySources();
    this.applyContributor(contributors, activationContext, propertySources);
    DefaultPropertiesPropertySource.moveToEnd(propertySources);
    Profiles profiles = activationContext.getProfiles();
    this.logger.trace(LogMessage.format("Setting default profiles: %s", profiles.getDefault()));
    this.environment.setDefaultProfiles(StringUtils.toStringArray(profiles.getDefault()));
    this.logger.trace(LogMessage.format("Setting active profiles: %s", profiles.getActive()));
    this.environment.setActiveProfiles(StringUtils.toStringArray(profiles.getActive()));
    this.environmentUpdateListener.onSetProfiles(profiles);
}

1》MutablePropertySources propertySources = this.environment.getPropertySources(); 获取到propertySources;

继续调用org.springframework.boot.context.config.ConfigDataEnvironment#applyContributor:(应用配置)

    private void applyContributor(ConfigDataEnvironmentContributors contributors, ConfigDataActivationContext activationContext, MutablePropertySources propertySources) {
        this.logger.trace("Applying config data environment contributions");
        Iterator var4 = contributors.iterator();

        while(var4.hasNext()) {
            ConfigDataEnvironmentContributor contributor = (ConfigDataEnvironmentContributor)var4.next();
            PropertySource<?> propertySource = contributor.getPropertySource();
            if (contributor.getKind() == Kind.BOUND_IMPORT && propertySource != null) {
                if (!contributor.isActive(activationContext)) {
                    this.logger.trace(LogMessage.format("Skipping inactive property source '%s'", propertySource.getName()));
                } else {
                    this.logger.trace(LogMessage.format("Adding imported property source '%s'", propertySource.getName()));
                    propertySources.addLast(propertySource);
                    this.environmentUpdateListener.onPropertySourceAdded(propertySource, contributor.getLocation(), contributor.getResource());
                }
            }
        }

    }

最终addLast(propertySource) 会加一个对象:

根据java引用传递,实际是向environment.getPropertySources(); 添加了一个propertySource 对象。实际也就是org.springframework.core.env.PropertySource 的实现类。

到这里大致流程走完。

3. 自定义配置后置处理器以及测试

假设一个场景是从自己定义配置来源。我们增加自定的配置后置处理器然后测试。

1. 建立自己的后置处理器

MyEnvironmentPostProcessor

package com.example.demo.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;

import java.util.HashMap;

public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        environment.getPropertySources().addLast(new MyPropertySource("myPropertySource", new HashMap<>()));
    }
}

MyPropertySource

package com.example.demo.config;

import org.springframework.core.env.PropertySource;

import java.util.HashMap;

public class MyPropertySource extends PropertySource<HashMap<String, Object>> {

    public MyPropertySource(String name, HashMap<String, Object> source) {
        super(name, source);

        // 模拟加几个配置
        source.put("name", "zs");
    }

    @Override
    public Object getProperty(String name) {
        return getSource().get(name);
    }
}

resource/META-INT/spring.factories 文件增加如下配置:

org.springframework.boot.env.EnvironmentPostProcessor=\
  com.example.demo.config.MyEnvironmentPostProcessor

2. 测试

测试类

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class TestComponent {

    @Autowired
    private Environment environment;

    @Value("${name}")
    private String name;

    @PostConstruct
    public void init() {
        System.out.println(name);
    }
}
  1. 输出zs

  2. com.example.demo.config.MyEnvironmentPostProcessor#postProcessEnvironment 调用连如下

  3. com.example.demo.config.MyPropertySource#getProperty 注入name属性调用连如下:

标签:配置文件,自定义,springframework,environment,context,org,new,config,springboot
From: https://www.cnblogs.com/qlqwjy/p/16972040.html

相关文章

  • 从头开始搭建一个SpringBoot项目--SpringSecurity的配置
    OverridetheentrypointofanimageIntroducedinGitLabandGitLabRunner9.4.Readmoreaboutthe extendedconfigurationoptions.Beforeexplainingtheav......
  • Python如何导入自定义模块?
    在C语言里为了工程文件的主程序main代码简洁的效果,我们经常用include“XXX”的来导入其.h文件在Python里Import自己的自定义模块需要注意几个坑以main主函数和需要导入的......
  • SpringBoot整合Swagger2在线文档
    SpringBoot整合Swagger2在线文档一什么是swagger?我们前面有讲到说开发时会创建Restful风格的API接口,供第三方或前端人员使用,那么前端人员在使用的过程中怎么知道有......
  • Springboot整合Swagger2在线文档
    SpringBoot整合Swagger2在线文档一什么是swagger?我们前面有讲到说开发时会创建Restful风格的API接口,供第三方或前端人员使用,那么前端人员在使用的过程中怎么知道有哪些接口......
  • containerd 配置文件 /etc/containerd/config.toml
    自动生成配置文件$containerdconfigdefault>/etc/containerd/config.toml修改配置文件后生效$systemctldaemon-reload&&systemctlrestartcontainerdvim/et......
  • 基于springboot多数据源的启动器
    一、简述dynamic-datasource-spring-boot-starter是一个基于springboot的快速集成多数据源的启动器。其支持 Jdk1.7+,SpringBoot1.4.x1.5.x2.x.x。二、准备数据多数......
  • SpringBoot3.x中spring.factories功能被移除的解决方案
    背景笔者所在项目组在搭建一个全新项目的时候选用了​​SpringBoot3.x​​​,项目中应用了很多​​SpringBoot2.x​​​时代相关的第三方组件例如​​baomidou​​​出品的​......
  • SpringBoot内置tomcat启动过程及原理
    作者:李岩科1背景SpringBoot是一个框架,一种全新的编程规范,他的产生简化了框架的使用,同时也提供了很多便捷的功能,比如内置tomcat就是其中一项,他让我们省去了搭建tomca......
  • element-ui upload自定义formdata上传文件和参数
     <el-uploadlist-type="text"action="":http-request="HandleHttpRequestDescFile":on-remove="handleRemoveDescFile":file-list="this.fileList"ref="upload"......
  • 浏览器滚动条的自定义和隐藏
    我们在处理业务的时候,偶尔会因为某些非抵抗力因素,需要对浏览器的滚动条进行处理,以提升产品的体验。本文我们来谈谈关于浏览器滚动条的自定义和隐藏......