Spring Boot

一、基础

1. 什么是Spring Boot

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.
We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration.

使用“习惯优于配置”(项目中存在大量的配置,此外还内置了一个习惯性的配置,让你无需手动配置)的理念让你的项目快速运行起来。

特征

  1. 创建独立的Spring应用程序;
  2. 直接嵌入Tomcat,Jetty或Undertow(无需部署WAR文件);
  3. 提供“初始”的POM文件内容,以简化Maven配置;
  4. 尽可能自动配置Spring;
  5. 提供生产就绪的功能,如指标,健康检查和外部化配置;
  6. 绝对无代码生成,也不需要XML配置。

构建

  • Spring Boot父级依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.1.RELEASE</version>
</dependency>

有了Spring Boot父级依赖,表示当前的项目就是Spring Boot项目了,spring-boot-starter-parent是一个特殊的starter,它用来提供相关的Maven默认依赖,使用它之后,常用的包依赖可以省去version标签。

  • Spring Boot Maven插件
<build>
       <plugins>
           <plugin>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring‐boot‐maven‐plugin</artifactId>
           </plugin>
       </plugins>
   </build>

Spring Boot Maven插件提供了许多方便的功能:

  1. 把项目打包成一个可执行的超级JAR(uber-JAR),包括把应用程序的所有依赖打入JAR文件内,并为JAR添加一个描述文件,其中的内容能让你用java -jar来运行应用程序。
  2. 搜索public static void main()方法来标记为可运行类。

二、原理

2.1 核心注解

2.1.1 @SpringBootApplication

@Target(ElementType.TYPE) // 注解的适用范围,其中TYPE用于描述类、接口(包括包注解类型)或enum声明
@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期,保留到class文件中(三个生命周期)
@Documented // 表明这个注解应该被javadoc记录
@Inherited // 子类可以继承该注解
@SpringBootConfiguration // 继承了Configuration,表示当前是注解类
@EnableAutoConfiguration // 开启springboot的注解功能,springboot的四大神器之一,其借助@import的帮助
@ComponentScan(excludeFilters = {// 扫描路径设置)
	@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
	@Filter(type = FilterType.CUSTOM, classes = 
	AutoConfigurationExcludeFilter.class)}) 
	
public @interface SpringBootApplication {
	...
}

注解三剑客:

  • @SpringBootConfiguration,即(Configuration)
  • @EnableAutoConfiguration
  • @ComponentScan
SpringBootConfiguration

里面只有一个@Configuration注解。

它就是JavaConfig形式的Spring Ioc容器的配置类使用的那个@Configuration,SpringBoot社区推荐使用基于JavaConfig的配置形式,所以,这里的启动类标注了@Configuration之后,本身其实也是一个IoC容器的配置类。

  • 任何一个标注了@Configuration的Java类定义都是一个JavaConfig配置类。
@Configuration
public class MockConfiguration{
//bean定义
}
  • 任何一个标注了@Bean的方法,其返回值将作为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id
@Configuration
public class MockConfiguration{
	@Bean
	public MockService mockService(){
		return new MockServiceImpl();
	}
}
  • 如果一个bean的定义依赖其他bean,则直接调用对应的JavaConfig类中依赖bean的创建方法就可以了。
@Configuration
public class MockConfiguration{
	@Bean
	public MockService mockService(){
		return new MockServiceImpl(dependencyService());
	}
	@Bean
	public DependencyService dependencyService(){
		return new DependencyServiceImpl();
	}
}
ComponentScan

@ComponentScan的功能其实就是自动扫描并加载符合条件的组件(比如@Component和@Repository等)或者bean定义,最终将这些bean定义加载到IoC容器中

我们可以通过basePackages等属性来细粒度的定制@ComponentScan自动扫描的范围,如果不指定,则默认Spring框架实现会从声明@ComponentScan所在类的package进行扫描。(默认不指定basePackages)

EnableAutoConfiguration(重中之重)

springboot的自动化配置

借助@Import的支持,收集和注册特定场景相关的bean定义,将所有符合自动配置条件的bean定义加载到IoC容器。

