diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java index bec1b653..02041e60 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2023 the original author or authors. + * Copyright 2013-2024 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. @@ -61,6 +61,12 @@ public class FeignClientProperties { */ private boolean decodeSlash = true; + /** + * If {@code true}, trailing slashes at the end + * of request urls will be removed. + */ + private boolean removeTrailingSlash; + public boolean isDefaultToProperties() { return defaultToProperties; } @@ -93,6 +99,14 @@ public class FeignClientProperties { this.decodeSlash = decodeSlash; } + public boolean isRemoveTrailingSlash() { + return removeTrailingSlash; + } + + public void setRemoveTrailingSlash(boolean removeTrailingSlash) { + this.removeTrailingSlash = removeTrailingSlash; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -103,12 +117,13 @@ public class FeignClientProperties { } FeignClientProperties that = (FeignClientProperties) o; return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig) - && Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash); + && Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash) + && Objects.equals(removeTrailingSlash, that.removeTrailingSlash); } @Override public int hashCode() { - return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash); + return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash, removeTrailingSlash); } /** diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index 3372a0c5..23dfcdfa 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 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. @@ -145,8 +145,7 @@ public class FeignClientsConfiguration { @Bean @ConditionalOnMissingBean public Contract feignContract(ConversionService feignConversionService) { - boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash(); - return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash); + return new SpringMvcContract(parameterProcessors, feignConversionService, feignClientProperties); } @Bean diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java index 9d9d7a8c..2ab0c5d4 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java @@ -41,6 +41,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.cloud.openfeign.CollectionFormat; +import org.springframework.cloud.openfeign.FeignClientProperties; import org.springframework.cloud.openfeign.SpringQueryMap; import org.springframework.cloud.openfeign.annotation.CookieValueParameterProcessor; import org.springframework.cloud.openfeign.annotation.MatrixVariableParameterProcessor; @@ -115,6 +116,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource private final boolean decodeSlash; + private final boolean removeTrailingSlash; + public SpringMvcContract() { this(Collections.emptyList()); } @@ -128,8 +131,32 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource this(annotatedParameterProcessors, conversionService, true); } + /** + * Creates a {@link SpringMvcContract} based on annotatedParameterProcessors, + * conversionService and decodeSlash value. + * @param annotatedParameterProcessors list of {@link AnnotatedParameterProcessor} objects used to resolve parameters + * @param conversionService {@link ConversionService} used for type conversion + * @param decodeSlash indicates whether slashes should be decoded + * @deprecated in favour of {@link SpringMvcContract#SpringMvcContract(List, ConversionService, FeignClientProperties)} + */ + @Deprecated public SpringMvcContract(List annotatedParameterProcessors, ConversionService conversionService, boolean decodeSlash) { + this(annotatedParameterProcessors, conversionService, decodeSlash, false); + } + + /** + * Creates a {@link SpringMvcContract} based on annotatedParameterProcessors, + * conversionService and decodeSlash value. + * @param annotatedParameterProcessors list of {@link AnnotatedParameterProcessor} objects used to resolve parameters + * @param conversionService {@link ConversionService} used for type conversion + * @param decodeSlash indicates whether slashes should be decoded + * @param removeTrailingSlash indicates whether trailing slashes should be removed + * @deprecated in favour of {@link SpringMvcContract#SpringMvcContract(List, ConversionService, FeignClientProperties)} + */ + @Deprecated + public SpringMvcContract(List annotatedParameterProcessors, + ConversionService conversionService, boolean decodeSlash, boolean removeTrailingSlash) { Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null."); Assert.notNull(conversionService, "ConversionService can not be null."); @@ -140,6 +167,14 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource this.conversionService = conversionService; convertingExpanderFactory = new ConvertingExpanderFactory(conversionService); this.decodeSlash = decodeSlash; + this.removeTrailingSlash = removeTrailingSlash; + } + + public SpringMvcContract(List annotatedParameterProcessors, + ConversionService conversionService, FeignClientProperties feignClientProperties) { + this(annotatedParameterProcessors, conversionService, + feignClientProperties == null || feignClientProperties.isDecodeSlash(), + feignClientProperties != null && feignClientProperties.isRemoveTrailingSlash()); } private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) { @@ -229,6 +264,9 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) { pathValue = "/" + pathValue; } + if (removeTrailingSlash && pathValue.endsWith("/")) { + pathValue = pathValue.substring(0, pathValue.length() - 1); + } data.template().uri(pathValue, true); if (data.template().decodeSlash() != decodeSlash) { data.template().decodeSlash(decodeSlash); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java index e9832691..5459dd66 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientsRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * Copyright 2013-2024 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. @@ -90,7 +90,7 @@ class FeignClientsRegistrarTests { } @Test - void removeLastSlashOfUrl() { + void testRemoveTrailingSlashFromUrl() { String url = FeignClientsRegistrar.getUrl("http://localhost/"); assertThat(url).isEqualTo("http://localhost"); }