diff --git a/.artifacts b/.artifacts index 28b0249adb27..1e02cf91f3cb 100644 --- a/.artifacts +++ b/.artifacts @@ -95,6 +95,7 @@ dubbo-spring-boot-actuator-autoconfigure dubbo-spring-boot-actuator-compatible dubbo-spring-boot-autoconfigure dubbo-spring-boot-3-autoconfigure +dubbo-spring-boot-4-autoconfigure dubbo-spring-boot-autoconfigure-compatible dubbo-spring-boot-actuator-autoconfigure-compatible dubbo-spring-boot-compatible @@ -103,6 +104,7 @@ dubbo-tracing-otel-zipkin-spring-boot-starter dubbo-tracing-otel-otlp-spring-boot-starter dubbo-observability-spring-boot-starter dubbo-spring-boot-starter +dubbo-spring-boot-4-starter dubbo-spring-boot-starters dubbo-spring-boot-interceptor dubbo-nacos-spring-boot-starter diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/logger/slf4j/Slf4jLoggerAdapter.java b/dubbo-common/src/main/java/org/apache/dubbo/common/logger/slf4j/Slf4jLoggerAdapter.java index a0e35d1bfd5a..f801e3b257b1 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/logger/slf4j/Slf4jLoggerAdapter.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/logger/slf4j/Slf4jLoggerAdapter.java @@ -27,6 +27,7 @@ public class Slf4jLoggerAdapter implements LoggerAdapter { public static final String NAME = "slf4j"; + private static final String NOP_LOGGER_FACTORY = "org.slf4j.helpers.NOPLoggerFactory"; private Level level; private File file; @@ -82,6 +83,14 @@ public void setFile(File file) { @Override public boolean isConfigured() { + try { + Object loggerFactory = LoggerFactory.getILoggerFactory(); + return loggerFactory != null + && !NOP_LOGGER_FACTORY.equals(loggerFactory.getClass().getName()); + } catch (Throwable ignore) { + // Fallback for unusual SLF4J initialization failures. + } + try { ClassUtils.forName("org.slf4j.impl.StaticLoggerBinder"); return true; diff --git a/dubbo-common/src/test/java/org/apache/dubbo/common/utils/ConcurrentHashMapUtilsTest.java b/dubbo-common/src/test/java/org/apache/dubbo/common/utils/ConcurrentHashMapUtilsTest.java index 45ce4e1b09cd..fd769af189f1 100644 --- a/dubbo-common/src/test/java/org/apache/dubbo/common/utils/ConcurrentHashMapUtilsTest.java +++ b/dubbo-common/src/test/java/org/apache/dubbo/common/utils/ConcurrentHashMapUtilsTest.java @@ -19,7 +19,9 @@ import java.util.concurrent.ConcurrentHashMap; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.DisabledOnJre; +import org.junit.jupiter.api.condition.EnabledOnJre; +import org.junit.jupiter.api.condition.JRE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -39,7 +41,7 @@ public void testComputeIfAbsent() { } @Test - @EnabledForJreRange(max = org.junit.jupiter.api.condition.JRE.JAVA_8) + @EnabledOnJre(JRE.JAVA_8) public void issue11986ForJava8Test() { // https://github.com/apache/dubbo/issues/11986 final ConcurrentHashMap map = new ConcurrentHashMap<>(); @@ -54,7 +56,7 @@ public void issue11986ForJava8Test() { } @Test - @EnabledForJreRange(min = org.junit.jupiter.api.condition.JRE.JAVA_9) + @DisabledOnJre(JRE.JAVA_8) public void issue11986ForJava17Test() { // https://github.com/apache/dubbo/issues/11986 final ConcurrentHashMap map = new ConcurrentHashMap<>(); diff --git a/dubbo-common/src/test/java/org/apache/dubbo/common/utils/NetUtilsTest.java b/dubbo-common/src/test/java/org/apache/dubbo/common/utils/NetUtilsTest.java index 9f093229ab0e..d80c56100875 100644 --- a/dubbo-common/src/test/java/org/apache/dubbo/common/utils/NetUtilsTest.java +++ b/dubbo-common/src/test/java/org/apache/dubbo/common/utils/NetUtilsTest.java @@ -336,8 +336,8 @@ void testMatchIpRangeMatchWhenIpv6Exception() { @Test void testMatchIpRangeMatchWhenIpWrongException() { UnknownHostException thrown = assertThrows( - UnknownHostException.class, () -> NetUtils.matchIpRange("192.168.1.63", "192.168.1.ff", 90)); - assertTrue(thrown.getMessage().contains("192.168.1.ff")); + UnknownHostException.class, () -> NetUtils.matchIpRange("192.168.1.63", "192.168.1.256", 90)); + assertTrue(thrown.getMessage().contains("192.168.1.256")); } @Test diff --git a/dubbo-compatible/src/test/java/org/apache/dubbo/metadata/rest/SpringRestService.java b/dubbo-compatible/src/test/java/org/apache/dubbo/metadata/rest/SpringRestService.java index b475bd53da82..ebe2c5e6fceb 100644 --- a/dubbo-compatible/src/test/java/org/apache/dubbo/metadata/rest/SpringRestService.java +++ b/dubbo-compatible/src/test/java/org/apache/dubbo/metadata/rest/SpringRestService.java @@ -75,7 +75,7 @@ public String form(@RequestParam("f") String form) { } @Override - @PostMapping(value = "/request/body/map", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @PostMapping(value = "/request/body/map", produces = MediaType.APPLICATION_JSON_VALUE) public User requestBodyMap(@RequestBody Map data, @RequestParam("param") String param) { User user = new User(); user.setId(((Integer) data.get("id")).longValue()); @@ -84,7 +84,7 @@ public User requestBodyMap(@RequestBody Map data, @RequestParam( return user; } - @PostMapping(value = "/request/body/user", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) + @PostMapping(value = "/request/body/user", consumes = MediaType.APPLICATION_JSON_VALUE) @Override public Map requestBodyUser(@RequestBody User user) { Map map = new HashMap<>(); diff --git a/dubbo-config/dubbo-config-spring/pom.xml b/dubbo-config/dubbo-config-spring/pom.xml index 363ab56fb821..568424817c78 100644 --- a/dubbo-config/dubbo-config-spring/pom.xml +++ b/dubbo-config/dubbo-config-spring/pom.xml @@ -29,10 +29,56 @@ false 2.7.18 + 9.0.117 + + + + org.springframework.boot + spring-boot + ${spring-boot.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${spring-boot.tomcat.version} + + + + org.apache.dubbo @@ -248,4 +294,21 @@ + + + + spring-boot-4 + + ${spring-boot-4.version} + ${tomcat.version} + + + + jakarta.servlet + jakarta.servlet-api + test + + + + diff --git a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/context/DubboDeployApplicationListener.java b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/context/DubboDeployApplicationListener.java index a52246bf9088..186827d5beb1 100644 --- a/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/context/DubboDeployApplicationListener.java +++ b/dubbo-config/dubbo-config-spring/src/main/java/org/apache/dubbo/config/spring/context/DubboDeployApplicationListener.java @@ -148,7 +148,7 @@ private void publishModuleEvent(DeployState state, Throwable cause) { @Override public void onApplicationEvent(ApplicationContextEvent event) { - if (nullSafeEquals(applicationContext, event.getSource())) { + if (nullSafeEquals(applicationContext, event.getApplicationContext())) { if (event instanceof ContextRefreshedEvent) { onContextRefreshedEvent((ContextRefreshedEvent) event); } else if (event instanceof ContextClosedEvent) { diff --git a/dubbo-demo/dubbo-demo-mcp-server/pom.xml b/dubbo-demo/dubbo-demo-mcp-server/pom.xml index 23dcc288fb87..d0729f0a8868 100644 --- a/dubbo-demo/dubbo-demo-mcp-server/pom.xml +++ b/dubbo-demo/dubbo-demo-mcp-server/pom.xml @@ -137,10 +137,10 @@ - jdk-version-ge-17 - - [17,) - + spring-boot-3 + + ${spring-boot-3.version} + org.apache.dubbo @@ -149,5 +149,18 @@ + + spring-boot-4 + + ${spring-boot-4.version} + + + + org.apache.dubbo + dubbo-spring-boot-4-autoconfigure + ${project.version} + + + diff --git a/dubbo-demo/dubbo-demo-spring-boot-idl/pom.xml b/dubbo-demo/dubbo-demo-spring-boot-idl/pom.xml index 1075b795dcd0..ea281c29672b 100644 --- a/dubbo-demo/dubbo-demo-spring-boot-idl/pom.xml +++ b/dubbo-demo/dubbo-demo-spring-boot-idl/pom.xml @@ -122,4 +122,32 @@ + + + spring-boot-3 + + ${spring-boot-3.version} + + + + org.apache.dubbo + dubbo-spring-boot-3-autoconfigure + ${project.version} + + + + + spring-boot-4 + + ${spring-boot-4.version} + + + + org.apache.dubbo + dubbo-spring-boot-4-autoconfigure + ${project.version} + + + + diff --git a/dubbo-demo/dubbo-demo-spring-boot/dubbo-demo-spring-boot-servlet/pom.xml b/dubbo-demo/dubbo-demo-spring-boot/dubbo-demo-spring-boot-servlet/pom.xml index 9996b76e7c6e..631e938bb930 100644 --- a/dubbo-demo/dubbo-demo-spring-boot/dubbo-demo-spring-boot-servlet/pom.xml +++ b/dubbo-demo/dubbo-demo-spring-boot/dubbo-demo-spring-boot-servlet/pom.xml @@ -122,10 +122,7 @@ - jdk-version-ge-17 - - [17,) - + spring-boot-3 org.apache.dubbo @@ -134,5 +131,15 @@ + + spring-boot-4 + + + org.apache.dubbo + dubbo-spring-boot-4-autoconfigure + ${project.version} + + + diff --git a/dubbo-demo/dubbo-demo-spring-boot/pom.xml b/dubbo-demo/dubbo-demo-spring-boot/pom.xml index 3fdf07d23a0a..f408e31aea4e 100644 --- a/dubbo-demo/dubbo-demo-spring-boot/pom.xml +++ b/dubbo-demo/dubbo-demo-spring-boot/pom.xml @@ -93,8 +93,15 @@ spring-boot-3 - 3.2.1 - 3.2.1 + ${spring-boot-3.version} + ${spring-boot-3.version} + + + + spring-boot-4 + + ${spring-boot-4.version} + ${spring-boot-4.version} diff --git a/dubbo-distribution/dubbo-bom/pom.xml b/dubbo-distribution/dubbo-bom/pom.xml index d6994783575e..3c3225ddc518 100644 --- a/dubbo-distribution/dubbo-bom/pom.xml +++ b/dubbo-distribution/dubbo-bom/pom.xml @@ -498,6 +498,11 @@ dubbo-spring-boot-3-autoconfigure ${project.version} + + org.apache.dubbo + dubbo-spring-boot-4-autoconfigure + ${project.version} + org.apache.dubbo dubbo-spring-boot-compatible @@ -519,6 +524,11 @@ dubbo-spring-boot-starter ${project.version} + + org.apache.dubbo + dubbo-spring-boot-4-starter + ${project.version} + org.apache.dubbo dubbo-spring-boot diff --git a/dubbo-plugin/dubbo-rest-spring/pom.xml b/dubbo-plugin/dubbo-rest-spring/pom.xml index c786df8317b0..c99cc65a6158 100644 --- a/dubbo-plugin/dubbo-rest-spring/pom.xml +++ b/dubbo-plugin/dubbo-rest-spring/pom.xml @@ -26,6 +26,10 @@ dubbo-rest-spring + + src/spring-boot-before-4/java + + org.apache.dubbo @@ -113,10 +117,50 @@ + + org.codehaus.mojo + build-helper-maven-plugin + + + add-spring-boot-sources + + add-source + + generate-sources + + + ${spring-boot.source.dir} + + + + + + + spring-boot-before-4 + + true + + + src/spring-boot-before-4/java + + + + spring-boot-4 + + src/spring-boot-4/java + + + + jakarta.servlet + jakarta.servlet-api + provided + + + rpc-rest-test diff --git a/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java new file mode 100644 index 000000000000..b50932ee393c --- /dev/null +++ b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java @@ -0,0 +1,110 @@ +/* + * 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 org.apache.dubbo.rpc.protocol.tri.rest.support.spring; + +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.remoting.http12.HttpRequest; +import org.apache.dubbo.remoting.http12.HttpResponse; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.RpcException; +import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants; +import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtensionAdapter; +import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestFilter; +import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestFilter.Listener; +import org.apache.dubbo.rpc.protocol.tri.rest.util.RestUtils; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.util.Arrays; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +@Activate(onClass = "org.springframework.web.servlet.HandlerInterceptor") +public final class HandlerInterceptorAdapter implements RestExtensionAdapter { + @Override + public boolean accept(Object extension) { + return extension instanceof HandlerInterceptor; + } + + @Override + public RestFilter adapt(HandlerInterceptor extension) { + return new HandlerInterceptorRestFilter(extension); + } + + private static final class HandlerInterceptorRestFilter implements RestFilter, Listener { + + private final HandlerInterceptor interceptor; + + @Override + public int getPriority() { + return RestUtils.getPriority(interceptor); + } + + @Override + public String[] getPatterns() { + return RestUtils.getPattens(interceptor); + } + + public HandlerInterceptorRestFilter(HandlerInterceptor interceptor) { + this.interceptor = interceptor; + } + + @Override + public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws Exception { + Object handler = request.attribute(RestConstants.HANDLER_ATTRIBUTE); + if (interceptor.preHandle((HttpServletRequest) request, (HttpServletResponse) response, handler)) { + chain.doFilter(request, response); + } + } + + @Override + public void onResponse(Result result, HttpRequest request, HttpResponse response) throws Exception { + if (result.hasException()) { + onError(result.getException(), request, response); + return; + } + Object handler = request.attribute(RestConstants.HANDLER_ATTRIBUTE); + ModelAndView mv = new ModelAndView(); + mv.addObject("result", result); + interceptor.postHandle((HttpServletRequest) request, (HttpServletResponse) response, handler, mv); + } + + @Override + public void onError(Throwable t, HttpRequest request, HttpResponse response) throws Exception { + Object handler = request.attribute(RestConstants.HANDLER_ATTRIBUTE); + Exception ex = t instanceof Exception ? (Exception) t : new RpcException(t); + interceptor.afterCompletion((HttpServletRequest) request, (HttpServletResponse) response, handler, ex); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("RestFilter{interceptor="); + sb.append(interceptor); + int priority = getPriority(); + if (priority != 0) { + sb.append(", priority=").append(priority); + } + String[] patterns = getPatterns(); + if (patterns != null) { + sb.append(", patterns=").append(Arrays.toString(patterns)); + } + return sb.append('}').toString(); + } + } +} diff --git a/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java new file mode 100644 index 000000000000..c9d641cc72fc --- /dev/null +++ b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java @@ -0,0 +1,70 @@ +/* + * 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 org.apache.dubbo.rpc.protocol.tri.rest.support.spring; + +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.remoting.http12.HttpRequest; +import org.apache.dubbo.remoting.http12.HttpResponse; +import org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.util.CollectionUtils; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.context.request.WebRequest; + +@Activate(onClass = "org.springframework.web.context.request.WebRequest") +public class SpringMiscArgumentResolver implements ArgumentResolver { + + private static final Set> SUPPORTED_TYPES = new HashSet<>(); + + static { + SUPPORTED_TYPES.add(WebRequest.class); + SUPPORTED_TYPES.add(NativeWebRequest.class); + SUPPORTED_TYPES.add(HttpEntity.class); + SUPPORTED_TYPES.add(HttpHeaders.class); + } + + @Override + public boolean accept(ParameterMeta parameter) { + return SUPPORTED_TYPES.contains(parameter.getActualType()); + } + + @Override + public Object resolve(ParameterMeta parameter, HttpRequest request, HttpResponse response) { + Class type = parameter.getActualType(); + if (type == WebRequest.class || type == NativeWebRequest.class) { + return new ServletWebRequest((HttpServletRequest) request); + } + if (type == HttpEntity.class) { + return new HttpEntity<>( + CollectionUtils.toMultiValueMap(request.headers().asMap())); + } + if (type == HttpHeaders.class) { + return new HttpHeaders( + CollectionUtils.toMultiValueMap(request.headers().asMap())); + } + return null; + } +} diff --git a/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java new file mode 100644 index 000000000000..fb03ecb85133 --- /dev/null +++ b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java @@ -0,0 +1,195 @@ +/* + * 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 org.apache.dubbo.rpc.protocol.tri.rest.support.spring; + +import org.apache.dubbo.common.extension.Activate; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.Pair; +import org.apache.dubbo.common.utils.StringUtils; +import org.apache.dubbo.remoting.http12.HttpRequest; +import org.apache.dubbo.remoting.http12.HttpResponse; +import org.apache.dubbo.remoting.http12.HttpResult; +import org.apache.dubbo.rpc.Result; +import org.apache.dubbo.rpc.model.FrameworkModel; +import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants; +import org.apache.dubbo.rpc.protocol.tri.rest.RestException; +import org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver; +import org.apache.dubbo.rpc.protocol.tri.rest.argument.CompositeArgumentResolver; +import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestFilter; +import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestFilter.Listener; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.HandlerMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ResponseMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ServiceMeta; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; + +@Activate(order = -10000, onClass = "org.springframework.http.ResponseEntity") +public class SpringResponseRestFilter implements RestFilter, Listener { + + private final ArgumentResolver argumentResolver; + private final Map> cache = CollectionUtils.newConcurrentHashMap(); + + public SpringResponseRestFilter(FrameworkModel frameworkModel) { + argumentResolver = frameworkModel.getOrRegisterBean(CompositeArgumentResolver.class); + } + + @Override + public void onResponse(Result result, HttpRequest request, HttpResponse response) { + if (result.hasException()) { + HandlerMeta handler = request.attribute(RestConstants.HANDLER_ATTRIBUTE); + if (handler == null) { + return; + } + + Throwable t = result.getException(); + Key key = new Key(handler.getMethod().getMethod(), t.getClass()); + cache.computeIfAbsent(key, k -> findSuitableExceptionHandler(handler.getService(), k.type)) + .ifPresent(m -> { + try { + result.setValue(invokeExceptionHandler(m, t, request, response)); + result.setException(null); + } catch (Throwable th) { + result.setException(th); + } + }); + return; + } + + Object value = result.getValue(); + if (value instanceof ResponseEntity) { + ResponseEntity entity = (ResponseEntity) value; + result.setValue(HttpResult.builder() + .body(entity.getBody()) + .status(Helper.getStatusCode(entity)) + .headers(entity.getHeaders().asMultiValueMap()) + .build()); + return; + } + + RequestMapping mapping = request.attribute(RestConstants.MAPPING_ATTRIBUTE); + if (mapping == null) { + return; + } + ResponseMeta responseMeta = mapping.getResponse(); + if (responseMeta == null) { + return; + } + String reason = responseMeta.getReason(); + result.setValue(HttpResult.builder() + .body(reason == null ? result.getValue() : reason) + .status(responseMeta.getStatus()) + .build()); + } + + private Optional findSuitableExceptionHandler(ServiceMeta serviceMeta, Class exType) { + if (serviceMeta.getExceptionHandlers() == null) { + return Optional.empty(); + } + List, MethodMeta>> candidates = new ArrayList<>(); + for (MethodMeta methodMeta : serviceMeta.getExceptionHandlers()) { + ExceptionHandler anno = methodMeta.getMethod().getAnnotation(ExceptionHandler.class); + if (anno == null) { + continue; + } + for (Class type : anno.value()) { + if (type.isAssignableFrom(exType)) { + candidates.add(Pair.of(type, methodMeta)); + } + } + } + int size = candidates.size(); + if (size == 0) { + return Optional.empty(); + } + if (size > 1) { + candidates.sort((o1, o2) -> { + Class c1 = o1.getLeft(); + Class c2 = o2.getLeft(); + if (c1.equals(c2)) { + return 0; + } + return c1.isAssignableFrom(c2) ? 1 : -1; + }); + } + return Optional.of(candidates.get(0).getRight()); + } + + private Object invokeExceptionHandler(MethodMeta meta, Throwable t, HttpRequest request, HttpResponse response) { + ParameterMeta[] parameters = meta.getParameters(); + int len = parameters.length; + Object[] args = new Object[len]; + for (int i = 0; i < len; i++) { + ParameterMeta parameter = parameters[i]; + if (parameter.getType().isInstance(t)) { + args[i] = t; + } else { + args[i] = argumentResolver.resolve(parameter, request, response); + } + } + Object value; + try { + value = meta.getMethod().invoke(meta.getServiceMeta().getService(), args); + } catch (Exception e) { + throw new RestException("Failed to invoke exception handler method: " + meta.getMethod(), e); + } + AnnotationMeta rs = meta.getAnnotation(ResponseStatus.class); + if (rs == null) { + return value; + } + HttpStatus status = rs.getEnum("value"); + String reason = rs.getString("reason"); + return HttpResult.builder() + .body(StringUtils.isEmpty(reason) ? value : reason) + .status(status.value()) + .build(); + } + + private static final class Key { + private final Method method; + private final Class type; + + Key(Method method, Class type) { + this.method = method; + this.type = type; + } + + @Override + @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "EqualsDoesntCheckParameterClass"}) + public boolean equals(Object obj) { + Key other = (Key) obj; + return method.equals(other.method) && type.equals(other.type); + } + + @Override + public int hashCode() { + return method.hashCode() * 31 + type.hashCode(); + } + } +} diff --git a/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java new file mode 100644 index 000000000000..9c7e1baac0f7 --- /dev/null +++ b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java @@ -0,0 +1,176 @@ +/* + * 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 org.apache.dubbo.rpc.protocol.tri.rest.support.spring; + +import org.apache.dubbo.common.logger.Logger; +import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.utils.CollectionUtils; +import org.apache.dubbo.common.utils.DefaultParameterNameReader; +import org.apache.dubbo.common.utils.ParameterNameReader; +import org.apache.dubbo.config.spring.extension.SpringExtensionInjector; +import org.apache.dubbo.remoting.http12.HttpRequest; +import org.apache.dubbo.remoting.http12.HttpResponse; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.FrameworkModel; +import org.apache.dubbo.rpc.protocol.tri.rest.Messages; +import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants; +import org.apache.dubbo.rpc.protocol.tri.rest.RestException; +import org.apache.dubbo.rpc.protocol.tri.rest.argument.CompositeArgumentResolver; +import org.apache.dubbo.rpc.protocol.tri.rest.argument.GeneralTypeConverter; +import org.apache.dubbo.rpc.protocol.tri.rest.argument.TypeConverter; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodParameterMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta; +import org.apache.dubbo.rpc.protocol.tri.rest.util.RestToolKit; +import org.apache.dubbo.rpc.protocol.tri.rest.util.RestUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.util.PropertyPlaceholderHelper; + +final class SpringRestToolKit implements RestToolKit { + + private static final Logger LOGGER = LoggerFactory.getLogger(SpringRestToolKit.class); + + private final Map cache = CollectionUtils.newConcurrentHashMap(); + private final ConfigurableBeanFactory beanFactory; + private final PropertyPlaceholderHelper placeholderHelper; + private final ConfigurationWrapper configuration; + private final ConversionService conversionService; + private final TypeConverter typeConverter; + private final BeanArgumentBinder argumentBinder; + private final ParameterNameReader parameterNameReader; + private final CompositeArgumentResolver argumentResolver; + + public SpringRestToolKit(FrameworkModel frameworkModel) { + ApplicationModel applicationModel = frameworkModel.defaultApplication(); + SpringExtensionInjector injector = SpringExtensionInjector.get(applicationModel); + ApplicationContext context = injector.getContext(); + if (context instanceof ConfigurableApplicationContext) { + beanFactory = ((ConfigurableApplicationContext) context).getBeanFactory(); + placeholderHelper = null; + configuration = null; + } else { + beanFactory = null; + placeholderHelper = new PropertyPlaceholderHelper("${", "}", ":", null, true); + configuration = new ConfigurationWrapper(applicationModel); + } + if (context != null && context.containsBean("mvcConversionService")) { + conversionService = context.getBean("mvcConversionService", ConversionService.class); + } else { + conversionService = DefaultConversionService.getSharedInstance(); + } + typeConverter = frameworkModel.getOrRegisterBean(GeneralTypeConverter.class); + parameterNameReader = frameworkModel.getOrRegisterBean(DefaultParameterNameReader.class); + argumentResolver = frameworkModel.getOrRegisterBean(CompositeArgumentResolver.class); + argumentBinder = new BeanArgumentBinder(argumentResolver, conversionService); + } + + @Override + public int getDialect() { + return RestConstants.DIALECT_SPRING_MVC; + } + + @Override + public String resolvePlaceholders(String text) { + if (!RestUtils.hasPlaceholder(text)) { + return text; + } + if (beanFactory != null) { + return beanFactory.resolveEmbeddedValue(text); + } + return placeholderHelper.replacePlaceholders(text, configuration); + } + + @Override + public Object convert(Object value, ParameterMeta parameter) { + boolean tried = false; + if (value instanceof Collection || value instanceof Map) { + tried = true; + Object target = typeConverter.convert(value, parameter.getGenericType()); + if (target != null) { + return target; + } + } + if (parameter instanceof MethodParameterMeta) { + TypeDescriptor targetType = cache.computeIfAbsent( + (MethodParameterMeta) parameter, + k -> new TypeDescriptor(new MethodParameter(k.getMethod(), k.getIndex()))); + TypeDescriptor sourceType = TypeDescriptor.forObject(value); + if (conversionService.canConvert(sourceType, targetType)) { + try { + return conversionService.convert(value, sourceType, targetType); + } catch (Throwable t) { + LOGGER.debug( + "Spring convert value '{}' from type [{}] to type [{}] failed", + value, + value.getClass(), + parameter.getGenericType(), + t); + } + } + } + Object target = tried ? null : typeConverter.convert(value, parameter.getGenericType()); + if (target == null && value != null) { + throw new RestException( + Messages.ARGUMENT_CONVERT_ERROR, + parameter.getName(), + value, + value.getClass(), + parameter.getGenericType()); + } + return target; + } + + @Override + public Object bind(ParameterMeta parameter, HttpRequest request, HttpResponse response) { + return argumentBinder.bind(parameter, request, response); + } + + @Override + public NamedValueMeta getNamedValueMeta(ParameterMeta parameter) { + return argumentResolver.getNamedValueMeta(parameter); + } + + @Override + public String[] getParameterNames(Method method) { + return parameterNameReader.readParameterNames(method); + } + + @Override + public String[] getParameterNames(Constructor ctor) { + return parameterNameReader.readParameterNames(ctor); + } + + @Override + public Map getAttributes(AnnotatedElement element, Annotation annotation) { + return AnnotatedElementUtils.getMergedAnnotationAttributes(element, annotation.annotationType()); + } +} diff --git a/dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-before-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java similarity index 100% rename from dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java rename to dubbo-plugin/dubbo-rest-spring/src/spring-boot-before-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java diff --git a/dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-before-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java similarity index 100% rename from dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java rename to dubbo-plugin/dubbo-rest-spring/src/spring-boot-before-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java diff --git a/dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-before-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java similarity index 100% rename from dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java rename to dubbo-plugin/dubbo-rest-spring/src/spring-boot-before-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java diff --git a/dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java b/dubbo-plugin/dubbo-rest-spring/src/spring-boot-before-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java similarity index 100% rename from dubbo-plugin/dubbo-rest-spring/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java rename to dubbo-plugin/dubbo-rest-spring/src/spring-boot-before-4/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java diff --git a/dubbo-plugin/dubbo-spring6-security/pom.xml b/dubbo-plugin/dubbo-spring6-security/pom.xml index 97d0ade28e92..6db04e6be258 100644 --- a/dubbo-plugin/dubbo-spring6-security/pom.xml +++ b/dubbo-plugin/dubbo-spring6-security/pom.xml @@ -192,6 +192,16 @@ ${spring-boot-3.version} test true + + + ch.qos.logback + logback-classic + + + org.apache.logging.log4j + log4j-to-slf4j + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure/pom.xml index 99ea01813228..7ee3a91d2989 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure/pom.xml @@ -28,8 +28,112 @@ jar Apache Dubbo Spring Boot 3 Auto-Configure + + 10.1.52 + + + + org.springframework.boot + spring-boot + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-actuator + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-actuator-autoconfigure + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-aop + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-json + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-validation + ${spring-boot-3.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot-3.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${spring-boot-3.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${spring-boot-3.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${spring-boot-3.tomcat.version} + org.springframework.boot spring-boot-dependencies diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot3Condition.java b/dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot3Condition.java index 489446f1d75c..1bad7b9ba937 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot3Condition.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot3Condition.java @@ -23,7 +23,7 @@ public class SpringBoot3Condition implements Condition { - public static boolean IS_SPRING_BOOT_3 = SpringBootVersion.getVersion().charAt(0) >= '3'; + public static boolean IS_SPRING_BOOT_3 = SpringBootVersion.getVersion().charAt(0) == '3'; @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/pom.xml new file mode 100644 index 000000000000..529e85405c1d --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/pom.xml @@ -0,0 +1,208 @@ + + + + 4.0.0 + + org.apache.dubbo + dubbo-parent + ${revision} + ../../pom.xml + + + dubbo-spring-boot-4-autoconfigure + jar + Apache Dubbo Spring Boot 4 Auto-Configure + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot-4.version} + pom + import + + + org.springframework.boot + spring-boot + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-http-client + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-http-converter + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-restclient + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-tomcat + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-web-server + ${spring-boot-4.version} + + + + + + + org.apache.dubbo + dubbo-spring-boot-autoconfigure + ${project.version} + + + + + org.springframework.boot + spring-boot-autoconfigure + true + + + org.springframework.boot + spring-boot-tomcat + true + + + org.springframework.boot + spring-boot-web-server + true + + + org.springframework.boot + spring-boot-restclient + true + + + + + org.apache.dubbo + dubbo + ${project.version} + true + + + org.apache.dubbo + dubbo-triple-servlet + ${project.version} + true + + + org.apache.dubbo + dubbo-triple-websocket + ${project.version} + true + + + + jakarta.servlet + jakarta.servlet-api + provided + + + jakarta.websocket + jakarta.websocket-api + provided + + + jakarta.websocket + jakarta.websocket-client-api + provided + + + + + io.micrometer + micrometer-tracing-bridge-brave + true + + + io.opentelemetry + opentelemetry-exporter-zipkin + true + + + io.zipkin.reporter2 + zipkin-reporter-brave + true + + + io.zipkin.reporter2 + zipkin-sender-urlconnection + true + + + + org.springframework + spring-web + true + + + org.springframework + spring-webflux + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple4AutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple4AutoConfiguration.java new file mode 100644 index 000000000000..aad826ebc4e9 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple4AutoConfiguration.java @@ -0,0 +1,108 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.apache.dubbo.rpc.protocol.tri.ServletExchanger; +import org.apache.dubbo.rpc.protocol.tri.servlet.jakarta.TripleFilter; +import org.apache.dubbo.rpc.protocol.tri.websocket.jakarta.TripleWebSocketFilter; + +import jakarta.servlet.Filter; +import org.apache.coyote.ProtocolHandler; +import org.apache.coyote.UpgradeProtocol; +import org.apache.coyote.http2.Http2Protocol; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.tomcat.ConfigurableTomcatWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Conditional(SpringBoot4Condition.class) +public class DubboTriple4AutoConfiguration { + + public static final String SERVLET_PREFIX = "dubbo.protocol.triple.servlet"; + + public static final String WEBSOCKET_PREFIX = "dubbo.protocol.triple.websocket"; + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Filter.class) + @ConditionalOnWebApplication(type = Type.SERVLET) + @ConditionalOnProperty(prefix = SERVLET_PREFIX, name = "enabled", havingValue = "true") + public static class TripleServletConfiguration { + + @Bean + public FilterRegistrationBean tripleProtocolFilter( + @Value("${" + SERVLET_PREFIX + ".filter-url-patterns:/*}") String[] urlPatterns, + @Value("${" + SERVLET_PREFIX + ".filter-order:-1000000}") int order, + @Value("${server.port:8080}") int serverPort) { + ServletExchanger.bindServerPort(serverPort); + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TripleFilter()); + registrationBean.addUrlPatterns(urlPatterns); + registrationBean.setOrder(order); + return registrationBean; + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({ConfigurableTomcatWebServerFactory.class, Http2Protocol.class}) + @ConditionalOnProperty(prefix = SERVLET_PREFIX, name = "max-concurrent-streams") + public static class TomcatHttp2Configuration { + + @Bean + public WebServerFactoryCustomizer tripleTomcatHttp2Customizer( + @Value("${" + SERVLET_PREFIX + ".max-concurrent-streams}") int maxConcurrentStreams) { + return factory -> factory.addConnectorCustomizers(connector -> { + ProtocolHandler handler = connector.getProtocolHandler(); + for (UpgradeProtocol upgradeProtocol : handler.findUpgradeProtocols()) { + if (upgradeProtocol instanceof Http2Protocol) { + Http2Protocol protocol = (Http2Protocol) upgradeProtocol; + int value = maxConcurrentStreams <= 0 ? Integer.MAX_VALUE : maxConcurrentStreams; + protocol.setMaxConcurrentStreams(value); + protocol.setMaxConcurrentStreamExecution(value); + } + } + }); + } + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Filter.class) + @ConditionalOnWebApplication(type = Type.SERVLET) + @ConditionalOnProperty(prefix = WEBSOCKET_PREFIX, name = "enabled", havingValue = "true") + public static class TripleWebSocketConfiguration { + + @Bean + public FilterRegistrationBean tripleWebSocketFilter( + @Value("${" + WEBSOCKET_PREFIX + ".filter-url-patterns:/*}") String[] urlPatterns, + @Value("${" + WEBSOCKET_PREFIX + ".filter-order:-1000000}") int order, + @Value("${server.port:8080}") int serverPort) { + ServletExchanger.bindServerPort(serverPort); + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TripleWebSocketFilter()); + registrationBean.addUrlPatterns(urlPatterns); + registrationBean.setOrder(order); + return registrationBean; + } + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/DubboZipkin4AutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/DubboZipkin4AutoConfiguration.java new file mode 100644 index 000000000000..511fd5280d91 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/DubboZipkin4AutoConfiguration.java @@ -0,0 +1,77 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin; + +import org.apache.dubbo.spring.boot.autoconfigure.SpringBoot4Condition; +import org.apache.dubbo.spring.boot.autoconfigure.observability.annotation.ConditionalOnDubboTracingEnable; +import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.Zipkin4Configurations.BraveConfiguration; +import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.Zipkin4Configurations.OpenTelemetryConfiguration; +import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.Zipkin4Configurations.ReporterConfiguration; +import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.Zipkin4Configurations.SenderConfiguration; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Import; +import zipkin2.Span; +import zipkin2.codec.BytesEncoder; +import zipkin2.codec.SpanBytesEncoder; +import zipkin2.reporter.Sender; + +import static org.apache.dubbo.spring.boot.autoconfigure.observability.ObservabilityUtils.DUBBO_TRACING_ZIPKIN_CONFIG_PREFIX; +import static org.apache.dubbo.spring.boot.util.DubboUtils.DUBBO_PREFIX; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Zipkin on Spring Boot 4. + * + * @since 3.3.7 + */ +@ConditionalOnProperty(prefix = DUBBO_PREFIX, name = "enabled", matchIfMissing = true) +@AutoConfiguration( + afterName = { + "org.springframework.boot.restclient.autoconfigure.RestTemplateAutoConfiguration", + "org.springframework.boot.zipkin.autoconfigure.ZipkinAutoConfiguration" + }) +@ConditionalOnClass(Sender.class) +@Import({ + SenderConfiguration.class, + ReporterConfiguration.class, + BraveConfiguration.class, + OpenTelemetryConfiguration.class +}) +@ConditionalOnDubboTracingEnable +@Conditional(SpringBoot4Condition.class) +public class DubboZipkin4AutoConfiguration { + + @Bean + @ConditionalOnProperty(prefix = DUBBO_TRACING_ZIPKIN_CONFIG_PREFIX, name = "endpoint") + @ConditionalOnMissingBean + public BytesEncoder spanBytesEncoder() { + return SpanBytesEncoder.JSON_V2; + } + + @Bean + @ConditionalOnProperty(prefix = DUBBO_TRACING_ZIPKIN_CONFIG_PREFIX, name = "endpoint") + @ConditionalOnMissingBean + public zipkin2.reporter.BytesEncoder reporterBytesEncoder() { + return zipkin2.reporter.SpanBytesEncoder.JSON_V2; + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/Zipkin4Configurations.java b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/Zipkin4Configurations.java new file mode 100644 index 000000000000..50c1e788f6ff --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/Zipkin4Configurations.java @@ -0,0 +1,182 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin; + +import org.apache.dubbo.config.nested.ExporterConfig; +import org.apache.dubbo.spring.boot.autoconfigure.DubboConfigurationProperties; +import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.customizer.Zipkin4RestTemplateBuilderCustomizer; +import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.customizer.ZipkinWebClientBuilderCustomizer; + +import java.util.concurrent.atomic.AtomicReference; + +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.restclient.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import zipkin2.Span; +import zipkin2.codec.BytesEncoder; +import zipkin2.reporter.AsyncReporter; +import zipkin2.reporter.Reporter; +import zipkin2.reporter.Sender; +import zipkin2.reporter.brave.ZipkinSpanHandler; +import zipkin2.reporter.urlconnection.URLConnectionSender; + +import static org.apache.dubbo.spring.boot.autoconfigure.observability.ObservabilityUtils.DUBBO_TRACING_ZIPKIN_CONFIG_PREFIX; + +/** + * Configurations for Zipkin on Spring Boot 4. Those are imported by {@link DubboZipkin4AutoConfiguration}. + */ +class Zipkin4Configurations { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnProperty(prefix = DUBBO_TRACING_ZIPKIN_CONFIG_PREFIX, name = "endpoint") + @Import({ + UrlConnectionSenderConfiguration.class, + WebClientSenderConfiguration.class, + RestTemplateSenderConfiguration.class + }) + static class SenderConfiguration {} + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(URLConnectionSender.class) + @EnableConfigurationProperties(DubboConfigurationProperties.class) + static class UrlConnectionSenderConfiguration { + + @Bean + @ConditionalOnMissingBean(Sender.class) + URLConnectionSender urlConnectionSender(DubboConfigurationProperties properties) { + URLConnectionSender.Builder builder = URLConnectionSender.newBuilder(); + ExporterConfig.ZipkinConfig zipkinConfig = + properties.getTracing().getTracingExporter().getZipkinConfig(); + builder.connectTimeout((int) zipkinConfig.getConnectTimeout().toMillis()); + builder.readTimeout((int) zipkinConfig.getReadTimeout().toMillis()); + builder.endpoint(zipkinConfig.getEndpoint()); + return builder.build(); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass({RestTemplate.class, RestTemplateBuilder.class}) + @EnableConfigurationProperties(DubboConfigurationProperties.class) + static class RestTemplateSenderConfiguration { + + @Bean + @ConditionalOnMissingBean(Sender.class) + Zipkin4RestTemplateSender restTemplateSender( + DubboConfigurationProperties properties, + ObjectProvider customizers) { + ExporterConfig.ZipkinConfig zipkinConfig = + properties.getTracing().getTracingExporter().getZipkinConfig(); + RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder() + .connectTimeout(zipkinConfig.getConnectTimeout()) + .readTimeout(zipkinConfig.getReadTimeout()); + restTemplateBuilder = applyCustomizers(restTemplateBuilder, customizers); + return new Zipkin4RestTemplateSender(zipkinConfig.getEndpoint(), restTemplateBuilder.build()); + } + + private RestTemplateBuilder applyCustomizers( + RestTemplateBuilder restTemplateBuilder, + ObjectProvider customizers) { + Iterable orderedCustomizers = + () -> customizers.orderedStream().iterator(); + RestTemplateBuilder currentBuilder = restTemplateBuilder; + for (Zipkin4RestTemplateBuilderCustomizer customizer : orderedCustomizers) { + currentBuilder = customizer.customize(currentBuilder); + } + return currentBuilder; + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(WebClient.class) + @EnableConfigurationProperties(DubboConfigurationProperties.class) + static class WebClientSenderConfiguration { + + @Bean + @ConditionalOnMissingBean(Sender.class) + ZipkinWebClientSender webClientSender( + DubboConfigurationProperties properties, ObjectProvider customizers) { + ExporterConfig.ZipkinConfig zipkinConfig = + properties.getTracing().getTracingExporter().getZipkinConfig(); + WebClient.Builder builder = WebClient.builder(); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return new ZipkinWebClientSender(zipkinConfig.getEndpoint(), builder.build()); + } + } + + @Configuration(proxyBeanMethods = false) + static class ReporterConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(Sender.class) + AsyncReporter spanReporter(Sender sender, zipkin2.reporter.BytesEncoder encoder) { + return AsyncReporter.builder(sender).build(encoder); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ZipkinSpanHandler.class) + static class BraveConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(Reporter.class) + ZipkinSpanHandler zipkinSpanHandler(Reporter spanReporter) { + return (ZipkinSpanHandler) + ZipkinSpanHandler.newBuilder(spanReporter).build(); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ZipkinSpanExporter.class) + @ConditionalOnProperty(prefix = DUBBO_TRACING_ZIPKIN_CONFIG_PREFIX, name = "endpoint") + @EnableConfigurationProperties(DubboConfigurationProperties.class) + static class OpenTelemetryConfiguration { + + @Bean + @ConditionalOnMissingBean + ZipkinSpanExporter zipkinSpanExporter( + DubboConfigurationProperties properties, BytesEncoder encoder, ObjectProvider senders) { + AtomicReference senderRef = new AtomicReference<>(); + senders.orderedStream().findFirst().ifPresent(senderRef::set); + Sender sender = senderRef.get(); + if (sender == null) { + ExporterConfig.ZipkinConfig zipkinConfig = + properties.getTracing().getTracingExporter().getZipkinConfig(); + return ZipkinSpanExporter.builder() + .setEncoder(encoder) + .setEndpoint(zipkinConfig.getEndpoint()) + .setReadTimeout(zipkinConfig.getReadTimeout()) + .build(); + } + return ZipkinSpanExporter.builder() + .setEncoder(encoder) + .setSender(sender) + .build(); + } + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/Zipkin4RestTemplateSender.java b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/Zipkin4RestTemplateSender.java new file mode 100644 index 000000000000..2eef3692ed14 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/Zipkin4RestTemplateSender.java @@ -0,0 +1,74 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin; + +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.web.client.RestTemplate; +import zipkin2.reporter.Call; +import zipkin2.reporter.Callback; + +class Zipkin4RestTemplateSender extends HttpSender { + private final String endpoint; + + private final RestTemplate restTemplate; + + Zipkin4RestTemplateSender(String endpoint, RestTemplate restTemplate) { + this.endpoint = endpoint; + this.restTemplate = restTemplate; + } + + @Override + public HttpPostCall sendSpans(byte[] batchedEncodedSpans) { + return new RestTemplateHttpPostCall(this.endpoint, batchedEncodedSpans, this.restTemplate); + } + + private static class RestTemplateHttpPostCall extends HttpPostCall { + + private final String endpoint; + + private final RestTemplate restTemplate; + + RestTemplateHttpPostCall(String endpoint, byte[] body, RestTemplate restTemplate) { + super(body); + this.endpoint = endpoint; + this.restTemplate = restTemplate; + } + + @Override + public Call clone() { + return new RestTemplateHttpPostCall(this.endpoint, getUncompressedBody(), this.restTemplate); + } + + @Override + protected Void doExecute() { + HttpEntity request = new HttpEntity<>(getBody(), getDefaultHeaders()); + this.restTemplate.exchange(this.endpoint, HttpMethod.POST, request, Void.class); + return null; + } + + @Override + protected void doEnqueue(Callback callback) { + try { + doExecute(); + callback.onSuccess(null); + } catch (Exception ex) { + callback.onError(ex); + } + } + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/customizer/Zipkin4RestTemplateBuilderCustomizer.java b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/customizer/Zipkin4RestTemplateBuilderCustomizer.java new file mode 100644 index 000000000000..372b3c35dcf4 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/customizer/Zipkin4RestTemplateBuilderCustomizer.java @@ -0,0 +1,37 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.customizer; + +import org.springframework.boot.restclient.RestTemplateBuilder; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link RestTemplateBuilder} used to send spans to Zipkin on Spring Boot 4. + * + * @since 3.3.7 + */ +@FunctionalInterface +public interface Zipkin4RestTemplateBuilderCustomizer { + + /** + * Customize the rest template builder. + * + * @param restTemplateBuilder the {@code RestTemplateBuilder} to customize + * @return the customized {@code RestTemplateBuilder} + */ + RestTemplateBuilder customize(RestTemplateBuilder restTemplateBuilder); +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/resources/META-INF/spring.factories b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000000..b1be32a5d2a3 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.apache.dubbo.spring.boot.autoconfigure.DubboTriple4AutoConfiguration,\ +org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.DubboZipkin4AutoConfiguration diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000000..b27b1650b5f6 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +org.apache.dubbo.spring.boot.autoconfigure.DubboTriple4AutoConfiguration +org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.DubboZipkin4AutoConfiguration diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot4DependencyCheckAutoConfigurationTests.java b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot4DependencyCheckAutoConfigurationTests.java new file mode 100644 index 000000000000..52935c7f4be4 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot4DependencyCheckAutoConfigurationTests.java @@ -0,0 +1,129 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.DubboZipkin4AutoConfiguration; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +class DubboSpringBoot4DependencyCheckAutoConfigurationTests { + + private static final String MISSING_BOOT4_AUTOCONFIGURE = + "Couldn't enable servlet support for triple at SpringBoot4: Missing dubbo-spring-boot-4-autoconfigure"; + + private static final String MISSING_BOOT4_ZIPKIN_AUTOCONFIGURE = + "Couldn't enable Zipkin tracing at SpringBoot4: Missing dubbo-spring-boot-4-autoconfigure"; + + private final boolean springBoot4 = SpringBoot4Condition.IS_SPRING_BOOT_4; + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DubboSpringBoot4DependencyCheckAutoConfiguration.class)) + .withClassLoader( + new FilteredClassLoader(DubboTriple4AutoConfiguration.class, DubboZipkin4AutoConfiguration.class)); + + private final WebApplicationContextRunner contextRunnerWithSpringBoot4Autoconfigure = + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DubboSpringBoot4DependencyCheckAutoConfiguration.class)); + + @BeforeEach + void enableSpringBoot4Condition() { + SpringBoot4Condition.IS_SPRING_BOOT_4 = true; + } + + @AfterEach + void resetSpringBoot4Condition() { + SpringBoot4Condition.IS_SPRING_BOOT_4 = springBoot4; + } + + @Test + void shouldFailWhenTripleServletEnabledWithoutSpringBoot4Autoconfigure() { + this.contextRunner + .withPropertyValues("dubbo.protocol.triple.servlet.enabled=true") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseMessage(MISSING_BOOT4_AUTOCONFIGURE); + }); + } + + @Test + void shouldFailWhenTripleWebSocketEnabledWithoutSpringBoot4Autoconfigure() { + this.contextRunner + .withPropertyValues("dubbo.protocol.triple.websocket.enabled=true") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseMessage(MISSING_BOOT4_AUTOCONFIGURE); + }); + } + + @Test + void shouldFailWhenZipkinEnabledWithoutSpringBoot4Autoconfigure() { + this.contextRunner + .withPropertyValues( + "dubbo.tracing.enabled=true", + "dubbo.tracing.tracing-exporter.zipkin-config.endpoint=http://localhost:9411/api/v2/spans") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseMessage(MISSING_BOOT4_ZIPKIN_AUTOCONFIGURE); + }); + } + + @Test + void shouldNotFailWhenSpringBoot4AutoconfigureIsPresent() { + this.contextRunnerWithSpringBoot4Autoconfigure + .withPropertyValues( + "dubbo.protocol.triple.servlet.enabled=true", + "dubbo.protocol.triple.websocket.enabled=true", + "dubbo.tracing.enabled=true", + "dubbo.tracing.tracing-exporter.zipkin-config.endpoint=http://localhost:9411/api/v2/spans") + .run(context -> assertThat(context).hasNotFailed()); + } + + @Test + void shouldNotFailWhenSpringBoot4ConditionIsDisabled() { + SpringBoot4Condition.IS_SPRING_BOOT_4 = false; + + this.contextRunner + .withPropertyValues( + "dubbo.protocol.triple.servlet.enabled=true", + "dubbo.protocol.triple.websocket.enabled=true", + "dubbo.tracing.enabled=true", + "dubbo.tracing.tracing-exporter.zipkin-config.endpoint=http://localhost:9411/api/v2/spans") + .run(context -> assertThat(context).hasNotFailed()); + } + + @Test + void shouldNotFailWhenZipkinEndpointIsMissing() { + this.contextRunner.withPropertyValues("dubbo.tracing.enabled=true").run(context -> assertThat(context) + .hasNotFailed()); + } + + @Test + void shouldNotFailWhenTracingIsDisabled() { + this.contextRunner + .withPropertyValues( + "dubbo.tracing.enabled=false", + "dubbo.tracing.tracing-exporter.zipkin-config.endpoint=http://localhost:9411/api/v2/spans") + .run(context -> assertThat(context).hasNotFailed()); + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple4AutoConfigurationTests.java b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple4AutoConfigurationTests.java new file mode 100644 index 000000000000..2b7c1bb6b224 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple4AutoConfigurationTests.java @@ -0,0 +1,105 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.apache.dubbo.rpc.protocol.tri.servlet.jakarta.TripleFilter; +import org.apache.dubbo.rpc.protocol.tri.websocket.jakarta.TripleWebSocketFilter; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.FilterRegistrationBean; + +import static org.assertj.core.api.Assertions.assertThat; + +class DubboTriple4AutoConfigurationTests { + + private final boolean springBoot4 = SpringBoot4Condition.IS_SPRING_BOOT_4; + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DubboTriple4AutoConfiguration.class)); + + @BeforeEach + void enableSpringBoot4Condition() { + SpringBoot4Condition.IS_SPRING_BOOT_4 = true; + } + + @AfterEach + void resetSpringBoot4Condition() { + SpringBoot4Condition.IS_SPRING_BOOT_4 = springBoot4; + } + + @Test + void shouldRegisterTripleServletFilter() { + this.contextRunner + .withPropertyValues( + "dubbo.protocol.triple.servlet.enabled=true", + "dubbo.protocol.triple.servlet.filter-url-patterns=/triple/*", + "dubbo.protocol.triple.servlet.filter-order=-100", + "server.port=20880") + .run(context -> { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + FilterRegistrationBean registrationBean = context.getBean(FilterRegistrationBean.class); + assertThat(registrationBean.getFilter()).isInstanceOf(TripleFilter.class); + assertThat(registrationBean.getUrlPatterns()).containsExactly("/triple/*"); + assertThat(registrationBean.getOrder()).isEqualTo(-100); + }); + } + + @Test + void shouldRegisterTripleWebSocketFilter() { + this.contextRunner + .withPropertyValues( + "dubbo.protocol.triple.websocket.enabled=true", + "dubbo.protocol.triple.websocket.filter-url-patterns=/websocket/*", + "dubbo.protocol.triple.websocket.filter-order=-200", + "server.port=20881") + .run(context -> { + assertThat(context).hasSingleBean(FilterRegistrationBean.class); + FilterRegistrationBean registrationBean = context.getBean(FilterRegistrationBean.class); + assertThat(registrationBean.getFilter()).isInstanceOf(TripleWebSocketFilter.class); + assertThat(registrationBean.getUrlPatterns()).containsExactly("/websocket/*"); + assertThat(registrationBean.getOrder()).isEqualTo(-200); + }); + } + + @Test + void shouldRegisterTomcatHttp2CustomizerWhenMaxConcurrentStreamsConfigured() { + this.contextRunner + .withPropertyValues( + "dubbo.protocol.triple.servlet.enabled=true", + "dubbo.protocol.triple.servlet.max-concurrent-streams=10") + .run(context -> assertThat(context).hasSingleBean(WebServerFactoryCustomizer.class)); + } + + @Test + void shouldIgnoreTomcatHttp2CustomizerWhenTomcatIsNotPresent() { + this.contextRunner + .withClassLoader(new FilteredClassLoader("org.springframework.boot.tomcat", "org.apache.coyote")) + .withPropertyValues( + "dubbo.protocol.triple.servlet.enabled=true", + "dubbo.protocol.triple.servlet.max-concurrent-streams=10") + .run(context -> { + assertThat(context).hasNotFailed(); + assertThat(context).doesNotHaveBean(WebServerFactoryCustomizer.class); + }); + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/DubboZipkin4AutoConfigurationTests.java b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/DubboZipkin4AutoConfigurationTests.java new file mode 100644 index 000000000000..e35cadcdf086 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/DubboZipkin4AutoConfigurationTests.java @@ -0,0 +1,170 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin; + +import org.apache.dubbo.spring.boot.autoconfigure.SpringBoot4Condition; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import io.opentelemetry.exporter.zipkin.ZipkinSpanExporter; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.restclient.RestTemplateBuilder; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; +import zipkin2.Span; +import zipkin2.reporter.AsyncReporter; +import zipkin2.reporter.Call; +import zipkin2.reporter.Encoding; +import zipkin2.reporter.Sender; +import zipkin2.reporter.brave.ZipkinSpanHandler; +import zipkin2.reporter.urlconnection.URLConnectionSender; + +import static org.assertj.core.api.Assertions.assertThat; + +class DubboZipkin4AutoConfigurationTests { + + private final boolean springBoot4 = SpringBoot4Condition.IS_SPRING_BOOT_4; + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DubboZipkin4AutoConfiguration.class)) + .withPropertyValues( + "dubbo.tracing.enabled=true", + "dubbo.tracing.tracing-exporter.zipkin-config.endpoint=http://localhost:9411/api/v2/spans"); + + @BeforeEach + void enableSpringBoot4Condition() { + SpringBoot4Condition.IS_SPRING_BOOT_4 = true; + } + + @AfterEach + void resetSpringBoot4Condition() { + SpringBoot4Condition.IS_SPRING_BOOT_4 = springBoot4; + } + + @Test + void shouldSupplyZipkinBeansOnSpringBoot4() { + this.contextRunner + .withUserConfiguration(NoOpReporterConfiguration.class, NoOpSenderConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(Sender.class); + assertThat(context).hasSingleBean(ZipkinSpanExporter.class); + }); + } + + @Test + void shouldOrderAfterSpringBootZipkinAutoConfiguration() { + AutoConfiguration autoConfiguration = + DubboZipkin4AutoConfiguration.class.getAnnotation(AutoConfiguration.class); + + assertThat(autoConfiguration.afterName()) + .contains("org.springframework.boot.zipkin.autoconfigure.ZipkinAutoConfiguration"); + } + + @Test + void shouldSupplyUrlConnectionSenderByDefault() { + this.contextRunner.run((context) -> { + assertThat(context).hasSingleBean(Sender.class); + assertThat(context.getBean(Sender.class)).isInstanceOf(URLConnectionSender.class); + assertThat(context).hasSingleBean(AsyncReporter.class); + assertThat(context).hasSingleBean(ZipkinSpanHandler.class); + assertThat(context).hasSingleBean(ZipkinSpanExporter.class); + }); + } + + @Test + void shouldSupplyWebClientSenderWhenUrlConnectionIsUnavailable() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(URLConnectionSender.class)) + .run((context) -> { + assertThat(context).hasSingleBean(Sender.class); + assertThat(context.getBean(Sender.class)).isInstanceOf(ZipkinWebClientSender.class); + assertThat(context).hasSingleBean(ZipkinSpanExporter.class); + }); + } + + @Test + void shouldSupplyRestTemplateSenderWhenUrlConnectionAndWebClientAreUnavailable() { + this.contextRunner + .withClassLoader(new FilteredClassLoader(URLConnectionSender.class, WebClient.class)) + .withUserConfiguration(NoOpReporterConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(Sender.class); + assertThat(context.getBean(Sender.class)).isInstanceOf(Zipkin4RestTemplateSender.class); + assertThat(context).hasSingleBean(ZipkinSpanExporter.class); + }); + } + + @Test + void shouldBackOffRestTemplateSenderWhenRestTemplateBuilderIsUnavailable() { + this.contextRunner + .withClassLoader( + new FilteredClassLoader(URLConnectionSender.class, WebClient.class, RestTemplateBuilder.class)) + .run((context) -> { + assertThat(context).hasNotFailed(); + assertThat(context).doesNotHaveBean(Sender.class); + assertThat(context).hasSingleBean(ZipkinSpanExporter.class); + }); + } + + @Configuration(proxyBeanMethods = false) + private static class NoOpReporterConfiguration { + + @Bean + AsyncReporter spanReporter(Sender sender, zipkin2.reporter.BytesEncoder encoder) { + return AsyncReporter.builder(sender) + .messageTimeout(0, TimeUnit.NANOSECONDS) + .build(encoder); + } + } + + @Configuration(proxyBeanMethods = false) + private static class NoOpSenderConfiguration { + + @Bean + Sender sender() { + return new Sender() { + @Override + public Encoding encoding() { + return Encoding.JSON; + } + + @Override + public int messageMaxBytes() { + return Integer.MAX_VALUE; + } + + @Override + public int messageSizeInBytes(List encodedSpans) { + return encodedSpans.stream().mapToInt(bytes -> bytes.length).sum(); + } + + @Override + public Call sendSpans(List encodedSpans) { + return Call.create(null); + } + }; + } + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/pom.xml index 27d7f6f39e79..302eb603a062 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/pom.xml @@ -112,12 +112,23 @@ org.springframework.boot spring-boot-starter-test test + + + org.springframework.boot + spring-boot-starter-logging + + org.springframework.boot spring-boot-starter-log4j2 test + + com.fasterxml.jackson.core + jackson-databind + test + net.bytebuddy @@ -141,4 +152,128 @@ test + + + + spring-boot-3 + + 9.0.117 + 2.7.18 + + + + + org.springframework.boot + spring-boot + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-actuator + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-actuator-autoconfigure + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-aop + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-json + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-validation + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot-2.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${spring-boot-2.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${spring-boot-2.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${spring-boot-2.tomcat.version} + + + + + + spring-boot-4 + + + org.springframework.boot + spring-boot-health + test + + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/CompatibleHealthIndicatorCondition.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/CompatibleHealthIndicatorCondition.java new file mode 100644 index 000000000000..0dbbf1bf50ac --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/CompatibleHealthIndicatorCondition.java @@ -0,0 +1,33 @@ +/* + * 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 org.apache.dubbo.spring.boot.actuate.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; + +class CompatibleHealthIndicatorCondition extends AnyNestedCondition { + + CompatibleHealthIndicatorCondition() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") + static class SpringBootBefore4HealthIndicator {} + + @ConditionalOnClass(name = "org.springframework.boot.health.contributor.HealthIndicator") + static class SpringBoot4HealthIndicator {} +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboEndpointMetadataAutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboEndpointMetadataAutoConfiguration.java index a4b2a101ba8b..51047a1b7c1b 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboEndpointMetadataAutoConfiguration.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboEndpointMetadataAutoConfiguration.java @@ -19,9 +19,9 @@ import org.apache.dubbo.spring.boot.actuate.endpoint.metadata.AbstractDubboMetadata; import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import static org.apache.dubbo.spring.boot.util.DubboUtils.DUBBO_PREFIX; @@ -30,9 +30,7 @@ * Dubbo Endpoints Metadata Auto-{@link Configuration} */ @ConditionalOnProperty(prefix = DUBBO_PREFIX, name = "enabled", matchIfMissing = true) -@ConditionalOnClass( - name = {"org.springframework.boot.actuate.health.Health" // If spring-boot-actuator is present - }) +@Conditional(CompatibleHealthIndicatorCondition.class) @Configuration @AutoConfigureAfter( name = { diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboExtensionEndpointAutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboExtensionEndpointAutoConfiguration.java index 87042c122cf4..76317ba23620 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboExtensionEndpointAutoConfiguration.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboExtensionEndpointAutoConfiguration.java @@ -19,9 +19,9 @@ import org.apache.dubbo.spring.boot.actuate.endpoint.DubboActuatorProperties; import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import static org.apache.dubbo.spring.boot.util.DubboUtils.DUBBO_PREFIX; @@ -30,7 +30,7 @@ * Dubbo Extension Endpoints Auto-{@link Configuration} */ @ConditionalOnProperty(prefix = DUBBO_PREFIX, name = "enabled", matchIfMissing = true) -@ConditionalOnClass(name = {"org.springframework.boot.actuate.health.Health"}) +@Conditional(CompatibleHealthIndicatorCondition.class) @Configuration @AutoConfigureAfter( name = {"org.apache.dubbo.spring.boot.actuate.autoconfigure.DubboEndpointMetadataAutoConfiguration"}) diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboHealthIndicatorAutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboHealthIndicatorAutoConfiguration.java index 50d31bc00a32..01bb152d56fa 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboHealthIndicatorAutoConfiguration.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboHealthIndicatorAutoConfiguration.java @@ -19,22 +19,20 @@ import org.apache.dubbo.spring.boot.actuate.health.DubboHealthIndicator; import org.apache.dubbo.spring.boot.actuate.health.DubboHealthIndicatorProperties; -import org.springframework.boot.actuate.health.HealthIndicator; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; /** * Dubbo {@link DubboHealthIndicator} Auto Configuration * - * @see HealthIndicator * @since 2.7.0 */ @Configuration -@ConditionalOnClass(name = {"org.springframework.boot.actuate.health.Health"}) +@Conditional(CompatibleHealthIndicatorCondition.class) @ConditionalOnProperty( name = {"management.health.dubbo.enabled", "dubbo.enabled"}, matchIfMissing = true, diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboMetricsAutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboMetricsAutoConfiguration.java index ee518190d300..5c9e532aa468 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboMetricsAutoConfiguration.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/actuate/autoconfigure/DubboMetricsAutoConfiguration.java @@ -19,7 +19,6 @@ import org.apache.dubbo.spring.boot.actuate.mertics.DubboMetricsBinder; import io.micrometer.core.instrument.MeterRegistry; -import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -33,7 +32,11 @@ @ConditionalOnProperty(prefix = DUBBO_PREFIX, name = "enabled", matchIfMissing = true) @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication -@AutoConfigureAfter(CompositeMeterRegistryAutoConfiguration.class) +@AutoConfigureAfter( + name = { + "org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration", + "org.springframework.boot.micrometer.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration" + }) public class DubboMetricsAutoConfiguration { @Bean @ConditionalOnBean({MeterRegistry.class}) diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicatorTest.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicatorTest.java index 3509ebde1cf9..3ed23229cf9b 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicatorTest.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicatorTest.java @@ -23,8 +23,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.actuate.health.Health; -import org.springframework.boot.actuate.health.Status; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.TestPropertySource; @@ -77,8 +75,6 @@ void testResolveStatusCheckerNamesMap() { @Test void testHealth() { - Health health = dubboHealthIndicator.health(); - - assertEquals(Status.UNKNOWN, health.getStatus()); + assertEquals("UNKNOWN", dubboHealthIndicator.health().getStatus().toString()); } } diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/pom.xml index d579624b0311..71d232cad5fd 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/pom.xml @@ -112,11 +112,6 @@ test - - org.junit.platform - junit-platform-runner - test - org.apache.dubbo dubbo-metrics-default @@ -125,4 +120,155 @@ + + + spring-boot-before-4 + + true + + + 2.7.18 + 9.0.117 + + + + + org.springframework.boot + spring-boot + ${spring-boot.version} + + + org.springframework.boot + spring-boot-actuator + ${spring-boot.version} + + + org.springframework.boot + spring-boot-actuator-autoconfigure + ${spring-boot.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-json + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${spring-boot.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${spring-boot.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${spring-boot.tomcat.version} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-spring-boot-before-4-sources + + add-source + + generate-sources + + + src/spring-boot-before-4/java + + + + + + + + + + spring-boot-4 + + + org.springframework.boot + spring-boot-health + true + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-spring-boot-4-sources + + add-source + + generate-sources + + + src/spring-boot-4/java + + + + + + + + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/main/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicatorProperties.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/main/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicatorProperties.java index b0ba917dd3af..5f8951528fba 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/main/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicatorProperties.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/main/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicatorProperties.java @@ -22,15 +22,13 @@ import java.util.LinkedHashSet; import java.util.Set; -import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.context.properties.ConfigurationProperties; import static org.apache.dubbo.spring.boot.actuate.health.DubboHealthIndicatorProperties.PREFIX; /** - * Dubbo {@link HealthIndicator} Properties + * Dubbo health indicator properties. * - * @see HealthIndicator * @since 2.7.0 */ @ConfigurationProperties(prefix = PREFIX, ignoreUnknownFields = false) diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/spring-boot-4/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicator.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/spring-boot-4/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicator.java new file mode 100644 index 000000000000..6846062e38a9 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/spring-boot-4/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicator.java @@ -0,0 +1,213 @@ +/* + * 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 org.apache.dubbo.spring.boot.actuate.health; + +import org.apache.dubbo.common.extension.ExtensionLoader; +import org.apache.dubbo.common.status.StatusChecker; +import org.apache.dubbo.config.ProtocolConfig; +import org.apache.dubbo.config.ProviderConfig; +import org.apache.dubbo.config.context.ConfigManager; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ModuleModel; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.health.contributor.AbstractHealthIndicator; +import org.springframework.boot.health.contributor.Health; +import org.springframework.boot.health.contributor.HealthIndicator; +import org.springframework.util.StringUtils; + +/** + * Dubbo {@link HealthIndicator} + * + * @see HealthIndicator + * @since 2.7.0 + */ +public class DubboHealthIndicator extends AbstractHealthIndicator { + + @Autowired + private DubboHealthIndicatorProperties dubboHealthIndicatorProperties; + + // @Autowired(required = false) + private Map protocolConfigs = Collections.emptyMap(); + + // @Autowired(required = false) + private Map providerConfigs = Collections.emptyMap(); + + @Autowired + private ConfigManager configManager; + + @Autowired + private ApplicationModel applicationModel; + + @Override + protected void doHealthCheck(Health.Builder builder) throws Exception { + + ExtensionLoader extensionLoader = applicationModel.getExtensionLoader(StatusChecker.class); + + Map statusCheckerNamesMap = resolveStatusCheckerNamesMap(); + + boolean hasError = false; + + boolean hasUnknown = false; + + // Up first + builder.up(); + + for (Map.Entry entry : statusCheckerNamesMap.entrySet()) { + + String statusCheckerName = entry.getKey(); + + String source = entry.getValue(); + + StatusChecker checker = extensionLoader.getExtension(statusCheckerName); + + org.apache.dubbo.common.status.Status status = checker.check(); + + org.apache.dubbo.common.status.Status.Level level = status.getLevel(); + + if (!hasError && level.equals(org.apache.dubbo.common.status.Status.Level.ERROR)) { + hasError = true; + builder.down(); + } + + if (!hasError && !hasUnknown && level.equals(org.apache.dubbo.common.status.Status.Level.UNKNOWN)) { + hasUnknown = true; + builder.unknown(); + } + + Map detail = new LinkedHashMap<>(); + + detail.put("source", source); + detail.put("status", status); + + builder.withDetail(statusCheckerName, detail); + } + } + + /** + * Resolves the map of {@link StatusChecker}'s name and its' source. + * + * @return non-null {@link Map} + */ + protected Map resolveStatusCheckerNamesMap() { + + Map statusCheckerNamesMap = new LinkedHashMap<>(); + + statusCheckerNamesMap.putAll(resolveStatusCheckerNamesMapFromDubboHealthIndicatorProperties()); + + statusCheckerNamesMap.putAll(resolveStatusCheckerNamesMapFromProtocolConfigs()); + + statusCheckerNamesMap.putAll(resolveStatusCheckerNamesMapFromProviderConfig()); + + return statusCheckerNamesMap; + } + + private Map resolveStatusCheckerNamesMapFromDubboHealthIndicatorProperties() { + + DubboHealthIndicatorProperties.Status status = dubboHealthIndicatorProperties.getStatus(); + + Map statusCheckerNamesMap = new LinkedHashMap<>(); + + for (String statusName : status.getDefaults()) { + + statusCheckerNamesMap.put(statusName, DubboHealthIndicatorProperties.PREFIX + ".status.defaults"); + } + + for (String statusName : status.getExtras()) { + + statusCheckerNamesMap.put(statusName, DubboHealthIndicatorProperties.PREFIX + ".status.extras"); + } + + return statusCheckerNamesMap; + } + + private Map resolveStatusCheckerNamesMapFromProtocolConfigs() { + + if (protocolConfigs.isEmpty()) { + protocolConfigs = configManager.getConfigsMap(ProtocolConfig.class); + } + + Map statusCheckerNamesMap = new LinkedHashMap<>(); + + for (Map.Entry entry : protocolConfigs.entrySet()) { + + String beanName = entry.getKey(); + + ProtocolConfig protocolConfig = entry.getValue(); + + Set statusCheckerNames = getStatusCheckerNames(protocolConfig); + + for (String statusCheckerName : statusCheckerNames) { + + String source = buildSource(beanName, protocolConfig); + + statusCheckerNamesMap.put(statusCheckerName, source); + } + } + + return statusCheckerNamesMap; + } + + private Map resolveStatusCheckerNamesMapFromProviderConfig() { + + if (providerConfigs.isEmpty()) { + providerConfigs = new LinkedHashMap<>(); + for (ModuleModel moduleModel : applicationModel.getModuleModels()) { + providerConfigs.putAll(moduleModel.getConfigManager().getConfigsMap(ProviderConfig.class)); + } + } + + Map statusCheckerNamesMap = new LinkedHashMap<>(); + + for (Map.Entry entry : providerConfigs.entrySet()) { + + String beanName = entry.getKey(); + + ProviderConfig providerConfig = entry.getValue(); + + Set statusCheckerNames = getStatusCheckerNames(providerConfig); + + for (String statusCheckerName : statusCheckerNames) { + + String source = buildSource(beanName, providerConfig); + + statusCheckerNamesMap.put(statusCheckerName, source); + } + } + + return statusCheckerNamesMap; + } + + private Set getStatusCheckerNames(ProtocolConfig protocolConfig) { + String status = protocolConfig.getStatus(); + return StringUtils.commaDelimitedListToSet(status); + } + + private Set getStatusCheckerNames(ProviderConfig providerConfig) { + String status = providerConfig.getStatus(); + return StringUtils.commaDelimitedListToSet(status); + } + + private String buildSource(String beanName, Object bean) { + return beanName + "@" + bean.getClass().getSimpleName() + ".getStatus()"; + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/main/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicator.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/spring-boot-before-4/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicator.java similarity index 100% rename from dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/main/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicator.java rename to dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/spring-boot-before-4/java/org/apache/dubbo/spring/boot/actuate/health/DubboHealthIndicator.java diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/test/java/org/apache/dubbo/spring/boot/actuate/endpoint/DubboEndpointTest.java b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/test/java/org/apache/dubbo/spring/boot/actuate/endpoint/DubboEndpointTest.java index c623eb7802d3..621bb6007859 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/test/java/org/apache/dubbo/spring/boot/actuate/endpoint/DubboEndpointTest.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-actuator/src/test/java/org/apache/dubbo/spring/boot/actuate/endpoint/DubboEndpointTest.java @@ -17,8 +17,11 @@ package org.apache.dubbo.spring.boot.actuate.endpoint; import org.apache.dubbo.config.bootstrap.DubboBootstrap; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.spring.boot.actuate.endpoint.metadata.DubboMetadata; import org.apache.dubbo.spring.boot.util.DubboUtils; +import java.util.Collections; import java.util.Map; import org.junit.jupiter.api.AfterEach; @@ -28,6 +31,8 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; import org.springframework.test.context.junit.jupiter.SpringExtension; import static org.apache.dubbo.common.Version.getVersion; @@ -43,7 +48,7 @@ */ @ExtendWith(SpringExtension.class) @SpringBootTest( - classes = {DubboQosEndpoints.class}, + classes = {DubboQosEndpoints.class, DubboEndpointTest.TestConfig.class}, properties = {"dubbo.application.name = dubbo-demo-application"}) @EnableAutoConfiguration class DubboEndpointTest { @@ -79,8 +84,29 @@ void testInvoke() { assertEquals("https://github.com/apache/dubbo", urls.get("dubbo")); assertEquals("dev@dubbo.apache.org", urls.get("mailing-list")); - assertEquals("https://github.com/apache/dubbo-spring-boot-project", urls.get("github")); - assertEquals("https://github.com/apache/dubbo-spring-boot-project/issues", urls.get("issues")); - assertEquals("https://github.com/apache/dubbo-spring-boot-project.git", urls.get("git")); + assertEquals("https://github.com/apache/dubbo/tree/3.0/dubbo-spring-boot", urls.get("github")); + assertEquals("https://github.com/apache/dubbo/issues", urls.get("issues")); + assertEquals("https://github.com/apache/dubbo.git", urls.get("git")); + } + + @TestConfiguration + static class TestConfig { + + @Bean + ApplicationModel applicationModel() { + return ApplicationModel.defaultModel(); + } + + @Bean + DubboMetadata dubboMetadata() { + return new DubboMetadata(); + } + + @Bean + DubboActuatorProperties dubboActuatorProperties() { + DubboActuatorProperties properties = new DubboActuatorProperties(); + properties.setDubbo(Collections.emptyMap()); + return properties; + } } } diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/pom.xml index 6b98ad05010f..78b805051f40 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/pom.xml @@ -238,6 +238,12 @@ org.springframework.boot spring-boot-starter-test test + + + org.springframework.boot + spring-boot-starter-logging + + @@ -347,4 +353,310 @@ + + + + spring-boot-3 + + 9.0.117 + 2.7.18 + + + + + org.springframework.boot + spring-boot + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-actuator + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-actuator-autoconfigure + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-aop + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-json + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-validation + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot-2.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${spring-boot-2.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${spring-boot-2.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${spring-boot-2.tomcat.version} + + + + + + spring-boot-4 + + 2.7.18 + 5.3.31 + 9.0.117 + + + + + org.springframework + spring-aop + ${spring-boot-2.spring.version} + + + org.springframework + spring-aspects + ${spring-boot-2.spring.version} + + + org.springframework + spring-beans + ${spring-boot-2.spring.version} + + + org.springframework + spring-context + ${spring-boot-2.spring.version} + + + org.springframework + spring-context-indexer + ${spring-boot-2.spring.version} + + + org.springframework + spring-context-support + ${spring-boot-2.spring.version} + + + org.springframework + spring-core + ${spring-boot-2.spring.version} + + + org.springframework + spring-expression + ${spring-boot-2.spring.version} + + + org.springframework + spring-instrument + ${spring-boot-2.spring.version} + + + org.springframework + spring-jdbc + ${spring-boot-2.spring.version} + + + org.springframework + spring-test + ${spring-boot-2.spring.version} + + + org.springframework + spring-tx + ${spring-boot-2.spring.version} + + + org.springframework + spring-web + ${spring-boot-2.spring.version} + + + org.springframework + spring-webflux + ${spring-boot-2.spring.version} + + + org.springframework + spring-webmvc + ${spring-boot-2.spring.version} + + + org.springframework + spring-websocket + ${spring-boot-2.spring.version} + + + org.springframework.boot + spring-boot + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-actuator + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-actuator-autoconfigure + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-aop + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-json + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-validation + ${spring-boot-2.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot-2.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${spring-boot-2.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${spring-boot-2.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${spring-boot-2.tomcat.version} + + + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot4DependencyCheckAutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot4DependencyCheckAutoConfiguration.java new file mode 100644 index 000000000000..541e93f08fee --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot4DependencyCheckAutoConfiguration.java @@ -0,0 +1,91 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.apache.dubbo.spring.boot.autoconfigure.observability.annotation.ConditionalOnDubboTracingEnable; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Conditional(SpringBoot4Condition.class) +public class DubboSpringBoot4DependencyCheckAutoConfiguration { + + public static final String SERVLET_PREFIX = "dubbo.protocol.triple.servlet"; + + public static final String WEBSOCKET_PREFIX = "dubbo.protocol.triple.websocket"; + + public static final String JAKARATA_SERVLET_FILTER = "jakarta.servlet.Filter"; + + public static final String ZIPKIN_SENDER = "zipkin2.reporter.Sender"; + + public static final String ZIPKIN_CONFIG_PREFIX = "dubbo.tracing.tracing-exporter.zipkin-config"; + + public static final String DUBBO_TRIPLE_4_AUTOCONFIGURATION = + "org.apache.dubbo.spring.boot.autoconfigure.DubboTriple4AutoConfiguration"; + + public static final String DUBBO_ZIPKIN_4_AUTOCONFIGURATION = + "org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.DubboZipkin4AutoConfiguration"; + + private static final String SPRING_BOOT_4_DEPENDENCY_CHECK_WARNING = + "Couldn't enable servlet support for triple at SpringBoot4: Missing dubbo-spring-boot-4-autoconfigure"; + + private static final String SPRING_BOOT_4_ZIPKIN_DEPENDENCY_CHECK_WARNING = + "Couldn't enable Zipkin tracing at SpringBoot4: Missing dubbo-spring-boot-4-autoconfigure"; + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = JAKARATA_SERVLET_FILTER) + @ConditionalOnWebApplication(type = Type.SERVLET) + @ConditionalOnProperty(prefix = SERVLET_PREFIX, name = "enabled", havingValue = "true") + @ConditionalOnMissingClass(DUBBO_TRIPLE_4_AUTOCONFIGURATION) + public static class tripleProtocolFilterDependencyCheckConfiguration { + @Bean + public Object tripleProtocolFilterDependencyCheck() { + throw new IllegalStateException(SPRING_BOOT_4_DEPENDENCY_CHECK_WARNING); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = JAKARATA_SERVLET_FILTER) + @ConditionalOnWebApplication(type = Type.SERVLET) + @ConditionalOnProperty(prefix = WEBSOCKET_PREFIX, name = "enabled", havingValue = "true") + @ConditionalOnMissingClass(DUBBO_TRIPLE_4_AUTOCONFIGURATION) + public static class tripleWebSocketFilterDependencyCheckConfiguration { + @Bean + public Object tripleWebSocketFilterDependencyCheck() { + throw new IllegalStateException(SPRING_BOOT_4_DEPENDENCY_CHECK_WARNING); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = ZIPKIN_SENDER) + @ConditionalOnProperty(prefix = ZIPKIN_CONFIG_PREFIX, name = "endpoint") + @ConditionalOnDubboTracingEnable + @ConditionalOnMissingClass(DUBBO_ZIPKIN_4_AUTOCONFIGURATION) + public static class zipkinDependencyCheckConfiguration { + @Bean + public Object zipkinDependencyCheck() { + throw new IllegalStateException(SPRING_BOOT_4_ZIPKIN_DEPENDENCY_CHECK_WARNING); + } + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot3Condition.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot3Condition.java index 489446f1d75c..1bad7b9ba937 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot3Condition.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot3Condition.java @@ -23,7 +23,7 @@ public class SpringBoot3Condition implements Condition { - public static boolean IS_SPRING_BOOT_3 = SpringBootVersion.getVersion().charAt(0) >= '3'; + public static boolean IS_SPRING_BOOT_3 = SpringBootVersion.getVersion().charAt(0) == '3'; @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot4Condition.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot4Condition.java new file mode 100644 index 000000000000..db803dab1380 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBoot4Condition.java @@ -0,0 +1,31 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class SpringBoot4Condition implements Condition { + + public static boolean IS_SPRING_BOOT_4 = SpringBootVersionUtils.isSpringBoot4(); + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return IS_SPRING_BOOT_4; + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootBefore4Condition.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootBefore4Condition.java new file mode 100644 index 000000000000..771c4d5d513b --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootBefore4Condition.java @@ -0,0 +1,31 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class SpringBootBefore4Condition implements Condition { + + public static boolean IS_SPRING_BOOT_BEFORE_4 = SpringBootVersionUtils.isBeforeSpringBoot4(); + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return IS_SPRING_BOOT_BEFORE_4; + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootVersionUtils.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootVersionUtils.java new file mode 100644 index 000000000000..b240e9dd5617 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootVersionUtils.java @@ -0,0 +1,78 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.springframework.boot.SpringBootVersion; +import org.springframework.util.ClassUtils; + +final class SpringBootVersionUtils { + + private static final int UNKNOWN_MAJOR_VERSION = -1; + + private static final int SPRING_BOOT_4_MAJOR_VERSION = 4; + + private static final String SPRING_BOOT_4_MARKER_CLASS = + "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean"; + + private SpringBootVersionUtils() {} + + static boolean isSpringBoot4() { + return isSpringBoot4(SpringBootVersion.getVersion()); + } + + static boolean isSpringBoot4(String version) { + int majorVersion = getMajorVersion(version); + if (majorVersion != UNKNOWN_MAJOR_VERSION) { + return majorVersion == SPRING_BOOT_4_MAJOR_VERSION; + } + return isSpringBoot4Present(); + } + + static boolean isBeforeSpringBoot4() { + return isBeforeSpringBoot4(SpringBootVersion.getVersion()); + } + + static boolean isBeforeSpringBoot4(String version) { + int majorVersion = getMajorVersion(version); + if (majorVersion != UNKNOWN_MAJOR_VERSION) { + return majorVersion < SPRING_BOOT_4_MAJOR_VERSION; + } + return !isSpringBoot4Present(); + } + + static int getMajorVersion(String version) { + if (version == null) { + return UNKNOWN_MAJOR_VERSION; + } + String trimmedVersion = version.trim(); + if (trimmedVersion.isEmpty()) { + return UNKNOWN_MAJOR_VERSION; + } + int endIndex = 0; + while (endIndex < trimmedVersion.length() && Character.isDigit(trimmedVersion.charAt(endIndex))) { + endIndex++; + } + if (endIndex == 0) { + return UNKNOWN_MAJOR_VERSION; + } + return Integer.parseInt(trimmedVersion.substring(0, endIndex)); + } + + private static boolean isSpringBoot4Present() { + return ClassUtils.isPresent(SPRING_BOOT_4_MARKER_CLASS, SpringBootVersionUtils.class.getClassLoader()); + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/DubboObservationAutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/DubboObservationAutoConfiguration.java index 46f90c74d8de..b63825bd96a2 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/DubboObservationAutoConfiguration.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/DubboObservationAutoConfiguration.java @@ -58,11 +58,20 @@ @ConditionalOnProperty(prefix = DUBBO_PREFIX, name = "enabled", matchIfMissing = true) @AutoConfiguration( after = DubboMicrometerTracingAutoConfiguration.class, - afterName = "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration") + afterName = { + "org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration", + "org.springframework.boot.micrometer.observation.autoconfigure.ObservationAutoConfiguration" + }) @ConditionalOnDubboTracingEnable @ConditionalOnClass(name = {"io.micrometer.observation.Observation", "io.micrometer.tracing.Tracer"}) public class DubboObservationAutoConfiguration implements BeanFactoryAware, ApplicationListener, Ordered { + private static final String BOOT3_OBSERVATION_REGISTRY_POST_PROCESSOR = + "org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryPostProcessor"; + + private static final String BOOT4_OBSERVATION_REGISTRY_POST_PROCESSOR = + "org.springframework.boot.micrometer.observation.autoconfigure.ObservationRegistryPostProcessor"; + private final ErrorTypeAwareLogger logger = LoggerFactory.getErrorTypeAwareLogger(QosProtocolWrapper.class); public DubboObservationAutoConfiguration(ApplicationModel applicationModel) { @@ -82,7 +91,7 @@ ObservationRegistry observationRegistry() { @Bean @ConditionalOnMissingBean( - type = "org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryPostProcessor") + type = {BOOT3_OBSERVATION_REGISTRY_POST_PROCESSOR, BOOT4_OBSERVATION_REGISTRY_POST_PROCESSOR}) @ConditionalOnClass(name = "io.micrometer.observation.ObservationHandler") public ObservationRegistryPostProcessor dubboObservationRegistryPostProcessor( ObjectProvider observationHandlerGrouping, @@ -119,7 +128,7 @@ public int getOrder() { @ConditionalOnClass(MeterRegistry.class) @ConditionalOnMissingClass("io.micrometer.tracing.Tracer") @ConditionalOnMissingBean( - type = "org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryPostProcessor") + type = {BOOT3_OBSERVATION_REGISTRY_POST_PROCESSOR, BOOT4_OBSERVATION_REGISTRY_POST_PROCESSOR}) static class OnlyMetricsConfiguration { @Bean @@ -133,7 +142,7 @@ ObservationHandlerGrouping metricsObservationHandlerGrouping() { @ConditionalOnClass(io.micrometer.tracing.Tracer.class) @ConditionalOnMissingClass("io.micrometer.core.instrument.MeterRegistry") @ConditionalOnMissingBean( - type = "org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryPostProcessor") + type = {BOOT3_OBSERVATION_REGISTRY_POST_PROCESSOR, BOOT4_OBSERVATION_REGISTRY_POST_PROCESSOR}) static class OnlyTracingConfiguration { @Bean @@ -146,7 +155,7 @@ ObservationHandlerGrouping tracingObservationHandlerGrouping() { @Configuration(proxyBeanMethods = false) @ConditionalOnClass({MeterRegistry.class, io.micrometer.tracing.Tracer.class}) @ConditionalOnMissingBean( - type = "org.springframework.boot.actuate.autoconfigure.observation.ObservationRegistryPostProcessor") + type = {BOOT3_OBSERVATION_REGISTRY_POST_PROCESSOR, BOOT4_OBSERVATION_REGISTRY_POST_PROCESSOR}) static class MetricsWithTracingConfiguration { @Bean diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/ZipkinAutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/ZipkinAutoConfiguration.java index 0b46777b5368..8742ffcf6ac8 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/ZipkinAutoConfiguration.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/java/org/apache/dubbo/spring/boot/autoconfigure/observability/zipkin/ZipkinAutoConfiguration.java @@ -16,6 +16,7 @@ */ package org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin; +import org.apache.dubbo.spring.boot.autoconfigure.SpringBootBefore4Condition; import org.apache.dubbo.spring.boot.autoconfigure.observability.annotation.ConditionalOnDubboTracingEnable; import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.ZipkinConfigurations.BraveConfiguration; import org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.ZipkinConfigurations.OpenTelemetryConfiguration; @@ -27,8 +28,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Import; import zipkin2.Span; import zipkin2.codec.BytesEncoder; @@ -49,8 +50,10 @@ */ @ConditionalOnProperty(prefix = DUBBO_PREFIX, name = "enabled", matchIfMissing = true) @AutoConfiguration( - after = RestTemplateAutoConfiguration.class, - afterName = "org.springframework.boot.actuate.autoconfigure.tracing.zipkin") + afterName = { + "org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration", + "org.springframework.boot.actuate.autoconfigure.tracing.zipkin.ZipkinAutoConfiguration" + }) @ConditionalOnClass(Sender.class) @Import({ SenderConfiguration.class, @@ -59,6 +62,7 @@ OpenTelemetryConfiguration.class }) @ConditionalOnDubboTracingEnable +@Conditional(SpringBootBefore4Condition.class) public class ZipkinAutoConfiguration { @Bean diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 5256a723cd8e..a72642e67b01 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -9,4 +9,5 @@ org.apache.dubbo.spring.boot.autoconfigure.observability.DubboObservationAutoCon org.apache.dubbo.spring.boot.autoconfigure.observability.brave.BraveAutoConfiguration,\ org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.ZipkinAutoConfiguration,\ org.apache.dubbo.spring.boot.autoconfigure.observability.otlp.OtlpAutoConfiguration,\ -org.apache.dubbo.spring.boot.autoconfigure.DubboSpringBoot3DependencyCheckAutoConfiguration +org.apache.dubbo.spring.boot.autoconfigure.DubboSpringBoot3DependencyCheckAutoConfiguration,\ +org.apache.dubbo.spring.boot.autoconfigure.DubboSpringBoot4DependencyCheckAutoConfiguration diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index c92a1ed528c2..cd23d61b1004 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -9,3 +9,4 @@ org.apache.dubbo.spring.boot.autoconfigure.observability.brave.BraveAutoConfigur org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.ZipkinAutoConfiguration org.apache.dubbo.spring.boot.autoconfigure.observability.otlp.OtlpAutoConfiguration org.apache.dubbo.spring.boot.autoconfigure.DubboSpringBoot3DependencyCheckAutoConfiguration +org.apache.dubbo.spring.boot.autoconfigure.DubboSpringBoot4DependencyCheckAutoConfiguration diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/jakarta/servlet/Filter.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/jakarta/servlet/Filter.java new file mode 100644 index 000000000000..e4cd7a97a6ea --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/jakarta/servlet/Filter.java @@ -0,0 +1,19 @@ +/* + * 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 jakarta.servlet; + +public interface Filter {} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot3DependencyCheckAutoConfigurationTests.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot3DependencyCheckAutoConfigurationTests.java new file mode 100644 index 000000000000..111af741e67d --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboSpringBoot3DependencyCheckAutoConfigurationTests.java @@ -0,0 +1,90 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +class DubboSpringBoot3DependencyCheckAutoConfigurationTests { + + private static final String MISSING_BOOT3_AUTOCONFIGURE = + "Couldn't enable servlet support for triple at SpringBoot3: Missing dubbo-spring-boot-3-autoconfigure"; + + private final boolean springBoot3 = SpringBoot3Condition.IS_SPRING_BOOT_3; + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DubboSpringBoot3DependencyCheckAutoConfiguration.class)) + .withClassLoader(new FilteredClassLoader(DubboTriple3AutoConfiguration.class)); + + private final WebApplicationContextRunner contextRunnerWithSpringBoot3Autoconfigure = + new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DubboSpringBoot3DependencyCheckAutoConfiguration.class)); + + @BeforeEach + void enableSpringBoot3Condition() { + SpringBoot3Condition.IS_SPRING_BOOT_3 = true; + } + + @AfterEach + void resetSpringBoot3Condition() { + SpringBoot3Condition.IS_SPRING_BOOT_3 = springBoot3; + } + + @Test + void shouldFailWhenTripleServletEnabledWithoutSpringBoot3Autoconfigure() { + this.contextRunner + .withPropertyValues("dubbo.protocol.triple.servlet.enabled=true") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseMessage(MISSING_BOOT3_AUTOCONFIGURE); + }); + } + + @Test + void shouldFailWhenTripleWebSocketEnabledWithoutSpringBoot3Autoconfigure() { + this.contextRunner + .withPropertyValues("dubbo.protocol.triple.websocket.enabled=true") + .run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).hasRootCauseMessage(MISSING_BOOT3_AUTOCONFIGURE); + }); + } + + @Test + void shouldNotFailWhenSpringBoot3AutoconfigureIsPresent() { + this.contextRunnerWithSpringBoot3Autoconfigure + .withPropertyValues( + "dubbo.protocol.triple.servlet.enabled=true", "dubbo.protocol.triple.websocket.enabled=true") + .run(context -> assertThat(context).hasNotFailed()); + } + + @Test + void shouldNotFailWhenSpringBoot3ConditionIsDisabled() { + SpringBoot3Condition.IS_SPRING_BOOT_3 = false; + + this.contextRunner + .withPropertyValues( + "dubbo.protocol.triple.servlet.enabled=true", "dubbo.protocol.triple.websocket.enabled=true") + .run(context -> assertThat(context).hasNotFailed()); + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple3AutoConfiguration.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple3AutoConfiguration.java new file mode 100644 index 000000000000..30a7e6a0bcd9 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/DubboTriple3AutoConfiguration.java @@ -0,0 +1,19 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +public class DubboTriple3AutoConfiguration {} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootConditionTests.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootConditionTests.java new file mode 100644 index 000000000000..3f4196c4e431 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/SpringBootConditionTests.java @@ -0,0 +1,70 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class SpringBootConditionTests { + + private final boolean springBootBefore4 = SpringBootBefore4Condition.IS_SPRING_BOOT_BEFORE_4; + private final boolean springBoot4 = SpringBoot4Condition.IS_SPRING_BOOT_4; + + @AfterEach + void resetConditions() { + SpringBootBefore4Condition.IS_SPRING_BOOT_BEFORE_4 = springBootBefore4; + SpringBoot4Condition.IS_SPRING_BOOT_4 = springBoot4; + } + + @Test + void shouldMatchOnlyBefore4ConditionBeforeSpringBoot4() { + SpringBootBefore4Condition.IS_SPRING_BOOT_BEFORE_4 = true; + SpringBoot4Condition.IS_SPRING_BOOT_4 = false; + + Assertions.assertTrue(new SpringBootBefore4Condition().matches(null, null)); + Assertions.assertFalse(new SpringBoot4Condition().matches(null, null)); + } + + @Test + void shouldMatchOnlySpringBoot4ConditionOnSpringBoot4() { + SpringBootBefore4Condition.IS_SPRING_BOOT_BEFORE_4 = false; + SpringBoot4Condition.IS_SPRING_BOOT_4 = true; + + Assertions.assertFalse(new SpringBootBefore4Condition().matches(null, null)); + Assertions.assertTrue(new SpringBoot4Condition().matches(null, null)); + } + + @Test + void shouldParseSpringBootVersionsNullSafely() { + Assertions.assertTrue(SpringBootVersionUtils.getMajorVersion(null) < 0); + Assertions.assertTrue(SpringBootVersionUtils.getMajorVersion("") < 0); + Assertions.assertEquals(3, SpringBootVersionUtils.getMajorVersion("3.5.14")); + Assertions.assertEquals(4, SpringBootVersionUtils.getMajorVersion("4.0.6")); + + Assertions.assertFalse(SpringBootVersionUtils.isSpringBoot4("3.5.14")); + Assertions.assertTrue(SpringBootVersionUtils.isBeforeSpringBoot4("3.5.14")); + Assertions.assertTrue(SpringBootVersionUtils.isSpringBoot4("4.0.6")); + Assertions.assertFalse(SpringBootVersionUtils.isBeforeSpringBoot4("4.0.6")); + } + + @Test + void shouldEvaluateConditionsWhenSpringBootVersionIsUnavailable() { + Assertions.assertDoesNotThrow(() -> SpringBootVersionUtils.isSpringBoot4(null)); + Assertions.assertDoesNotThrow(() -> SpringBootVersionUtils.isBeforeSpringBoot4(null)); + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/observability/DubboObservationAutoConfigurationTests.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/observability/DubboObservationAutoConfigurationTests.java new file mode 100644 index 000000000000..35582daf5bd2 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/apache/dubbo/spring/boot/autoconfigure/observability/DubboObservationAutoConfigurationTests.java @@ -0,0 +1,69 @@ +/* + * 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 org.apache.dubbo.spring.boot.autoconfigure.observability; + +import org.apache.dubbo.rpc.model.ApplicationModel; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class DubboObservationAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DubboObservationAutoConfiguration.class)) + .withUserConfiguration(ApplicationModelConfiguration.class) + .withPropertyValues("dubbo.tracing.enabled=true"); + + @Test + void shouldBackOffObservationRegistryPostProcessorWhenSpringBoot4ProvidesOne() { + this.contextRunner + .withUserConfiguration(SpringBoot4ObservationConfiguration.class) + .run((context) -> { + assertThat(context) + .hasSingleBean( + org.springframework.boot.micrometer.observation.autoconfigure + .ObservationRegistryPostProcessor.class); + assertThat(context).doesNotHaveBean(ObservationRegistryPostProcessor.class); + assertThat(context).doesNotHaveBean(ObservationHandlerGrouping.class); + }); + } + + @Configuration(proxyBeanMethods = false) + private static class ApplicationModelConfiguration { + + @Bean + ApplicationModel applicationModel() { + return mock(ApplicationModel.class); + } + } + + @Configuration(proxyBeanMethods = false) + private static class SpringBoot4ObservationConfiguration { + + @Bean + org.springframework.boot.micrometer.observation.autoconfigure.ObservationRegistryPostProcessor + springBoot4ObservationRegistryPostProcessor() { + return new org.springframework.boot.micrometer.observation.autoconfigure.ObservationRegistryPostProcessor(); + } + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/springframework/boot/micrometer/observation/autoconfigure/ObservationRegistryPostProcessor.java b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/springframework/boot/micrometer/observation/autoconfigure/ObservationRegistryPostProcessor.java new file mode 100644 index 000000000000..da5a223c1ff8 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-autoconfigure/src/test/java/org/springframework/boot/micrometer/observation/autoconfigure/ObservationRegistryPostProcessor.java @@ -0,0 +1,19 @@ +/* + * 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 org.springframework.boot.micrometer.observation.autoconfigure; + +public class ObservationRegistryPostProcessor {} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-compatible/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-compatible/pom.xml index 0b9bf7cbda23..bb332ea98de2 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-compatible/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot-compatible/pom.xml @@ -35,6 +35,7 @@ 1.5.22.RELEASE + 8.5.43 4.11.0 @@ -46,6 +47,81 @@ ${mockito.version} test + + org.springframework.boot + spring-boot + ${spring-boot.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot.version} + + + org.springframework.boot + spring-boot-actuator + ${spring-boot.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${spring-boot.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${spring-boot.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${spring-boot.tomcat.version} + org.springframework.boot spring-boot-dependencies @@ -92,6 +168,7 @@ spring-boot-1.4 1.4.7.RELEASE + 8.5.15 diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-spring-boot-4-starter/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-spring-boot-4-starter/pom.xml new file mode 100644 index 000000000000..89ea76d15e21 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-spring-boot-4-starter/pom.xml @@ -0,0 +1,83 @@ + + + + 4.0.0 + + org.apache.dubbo + dubbo-spring-boot-starters + ${revision} + ../pom.xml + + + dubbo-spring-boot-4-starter + jar + Apache Dubbo Spring Boot 4 Starter + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot-4.version} + pom + import + + + org.springframework.boot + spring-boot + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot-4.version} + + + + + + + + org.springframework.boot + spring-boot-starter + true + + + + org.apache.dubbo + dubbo-spring-boot-autoconfigure + ${project.version} + + + + org.apache.dubbo + dubbo-spring-boot-4-autoconfigure + ${project.version} + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-tracing-brave-zipkin-spring-boot-starter/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-tracing-brave-zipkin-spring-boot-starter/pom.xml index 7de16bdbde59..7d4619e330e0 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-tracing-brave-zipkin-spring-boot-starter/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-tracing-brave-zipkin-spring-boot-starter/pom.xml @@ -52,4 +52,17 @@ + + + spring-boot-4 + + + org.apache.dubbo + dubbo-spring-boot-4-autoconfigure + ${project.version} + + + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-tracing-otel-zipkin-spring-boot-starter/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-tracing-otel-zipkin-spring-boot-starter/pom.xml index b999ce847b51..222ad71db8f7 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-tracing-otel-zipkin-spring-boot-starter/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot-starters/dubbo-tracing-otel-zipkin-spring-boot-starter/pom.xml @@ -44,4 +44,17 @@ + + + spring-boot-4 + + + org.apache.dubbo + dubbo-spring-boot-4-autoconfigure + ${project.version} + + + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot-starters/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot-starters/pom.xml index d3bcc63bcb0c..298a7f16e29c 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot-starters/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot-starters/pom.xml @@ -41,4 +41,16 @@ dubbo-observability-spring-boot-starter + + + jdk-version-ge-17 + + [17,) + + + dubbo-spring-boot-4-starter + + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot/pom.xml b/dubbo-spring-boot-project/dubbo-spring-boot/pom.xml index a43652782d67..b78395874851 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot/pom.xml +++ b/dubbo-spring-boot-project/dubbo-spring-boot/pom.xml @@ -52,6 +52,11 @@ spring-boot-starter-web true + + javax.servlet + javax.servlet-api + provided + org.apache.dubbo dubbo-rpc-api @@ -95,4 +100,140 @@ + + + spring-boot-before-4 + + true + + + 2.7.18 + 9.0.117 + + + + + org.springframework.boot + spring-boot + ${spring-boot.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-json + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${spring-boot.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${spring-boot.tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${spring-boot.tomcat.version} + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-spring-boot-before-4-sources + + add-source + + generate-sources + + + src/spring-boot-before-4/java + + + + + + + + + + spring-boot-4 + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-spring-boot-4-sources + + add-source + + generate-sources + + + src/spring-boot-4/java + + + + + + + + + + diff --git a/dubbo-spring-boot-project/dubbo-spring-boot/src/main/java/org/apache/dubbo/spring/boot/env/DubboDefaultPropertiesEnvironmentPostProcessor.java b/dubbo-spring-boot-project/dubbo-spring-boot/src/main/java/org/apache/dubbo/spring/boot/env/DubboDefaultPropertiesEnvironmentPostProcessor.java index c2180f6a48e5..dc50c63afad3 100644 --- a/dubbo-spring-boot-project/dubbo-spring-boot/src/main/java/org/apache/dubbo/spring/boot/env/DubboDefaultPropertiesEnvironmentPostProcessor.java +++ b/dubbo-spring-boot-project/dubbo-spring-boot/src/main/java/org/apache/dubbo/spring/boot/env/DubboDefaultPropertiesEnvironmentPostProcessor.java @@ -41,6 +41,7 @@ * {@link SpringApplication#setDefaultProperties(Properties) Spring Boot default properties} for Dubbo * as late as possible before {@link ConfigurableApplicationContext#refresh() application context refresh}. */ +@SuppressWarnings("removal") public class DubboDefaultPropertiesEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { /** diff --git a/dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagCookieInterceptor.java b/dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagCookieInterceptor.java new file mode 100644 index 000000000000..a701d211977a --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagCookieInterceptor.java @@ -0,0 +1,55 @@ +/* + * 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 org.apache.dubbo.spring.boot.interceptor; + +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.rpc.RpcContext; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.HandlerInterceptor; + +public class DubboTagCookieInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String tag = getSingleCookieValue(request.getCookies(), CommonConstants.TAG_KEY); + RpcContext.getClientAttachment().setAttachment(CommonConstants.TAG_KEY, tag); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + RpcContext.getClientAttachment().removeAttachment(CommonConstants.TAG_KEY); + } + + private static String getSingleCookieValue(Cookie[] cookies, String name) { + if (cookies == null || cookies.length == 0) { + return null; + } + for (Cookie cookie : cookies) { + if (name.equals(cookie.getName())) { + return cookie.getValue(); + } + } + return null; + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagHeaderOrParameterInterceptor.java b/dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagHeaderOrParameterInterceptor.java new file mode 100644 index 000000000000..fb2782e80058 --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagHeaderOrParameterInterceptor.java @@ -0,0 +1,47 @@ +/* + * 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 org.apache.dubbo.spring.boot.interceptor; + +import org.apache.dubbo.common.constants.CommonConstants; +import org.apache.dubbo.rpc.RpcContext; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.HandlerInterceptor; + +public class DubboTagHeaderOrParameterInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String tag = request.getHeader(CommonConstants.DUBBO_TAG_HEADER); + if (tag == null) { + tag = request.getParameter(CommonConstants.TAG_KEY); + } + if (tag != null) { + RpcContext.getClientAttachment().setAttachment(CommonConstants.TAG_KEY, tag); + } + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + throws Exception { + RpcContext.getClientAttachment().removeAttachment(CommonConstants.TAG_KEY); + } +} diff --git a/dubbo-spring-boot-project/dubbo-spring-boot/src/main/java/org/apache/dubbo/spring/boot/interceptor/DubboTagCookieInterceptor.java b/dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-before-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagCookieInterceptor.java similarity index 100% rename from dubbo-spring-boot-project/dubbo-spring-boot/src/main/java/org/apache/dubbo/spring/boot/interceptor/DubboTagCookieInterceptor.java rename to dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-before-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagCookieInterceptor.java diff --git a/dubbo-spring-boot-project/dubbo-spring-boot/src/main/java/org/apache/dubbo/spring/boot/interceptor/DubboTagHeaderOrParameterInterceptor.java b/dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-before-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagHeaderOrParameterInterceptor.java similarity index 100% rename from dubbo-spring-boot-project/dubbo-spring-boot/src/main/java/org/apache/dubbo/spring/boot/interceptor/DubboTagHeaderOrParameterInterceptor.java rename to dubbo-spring-boot-project/dubbo-spring-boot/src/spring-boot-before-4/java/org/apache/dubbo/spring/boot/interceptor/DubboTagHeaderOrParameterInterceptor.java diff --git a/dubbo-spring-boot-project/dubbo-spring-boot/src/test/resources/META-INF/dubbo.properties b/dubbo-spring-boot-project/dubbo-spring-boot/src/test/resources/META-INF/dubbo.properties new file mode 100644 index 000000000000..bc3b7b15384d --- /dev/null +++ b/dubbo-spring-boot-project/dubbo-spring-boot/src/test/resources/META-INF/dubbo.properties @@ -0,0 +1 @@ +dubbo.application.id = test-dubbo-application diff --git a/dubbo-test/dubbo-dependencies-all/pom.xml b/dubbo-test/dubbo-dependencies-all/pom.xml index 9d5b67d66efb..5557a3671349 100644 --- a/dubbo-test/dubbo-dependencies-all/pom.xml +++ b/dubbo-test/dubbo-dependencies-all/pom.xml @@ -499,6 +499,16 @@ dubbo-spring-boot-3-autoconfigure ${project.version} + + org.apache.dubbo + dubbo-spring-boot-4-autoconfigure + ${project.version} + + + org.apache.dubbo + dubbo-spring-boot-4-starter + ${project.version} + diff --git a/dubbo-test/dubbo-test-modules/src/test/java/org/apache/dubbo/dependency/FileTest.java b/dubbo-test/dubbo-test-modules/src/test/java/org/apache/dubbo/dependency/FileTest.java index fdc0d7d95944..07cc9b83d21a 100644 --- a/dubbo-test/dubbo-test-modules/src/test/java/org/apache/dubbo/dependency/FileTest.java +++ b/dubbo-test/dubbo-test-modules/src/test/java/org/apache/dubbo/dependency/FileTest.java @@ -57,6 +57,8 @@ class FileTest { ignoredModules.add(Pattern.compile("dubbo-config-spring6")); ignoredModules.add(Pattern.compile("dubbo-spring6-security")); ignoredModules.add(Pattern.compile("dubbo-spring-boot-3-autoconfigure")); + ignoredModules.add(Pattern.compile("dubbo-spring-boot-4-autoconfigure")); + ignoredModules.add(Pattern.compile("dubbo-spring-boot-4-starter")); ignoredModules.add(Pattern.compile("dubbo-plugin-loom.*")); ignoredModules.add(Pattern.compile("dubbo-mutiny.*")); ignoredModules.add(Pattern.compile("dubbo-mcp")); @@ -208,6 +210,630 @@ void checkDubboDependenciesAll() throws DocumentException { "Newly created modules must be added to dubbo-dependencies-all. Found modules: " + expectedArtifactIds); } + @Test + void checkZipkinSpringBootStartersSupportSpringBoot4() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + List zipkinStarters = new LinkedList<>(); + zipkinStarters.add("dubbo-tracing-brave-zipkin-spring-boot-starter"); + zipkinStarters.add("dubbo-tracing-otel-zipkin-spring-boot-starter"); + + for (String zipkinStarter : zipkinStarters) { + String pomPath = "dubbo-spring-boot-project" + File.separator + "dubbo-spring-boot-starters" + + File.separator + zipkinStarter + File.separator + "pom.xml"; + Document document = reader.read(new File(baseFile, pomPath)); + + Assertions.assertTrue( + hasProfileDependency( + document.getRootElement(), "spring-boot-4", "dubbo-spring-boot-4-autoconfigure"), + zipkinStarter + " must depend on dubbo-spring-boot-4-autoconfigure for Spring Boot 4"); + Assertions.assertFalse( + hasProfileDependency( + document.getRootElement(), "jdk-version-ge-17", "dubbo-spring-boot-4-autoconfigure"), + zipkinStarter + " must not enable Spring Boot 4 autoconfigure only because the JDK is 17+"); + } + } + + @Test + void checkSpringBootDemosSupportSpringBoot4SmokeCompile() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + List demoPomPaths = new LinkedList<>(); + demoPomPaths.add("dubbo-demo" + File.separator + "dubbo-demo-spring-boot" + File.separator + + "dubbo-demo-spring-boot-servlet" + File.separator + "pom.xml"); + demoPomPaths.add("dubbo-demo" + File.separator + "dubbo-demo-spring-boot-idl" + File.separator + "pom.xml"); + demoPomPaths.add("dubbo-demo" + File.separator + "dubbo-demo-mcp-server" + File.separator + "pom.xml"); + + for (String demoPomPath : demoPomPaths) { + Document document = reader.read(new File(baseFile, demoPomPath)); + + Assertions.assertTrue( + hasProfileDependency( + document.getRootElement(), "spring-boot-4", "dubbo-spring-boot-4-autoconfigure"), + demoPomPath + " must depend on dubbo-spring-boot-4-autoconfigure for Spring Boot 4 smoke compile"); + Assertions.assertTrue( + hasProfileDependency( + document.getRootElement(), "spring-boot-3", "dubbo-spring-boot-3-autoconfigure"), + demoPomPath + " must depend on dubbo-spring-boot-3-autoconfigure only for Spring Boot 3"); + Assertions.assertFalse( + hasProfileDependency( + document.getRootElement(), "jdk-version-ge-17", "dubbo-spring-boot-3-autoconfigure"), + demoPomPath + " must not enable Spring Boot 3 autoconfigure only because the JDK is 17+"); + } + } + + @Test + void checkSpringBootIdlDemoUsesRequestedSpringBootProfiles() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String demoPomPath = "dubbo-demo" + File.separator + "dubbo-demo-spring-boot-idl" + File.separator + "pom.xml"; + Element rootElement = reader.read(new File(baseFile, demoPomPath)).getRootElement(); + + Assertions.assertTrue( + hasProfileProperty(rootElement, "spring-boot-3", "spring-boot.version", "${spring-boot-3.version}"), + demoPomPath + " must use ${spring-boot-3.version} under spring-boot-3"); + Assertions.assertTrue( + hasProfileProperty(rootElement, "spring-boot-4", "spring-boot.version", "${spring-boot-4.version}"), + demoPomPath + " must use ${spring-boot-4.version} under spring-boot-4"); + } + + @Test + void checkSpringBootDemoUsesRequestedSpringBootProfiles() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String demoPomPath = "dubbo-demo" + File.separator + "dubbo-demo-spring-boot" + File.separator + "pom.xml"; + Element rootElement = reader.read(new File(baseFile, demoPomPath)).getRootElement(); + + Assertions.assertTrue( + hasProfileProperty(rootElement, "spring-boot-3", "spring-boot.version", "${spring-boot-3.version}"), + demoPomPath + " must use ${spring-boot-3.version} under spring-boot-3"); + Assertions.assertTrue( + hasProfileProperty( + rootElement, "spring-boot-3", "spring-boot-maven-plugin.version", "${spring-boot-3.version}"), + demoPomPath + + " must use ${spring-boot-3.version} for the Spring Boot Maven plugin under spring-boot-3"); + Assertions.assertTrue( + hasProfileProperty(rootElement, "spring-boot-4", "spring-boot.version", "${spring-boot-4.version}"), + demoPomPath + " must use ${spring-boot-4.version} under spring-boot-4"); + Assertions.assertTrue( + hasProfileProperty( + rootElement, "spring-boot-4", "spring-boot-maven-plugin.version", "${spring-boot-4.version}"), + demoPomPath + + " must use ${spring-boot-4.version} for the Spring Boot Maven plugin under spring-boot-4"); + } + + @Test + void checkSpringBootMcpServerUsesRequestedSpringBootPluginProfiles() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String demoPomPath = "dubbo-demo" + File.separator + "dubbo-demo-mcp-server" + File.separator + "pom.xml"; + Element rootElement = reader.read(new File(baseFile, demoPomPath)).getRootElement(); + + Assertions.assertTrue( + hasProfileProperty( + rootElement, "spring-boot-3", "spring-boot-maven-plugin.version", "${spring-boot-3.version}"), + demoPomPath + + " must use ${spring-boot-3.version} for the Spring Boot Maven plugin under spring-boot-3"); + Assertions.assertTrue( + hasProfileProperty( + rootElement, "spring-boot-4", "spring-boot-maven-plugin.version", "${spring-boot-4.version}"), + demoPomPath + + " must use ${spring-boot-4.version} for the Spring Boot Maven plugin under spring-boot-4"); + } + + @Test + void checkSpringBootModulesKeepVersionedSourceProfiles() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String[][] modules = { + { + "dubbo-spring-boot-project" + File.separator + "dubbo-spring-boot" + File.separator + "pom.xml", + "src/spring-boot-before-4/java", + "src/spring-boot-4/java" + }, + { + "dubbo-spring-boot-project" + File.separator + "dubbo-spring-boot-actuator" + File.separator + + "pom.xml", + "src/spring-boot-before-4/java", + "src/spring-boot-4/java" + }, + { + "dubbo-plugin" + File.separator + "dubbo-rest-spring" + File.separator + "pom.xml", + "src/spring-boot-before-4/java", + "src/spring-boot-4/java" + } + }; + + for (String[] module : modules) { + Element rootElement = reader.read(new File(baseFile, module[0])).getRootElement(); + Assertions.assertTrue( + hasProfileSource(rootElement, "spring-boot-before-4", module[1]), + module[0] + " must keep its Spring Boot before 4 source set"); + Assertions.assertTrue( + hasProfileSource(rootElement, "spring-boot-4", module[2]), + module[0] + " must keep its Spring Boot 4 source set"); + } + } + + @Test + void checkDubboConfigSpringUsesSpringBoot4Profile() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String pomPath = "dubbo-config" + File.separator + "dubbo-config-spring" + File.separator + "pom.xml"; + Element rootElement = reader.read(new File(baseFile, pomPath)).getRootElement(); + + Assertions.assertTrue( + hasProfileProperty(rootElement, "spring-boot-4", "spring-boot.version", "${spring-boot-4.version}"), + pomPath + " must use ${spring-boot-4.version} under spring-boot-4"); + Assertions.assertTrue( + hasProfileProperty(rootElement, "spring-boot-4", "spring-boot.tomcat.version", "${tomcat.version}"), + pomPath + " must use ${tomcat.version} under spring-boot-4"); + } + + @Test + void checkDubboBomIncludesSpringBoot4Artifacts() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String bomPath = "dubbo-distribution" + File.separator + "dubbo-bom" + File.separator + "pom.xml"; + Element rootElement = reader.read(new File(baseFile, bomPath)).getRootElement(); + String[] artifactIds = { + "dubbo-spring-boot-3-autoconfigure", "dubbo-spring-boot-4-autoconfigure", "dubbo-spring-boot-4-starter" + }; + + for (String artifactId : artifactIds) { + Assertions.assertTrue( + hasManagedDependency(rootElement, "org.apache.dubbo", artifactId, "${project.version}"), + "dubbo-bom must include " + artifactId); + } + } + + @Test + void checkSpringBootDependencyCheckAutoConfigurationsRegistered() throws IOException { + File baseFile = getBaseFile(); + String autoConfigurationImportsPath = "dubbo-spring-boot-project" + File.separator + + "dubbo-spring-boot-autoconfigure" + File.separator + "src" + File.separator + "main" + + File.separator + "resources" + File.separator + "META-INF" + File.separator + "spring" + + File.separator + "org.springframework.boot.autoconfigure.AutoConfiguration.imports"; + String springFactoriesPath = "dubbo-spring-boot-project" + File.separator + "dubbo-spring-boot-autoconfigure" + + File.separator + "src" + File.separator + "main" + File.separator + "resources" + File.separator + + "META-INF" + File.separator + "spring.factories"; + + String[] dependencyCheckAutoConfigurations = { + "org.apache.dubbo.spring.boot.autoconfigure.DubboSpringBoot3DependencyCheckAutoConfiguration", + "org.apache.dubbo.spring.boot.autoconfigure.DubboSpringBoot4DependencyCheckAutoConfiguration" + }; + for (String autoConfiguration : dependencyCheckAutoConfigurations) { + Assertions.assertTrue( + hasResourceEntry(baseFile, autoConfigurationImportsPath, autoConfiguration), + autoConfiguration + " must be registered in AutoConfiguration.imports"); + Assertions.assertTrue( + hasResourceEntry(baseFile, springFactoriesPath, autoConfiguration), + autoConfiguration + " must be registered in spring.factories"); + } + } + + @Test + void checkSpringBoot4AutoConfigurationResources() throws IOException { + File baseFile = getBaseFile(); + String autoConfigurationImportsPath = "dubbo-spring-boot-project" + File.separator + + "dubbo-spring-boot-4-autoconfigure" + File.separator + "src" + File.separator + "main" + + File.separator + "resources" + File.separator + "META-INF" + File.separator + "spring" + + File.separator + "org.springframework.boot.autoconfigure.AutoConfiguration.imports"; + String springFactoriesPath = "dubbo-spring-boot-project" + File.separator + "dubbo-spring-boot-4-autoconfigure" + + File.separator + "src" + File.separator + "main" + File.separator + "resources" + File.separator + + "META-INF" + File.separator + "spring.factories"; + + String[] springBoot4AutoConfigurations = { + "org.apache.dubbo.spring.boot.autoconfigure.DubboTriple4AutoConfiguration", + "org.apache.dubbo.spring.boot.autoconfigure.observability.zipkin.DubboZipkin4AutoConfiguration" + }; + for (String autoConfiguration : springBoot4AutoConfigurations) { + Assertions.assertTrue( + hasResourceEntry(baseFile, autoConfigurationImportsPath, autoConfiguration), + autoConfiguration + " must be registered in AutoConfiguration.imports"); + Assertions.assertTrue( + hasResourceEntry(baseFile, springFactoriesPath, autoConfiguration), + autoConfiguration + " must be registered in spring.factories"); + } + } + + @Test + void checkSpringBoot4StarterDependsOnAutoConfigurations() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String springBoot4StarterPath = "dubbo-spring-boot-project" + File.separator + "dubbo-spring-boot-starters" + + File.separator + "dubbo-spring-boot-4-starter" + File.separator + "pom.xml"; + Element rootElement = + reader.read(new File(baseFile, springBoot4StarterPath)).getRootElement(); + + Assertions.assertTrue( + hasDependency(rootElement, "dubbo-spring-boot-autoconfigure"), + "dubbo-spring-boot-4-starter must depend on dubbo-spring-boot-autoconfigure"); + Assertions.assertTrue( + hasDependency(rootElement, "dubbo-spring-boot-4-autoconfigure"), + "dubbo-spring-boot-4-starter must depend on dubbo-spring-boot-4-autoconfigure"); + } + + @Test + void checkDubboDependenciesAllSupportSpringBoot4() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String dubboDependenciesAllPath = + "dubbo-test" + File.separator + "dubbo-dependencies-all" + File.separator + "pom.xml"; + Element rootElement = + reader.read(new File(baseFile, dubboDependenciesAllPath)).getRootElement(); + + Assertions.assertTrue( + hasProfileDependency(rootElement, "jdk-version-ge-17", "dubbo-spring-boot-4-autoconfigure"), + "dubbo-dependencies-all must include dubbo-spring-boot-4-autoconfigure on JDK 17+"); + Assertions.assertTrue( + hasProfileDependency(rootElement, "jdk-version-ge-17", "dubbo-spring-boot-4-starter"), + "dubbo-dependencies-all must include dubbo-spring-boot-4-starter on JDK 17+"); + } + + @Test + void checkRootSpringBoot4Profile() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + Element rootElement = reader.read(new File(baseFile, "pom.xml")).getRootElement(); + + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, "spring-boot-4", "org.junit", "junit-bom", "${junit-bom.version}"), + "spring-boot-4 profile must import junit-bom with ${junit-bom.version}"); + + String[] junitJupiterArtifacts = {"junit-jupiter-api", "junit-jupiter-engine", "junit-jupiter-params"}; + for (String artifactId : junitJupiterArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, "spring-boot-4", "org.junit.jupiter", artifactId, "${junit-bom.version}"), + "spring-boot-4 profile must manage " + artifactId + " with ${junit-bom.version}"); + } + + String[] junitPlatformArtifacts = {"junit-platform-commons", "junit-platform-engine", "junit-platform-launcher" + }; + for (String artifactId : junitPlatformArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, "spring-boot-4", "org.junit.platform", artifactId, "${junit-bom.version}"), + "spring-boot-4 profile must manage " + artifactId + " with ${junit-bom.version}"); + } + + String[] logbackArtifacts = {"logback-classic", "logback-core"}; + for (String artifactId : logbackArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, "spring-boot-4", "ch.qos.logback", artifactId, "${logback-boot-4.version}"), + "spring-boot-4 profile must manage " + artifactId + " with ${logback-boot-4.version}"); + } + + String[] slf4jArtifacts = {"jcl-over-slf4j", "jul-to-slf4j", "log4j-over-slf4j", "slf4j-api"}; + for (String artifactId : slf4jArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, "spring-boot-4", "org.slf4j", artifactId, "${slf4j-boot-4.version}"), + "spring-boot-4 profile must manage " + artifactId + " with ${slf4j-boot-4.version}"); + } + + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, + "spring-boot-4", + "com.fasterxml.jackson.core", + "jackson-annotations", + "${jackson-annotations-boot-4.version}"), + "spring-boot-4 profile must manage jackson-annotations with ${jackson-annotations-boot-4.version}"); + + String[] springBootArtifacts = { + "spring-boot", + "spring-boot-autoconfigure", + "spring-boot-starter", + "spring-boot-test", + "spring-boot-test-autoconfigure", + "spring-boot-starter-test", + "spring-boot-actuator", + "spring-boot-actuator-autoconfigure", + "spring-boot-starter-actuator", + "spring-boot-health", + "spring-boot-micrometer-metrics", + "spring-boot-configuration-processor", + "spring-boot-http-client", + "spring-boot-http-converter", + "spring-boot-jackson", + "spring-boot-restclient", + "spring-boot-starter-aop", + "spring-boot-starter-json", + "spring-boot-starter-log4j2", + "spring-boot-starter-logging", + "spring-boot-starter-tomcat", + "spring-boot-starter-validation", + "spring-boot-starter-web", + "spring-boot-tomcat", + "spring-boot-web-server" + }; + for (String artifactId : springBootArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, + "spring-boot-4", + "org.springframework.boot", + artifactId, + "${spring-boot-4.version}"), + "spring-boot-4 profile must manage " + artifactId + " with ${spring-boot-4.version}"); + } + + String[] springFrameworkArtifacts = { + "spring-aop", + "spring-aspects", + "spring-beans", + "spring-context", + "spring-context-indexer", + "spring-context-support", + "spring-core", + "spring-core-test", + "spring-expression", + "spring-instrument", + "spring-jdbc", + "spring-test", + "spring-tx", + "spring-web", + "spring-webflux", + "spring-webmvc", + "spring-websocket" + }; + for (String artifactId : springFrameworkArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, "spring-boot-4", "org.springframework", artifactId, "${spring-7.version}"), + "spring-boot-4 profile must manage " + artifactId + " with ${spring-7.version}"); + } + + String[] tomcatArtifacts = {"tomcat-embed-core", "tomcat-embed-el", "tomcat-embed-websocket"}; + for (String artifactId : tomcatArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, "spring-boot-4", "org.apache.tomcat.embed", artifactId, "${tomcat.version}"), + "spring-boot-4 profile must manage " + artifactId + " with ${tomcat.version}"); + } + } + + @Test + void checkRootSpringBoot3Profile() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + Element rootElement = reader.read(new File(baseFile, "pom.xml")).getRootElement(); + + String[] springBootArtifacts = { + "spring-boot", + "spring-boot-autoconfigure", + "spring-boot-starter", + "spring-boot-test", + "spring-boot-test-autoconfigure", + "spring-boot-starter-test", + "spring-boot-actuator", + "spring-boot-actuator-autoconfigure", + "spring-boot-starter-actuator", + "spring-boot-configuration-processor", + "spring-boot-starter-aop", + "spring-boot-starter-json", + "spring-boot-starter-log4j2", + "spring-boot-starter-logging", + "spring-boot-starter-tomcat", + "spring-boot-starter-validation", + "spring-boot-starter-web" + }; + for (String artifactId : springBootArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, + "spring-boot-3", + "org.springframework.boot", + artifactId, + "${spring-boot-3.version}"), + "spring-boot-3 profile must manage " + artifactId + " with ${spring-boot-3.version}"); + } + + String[] tomcatArtifacts = {"tomcat-embed-core", "tomcat-embed-el", "tomcat-embed-websocket"}; + for (String artifactId : tomcatArtifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, "spring-boot-3", "org.apache.tomcat.embed", artifactId, "${tomcat.version}"), + "spring-boot-3 profile must manage " + artifactId + " with ${tomcat.version}"); + } + } + + @Test + void checkSpringBootCompatibleKeepsSpringBoot1DependencyManagement() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String springBootCompatiblePath = "dubbo-spring-boot-project" + File.separator + "dubbo-spring-boot-compatible" + + File.separator + "pom.xml"; + Element rootElement = + reader.read(new File(baseFile, springBootCompatiblePath)).getRootElement(); + + String[] springBoot1Artifacts = { + "spring-boot", + "spring-boot-autoconfigure", + "spring-boot-configuration-processor", + "spring-boot-starter", + "spring-boot-test", + "spring-boot-test-autoconfigure", + "spring-boot-actuator", + "spring-boot-starter-actuator", + "spring-boot-starter-log4j2", + "spring-boot-starter-tomcat", + "spring-boot-starter-test", + "spring-boot-starter-web" + }; + for (String artifactId : springBoot1Artifacts) { + Assertions.assertTrue( + hasManagedDependency(rootElement, "org.springframework.boot", artifactId, "${spring-boot.version}"), + "spring-boot-compatible must manage " + artifactId + " with ${spring-boot.version}"); + } + + String[] tomcat8Artifacts = {"tomcat-embed-core", "tomcat-embed-el", "tomcat-embed-websocket"}; + for (String artifactId : tomcat8Artifacts) { + Assertions.assertTrue( + hasManagedDependency( + rootElement, "org.apache.tomcat.embed", artifactId, "${spring-boot.tomcat.version}"), + "spring-boot-compatible must manage " + artifactId + " with ${spring-boot.tomcat.version}"); + } + } + + @Test + void checkSpringBoot3AutoconfigureKeepsSpringBoot3DependencyManagement() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String springBoot3AutoconfigurePath = "dubbo-spring-boot-project" + File.separator + + "dubbo-spring-boot-3-autoconfigure" + File.separator + "pom.xml"; + Element rootElement = + reader.read(new File(baseFile, springBoot3AutoconfigurePath)).getRootElement(); + + String[] springBoot3Artifacts = { + "spring-boot", + "spring-boot-autoconfigure", + "spring-boot-starter", + "spring-boot-test", + "spring-boot-test-autoconfigure", + "spring-boot-starter-test", + "spring-boot-actuator", + "spring-boot-actuator-autoconfigure", + "spring-boot-starter-actuator", + "spring-boot-configuration-processor", + "spring-boot-starter-aop", + "spring-boot-starter-json", + "spring-boot-starter-log4j2", + "spring-boot-starter-logging", + "spring-boot-starter-tomcat", + "spring-boot-starter-validation", + "spring-boot-starter-web" + }; + for (String artifactId : springBoot3Artifacts) { + Assertions.assertTrue( + hasManagedDependency( + rootElement, "org.springframework.boot", artifactId, "${spring-boot-3.version}"), + "dubbo-spring-boot-3-autoconfigure must manage " + artifactId + " with ${spring-boot-3.version}"); + } + String[] tomcat10Artifacts = {"tomcat-embed-core", "tomcat-embed-el", "tomcat-embed-websocket"}; + for (String artifactId : tomcat10Artifacts) { + Assertions.assertTrue( + hasManagedDependency( + rootElement, "org.apache.tomcat.embed", artifactId, "${spring-boot-3.tomcat.version}"), + "dubbo-spring-boot-3-autoconfigure must manage " + artifactId + + " with ${spring-boot-3.tomcat.version}"); + } + } + + @Test + void checkSpringBootAutoconfigureKeepsBefore4DependencyManagement() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String springBootAutoconfigurePath = "dubbo-spring-boot-project" + File.separator + + "dubbo-spring-boot-autoconfigure" + File.separator + "pom.xml"; + Element rootElement = + reader.read(new File(baseFile, springBootAutoconfigurePath)).getRootElement(); + + String[] profileIds = {"spring-boot-3", "spring-boot-4"}; + String[] springBootBefore4Artifacts = { + "spring-boot", + "spring-boot-autoconfigure", + "spring-boot-starter", + "spring-boot-test", + "spring-boot-test-autoconfigure", + "spring-boot-starter-test", + "spring-boot-actuator", + "spring-boot-actuator-autoconfigure", + "spring-boot-starter-actuator", + "spring-boot-configuration-processor", + "spring-boot-starter-aop", + "spring-boot-starter-json", + "spring-boot-starter-log4j2", + "spring-boot-starter-logging", + "spring-boot-starter-tomcat", + "spring-boot-starter-validation", + "spring-boot-starter-web" + }; + String[] tomcatBefore4Artifacts = {"tomcat-embed-core", "tomcat-embed-el", "tomcat-embed-websocket"}; + for (String profileId : profileIds) { + for (String artifactId : springBootBefore4Artifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, + profileId, + "org.springframework.boot", + artifactId, + "${spring-boot-2.version}"), + "dubbo-spring-boot-autoconfigure must manage " + artifactId + + " with ${spring-boot-2.version} under " + profileId); + } + for (String artifactId : tomcatBefore4Artifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, + profileId, + "org.apache.tomcat.embed", + artifactId, + "${spring-boot-2.tomcat.version}"), + "dubbo-spring-boot-autoconfigure must manage " + artifactId + + " with ${spring-boot-2.tomcat.version} under " + profileId); + } + } + } + + @Test + void checkSpringBootActuatorAutoconfigureKeepsBefore4DependencyManagement() throws DocumentException { + File baseFile = getBaseFile(); + SAXReader reader = new SAXReader(); + String springBootActuatorAutoconfigurePath = "dubbo-spring-boot-project" + File.separator + + "dubbo-spring-boot-actuator-autoconfigure" + File.separator + "pom.xml"; + Element rootElement = reader.read(new File(baseFile, springBootActuatorAutoconfigurePath)) + .getRootElement(); + + String[] springBootBefore4Artifacts = { + "spring-boot", + "spring-boot-autoconfigure", + "spring-boot-starter", + "spring-boot-test", + "spring-boot-test-autoconfigure", + "spring-boot-starter-test", + "spring-boot-actuator", + "spring-boot-actuator-autoconfigure", + "spring-boot-starter-actuator", + "spring-boot-configuration-processor", + "spring-boot-starter-aop", + "spring-boot-starter-json", + "spring-boot-starter-log4j2", + "spring-boot-starter-logging", + "spring-boot-starter-tomcat", + "spring-boot-starter-validation", + "spring-boot-starter-web" + }; + for (String artifactId : springBootBefore4Artifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, + "spring-boot-3", + "org.springframework.boot", + artifactId, + "${spring-boot-2.version}"), + "dubbo-spring-boot-actuator-autoconfigure must manage " + artifactId + + " with ${spring-boot-2.version} under spring-boot-3"); + } + + String[] tomcatBefore4Artifacts = {"tomcat-embed-core", "tomcat-embed-el", "tomcat-embed-websocket"}; + for (String artifactId : tomcatBefore4Artifacts) { + Assertions.assertTrue( + hasProfileManagedDependency( + rootElement, + "spring-boot-3", + "org.apache.tomcat.embed", + artifactId, + "${spring-boot-2.tomcat.version}"), + "dubbo-spring-boot-actuator-autoconfigure must manage " + artifactId + + " with ${spring-boot-2.tomcat.version} under spring-boot-3"); + } + } + @Test void checkDubboAllDependencies() throws DocumentException { File baseFile = getBaseFile(); @@ -817,4 +1443,104 @@ public void readSPIUnexpectedResource(File path, List spis) { } } } + + private boolean hasProfileDependency(Element rootElement, String profileId, String artifactId) { + Element profiles = rootElement.element("profiles"); + if (profiles == null) { + return false; + } + return profiles.elements("profile").stream() + .filter(profile -> Objects.equals(profileId, profile.elementText("id"))) + .map(profile -> profile.element("dependencies")) + .filter(Objects::nonNull) + .map(dependencies -> dependencies.elements("dependency")) + .flatMap(Collection::stream) + .anyMatch(dependency -> Objects.equals(artifactId, dependency.elementText("artifactId"))); + } + + private boolean hasProfileProperty(Element rootElement, String profileId, String propertyName, String value) { + Element profiles = rootElement.element("profiles"); + if (profiles == null) { + return false; + } + return profiles.elements("profile").stream() + .filter(profile -> Objects.equals(profileId, profile.elementText("id"))) + .map(profile -> profile.element("properties")) + .filter(Objects::nonNull) + .anyMatch(properties -> Objects.equals(value, properties.elementText(propertyName))); + } + + private boolean hasProfileSource(Element rootElement, String profileId, String source) { + Element profiles = rootElement.element("profiles"); + if (profiles == null) { + return false; + } + return profiles.elements("profile").stream() + .filter(profile -> Objects.equals(profileId, profile.elementText("id"))) + .map(profile -> profile.element("build")) + .filter(Objects::nonNull) + .map(build -> build.element("plugins")) + .filter(Objects::nonNull) + .map(plugins -> plugins.elements("plugin")) + .flatMap(Collection::stream) + .map(plugin -> plugin.element("executions")) + .filter(Objects::nonNull) + .map(executions -> executions.elements("execution")) + .flatMap(Collection::stream) + .map(execution -> execution.element("configuration")) + .filter(Objects::nonNull) + .map(configuration -> configuration.element("sources")) + .filter(Objects::nonNull) + .map(sources -> sources.elements("source")) + .flatMap(Collection::stream) + .anyMatch(sourceElement -> Objects.equals(source, sourceElement.getTextTrim())); + } + + private boolean hasDependency(Element rootElement, String artifactId) { + Element dependencies = rootElement.element("dependencies"); + if (dependencies == null) { + return false; + } + return dependencies.elements("dependency").stream() + .anyMatch(dependency -> Objects.equals(artifactId, dependency.elementText("artifactId"))); + } + + private boolean hasManagedDependency(Element rootElement, String groupId, String artifactId, String version) { + Element dependencyManagement = rootElement.element("dependencyManagement"); + if (dependencyManagement == null) { + return false; + } + Element dependencies = dependencyManagement.element("dependencies"); + if (dependencies == null) { + return false; + } + return dependencies.elements("dependency").stream() + .anyMatch(dependency -> Objects.equals(groupId, dependency.elementText("groupId")) + && Objects.equals(artifactId, dependency.elementText("artifactId")) + && Objects.equals(version, dependency.elementText("version"))); + } + + private boolean hasProfileManagedDependency( + Element rootElement, String profileId, String groupId, String artifactId, String version) { + Element profiles = rootElement.element("profiles"); + if (profiles == null) { + return false; + } + return profiles.elements("profile").stream() + .filter(profile -> Objects.equals(profileId, profile.elementText("id"))) + .map(profile -> profile.element("dependencyManagement")) + .filter(Objects::nonNull) + .map(dependencyManagement -> dependencyManagement.element("dependencies")) + .filter(Objects::nonNull) + .map(dependencies -> dependencies.elements("dependency")) + .flatMap(Collection::stream) + .anyMatch(dependency -> Objects.equals(groupId, dependency.elementText("groupId")) + && Objects.equals(artifactId, dependency.elementText("artifactId")) + && Objects.equals(version, dependency.elementText("version"))); + } + + private boolean hasResourceEntry(File baseFile, String resourcePath, String resourceEntry) throws IOException { + return FileUtils.readFileToString(new File(baseFile, resourcePath), StandardCharsets.UTF_8) + .contains(resourceEntry); + } } diff --git a/pom.xml b/pom.xml index 470e44a6c4e6..7b886d17b3e8 100644 --- a/pom.xml +++ b/pom.xml @@ -163,10 +163,16 @@ 0.6.1 3.0.2 - + 6.2.18 + 7.0.7 6.5.10 3.5.14 + 4.0.6 + 2.21 + 6.0.3 + 1.5.32 + 2.0.17 4.33.4 @@ -498,6 +504,7 @@ dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure + dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure dubbo-plugin/dubbo-spring6-security dubbo-plugin/dubbo-mutiny @@ -649,6 +656,7 @@ release dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure + dubbo-spring-boot-project/dubbo-spring-boot-4-autoconfigure dubbo-plugin/dubbo-spring6-security dubbo-plugin/dubbo-plugin-loom dubbo-plugin/dubbo-mutiny @@ -1211,5 +1219,312 @@ + + spring-boot-4 + + 11.0.21 + + + + + org.junit + junit-bom + ${junit-bom.version} + pom + import + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson-annotations-boot-4.version} + + + org.junit.jupiter + junit-jupiter-api + ${junit-bom.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit-bom.version} + + + org.junit.jupiter + junit-jupiter-params + ${junit-bom.version} + + + org.junit.platform + junit-platform-commons + ${junit-bom.version} + + + org.junit.platform + junit-platform-engine + ${junit-bom.version} + + + org.junit.platform + junit-platform-launcher + ${junit-bom.version} + + + ch.qos.logback + logback-classic + ${logback-boot-4.version} + + + ch.qos.logback + logback-core + ${logback-boot-4.version} + + + org.slf4j + jcl-over-slf4j + ${slf4j-boot-4.version} + + + org.slf4j + jul-to-slf4j + ${slf4j-boot-4.version} + + + org.slf4j + log4j-over-slf4j + ${slf4j-boot-4.version} + + + org.slf4j + slf4j-api + ${slf4j-boot-4.version} + + + org.springframework + spring-aop + ${spring-7.version} + + + org.springframework + spring-aspects + ${spring-7.version} + + + org.springframework + spring-beans + ${spring-7.version} + + + org.springframework + spring-context + ${spring-7.version} + + + org.springframework + spring-context-indexer + ${spring-7.version} + + + org.springframework + spring-context-support + ${spring-7.version} + + + org.springframework + spring-core + ${spring-7.version} + + + org.springframework + spring-core-test + ${spring-7.version} + + + org.springframework + spring-expression + ${spring-7.version} + + + org.springframework + spring-instrument + ${spring-7.version} + + + org.springframework + spring-jdbc + ${spring-7.version} + + + org.springframework + spring-test + ${spring-7.version} + + + org.springframework + spring-tx + ${spring-7.version} + + + org.springframework + spring-web + ${spring-7.version} + + + org.springframework + spring-webflux + ${spring-7.version} + + + org.springframework + spring-webmvc + ${spring-7.version} + + + org.springframework + spring-websocket + ${spring-7.version} + + + org.springframework.boot + spring-boot + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-autoconfigure + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-test + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-test-autoconfigure + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-actuator + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-actuator-autoconfigure + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-health + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-micrometer-metrics + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-actuator + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-configuration-processor + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-http-client + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-http-converter + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-jackson + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-restclient + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-aop + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-json + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-log4j2 + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-logging + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-tomcat + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-validation + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-starter-web + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-tomcat + ${spring-boot-4.version} + + + org.springframework.boot + spring-boot-web-server + ${spring-boot-4.version} + + + org.apache.tomcat.embed + tomcat-embed-core + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-el + ${tomcat.version} + + + org.apache.tomcat.embed + tomcat-embed-websocket + ${tomcat.version} + + + +