@Target({java.lang.annotation.ElementType.TYPE})
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    java.lang.String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    java.lang.Class<?>[] exclude() default {};

    java.lang.String[] excludeName() default {};
}
@AutoConfigurationPackage:自动配置包

作用是将 添加该注解的类所在的package 作为 自动配置package 进行管理

// 它其实是注册了一个Bean的定义。
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		register(
			registry, 
			new PackageImport(metadata).getPackageName() // 返回当前主程序类的同级以及子级的包组件。(这也就是为什么,我们要把DemoApplication放在项目的最高级中。)
		);
	}
	@Override
	public Set<Object> determineImports(AnnotationMetadata metadata) {
		return Collections.singleton(new PackageImport(metadata));
	}
}
@Import(AutoConfigurationImportSelector.class): 导入自动配置的组件

AutoConfigurationImportSelector 继承了 DeferredImportSelector 继承了 ImportSelector。

// 它其实是去加载spring.factories外部文件,这个外部文件,有很多自动配置的类。
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; 
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
		
	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
				// 读取spring.factories里面的配置类
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
				annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}
	
	protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
			AnnotationMetadata annotationMetadata) {
		if(!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
		/*
		* 通过SpringFactoriesLoader.loadFactoryNames读取spring.factories的配置类
		* 格式为全路径包名(便于后续反射创建实例)
		*/
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
		/*
		* 去重 new ArrayList<>(new LinkedHashSet<>(list));
		*/
		configurations = removeDuplicates(configurations);
		/*
		* 获取注解上exclude或者excludeName属性标记的需要排除的配置类,
		* 以及spring.autoconfigure.exclude标记的需要排除的配置类
		*/
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		/*
		* 检查要排除的配置类是否在configurations中,如果不在则抛出IllegalStateException异常
		*/
		checkExcludedClasses(configurations, exclusions);
		// configurations去除要排除的配置类
		configurations.removeAll(exclusions);
		/*
		* 过滤在spring.factories里面配置了
		* org.springframework.boot.autoconfigure.AutoConfigurationImportFilter
		* 为key的配置类,实际是过滤有条件的bean创建。
		*/ 
		configurations = filter(configurations, autoConfigurationMetadata);
		/*
		* 在spring.factories里面查找配置了
		* org.springframework.boot.autoconfigure.AutoConfigurationImportListener
		* 为key的listener,并绑定AutoConfigurationImportEvent(包含配置类)
		*/
		fireAutoConfigurationImportEvents(configurations, exclusions);
		// 封装成AutoConfigurationEntry,供run方法通过反射创建实例
		return new AutoConfigurationEntry(configurations, exclusions);
	}
}

其中,最关键的要属 @Import(AutoConfigurationImportSelector.class),借助
AutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。就像一只“八爪鱼”一样。
 EnableAutoConfiguration得以生效的关键组件关系图

SpringFactoriesLoader详解

借助于Spring框架原有的一个工具类:SpringFactoriesLoader的支持,@EnableAutoConfiguration可以 “智能” 的自动配置功效才得以大功告成!

SpringFactoriesLoader属于Spring框架私有的一种扩展方案,其主要功能就是从指定的配置文件META-INF/spring.factories加载配置。

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
	String factoryClassName = factoryClass.getName();
	return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

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

	try {
		Enumeration<URL> urls = (classLoader != null ?
				classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
				ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
		result = new LinkedMultiValueMap<>();
		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();
			UrlResource resource = new UrlResource(url);
			Properties properties = PropertiesLoaderUtils.loadProperties(resource);
			for (Map.Entry<?, ?> entry : properties.entrySet()) {
				List<String> factoryClassNames = Arrays.asList(
						StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
				result.addAll((String) entry.getKey(), factoryClassNames);
			}
		}
		cache.put(classLoader, result);
		return result;
	}
	catch (IOException ex) {
		throw new IllegalArgumentException("Unable to load factories from location [" +
				FACTORIES_RESOURCE_LOCATION + "]", ex);
	}
}

