mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23bb745ea4 | |||
| 9197f15a30 | |||
| 15a6373fed | |||
| 2a268701c4 | |||
| 11a0df3fd1 | |||
| b167e1a93f | |||
| 8d735e6e60 | |||
| 9bc09631d6 | |||
| 39be572a7a | |||
| 6ac780d595 | |||
| c6ce65ef56 | |||
| b5743966d6 | |||
| c5de5c9939 | |||
| ff1485fd8d | |||
| 1b4438caac | |||
| 0c7e000250 | |||
| 694db2273f | |||
| 47d3819eab | |||
| f295c8b2fb | |||
| 40d84c297b | |||
| 0416168d0e | |||
| c4c3d59d07 | |||
| 0268e76298 | |||
| 4ec8ae42f4 | |||
| 995aaa6159 | |||
| 2c89ff934d | |||
| c58853f5e5 | |||
| 66826ac960 | |||
| a603779f33 | |||
| e5475d698a | |||
| b728b4640b | |||
| bc657eb4d5 | |||
| 9b967775ed | |||
| 9af11ad5ce | |||
| 7a54ff2d21 | |||
| ec3f857bda | |||
| b3eb1a2ad7 | |||
| bce850aa12 |
+7
-7
@@ -28,8 +28,8 @@ configure(allprojects) { project ->
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.5"
|
||||
mavenBom "io.netty:netty-bom:4.1.69.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.12"
|
||||
mavenBom "io.netty:netty-bom:4.1.70.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.13"
|
||||
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR10"
|
||||
mavenBom "io.rsocket:rsocket-bom:1.1.1"
|
||||
mavenBom "org.eclipse.jetty:jetty-bom:9.4.44.v20210927"
|
||||
@@ -128,14 +128,14 @@ configure(allprojects) { project ->
|
||||
dependency "org.webjars:webjars-locator-core:0.48"
|
||||
dependency "org.webjars:underscorejs:1.8.3"
|
||||
|
||||
dependencySet(group: 'org.apache.tomcat', version: '9.0.53') {
|
||||
dependencySet(group: 'org.apache.tomcat', version: '9.0.54') {
|
||||
entry 'tomcat-util'
|
||||
entry('tomcat-websocket') {
|
||||
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
|
||||
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
|
||||
}
|
||||
}
|
||||
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.53') {
|
||||
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.54') {
|
||||
entry 'tomcat-embed-core'
|
||||
entry 'tomcat-embed-websocket'
|
||||
}
|
||||
@@ -192,7 +192,7 @@ configure(allprojects) { project ->
|
||||
dependency "org.hamcrest:hamcrest:2.1"
|
||||
dependency "org.awaitility:awaitility:3.1.6"
|
||||
dependency "org.assertj:assertj-core:3.21.0"
|
||||
dependencySet(group: 'org.xmlunit', version: '2.8.2') {
|
||||
dependencySet(group: 'org.xmlunit', version: '2.8.3') {
|
||||
entry 'xmlunit-assertj'
|
||||
entry('xmlunit-matchers') {
|
||||
exclude group: "org.hamcrest", name: "hamcrest-core"
|
||||
@@ -206,10 +206,10 @@ configure(allprojects) { project ->
|
||||
}
|
||||
dependency "io.mockk:mockk:1.12.0"
|
||||
|
||||
dependency("net.sourceforge.htmlunit:htmlunit:2.53.0") {
|
||||
dependency("net.sourceforge.htmlunit:htmlunit:2.54.0") {
|
||||
exclude group: "commons-logging", name: "commons-logging"
|
||||
}
|
||||
dependency("org.seleniumhq.selenium:htmlunit-driver:2.53.0") {
|
||||
dependency("org.seleniumhq.selenium:htmlunit-driver:2.54.0") {
|
||||
exclude group: "commons-logging", name: "commons-logging"
|
||||
}
|
||||
dependency("org.seleniumhq.selenium:selenium-java:3.141.59") {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=5.3.12-SNAPSHOT
|
||||
version=5.3.13
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
+7
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -422,10 +422,13 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
}
|
||||
if (editor == null) {
|
||||
// Find editor for superclass or interface.
|
||||
for (Iterator<Class<?>> it = this.customEditors.keySet().iterator(); it.hasNext() && editor == null;) {
|
||||
Class<?> key = it.next();
|
||||
for (Map.Entry<Class<?>, PropertyEditor> entry : this.customEditors.entrySet()) {
|
||||
if (editor != null) {
|
||||
break;
|
||||
}
|
||||
Class<?> key = entry.getKey();
|
||||
if (key.isAssignableFrom(requiredType)) {
|
||||
editor = this.customEditors.get(key);
|
||||
editor = entry.getValue();
|
||||
// Cache editor for search type, to avoid the overhead
|
||||
// of repeated assignable-from checks.
|
||||
if (this.customEditorCache == null) {
|
||||
|
||||
+1
-1
@@ -459,7 +459,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
|
||||
private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {
|
||||
if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
|
||||
return InjectionMetadata.EMPTY;
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -570,7 +570,7 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
|
||||
|
||||
CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps);
|
||||
if (this.dataSource != null) {
|
||||
mergedProps.setProperty(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName());
|
||||
mergedProps.putIfAbsent(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName());
|
||||
}
|
||||
|
||||
// Determine scheduler name across local settings and Quartz properties...
|
||||
|
||||
+57
-24
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.scheduling.quartz;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@@ -30,6 +31,7 @@ import org.quartz.SchedulerContext;
|
||||
import org.quartz.SchedulerFactory;
|
||||
import org.quartz.impl.JobDetailImpl;
|
||||
import org.quartz.impl.SchedulerRepository;
|
||||
import org.quartz.impl.jdbcjobstore.JobStoreTX;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
@@ -40,6 +42,8 @@ import org.springframework.context.support.StaticApplicationContext;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.core.testfixture.EnabledForTestGroups;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
@@ -57,10 +61,10 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;
|
||||
* @author Sam Brannen
|
||||
* @since 20.02.2004
|
||||
*/
|
||||
public class QuartzSupportTests {
|
||||
class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
public void schedulerFactoryBeanWithApplicationContext() throws Exception {
|
||||
void schedulerFactoryBeanWithApplicationContext() throws Exception {
|
||||
TestBean tb = new TestBean("tb", 99);
|
||||
StaticApplicationContext ac = new StaticApplicationContext();
|
||||
|
||||
@@ -97,7 +101,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void schedulerWithTaskExecutor() throws Exception {
|
||||
void schedulerWithTaskExecutor() throws Exception {
|
||||
CountingTaskExecutor taskExecutor = new CountingTaskExecutor();
|
||||
DummyJob.count = 0;
|
||||
|
||||
@@ -130,7 +134,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public void jobDetailWithRunnableInsteadOfJob() {
|
||||
void jobDetailWithRunnableInsteadOfJob() {
|
||||
JobDetailImpl jobDetail = new JobDetailImpl();
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
jobDetail.setJobClass((Class) DummyRunnable.class));
|
||||
@@ -138,7 +142,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void schedulerWithQuartzJobBean() throws Exception {
|
||||
void schedulerWithQuartzJobBean() throws Exception {
|
||||
DummyJob.param = 0;
|
||||
DummyJob.count = 0;
|
||||
|
||||
@@ -171,7 +175,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void schedulerWithSpringBeanJobFactory() throws Exception {
|
||||
void schedulerWithSpringBeanJobFactory() throws Exception {
|
||||
DummyJob.param = 0;
|
||||
DummyJob.count = 0;
|
||||
|
||||
@@ -206,7 +210,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception {
|
||||
void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception {
|
||||
DummyJob.param = 0;
|
||||
DummyJob.count = 0;
|
||||
|
||||
@@ -242,7 +246,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception {
|
||||
void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception {
|
||||
DummyJobBean.param = 0;
|
||||
DummyJobBean.count = 0;
|
||||
|
||||
@@ -276,7 +280,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception {
|
||||
void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception {
|
||||
DummyJob.param = 0;
|
||||
DummyJob.count = 0;
|
||||
|
||||
@@ -294,7 +298,7 @@ public class QuartzSupportTests {
|
||||
}
|
||||
|
||||
@Test // SPR-772
|
||||
public void multipleSchedulers() throws Exception {
|
||||
void multipleSchedulers() throws Exception {
|
||||
try (ClassPathXmlApplicationContext ctx = context("multipleSchedulers.xml")) {
|
||||
Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1");
|
||||
Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2");
|
||||
@@ -305,7 +309,7 @@ public class QuartzSupportTests {
|
||||
}
|
||||
|
||||
@Test // SPR-16884
|
||||
public void multipleSchedulersWithQuartzProperties() throws Exception {
|
||||
void multipleSchedulersWithQuartzProperties() throws Exception {
|
||||
try (ClassPathXmlApplicationContext ctx = context("multipleSchedulersWithQuartzProperties.xml")) {
|
||||
Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1");
|
||||
Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2");
|
||||
@@ -317,12 +321,13 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception {
|
||||
Thread.sleep(3000);
|
||||
void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception {
|
||||
try (ClassPathXmlApplicationContext ctx = context("multipleAnonymousMethodInvokingJobDetailFB.xml")) {
|
||||
QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService");
|
||||
QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService");
|
||||
|
||||
Thread.sleep(400);
|
||||
|
||||
assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0);
|
||||
assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2);
|
||||
assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2);
|
||||
@@ -332,12 +337,13 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@EnabledForTestGroups(LONG_RUNNING)
|
||||
public void schedulerAccessorBean() throws Exception {
|
||||
Thread.sleep(3000);
|
||||
void schedulerAccessorBean() throws Exception {
|
||||
try (ClassPathXmlApplicationContext ctx = context("schedulerAccessorBean.xml")) {
|
||||
QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService");
|
||||
QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService");
|
||||
|
||||
Thread.sleep(400);
|
||||
|
||||
assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0);
|
||||
assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2);
|
||||
assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2);
|
||||
@@ -347,7 +353,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception {
|
||||
void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
context.registerBeanDefinition("scheduler", new RootBeanDefinition(SchedulerFactoryBean.class));
|
||||
Scheduler bean = context.getBean("scheduler", Scheduler.class);
|
||||
@@ -358,7 +364,7 @@ public class QuartzSupportTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
public void schedulerAutoStartupFalse() throws Exception {
|
||||
void schedulerAutoStartupFalse() throws Exception {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class)
|
||||
.addPropertyValue("autoStartup", false).getBeanDefinition();
|
||||
@@ -370,7 +376,7 @@ public class QuartzSupportTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void schedulerRepositoryExposure() throws Exception {
|
||||
void schedulerRepositoryExposure() throws Exception {
|
||||
try (ClassPathXmlApplicationContext ctx = context("schedulerRepositoryExposure.xml")) {
|
||||
assertThat(ctx.getBean("scheduler")).isSameAs(SchedulerRepository.getInstance().lookup("myScheduler"));
|
||||
}
|
||||
@@ -381,7 +387,7 @@ public class QuartzSupportTests {
|
||||
* TODO: Against Quartz 2.2, this test's job doesn't actually execute anymore...
|
||||
*/
|
||||
@Test
|
||||
public void schedulerWithHsqlDataSource() throws Exception {
|
||||
void schedulerWithHsqlDataSource() throws Exception {
|
||||
DummyJob.param = 0;
|
||||
DummyJob.count = 0;
|
||||
|
||||
@@ -396,12 +402,36 @@ public class QuartzSupportTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("resource")
|
||||
void schedulerFactoryBeanWithCustomJobStore() throws Exception {
|
||||
StaticApplicationContext context = new StaticApplicationContext();
|
||||
|
||||
String dbName = "mydb";
|
||||
EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setName(dbName).build();
|
||||
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("org.quartz.jobStore.class", JobStoreTX.class.getName());
|
||||
properties.setProperty("org.quartz.jobStore.dataSource", dbName);
|
||||
|
||||
BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class)
|
||||
.addPropertyValue("autoStartup", false)
|
||||
.addPropertyValue("dataSource", database)
|
||||
.addPropertyValue("quartzProperties", properties)
|
||||
.getBeanDefinition();
|
||||
context.registerBeanDefinition("scheduler", beanDefinition);
|
||||
|
||||
Scheduler scheduler = context.getBean(Scheduler.class);
|
||||
|
||||
assertThat(scheduler.getMetaData().getJobStoreClass()).isEqualTo(JobStoreTX.class);
|
||||
}
|
||||
|
||||
private ClassPathXmlApplicationContext context(String path) {
|
||||
return new ClassPathXmlApplicationContext(path, getClass());
|
||||
}
|
||||
|
||||
|
||||
public static class CountingTaskExecutor implements TaskExecutor {
|
||||
private static class CountingTaskExecutor implements TaskExecutor {
|
||||
|
||||
private int count;
|
||||
|
||||
@@ -413,12 +443,14 @@ public class QuartzSupportTests {
|
||||
}
|
||||
|
||||
|
||||
public static class DummyJob implements Job {
|
||||
private static class DummyJob implements Job {
|
||||
|
||||
private static int param;
|
||||
|
||||
private static int count;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// Must be public
|
||||
public void setParam(int value) {
|
||||
if (param > 0) {
|
||||
throw new IllegalStateException("Param already set");
|
||||
@@ -433,12 +465,13 @@ public class QuartzSupportTests {
|
||||
}
|
||||
|
||||
|
||||
public static class DummyJobBean extends QuartzJobBean {
|
||||
private static class DummyJobBean extends QuartzJobBean {
|
||||
|
||||
private static int param;
|
||||
|
||||
private static int count;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void setParam(int value) {
|
||||
if (param > 0) {
|
||||
throw new IllegalStateException("Param already set");
|
||||
@@ -453,7 +486,7 @@ public class QuartzSupportTests {
|
||||
}
|
||||
|
||||
|
||||
public static class DummyRunnable implements Runnable {
|
||||
private static class DummyRunnable implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@
|
||||
<property name="targetMethod" value="doExport"/>
|
||||
</bean>
|
||||
</property>
|
||||
<property name="repeatInterval" value="1000"/>
|
||||
<property name="repeatInterval" value="200"/>
|
||||
<property name="repeatCount" value="1"/>
|
||||
</bean>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<property name="targetMethod" value="doImport"/>
|
||||
</bean>
|
||||
</property>
|
||||
<property name="repeatInterval" value="1000"/>
|
||||
<property name="repeatInterval" value="200"/>
|
||||
<property name="repeatCount" value="1"/>
|
||||
</bean>
|
||||
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@
|
||||
<property name="targetMethod" value="doExport"/>
|
||||
</bean>
|
||||
</property>
|
||||
<property name="repeatInterval" value="1000"/>
|
||||
<property name="repeatInterval" value="200"/>
|
||||
<property name="repeatCount" value="1"/>
|
||||
</bean>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<property name="targetMethod" value="doImport"/>
|
||||
</bean>
|
||||
</property>
|
||||
<property name="repeatInterval" value="1000"/>
|
||||
<property name="repeatInterval" value="200"/>
|
||||
<property name="repeatCount" value="1"/>
|
||||
</bean>
|
||||
|
||||
|
||||
+2
-2
@@ -343,7 +343,7 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
|
||||
}
|
||||
|
||||
|
||||
private InjectionMetadata findResourceMetadata(String beanName, final Class<?> clazz, @Nullable PropertyValues pvs) {
|
||||
private InjectionMetadata findResourceMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
|
||||
// Fall back to class name as cache key, for backwards compatibility with custom callers.
|
||||
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
|
||||
// Quick check on the concurrent map first, with minimal locking.
|
||||
@@ -363,7 +363,7 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private InjectionMetadata buildResourceMetadata(final Class<?> clazz) {
|
||||
private InjectionMetadata buildResourceMetadata(Class<?> clazz) {
|
||||
if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {
|
||||
return InjectionMetadata.EMPTY;
|
||||
}
|
||||
|
||||
+10
-59
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,13 +16,10 @@
|
||||
|
||||
package org.springframework.context.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
@@ -33,12 +30,7 @@ import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.core.type.filter.AspectJTypeFilter;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
import org.springframework.core.type.filter.RegexPatternTypeFilter;
|
||||
import org.springframework.core.type.filter.TypeFilter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -73,7 +65,7 @@ class ComponentScanAnnotationParser {
|
||||
}
|
||||
|
||||
|
||||
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
|
||||
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
|
||||
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
|
||||
|
||||
@@ -93,13 +85,17 @@ class ComponentScanAnnotationParser {
|
||||
|
||||
scanner.setResourcePattern(componentScan.getString("resourcePattern"));
|
||||
|
||||
for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
|
||||
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
|
||||
for (AnnotationAttributes includeFilterAttributes : componentScan.getAnnotationArray("includeFilters")) {
|
||||
List<TypeFilter> typeFilters = TypeFilterUtils.createTypeFiltersFor(includeFilterAttributes, this.environment,
|
||||
this.resourceLoader, this.registry);
|
||||
for (TypeFilter typeFilter : typeFilters) {
|
||||
scanner.addIncludeFilter(typeFilter);
|
||||
}
|
||||
}
|
||||
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
|
||||
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
|
||||
for (AnnotationAttributes excludeFilterAttributes : componentScan.getAnnotationArray("excludeFilters")) {
|
||||
List<TypeFilter> typeFilters = TypeFilterUtils.createTypeFiltersFor(excludeFilterAttributes, this.environment,
|
||||
this.resourceLoader, this.registry);
|
||||
for (TypeFilter typeFilter : typeFilters) {
|
||||
scanner.addExcludeFilter(typeFilter);
|
||||
}
|
||||
}
|
||||
@@ -132,49 +128,4 @@ class ComponentScanAnnotationParser {
|
||||
return scanner.doScan(StringUtils.toStringArray(basePackages));
|
||||
}
|
||||
|
||||
private List<TypeFilter> typeFiltersFor(AnnotationAttributes filterAttributes) {
|
||||
List<TypeFilter> typeFilters = new ArrayList<>();
|
||||
FilterType filterType = filterAttributes.getEnum("type");
|
||||
|
||||
for (Class<?> filterClass : filterAttributes.getClassArray("classes")) {
|
||||
switch (filterType) {
|
||||
case ANNOTATION:
|
||||
Assert.isAssignable(Annotation.class, filterClass,
|
||||
"@ComponentScan ANNOTATION type filter requires an annotation type");
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<Annotation> annotationType = (Class<Annotation>) filterClass;
|
||||
typeFilters.add(new AnnotationTypeFilter(annotationType));
|
||||
break;
|
||||
case ASSIGNABLE_TYPE:
|
||||
typeFilters.add(new AssignableTypeFilter(filterClass));
|
||||
break;
|
||||
case CUSTOM:
|
||||
Assert.isAssignable(TypeFilter.class, filterClass,
|
||||
"@ComponentScan CUSTOM type filter requires a TypeFilter implementation");
|
||||
|
||||
TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class,
|
||||
this.environment, this.resourceLoader, this.registry);
|
||||
typeFilters.add(filter);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType);
|
||||
}
|
||||
}
|
||||
|
||||
for (String expression : filterAttributes.getStringArray("pattern")) {
|
||||
switch (filterType) {
|
||||
case ASPECTJ:
|
||||
typeFilters.add(new AspectJTypeFilter(expression, this.resourceLoader.getClassLoader()));
|
||||
break;
|
||||
case REGEX:
|
||||
typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression)));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType);
|
||||
}
|
||||
}
|
||||
|
||||
return typeFilters;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -453,8 +453,8 @@ class ConfigurationClassEnhancer {
|
||||
* instance directly. If a FactoryBean instance is fetched through the container via &-dereferencing,
|
||||
* it will not be proxied. This too is aligned with the way XML configuration works.
|
||||
*/
|
||||
private Object enhanceFactoryBean(final Object factoryBean, Class<?> exposedType,
|
||||
final ConfigurableBeanFactory beanFactory, final String beanName) {
|
||||
private Object enhanceFactoryBean(Object factoryBean, Class<?> exposedType,
|
||||
ConfigurableBeanFactory beanFactory, String beanName) {
|
||||
|
||||
try {
|
||||
Class<?> clazz = factoryBean.getClass();
|
||||
@@ -489,8 +489,8 @@ class ConfigurationClassEnhancer {
|
||||
return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName);
|
||||
}
|
||||
|
||||
private Object createInterfaceProxyForFactoryBean(final Object factoryBean, Class<?> interfaceType,
|
||||
final ConfigurableBeanFactory beanFactory, final String beanName) {
|
||||
private Object createInterfaceProxyForFactoryBean(Object factoryBean, Class<?> interfaceType,
|
||||
ConfigurableBeanFactory beanFactory, String beanName) {
|
||||
|
||||
return Proxy.newProxyInstance(
|
||||
factoryBean.getClass().getClassLoader(), new Class<?>[] {interfaceType},
|
||||
@@ -502,8 +502,8 @@ class ConfigurationClassEnhancer {
|
||||
});
|
||||
}
|
||||
|
||||
private Object createCglibProxyForFactoryBean(final Object factoryBean,
|
||||
final ConfigurableBeanFactory beanFactory, final String beanName) {
|
||||
private Object createCglibProxyForFactoryBean(Object factoryBean,
|
||||
ConfigurableBeanFactory beanFactory, String beanName) {
|
||||
|
||||
Enhancer enhancer = new Enhancer();
|
||||
enhancer.setSuperclass(factoryBean.getClass());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -47,6 +47,11 @@ import java.lang.annotation.Target;
|
||||
* or {@link javax.inject.Inject}: In that context, it leads to the creation of a
|
||||
* lazy-resolution proxy for all affected dependencies, as an alternative to using
|
||||
* {@link org.springframework.beans.factory.ObjectFactory} or {@link javax.inject.Provider}.
|
||||
* Please note that such a lazy-resolution proxy will always be injected; if the target
|
||||
* dependency does not exist, you will only be able to find out through an exception on
|
||||
* invocation. As a consequence, such an injection point results in unintuitive behavior
|
||||
* for optional dependencies. For a programmatic equivalent, allowing for lazy references
|
||||
* with more sophistication, consider {@link org.springframework.beans.factory.ObjectProvider}.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.core.type.filter.AspectJTypeFilter;
|
||||
import org.springframework.core.type.filter.AssignableTypeFilter;
|
||||
import org.springframework.core.type.filter.RegexPatternTypeFilter;
|
||||
import org.springframework.core.type.filter.TypeFilter;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Collection of utilities for working with {@link ComponentScan @ComponentScan}
|
||||
* {@linkplain ComponentScan.Filter type filters}.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 5.3.13
|
||||
* @see ComponentScan.Filter
|
||||
* @see org.springframework.core.type.filter.TypeFilter
|
||||
*/
|
||||
public abstract class TypeFilterUtils {
|
||||
|
||||
/**
|
||||
* Create {@linkplain TypeFilter type filters} from the supplied
|
||||
* {@link AnnotationAttributes}, such as those sourced from
|
||||
* {@link ComponentScan#includeFilters()} or {@link ComponentScan#excludeFilters()}.
|
||||
* <p>Each {@link TypeFilter} will be instantiated using an appropriate
|
||||
* constructor, with {@code BeanClassLoaderAware}, {@code BeanFactoryAware},
|
||||
* {@code EnvironmentAware}, and {@code ResourceLoaderAware} contracts
|
||||
* invoked if they are implemented by the type filter.
|
||||
* @param filterAttributes {@code AnnotationAttributes} for a
|
||||
* {@link ComponentScan.Filter @Filter} declaration
|
||||
* @param environment the {@code Environment} to make available to filters
|
||||
* @param resourceLoader the {@code ResourceLoader} to make available to filters
|
||||
* @param registry the {@code BeanDefinitionRegistry} to make available to filters
|
||||
* as a {@link org.springframework.beans.factory.BeanFactory} if applicable
|
||||
* @return a list of instantiated and configured type filters
|
||||
* @see TypeFilter
|
||||
* @see AnnotationTypeFilter
|
||||
* @see AssignableTypeFilter
|
||||
* @see AspectJTypeFilter
|
||||
* @see RegexPatternTypeFilter
|
||||
* @see org.springframework.beans.factory.BeanClassLoaderAware
|
||||
* @see org.springframework.beans.factory.BeanFactoryAware
|
||||
* @see org.springframework.context.EnvironmentAware
|
||||
* @see org.springframework.context.ResourceLoaderAware
|
||||
*/
|
||||
public static List<TypeFilter> createTypeFiltersFor(AnnotationAttributes filterAttributes, Environment environment,
|
||||
ResourceLoader resourceLoader, BeanDefinitionRegistry registry) {
|
||||
|
||||
List<TypeFilter> typeFilters = new ArrayList<>();
|
||||
FilterType filterType = filterAttributes.getEnum("type");
|
||||
|
||||
for (Class<?> filterClass : filterAttributes.getClassArray("classes")) {
|
||||
switch (filterType) {
|
||||
case ANNOTATION:
|
||||
Assert.isAssignable(Annotation.class, filterClass,
|
||||
"@ComponentScan ANNOTATION type filter requires an annotation type");
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<Annotation> annotationType = (Class<Annotation>) filterClass;
|
||||
typeFilters.add(new AnnotationTypeFilter(annotationType));
|
||||
break;
|
||||
case ASSIGNABLE_TYPE:
|
||||
typeFilters.add(new AssignableTypeFilter(filterClass));
|
||||
break;
|
||||
case CUSTOM:
|
||||
Assert.isAssignable(TypeFilter.class, filterClass,
|
||||
"@ComponentScan CUSTOM type filter requires a TypeFilter implementation");
|
||||
TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class,
|
||||
environment, resourceLoader, registry);
|
||||
typeFilters.add(filter);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType);
|
||||
}
|
||||
}
|
||||
|
||||
for (String expression : filterAttributes.getStringArray("pattern")) {
|
||||
switch (filterType) {
|
||||
case ASPECTJ:
|
||||
typeFilters.add(new AspectJTypeFilter(expression, resourceLoader.getClassLoader()));
|
||||
break;
|
||||
case REGEX:
|
||||
typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression)));
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType);
|
||||
}
|
||||
}
|
||||
|
||||
return typeFilters;
|
||||
}
|
||||
|
||||
}
|
||||
+11
-2
@@ -75,7 +75,7 @@ public abstract class CachedExpressionEvaluator {
|
||||
|
||||
/**
|
||||
* Return the {@link Expression} for the specified SpEL value
|
||||
* <p>Parse the expression if it hasn't been already.
|
||||
* <p>{@link #parseExpression(String) Parse the expression} if it hasn't been already.
|
||||
* @param cache the cache to use
|
||||
* @param elementKey the element on which the expression is defined
|
||||
* @param expression the expression to parse
|
||||
@@ -86,12 +86,21 @@ public abstract class CachedExpressionEvaluator {
|
||||
ExpressionKey expressionKey = createKey(elementKey, expression);
|
||||
Expression expr = cache.get(expressionKey);
|
||||
if (expr == null) {
|
||||
expr = getParser().parseExpression(expression);
|
||||
expr = parseExpression(expression);
|
||||
cache.put(expressionKey, expr);
|
||||
}
|
||||
return expr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the specified {@code expression}.
|
||||
* @param expression the expression to parse
|
||||
* @since 5.3.13
|
||||
*/
|
||||
protected Expression parseExpression(String expression) {
|
||||
return getParser().parseExpression(expression);
|
||||
}
|
||||
|
||||
private ExpressionKey createKey(AnnotatedElementKey elementKey, String expression) {
|
||||
return new ExpressionKey(elementKey, expression);
|
||||
}
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ public final class CandidateComponentsIndexLoader {
|
||||
result.add(properties);
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Loaded " + result.size() + "] index(es)");
|
||||
logger.debug("Loaded " + result.size() + " index(es)");
|
||||
}
|
||||
int totalCount = result.stream().mapToInt(Properties::size).sum();
|
||||
return (totalCount > 0 ? new CandidateComponentsIndex(result) : null);
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
+12
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -67,6 +67,17 @@ public class MapAccessorTests {
|
||||
assertThat(ex.getValue(sec,mapGetter)).isEqualTo("bar");
|
||||
assertThat(SpelCompiler.compile(ex)).isTrue();
|
||||
assertThat(ex.getValue(sec,mapGetter)).isEqualTo("bar");
|
||||
|
||||
// basic isWritable
|
||||
ex = sep.parseExpression("foo");
|
||||
assertThat(ex.isWritable(sec,testMap)).isTrue();
|
||||
|
||||
// basic write
|
||||
ex = sep.parseExpression("foo2");
|
||||
ex.setValue(sec, testMap, "bar2");
|
||||
assertThat(ex.getValue(sec,testMap)).isEqualTo("bar2");
|
||||
assertThat(SpelCompiler.compile(ex)).isTrue();
|
||||
assertThat(ex.getValue(sec,testMap)).isEqualTo("bar2");
|
||||
}
|
||||
|
||||
public static class MapGetter {
|
||||
|
||||
+1
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -321,28 +321,6 @@ public class FormattingConversionServiceTests {
|
||||
TypeDescriptor.valueOf(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void registerDefaultValueViaFormatter() {
|
||||
registerDefaultValue(Date.class, new Date());
|
||||
}
|
||||
|
||||
private <T> void registerDefaultValue(Class<T> clazz, final T defaultValue) {
|
||||
formattingService.addFormatterForFieldType(clazz, new Formatter<T>() {
|
||||
@Override
|
||||
public T parse(String text, Locale locale) {
|
||||
return defaultValue;
|
||||
}
|
||||
@Override
|
||||
public String print(T t, Locale locale) {
|
||||
return defaultValue.toString();
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return defaultValue.toString();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void introspectedFormatter() {
|
||||
formattingService.addFormatter(new NumberStyleFormatter("#,#00.0#"));
|
||||
|
||||
@@ -37,7 +37,7 @@ import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Contextual descriptor about a type to convert from or to.
|
||||
* Capable of representing arrays and generic collection types.
|
||||
* <p>Capable of representing arrays and generic collection types.
|
||||
*
|
||||
* @author Keith Donald
|
||||
* @author Andy Clement
|
||||
@@ -345,9 +345,9 @@ public class TypeDescriptor implements Serializable {
|
||||
* from the provided collection or array element.
|
||||
* <p>Narrows the {@link #getElementTypeDescriptor() elementType} property to the class
|
||||
* of the provided collection or array element. For example, if this describes a
|
||||
* {@code java.util.List<java.lang.Number<} and the element argument is an
|
||||
* {@code java.util.List<java.lang.Number>} and the element argument is a
|
||||
* {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer}.
|
||||
* If this describes a {@code java.util.List<?>} and the element argument is an
|
||||
* If this describes a {@code java.util.List<?>} and the element argument is a
|
||||
* {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer}
|
||||
* as well.
|
||||
* <p>Annotation and nested type context will be preserved in the narrowed
|
||||
@@ -388,9 +388,9 @@ public class TypeDescriptor implements Serializable {
|
||||
* from the provided map key.
|
||||
* <p>Narrows the {@link #getMapKeyTypeDescriptor() mapKeyType} property
|
||||
* to the class of the provided map key. For example, if this describes a
|
||||
* {@code java.util.Map<java.lang.Number, java.lang.String<} and the key
|
||||
* {@code java.util.Map<java.lang.Number, java.lang.String>} and the key
|
||||
* argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be
|
||||
* {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>}
|
||||
* {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>}
|
||||
* and the key argument is a {@code java.lang.Integer}, the returned
|
||||
* TypeDescriptor will be {@code java.lang.Integer} as well.
|
||||
* <p>Annotation and nested type context will be preserved in the narrowed
|
||||
@@ -425,9 +425,9 @@ public class TypeDescriptor implements Serializable {
|
||||
* from the provided map value.
|
||||
* <p>Narrows the {@link #getMapValueTypeDescriptor() mapValueType} property
|
||||
* to the class of the provided map value. For example, if this describes a
|
||||
* {@code java.util.Map<java.lang.String, java.lang.Number<} and the value
|
||||
* {@code java.util.Map<java.lang.String, java.lang.Number>} and the value
|
||||
* argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be
|
||||
* {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>}
|
||||
* {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>}
|
||||
* and the value argument is a {@code java.lang.Integer}, the returned
|
||||
* TypeDescriptor will be {@code java.lang.Integer} as well.
|
||||
* <p>Annotation and nested type context will be preserved in the narrowed
|
||||
|
||||
@@ -36,8 +36,9 @@ import org.springframework.lang.Nullable;
|
||||
public abstract class LogFormatUtils {
|
||||
|
||||
/**
|
||||
* Variant of {@link #formatValue(Object, int, boolean)} and a convenience
|
||||
* method that truncates at 100 characters when {@code limitLength} is set.
|
||||
* Convenience variant of {@link #formatValue(Object, int, boolean)} that
|
||||
* limits the length of a log message to 100 characters and also replaces
|
||||
* newline characters if {@code limitLength} is set to "true".
|
||||
* @param value the value to format
|
||||
* @param limitLength whether to truncate the value at a length of 100
|
||||
* @return the formatted value
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -44,7 +44,7 @@ public interface TypeConverter {
|
||||
* Convert (or coerce) a value from one type to another, for example from a
|
||||
* {@code boolean} to a {@code String}.
|
||||
* <p>The {@link TypeDescriptor} parameters enable support for typed collections:
|
||||
* A caller may prefer a {@code List<Integer>}, for example, rather than
|
||||
* A caller may prefer a {@code List<Integer>}, for example, rather than
|
||||
* simply any {@code List}.
|
||||
* @param value the value to be converted
|
||||
* @param sourceType a type descriptor that supplies extra information about the
|
||||
|
||||
+32
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,6 +38,7 @@ import org.springframework.util.MethodInvoker;
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
*/
|
||||
public abstract class ReflectionHelper {
|
||||
@@ -281,25 +282,32 @@ public abstract class ReflectionHelper {
|
||||
arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
|
||||
conversionOccurred |= (argument != arguments[i]);
|
||||
}
|
||||
|
||||
MethodParameter methodParam = MethodParameter.forExecutable(executable, varargsPosition);
|
||||
|
||||
// If the target is varargs and there is just one more argument, then convert it here.
|
||||
if (varargsPosition == arguments.length - 1) {
|
||||
// If the target is varargs and there is just one more argument
|
||||
// then convert it here
|
||||
TypeDescriptor targetType = new TypeDescriptor(methodParam);
|
||||
Object argument = arguments[varargsPosition];
|
||||
TypeDescriptor targetType = new TypeDescriptor(methodParam);
|
||||
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
|
||||
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
|
||||
// Three outcomes of that previous line:
|
||||
// 1) the input argument was already compatible (ie. array of valid type) and nothing was done
|
||||
// 2) the input argument was correct type but not in an array so it was made into an array
|
||||
// 3) the input argument was the wrong type and got converted and put into an array
|
||||
// If the argument type is equal to the varargs element type, there is no need
|
||||
// to convert it or wrap it in an array. For example, using StringToArrayConverter
|
||||
// to convert a String containing a comma would result in the String being split
|
||||
// and repackaged in an array when it should be used as-is.
|
||||
if (!sourceType.equals(targetType.getElementTypeDescriptor())) {
|
||||
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
|
||||
}
|
||||
// Three outcomes of the above if-block:
|
||||
// 1) the input argument was correct type but not wrapped in an array, and nothing was done.
|
||||
// 2) the input argument was already compatible (i.e., array of valid type), and nothing was done.
|
||||
// 3) the input argument was the wrong type and got converted and wrapped in an array.
|
||||
if (argument != arguments[varargsPosition] &&
|
||||
!isFirstEntryInArray(argument, arguments[varargsPosition])) {
|
||||
conversionOccurred = true; // case 3
|
||||
}
|
||||
}
|
||||
// Otherwise, convert remaining arguments to the varargs element type.
|
||||
else {
|
||||
// Convert remaining arguments to the varargs element type
|
||||
TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor();
|
||||
Assert.state(targetType != null, "No element type");
|
||||
for (int i = varargsPosition; i < arguments.length; i++) {
|
||||
@@ -332,8 +340,8 @@ public abstract class ReflectionHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Package up the arguments so that they correctly match what is expected in parameterTypes.
|
||||
* For example, if parameterTypes is {@code (int, String[])} because the second parameter
|
||||
* Package up the arguments so that they correctly match what is expected in requiredParameterTypes.
|
||||
* <p>For example, if requiredParameterTypes is {@code (int, String[])} because the second parameter
|
||||
* was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be
|
||||
* repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types.
|
||||
* @param requiredParameterTypes the types of the parameters for the invocation
|
||||
@@ -350,23 +358,24 @@ public abstract class ReflectionHelper {
|
||||
requiredParameterTypes[parameterCount - 1] !=
|
||||
(args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) {
|
||||
|
||||
int arraySize = 0; // zero size array if nothing to pass as the varargs parameter
|
||||
if (argumentCount >= parameterCount) {
|
||||
arraySize = argumentCount - (parameterCount - 1);
|
||||
}
|
||||
|
||||
// Create an array for the varargs arguments
|
||||
// Create an array for the leading arguments plus the varargs array argument.
|
||||
Object[] newArgs = new Object[parameterCount];
|
||||
// Copy all leading arguments to the new array, omitting the varargs array argument.
|
||||
System.arraycopy(args, 0, newArgs, 0, newArgs.length - 1);
|
||||
|
||||
// Now sort out the final argument, which is the varargs one. Before entering this method,
|
||||
// the arguments should have been converted to the box form of the required type.
|
||||
Class<?> componentType = requiredParameterTypes[parameterCount - 1].getComponentType();
|
||||
Object repackagedArgs = Array.newInstance(componentType, arraySize);
|
||||
for (int i = 0; i < arraySize; i++) {
|
||||
Array.set(repackagedArgs, i, args[parameterCount - 1 + i]);
|
||||
int varargsArraySize = 0; // zero size array if nothing to pass as the varargs parameter
|
||||
if (argumentCount >= parameterCount) {
|
||||
varargsArraySize = argumentCount - (parameterCount - 1);
|
||||
}
|
||||
newArgs[newArgs.length - 1] = repackagedArgs;
|
||||
Class<?> componentType = requiredParameterTypes[parameterCount - 1].getComponentType();
|
||||
Object varargsArray = Array.newInstance(componentType, varargsArraySize);
|
||||
for (int i = 0; i < varargsArraySize; i++) {
|
||||
Array.set(varargsArray, i, args[parameterCount - 1 + i]);
|
||||
}
|
||||
// Finally, add the varargs array to the new arguments array.
|
||||
newArgs[newArgs.length - 1] = varargsArray;
|
||||
return newArgs;
|
||||
}
|
||||
return args;
|
||||
|
||||
+36
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class MethodInvocationTests extends AbstractExpressionTests {
|
||||
|
||||
@@ -233,26 +234,54 @@ public class MethodInvocationTests extends AbstractExpressionTests {
|
||||
|
||||
@Test
|
||||
public void testVarargsInvocation01() {
|
||||
// Calling 'public int aVarargsMethod(String... strings)'
|
||||
//evaluate("aVarargsMethod('a','b','c')", 3, Integer.class);
|
||||
//evaluate("aVarargsMethod('a')", 1, Integer.class);
|
||||
// Calling 'public int aVarargsMethod(String... strings)' - returns number of arguments
|
||||
evaluate("aVarargsMethod('a','b','c')", 3, Integer.class);
|
||||
evaluate("aVarargsMethod('a')", 1, Integer.class);
|
||||
evaluate("aVarargsMethod()", 0, Integer.class);
|
||||
evaluate("aVarargsMethod(1,2,3)", 3, Integer.class); // all need converting to strings
|
||||
evaluate("aVarargsMethod(1)", 1, Integer.class); // needs string conversion
|
||||
evaluate("aVarargsMethod(1,'a',3.0d)", 3, Integer.class); // first and last need conversion
|
||||
// evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class);
|
||||
evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVarargsInvocation02() {
|
||||
// Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int+length_of_strings
|
||||
// Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int + length_of_strings
|
||||
evaluate("aVarargsMethod2(5,'a','b','c')", 8, Integer.class);
|
||||
evaluate("aVarargsMethod2(2,'a')", 3, Integer.class);
|
||||
evaluate("aVarargsMethod2(4)", 4, Integer.class);
|
||||
evaluate("aVarargsMethod2(8,2,3)", 10, Integer.class);
|
||||
evaluate("aVarargsMethod2(9)", 9, Integer.class);
|
||||
evaluate("aVarargsMethod2(2,'a',3.0d)", 4, Integer.class);
|
||||
// evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class);
|
||||
evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVarargsInvocation03() {
|
||||
// Calling 'public int aVarargsMethod3(String str1, String... strings)' - returns all strings concatenated with "-"
|
||||
|
||||
// No conversion necessary
|
||||
evaluate("aVarargsMethod3('x')", "x", String.class);
|
||||
evaluate("aVarargsMethod3('x', 'a')", "x-a", String.class);
|
||||
evaluate("aVarargsMethod3('x', 'a', 'b', 'c')", "x-a-b-c", String.class);
|
||||
|
||||
// Conversion necessary
|
||||
evaluate("aVarargsMethod3(9)", "9", String.class);
|
||||
evaluate("aVarargsMethod3(8,2,3)", "8-2-3", String.class);
|
||||
evaluate("aVarargsMethod3('2','a',3.0d)", "2-a-3.0", String.class);
|
||||
evaluate("aVarargsMethod3('8',new String[]{'a','b','c'})", "8-a-b-c", String.class);
|
||||
|
||||
// Individual string contains a comma with multiple varargs arguments
|
||||
evaluate("aVarargsMethod3('foo', ',', 'baz')", "foo-,-baz", String.class);
|
||||
evaluate("aVarargsMethod3('foo', 'bar', ',baz')", "foo-bar-,baz", String.class);
|
||||
evaluate("aVarargsMethod3('foo', 'bar,', 'baz')", "foo-bar,-baz", String.class);
|
||||
|
||||
// Individual string contains a comma with single varargs argument.
|
||||
// Reproduces https://github.com/spring-projects/spring-framework/issues/27582
|
||||
evaluate("aVarargsMethod3('foo', ',')", "foo-,", String.class);
|
||||
evaluate("aVarargsMethod3('foo', ',bar')", "foo-,bar", String.class);
|
||||
evaluate("aVarargsMethod3('foo', 'bar,')", "foo-bar,", String.class);
|
||||
evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+7
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
* Tests the evaluation of expressions that access variables and functions (lambda/java).
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class VariableAndFunctionTests extends AbstractExpressionTests {
|
||||
|
||||
@@ -58,12 +59,17 @@ public class VariableAndFunctionTests extends AbstractExpressionTests {
|
||||
|
||||
@Test
|
||||
public void testCallVarargsFunction() {
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge('a,b')", "a,b", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge('a', 'b,c', 'd')", "db,ca", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge('a','b','c')", "cba", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge('a')", "a", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge()", "", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge('b',25)", "25b", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge(25)", "25", String.class);
|
||||
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge2(1, 'a,b')", "1a,b", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge2(1,'a','b','c')", "1cba", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge2(1, 'a', 'b,c', 'd')", "1db,ca", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge2(2,'a')", "2a", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge2(3)", "3", String.class);
|
||||
evaluate("#varargsFunctionReverseStringsAndMerge2(4,'b',25)", "425b", String.class);
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +28,7 @@ import org.springframework.util.ObjectUtils;
|
||||
///CLOVER:OFF
|
||||
@SuppressWarnings("unused")
|
||||
public class Inventor {
|
||||
|
||||
private String name;
|
||||
public String _name;
|
||||
public String _name_;
|
||||
@@ -202,8 +203,14 @@ public class Inventor {
|
||||
return strings.length + i;
|
||||
}
|
||||
|
||||
public Inventor(String... strings) {
|
||||
public String aVarargsMethod3(String str1, String... strings) {
|
||||
if (ObjectUtils.isEmpty(strings)) {
|
||||
return str1;
|
||||
}
|
||||
return str1 + "-" + String.join("-", strings);
|
||||
}
|
||||
|
||||
public Inventor(String... strings) {
|
||||
}
|
||||
|
||||
public boolean getSomeProperty() {
|
||||
|
||||
+4
-4
@@ -230,10 +230,10 @@ public abstract class ExtendedEntityManagerCreator {
|
||||
|
||||
if (emIfc != null) {
|
||||
interfaces = cachedEntityManagerInterfaces.computeIfAbsent(emIfc, key -> {
|
||||
Set<Class<?>> ifcs = new LinkedHashSet<>(4);
|
||||
ifcs.add(key);
|
||||
ifcs.add(EntityManagerProxy.class);
|
||||
return ClassUtils.toClassArray(ifcs);
|
||||
if (EntityManagerProxy.class.equals(key)) {
|
||||
return new Class<?>[] {key};
|
||||
}
|
||||
return new Class<?>[] {key, EntityManagerProxy.class};
|
||||
});
|
||||
}
|
||||
else {
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -373,7 +373,7 @@ public class PersistenceAnnotationBeanPostProcessor
|
||||
}
|
||||
|
||||
|
||||
private InjectionMetadata findPersistenceMetadata(String beanName, final Class<?> clazz, @Nullable PropertyValues pvs) {
|
||||
private InjectionMetadata findPersistenceMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
|
||||
// Fall back to class name as cache key, for backwards compatibility with custom callers.
|
||||
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
|
||||
// Quick check on the concurrent map first, with minimal locking.
|
||||
@@ -393,7 +393,7 @@ public class PersistenceAnnotationBeanPostProcessor
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private InjectionMetadata buildPersistenceMetadata(final Class<?> clazz) {
|
||||
private InjectionMetadata buildPersistenceMetadata(Class<?> clazz) {
|
||||
if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(PersistenceContext.class, PersistenceUnit.class))) {
|
||||
return InjectionMetadata.EMPTY;
|
||||
}
|
||||
|
||||
+4
-3
@@ -17,6 +17,7 @@
|
||||
package org.springframework.test.web.reactive.server;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
@@ -210,10 +211,10 @@ public class CookieAssertions {
|
||||
private ResponseCookie getCookie(String name) {
|
||||
ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name);
|
||||
if (cookie == null) {
|
||||
String message = "No cookie with name '" + name + "'";
|
||||
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message));
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
AssertionErrors.fail("No cookie with name '" + name + "'"));
|
||||
}
|
||||
return cookie;
|
||||
return Objects.requireNonNull(cookie);
|
||||
}
|
||||
|
||||
private String getMessage(String cookie) {
|
||||
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.test.web.reactive.server;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.hamcrest.Matcher;
|
||||
@@ -73,7 +74,7 @@ public class HeaderAssertions {
|
||||
String actual = getHeaders().getFirst(headerName);
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
assertTrue("Response does not contain header '" + headerName + "'", actual != null));
|
||||
return assertHeader(headerName, value, Long.parseLong(actual));
|
||||
return assertHeader(headerName, value, Long.parseLong(Objects.requireNonNull(actual)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,7 +116,7 @@ public class HeaderAssertions {
|
||||
/**
|
||||
* Match all values of the response header with the given regex
|
||||
* patterns which are applied to the values of the header in the
|
||||
* same order. Note that the number of pattenrs must match the
|
||||
* same order. Note that the number of patterns must match the
|
||||
* number of actual values.
|
||||
* @param name the header name
|
||||
* @param patterns one or more regex patterns, one per expected value
|
||||
@@ -203,7 +204,7 @@ public class HeaderAssertions {
|
||||
this.exchangeResult.assertWithDiagnostics(() ->
|
||||
AssertionErrors.fail(getMessage(name) + " not found"));
|
||||
}
|
||||
return values;
|
||||
return Objects.requireNonNull(values);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+6
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,7 +38,6 @@ import org.springframework.util.Assert;
|
||||
* PersistenceExceptionTranslator} interface, which are subsequently asked to translate
|
||||
* candidate exceptions.
|
||||
*
|
||||
|
||||
* <p>All of Spring's applicable resource factories (e.g.
|
||||
* {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean})
|
||||
* implement the {@code PersistenceExceptionTranslator} interface out of the box.
|
||||
@@ -47,6 +46,11 @@ import org.springframework.util.Assert;
|
||||
* with the {@code @Repository} annotation, along with defining this post-processor
|
||||
* as a bean in the application context.
|
||||
*
|
||||
* <p>As of 5.3, {@code PersistenceExceptionTranslator} beans will be sorted according
|
||||
* to Spring's dependency ordering rules: see {@link org.springframework.core.Ordered}
|
||||
* and {@link org.springframework.core.annotation.Order}. Note that such beans will
|
||||
* get retrieved from any scope, not just singleton scope, as of this 5.3 revision.
|
||||
*
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
* @since 2.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,12 +23,12 @@ import org.springframework.util.ObjectUtils;
|
||||
/**
|
||||
* Represents an HTTP request or response entity, consisting of headers and body.
|
||||
*
|
||||
* <p>Typically used in combination with the {@link org.springframework.web.client.RestTemplate},
|
||||
* <p>Often used in combination with the {@link org.springframework.web.client.RestTemplate},
|
||||
* like so:
|
||||
* <pre class="code">
|
||||
* HttpHeaders headers = new HttpHeaders();
|
||||
* headers.setContentType(MediaType.TEXT_PLAIN);
|
||||
* HttpEntity<String> entity = new HttpEntity<String>(helloWorld, headers);
|
||||
* HttpEntity<String> entity = new HttpEntity<>("Hello World", headers);
|
||||
* URI location = template.postForLocation("https://example.com", entity);
|
||||
* </pre>
|
||||
* or
|
||||
@@ -39,11 +39,11 @@ import org.springframework.util.ObjectUtils;
|
||||
* </pre>
|
||||
* Can also be used in Spring MVC, as a return value from a @Controller method:
|
||||
* <pre class="code">
|
||||
* @RequestMapping("/handle")
|
||||
* @GetMapping("/handle")
|
||||
* public HttpEntity<String> handle() {
|
||||
* HttpHeaders responseHeaders = new HttpHeaders();
|
||||
* responseHeaders.set("MyResponseHeader", "MyValue");
|
||||
* return new HttpEntity<String>("Hello World", responseHeaders);
|
||||
* return new HttpEntity<>("Hello World", responseHeaders);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
|
||||
|
||||
private int maxInMemorySize = 256 * 1024;
|
||||
|
||||
private int maxHeadersSize = 8 * 1024;
|
||||
private int maxHeadersSize = 10 * 1024;
|
||||
|
||||
private long maxDiskUsagePerPart = -1;
|
||||
|
||||
|
||||
+141
-11
@@ -16,10 +16,15 @@
|
||||
|
||||
package org.springframework.http.codec.multipart;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Scheduler;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
@@ -50,17 +55,40 @@ abstract class DefaultParts {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Part} or {@link FilePart} with the given parameters.
|
||||
* Create a new {@link Part} or {@link FilePart} based on a flux of data
|
||||
* buffers. Returns {@link FilePart} if the {@code Content-Disposition} of
|
||||
* the given headers contains a filename, or a "normal" {@link Part}
|
||||
* otherwise.
|
||||
* @param headers the part headers
|
||||
* @param dataBuffers the content of the part
|
||||
* @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()}
|
||||
*/
|
||||
public static Part part(HttpHeaders headers, Flux<DataBuffer> dataBuffers) {
|
||||
Assert.notNull(headers, "Headers must not be null");
|
||||
Assert.notNull(dataBuffers, "DataBuffers must not be null");
|
||||
|
||||
return partInternal(headers, new FluxContent(dataBuffers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Part} or {@link FilePart} based on the given file.
|
||||
* Returns {@link FilePart} if the {@code Content-Disposition} of the given
|
||||
* headers contains a filename, or a "normal" {@link Part} otherwise
|
||||
* @param headers the part headers
|
||||
* @param content the content of the part
|
||||
* @param file the file
|
||||
* @param scheduler the scheduler used for reading the file
|
||||
* @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()}
|
||||
*/
|
||||
public static Part part(HttpHeaders headers, Flux<DataBuffer> content) {
|
||||
public static Part part(HttpHeaders headers, Path file, Scheduler scheduler) {
|
||||
Assert.notNull(headers, "Headers must not be null");
|
||||
Assert.notNull(content, "Content must not be null");
|
||||
Assert.notNull(file, "File must not be null");
|
||||
Assert.notNull(scheduler, "Scheduler must not be null");
|
||||
|
||||
return partInternal(headers, new FileContent(file, scheduler));
|
||||
}
|
||||
|
||||
|
||||
private static Part partInternal(HttpHeaders headers, Content content) {
|
||||
String filename = headers.getContentDisposition().getFilename();
|
||||
if (filename != null) {
|
||||
return new DefaultFilePart(headers, content);
|
||||
@@ -142,16 +170,22 @@ abstract class DefaultParts {
|
||||
*/
|
||||
private static class DefaultPart extends AbstractPart {
|
||||
|
||||
private final Flux<DataBuffer> content;
|
||||
protected final Content content;
|
||||
|
||||
public DefaultPart(HttpHeaders headers, Flux<DataBuffer> content) {
|
||||
|
||||
public DefaultPart(HttpHeaders headers, Content content) {
|
||||
super(headers);
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> content() {
|
||||
return this.content;
|
||||
return this.content.content();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete() {
|
||||
return this.content.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,9 +205,9 @@ abstract class DefaultParts {
|
||||
/**
|
||||
* Default implementation of {@link FilePart}.
|
||||
*/
|
||||
private static class DefaultFilePart extends DefaultPart implements FilePart {
|
||||
private static final class DefaultFilePart extends DefaultPart implements FilePart {
|
||||
|
||||
public DefaultFilePart(HttpHeaders headers, Flux<DataBuffer> content) {
|
||||
public DefaultFilePart(HttpHeaders headers, Content content) {
|
||||
super(headers, content);
|
||||
}
|
||||
|
||||
@@ -186,7 +220,7 @@ abstract class DefaultParts {
|
||||
|
||||
@Override
|
||||
public Mono<Void> transferTo(Path dest) {
|
||||
return DataBufferUtils.write(content(), dest);
|
||||
return this.content.transferTo(dest);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -195,7 +229,7 @@ abstract class DefaultParts {
|
||||
String name = contentDisposition.getName();
|
||||
String filename = contentDisposition.getFilename();
|
||||
if (name != null) {
|
||||
return "DefaultFilePart{" + name() + " (" + filename + ")}";
|
||||
return "DefaultFilePart{" + name + " (" + filename + ")}";
|
||||
}
|
||||
else {
|
||||
return "DefaultFilePart{(" + filename + ")}";
|
||||
@@ -204,4 +238,100 @@ abstract class DefaultParts {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Part content abstraction.
|
||||
*/
|
||||
private interface Content {
|
||||
|
||||
Flux<DataBuffer> content();
|
||||
|
||||
Mono<Void> transferTo(Path dest);
|
||||
|
||||
Mono<Void> delete();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code Content} implementation based on a flux of data buffers.
|
||||
*/
|
||||
private static final class FluxContent implements Content {
|
||||
|
||||
private final Flux<DataBuffer> content;
|
||||
|
||||
|
||||
public FluxContent(Flux<DataBuffer> content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> content() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> transferTo(Path dest) {
|
||||
return DataBufferUtils.write(this.content, dest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete() {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@code Content} implementation based on a file.
|
||||
*/
|
||||
private static final class FileContent implements Content {
|
||||
|
||||
private final Path file;
|
||||
|
||||
private final Scheduler scheduler;
|
||||
|
||||
|
||||
public FileContent(Path file, Scheduler scheduler) {
|
||||
this.file = file;
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> content() {
|
||||
return DataBufferUtils.readByteChannel(
|
||||
() -> Files.newByteChannel(this.file, StandardOpenOption.READ),
|
||||
DefaultDataBufferFactory.sharedInstance, 1024)
|
||||
.subscribeOn(this.scheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> transferTo(Path dest) {
|
||||
return blockingOperation(() -> Files.copy(this.file, dest, StandardCopyOption.REPLACE_EXISTING));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete() {
|
||||
return blockingOperation(() -> {
|
||||
Files.delete(this.file);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> blockingOperation(Callable<?> callable) {
|
||||
return Mono.<Void>create(sink -> {
|
||||
try {
|
||||
callable.call();
|
||||
sink.success();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
sink.error(ex);
|
||||
}
|
||||
})
|
||||
.subscribeOn(this.scheduler);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
-29
@@ -342,33 +342,23 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
|
||||
|
||||
/**
|
||||
* First checks whether the multipart boundary leading to this state
|
||||
* was the final boundary, or whether {@link #maxHeadersSize} is
|
||||
* exceeded. Then looks for the header-body boundary
|
||||
* ({@code CR LF CR LF}) in the given buffer. If found, convert
|
||||
* all buffers collected so far into a {@link HttpHeaders} object
|
||||
* was the final boundary. Then looks for the header-body boundary
|
||||
* ({@code CR LF CR LF}) in the given buffer. If found, checks whether
|
||||
* the size of all header buffers does not exceed {@link #maxHeadersSize},
|
||||
* converts all buffers collected so far into a {@link HttpHeaders} object
|
||||
* and changes to {@link BodyState}, passing the remainder of the
|
||||
* buffer. If the boundary is not found, the buffer is collected.
|
||||
* buffer. If the boundary is not found, the buffer is collected if
|
||||
* its size does not exceed {@link #maxHeadersSize}.
|
||||
*/
|
||||
@Override
|
||||
public void onNext(DataBuffer buf) {
|
||||
long prevCount = this.byteCount.get();
|
||||
long count = this.byteCount.addAndGet(buf.readableByteCount());
|
||||
if (prevCount < 2 && count >= 2) {
|
||||
if (isLastBoundary(buf)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Last boundary found in " + buf);
|
||||
}
|
||||
|
||||
if (changeState(this, DisposedState.INSTANCE, buf)) {
|
||||
emitComplete();
|
||||
}
|
||||
return;
|
||||
if (isLastBoundary(buf)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Last boundary found in " + buf);
|
||||
}
|
||||
}
|
||||
else if (count > MultipartParser.this.maxHeadersSize) {
|
||||
|
||||
if (changeState(this, DisposedState.INSTANCE, buf)) {
|
||||
emitError(new DataBufferLimitException("Part headers exceeded the memory usage limit of " +
|
||||
MultipartParser.this.maxHeadersSize + " bytes"));
|
||||
emitComplete();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -377,17 +367,23 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("End of headers found @" + endIdx + " in " + buf);
|
||||
}
|
||||
DataBuffer headerBuf = MultipartUtils.sliceTo(buf, endIdx);
|
||||
this.buffers.add(headerBuf);
|
||||
DataBuffer bodyBuf = MultipartUtils.sliceFrom(buf, endIdx);
|
||||
DataBufferUtils.release(buf);
|
||||
long count = this.byteCount.addAndGet(endIdx);
|
||||
if (belowMaxHeaderSize(count)) {
|
||||
DataBuffer headerBuf = MultipartUtils.sliceTo(buf, endIdx);
|
||||
this.buffers.add(headerBuf);
|
||||
DataBuffer bodyBuf = MultipartUtils.sliceFrom(buf, endIdx);
|
||||
DataBufferUtils.release(buf);
|
||||
|
||||
emitHeaders(parseHeaders());
|
||||
changeState(this, new BodyState(), bodyBuf);
|
||||
emitHeaders(parseHeaders());
|
||||
changeState(this, new BodyState(), bodyBuf);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.buffers.add(buf);
|
||||
requestBuffer();
|
||||
long count = this.byteCount.addAndGet(buf.readableByteCount());
|
||||
if (belowMaxHeaderSize(count)) {
|
||||
this.buffers.add(buf);
|
||||
requestBuffer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,6 +403,21 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
|
||||
buf.getByte(0) == HYPHEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given {@code count} is below or equal to {@link #maxHeadersSize}
|
||||
* and emits a {@link DataBufferLimitException} if not.
|
||||
*/
|
||||
private boolean belowMaxHeaderSize(long count) {
|
||||
if (count <= MultipartParser.this.maxHeadersSize) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
emitError(new DataBufferLimitException("Part headers exceeded the memory usage limit of " +
|
||||
MultipartParser.this.maxHeadersSize + " bytes"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the list of buffers into a {@link HttpHeaders} instance.
|
||||
* Converts the joined buffers into a string using ISO=8859-1, and parses
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.http.codec.multipart;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
@@ -57,4 +58,13 @@ public interface Part {
|
||||
*/
|
||||
Flux<DataBuffer> content();
|
||||
|
||||
/**
|
||||
* Return a mono that, when subscribed to, deletes the underlying storage
|
||||
* for this part.
|
||||
* @since 5.3.13
|
||||
*/
|
||||
default Mono<Void> delete() {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-10
@@ -674,21 +674,12 @@ final class PartGenerator extends BaseSubscriber<MultipartParser.Token> {
|
||||
@Override
|
||||
public void partComplete(boolean finalPart) {
|
||||
MultipartUtils.closeChannel(this.channel);
|
||||
Flux<DataBuffer> content = partContent();
|
||||
emitPart(DefaultParts.part(this.headers, content));
|
||||
emitPart(DefaultParts.part(this.headers, this.file, PartGenerator.this.blockingOperationScheduler));
|
||||
if (finalPart) {
|
||||
emitComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private Flux<DataBuffer> partContent() {
|
||||
return DataBufferUtils
|
||||
.readByteChannel(
|
||||
() -> Files.newByteChannel(this.file, StandardOpenOption.READ),
|
||||
DefaultDataBufferFactory.sharedInstance, 1024)
|
||||
.subscribeOn(PartGenerator.this.blockingOperationScheduler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (this.closeOnDispose) {
|
||||
|
||||
+35
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.http.codec.multipart;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
@@ -40,6 +42,7 @@ import org.synchronoss.cloud.nio.multipart.MultipartUtils;
|
||||
import org.synchronoss.cloud.nio.multipart.NioMultipartParser;
|
||||
import org.synchronoss.cloud.nio.multipart.NioMultipartParserListener;
|
||||
import org.synchronoss.cloud.nio.multipart.PartBodyStreamStorageFactory;
|
||||
import org.synchronoss.cloud.nio.stream.storage.NameAwarePurgableFileInputStream;
|
||||
import org.synchronoss.cloud.nio.stream.storage.StreamStorage;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -497,6 +500,38 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
|
||||
protected StreamStorage getStorage() {
|
||||
return this.storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete() {
|
||||
return Mono.fromRunnable(() -> {
|
||||
File file = getFile();
|
||||
if (file != null) {
|
||||
file.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private File getFile() {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = getStorage().getInputStream();
|
||||
if (inputStream instanceof NameAwarePurgableFileInputStream) {
|
||||
NameAwarePurgableFileInputStream stream = (NameAwarePurgableFileInputStream) inputStream;
|
||||
return stream.getFile();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
}
|
||||
catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+6
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.http.server.reactive;
|
||||
|
||||
import java.util.AbstractSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -36,6 +37,7 @@ import org.springframework.util.MultiValueMap;
|
||||
* {@code MultiValueMap} implementation for wrapping Undertow HTTP headers.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Sam Brannen
|
||||
* @since 5.1.1
|
||||
*/
|
||||
class UndertowHeadersAdapter implements MultiValueMap<String, String> {
|
||||
@@ -131,7 +133,10 @@ class UndertowHeadersAdapter implements MultiValueMap<String, String> {
|
||||
@Nullable
|
||||
public List<String> remove(Object key) {
|
||||
if (key instanceof String) {
|
||||
this.headers.remove((String) key);
|
||||
Collection<String> removed = this.headers.remove((String) key);
|
||||
if (removed != null) {
|
||||
return new ArrayList<>(removed);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -405,16 +405,18 @@ public class UrlPathHelper {
|
||||
* </ul>
|
||||
*/
|
||||
private static String getSanitizedPath(final String path) {
|
||||
int index = path.indexOf("//");
|
||||
if (index >= 0) {
|
||||
StringBuilder sanitized = new StringBuilder(path);
|
||||
while (index != -1) {
|
||||
sanitized.deleteCharAt(index);
|
||||
index = sanitized.indexOf("//", index);
|
||||
}
|
||||
return sanitized.toString();
|
||||
int start = path.indexOf("//");
|
||||
if (start == -1) {
|
||||
return path;
|
||||
}
|
||||
return path;
|
||||
char[] content = path.toCharArray();
|
||||
int slowIndex = start;
|
||||
for (int fastIndex = start + 1; fastIndex < content.length; fastIndex++) {
|
||||
if (content[fastIndex] != '/' || content[slowIndex] != '/') {
|
||||
content[++slowIndex] = content[fastIndex];
|
||||
}
|
||||
}
|
||||
return new String(content, 0, slowIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -532,7 +534,7 @@ public class UrlPathHelper {
|
||||
*/
|
||||
public String getOriginatingQueryString(HttpServletRequest request) {
|
||||
if ((request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE) != null) ||
|
||||
(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null)) {
|
||||
(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null)) {
|
||||
return (String) request.getAttribute(WebUtils.FORWARD_QUERY_STRING_ATTRIBUTE);
|
||||
}
|
||||
else {
|
||||
|
||||
+25
@@ -270,6 +270,31 @@ public class DefaultPartHttpMessageReaderTests {
|
||||
latch.await();
|
||||
}
|
||||
|
||||
// gh-27612
|
||||
@Test
|
||||
public void exceedHeaderLimit() throws InterruptedException {
|
||||
Flux<DataBuffer> body = DataBufferUtils
|
||||
.readByteChannel((new ClassPathResource("files.multipart", getClass()))::readableChannel, bufferFactory, 282);
|
||||
|
||||
MediaType contentType = new MediaType("multipart", "form-data", singletonMap("boundary", "----WebKitFormBoundaryG8fJ50opQOML0oGD"));
|
||||
MockServerHttpRequest request = MockServerHttpRequest.post("/")
|
||||
.contentType(contentType)
|
||||
.body(body);
|
||||
|
||||
DefaultPartHttpMessageReader reader = new DefaultPartHttpMessageReader();
|
||||
|
||||
reader.setMaxHeadersSize(230);
|
||||
|
||||
Flux<Part> result = reader.read(forClass(Part.class), request, emptyMap());
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(2);
|
||||
StepVerifier.create(result)
|
||||
.consumeNextWith(part -> testPart(part, null, LOREM_IPSUM, latch))
|
||||
.consumeNextWith(part -> testPart(part, null, MUSPI_MEROL, latch))
|
||||
.verifyComplete();
|
||||
|
||||
latch.await();
|
||||
}
|
||||
|
||||
private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary)
|
||||
throws InterruptedException {
|
||||
|
||||
@@ -232,12 +232,12 @@ class UrlPathHelperTests {
|
||||
request.setContextPath("/SPR-12372");
|
||||
request.setPathInfo(null);
|
||||
request.setServletPath("/foo/bar/");
|
||||
request.setRequestURI("/SPR-12372/foo//bar/");
|
||||
request.setRequestURI("/SPR-12372/foo///bar/");
|
||||
|
||||
assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar/");
|
||||
|
||||
request.setServletPath("/foo/bar/");
|
||||
request.setRequestURI("/SPR-12372/foo/bar//");
|
||||
request.setRequestURI("////SPR-12372/foo/bar//");
|
||||
|
||||
assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar/");
|
||||
|
||||
@@ -246,6 +246,12 @@ class UrlPathHelperTests {
|
||||
request.setRequestURI("/SPR-12372/foo/bar//");
|
||||
|
||||
assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar//");
|
||||
|
||||
// "enhance" case
|
||||
request.setServletPath("/foo/bar//");
|
||||
request.setRequestURI("/SPR-12372////////////////////////foo//////////////////bar////////////////////");
|
||||
|
||||
assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar//");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -3,11 +3,9 @@ Content-Disposition: form-data; name="file2"; filename="a.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer iaculis metus id vestibulum nullam.
|
||||
|
||||
------WebKitFormBoundaryG8fJ50opQOML0oGD
|
||||
Content-Disposition: form-data; name="file2"; filename="b.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
.mallun mulubitsev di sutem silucai regetnI .tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL
|
||||
|
||||
------WebKitFormBoundaryG8fJ50opQOML0oGD--
|
||||
|
||||
+23
-2
@@ -54,6 +54,8 @@ public class ResourceHandlerRegistration {
|
||||
|
||||
private boolean useLastModified = true;
|
||||
|
||||
private boolean optimizeLocations = false;
|
||||
|
||||
@Nullable
|
||||
private Map<String, MediaType> mediaTypes;
|
||||
|
||||
@@ -105,15 +107,33 @@ public class ResourceHandlerRegistration {
|
||||
/**
|
||||
* Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses.
|
||||
* <p>This configuration is set to {@code true} by default.
|
||||
* @param useLastModified whether the "last modified" resource information should be used.
|
||||
* @param useLastModified whether the "last modified" resource information should be used
|
||||
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
|
||||
* @since 5.3
|
||||
* @see ResourceWebHandler#setUseLastModified
|
||||
*/
|
||||
public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) {
|
||||
this.useLastModified = useLastModified;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to optimize the specified locations through an existence check on startup,
|
||||
* filtering non-existing directories upfront so that they do not have to be checked
|
||||
* on every resource access.
|
||||
* <p>The default is {@code false}, for defensiveness against zip files without directory
|
||||
* entries which are unable to expose the existence of a directory upfront. Switch this flag to
|
||||
* {@code true} for optimized access in case of a consistent jar layout with directory entries.
|
||||
* @param optimizeLocations whether to optimize the locations through an existence check on startup
|
||||
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
|
||||
* @since 5.3.13
|
||||
* @see ResourceWebHandler#setOptimizeLocations
|
||||
*/
|
||||
public ResourceHandlerRegistration setOptimizeLocations(boolean optimizeLocations) {
|
||||
this.optimizeLocations = optimizeLocations;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a chain of resource resolvers and transformers to use. This
|
||||
* can be useful, for example, to apply a version strategy to resource URLs.
|
||||
@@ -181,8 +201,8 @@ public class ResourceHandlerRegistration {
|
||||
*/
|
||||
protected ResourceWebHandler getRequestHandler() {
|
||||
ResourceWebHandler handler = new ResourceWebHandler();
|
||||
handler.setLocationValues(this.locationValues);
|
||||
handler.setResourceLoader(this.resourceLoader);
|
||||
handler.setLocationValues(this.locationValues);
|
||||
if (this.resourceChainRegistration != null) {
|
||||
handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
|
||||
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
|
||||
@@ -191,6 +211,7 @@ public class ResourceHandlerRegistration {
|
||||
handler.setCacheControl(this.cacheControl);
|
||||
}
|
||||
handler.setUseLastModified(this.useLastModified);
|
||||
handler.setOptimizeLocations(this.optimizeLocations);
|
||||
if (this.mediaTypes != null) {
|
||||
handler.setMediaTypes(this.mediaTypes);
|
||||
}
|
||||
|
||||
+1
-7
@@ -195,13 +195,7 @@ class DefaultClientResponse implements ClientResponse {
|
||||
|
||||
@Override
|
||||
public Mono<WebClientResponseException> createException() {
|
||||
return DataBufferUtils.join(body(BodyExtractors.toDataBuffers()))
|
||||
.map(dataBuffer -> {
|
||||
byte[] bytes = new byte[dataBuffer.readableByteCount()];
|
||||
dataBuffer.read(bytes);
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
return bytes;
|
||||
})
|
||||
return bodyToMono(byte[].class)
|
||||
.defaultIfEmpty(EMPTY)
|
||||
.onErrorReturn(ex -> !(ex instanceof Error), EMPTY)
|
||||
.map(bodyBytes -> {
|
||||
|
||||
+75
-47
@@ -72,8 +72,8 @@ import org.springframework.web.server.WebHandler;
|
||||
* <p>This request handler may also be configured with a
|
||||
* {@link #setResourceResolvers(List) resourcesResolver} and
|
||||
* {@link #setResourceTransformers(List) resourceTransformer} chains to support
|
||||
* arbitrary resolution and transformation of resources being served. By default a
|
||||
* {@link PathResourceResolver} simply finds resources based on the configured
|
||||
* arbitrary resolution and transformation of resources being served. By default
|
||||
* a {@link PathResourceResolver} simply finds resources based on the configured
|
||||
* "locations". An application can configure additional resolvers and
|
||||
* transformers such as the {@link VersionResourceResolver} which can resolve
|
||||
* and prepare URLs for resources with a version in the URL.
|
||||
@@ -85,6 +85,7 @@ import org.springframework.web.server.WebHandler;
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
*/
|
||||
public class ResourceWebHandler implements WebHandler, InitializingBean {
|
||||
@@ -94,6 +95,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
|
||||
private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);
|
||||
|
||||
|
||||
@Nullable
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
private final List<String> locationValues = new ArrayList<>(4);
|
||||
|
||||
private final List<Resource> locationResources = new ArrayList<>(4);
|
||||
@@ -119,11 +123,18 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
|
||||
@Nullable
|
||||
private Map<String, MediaType> mediaTypes;
|
||||
|
||||
@Nullable
|
||||
private ResourceLoader resourceLoader;
|
||||
|
||||
private boolean useLastModified = true;
|
||||
|
||||
private boolean optimizeLocations = false;
|
||||
|
||||
|
||||
/**
|
||||
* Provide the ResourceLoader to load {@link #setLocationValues location values} with.
|
||||
* @since 5.1
|
||||
*/
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a list of String-based location values to be resolved into
|
||||
@@ -161,9 +172,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
|
||||
* <p>Note that if {@link #setLocationValues(List) locationValues} are provided,
|
||||
* instead of loaded Resource-based locations, this method will return
|
||||
* empty until after initialization via {@link #afterPropertiesSet()}.
|
||||
* <p><strong>Note:</strong> As of 5.3.11 the list of locations is filtered
|
||||
* to exclude those that don't actually exist and therefore the list returned
|
||||
* from this method may be a subset of all given locations.
|
||||
* <p><strong>Note:</strong> As of 5.3.11 the list of locations may be filtered to
|
||||
* exclude those that don't actually exist and therefore the list returned from this
|
||||
* method may be a subset of all given locations. See {@link #setOptimizeLocations}.
|
||||
* @see #setLocationValues
|
||||
* @see #setLocations
|
||||
*/
|
||||
@@ -212,6 +223,22 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
|
||||
return this.resourceTransformers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ResourceHttpMessageWriter} to use.
|
||||
* <p>By default a {@link ResourceHttpMessageWriter} will be configured.
|
||||
*/
|
||||
public void setResourceHttpMessageWriter(@Nullable ResourceHttpMessageWriter httpMessageWriter) {
|
||||
this.resourceHttpMessageWriter = httpMessageWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured resource message writer.
|
||||
*/
|
||||
@Nullable
|
||||
public ResourceHttpMessageWriter getResourceHttpMessageWriter() {
|
||||
return this.resourceHttpMessageWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link org.springframework.http.CacheControl} instance to build
|
||||
* the Cache-Control HTTP response header.
|
||||
@@ -230,19 +257,48 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link ResourceHttpMessageWriter} to use.
|
||||
* <p>By default a {@link ResourceHttpMessageWriter} will be configured.
|
||||
* Set whether we should look at the {@link Resource#lastModified()}
|
||||
* when serving resources and use this information to drive {@code "Last-Modified"}
|
||||
* HTTP response headers.
|
||||
* <p>This option is enabled by default and should be turned off if the metadata of
|
||||
* the static files should be ignored.
|
||||
* @since 5.3
|
||||
*/
|
||||
public void setResourceHttpMessageWriter(@Nullable ResourceHttpMessageWriter httpMessageWriter) {
|
||||
this.resourceHttpMessageWriter = httpMessageWriter;
|
||||
public void setUseLastModified(boolean useLastModified) {
|
||||
this.useLastModified = useLastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured resource message writer.
|
||||
* Return whether the {@link Resource#lastModified()} information is used
|
||||
* to drive HTTP responses when serving static resources.
|
||||
* @since 5.3
|
||||
*/
|
||||
@Nullable
|
||||
public ResourceHttpMessageWriter getResourceHttpMessageWriter() {
|
||||
return this.resourceHttpMessageWriter;
|
||||
public boolean isUseLastModified() {
|
||||
return this.useLastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to optimize the specified locations through an existence
|
||||
* check on startup, filtering non-existing directories upfront so that
|
||||
* they do not have to be checked on every resource access.
|
||||
* <p>The default is {@code false}, for defensiveness against zip files
|
||||
* without directory entries which are unable to expose the existence of
|
||||
* a directory upfront. Switch this flag to {@code true} for optimized
|
||||
* access in case of a consistent jar layout with directory entries.
|
||||
* @since 5.3.13
|
||||
*/
|
||||
public void setOptimizeLocations(boolean optimizeLocations) {
|
||||
this.optimizeLocations = optimizeLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether to optimize the specified locations through an existence
|
||||
* check on startup, filtering non-existing directories upfront so that
|
||||
* they do not have to be checked on every resource access.
|
||||
* @since 5.3.13
|
||||
*/
|
||||
public boolean isOptimizeLocations() {
|
||||
return this.optimizeLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,36 +325,6 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
|
||||
return (this.mediaTypes != null ? this.mediaTypes : Collections.emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide the ResourceLoader to load {@link #setLocationValues(List)
|
||||
* location values} with.
|
||||
* @since 5.1
|
||||
*/
|
||||
public void setResourceLoader(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the {@link Resource#lastModified()} information is used
|
||||
* to drive HTTP responses when serving static resources.
|
||||
* @since 5.3
|
||||
*/
|
||||
public boolean isUseLastModified() {
|
||||
return this.useLastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether we should look at the {@link Resource#lastModified()}
|
||||
* when serving resources and use this information to drive {@code "Last-Modified"}
|
||||
* HTTP response headers.
|
||||
* <p>This option is enabled by default and should be turned off if the metadata of
|
||||
* the static files should be ignored.
|
||||
* @param useLastModified whether to use the resource last-modified information.
|
||||
* @since 5.3
|
||||
*/
|
||||
public void setUseLastModified(boolean useLastModified) {
|
||||
this.useLastModified = useLastModified;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
@@ -332,7 +358,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
|
||||
}
|
||||
}
|
||||
|
||||
result = result.stream().filter(Resource::exists).collect(Collectors.toList());
|
||||
if (isOptimizeLocations()) {
|
||||
result = result.stream().filter(Resource::exists).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
this.locationsToUse.clear();
|
||||
this.locationsToUse.addAll(result);
|
||||
|
||||
+25
@@ -30,6 +30,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.codec.ByteArrayDecoder;
|
||||
import org.springframework.core.codec.StringDecoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBuffer;
|
||||
@@ -48,6 +49,7 @@ import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
|
||||
@@ -328,6 +330,29 @@ public class DefaultClientResponseTests {
|
||||
assertThat(result.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_PLAIN);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createException() {
|
||||
byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8);
|
||||
DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes));
|
||||
Flux<DataBuffer> body = Flux.just(dataBuffer);
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
given(mockResponse.getStatusCode()).willReturn(HttpStatus.NOT_FOUND);
|
||||
given(mockResponse.getRawStatusCode()).willReturn(HttpStatus.NOT_FOUND.value());
|
||||
given(mockResponse.getBody()).willReturn(body);
|
||||
|
||||
List<HttpMessageReader<?>> messageReaders = Collections.singletonList(
|
||||
new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
|
||||
given(mockExchangeStrategies.messageReaders()).willReturn(messageReaders);
|
||||
|
||||
Mono<WebClientResponseException> resultMono = defaultClientResponse.createException();
|
||||
WebClientResponseException exception = resultMono.block();
|
||||
assertThat(exception.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
assertThat(exception.getMessage()).isEqualTo("404 Not Found");
|
||||
assertThat(exception.getHeaders()).containsExactly(entry("Content-Type",
|
||||
Collections.singletonList("text/plain")));
|
||||
assertThat(exception.getResponseBodyAsByteArray()).isEqualTo(bytes);
|
||||
}
|
||||
|
||||
|
||||
private void mockTextPlainResponse(Flux<DataBuffer> body) {
|
||||
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -133,6 +133,7 @@ public class DefaultWebClientTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
public void contextFromThreadLocal() {
|
||||
WebClient client = this.builder
|
||||
.filter((request, next) ->
|
||||
|
||||
+3
-1
@@ -74,6 +74,7 @@ public class ResourceWebHandlerTests {
|
||||
|
||||
private ResourceWebHandler handler;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setup() throws Exception {
|
||||
List<Resource> locations = new ArrayList<>(2);
|
||||
@@ -253,7 +254,7 @@ public class ResourceWebHandlerTests {
|
||||
assertResponseBody(exchange, "h1 { color:red; }");
|
||||
}
|
||||
|
||||
@Test // gh-27538
|
||||
@Test // gh-27538, gh-27624
|
||||
public void filterNonExistingLocations() throws Exception {
|
||||
List<Resource> inputLocations = Arrays.asList(
|
||||
new ClassPathResource("test/", getClass()),
|
||||
@@ -262,6 +263,7 @@ public class ResourceWebHandlerTests {
|
||||
|
||||
ResourceWebHandler handler = new ResourceWebHandler();
|
||||
handler.setLocations(inputLocations);
|
||||
handler.setOptimizeLocations(true);
|
||||
handler.afterPropertiesSet();
|
||||
|
||||
List<Resource> actual = handler.getLocations();
|
||||
|
||||
+22
-1
@@ -55,6 +55,8 @@ public class ResourceHandlerRegistration {
|
||||
|
||||
private boolean useLastModified = true;
|
||||
|
||||
private boolean optimizeLocations = false;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link ResourceHandlerRegistration} instance.
|
||||
@@ -130,15 +132,33 @@ public class ResourceHandlerRegistration {
|
||||
/**
|
||||
* Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses.
|
||||
* <p>This configuration is set to {@code true} by default.
|
||||
* @param useLastModified whether the "last modified" resource information should be used.
|
||||
* @param useLastModified whether the "last modified" resource information should be used
|
||||
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
|
||||
* @since 5.3
|
||||
* @see ResourceHttpRequestHandler#setUseLastModified
|
||||
*/
|
||||
public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) {
|
||||
this.useLastModified = useLastModified;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether to optimize the specified locations through an existence check on startup,
|
||||
* filtering non-existing directories upfront so that they do not have to be checked
|
||||
* on every resource access.
|
||||
* <p>The default is {@code false}, for defensiveness against zip files without directory
|
||||
* entries which are unable to expose the existence of a directory upfront. Switch this flag to
|
||||
* {@code true} for optimized access in case of a consistent jar layout with directory entries.
|
||||
* @param optimizeLocations whether to optimize the locations through an existence check on startup
|
||||
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
|
||||
* @since 5.3.13
|
||||
* @see ResourceHttpRequestHandler#setOptimizeLocations
|
||||
*/
|
||||
public ResourceHandlerRegistration setOptimizeLocations(boolean optimizeLocations) {
|
||||
this.optimizeLocations = optimizeLocations;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a chain of resource resolvers and transformers to use. This
|
||||
* can be useful, for example, to apply a version strategy to resource URLs.
|
||||
@@ -204,6 +224,7 @@ public class ResourceHandlerRegistration {
|
||||
handler.setCacheSeconds(this.cachePeriod);
|
||||
}
|
||||
handler.setUseLastModified(this.useLastModified);
|
||||
handler.setOptimizeLocations(this.optimizeLocations);
|
||||
return handler;
|
||||
}
|
||||
|
||||
|
||||
+53
-24
@@ -140,11 +140,13 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
@Nullable
|
||||
private UrlPathHelper urlPathHelper;
|
||||
|
||||
private boolean useLastModified = true;
|
||||
|
||||
private boolean optimizeLocations = false;
|
||||
|
||||
@Nullable
|
||||
private StringValueResolver embeddedValueResolver;
|
||||
|
||||
private boolean useLastModified = true;
|
||||
|
||||
|
||||
public ResourceHttpRequestHandler() {
|
||||
super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
|
||||
@@ -185,13 +187,13 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
/**
|
||||
* Return the configured {@code List} of {@code Resource} locations including
|
||||
* both String-based locations provided via
|
||||
* {@link #setLocationValues(List) setLocationValues} and pre-resolved {@code Resource}
|
||||
* locations provided via {@link #setLocations(List) setLocations}.
|
||||
* {@link #setLocationValues(List) setLocationValues} and pre-resolved
|
||||
* {@code Resource} locations provided via {@link #setLocations(List) setLocations}.
|
||||
* <p>Note that the returned list is fully initialized only after
|
||||
* initialization via {@link #afterPropertiesSet()}.
|
||||
* <p><strong>Note:</strong> As of 5.3.11 the list of locations is filtered
|
||||
* to exclude those that don't actually exist and therefore the list returned
|
||||
* from this method may be a subset of all given locations.
|
||||
* <p><strong>Note:</strong> As of 5.3.11 the list of locations may be filtered to
|
||||
* exclude those that don't actually exist and therefore the list returned from this
|
||||
* method may be a subset of all given locations. See {@link #setOptimizeLocations}.
|
||||
* @see #setLocationValues
|
||||
* @see #setLocations
|
||||
*/
|
||||
@@ -293,7 +295,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
/**
|
||||
* Return the configured content negotiation manager.
|
||||
* @since 4.3
|
||||
* @deprecated as of 5.2.4.
|
||||
* @deprecated as of 5.2.4
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated
|
||||
@@ -303,7 +305,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
|
||||
/**
|
||||
* Add mappings between file extensions, extracted from the filename of a
|
||||
* static {@link Resource}, and corresponding media type to set on the
|
||||
* static {@link Resource}, and corresponding media type to set on the
|
||||
* response.
|
||||
* <p>Use of this method is typically not necessary since mappings are
|
||||
* otherwise determined via
|
||||
@@ -361,9 +363,16 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
return this.urlPathHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEmbeddedValueResolver(StringValueResolver resolver) {
|
||||
this.embeddedValueResolver = resolver;
|
||||
/**
|
||||
* Set whether we should look at the {@link Resource#lastModified()} when
|
||||
* serving resources and use this information to drive {@code "Last-Modified"}
|
||||
* HTTP response headers.
|
||||
* <p>This option is enabled by default and should be turned off if the metadata
|
||||
* of the static files should be ignored.
|
||||
* @since 5.3
|
||||
*/
|
||||
public void setUseLastModified(boolean useLastModified) {
|
||||
this.useLastModified = useLastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,18 +385,35 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether we should look at the {@link Resource#lastModified()}
|
||||
* when serving resources and use this information to drive {@code "Last-Modified"}
|
||||
* HTTP response headers.
|
||||
* <p>This option is enabled by default and should be turned off if the metadata of
|
||||
* the static files should be ignored.
|
||||
* @param useLastModified whether to use the resource last-modified information.
|
||||
* @since 5.3
|
||||
* Set whether to optimize the specified locations through an existence
|
||||
* check on startup, filtering non-existing directories upfront so that
|
||||
* they do not have to be checked on every resource access.
|
||||
* <p>The default is {@code false}, for defensiveness against zip files
|
||||
* without directory entries which are unable to expose the existence of
|
||||
* a directory upfront. Switch this flag to {@code true} for optimized
|
||||
* access in case of a consistent jar layout with directory entries.
|
||||
* @since 5.3.13
|
||||
*/
|
||||
public void setUseLastModified(boolean useLastModified) {
|
||||
this.useLastModified = useLastModified;
|
||||
public void setOptimizeLocations(boolean optimizeLocations) {
|
||||
this.optimizeLocations = optimizeLocations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether to optimize the specified locations through an existence
|
||||
* check on startup, filtering non-existing directories upfront so that
|
||||
* they do not have to be checked on every resource access.
|
||||
* @since 5.3.13
|
||||
*/
|
||||
public boolean isOptimizeLocations() {
|
||||
return this.optimizeLocations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEmbeddedValueResolver(StringValueResolver resolver) {
|
||||
this.embeddedValueResolver = resolver;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
resolveResourceLocations();
|
||||
@@ -449,8 +475,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
if (location.equals("/") && !(resource instanceof ServletContextResource)) {
|
||||
throw new IllegalStateException(
|
||||
"The String-based location \"/\" should be relative to the web application root " +
|
||||
"but resolved to a Resource of type: " + resource.getClass() + ". " +
|
||||
"If this is intentional, please pass it as a pre-configured Resource via setLocations.");
|
||||
"but resolved to a Resource of type: " + resource.getClass() + ". " +
|
||||
"If this is intentional, please pass it as a pre-configured Resource via setLocations.");
|
||||
}
|
||||
result.add(resource);
|
||||
if (charset != null) {
|
||||
@@ -463,7 +489,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
}
|
||||
|
||||
result.addAll(this.locationResources);
|
||||
result = result.stream().filter(Resource::exists).collect(Collectors.toList());
|
||||
if (isOptimizeLocations()) {
|
||||
result = result.stream().filter(Resource::exists).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
this.locationsToUse.clear();
|
||||
this.locationsToUse.addAll(result);
|
||||
@@ -508,6 +536,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes a resource request.
|
||||
* <p>Checks for the existence of the requested resource in the configured list of locations.
|
||||
|
||||
+2
-1
@@ -311,7 +311,7 @@ public class ResourceHttpRequestHandlerTests {
|
||||
assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }");
|
||||
}
|
||||
|
||||
@Test // gh-27538
|
||||
@Test // gh-27538, gh-27624
|
||||
public void filterNonExistingLocations() throws Exception {
|
||||
List<Resource> inputLocations = Arrays.asList(
|
||||
new ClassPathResource("test/", getClass()),
|
||||
@@ -321,6 +321,7 @@ public class ResourceHttpRequestHandlerTests {
|
||||
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
|
||||
handler.setServletContext(new MockServletContext());
|
||||
handler.setLocations(inputLocations);
|
||||
handler.setOptimizeLocations(true);
|
||||
handler.afterPropertiesSet();
|
||||
|
||||
List<Resource> actual = handler.getLocations();
|
||||
|
||||
@@ -3039,7 +3039,7 @@ constructor or setter argument or autowired field) as `ObjectFactory<MyTargetBea
|
||||
allowing for a `getObject()` call to retrieve the current instance on demand every
|
||||
time it is needed -- without holding on to the instance or storing it separately.
|
||||
|
||||
As an extended variant, you may declare `ObjectProvider<MyTargetBean>`, which delivers
|
||||
As an extended variant, you may declare `ObjectProvider<MyTargetBean>` which delivers
|
||||
several additional access variants, including `getIfAvailable` and `getIfUnique`.
|
||||
|
||||
The JSR-330 variant of this is called `Provider` and is used with a `Provider<MyTargetBean>`
|
||||
@@ -6675,9 +6675,11 @@ factory method and other bean definition properties, such as a qualifier value t
|
||||
the `@Qualifier` annotation. Other method-level annotations that can be specified are
|
||||
`@Scope`, `@Lazy`, and custom qualifier annotations.
|
||||
|
||||
TIP: In addition to its role for component initialization, you can also place the `@Lazy` annotation
|
||||
on injection points marked with `@Autowired` or `@Inject`. In this context, it
|
||||
leads to the injection of a lazy-resolution proxy.
|
||||
TIP: In addition to its role for component initialization, you can also place the `@Lazy`
|
||||
annotation on injection points marked with `@Autowired` or `@Inject`. In this context,
|
||||
it leads to the injection of a lazy-resolution proxy. However, such a proxy approach
|
||||
is rather limited. For sophisticated lazy interactions, in particular in combination
|
||||
with optional dependencies, we recommend `ObjectProvider<MyTargetBean>` instead.
|
||||
|
||||
Autowired fields and methods are supported, as previously discussed, with additional
|
||||
support for autowiring of `@Bean` methods. The following example shows how to do so:
|
||||
|
||||
@@ -89,7 +89,7 @@ modified copy as follows:
|
||||
=== MaxInMemorySize
|
||||
|
||||
Codecs have <<web-reactive.adoc#webflux-codecs-limits,limits>> for buffering data in
|
||||
memory to avoid application memory issues. By the default those are set to 256KB.
|
||||
memory to avoid application memory issues. By default those are set to 256KB.
|
||||
If that's not enough you'll get the following error:
|
||||
|
||||
----
|
||||
|
||||
@@ -4170,8 +4170,8 @@ the example:
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/resources/**")
|
||||
.addResourceLocations("/public", "classpath:/static/")
|
||||
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
|
||||
.addResourceLocations("/public", "classpath:/static/")
|
||||
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4259,6 +4259,9 @@ re-write URLs to include the version of the jar and can also match against incom
|
||||
without versions -- for example, from `/jquery/jquery.min.js` to
|
||||
`/jquery/1.2.0/jquery.min.js`.
|
||||
|
||||
TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options
|
||||
for fine-grained control, e.g. last-modified behavior and optimized resource resolution.
|
||||
|
||||
|
||||
|
||||
[[webflux-config-path-matching]]
|
||||
|
||||
@@ -5738,8 +5738,8 @@ The following listing shows how to do so with Java configuration:
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/resources/**")
|
||||
.addResourceLocations("/public", "classpath:/static/")
|
||||
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
|
||||
.addResourceLocations("/public", "classpath:/static/")
|
||||
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -5846,6 +5846,9 @@ re-write URLs to include the version of the jar and can also match against incom
|
||||
without versions -- for example, from `/jquery/jquery.min.js` to
|
||||
`/jquery/1.2.0/jquery.min.js`.
|
||||
|
||||
TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options
|
||||
for fine-grained control, e.g. last-modified behavior and optimized resource resolution.
|
||||
|
||||
|
||||
|
||||
[[mvc-default-servlet-handler]]
|
||||
|
||||
Reference in New Issue
Block a user