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

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

时间:2022-12-17 18:39:51浏览次数:58  
标签:配置文件 自定义 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:

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

接着遍历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个后置处理器如下:

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

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()));
}

参数如下:

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

解析到的结果如下:

class path resource [config/application.properties]

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

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

(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) 会加一个对象:

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

根据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. springboot启动读取配置文件过程&自定义配置文件处理器_spring_06

  4. com.example.demo.config.MyPropertySource#getProperty 注入name属性调用连如下:
  5. springboot启动读取配置文件过程&自定义配置文件处理器_bootstrap_07

【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】



标签:配置文件,自定义,springframework,environment,context,org,new,config,springboot
From: https://blog.51cto.com/u_12826294/5949948

相关文章

  • javaclient操作kafka&springboot整合kafka&kafka分区
    1.javaclient测试kafka1.配置kafka允许远程推送修改config/Kraft/server.properties文件,,将地址变为服务器公网IP地址。advertised.listeners=PLAINTEXT://localhost:90......
  • 关于app.comfig配置文件
    尝试手动创建一个和应用同名的.exe.config文件,右键属性设置为copy到可执行属输出目录下。使用ConfigurationManager.GetSection("")总也读取不到。然后删除执行目录下的......
  • SpringBoot注解~@PropertySource
    1.@PropertySourceSpringBoot读取配置信息的方式有以下几种。但实际开发中一个配置文件是不够用的,比如项目中集成mongorediskafka等需要多个配置文件,这样有利于开发以......
  • C# Log4net配置文件 总结
    前言因为项目日志太杂乱而且很大,打开一个就卡死了,何况用户电脑也扛不住日志积累,要做一个日志记录器管理日志。但对里面的配置有一些不熟悉(毕竟都是复制粘贴的),所以记录一......
  • SpringBoot启动流程
    1.简述Springboot启动是通过Application启动类实现@SpringBootApplication(exclude={MongoAutoConfiguration.class,MongoDataAutoConfiguration.class},......
  • <三>自定义删除器
    unique_ptr的成员函数在上一篇博客中几乎全部涵盖,其实还有一个很有踢掉,即std::unique_ptr::get_deleter字面已经很明显了,就获得deleter智能指针采通过引用计数我们能解决......
  • 【SpringBoot】Spring Data Redis封装和Spring Cache
    一、参考资料​​RedisUtil:最全的Java操作Redis的工具类,使用StringRedisTemplate实现,封装了对Redis五种基本类型的各种操作!​​​​SpringCache-简书​​​​redis分布......
  • 【SpringBoot】封装自定义的starter
    一、参考资料​​SpringBoot封装自己的Starter-码农教程​​​​[Gradle]发布构件到本地仓库​​​​Gradle插件之maven-publish:发布androidlibrary到maven仓库-知乎......
  • SpringBoot2.x 优秀开源项目
    前后端分离vue开源项目:项目名开源地址eladmin-web​​https://gitee.com/elunez/eladmin-web​​eladmin​​https://gitee.com/elunez/eladmin​​RuoYi-Vue​​https://gi......
  • android自定义属性
    1、引言对于自定义属性,大家肯定都不陌生,遵循以下几步,就可以实现:1.自定义一个CustomView(extendsView)类2.编写values/attrs.xml,在其中编写styleable和item等标签元素3.......