@EnableAutoConfiguration 的场景中,SpringFactoriesLoader提供了一种配置查找的功能支持,即根据 @EnableAutoConfiguration 的完整类名 org.springframework.boot.autoconfigure.EnableAutoConfiguration 作为查找的 Key,获取对应的一组 @Configuration 类。

总结:@EnableAutoConfiguration 自动配置其实就是从classpath 中搜寻所有 META-INF/spring.factories 配置文件,并将其中 org.spring-framework.boot.autoconfigure.EnableAutoConfiguration 对应的配置项通过反射实例化为对应的标注了 @Configuration 的 JavaConfig 形式的 IoC 容器配置类,然后汇总为一个并加载到 IoC 容器

2.1.2 Spring Boot启动原理

springboot启动流程图

  1. 如果我们使用的是SpringApplication的静态run方法,那么,这个方法里面首先要创建一个SpringApplication对象实例,然后调用这个创建好的SpringApplication的实例run方法。在SpringApplication实例初始化的时候,它会提前做几件事情:
    • 根据 classpath 里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为 Web 应用使用的 ApplicationContext 类型,还是应该创建一个标准 Standalone 应用使用的 ApplicationContext 类型。
    • 使用 SpringFactoriesLoader 在应用的 classpath 中查找并加载所有可用的 ApplicationContextInitializer。
    • 使用 SpringFactoriesLoader 在应用的 classpath 中查找并加载所有可用的 ApplicationListener。
    • 推断并设置 main 方法的定义类。
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	this.resourceLoader = resourceLoader;
	Assert.notNull(primarySources, "PrimarySources must not be null");
	//把SpringdemoApplication.class设置为属性存储起来
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
	//设置应用类型为Standard还是Web
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
	//设置初始化器(Initializer),最后会调用这些初始化器
	setInitializers((Collection)getSpringFactoriesInstances(ApplicationContextInitializer.class));
	//设置监听器(Listener)
	setListeners((Collection)getSpringFactoriesInstances(ApplicationListener.class));
	this.mainApplicationClass = deduceMainApplicationClass();
}
  1. SpringApplication实例初始化完成并且完成设置后,就开始执行run方法的逻辑了,方法执行伊始,首先遍历执行所有通过SpringFactoriesLoader可以查找到并加载的SpringApplicationRunListener。调用它们的started()方法,告诉这些SpringApplicationRunListener,“嘿,SpringBoot应用要开始执行咯!”。
/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
	//计时工具
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	ConfigurableApplicationContext context = null;
	Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
	configureHeadlessProperty();
	//第一步:获取并启动监听器
	SpringApplicationRunListeners listeners = getRunListeners(args);
	listeners.starting();
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		//第二步:根据SpringApplicationRunListeners以及参数来准备环境
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
		configureIgnoreBeanInfo(environment);
		//准备Banner打印器‐就是启动Spring Boot的时候在console上的ASCII艺术字体
		Banner printedBanner = printBanner(environment);
		//第三步:创建Spring容器
		context = createApplicationContext();
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context);
		//第四步:Spring容器前置处理
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		//第五步:刷新容器
		refreshContext(context);
		//第六步:Spring容器后置处理
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new	StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		//第七步:发出结束执行的事件
		listeners.started(context);
		//第八步:执行Runners
		callRunners(context, applicationArguments);
	} catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}
	
	try {
		listeners.running(context);
	} catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, null);
		throw new IllegalStateException(ex);
	}
	//返回容器
	return context;
}
  1. 创建并配置当前 SpringBoot 应用将要使用的 Environment(包括配置要使用的 PropertySource 以及 Profile)。
    • Environment是SpringFramework中一个很重要的接口,用于存放应用程序的配置信息。
    • PropertySource(org.springframework.core.env.PropertySource)是用来将一个对象以键值对的形式表示,Spring将多种来源的属性键值对转换成PropertySource来表示。
