diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 5c7c302af..bdcb11ae0 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -21,7 +21,8 @@ jobs: matrix: java: [ '17' , '21' , '25' ] maven-profile-spring-framework: [ - 'spring-framework-6.0' , 'spring-framework-6.1', 'spring-framework-6.2' + 'spring-framework-6.0' , 'spring-framework-6.1', 'spring-framework-6.2', + 'spring-framework-7.0' ] steps: - name: Checkout Source diff --git a/README.md b/README.md index 8a028e1bc..ec38478ad 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,8 @@ The easiest way to get started is by adding the Microsphere Spring BOM (Bill of | **Branches** | **Purpose** | **Latest Version** | |--------------|------------------------------------------------|--------------------| -| **0.2.x** | Compatible with Spring Framework 6.0.x - 6.2.x | 0.2.4 | -| **0.1.x** | Compatible with Spring Framework 4.3.x - 5.3.x | 0.1.4 | +| **0.2.x** | Compatible with Spring Framework 6.0.x - 6.2.x | 0.2.5 | +| **0.1.x** | Compatible with Spring Framework 4.3.x - 5.3.x | 0.1.5 | Then add the specific modules you need: diff --git a/microsphere-spring-context/src/main/java/io/microsphere/spring/core/SpringVersion.java b/microsphere-spring-context/src/main/java/io/microsphere/spring/core/SpringVersion.java index f676909b7..14e5a6db0 100644 --- a/microsphere-spring-context/src/main/java/io/microsphere/spring/core/SpringVersion.java +++ b/microsphere-spring-context/src/main/java/io/microsphere/spring/core/SpringVersion.java @@ -156,6 +156,12 @@ public enum SpringVersion { SPRING_6_2_11, + SPRING_6_2_12, + + SPRING_7_0, + + SPRING_7_0_0, + CURRENT(of(org.springframework.core.SpringVersion.getVersion())); private final Version version; diff --git a/microsphere-spring-context/src/main/java/io/microsphere/spring/util/MimeTypeUtils.java b/microsphere-spring-context/src/main/java/io/microsphere/spring/util/MimeTypeUtils.java index 94e1d7913..a3afe2aa6 100644 --- a/microsphere-spring-context/src/main/java/io/microsphere/spring/util/MimeTypeUtils.java +++ b/microsphere-spring-context/src/main/java/io/microsphere/spring/util/MimeTypeUtils.java @@ -21,8 +21,11 @@ import org.springframework.util.MimeType; import java.util.Collection; +import java.util.Comparator; import static io.microsphere.collection.CollectionUtils.isEmpty; +import static io.microsphere.util.StringUtils.EMPTY_STRING; +import static io.microsphere.util.StringUtils.substringAfter; /** * The utility class for MIME Type @@ -101,14 +104,43 @@ public static String getSubtypeSuffix(MimeType one) { return null; } String subtype = one.getSubtype(); - int suffixIndex = subtype.lastIndexOf('+'); - if (suffixIndex != -1 && subtype.length() > suffixIndex) { - return subtype.substring(suffixIndex + 1); + String suffix = substringAfter(subtype, "+"); + return EMPTY_STRING.equals(suffix) ? null : suffix; + } + + /** + * A {@link Comparator} for {@link MimeType} that orders by specificity. + */ + public static class SpecificityComparator implements Comparator { + + @Override + public int compare(T mimeType1, T mimeType2) { + if (mimeType1.isWildcardType() && !mimeType2.isWildcardType()) { // */* < audio/* + return 1; + } else if (mimeType2.isWildcardType() && !mimeType1.isWildcardType()) { // audio/* > */* + return -1; + } else if (!mimeType1.getType().equals(mimeType2.getType())) { // audio/basic == text/html + return 0; + } else { // mediaType1.getType().equals(mediaType2.getType()) + if (mimeType1.isWildcardSubtype() && !mimeType2.isWildcardSubtype()) { // audio/* < audio/basic + return 1; + } else if (mimeType2.isWildcardSubtype() && !mimeType1.isWildcardSubtype()) { // audio/basic > audio/* + return -1; + } else if (!mimeType1.getSubtype().equals(mimeType2.getSubtype())) { // audio/basic == audio/wave + return 0; + } else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) + return compareParameters(mimeType1, mimeType2); + } + } + } + + protected int compareParameters(T mimeType1, T mimeType2) { + int paramsSize1 = mimeType1.getParameters().size(); + int paramsSize2 = mimeType2.getParameters().size(); + return Integer.compare(paramsSize2, paramsSize1); // audio/basic;level=1 < audio/basic } - return null; } private MimeTypeUtils() { } - } diff --git a/microsphere-spring-context/src/test/java/io/microsphere/spring/beans/factory/BeanFactoryUtilsTest.java b/microsphere-spring-context/src/test/java/io/microsphere/spring/beans/factory/BeanFactoryUtilsTest.java index 58845bf98..46546ab53 100644 --- a/microsphere-spring-context/src/test/java/io/microsphere/spring/beans/factory/BeanFactoryUtilsTest.java +++ b/microsphere-spring-context/src/test/java/io/microsphere/spring/beans/factory/BeanFactoryUtilsTest.java @@ -28,6 +28,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Component; @@ -215,6 +216,10 @@ public ObjectProvider getBeanProvider(ResolvableType requiredType) { return null; } + public ObjectProvider getBeanProvider(ParameterizedTypeReference requiredType) { + return null; + } + public boolean containsBean(String name) { return false; } diff --git a/microsphere-spring-context/src/test/java/io/microsphere/spring/core/SpringVersionTest.java b/microsphere-spring-context/src/test/java/io/microsphere/spring/core/SpringVersionTest.java index 6ed83a2c5..719d79688 100644 --- a/microsphere-spring-context/src/test/java/io/microsphere/spring/core/SpringVersionTest.java +++ b/microsphere-spring-context/src/test/java/io/microsphere/spring/core/SpringVersionTest.java @@ -40,6 +40,8 @@ void testVersionRange() { testVersionRange(SpringVersion.SPRING_6_1, 0, 21); // Spring Framework 6.2 -> [6.2.0, 6.2.1] testVersionRange(SpringVersion.SPRING_6_2, 0, 10); + // Spring Framework 7.0 -> [7.0.0, 7.0.0] + testVersionRange(SpringVersion.SPRING_7_0, 0, 0); } private void testVersionRange(SpringVersion baseVersion, int start, int end) { @@ -64,4 +66,4 @@ void testGetVersion() { assertTrue(springVersion.getVersion().eq(version)); } } -} +} \ No newline at end of file diff --git a/microsphere-spring-context/src/test/java/io/microsphere/spring/util/MimeTypeUtilsTest.java b/microsphere-spring-context/src/test/java/io/microsphere/spring/util/MimeTypeUtilsTest.java index e4b855208..ca06eea9f 100644 --- a/microsphere-spring-context/src/test/java/io/microsphere/spring/util/MimeTypeUtilsTest.java +++ b/microsphere-spring-context/src/test/java/io/microsphere/spring/util/MimeTypeUtilsTest.java @@ -16,6 +16,7 @@ */ package io.microsphere.spring.util; +import io.microsphere.spring.util.MimeTypeUtils.SpecificityComparator; import org.junit.jupiter.api.Test; import org.springframework.util.MimeType; @@ -31,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.util.MimeType.valueOf; /** * {@link MimeTypeUtils} Test @@ -78,4 +80,38 @@ void testIsPresentIn() { assertTrue(isPresentIn(APPLICATION_GRAPHQL, asList(APPLICATION_TEXT, APPLICATION_GRAPHQL))); assertTrue(isPresentIn(APPLICATION_GRAPHQL, ofList(APPLICATION_GRAPHQL))); } + + @Test + void testSpecificityComparator() { + SpecificityComparator comparator = new SpecificityComparator<>(); + + MimeType allTypes = valueOf("*/*"); + MimeType audioType = valueOf("audio/*"); + assertEquals(1, comparator.compare(allTypes, audioType)); + assertEquals(0, comparator.compare(allTypes, allTypes)); + assertEquals(-1, comparator.compare(audioType, allTypes)); + assertEquals(0, comparator.compare(audioType, audioType)); + + MimeType audioWildcard = valueOf("audio/*"); + MimeType audioBasic = valueOf("audio/basic"); + MimeType audioWave = valueOf("audio/wave"); + assertEquals(1, comparator.compare(audioWildcard, audioBasic)); + assertEquals(-1, comparator.compare(audioBasic, audioWildcard)); + assertEquals(0, comparator.compare(audioBasic, audioWave)); + + MimeType withParams = valueOf("audio/basic;level=1;charset=utf-8"); + MimeType withoutParams = valueOf("audio/basic"); + assertEquals(-1, comparator.compare(withParams, withoutParams)); + assertEquals(1, comparator.compare(withoutParams, withParams)); + + audioType = valueOf("audio/basic"); + MimeType textType = valueOf("text/html"); + assertEquals(0, comparator.compare(audioType, textType)); + assertEquals(0, comparator.compare(textType, audioType)); + + MimeType withOneParam = MimeType.valueOf("application/json;version=1"); + MimeType withTwoParams = MimeType.valueOf("application/json;version=1;charset=utf-8"); + assertEquals(1, comparator.compare(withOneParam, withTwoParams)); + assertEquals(-1, comparator.compare(withTwoParams, withOneParam)); + } } diff --git a/microsphere-spring-parent/pom.xml b/microsphere-spring-parent/pom.xml index e35a72773..d55909e61 100644 --- a/microsphere-spring-parent/pom.xml +++ b/microsphere-spring-parent/pom.xml @@ -19,7 +19,7 @@ Microsphere Spring Parent - 0.1.6 + 0.1.7 6.1.0 2.2.0 2.18.2 @@ -32,11 +32,19 @@ 1.5.3 3.41.2.2 3.0 - 10.1.46 + 10.1.47 3.9.4 5.9.0 + + + spring-milestone + Spring Portfolio Milestone Repository + https://repo.spring.io/milestone + + + @@ -256,6 +264,15 @@ 2024.0.11 + + + spring-framework-7.0 + + 7.0.0-RC3 + 2025.0.0-RC1 + 6.0.1 + + \ No newline at end of file diff --git a/microsphere-spring-test/pom.xml b/microsphere-spring-test/pom.xml index f8d10fc09..2bcb711f6 100644 --- a/microsphere-spring-test/pom.xml +++ b/microsphere-spring-test/pom.xml @@ -129,38 +129,45 @@ org.apache.tomcat.embed tomcat-embed-core + true org.apache.tomcat tomcat-jasper + true org.apache.tomcat tomcat-tribes + true org.apache.tomcat tomcat-catalina-ha + true org.apache.zookeeper zookeeper + true org.apache.curator curator-recipes + true org.apache.curator curator-test + true diff --git a/microsphere-spring-web/src/main/java/io/microsphere/spring/web/metadata/WebEndpointMapping.java b/microsphere-spring-web/src/main/java/io/microsphere/spring/web/metadata/WebEndpointMapping.java index 222f62332..fc2e20e51 100644 --- a/microsphere-spring-web/src/main/java/io/microsphere/spring/web/metadata/WebEndpointMapping.java +++ b/microsphere-spring-web/src/main/java/io/microsphere/spring/web/metadata/WebEndpointMapping.java @@ -59,6 +59,7 @@ import static java.util.function.Function.identity; import static org.springframework.http.HttpHeaders.ACCEPT; import static org.springframework.http.HttpHeaders.CONTENT_TYPE; +import static org.springframework.http.HttpMethod.values; import static org.springframework.util.ObjectUtils.isEmpty; /** @@ -243,7 +244,7 @@ private Builder(Kind kind) throws IllegalArgumentException { * *

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.endpoint("myServlet");
          *
@@ -267,7 +268,7 @@ public Builder endpoint(@Nonnull E endpoint) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.pattern("/api/users");
          *
@@ -296,7 +297,7 @@ public Builder pattern(@Nonnull String pattern) throws IllegalArgumentExcepti
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint with a list of pattern strings
+         * // For a Servlet endpoint with a list of pattern strings
          * List patternList = Arrays.asList("/api/users", "/api/orders");
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.patterns(patternList, Function.identity());
@@ -323,7 +324,7 @@ public  Builder patterns(Collection values, Function stringF
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.patterns("/api/users", "/api/orders");
          *
@@ -346,7 +347,7 @@ public Builder patterns(@Nonnull String... patterns) throws IllegalArgumentEx
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.patterns(Arrays.asList("/api/users", "/api/orders"));
          *
@@ -372,7 +373,7 @@ public Builder patterns(@Nonnull Collection patterns) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.method(HttpMethod.GET);
          *
@@ -396,7 +397,7 @@ public Builder method(@Nonnull HttpMethod method) throws IllegalArgumentExcep
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.method("GET");
          *
@@ -424,7 +425,7 @@ public Builder method(@Nonnull String method) throws IllegalArgumentException
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint with a list of HttpMethod enums
+         * // For a Servlet endpoint with a list of HttpMethod enums
          * List methodList = Arrays.asList(HttpMethod.GET, HttpMethod.POST);
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.methods(methodList, HttpMethod::name);
@@ -451,7 +452,7 @@ public  Builder methods(Collection values, Function stringFu
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.methods(HttpMethod.GET, HttpMethod.POST);
          *
@@ -476,7 +477,7 @@ public Builder methods(@Nonnull HttpMethod... methods) throws IllegalArgument
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.methods("GET", "POST");
          *
@@ -506,7 +507,7 @@ public Builder methods(@Nonnull Collection methods) throws IllegalArg
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.param("version", "v1");
          *
@@ -531,7 +532,7 @@ public Builder param(@Nonnull String name, @Nullable String value) throws Ill
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.param("version=v1");
          *
@@ -558,7 +559,7 @@ public Builder param(@Nonnull String nameAndValue) throws IllegalArgumentExce
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint with a list of parameter objects
+         * // For a Servlet endpoint with a list of parameter objects
          * List paramList = Arrays.asList(new MyParam("version", "v1"), new MyParam("lang", "en"));
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.params(paramList, p -> p.getName() + "=" + p.getValue());
@@ -585,7 +586,7 @@ public  Builder params(Collection values, Function stringFun
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.params("version=v1", "lang=en");
          *
@@ -610,7 +611,7 @@ public Builder params(@Nullable String... params) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.header("X-API-Version", "v1");
          *
@@ -640,7 +641,7 @@ public  Builder header(@Nonnull String name, @Nullable String value) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.header("X-API-Version:v1");
          *
@@ -668,7 +669,7 @@ public Builder header(@Nonnull String nameAndValue) throws IllegalArgumentExc
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint with a list of header objects
+         * // For a Servlet endpoint with a list of header objects
          * List headerList = Arrays.asList(new MyHeader("X-API-Version", "v1"), new MyHeader("Accept-Language", "en"));
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.headers(headerList, h -> h.getName() + ":" + h.getValue());
@@ -695,7 +696,7 @@ public  Builder headers(Collection values, Function stringFu
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.headers("X-API-Version:v1", "Accept-Language:en");
          *
@@ -720,7 +721,7 @@ public Builder headers(String... headers) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.consume(MediaType.APPLICATION_JSON);
          *
@@ -744,7 +745,7 @@ public Builder consume(MediaType mediaType) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.consume("application/json");
          *
@@ -772,7 +773,7 @@ public Builder consume(String consume) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.consumes(MediaType.APPLICATION_JSON, MediaType.TEXT_XML);
          *
@@ -795,7 +796,7 @@ public Builder consumes(MediaType... mediaTypes) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint with a list of MediaType objects
+         * // For a Servlet endpoint with a list of MediaType objects
          * List mediaTypeList = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_XML);
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.consumes(mediaTypeList, MediaType::toString);
@@ -822,7 +823,7 @@ public  Builder consumes(Collection values, Function stringF
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.consumes("application/json", "text/xml");
          *
@@ -847,7 +848,7 @@ public Builder consumes(String... consumes) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.produce(MediaType.APPLICATION_JSON);
          *
@@ -871,7 +872,7 @@ public Builder produce(MediaType mediaType) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.produce("application/json");
          *
@@ -899,7 +900,7 @@ public Builder produce(String produce) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.produces(MediaType.APPLICATION_JSON, MediaType.TEXT_XML);
          *
@@ -924,7 +925,7 @@ public Builder produces(MediaType... mediaTypes) {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint with a list of MediaType objects
+         * // For a Servlet endpoint with a list of MediaType objects
          * List mediaTypeList = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.TEXT_XML);
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.produces(mediaTypeList, MediaType::toString);
@@ -951,7 +952,7 @@ public  Builder produces(Collection values, Function stringF
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.produces("application/json", "text/xml");
          *
@@ -982,7 +983,7 @@ public Builder negate() {
          *
          * 

Example Usage

*
{@code
-         * // For a servlet endpoint
+         * // For a Servlet endpoint
          * WebEndpointMapping.Builder builder = WebEndpointMapping.servlet("myServlet");
          * builder.source(servletContext);
          *
@@ -1105,10 +1106,13 @@ public WebEndpointMapping build() throws IllegalArgumentException {
             assertNotNull(this.endpoint, () -> "The 'endpoint' must not be null");
 
             assertNotEmpty(this.patterns, () -> "The 'pattern' must not be empty");
-            assertNoNullElements(this.patterns, "Any element of 'patterns' must not be null");
+            assertNoNullElements(this.patterns, () -> "Any element of 'patterns' must not be null");
 
-            assertNotEmpty(this.methods, () -> "The 'methods' must not be empty");
-            assertNoNullElements(this.methods, "Any element of 'methods' must not be null");
+            if (isEmpty(this.methods)) {
+                methods(values());
+            }
+
+            assertNoNullElements(this.methods, () -> "Any element of 'methods' must not be null");
 
             return new WebEndpointMapping(
                     this.kind,
diff --git a/microsphere-spring-web/src/main/java/io/microsphere/spring/web/rule/GenericMediaTypeExpression.java b/microsphere-spring-web/src/main/java/io/microsphere/spring/web/rule/GenericMediaTypeExpression.java
index b32195f98..2e17d18a6 100644
--- a/microsphere-spring-web/src/main/java/io/microsphere/spring/web/rule/GenericMediaTypeExpression.java
+++ b/microsphere-spring-web/src/main/java/io/microsphere/spring/web/rule/GenericMediaTypeExpression.java
@@ -23,7 +23,7 @@
 import java.util.Map;
 import java.util.Map.Entry;
 
-import static org.springframework.http.MediaType.SPECIFICITY_COMPARATOR;
+import static io.microsphere.spring.web.util.MediaTypeUtils.SPECIFICITY_COMPARATOR;
 import static org.springframework.http.MediaType.parseMediaType;
 import static org.springframework.util.StringUtils.hasText;
 
diff --git a/microsphere-spring-web/src/main/java/io/microsphere/spring/web/util/MediaTypeUtils.java b/microsphere-spring-web/src/main/java/io/microsphere/spring/web/util/MediaTypeUtils.java
new file mode 100644
index 000000000..b7f9f6ca1
--- /dev/null
+++ b/microsphere-spring-web/src/main/java/io/microsphere/spring/web/util/MediaTypeUtils.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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 io.microsphere.spring.web.util;
+
+import io.microsphere.spring.util.MimeTypeUtils.SpecificityComparator;
+import org.springframework.http.MediaType;
+
+import java.util.Comparator;
+
+/**
+ * The utility class for {@link MediaType}
+ *
+ * @author Mercy
+ * @see MediaType
+ * @since 1.0.0
+ */
+public abstract class MediaTypeUtils {
+
+    /**
+     * The {@link Comparator} for {@link MediaType}
+     */
+    public static final Comparator SPECIFICITY_COMPARATOR = new SpecificityComparator<>() {
+
+        @Override
+        protected int compareParameters(MediaType mediaType1, MediaType mediaType2) {
+            double quality1 = mediaType1.getQualityValue();
+            double quality2 = mediaType2.getQualityValue();
+            int qualityComparison = Double.compare(quality2, quality1);
+            if (qualityComparison != 0) {
+                return qualityComparison;  // audio/*;q=0.7 < audio/*;q=0.3
+            }
+            return super.compareParameters(mediaType1, mediaType2);
+        }
+    };
+}
diff --git a/microsphere-spring-web/src/test/java/io/microsphere/spring/web/metadata/WebEndpointMappingTest.java b/microsphere-spring-web/src/test/java/io/microsphere/spring/web/metadata/WebEndpointMappingTest.java
index 3658d5a64..c7772a4f9 100644
--- a/microsphere-spring-web/src/test/java/io/microsphere/spring/web/metadata/WebEndpointMappingTest.java
+++ b/microsphere-spring-web/src/test/java/io/microsphere/spring/web/metadata/WebEndpointMappingTest.java
@@ -33,6 +33,7 @@
 import static io.microsphere.io.IOUtils.copyToString;
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.Builder.assertBuilders;
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.Builder.pair;
+import static io.microsphere.spring.web.metadata.WebEndpointMapping.Builder.toStrings;
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.Kind.CUSTOMIZED;
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.Kind.FILTER;
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.Kind.SERVLET;
@@ -62,6 +63,7 @@
 import static org.springframework.http.HttpMethod.GET;
 import static org.springframework.http.HttpMethod.POST;
 import static org.springframework.http.HttpMethod.PUT;
+import static org.springframework.http.HttpMethod.values;
 import static org.springframework.http.MediaType.APPLICATION_JSON;
 import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
 import static org.springframework.http.MediaType.APPLICATION_XML;
@@ -123,7 +125,8 @@ void testBuildWithoutPatterns() {
 
     @Test
     void testBuildWithoutMethods() {
-        assertThrows(IllegalArgumentException.class, servlet().endpoint(this).patterns(TEST_URL_PATTERNS)::build);
+        WebEndpointMapping mapping = servlet().endpoint(this).patterns(TEST_URL_PATTERNS).build();
+        assertArrayEquals(toStrings(values(), HttpMethod::name), mapping.getMethods());
     }
 
     @Test
diff --git a/microsphere-spring-web/src/test/java/io/microsphere/spring/web/util/MediaTypeUtilsTest.java b/microsphere-spring-web/src/test/java/io/microsphere/spring/web/util/MediaTypeUtilsTest.java
new file mode 100644
index 000000000..2c57625b7
--- /dev/null
+++ b/microsphere-spring-web/src/test/java/io/microsphere/spring/web/util/MediaTypeUtilsTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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
+ *
+ *     http://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 io.microsphere.spring.web.util;
+
+
+import org.junit.jupiter.api.Test;
+import org.springframework.http.MediaType;
+
+import static io.microsphere.spring.web.util.MediaTypeUtils.SPECIFICITY_COMPARATOR;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.springframework.http.MediaType.parseMediaType;
+
+/**
+ * {@link MediaTypeUtils} Test
+ *
+ * @author Mercy
+ * @see MediaTypeUtils
+ * @since 1.0.0
+ */
+class MediaTypeUtilsTest {
+
+    @Test
+    void testSPECIFICITY_COMPARATOR() {
+        MediaType audioType07 = parseMediaType("audio/*;q=0.7");
+        MediaType audioType03 = parseMediaType("audio/*;q=0.3");
+        MediaType audioType07A = parseMediaType("audio/*;q=0.7;a=b");
+        assertEquals(-1, SPECIFICITY_COMPARATOR.compare(audioType07, audioType03));
+        assertEquals(1, SPECIFICITY_COMPARATOR.compare(audioType07, audioType07A));
+    }
+}
\ No newline at end of file
diff --git a/microsphere-spring-web/src/test/java/io/microsphere/spring/web/util/WebSourceTest.java b/microsphere-spring-web/src/test/java/io/microsphere/spring/web/util/WebSourceTest.java
index 84149c446..b5f10b79e 100644
--- a/microsphere-spring-web/src/test/java/io/microsphere/spring/web/util/WebSourceTest.java
+++ b/microsphere-spring-web/src/test/java/io/microsphere/spring/web/util/WebSourceTest.java
@@ -20,7 +20,7 @@
 
 import jakarta.servlet.http.Cookie;
 import org.junit.jupiter.api.Test;
-import org.springframework.http.HttpHeaders;
+import org.springframework.util.LinkedMultiValueMap;
 import org.springframework.util.MultiValueMap;
 import org.springframework.web.context.request.NativeWebRequest;
 import org.springframework.web.method.HandlerMethod;
@@ -132,7 +132,7 @@ void testGetValueForPATH_VARIABLEOnNull() {
 
     @Test
     void testGetValueForMATRIX_VARIABLE() {
-        HttpHeaders httpHeaders = new HttpHeaders();
+        MultiValueMap httpHeaders = new LinkedMultiValueMap<>();
         httpHeaders.add("h1", "v1");
         Map> matrixVariables = ofMap(testName, httpHeaders);
         NativeWebRequest request = createWebRequest(r -> {
@@ -146,7 +146,7 @@ void testGetValueForMATRIX_VARIABLEOnNull() {
         NativeWebRequest request = createWebRequest();
         assertNull(MATRIX_VARIABLE.getValue(request, testName));
 
-        HttpHeaders httpHeaders = new HttpHeaders();
+        MultiValueMap httpHeaders = new LinkedMultiValueMap<>();
         Map> matrixVariables = ofMap(testName, httpHeaders);
         REQUEST.setAttribute(request, MATRIX_VARIABLES_ATTRIBUTE, matrixVariables);
         assertNull(MATRIX_VARIABLE.getValue(request, testName));
diff --git a/microsphere-spring-webflux/src/main/java/io/microsphere/spring/webflux/context/request/ServerWebRequest.java b/microsphere-spring-webflux/src/main/java/io/microsphere/spring/webflux/context/request/ServerWebRequest.java
index 89bae8673..019afc397 100644
--- a/microsphere-spring-webflux/src/main/java/io/microsphere/spring/webflux/context/request/ServerWebRequest.java
+++ b/microsphere-spring-webflux/src/main/java/io/microsphere/spring/webflux/context/request/ServerWebRequest.java
@@ -40,8 +40,10 @@
 import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 
 import static io.microsphere.collection.MapUtils.newFixedLinkedHashMap;
+import static io.microsphere.collection.SetUtils.newHashSet;
 import static io.microsphere.logging.LoggerFactory.getLogger;
 import static io.microsphere.spring.web.util.MonoUtils.getValue;
 import static io.microsphere.util.ClassUtils.isAssignableFrom;
@@ -160,7 +162,9 @@ public String[] getHeaderValues(String headerName) {
     @Nonnull
     public Iterator getHeaderNames() {
         HttpHeaders httpHeaders = getRequestHeaders();
-        return httpHeaders.keySet().iterator();
+        Set keys = newHashSet(httpHeaders.size());
+        httpHeaders.forEach((key, values) -> keys.add(key));
+        return keys.iterator();
     }
 
     @Override
diff --git a/microsphere-spring-webflux/src/main/java/io/microsphere/spring/webflux/function/server/RequestPredicateVisitorAdapter.java b/microsphere-spring-webflux/src/main/java/io/microsphere/spring/webflux/function/server/RequestPredicateVisitorAdapter.java
index 6814b8d19..f7d4aee6e 100644
--- a/microsphere-spring-webflux/src/main/java/io/microsphere/spring/webflux/function/server/RequestPredicateVisitorAdapter.java
+++ b/microsphere-spring-webflux/src/main/java/io/microsphere/spring/webflux/function/server/RequestPredicateVisitorAdapter.java
@@ -55,6 +55,14 @@ default void header(String name, String value) {
     default void queryParam(String name, String value) {
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * @since Spring Framework 7.0
+     */
+    default void version(String version) {
+    }
+
     @Override
     default void startAnd() {
     }
diff --git a/microsphere-spring-webflux/src/test/java/io/microsphere/spring/webflux/function/server/RequestPredicateKindTest.java b/microsphere-spring-webflux/src/test/java/io/microsphere/spring/webflux/function/server/RequestPredicateKindTest.java
index 6743d244d..37107860e 100644
--- a/microsphere-spring-webflux/src/test/java/io/microsphere/spring/webflux/function/server/RequestPredicateKindTest.java
+++ b/microsphere-spring-webflux/src/test/java/io/microsphere/spring/webflux/function/server/RequestPredicateKindTest.java
@@ -73,7 +73,6 @@
 import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
 import static org.springframework.http.MediaType.APPLICATION_PDF;
 import static org.springframework.http.MediaType.APPLICATION_PDF_VALUE;
-import static org.springframework.http.server.PathContainer.parsePath;
 import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
 import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
 import static org.springframework.web.reactive.function.server.RequestPredicates.all;
@@ -419,7 +418,7 @@ void testPredicateOnMethodOnInvalidMethod() {
     @Test
     void testPredicateOnPath() {
         ServerRequest request = mockServerRequest();
-        when(request.pathContainer()).thenReturn(parsePath(TEST_ROOT_PATH));
+//        when(request.path()).thenReturn(parsePath(TEST_ROOT_PATH));
         RequestPredicate predicate = PATH.predicate(TEST_ROOT_PATH);
         assertTrue(predicate.test(request));
     }
@@ -494,7 +493,6 @@ void testPredicateOnContentType() {
     void testPredicateOnAnd() {
         ServerRequest request = mockServerRequest();
         when(request.method()).thenReturn(GET);
-        when(request.pathContainer()).thenReturn(parsePath(TEST_ROOT_PATH));
         RequestPredicate predicate = AND.predicate("(GET && /test)");
         assertTrue(predicate.test(request));
     }
@@ -503,7 +501,6 @@ void testPredicateOnAnd() {
     void testPredicateOnOr() {
         ServerRequest request = mockServerRequest();
         when(request.method()).thenReturn(GET);
-        when(request.pathContainer()).thenReturn(parsePath(TEST_EXTENSION_PATH));
         RequestPredicate predicate = OR.predicate("(GET || /test)");
         assertTrue(predicate.test(request));
     }
diff --git a/microsphere-spring-webflux/src/test/java/io/microsphere/spring/webflux/handler/ReversedProxyHandlerMappingTest.java b/microsphere-spring-webflux/src/test/java/io/microsphere/spring/webflux/handler/ReversedProxyHandlerMappingTest.java
index ef740e1a5..463956781 100644
--- a/microsphere-spring-webflux/src/test/java/io/microsphere/spring/webflux/handler/ReversedProxyHandlerMappingTest.java
+++ b/microsphere-spring-webflux/src/test/java/io/microsphere/spring/webflux/handler/ReversedProxyHandlerMappingTest.java
@@ -38,7 +38,6 @@
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.servlet;
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.webflux;
 import static java.lang.String.valueOf;
-import static java.lang.System.currentTimeMillis;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.springframework.http.HttpMethod.GET;
 import static org.springframework.http.MediaType.APPLICATION_JSON;
@@ -115,7 +114,7 @@ protected void testUser() {
     protected void testResponseEntity() {
         this.webTestClient.put()
                 .uri("/test/response-entity")
-                .header(ID_HEADER_NAME, valueOf(currentTimeMillis()))
+                .header(ID_HEADER_NAME, "1")
                 .exchange()
                 .expectBody(String.class)
                 .isEqualTo("OK");
diff --git a/microsphere-spring-webmvc/pom.xml b/microsphere-spring-webmvc/pom.xml
index 737c2011d..ffd8a6989 100644
--- a/microsphere-spring-webmvc/pom.xml
+++ b/microsphere-spring-webmvc/pom.xml
@@ -97,6 +97,13 @@
             test
         
 
+        
+        
+            org.apache.tomcat.embed
+            tomcat-embed-core
+            test
+        
+
         
         
             org.skyscreamer
diff --git a/microsphere-spring-webmvc/src/main/java/io/microsphere/spring/web/servlet/function/RequestPredicateVisitorAdapter.java b/microsphere-spring-webmvc/src/main/java/io/microsphere/spring/web/servlet/function/RequestPredicateVisitorAdapter.java
index feb06b85a..1d90d424c 100644
--- a/microsphere-spring-webmvc/src/main/java/io/microsphere/spring/web/servlet/function/RequestPredicateVisitorAdapter.java
+++ b/microsphere-spring-webmvc/src/main/java/io/microsphere/spring/web/servlet/function/RequestPredicateVisitorAdapter.java
@@ -55,6 +55,14 @@ default void header(String name, String value) {
     default void param(String name, String value) {
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * @since Spring Framework 7.0
+     */
+    default void version(String version) {
+    }
+
     @Override
     default void startAnd() {
     }
diff --git a/microsphere-spring-webmvc/src/test/java/io/microsphere/spring/webmvc/config/ConfigurableContentNegotiationManagerWebMvcConfigurerTest.java b/microsphere-spring-webmvc/src/test/java/io/microsphere/spring/webmvc/config/ConfigurableContentNegotiationManagerWebMvcConfigurerTest.java
index 5c4273f55..4ddb2c51e 100644
--- a/microsphere-spring-webmvc/src/test/java/io/microsphere/spring/webmvc/config/ConfigurableContentNegotiationManagerWebMvcConfigurerTest.java
+++ b/microsphere-spring-webmvc/src/test/java/io/microsphere/spring/webmvc/config/ConfigurableContentNegotiationManagerWebMvcConfigurerTest.java
@@ -33,8 +33,6 @@
 import org.springframework.web.accept.ContentNegotiationStrategy;
 import org.springframework.web.accept.HeaderContentNegotiationStrategy;
 import org.springframework.web.accept.ParameterContentNegotiationStrategy;
-import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
-import org.springframework.web.accept.ServletPathExtensionContentNegotiationStrategy;
 import org.springframework.web.context.request.NativeWebRequest;
 import org.springframework.web.servlet.config.annotation.EnableWebMvc;
 
@@ -96,12 +94,8 @@ class ConfigurableContentNegotiationManagerWebMvcConfigurerTest {
     void testConfigureContentNegotiation() throws HttpMediaTypeNotAcceptableException {
         assertNotNull(this.contentNegotiationManager);
 
-
         List strategies = this.contentNegotiationManager.getStrategies();
-        assertEquals(4, strategies.size());
-
-        PathExtensionContentNegotiationStrategy pathExtensionContentNegotiationStrategy = this.contentNegotiationManager.getStrategy(ServletPathExtensionContentNegotiationStrategy.class);
-        assertNotNull(pathExtensionContentNegotiationStrategy);
+        assertTrue(strategies.size() > 1);
 
         ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = this.contentNegotiationManager.getStrategy(ParameterContentNegotiationStrategy.class);
         assertNotNull(parameterContentNegotiationStrategy);
diff --git a/microsphere-spring-webmvc/src/test/java/io/microsphere/spring/webmvc/handler/ReversedProxyHandlerMappingTest.java b/microsphere-spring-webmvc/src/test/java/io/microsphere/spring/webmvc/handler/ReversedProxyHandlerMappingTest.java
index 9fa225b96..5edc2e342 100644
--- a/microsphere-spring-webmvc/src/test/java/io/microsphere/spring/webmvc/handler/ReversedProxyHandlerMappingTest.java
+++ b/microsphere-spring-webmvc/src/test/java/io/microsphere/spring/webmvc/handler/ReversedProxyHandlerMappingTest.java
@@ -37,7 +37,6 @@
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.ID_HEADER_NAME;
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.servlet;
 import static io.microsphere.spring.web.metadata.WebEndpointMapping.webmvc;
-import static java.lang.System.currentTimeMillis;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.springframework.http.HttpMethod.GET;
 import static org.springframework.http.MediaType.APPLICATION_JSON;
@@ -122,7 +121,7 @@ protected void testUser() throws Exception {
     @Test
     protected void testResponseEntity() throws Exception {
         String pattern = "/test/response-entity";
-        this.mockMvc.perform(put(pattern).header(ID_HEADER_NAME, currentTimeMillis()))
+        this.mockMvc.perform(put(pattern).header(ID_HEADER_NAME, "1"))
                 .andExpect(status().isOk())
                 .andExpect(content().string("OK"));
     }
diff --git a/pom.xml b/pom.xml
index b7ee1d65d..12395a336 100644
--- a/pom.xml
+++ b/pom.xml
@@ -52,7 +52,7 @@
     
 
     
-        0.2.4-SNAPSHOT
+        0.2.5-SNAPSHOT
         17
     
 
@@ -68,28 +68,4 @@
         microsphere-spring-test
     
 
-    
-        
-            ossrh
-            https://s01.oss.sonatype.org/content/repositories/snapshots
-        
-        
-            ossrh
-            https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
-        
-    
-
-    
-        
-            snapshot
-            
-                true
-            
-            
-                false
-            
-            https://s01.oss.sonatype.org/content/repositories/snapshots
-        
-    
-
 
\ No newline at end of file