private ConfigurableEnvironment prepareEnvironment(
			SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		/*
		* 一、创建Environment对象。
		* 在getOrCreateEnvironment方法中,会根据之前推断的webApplicationType(web程序类型)创建不同了实现的Environment对象
		*/
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		/*
		* 二、配置Environment对象。
		* 1. 应用程序如果有命令行参数,则在Environment中添加一个与这个命令行参数相关的PropertySource;
		* 2. 根据命令行参数中spring.profiles.active属性配置Environment对象中的activeProfile。
		*/
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		// 三、发布ApplicationEnvironmentPreparedEvent(应用环境已准备)事件。
		listeners.environmentPrepared(environment);
		/*
		* 四、将Environment中的spring.main属性绑定到SpringAppilcation对象中,在执行到这一步时,Environment中已经包含了用户设置的profile文件属性。
		*/
		bindToSpringApplication(environment);
		/*
		* 五、转换Environment对象的类型。
		* 在上一步中,如果用户使用spring.main.web-application-type属性手动设置了应用程序的webApplicationType并且用户设置的类型与SpringApplication推断出来的不一致,则
SpringApplication会将环境对象转换成用户设置的webApplicationType相关的类型。
		*/
		if (this.webApplicationType == WebApplicationType.NONE) {
			environment = new EnvironmentConverter(getClassLoader())
					.convertToStandardEnvironmentIfNecessary(environment);
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}
  1. 遍历调用所有SpringApplicationRunListener的environmentPrepared()的方法,告诉他
    们:“当前SpringBoot应用使用的Environment准备好了咯!”。
public void environmentPrepared(ConfigurableEnvironment environment) {
	for (SpringApplicationRunListener listener : this.listeners) {
		listener.environmentPrepared(environment);
	}
}
  1. 如果SpringApplication的showBanner属性被设置为true,则打印banner。
private Banner printBanner(ConfigurableEnvironment environment) {
	if (this.bannerMode == Banner.Mode.OFF) {
		return null;
	}
	ResourceLoader resourceLoader = (this.resourceLoader != null ? this.resourceLoader
			: new DefaultResourceLoader(getClassLoader()));
	SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(
			resourceLoader, this.banner);
	if (this.bannerMode == Mode.LOG) {
		return bannerPrinter.print(environment, this.mainApplicationClass, logger);
	}
	return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}
  1. 创建ApplicationContext,根据用户是否明确设置了applicationContextClass类型以及初始化阶段的推断结果,决定该为当前SpringBoot应用创建什么类型的ApplicationContext(通过webApplicationType类型判断),最后通过BeanUtils实例化上下文对象,并返回。
protected ConfigurableApplicationContext createApplicationContext() {
	Class<?> contextClass = this.applicationContextClass;
	if (contextClass == null) {
		try {
			switch (this.webApplicationType) {
			case SERVLET:
				contextClass = Class.forName(DEFAULT_WEB_CONTEXT_CLASS);
				break;
			case REACTIVE:
				contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
				break;
			default:
				contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
			}
		}
		catch (ClassNotFoundException ex) {
			throw new IllegalStateException(
					"Unable create a default ApplicationContext, "
							+ "please specify an ApplicationContextClass",
					ex);
		}
	}
	return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
  1. SpringApplication在prepareContext方法中对上下文对象进行预配置
private void prepareContext(ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
	context.setEnvironment(environment);
	postProcessApplicationContext(context);
	/*
	* 发布ApplicationContextInitializedEvent(上下文已初始化)事件。
	* 执行所有ApplicationContextInitializer的initialize方法。
	* 这些ApplicationContextInitializer是在SpringApplication中的构造函数中加载的(通过读取spring.factories加载)
	*/
	applyInitializers(context);
	/*
	* 发布ApplicationPreparedEvent(上下文已准备)事件。
	*/
	listeners.contextPrepared(context);
	if (this.logStartupInfo) {
		logStartupInfo(context.getParent() == null);
		logStartupProfileInfo(context);
	}

	// Add boot specific singleton beans
	context.getBeanFactory().registerSingleton("springApplicationArguments",
			applicationArguments);
	if (printedBanner != null) {
		context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
	}

	// Load the sources
	Set<Object> sources = getAllSources();
	Assert.notEmpty(sources, "Sources must not be empty");
	load(context, sources.toArray(new Object[0]));
	listeners.contextLoaded(context);
}
  1. 刷新应用上下文对象
private void refreshContext(ConfigurableApplicationContext context) {
	refresh(context);
	if (this.registerShutdownHook) {
		try {
			context.registerShutdownHook();
		}
		catch (AccessControlException ex) {
			// Not allowed in some environments.
		}
	}
}
  1. 发布应用程序已启动(ApplicationStartedEvent)事件
  2. 在BeanFactory中获取所有ApplicationRunner和CommandLineRunner并调用他们的run方法
private void callRunners(ApplicationContext context, ApplicationArguments args) {
	List<Object> runners = new ArrayList<>();
	runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
	runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
	AnnotationAwareOrderComparator.sort(runners);
	for (Object runner : new LinkedHashSet<>(runners)) {
		if (runner instanceof ApplicationRunner) {
			callRunner((ApplicationRunner) runner, args);
		}
		if (runner instanceof CommandLineRunner) {
			callRunner((CommandLineRunner) runner, args);
		}
	}
}
  1. 异常处理,如果run方法的处理过程中发生异常,则对exitCode进行相应处理
private void handleRunFailure(ConfigurableApplicationContext context,
			Throwable exception,
			Collection<SpringBootExceptionReporter> exceptionReporters,
			SpringApplicationRunListeners listeners) {
	try {
		try {
			handleExitCode(context, exception);
			if (listeners != null) {
				listeners.failed(context, exception);
			}
		}
		finally {
			reportFailure(exceptionReporters, exception);
			if (context != null) {
				context.close();
			}
		}
	}
	catch (Exception ex) {
		logger.warn("Unable to close ApplicationContext", ex);
	}
	ReflectionUtils.rethrowRuntimeException(exception);
}

SpringBoot应用启动步骤简要示意图如下:
 SpringBoot应用启动步骤简要示意图

  • shutdownHook简单介绍 [^1]
    • 作用:JVM退出时执行的业务逻辑
    • 添加:Runtime.getRuntime().addShutdownHook()
    • 移除:Runtime.getRuntime().removeShutdownHook(this.shutdownHook)
  • *什么时候会调用Shutdown Hook
    • 程序正常停止
    • 程序异常退出
    • 受到外界影响停止

2.1.3 配置静态资源地址和访问路径

spring.mvc.static-path-pattern

spring.mvc.static-path-pattern代表的含义是我们应该以什么样的路径来访问静态资源,换句话说,只有静态资源满足什么样的匹配条件,Spring Boot才会处理静态资源请求,以官方配置为例:

# 这表示只有静态资源的访问路径为/resources/**时,才会处理请求
spring.mvc.static‐path‐pattern=/resources/**

请求地址类似于“http://localhost:8080/resources/jquery.js” 时,Spring Boot才会处理此请求,处理方式是将根据模式匹配后的文件名查找本地文件,那么应该在什么地方查找本地文件呢?这就是“spring.resources.static-locations”的作用了。

spring.resources.static-locations

spring.resources.static-locations”用于告诉Spring Boot应该在何处查找静态资源文件,这是一个列表性的配置,查找文件时会依赖于配置的先后顺序依次进行,默认的官方配置如下:

spring.resources.static‐locations=classpath:/static,classpath:/public,classpath:/resources,classpath:/META‐INF/resources
静态资源的Bean配置

xml配置如下:

<mvc:resources mapping="/resources/**" location="/public‐resources/">
<mvc:cache‐control max‐age="3600" cache‐public="true"/>
</mvc:resources>

java config配置如下:

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/resources/**")
			.addResourceLocations("/public‐resources/")
			.setCacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)
			.cachePublic());
	}
}

综上,“spring.mvc.static-path-pattern”用于阐述HTTP请求地址,而“spring.resources.staticlocations”则用于描述静态资源的存放位置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

抽抽了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值