diff --git a/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigTransportClient.java b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigTransportClient.java index b5518265e7c..1bd5a8e10c5 100644 --- a/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigTransportClient.java +++ b/client/src/main/java/com/alibaba/nacos/client/config/impl/ConfigTransportClient.java @@ -79,7 +79,7 @@ public ConfigTransportClient(NacosClientProperties properties, ConfigServerListM this.tenant = properties.getProperty(PropertyKeyConst.NAMESPACE); this.serverListManager = serverListManager; this.properties = properties.asProperties(); - this.securityProxy = new SecurityProxy(serverListManager.getServerList(), + this.securityProxy = new SecurityProxy(serverListManager, ConfigHttpClientManager.getInstance().getNacosRestTemplate()); } diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingMaintainService.java b/client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingMaintainService.java index 85351f12d5f..136ee66c824 100644 --- a/client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingMaintainService.java +++ b/client/src/main/java/com/alibaba/nacos/client/naming/NacosNamingMaintainService.java @@ -81,7 +81,7 @@ private void init(Properties properties) throws NacosException { InitUtils.initWebRootContext(nacosClientProperties); serverListManager = new NamingServerListManager(nacosClientProperties, namespace); serverListManager.start(); - securityProxy = new SecurityProxy(serverListManager.getServerList(), + securityProxy = new SecurityProxy(serverListManager, NamingHttpClientManager.getInstance().getNacosRestTemplate()); initSecurityProxy(properties); serverProxy = new NamingHttpClientProxy(namespace, securityProxy, serverListManager, nacosClientProperties); diff --git a/client/src/main/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegate.java b/client/src/main/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegate.java index beef78ad036..efd7bdabf46 100644 --- a/client/src/main/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegate.java +++ b/client/src/main/java/com/alibaba/nacos/client/naming/remote/NamingClientProxyDelegate.java @@ -74,7 +74,7 @@ public NamingClientProxyDelegate(String namespace, ServiceInfoHolder serviceInfo this.serverListManager = new NamingServerListManager(properties, namespace); this.serverListManager.start(); this.serviceInfoHolder = serviceInfoHolder; - this.securityProxy = new SecurityProxy(this.serverListManager.getServerList(), + this.securityProxy = new SecurityProxy(this.serverListManager, NamingHttpClientManager.getInstance().getNacosRestTemplate()); initSecurityProxy(properties); this.httpClientProxy = new NamingHttpClientProxy(namespace, securityProxy, serverListManager, properties); diff --git a/client/src/main/java/com/alibaba/nacos/client/security/SecurityProxy.java b/client/src/main/java/com/alibaba/nacos/client/security/SecurityProxy.java index c695a98daa1..5199662d6c9 100644 --- a/client/src/main/java/com/alibaba/nacos/client/security/SecurityProxy.java +++ b/client/src/main/java/com/alibaba/nacos/client/security/SecurityProxy.java @@ -17,18 +17,22 @@ package com.alibaba.nacos.client.security; import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.client.address.AbstractServerListManager; +import com.alibaba.nacos.client.address.ServerListChangeEvent; import com.alibaba.nacos.client.auth.impl.NacosAuthLoginConstant; -import com.alibaba.nacos.plugin.auth.spi.client.ClientAuthPluginManager; -import com.alibaba.nacos.plugin.auth.api.LoginIdentityContext; -import com.alibaba.nacos.plugin.auth.spi.client.ClientAuthService; -import com.alibaba.nacos.plugin.auth.api.RequestResource; import com.alibaba.nacos.common.http.client.NacosRestTemplate; import com.alibaba.nacos.common.lifecycle.Closeable; +import com.alibaba.nacos.common.notify.Event; +import com.alibaba.nacos.common.notify.NotifyCenter; +import com.alibaba.nacos.common.notify.listener.Subscriber; +import com.alibaba.nacos.plugin.auth.api.LoginIdentityContext; +import com.alibaba.nacos.plugin.auth.api.RequestResource; +import com.alibaba.nacos.plugin.auth.spi.client.ClientAuthPluginManager; +import com.alibaba.nacos.plugin.auth.spi.client.ClientAuthService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Properties; @@ -45,15 +49,25 @@ public class SecurityProxy implements Closeable { private ClientAuthPluginManager clientAuthPluginManager; /** - * Construct from serverList, nacosRestTemplate, init client auth plugin. - * // TODO change server list to serverListManager after serverListManager refactor and unite. + * Construct from serverListManager, nacosRestTemplate, init client auth plugin. * - * @param serverList a server list that client request to. + * @param serverListManager a server list manager that client request to. * @Param nacosRestTemplate http request template. */ - public SecurityProxy(List serverList, NacosRestTemplate nacosRestTemplate) { + public SecurityProxy(AbstractServerListManager serverListManager, NacosRestTemplate nacosRestTemplate) { clientAuthPluginManager = new ClientAuthPluginManager(); - clientAuthPluginManager.init(serverList, nacosRestTemplate); + clientAuthPluginManager.init(serverListManager.getServerList(), nacosRestTemplate); + NotifyCenter.registerSubscriber(new Subscriber() { + @Override + public void onEvent(ServerListChangeEvent event) { + clientAuthPluginManager.refreshServerList(serverListManager.getServerList()); + } + + @Override + public Class subscribeType() { + return ServerListChangeEvent.class; + } + }); } /** diff --git a/client/src/test/java/com/alibaba/nacos/client/security/SecurityProxyTest.java b/client/src/test/java/com/alibaba/nacos/client/security/SecurityProxyTest.java index 739bfc1f77d..ef9e1693e51 100644 --- a/client/src/test/java/com/alibaba/nacos/client/security/SecurityProxyTest.java +++ b/client/src/test/java/com/alibaba/nacos/client/security/SecurityProxyTest.java @@ -18,7 +18,9 @@ import com.alibaba.nacos.api.PropertyKeyConst; import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.client.address.AbstractServerListManager; import com.alibaba.nacos.client.auth.impl.NacosAuthLoginConstant; +import com.alibaba.nacos.client.env.NacosClientProperties; import com.alibaba.nacos.common.http.HttpRestResult; import com.alibaba.nacos.common.http.client.NacosRestTemplate; import com.alibaba.nacos.common.http.param.Header; @@ -68,7 +70,34 @@ void setUp() throws Exception { List serverList = new ArrayList<>(); serverList.add("localhost"); - securityProxy = new SecurityProxy(serverList, nacosRestTemplate); + NacosClientProperties properties = NacosClientProperties.PROTOTYPE.derive(new Properties()); + AbstractServerListManager serverListManager = new AbstractServerListManager(properties) { + @Override + protected String getModuleName() { + return "Test"; + } + + @Override + protected NacosRestTemplate getNacosRestTemplate() { + return nacosRestTemplate; + } + + @Override + public String genNextServer() { + return serverList.get(0); + } + + @Override + public String getCurrentServer() { + return serverList.get(0); + } + + @Override + public List getServerList() { + return serverList; + } + }; + securityProxy = new SecurityProxy(serverListManager, nacosRestTemplate); } @Test diff --git a/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java b/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java index 9f29c0e6b0c..07754698852 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigController.java @@ -28,6 +28,7 @@ import com.alibaba.nacos.config.server.constant.Constants; import com.alibaba.nacos.config.server.constant.ParametersField; import com.alibaba.nacos.config.server.controller.parameters.SameNamespaceCloneConfigBean; +import com.alibaba.nacos.config.server.enums.ApiVersionEnum; import com.alibaba.nacos.config.server.model.ConfigAdvanceInfo; import com.alibaba.nacos.config.server.model.ConfigAllInfo; import com.alibaba.nacos.config.server.model.ConfigInfo; @@ -244,7 +245,7 @@ public void getConfig(HttpServletRequest request, HttpServletResponse response, final String clientIp = RequestUtil.getRemoteIp(request); String isNotify = request.getHeader("notify"); - inner.doGetConfig(request, response, dataId, group, tenant, tag, isNotify, clientIp); + inner.doGetConfig(request, response, dataId, group, tenant, tag, isNotify, clientIp, ApiVersionEnum.V1); } /** diff --git a/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigServletInner.java b/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigServletInner.java index f5c94e66265..a78a454192f 100755 --- a/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigServletInner.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/controller/ConfigServletInner.java @@ -24,39 +24,39 @@ import com.alibaba.nacos.common.utils.Pair; import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.config.server.constant.Constants; +import com.alibaba.nacos.config.server.enums.ApiVersionEnum; import com.alibaba.nacos.config.server.enums.FileTypeEnum; -import com.alibaba.nacos.config.server.model.CacheItem; +import com.alibaba.nacos.config.server.exception.NacosConfigException; import com.alibaba.nacos.config.server.model.ConfigCacheGray; import com.alibaba.nacos.config.server.model.gray.BetaGrayRule; import com.alibaba.nacos.config.server.model.gray.TagGrayRule; -import com.alibaba.nacos.config.server.service.ConfigCacheService; +import com.alibaba.nacos.config.server.service.query.ConfigChainRequestExtractorService; +import com.alibaba.nacos.config.server.service.query.ConfigQueryChainService; import com.alibaba.nacos.config.server.service.LongPollingService; -import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskServiceFactory; +import com.alibaba.nacos.config.server.service.query.enums.ResponseCode; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; import com.alibaba.nacos.config.server.service.trace.ConfigTraceService; -import com.alibaba.nacos.config.server.utils.GroupKey2; -import com.alibaba.nacos.config.server.utils.LogUtil; import com.alibaba.nacos.config.server.utils.MD5Util; import com.alibaba.nacos.config.server.utils.Protocol; import com.alibaba.nacos.config.server.utils.RequestUtil; -import com.alibaba.nacos.config.server.utils.TimeUtils; import com.alibaba.nacos.plugin.encryption.handler.EncryptionHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.HashMap; import java.util.List; import java.util.Map; -import static com.alibaba.nacos.config.server.constant.Constants.ENCODE_UTF8; -import static com.alibaba.nacos.config.server.utils.LogUtil.PULL_LOG; +import static com.alibaba.nacos.api.common.Constants.CONFIG_TYPE; +import static com.alibaba.nacos.api.common.Constants.VIPSERVER_TAG; +import static com.alibaba.nacos.config.server.constant.Constants.CONTENT_MD5; /** * ConfigServlet inner for aop. @@ -74,8 +74,11 @@ public class ConfigServletInner { private final LongPollingService longPollingService; - public ConfigServletInner(LongPollingService longPollingService) { + private final ConfigQueryChainService configQueryChainService; + + public ConfigServletInner(LongPollingService longPollingService, ConfigQueryChainService configQueryChainService) { this.longPollingService = longPollingService; + this.configQueryChainService = configQueryChainService; } /** @@ -119,177 +122,211 @@ public String doPollingConfig(HttpServletRequest request, HttpServletResponse re return HttpServletResponse.SC_OK + ""; } - /** - * Execute to get config [API V1]. - */ - public String doGetConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group, - String tenant, String tag, String isNotify, String clientIp) throws IOException, ServletException { - return doGetConfig(request, response, dataId, group, tenant, tag, isNotify, clientIp, false); - } - /** * Execute to get config [API V1] or [API V2]. */ public String doGetConfig(HttpServletRequest request, HttpServletResponse response, String dataId, String group, - String tenant, String tag, String isNotify, String clientIp, boolean isV2) throws IOException { + String tenant, String tag, String isNotify, String clientIp, ApiVersionEnum apiVersion) throws IOException { boolean notify = StringUtils.isNotBlank(isNotify) && Boolean.parseBoolean(isNotify); + String requestIpApp = RequestUtil.getAppName(request); - String acceptCharset = ENCODE_UTF8; + ConfigQueryChainRequest chainRequest = ConfigChainRequestExtractorService.getExtractor().extract(request); + ConfigQueryChainResponse chainResponse = configQueryChainService.handle(chainRequest); - if (isV2) { - response.setHeader(HttpHeaderConsts.CONTENT_TYPE, MediaType.APPLICATION_JSON); + if (ResponseCode.FAIL.getCode() == chainResponse.getResultCode()) { + throw new NacosConfigException(chainResponse.getMessage()); } - final String groupKey = GroupKey2.getKey(dataId, group, tenant); - String autoTag = request.getHeader(com.alibaba.nacos.api.common.Constants.VIPSERVER_TAG); - - String requestIpApp = RequestUtil.getAppName(request); - int lockResult = ConfigCacheService.tryConfigReadLock(groupKey); - CacheItem cacheItem = ConfigCacheService.getContentCache(groupKey); + logPullEvent(dataId, group, tenant, requestIpApp, chainResponse, clientIp, notify, tag); - final String requestIp = RequestUtil.getRemoteIp(request); - if (lockResult > 0 && cacheItem != null) { - try { - long lastModified; - - final String configType = - (null != cacheItem.getType()) ? cacheItem.getType() : FileTypeEnum.TEXT.getFileType(); - response.setHeader(com.alibaba.nacos.api.common.Constants.CONFIG_TYPE, configType); - FileTypeEnum fileTypeEnum = FileTypeEnum.getFileTypeEnumByFileExtensionOrFileType(configType); - String contentTypeHeader = fileTypeEnum.getContentType(); - response.setHeader(HttpHeaderConsts.CONTENT_TYPE, - isV2 ? MediaType.APPLICATION_JSON : contentTypeHeader); - - ConfigCacheGray matchedGray = null; - Map appLabels = new HashMap(4); - appLabels.put(BetaGrayRule.CLIENT_IP_LABEL, clientIp); - boolean specificTag = StringUtils.isNotBlank(tag); - - if (specificTag) { - appLabels.put(TagGrayRule.VIP_SERVER_TAG_LABEL, tag); - } else if (StringUtils.isNotBlank(autoTag)) { - appLabels.put(TagGrayRule.VIP_SERVER_TAG_LABEL, autoTag); - } - - if (cacheItem.getSortConfigGrays() != null && !cacheItem.getSortConfigGrays().isEmpty()) { - for (ConfigCacheGray configCacheGray : cacheItem.getSortConfigGrays()) { - if (configCacheGray.match(appLabels)) { - matchedGray = configCacheGray; - break; - } - } - } - - String pullEvent; - String content; - String md5; - String encryptedDataKey; - - if (matchedGray != null) { - md5 = matchedGray.getMd5(acceptCharset); - lastModified = matchedGray.getLastModifiedTs(); - encryptedDataKey = matchedGray.getEncryptedDataKey(); - content = ConfigDiskServiceFactory.getInstance() - .getGrayContent(dataId, group, tenant, matchedGray.getGrayName()); - pullEvent = ConfigTraceService.PULL_EVENT + "-" + matchedGray.getGrayName(); - if (BetaGrayRule.TYPE_BETA.equals(matchedGray.getGrayName())) { - response.setHeader("isBeta", "true"); - } - if (TagGrayRule.TYPE_TAG.equals(matchedGray.getGrayRule().getType())) { - response.setHeader(com.alibaba.nacos.api.common.Constants.VIPSERVER_TAG, - URLEncoder.encode(matchedGray.getGrayRule().getRawGrayRuleExp(), - StandardCharsets.UTF_8.displayName())); - } - } else if (specificTag) { - //specific tag is not found - md5 = null; - lastModified = 0L; - encryptedDataKey = null; - content = null; - pullEvent = ConfigTraceService.PULL_EVENT + "-" + TagGrayRule.TYPE_TAG + "-" + tag; - response.setHeader(com.alibaba.nacos.api.common.Constants.VIPSERVER_TAG, - URLEncoder.encode(tag, StandardCharsets.UTF_8.displayName())); - } else { - md5 = cacheItem.getConfigCache().getMd5(acceptCharset); - lastModified = cacheItem.getConfigCache().getLastModifiedTs(); - encryptedDataKey = cacheItem.getConfigCache().getEncryptedDataKey(); - content = ConfigDiskServiceFactory.getInstance().getContent(dataId, group, tenant); - pullEvent = ConfigTraceService.PULL_EVENT; - } - - if (content == null) { - ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1, pullEvent, - ConfigTraceService.PULL_TYPE_NOTFOUND, -1, requestIp, notify, "http"); - return get404Result(response, isV2); - - } - response.setHeader(Constants.CONTENT_MD5, md5); - - // Disable cache. - response.setHeader("Pragma", "no-cache"); - response.setDateHeader("Expires", 0); - response.setHeader("Cache-Control", "no-cache,no-store"); - response.setDateHeader("Last-Modified", lastModified); - if (encryptedDataKey != null) { - response.setHeader("Encrypted-Data-Key", encryptedDataKey); - } - PrintWriter out; - Pair pair = EncryptionHandler.decryptHandler(dataId, encryptedDataKey, content); - String decryptContent = pair.getSecond(); - out = response.getWriter(); - if (isV2) { - out.print(JacksonUtils.toJson(Result.success(decryptContent))); - } else { - out.print(decryptContent); - } - - out.flush(); - out.close(); - - LogUtil.PULL_CHECK_LOG.warn("{}|{}|{}|{}", groupKey, requestIp, md5, TimeUtils.getCurrentTimeStr()); - - final long delayed = notify ? -1 : System.currentTimeMillis() - lastModified; - ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, lastModified, pullEvent, - ConfigTraceService.PULL_TYPE_OK, delayed, clientIp, notify, "http"); - } finally { - ConfigCacheService.releaseReadLock(groupKey); - } - } else if (lockResult == 0 || cacheItem == null) { - - ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1, ConfigTraceService.PULL_EVENT, - ConfigTraceService.PULL_TYPE_NOTFOUND, -1, requestIp, notify, "http"); - return get404Result(response, isV2); - - } else { - - PULL_LOG.info("[client-get] clientIp={}, {}, get data during dump", clientIp, groupKey); - return get409Result(response, isV2); + switch (chainResponse.getStatus()) { + case CONFIG_NOT_FOUND: + return handlerConfigNotFound(response, apiVersion); + case CONFIG_QUERY_CONFLICT: + return handlerConfigConflict(response, apiVersion); + default: + return handleResponse(response, chainResponse, dataId, group, apiVersion); } - - return HttpServletResponse.SC_OK + ""; } - private String get404Result(HttpServletResponse response, boolean isV2) throws IOException { + private String handlerConfigNotFound(HttpServletResponse response, ApiVersionEnum apiVersion) throws IOException { response.setStatus(HttpServletResponse.SC_NOT_FOUND); - PrintWriter writer = response.getWriter(); - if (isV2) { - writer.println(JacksonUtils.toJson(Result.failure(ErrorCode.RESOURCE_NOT_FOUND, "config data not exist"))); + if (apiVersion == ApiVersionEnum.V1) { + return writeResponseForV1(response, Result.failure(ErrorCode.RESOURCE_NOT_FOUND, "config data not exist")); } else { - writer.println("config data not exist"); + return writeResponseForV2(response, Result.failure(ErrorCode.RESOURCE_NOT_FOUND, "config data not exist")); } - return HttpServletResponse.SC_NOT_FOUND + ""; } - private String get409Result(HttpServletResponse response, boolean isV2) throws IOException { + private String handlerConfigConflict(HttpServletResponse response, ApiVersionEnum apiVersion) throws IOException { response.setStatus(HttpServletResponse.SC_CONFLICT); + if (apiVersion == ApiVersionEnum.V1) { + return writeResponseForV1(response, Result.failure(ErrorCode.RESOURCE_CONFLICT, "requested file is being modified, please try later.")); + } else { + return writeResponseForV2(response, Result.failure(ErrorCode.RESOURCE_CONFLICT, "requested file is being modified, please try later.")); + } + } + + private String handleResponse(HttpServletResponse response, ConfigQueryChainResponse chainResponse, String dataId, + String group, ApiVersionEnum apiVersion) throws IOException { + if (apiVersion == ApiVersionEnum.V1) { + return handleResponseForV1(response, chainResponse, dataId, group); + } else { + return handleResponseForV2(response, chainResponse, dataId, group); + } + } + + private String handleResponseForV1(HttpServletResponse response, ConfigQueryChainResponse chainResponse, + String dataId, String tag) throws IOException { + if (chainResponse.getContent() == null) { + return handlerConfigNotFound(response, ApiVersionEnum.V1); + } + + setCommonResponseHead(response, chainResponse, tag); + setResponseHeadForV1(response, chainResponse); + writeContentForV1(response, chainResponse, dataId); + + return HttpServletResponse.SC_OK + ""; + } + + private String handleResponseForV2(HttpServletResponse response, ConfigQueryChainResponse chainResponse, + String dataId, String tag) throws IOException { + if (chainResponse.getContent() == null) { + return handlerConfigNotFound(response, ApiVersionEnum.V2); + } + + setCommonResponseHead(response, chainResponse, tag); + setResponseHeadForV2(response); + writeContentForV2(response, chainResponse, dataId); + + return HttpServletResponse.SC_OK + ""; + } + + private void setResponseHeadForV1(HttpServletResponse response, ConfigQueryChainResponse chainResponse) { + String contentType = chainResponse.getContentType() != null ? chainResponse.getContentType() : FileTypeEnum.TEXT.getFileType(); + FileTypeEnum fileTypeEnum = FileTypeEnum.getFileTypeEnumByFileExtensionOrFileType(contentType); + String contentTypeHeader = fileTypeEnum.getContentType(); + response.setHeader(HttpHeaderConsts.CONTENT_TYPE, contentTypeHeader); + } + + private void setResponseHeadForV2(HttpServletResponse response) { + response.setHeader(HttpHeaderConsts.CONTENT_TYPE, MediaType.APPLICATION_JSON); + } + + private void writeContentForV1(HttpServletResponse response, ConfigQueryChainResponse chainResponse, String dataId) + throws IOException { + PrintWriter out = response.getWriter(); + try { + String decryptContent = getDecryptContent(chainResponse, dataId); + out.print(decryptContent); + } finally { + out.flush(); + out.close(); + } + } + + private void writeContentForV2(HttpServletResponse response, ConfigQueryChainResponse chainResponse, String dataId) + throws IOException { + PrintWriter out = response.getWriter(); + try { + String decryptContent = getDecryptContent(chainResponse, dataId); + out.print(JacksonUtils.toJson(Result.success(decryptContent))); + } finally { + out.flush(); + out.close(); + } + } + + private static String getDecryptContent(ConfigQueryChainResponse chainResponse, String dataId) { + Pair pair = EncryptionHandler.decryptHandler(dataId, chainResponse.getEncryptedDataKey(), + chainResponse.getContent()); + return pair.getSecond(); + } + + private String writeResponseForV1(HttpServletResponse response, Result result) throws IOException { + PrintWriter writer = response.getWriter(); + writer.println(result.getData()); + return response.getStatus() + ""; + } + + private String writeResponseForV2(HttpServletResponse response, Result result) throws IOException { PrintWriter writer = response.getWriter(); - if (isV2) { - writer.println(JacksonUtils.toJson(Result.failure(ErrorCode.RESOURCE_CONFLICT, - "requested file is being modified, please try later."))); + writer.println(JacksonUtils.toJson(result)); + return response.getStatus() + ""; + } + + private String resolvePullEvent(ConfigQueryChainResponse chainResponse, String tag) { + switch (chainResponse.getStatus()) { + case CONFIG_FOUND_GRAY: + ConfigCacheGray matchedGray = chainResponse.getMatchedGray(); + if (matchedGray != null) { + return ConfigTraceService.PULL_EVENT + "-" + matchedGray.getGrayName(); + } else { + return ConfigTraceService.PULL_EVENT; + } + case SPECIAL_TAG_CONFIG_NOT_FOUND: + return ConfigTraceService.PULL_EVENT + "-" + TagGrayRule.TYPE_TAG + "-" + tag; + default: + return ConfigTraceService.PULL_EVENT; + } + } + + private void logPullEvent(String dataId, String group, String tenant, String requestIpApp, + ConfigQueryChainResponse chainResponse, String clientIp, boolean notify, String tag) { + + String pullEvent = resolvePullEvent(chainResponse, tag); + + ConfigQueryChainResponse.ConfigQueryStatus status = chainResponse.getStatus(); + + if (status == ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_QUERY_CONFLICT) { + ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1, pullEvent, + ConfigTraceService.PULL_TYPE_CONFLICT, -1, clientIp, notify, "http"); + } else if (status == ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_NOT_FOUND || chainResponse.getContent() == null) { + ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1, pullEvent, + ConfigTraceService.PULL_TYPE_NOTFOUND, -1, clientIp, notify, "http"); } else { - writer.println("requested file is being modified, please try later."); + long delayed = notify ? -1 : System.currentTimeMillis() - chainResponse.getLastModified(); + ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, chainResponse.getLastModified(), pullEvent, + ConfigTraceService.PULL_TYPE_OK, delayed, clientIp, notify, "http"); + } + } + + private void setCommonResponseHead(HttpServletResponse response, ConfigQueryChainResponse chainResponse, String tag) { + String contentType = chainResponse.getContentType() != null ? chainResponse.getContentType() : FileTypeEnum.TEXT.getFileType(); + + response.setHeader(CONFIG_TYPE, contentType); + response.setHeader(CONTENT_MD5, chainResponse.getMd5()); + response.setHeader("Pragma", "no-cache"); + response.setDateHeader("Expires", 0); + response.setHeader("Cache-Control", "no-cache,no-store"); + response.setDateHeader("Last-Modified", chainResponse.getLastModified()); + + if (chainResponse.getEncryptedDataKey() != null) { + response.setHeader("Encrypted-Data-Key", chainResponse.getEncryptedDataKey()); + } + + // Check if there is a matched gray rule + if (ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_FOUND_GRAY == chainResponse.getStatus()) { + if (BetaGrayRule.TYPE_BETA.equals(chainResponse.getMatchedGray().getGrayRule().getType())) { + response.setHeader("isBeta", "true"); + } else if (TagGrayRule.TYPE_TAG.equals(chainResponse.getMatchedGray().getGrayRule().getType())) { + try { + response.setHeader(TagGrayRule.TYPE_TAG, URLEncoder.encode(chainResponse.getMatchedGray().getGrayRule().getRawGrayRuleExp(), + StandardCharsets.UTF_8.displayName())); + } catch (Exception e) { + LOGGER.error("Error encoding tag", e); + } + } + } + + // Check if there is a special tag + if (ConfigQueryChainResponse.ConfigQueryStatus.SPECIAL_TAG_CONFIG_NOT_FOUND == chainResponse.getStatus()) { + try { + response.setHeader(VIPSERVER_TAG, URLEncoder.encode(tag, StandardCharsets.UTF_8.displayName())); + } catch (Exception e) { + LOGGER.error("Error encoding tag", e); + } } - return HttpServletResponse.SC_CONFLICT + ""; } } diff --git a/config/src/main/java/com/alibaba/nacos/config/server/controller/v2/ConfigControllerV2.java b/config/src/main/java/com/alibaba/nacos/config/server/controller/v2/ConfigControllerV2.java index b7f4b9ffdd9..f6897265173 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/controller/v2/ConfigControllerV2.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/controller/v2/ConfigControllerV2.java @@ -27,6 +27,7 @@ import com.alibaba.nacos.common.utils.StringUtils; import com.alibaba.nacos.config.server.constant.Constants; import com.alibaba.nacos.config.server.controller.ConfigServletInner; +import com.alibaba.nacos.config.server.enums.ApiVersionEnum; import com.alibaba.nacos.config.server.model.ConfigInfo; import com.alibaba.nacos.config.server.model.ConfigRequestInfo; import com.alibaba.nacos.config.server.paramcheck.ConfigBlurSearchHttpParamExtractor; @@ -105,7 +106,7 @@ public void getConfig(HttpServletRequest request, HttpServletResponse response, ParamUtils.checkParamV2(tag); final String clientIp = RequestUtil.getRemoteIp(request); String isNotify = request.getHeader("notify"); - inner.doGetConfig(request, response, dataId, group, namespaceId, tag, isNotify, clientIp, true); + inner.doGetConfig(request, response, dataId, group, namespaceId, tag, isNotify, clientIp, ApiVersionEnum.V2); } /** diff --git a/config/src/main/java/com/alibaba/nacos/config/server/enums/ApiVersionEnum.java b/config/src/main/java/com/alibaba/nacos/config/server/enums/ApiVersionEnum.java new file mode 100644 index 00000000000..7235ace37b0 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/enums/ApiVersionEnum.java @@ -0,0 +1,44 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.enums; + +/** + * Config Api Version enum. + * @author Nacos + */ +public enum ApiVersionEnum { + + /** + * API version v1. + */ + V1("v1"), + + /** + * API version v2. + */ + V2("v2"); + + private final String version; + + ApiVersionEnum(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigQueryRequestHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigQueryRequestHandler.java index 7b0d4d88f69..b49f24ed3ad 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigQueryRequestHandler.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/remote/ConfigQueryRequestHandler.java @@ -22,17 +22,16 @@ import com.alibaba.nacos.api.remote.request.RequestMeta; import com.alibaba.nacos.api.remote.response.ResponseCode; import com.alibaba.nacos.auth.annotation.Secured; -import com.alibaba.nacos.common.utils.StringUtils; -import com.alibaba.nacos.config.server.model.CacheItem; import com.alibaba.nacos.config.server.model.ConfigCacheGray; import com.alibaba.nacos.config.server.model.gray.BetaGrayRule; import com.alibaba.nacos.config.server.model.gray.TagGrayRule; -import com.alibaba.nacos.config.server.service.ConfigCacheService; -import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskServiceFactory; +import com.alibaba.nacos.config.server.service.query.ConfigChainRequestExtractorService; +import com.alibaba.nacos.config.server.service.query.ConfigQueryChainService; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; import com.alibaba.nacos.config.server.service.trace.ConfigTraceService; import com.alibaba.nacos.config.server.utils.GroupKey2; import com.alibaba.nacos.config.server.utils.LogUtil; -import com.alibaba.nacos.config.server.utils.ParamUtils; import com.alibaba.nacos.config.server.utils.TimeUtils; import com.alibaba.nacos.core.control.TpsControl; import com.alibaba.nacos.core.paramcheck.ExtractorManager; @@ -40,13 +39,11 @@ import com.alibaba.nacos.core.remote.RequestHandler; import com.alibaba.nacos.plugin.auth.constant.ActionTypes; import com.alibaba.nacos.plugin.auth.constant.SignType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.net.URLEncoder; -import java.util.HashMap; -import java.util.Map; - -import static com.alibaba.nacos.api.common.Constants.CLIENT_IP; import static com.alibaba.nacos.config.server.constant.Constants.ENCODE_UTF8; import static com.alibaba.nacos.config.server.utils.LogUtil.PULL_LOG; import static com.alibaba.nacos.config.server.utils.RequestUtil.CLIENT_APPNAME_HEADER; @@ -60,7 +57,12 @@ @Component public class ConfigQueryRequestHandler extends RequestHandler { - public ConfigQueryRequestHandler() { + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigQueryRequestHandler.class); + + private final ConfigQueryChainService configQueryChainService; + + public ConfigQueryRequestHandler(ConfigQueryChainService configQueryChainService) { + this.configQueryChainService = configQueryChainService; } @Override @@ -68,126 +70,110 @@ public ConfigQueryRequestHandler() { @Secured(action = ActionTypes.READ, signType = SignType.CONFIG) @ExtractorManager.Extractor(rpcExtractor = ConfigRequestParamExtractor.class) public ConfigQueryResponse handle(ConfigQueryRequest request, RequestMeta meta) throws NacosException { - try { - return getContext(request, meta, request.isNotify()); + String dataId = request.getDataId(); + String group = request.getGroup(); + String tenant = request.getTenant(); + String groupKey = GroupKey2.getKey(dataId, group, tenant); + boolean notify = request.isNotify(); + + String requestIpApp = meta.getLabels().get(CLIENT_APPNAME_HEADER); + String clientIp = meta.getClientIp(); + + ConfigQueryChainRequest chainRequest = ConfigChainRequestExtractorService.getExtractor().extract(request, meta); + ConfigQueryChainResponse chainResponse = configQueryChainService.handle(chainRequest); + + if (ResponseCode.FAIL.getCode() == chainResponse.getResultCode()) { + return ConfigQueryResponse.buildFailResponse(ResponseCode.FAIL.getCode(), chainResponse.getMessage()); + } + + if (chainResponse.getStatus() == ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_NOT_FOUND) { + return handlerConfigNotFound(request.getDataId(), request.getGroup(), request.getTenant(), requestIpApp, clientIp, notify); + } + + if (chainResponse.getStatus() == ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_QUERY_CONFLICT) { + return handlerConfigConflict(clientIp, groupKey); + } + + ConfigQueryResponse response = new ConfigQueryResponse(); + + // Check if there is a matched gray rule + if (chainResponse.getStatus() == ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_FOUND_GRAY) { + if (BetaGrayRule.TYPE_BETA.equals(chainResponse.getMatchedGray().getGrayRule().getType())) { + response.setBeta(true); + } else if (TagGrayRule.TYPE_TAG.equals(chainResponse.getMatchedGray().getGrayRule().getType())) { + response.setTag(URLEncoder.encode(chainResponse.getMatchedGray().getRawGrayRule(), ENCODE_UTF8)); + } + } + + // Check if there is a special tag + if (chainResponse.getStatus() == ConfigQueryChainResponse.ConfigQueryStatus.SPECIAL_TAG_CONFIG_NOT_FOUND) { + response.setTag(request.getTag()); + } + + response.setMd5(chainResponse.getMd5()); + response.setEncryptedDataKey(chainResponse.getEncryptedDataKey()); + response.setContent(chainResponse.getContent()); + response.setLastModified(chainResponse.getLastModified()); + + String pullType = ConfigTraceService.PULL_TYPE_OK; + if (chainResponse.getContent() == null) { + pullType = ConfigTraceService.PULL_TYPE_NOTFOUND; + response.setErrorInfo(ConfigQueryResponse.CONFIG_NOT_FOUND, "config data not exist"); + } else { + response.setResultCode(ResponseCode.SUCCESS.getCode()); + } + + String pullEvent = resolvePullEventType(chainResponse, request.getTag()); + LogUtil.PULL_CHECK_LOG.warn("{}|{}|{}|{}", groupKey, clientIp, response.getMd5(), TimeUtils.getCurrentTimeStr()); + final long delayed = notify ? -1 : System.currentTimeMillis() - response.getLastModified(); + ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, response.getLastModified(), pullEvent, pullType, + delayed, clientIp, notify, "grpc"); + + return response; + } catch (Exception e) { + LOGGER.error("Failed to handle grpc configuration query", e); return ConfigQueryResponse.buildFailResponse(ResponseCode.FAIL.getCode(), e.getMessage()); } } - private ConfigQueryResponse getContext(ConfigQueryRequest configQueryRequest, RequestMeta meta, boolean notify) - throws Exception { - String dataId = configQueryRequest.getDataId(); - String group = configQueryRequest.getGroup(); - String tenant = configQueryRequest.getTenant(); - String clientIp = meta.getClientIp(); - String tag = configQueryRequest.getTag(); + private ConfigQueryResponse handlerConfigConflict(String clientIp, String groupKey) { + ConfigQueryResponse response = new ConfigQueryResponse(); - String groupKey = GroupKey2.getKey(configQueryRequest.getDataId(), configQueryRequest.getGroup(), - configQueryRequest.getTenant()); - String requestIpApp = meta.getLabels().get(CLIENT_APPNAME_HEADER); - String acceptCharset = ENCODE_UTF8; - ParamUtils.checkParam(tag); - int lockResult = ConfigCacheService.tryConfigReadLock(groupKey); - String pullEvent = ConfigTraceService.PULL_EVENT; - String pullType = ConfigTraceService.PULL_TYPE_OK; + PULL_LOG.info("[client-get] clientIp={}, {}, get data during dump", clientIp, groupKey); + response.setErrorInfo(ConfigQueryResponse.CONFIG_QUERY_CONFLICT, + "requested file is being modified, please try later."); + return response; + } + + private ConfigQueryResponse handlerConfigNotFound(String dataId, String group, String tenant, String requestIpApp, + String clientIp, boolean notify) { + //CacheItem No longer exists. It is impossible to simply calculate the push delayed. Here, simply record it as - 1. ConfigQueryResponse response = new ConfigQueryResponse(); - CacheItem cacheItem = ConfigCacheService.getContentCache(groupKey); + ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1, ConfigTraceService.PULL_EVENT, + ConfigTraceService.PULL_TYPE_NOTFOUND, -1, clientIp, notify, "grpc"); + response.setErrorInfo(ConfigQueryResponse.CONFIG_NOT_FOUND, "config data not exist"); - if (lockResult > 0 && cacheItem != null) { - try { - long lastModified = 0L; - String configType = cacheItem.getType(); - response.setContentType((null != configType) ? configType : "text"); - - String content; - String md5; - String encryptedDataKey; - ConfigCacheGray matchedGray = null; - Map appLabels = null; - boolean specificTag = StringUtils.isNotBlank(tag); - if (specificTag) { - appLabels = new HashMap<>(4); - appLabels.put(TagGrayRule.VIP_SERVER_TAG_LABEL, tag); - appLabels.put(CLIENT_IP, clientIp); - } else { - appLabels = new HashMap(meta.getAppLabels()); - if (!appLabels.containsKey(CLIENT_IP)) { - appLabels.put(CLIENT_IP, clientIp); - } - } - - if (cacheItem.getSortConfigGrays() != null && !cacheItem.getSortConfigGrays().isEmpty()) { - for (ConfigCacheGray configCacheGray : cacheItem.getSortConfigGrays()) { - if (configCacheGray.match(appLabels)) { - matchedGray = configCacheGray; - break; - } - } - } + return response; + + } + + private String resolvePullEventType(ConfigQueryChainResponse chainResponse, String tag) { + switch (chainResponse.getStatus()) { + case CONFIG_FOUND_GRAY: + ConfigCacheGray matchedGray = chainResponse.getMatchedGray(); if (matchedGray != null) { - md5 = matchedGray.getMd5(acceptCharset); - lastModified = matchedGray.getLastModifiedTs(); - encryptedDataKey = matchedGray.getEncryptedDataKey(); - content = ConfigDiskServiceFactory.getInstance() - .getGrayContent(dataId, group, tenant, matchedGray.getGrayName()); - pullEvent = ConfigTraceService.PULL_EVENT + "-" + matchedGray.getGrayName(); - if (BetaGrayRule.TYPE_BETA.equals(matchedGray.getGrayName())) { - response.setBeta(true); - } - if (TagGrayRule.TYPE_TAG.equals(matchedGray.getGrayRule().getType())) { - response.setTag(URLEncoder.encode(matchedGray.getRawGrayRule(), ENCODE_UTF8)); - } - } else if (specificTag) { - //specific tag is not found - md5 = null; - lastModified = 0L; - encryptedDataKey = null; - content = null; - pullEvent = ConfigTraceService.PULL_EVENT + "-" + TagGrayRule.TYPE_TAG + "-" + tag; - response.setTag(tag); - } else { - md5 = cacheItem.getConfigCache().getMd5(acceptCharset); - lastModified = cacheItem.getConfigCache().getLastModifiedTs(); - encryptedDataKey = cacheItem.getConfigCache().getEncryptedDataKey(); - content = ConfigDiskServiceFactory.getInstance().getContent(dataId, group, tenant); - pullEvent = ConfigTraceService.PULL_EVENT; - } - - response.setMd5(md5); - response.setEncryptedDataKey(encryptedDataKey); - response.setContent(content); - response.setLastModified(lastModified); - if (content == null) { - pullType = ConfigTraceService.PULL_TYPE_NOTFOUND; - response.setErrorInfo(ConfigQueryResponse.CONFIG_NOT_FOUND, "config data not exist"); + return ConfigTraceService.PULL_EVENT + "-" + matchedGray.getGrayName(); } else { - response.setResultCode(ResponseCode.SUCCESS.getCode()); + return ConfigTraceService.PULL_EVENT; } - LogUtil.PULL_CHECK_LOG.warn("{}|{}|{}|{}", groupKey, clientIp, md5, TimeUtils.getCurrentTimeStr()); - - final long delayed = notify ? -1 : System.currentTimeMillis() - lastModified; - ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, lastModified, pullEvent, pullType, - delayed, clientIp, notify, "grpc"); - } finally { - ConfigCacheService.releaseReadLock(groupKey); - } - } else if (lockResult == 0 || cacheItem == null) { - - //CacheItem No longer exists. It is impossible to simply calculate the push delayed. Here, simply record it as - 1. - ConfigTraceService.logPullEvent(dataId, group, tenant, requestIpApp, -1, pullEvent, - ConfigTraceService.PULL_TYPE_NOTFOUND, -1, clientIp, notify, "grpc"); - response.setErrorInfo(ConfigQueryResponse.CONFIG_NOT_FOUND, "config data not exist"); - - } else { - PULL_LOG.info("[client-get] clientIp={}, {}, get data during dump", clientIp, groupKey); - response.setErrorInfo(ConfigQueryResponse.CONFIG_QUERY_CONFLICT, - "requested file is being modified, please try later."); + case SPECIAL_TAG_CONFIG_NOT_FOUND: + return ConfigTraceService.PULL_EVENT + "-" + TagGrayRule.TYPE_TAG + "-" + tag; + default: + return ConfigTraceService.PULL_EVENT; } - return response; } - } diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigChainRequestExtractorService.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigChainRequestExtractorService.java new file mode 100644 index 00000000000..a847625863d --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigChainRequestExtractorService.java @@ -0,0 +1,56 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query; + +import com.alibaba.nacos.common.spi.NacosServiceLoader; +import com.alibaba.nacos.config.server.exception.NacosConfigException; +import com.alibaba.nacos.sys.env.EnvUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.Optional; + +/** + * Service class for initializing and retrieving the configuration query request extractor. + * + * @author Nacos + */ +public class ConfigChainRequestExtractorService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigChainRequestExtractorService.class); + + private static ConfigQueryChainRequestExtractor extractor; + + static { + String curExtractor = EnvUtil.getProperty("nacos.config.query.chain.request.extractor", "nacos"); + Optional optionalBuilder = NacosServiceLoader.load(ConfigQueryChainRequestExtractor.class) + .stream() + .filter(builder -> builder.getName().equals(curExtractor)) + .findFirst(); + if (optionalBuilder.isPresent()) { + extractor = optionalBuilder.get(); + LOGGER.info("ConfigQueryRequestExtractor has been initialized successfully with extractor: {}", curExtractor); + } else { + String errorMessage = "No suitable ConfigQueryRequestExtractor found for name: " + curExtractor; + LOGGER.error(errorMessage); + throw new NacosConfigException(errorMessage); + } + } + + public static ConfigQueryChainRequestExtractor getExtractor() { + return extractor; + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryChainRequestExtractor.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryChainRequestExtractor.java new file mode 100644 index 00000000000..ae229cb4709 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryChainRequestExtractor.java @@ -0,0 +1,55 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query; + +import com.alibaba.nacos.api.config.remote.request.ConfigQueryRequest; +import com.alibaba.nacos.api.remote.request.RequestMeta; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; + +import javax.servlet.http.HttpServletRequest; + +/** + * Interface for extracting configuration query chain requests from different sources. + * + * @author Nacos + */ +public interface ConfigQueryChainRequestExtractor { + + /** + * Gets the name of the current implementation. + * + * @return the name of the current implementation + */ + String getName(); + + /** + * Extracts a configuration query chain request from an HTTP request. + * + * @param request the HTTP request object + * @return the extracted configuration query chain request + */ + ConfigQueryChainRequest extract(HttpServletRequest request); + + /** + * Extracts a configuration query chain request from a configuration query request object. + * + * @param request the configuration query request object + * @param requestMeta the request metadata + * @return the extracted configuration query chain request + */ + ConfigQueryChainRequest extract(ConfigQueryRequest request, RequestMeta requestMeta); +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryChainService.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryChainService.java new file mode 100644 index 00000000000..57c8d5659b6 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryChainService.java @@ -0,0 +1,73 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query; + +import com.alibaba.nacos.common.spi.NacosServiceLoader; +import com.alibaba.nacos.config.server.exception.NacosConfigException; +import com.alibaba.nacos.config.server.service.query.enums.ResponseCode; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; +import com.alibaba.nacos.sys.env.EnvUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +/** + * Service class for initializing and retrieving the configuration query chain builder. + * + * @author Nacos + */ +@Service +public class ConfigQueryChainService { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigQueryChainService.class); + + private final ConfigQueryHandlerChain chain; + + public ConfigQueryChainService() { + String curChain = EnvUtil.getProperty("nacos.config.query.chain.builder", "nacos"); + Optional optionalBuilder = NacosServiceLoader.load(ConfigQueryHandlerChainBuilder.class) + .stream() + .filter(builder -> builder.getName().equals(curChain)) + .findFirst(); + if (optionalBuilder.isPresent()) { + chain = optionalBuilder.get().build(); + LOGGER.info("ConfigQueryHandlerChain has been initialized successfully with chain: {}", curChain); + } else { + String errorMessage = "No suitable ConfigQueryHandlerChainBuilder found for name: " + curChain; + LOGGER.error(errorMessage); + throw new NacosConfigException(errorMessage); + } + } + + /** + * Handles the configuration query request. + * + * @param request the configuration query request object + * @return the configuration query response object + */ + public ConfigQueryChainResponse handle(ConfigQueryChainRequest request) { + try { + return chain.handle(request); + } catch (Exception e) { + LOGGER.error("[Error] Fail to handle ConfigQueryChainRequest", e); + return ConfigQueryChainResponse.buildFailResponse(ResponseCode.FAIL.getCode(), e.getMessage()); + } + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryHandlerChain.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryHandlerChain.java new file mode 100644 index 00000000000..720fcb811b4 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryHandlerChain.java @@ -0,0 +1,70 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query; + +import com.alibaba.nacos.config.server.service.query.handler.ConfigQueryHandler; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Objects; + +/** + * ConfigQueryHandlerChain. + * @author Nacos + */ +public class ConfigQueryHandlerChain { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigQueryHandlerChain.class); + + private ConfigQueryHandler head; + + private ConfigQueryHandler tail; + + public ConfigQueryHandlerChain() { + } + + /** + * Adds a new configuration query handler to the chain. + * + * @param handler the configuration query handler to be added + * @return the current configuration query handler chain object, supporting method chaining + */ + public ConfigQueryHandlerChain addHandler(ConfigQueryHandler handler) { + if (Objects.isNull(handler)) { + LOGGER.warn("Attempted to add a null config query handler"); + return this; + } + + if (head == null) { + head = handler; + tail = handler; + } else { + tail.setNextHandler(handler); + tail = handler; + } + + return this; + } + + public ConfigQueryChainResponse handle(ConfigQueryChainRequest request) throws IOException { + return head.handle(request); + } + +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryHandlerChainBuilder.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryHandlerChainBuilder.java new file mode 100644 index 00000000000..e9f1b0d544c --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/ConfigQueryHandlerChainBuilder.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query; + +/** + * ConfigQueryHandlerChainBuilder. + * + * @author Nacos + */ +public interface ConfigQueryHandlerChainBuilder { + + /** + * Builds the configuration query handler chain. + * + * @return the configuration query handler chain + */ + ConfigQueryHandlerChain build(); + + /** + * Gets the name of the builder. + * + * @return the name of the builder + */ + String getName(); +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/DefaultChainRequestExtractor.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/DefaultChainRequestExtractor.java new file mode 100644 index 00000000000..4e3ca0c7ea4 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/DefaultChainRequestExtractor.java @@ -0,0 +1,96 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query; + +import com.alibaba.nacos.api.config.remote.request.ConfigQueryRequest; +import com.alibaba.nacos.api.remote.request.RequestMeta; +import com.alibaba.nacos.common.utils.StringUtils; +import com.alibaba.nacos.config.server.model.gray.BetaGrayRule; +import com.alibaba.nacos.config.server.model.gray.TagGrayRule; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.utils.RequestUtil; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +import static com.alibaba.nacos.api.common.Constants.VIPSERVER_TAG; + +/** + * DefaultChainRequestExtractor. + * + * @author Nacos + */ +public class DefaultChainRequestExtractor implements ConfigQueryChainRequestExtractor { + + @Override + public String getName() { + return "nacos"; + } + + @Override + public ConfigQueryChainRequest extract(HttpServletRequest request) { + final String dataId = request.getParameter("dataId"); + final String group = request.getParameter("group"); + String tenant = request.getParameter("tenant"); + if (StringUtils.isBlank(tenant)) { + tenant = StringUtils.EMPTY; + } + String tag = request.getParameter("tag"); + String autoTag = request.getHeader(VIPSERVER_TAG); + String clientIp = RequestUtil.getRemoteIp(request); + + Map appLabels = new HashMap<>(4); + appLabels.put(BetaGrayRule.CLIENT_IP_LABEL, clientIp); + if (StringUtils.isNotBlank(tag)) { + appLabels.put(TagGrayRule.VIP_SERVER_TAG_LABEL, tag); + } else if (StringUtils.isNotBlank(autoTag)) { + appLabels.put(TagGrayRule.VIP_SERVER_TAG_LABEL, autoTag); + } + + ConfigQueryChainRequest chainRequest = new ConfigQueryChainRequest(); + chainRequest.setDataId(dataId); + chainRequest.setGroup(group); + chainRequest.setTenant(tenant); + chainRequest.setTag(tag); + chainRequest.setAppLabels(appLabels); + + return chainRequest; + } + + @Override + public ConfigQueryChainRequest extract(ConfigQueryRequest request, RequestMeta requestMeta) { + ConfigQueryChainRequest chainRequest = new ConfigQueryChainRequest(); + + String tag = request.getTag(); + Map appLabels = new HashMap<>(4); + appLabels.put(BetaGrayRule.CLIENT_IP_LABEL, requestMeta.getClientIp()); + if (StringUtils.isNotBlank(tag)) { + appLabels.put(TagGrayRule.VIP_SERVER_TAG_LABEL, tag); + } else { + appLabels.putAll(requestMeta.getAppLabels()); + } + + chainRequest.setDataId(request.getDataId()); + chainRequest.setGroup(request.getGroup()); + chainRequest.setTenant(request.getTenant()); + chainRequest.setTag(request.getTag()); + chainRequest.setAppLabels(appLabels); + + return chainRequest; + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/DefaultConfigQueryHandlerChainBuilder.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/DefaultConfigQueryHandlerChainBuilder.java new file mode 100644 index 00000000000..464befe65e8 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/DefaultConfigQueryHandlerChainBuilder.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query; + +import com.alibaba.nacos.config.server.service.query.handler.ConfigChainEntryHandler; +import com.alibaba.nacos.config.server.service.query.handler.FormalHandler; +import com.alibaba.nacos.config.server.service.query.handler.GrayRuleMatchHandler; +import com.alibaba.nacos.config.server.service.query.handler.SpecialTagNotFoundHandler; + +/** + * DefaultConfigQueryHandlerChainBuilder. + * + * @author Nacos + */ +public class DefaultConfigQueryHandlerChainBuilder implements ConfigQueryHandlerChainBuilder { + + @Override + public ConfigQueryHandlerChain build() { + ConfigQueryHandlerChain chain = new ConfigQueryHandlerChain(); + chain.addHandler(new ConfigChainEntryHandler()) + .addHandler(new GrayRuleMatchHandler()) + .addHandler(new SpecialTagNotFoundHandler()) + .addHandler(new FormalHandler()); + return chain; + } + + @Override + public String getName() { + return "nacos"; + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/enums/ResponseCode.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/enums/ResponseCode.java new file mode 100644 index 00000000000..7dc0af9f9db --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/enums/ResponseCode.java @@ -0,0 +1,61 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.enums; + +/** + * ResponseCode. + * + * @author Nacos + */ +public enum ResponseCode { + /** + * Request success. + */ + SUCCESS(200, "Response ok"), + + /** + * Request failed. + */ + FAIL(500, "Response fail"); + + int code; + + String desc; + + ResponseCode(int code, String desc) { + this.code = code; + this.desc = desc; + } + + /** + * Getter method for property code. + * + * @return property value of code + */ + public int getCode() { + return code; + } + + /** + * Getter method for property desc. + * + * @return property value of desc + */ + public String getDesc() { + return desc; + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/AbstractConfigQueryHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/AbstractConfigQueryHandler.java new file mode 100644 index 00000000000..90254ea819d --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/AbstractConfigQueryHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.handler; + +/** + * AbstractConfigQueryHandler. + * This abstract class provides a base implementation for configuration query handlers. + * It implements the {@link ConfigQueryHandler} interface and handles the chaining of handlers. + * + * @author Nacos + */ +public abstract class AbstractConfigQueryHandler implements ConfigQueryHandler { + + public ConfigQueryHandler nextHandler; + + public void setNextHandler(ConfigQueryHandler nextHandler) { + this.nextHandler = nextHandler; + } + + public ConfigQueryHandler getNextHandler() { + return this.nextHandler; + } + +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/ConfigChainEntryHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/ConfigChainEntryHandler.java new file mode 100644 index 00000000000..a87e92a1482 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/ConfigChainEntryHandler.java @@ -0,0 +1,86 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.handler; + +import com.alibaba.nacos.config.server.model.CacheItem; +import com.alibaba.nacos.config.server.service.ConfigCacheService; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; +import com.alibaba.nacos.config.server.utils.GroupKey2; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +/** + * ConfigChainEntryHandler. + * The entry point handler for the responsibility chain, responsible for initializing the chain and handling configuration query requests. + * + * @author Nacos + */ +public class ConfigChainEntryHandler extends AbstractConfigQueryHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigChainEntryHandler.class); + + private static final String CHAIN_ENTRY_HANDLER = "chainEntryHandler"; + + private static final ThreadLocal CACHE_ITEM_THREAD_LOCAL = new ThreadLocal<>(); + + @Override + public String getName() { + return CHAIN_ENTRY_HANDLER; + } + + @Override + public ConfigQueryChainResponse handle(ConfigQueryChainRequest request) throws IOException { + String groupKey = GroupKey2.getKey(request.getDataId(), request.getGroup(), request.getTenant()); + int lockResult = ConfigCacheService.tryConfigReadLock(groupKey); + CacheItem cacheItem = ConfigCacheService.getContentCache(groupKey); + + if (lockResult > 0 && cacheItem != null) { + try { + CACHE_ITEM_THREAD_LOCAL.set(cacheItem); + if (nextHandler != null) { + return nextHandler.handle(request); + } else { + LOGGER.warn("chainEntryHandler's next handler is null"); + return new ConfigQueryChainResponse(); + } + } finally { + CACHE_ITEM_THREAD_LOCAL.remove(); + ConfigCacheService.releaseReadLock(groupKey); + } + } else if (lockResult == 0 || cacheItem == null) { + ConfigQueryChainResponse response = new ConfigQueryChainResponse(); + response.setStatus(ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_NOT_FOUND); + return response; + } else { + ConfigQueryChainResponse response = new ConfigQueryChainResponse(); + response.setStatus(ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_QUERY_CONFLICT); + return response; + } + } + + public static CacheItem getThreadLocalCacheItem() { + return CACHE_ITEM_THREAD_LOCAL.get(); + } + + public static void removeThreadLocalCacheItem() { + CACHE_ITEM_THREAD_LOCAL.remove(); + } + +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/ConfigQueryHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/ConfigQueryHandler.java new file mode 100644 index 00000000000..a1a690b30f0 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/ConfigQueryHandler.java @@ -0,0 +1,58 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.handler; + +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; + +import java.io.IOException; + +/** + * Configuration Query Handler Interface. + * This interface defines the standard methods for handling configuration query requests. + * + * @author Nacos + */ +public interface ConfigQueryHandler { + + /** + * Gets the name of the handler. + * @return The name of the handler. + */ + String getName(); + + /** + * Handles the configuration query request. + * If the current handler cannot process the request, it should throw an IOException. + * @param request The configuration query request. + * @return The response to the configuration query. + * @throws IOException If an I/O error occurs. + */ + ConfigQueryChainResponse handle(ConfigQueryChainRequest request) throws IOException; + + /** + * Sets the next handler in the chain. + * @param nextHandler The next handler to which the request can be passed if the current handler cannot process it. + */ + void setNextHandler(ConfigQueryHandler nextHandler); + + /** + * Gets the next handler in the chain. + * @return The next handler. + */ + ConfigQueryHandler getNextHandler(); +} diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/FormalHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/FormalHandler.java new file mode 100644 index 00000000000..660c956f3ce --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/FormalHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.handler; + +import com.alibaba.nacos.config.server.model.CacheItem; +import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskServiceFactory; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; + +import java.io.IOException; + +import static com.alibaba.nacos.config.server.constant.Constants.ENCODE_UTF8; + +/** + * Formal Handler. + * This class represents a formal handler in the configuration query processing chain. + * If the request has not been processed by previous handlers, it will be handled by this handler. + * @author Nacos + */ +public class FormalHandler extends AbstractConfigQueryHandler { + + private static final String FORMAL_HANDLER = "formalHandler"; + + @Override + public String getName() { + return FORMAL_HANDLER; + } + + @Override + public ConfigQueryChainResponse handle(ConfigQueryChainRequest request) throws IOException { + ConfigQueryChainResponse response = new ConfigQueryChainResponse(); + + String dataId = request.getDataId(); + String group = request.getGroup(); + String tenant = request.getTenant(); + + CacheItem cacheItem = ConfigChainEntryHandler.getThreadLocalCacheItem(); + String md5 = cacheItem.getConfigCache().getMd5(ENCODE_UTF8); + long lastModified = cacheItem.getConfigCache().getLastModifiedTs(); + String encryptedDataKey = cacheItem.getConfigCache().getEncryptedDataKey(); + String contentType = cacheItem.getType(); + String content = ConfigDiskServiceFactory.getInstance().getContent(dataId, group, tenant); + + response.setContent(content); + response.setMd5(md5); + response.setLastModified(lastModified); + response.setEncryptedDataKey(encryptedDataKey); + response.setContentType(contentType); + response.setStatus(ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_FOUND_FORMAL); + + return response; + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/GrayRuleMatchHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/GrayRuleMatchHandler.java new file mode 100644 index 00000000000..e95640539d9 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/GrayRuleMatchHandler.java @@ -0,0 +1,82 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.handler; + +import com.alibaba.nacos.config.server.model.CacheItem; +import com.alibaba.nacos.config.server.model.ConfigCacheGray; +import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskServiceFactory; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; + +import java.io.IOException; + +import static com.alibaba.nacos.config.server.constant.Constants.ENCODE_UTF8; + +/** + * GrayRuleMatchHandler. + * This class represents a gray rule handler in the configuration query processing chain. + * It checks if the request matches any gray rules and processes the request accordingly. + * + * @author Nacos + */ +public class GrayRuleMatchHandler extends AbstractConfigQueryHandler { + + private static final String GRAY_RULE_MATCH_HANDLER = "grayRuleMatchHandler"; + + @Override + public String getName() { + return GRAY_RULE_MATCH_HANDLER; + } + + @Override + public ConfigQueryChainResponse handle(ConfigQueryChainRequest request) throws IOException { + // Check if the request matches any gray rules + CacheItem cacheItem = ConfigChainEntryHandler.getThreadLocalCacheItem(); + ConfigCacheGray matchedGray = null; + if (cacheItem.getSortConfigGrays() != null && !cacheItem.getSortConfigGrays().isEmpty()) { + for (ConfigCacheGray configCacheGray : cacheItem.getSortConfigGrays()) { + if (configCacheGray.match(request.getAppLabels())) { + matchedGray = configCacheGray; + break; + } + } + } + + if (matchedGray != null) { + ConfigQueryChainResponse response = new ConfigQueryChainResponse(); + + long lastModified = matchedGray.getLastModifiedTs(); + String md5 = matchedGray.getMd5(ENCODE_UTF8); + String encryptedDataKey = matchedGray.getEncryptedDataKey(); + String content = ConfigDiskServiceFactory.getInstance() + .getGrayContent(request.getDataId(), request.getGroup(), request.getTenant(), + matchedGray.getGrayName()); + + response.setContent(content); + response.setMd5(md5); + response.setLastModified(lastModified); + response.setEncryptedDataKey(encryptedDataKey); + response.setMatchedGray(matchedGray); + response.setContentType(cacheItem.getType()); + response.setStatus(ConfigQueryChainResponse.ConfigQueryStatus.CONFIG_FOUND_GRAY); + + return response; + } else { + return nextHandler.handle(request); + } + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/SpecialTagNotFoundHandler.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/SpecialTagNotFoundHandler.java new file mode 100644 index 00000000000..08dee4c44ce --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/handler/SpecialTagNotFoundHandler.java @@ -0,0 +1,72 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.handler; + +import com.alibaba.nacos.common.utils.StringUtils; +import com.alibaba.nacos.config.server.model.CacheItem; +import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskServiceFactory; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainRequest; +import com.alibaba.nacos.config.server.service.query.model.ConfigQueryChainResponse; + +import java.io.IOException; + +import static com.alibaba.nacos.config.server.constant.Constants.ENCODE_UTF8; + +/** + * SpecialTagNotFound Handler. + * This class represents special tag not found handler in the configuration query processing chain. + * + * @author Nacos + */ +public class SpecialTagNotFoundHandler extends AbstractConfigQueryHandler { + + private static final String SPECIAL_TAG_NOT_FOUND_HANDLER = "specialTagNotFoundHandler"; + + @Override + public String getName() { + return SPECIAL_TAG_NOT_FOUND_HANDLER; + } + + @Override + public ConfigQueryChainResponse handle(ConfigQueryChainRequest request) throws IOException { + if (StringUtils.isNotBlank(request.getTag())) { + ConfigQueryChainResponse response = new ConfigQueryChainResponse(); + + String dataId = request.getDataId(); + String group = request.getGroup(); + String tenant = request.getTenant(); + + CacheItem cacheItem = ConfigChainEntryHandler.getThreadLocalCacheItem(); + String md5 = cacheItem.getConfigCache().getMd5(ENCODE_UTF8); + long lastModified = cacheItem.getConfigCache().getLastModifiedTs(); + String encryptedDataKey = cacheItem.getConfigCache().getEncryptedDataKey(); + String contentType = cacheItem.getType(); + String content = ConfigDiskServiceFactory.getInstance().getContent(dataId, group, tenant); + + response.setContent(content); + response.setMd5(md5); + response.setLastModified(lastModified); + response.setEncryptedDataKey(encryptedDataKey); + response.setContentType(contentType); + response.setStatus(ConfigQueryChainResponse.ConfigQueryStatus.SPECIAL_TAG_CONFIG_NOT_FOUND); + + return response; + } else { + return nextHandler.handle(request); + } + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/model/ConfigQueryChainRequest.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/model/ConfigQueryChainRequest.java new file mode 100644 index 00000000000..7b20943e03c --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/model/ConfigQueryChainRequest.java @@ -0,0 +1,99 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.model; + +import java.util.Map; +import java.util.Objects; + +/** + * ConfigQueryChainRequest. + * + * @author Nacos + */ +public class ConfigQueryChainRequest { + + private String dataId; + + private String group; + + private String tenant; + + private String tag; + + private Map appLabels; + + public String getDataId() { + return dataId; + } + + public void setDataId(String dataId) { + this.dataId = dataId; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public String getTag() { + return tag; + } + + public void setTag(String tag) { + this.tag = tag; + } + + public Map getAppLabels() { + return appLabels; + } + + public void setAppLabels(Map appLabels) { + this.appLabels = appLabels; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigQueryChainRequest that = (ConfigQueryChainRequest) o; + return Objects.equals(dataId, that.dataId) + && Objects.equals(group, that.group) + && Objects.equals(tenant, that.tenant) + && Objects.equals(tag, that.tag) + && Objects.equals(appLabels, that.appLabels); + } + + @Override + public int hashCode() { + return Objects.hash(dataId, group, tenant, tag, appLabels); + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/query/model/ConfigQueryChainResponse.java b/config/src/main/java/com/alibaba/nacos/config/server/service/query/model/ConfigQueryChainResponse.java new file mode 100644 index 00000000000..1028e35aa03 --- /dev/null +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/query/model/ConfigQueryChainResponse.java @@ -0,0 +1,190 @@ +/* + * Copyright 1999-$toady.year Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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 com.alibaba.nacos.config.server.service.query.model; + +import com.alibaba.nacos.config.server.model.ConfigCacheGray; +import com.alibaba.nacos.config.server.service.query.enums.ResponseCode; + +import java.util.Objects; + +/** + * ConfigQueryChainResponse. + * + * @author Nacos + */ +public class ConfigQueryChainResponse { + + private String content; + + private String contentType; + + private String encryptedDataKey; + + private String md5; + + private long lastModified; + + private ConfigCacheGray matchedGray; + + private int resultCode; + + private String message; + + private ConfigQueryStatus status; + + public enum ConfigQueryStatus { + /** + * Indicates that the configuration was found and is formal. + */ + CONFIG_FOUND_FORMAL, + + /** + * Indicates that the configuration was found and is gray. + */ + CONFIG_FOUND_GRAY, + + /** + * Indicates that the configuration special tag was not found. + */ + SPECIAL_TAG_CONFIG_NOT_FOUND, + + /** + * Indicates that the configuration was not found. + */ + CONFIG_NOT_FOUND, + + /** + * Indicates a conflict in the configuration query. + */ + CONFIG_QUERY_CONFLICT, + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getEncryptedDataKey() { + return encryptedDataKey; + } + + public void setEncryptedDataKey(String encryptedDataKey) { + this.encryptedDataKey = encryptedDataKey; + } + + public String getMd5() { + return md5; + } + + public void setMd5(String md5) { + this.md5 = md5; + } + + public long getLastModified() { + return lastModified; + } + + public void setLastModified(long lastModified) { + this.lastModified = lastModified; + } + + public ConfigCacheGray getMatchedGray() { + return matchedGray; + } + + public void setMatchedGray(ConfigCacheGray matchedGray) { + this.matchedGray = matchedGray; + } + + public int getResultCode() { + return resultCode; + } + + public void setResultCode(int resultCode) { + this.resultCode = resultCode; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public ConfigQueryStatus getStatus() { + return status; + } + + public void setStatus(ConfigQueryStatus status) { + this.status = status; + } + + /** + * Build fail response. + * + * @param errorCode errorCode. + * @param message message. + * @return response. + */ + public static ConfigQueryChainResponse buildFailResponse(int errorCode, String message) { + ConfigQueryChainResponse response = new ConfigQueryChainResponse(); + response.setErrorInfo(errorCode, message); + return response; + } + + public void setErrorInfo(int errorCode, String errorMsg) { + this.resultCode = ResponseCode.FAIL.getCode(); + this.message = errorMsg; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConfigQueryChainResponse that = (ConfigQueryChainResponse) o; + return lastModified == that.lastModified + && Objects.equals(content, that.content) + && Objects.equals(contentType, that.contentType) + && Objects.equals(encryptedDataKey, that.encryptedDataKey) + && Objects.equals(md5, that.md5) + && Objects.equals(matchedGray, that.matchedGray) + && Objects.equals(resultCode, that.resultCode) + && Objects.equals(message, that.message) + && status == that.status; + } + + @Override + public int hashCode() { + return Objects.hash(content, contentType, encryptedDataKey, md5, lastModified, matchedGray, resultCode, message, status); + } +} \ No newline at end of file diff --git a/config/src/main/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImpl.java b/config/src/main/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImpl.java index 1f04f57a4c3..4c6b939edfe 100644 --- a/config/src/main/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImpl.java +++ b/config/src/main/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImpl.java @@ -1030,8 +1030,11 @@ public ConfigAllInfo findConfigAllInfo(final String dataId, final String group, public ConfigInfoStateWrapper findConfigInfoState(final String dataId, final String group, final String tenant) { String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant; try { - return this.jt.queryForObject( - "SELECT id,data_id,group_id,tenant_id,gmt_modified FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?", + ConfigInfoMapper configInfoMapper = mapperManager.findMapper(dataSourceService.getDataSourceType(), + TableConstant.CONFIG_INFO); + return this.jt.queryForObject(configInfoMapper.select( + Arrays.asList("id", "data_id", "group_id", "tenant_id", "gmt_modified"), + Arrays.asList("data_id", "group_id", "tenant_id")), new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_STATE_WRAPPER_ROW_MAPPER); } catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null. return null; diff --git a/config/src/main/resources/META-INF/services/com.alibaba.nacos.config.server.service.query.ConfigQueryChainRequestExtractor b/config/src/main/resources/META-INF/services/com.alibaba.nacos.config.server.service.query.ConfigQueryChainRequestExtractor new file mode 100644 index 00000000000..315a4ab2c4f --- /dev/null +++ b/config/src/main/resources/META-INF/services/com.alibaba.nacos.config.server.service.query.ConfigQueryChainRequestExtractor @@ -0,0 +1,17 @@ +# +# Copyright 1999-$toady.year Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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. +# + +com.alibaba.nacos.config.server.service.query.DefaultChainRequestExtractor \ No newline at end of file diff --git a/config/src/main/resources/META-INF/services/com.alibaba.nacos.config.server.service.query.ConfigQueryHandlerChainBuilder b/config/src/main/resources/META-INF/services/com.alibaba.nacos.config.server.service.query.ConfigQueryHandlerChainBuilder new file mode 100644 index 00000000000..26df4b5395f --- /dev/null +++ b/config/src/main/resources/META-INF/services/com.alibaba.nacos.config.server.service.query.ConfigQueryHandlerChainBuilder @@ -0,0 +1,17 @@ +# +# Copyright 1999-$toady.year Alibaba Group Holding Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# 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. +# + +com.alibaba.nacos.config.server.service.query.DefaultConfigQueryHandlerChainBuilder \ No newline at end of file diff --git a/config/src/test/java/com/alibaba/nacos/config/server/controller/ConfigServletInnerTest.java b/config/src/test/java/com/alibaba/nacos/config/server/controller/ConfigServletInnerTest.java index 651ca61fb84..a08073fd79d 100644 --- a/config/src/test/java/com/alibaba/nacos/config/server/controller/ConfigServletInnerTest.java +++ b/config/src/test/java/com/alibaba/nacos/config/server/controller/ConfigServletInnerTest.java @@ -22,6 +22,7 @@ import com.alibaba.nacos.common.utils.JacksonUtils; import com.alibaba.nacos.common.utils.MD5Utils; import com.alibaba.nacos.config.server.constant.Constants; +import com.alibaba.nacos.config.server.enums.ApiVersionEnum; import com.alibaba.nacos.config.server.model.CacheItem; import com.alibaba.nacos.config.server.model.ConfigCacheGray; import com.alibaba.nacos.config.server.model.gray.BetaGrayRule; @@ -32,6 +33,7 @@ import com.alibaba.nacos.config.server.service.LongPollingService; import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskServiceFactory; import com.alibaba.nacos.config.server.service.dump.disk.ConfigRocksDbDiskService; +import com.alibaba.nacos.config.server.service.query.ConfigQueryChainService; import com.alibaba.nacos.config.server.utils.GroupKey; import com.alibaba.nacos.config.server.utils.GroupKey2; import com.alibaba.nacos.config.server.utils.MD5Util; @@ -93,6 +95,7 @@ class ConfigServletInnerTest { void setUp() { EnvUtil.setEnvironment(new StandardEnvironment()); ReflectionTestUtils.setField(configServletInner, "longPollingService", longPollingService); + ReflectionTestUtils.setField(configServletInner, "configQueryChainService", new ConfigQueryChainService()); configCacheServiceMockedStatic = Mockito.mockStatic(ConfigCacheService.class); propertyUtilMockedStatic = Mockito.mockStatic(PropertyUtil.class); propertyUtilMockedStatic.when(PropertyUtil::getMaxContent).thenReturn(1024 * 1000); @@ -160,14 +163,17 @@ void testDoGetConfigV1Beta() throws Exception { String mockBetaContent = "content3456543"; mockGray4Beta(cacheItem, mockBetaContent, "localhost", "betaKey1234567"); MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr("localhost:8080"); + request.setParameter("dataId", dataId); + request.setParameter("group", group); + request.setParameter("tenant", tenant); + request.setRemoteAddr("localhost"); request.addHeader(CLIENT_APPNAME_HEADER, "test"); MockHttpServletResponse response = new MockHttpServletResponse(); when(configRocksDbDiskService.getGrayContent(dataId, group, tenant, BetaGrayRule.TYPE_BETA)).thenReturn( mockBetaContent); String actualValue = configServletInner.doGetConfig(request, response, dataId, group, tenant, "", "true", - "localhost"); + "localhost", ApiVersionEnum.V1); assertEquals(HttpServletResponse.SC_OK + "", actualValue); assertEquals("true", response.getHeader("isBeta")); assertEquals(MD5Utils.md5Hex(mockBetaContent, ENCODE_UTF8), response.getHeader(CONTENT_MD5)); @@ -232,7 +238,10 @@ void testDoGetConfigV1Tag() throws Exception { //test auto tag. MockHttpServletRequest request = new MockHttpServletRequest(); - request.setRemoteAddr("localhost:8080"); + request.setParameter("dataId", dataId); + request.setParameter("group", group); + request.setParameter("tenant", tenant); + request.setRemoteAddr("localhost"); request.addHeader(CLIENT_APPNAME_HEADER, "test"); request.addHeader(VIPSERVER_TAG, autoTag); MockHttpServletResponse response = new MockHttpServletResponse(); @@ -241,7 +250,7 @@ void testDoGetConfigV1Tag() throws Exception { configRocksDbDiskService.getGrayContent(dataId, group, tenant, TagGrayRule.TYPE_TAG + "_" + autoTag)) .thenReturn(autoTagContent); String actualValue = configServletInner.doGetConfig(request, response, dataId, group, tenant, null, "true", - "localhost"); + "localhost", ApiVersionEnum.V1); assertEquals(HttpServletResponse.SC_OK + "", actualValue); assertEquals(autoTagContent, response.getContentAsString()); assertEquals(MD5Utils.md5Hex(autoTagContent, "UTF-8"), response.getHeader(CONTENT_MD5)); @@ -249,21 +258,23 @@ void testDoGetConfigV1Tag() throws Exception { //test for specific tag. has higher propority than auto tag. response = new MockHttpServletResponse(); + request.setParameter("tag", specificTag); when(configRocksDbDiskService.getGrayContent(dataId, group, tenant, TagGrayRule.TYPE_TAG + "_" + specificTag)).thenReturn(specificTagContent); actualValue = configServletInner.doGetConfig(request, response, dataId, group, tenant, specificTag, "true", - "localhost"); + "localhost", ApiVersionEnum.V1); assertEquals(HttpServletResponse.SC_OK + "", actualValue); assertEquals(specificTagContent, response.getContentAsString()); assertEquals(MD5Utils.md5Hex(specificTagContent, "UTF-8"), response.getHeader(CONTENT_MD5)); assertEquals("specificTagkey", response.getHeader("Encrypted-Data-Key")); // test for specific tag ,not exist + request.setParameter("tag", "auto-tag-test-not-exist"); when(configRocksDbDiskService.getGrayContent(dataId, group, tenant, TagGrayRule.TYPE_TAG + "_" + "auto-tag-test-not-exist")).thenReturn(null); response = new MockHttpServletResponse(); actualValue = configServletInner.doGetConfig(request, response, dataId, group, tenant, - "auto-tag-test-not-exist", "true", "localhost"); + "auto-tag-test-not-exist", "true", "localhost", ApiVersionEnum.V1); assertEquals(HttpServletResponse.SC_NOT_FOUND + "", actualValue); String expectedContent = "config data not exist"; String actualContent = response.getContentAsString(); @@ -283,7 +294,7 @@ void testDoGetConfigFormal() throws Exception { //mock cache item . CacheItem cacheItem = new CacheItem("test"); String md5 = "md5wertyui"; - String content = "content345678"; + final String content = "content345678"; cacheItem.getConfigCache().setMd5Utf8(md5); long ts = System.currentTimeMillis(); cacheItem.getConfigCache().setLastModifiedTs(ts); @@ -292,11 +303,14 @@ void testDoGetConfigFormal() throws Exception { () -> ConfigCacheService.getContentCache(GroupKey.getKeyTenant(dataId, group, tenant))) .thenReturn(cacheItem); MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("dataId", dataId); + request.setParameter("group", group); + request.setParameter("tenant", tenant); MockHttpServletResponse response = new MockHttpServletResponse(); when(configRocksDbDiskService.getContent(dataId, group, tenant)).thenReturn(content); String actualValue = configServletInner.doGetConfig(request, response, dataId, group, tenant, null, "true", - "localhost"); + "localhost", ApiVersionEnum.V1); assertEquals(content, response.getContentAsString()); assertEquals(HttpServletResponse.SC_OK + "", actualValue); assertEquals(md5, response.getHeader(CONTENT_MD5)); @@ -315,7 +329,7 @@ void testDoGetConfigFormalV2() throws Exception { //mock cache item . CacheItem cacheItem = new CacheItem("test"); String md5 = "md5wertyui"; - String content = "content345678"; + final String content = "content345678"; cacheItem.getConfigCache().setMd5Utf8(md5); long ts = System.currentTimeMillis(); cacheItem.getConfigCache().setLastModifiedTs(ts); @@ -324,11 +338,14 @@ void testDoGetConfigFormalV2() throws Exception { () -> ConfigCacheService.getContentCache(GroupKey.getKeyTenant(dataId, group, tenant))) .thenReturn(cacheItem); MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("dataId", dataId); + request.setParameter("group", group); + request.setParameter("tenant", tenant); MockHttpServletResponse response = new MockHttpServletResponse(); when(configRocksDbDiskService.getContent(dataId, group, tenant)).thenReturn(content); String actualValue = configServletInner.doGetConfig(request, response, dataId, group, tenant, null, "true", - "localhost", true); + "localhost", ApiVersionEnum.V2); assertEquals(JacksonUtils.toJson(Result.success(content)), response.getContentAsString()); assertEquals(HttpServletResponse.SC_OK + "", actualValue); assertEquals(md5, response.getHeader(CONTENT_MD5)); @@ -338,13 +355,21 @@ void testDoGetConfigFormalV2() throws Exception { @Test void testDoGetConfigNotExist() throws Exception { + String dataId = "test"; + String group = "test"; + final String tenant = "test"; + final String tag = "test"; // if lockResult equals 0,cache item not exist. configCacheServiceMockedStatic.when(() -> ConfigCacheService.tryConfigReadLock(anyString())).thenReturn(0); MockHttpServletRequest request = new MockHttpServletRequest(); + request.setParameter("dataId", dataId); + request.setParameter("group", group); + request.setParameter("tenant", tenant); + request.setParameter("tag", tag); MockHttpServletResponse response = new MockHttpServletResponse(); - String actualValue = configServletInner.doGetConfig(request, response, "test", "test", "test", "test", "true", - "localhost"); + String actualValue = configServletInner.doGetConfig(request, response, dataId, group, tenant, tag, "true", + "localhost", ApiVersionEnum.V1); assertEquals(HttpServletResponse.SC_NOT_FOUND + "", actualValue); configCacheServiceMockedStatic.when( @@ -353,7 +378,7 @@ void testDoGetConfigNotExist() throws Exception { // if lockResult less than 0 configCacheServiceMockedStatic.when(() -> ConfigCacheService.tryConfigReadLock(anyString())).thenReturn(-1); actualValue = configServletInner.doGetConfig(request, response, "test", "test", "test", "test", "true", - "localhost"); + "localhost", ApiVersionEnum.V1); assertEquals(HttpServletResponse.SC_CONFLICT + "", actualValue); } diff --git a/config/src/test/java/com/alibaba/nacos/config/server/controller/v2/ConfigControllerV2Test.java b/config/src/test/java/com/alibaba/nacos/config/server/controller/v2/ConfigControllerV2Test.java index 7eb34abfa8a..a94130b7f07 100644 --- a/config/src/test/java/com/alibaba/nacos/config/server/controller/v2/ConfigControllerV2Test.java +++ b/config/src/test/java/com/alibaba/nacos/config/server/controller/v2/ConfigControllerV2Test.java @@ -22,6 +22,7 @@ import com.alibaba.nacos.common.utils.JacksonUtils; import com.alibaba.nacos.config.server.constant.Constants; import com.alibaba.nacos.config.server.controller.ConfigServletInner; +import com.alibaba.nacos.config.server.enums.ApiVersionEnum; import com.alibaba.nacos.config.server.model.ConfigInfo; import com.alibaba.nacos.config.server.model.ConfigRequestInfo; import com.alibaba.nacos.config.server.model.form.ConfigForm; @@ -136,12 +137,12 @@ void testGetConfig() throws Exception { x.getArgument(1, HttpServletResponse.class).getWriter().print(JacksonUtils.toJson(stringResult)); return null; }).when(inner).doGetConfig(any(HttpServletRequest.class), any(HttpServletResponse.class), eq(TEST_DATA_ID), eq(TEST_GROUP), - eq(TEST_NAMESPACE_ID), eq(TEST_TAG), eq(null), anyString(), eq(true)); + eq(TEST_NAMESPACE_ID), eq(TEST_TAG), eq(null), anyString(), eq(ApiVersionEnum.V2)); configControllerV2.getConfig(request, response, TEST_DATA_ID, TEST_GROUP, TEST_NAMESPACE_ID, TEST_TAG); verify(inner).doGetConfig(eq(request), eq(response), eq(TEST_DATA_ID), eq(TEST_GROUP), eq(TEST_NAMESPACE_ID), eq(TEST_TAG), - eq(null), anyString(), eq(true)); + eq(null), anyString(), eq(ApiVersionEnum.V2)); JsonNode resNode = JacksonUtils.toObj(response.getContentAsString()); Integer errCode = JacksonUtils.toObj(resNode.get("code").toString(), Integer.class); String actContent = JacksonUtils.toObj(resNode.get("data").toString(), String.class); diff --git a/config/src/test/java/com/alibaba/nacos/config/server/remote/ConfigQueryRequestHandlerTest.java b/config/src/test/java/com/alibaba/nacos/config/server/remote/ConfigQueryRequestHandlerTest.java index 7762f6f56c0..4715b525ea3 100644 --- a/config/src/test/java/com/alibaba/nacos/config/server/remote/ConfigQueryRequestHandlerTest.java +++ b/config/src/test/java/com/alibaba/nacos/config/server/remote/ConfigQueryRequestHandlerTest.java @@ -26,6 +26,7 @@ import com.alibaba.nacos.config.server.model.gray.ConfigGrayPersistInfo; import com.alibaba.nacos.config.server.model.gray.GrayRuleManager; import com.alibaba.nacos.config.server.model.gray.TagGrayRule; +import com.alibaba.nacos.config.server.service.query.ConfigQueryChainService; import com.alibaba.nacos.config.server.service.ConfigCacheService; import com.alibaba.nacos.config.server.service.dump.disk.ConfigDiskServiceFactory; import com.alibaba.nacos.config.server.service.dump.disk.ConfigRocksDbDiskService; @@ -85,7 +86,7 @@ void setUp() throws IOException { configCacheServiceMockedStatic = Mockito.mockStatic(ConfigCacheService.class); propertyUtilMockedStatic = Mockito.mockStatic(PropertyUtil.class); configDiskServiceFactoryMockedStatic = Mockito.mockStatic(ConfigDiskServiceFactory.class); - configQueryRequestHandler = new ConfigQueryRequestHandler(); + configQueryRequestHandler = new ConfigQueryRequestHandler(new ConfigQueryChainService()); final String groupKey = GroupKey2.getKey(dataId, group, ""); when(ConfigCacheService.tryConfigReadLock(groupKey)).thenReturn(1); propertyUtilMockedStatic.when(PropertyUtil::getMaxContent).thenReturn(1024 * 1000); @@ -207,9 +208,9 @@ void testGetTagNotFound() throws Exception { //check content&md5 assertNull(response.getContent()); - assertNull(response.getMd5()); + assertEquals(MD5Utils.md5Hex(content, "UTF-8"), response.getMd5()); assertEquals(CONFIG_NOT_FOUND, response.getErrorCode()); - assertNull(response.getEncryptedDataKey()); + assertEquals("key_testGetTag_NotFound", response.getEncryptedDataKey()); //check flags. assertFalse(response.isBeta()); diff --git a/config/src/test/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImplTest.java b/config/src/test/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImplTest.java index 37d7614afe0..adce23f16e0 100644 --- a/config/src/test/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImplTest.java +++ b/config/src/test/java/com/alibaba/nacos/config/server/service/repository/extrnal/ExternalConfigInfoPersistServiceImplTest.java @@ -34,6 +34,7 @@ import com.alibaba.nacos.persistence.datasource.DataSourceService; import com.alibaba.nacos.persistence.datasource.DynamicDataSource; import com.alibaba.nacos.persistence.model.Page; +import com.alibaba.nacos.plugin.datasource.MapperManager; import com.alibaba.nacos.plugin.datasource.constants.TableConstant; import com.alibaba.nacos.plugin.datasource.mapper.ConfigInfoMapper; import com.alibaba.nacos.sys.env.EnvUtil; @@ -1114,7 +1115,7 @@ void testFindConfigInfoState() { assertTrue(e.getMessage().endsWith("mock exp")); } } - + @Test void testFindAllConfigInfo4Export() { @@ -1250,5 +1251,16 @@ void testFindAllConfigInfoFragment() { } } + + @Test + void testBuildFindConfigInfoStateSql() { + MapperManager mapperManager = MapperManager.instance(false); + ConfigInfoMapper configInfoMapper = mapperManager.findMapper(dataSourceService.getDataSourceType(), + TableConstant.CONFIG_INFO); + String select = configInfoMapper.select( + Arrays.asList("id", "data_id", "group_id", "tenant_id", "gmt_modified"), + Arrays.asList("data_id", "group_id", "tenant_id")); + assertEquals("SELECT id,data_id,group_id,tenant_id,gmt_modified FROM config_info WHERE data_id = ? AND group_id = ? AND tenant_id = ?", select); + } } diff --git a/console-ui/src/locales/en-US.js b/console-ui/src/locales/en-US.js index 3c34dfccdd4..761e12a9a41 100644 --- a/console-ui/src/locales/en-US.js +++ b/console-ui/src/locales/en-US.js @@ -672,6 +672,7 @@ const I18N_CONF = { defaultFuzzyd: 'Default fuzzy query mode opened', fuzzyd: "Add wildcard '*' for fuzzy query", query: 'Search', + checkPermission: 'This role permission already exists!', }, NewPermissions: { addPermission: 'Add Permission', diff --git a/console-ui/src/locales/zh-CN.js b/console-ui/src/locales/zh-CN.js index 360a154bab3..59f7f554292 100644 --- a/console-ui/src/locales/zh-CN.js +++ b/console-ui/src/locales/zh-CN.js @@ -667,6 +667,7 @@ const I18N_CONF = { defaultFuzzyd: '已开启默认模糊查询', fuzzyd: "添加通配符'*'进行模糊查询", query: '查询', + checkPermission: '此角色权限已存在!', }, NewPermissions: { addPermission: '添加权限', diff --git a/console-ui/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.js b/console-ui/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.js index 74c1a71a56d..ebe5c3091a7 100644 --- a/console-ui/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.js +++ b/console-ui/src/pages/AuthorityControl/PermissionsManagement/PermissionsManagement.js @@ -25,9 +25,15 @@ import { Form, Input, Switch, + Message, } from '@alifd/next'; import { connect } from 'react-redux'; -import { getPermissions, createPermission, deletePermission } from '../../../reducers/authority'; +import { + getPermissions, + checkPermission, + createPermission, + deletePermission, +} from '../../../reducers/authority'; import { getNamespaces } from '../../../reducers/namespace'; import RegionGroup from '../../../components/RegionGroup'; import NewPermissions from './NewPermissions'; @@ -217,9 +223,17 @@ class PermissionsManagement extends React.Component { - createPermission(permission).then(res => { - this.setState({ pageNo: 1 }, () => this.getPermissions()); - return res; + checkPermission(permission).then(res => { + if (res) { + Message.error({ + content: locale.checkPermission, + }); + } else { + createPermission(permission).then(res => { + this.setState({ pageNo: 1 }, () => this.getPermissions()); + return res; + }); + } }) } onCancel={() => this.colseCreatePermission()} diff --git a/console-ui/src/reducers/authority.js b/console-ui/src/reducers/authority.js index e52d30ba916..8606029fae4 100644 --- a/console-ui/src/reducers/authority.js +++ b/console-ui/src/reducers/authority.js @@ -119,6 +119,15 @@ const getPermissions = params => dispatch => .get('v1/auth/permissions', { params }) .then(data => dispatch({ type: PERMISSIONS_LIST, data })); +/** + * 添加权限前置校验 + * @param {*} param0 + */ +const checkPermission = ([role, resource, action]) => { + const params = { role, resource, action }; + return request.get('v1/auth/permissions', { params }).then(res => res.data); +}; + /** * 给角色添加权限 * @param {*} param0 @@ -157,6 +166,7 @@ export { createRole, deleteRole, getPermissions, + checkPermission, createPermission, deletePermission, }; diff --git a/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/controller/PermissionController.java b/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/controller/PermissionController.java index 1392f7d2129..fdd57ccee29 100644 --- a/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/controller/PermissionController.java +++ b/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/controller/PermissionController.java @@ -16,6 +16,7 @@ package com.alibaba.nacos.plugin.auth.impl.controller; +import com.alibaba.nacos.api.model.v2.Result; import com.alibaba.nacos.auth.annotation.Secured; import com.alibaba.nacos.common.model.RestResultUtils; import com.alibaba.nacos.common.utils.StringUtils; @@ -105,4 +106,18 @@ public Object deletePermission(@RequestParam String role, @RequestParam String r nacosRoleService.deletePermission(role, resource, action); return RestResultUtils.success("delete permission ok!"); } + + /** + * Judge whether a permission is duplicate. + * + * @param role the role + * @param resource the related resource + * @param action the related action + * @return true if duplicate, false otherwise + */ + @GetMapping + @Secured(resource = AuthConstants.CONSOLE_RESOURCE_NAME_PREFIX + "permissions", action = ActionTypes.READ) + public Result isDuplicatePermission(@RequestParam String role, @RequestParam String resource, @RequestParam String action) { + return nacosRoleService.isDuplicatePermission(role, resource, action); + } } diff --git a/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/roles/NacosRoleServiceImpl.java b/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/roles/NacosRoleServiceImpl.java index 7e6803d4b5d..e2907d1a37c 100644 --- a/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/roles/NacosRoleServiceImpl.java +++ b/plugin-default-impl/nacos-default-auth-plugin/src/main/java/com/alibaba/nacos/plugin/auth/impl/roles/NacosRoleServiceImpl.java @@ -16,6 +16,7 @@ package com.alibaba.nacos.plugin.auth.impl.roles; +import com.alibaba.nacos.api.model.v2.Result; import com.alibaba.nacos.auth.config.AuthConfigs; import com.alibaba.nacos.common.utils.CollectionUtils; import com.alibaba.nacos.common.utils.ConcurrentHashSet; @@ -370,5 +371,28 @@ public boolean hasGlobalAdminRole() { authConfigs.setHasGlobalAdminRole(hasGlobalAdminRole); return hasGlobalAdminRole; } + + /** + * judge whether the permission is duplicate. + * + * @param role role name + * @param resource resource + * @param action action + * @return true if duplicate, false otherwise + */ + public Result isDuplicatePermission(String role, String resource, String action) { + List permissionInfos = getPermissions(role); + if (CollectionUtils.isEmpty(permissionInfos)) { + return Result.success(Boolean.FALSE); + } + for (PermissionInfo permissionInfo : permissionInfos) { + boolean resourceMatch = StringUtils.equals(resource, permissionInfo.getResource()); + boolean actionMatch = StringUtils.equals(action, permissionInfo.getAction()) || "rw".equals(permissionInfo.getAction()); + if (resourceMatch && actionMatch) { + return Result.success(Boolean.TRUE); + } + } + return Result.success(Boolean.FALSE); + } } diff --git a/plugin-default-impl/nacos-default-auth-plugin/src/test/java/com/alibaba/nacos/plugin/auth/impl/controller/PermissionControllerTest.java b/plugin-default-impl/nacos-default-auth-plugin/src/test/java/com/alibaba/nacos/plugin/auth/impl/controller/PermissionControllerTest.java index 6f73ec63859..60eba753336 100644 --- a/plugin-default-impl/nacos-default-auth-plugin/src/test/java/com/alibaba/nacos/plugin/auth/impl/controller/PermissionControllerTest.java +++ b/plugin-default-impl/nacos-default-auth-plugin/src/test/java/com/alibaba/nacos/plugin/auth/impl/controller/PermissionControllerTest.java @@ -16,6 +16,7 @@ package com.alibaba.nacos.plugin.auth.impl.controller; +import com.alibaba.nacos.api.model.v2.Result; import com.alibaba.nacos.common.model.RestResult; import com.alibaba.nacos.persistence.model.Page; import com.alibaba.nacos.plugin.auth.impl.persistence.PermissionInfo; @@ -86,4 +87,12 @@ void testDeletePermission() { assertEquals(200, result.getCode()); } + @Test + void testDuplicatePermission() { + when(nacosRoleService.isDuplicatePermission(anyString(), anyString(), anyString())).thenReturn( + Result.success(Boolean.TRUE)); + Result result = permissionController.isDuplicatePermission("admin", "test", "test"); + assertEquals(0, result.getCode()); + } + } diff --git a/plugin-default-impl/nacos-default-auth-plugin/src/test/java/com/alibaba/nacos/plugin/auth/impl/roles/NacosRoleServiceImplTest.java b/plugin-default-impl/nacos-default-auth-plugin/src/test/java/com/alibaba/nacos/plugin/auth/impl/roles/NacosRoleServiceImplTest.java index 315b3b3cac2..5405b9ce523 100644 --- a/plugin-default-impl/nacos-default-auth-plugin/src/test/java/com/alibaba/nacos/plugin/auth/impl/roles/NacosRoleServiceImplTest.java +++ b/plugin-default-impl/nacos-default-auth-plugin/src/test/java/com/alibaba/nacos/plugin/auth/impl/roles/NacosRoleServiceImplTest.java @@ -36,6 +36,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Properties; @@ -45,6 +46,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; /** * NacosRoleServiceImpl Test. @@ -203,4 +206,16 @@ void joinResource() throws Exception { Object invoke = method.invoke(nacosRoleService, new Resource[] {resource}); assertNotNull(invoke); } + + @Test + void duplicatePermission() { + List permissionInfos = new ArrayList<>(); + PermissionInfo permissionInfo = new PermissionInfo(); + permissionInfo.setAction("rw"); + permissionInfo.setResource("test"); + permissionInfos.add(permissionInfo); + NacosRoleServiceImpl spy = spy(nacosRoleService); + when(spy.getPermissions("admin")).thenReturn(permissionInfos); + spy.isDuplicatePermission("admin", "test", "r"); + } } diff --git a/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/client/AbstractClientAuthService.java b/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/client/AbstractClientAuthService.java index e95d01bf212..d5d63411a19 100644 --- a/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/client/AbstractClientAuthService.java +++ b/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/client/AbstractClientAuthService.java @@ -27,7 +27,7 @@ */ public abstract class AbstractClientAuthService implements ClientAuthService { - protected List serverList; + protected volatile List serverList; protected NacosRestTemplate nacosRestTemplate; diff --git a/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/client/ClientAuthPluginManager.java b/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/client/ClientAuthPluginManager.java index 31cd731ff72..e1f84241c22 100644 --- a/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/client/ClientAuthPluginManager.java +++ b/plugin/auth/src/main/java/com/alibaba/nacos/plugin/auth/spi/client/ClientAuthPluginManager.java @@ -61,6 +61,17 @@ public void init(List serverList, NacosRestTemplate nacosRestTemplate) { } } + /** + * refresh ClientAuthService server list. + * + * @param serverList the new server list. + */ + public void refreshServerList(List serverList) { + for (ClientAuthService clientAuthService : clientAuthServiceHashSet) { + clientAuthService.setServerList(serverList); + } + } + /** * get all ClientAuthService instance. * diff --git a/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/mapper/AbstractMapper.java b/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/mapper/AbstractMapper.java index 0a6c307478c..f61db2a97fd 100644 --- a/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/mapper/AbstractMapper.java +++ b/plugin/datasource/src/main/java/com/alibaba/nacos/plugin/datasource/mapper/AbstractMapper.java @@ -113,6 +113,8 @@ public String update(List columns, List where) { public String delete(List params) { StringBuilder sql = new StringBuilder(); String method = "DELETE "; + sql.append(method).append("FROM ").append(getTableName()).append(" "); + appendWhereClause(params, sql); sql.append(method).append("FROM ").append(getTableName()).append(" ").append("WHERE "); sql.append(params.stream().map(str -> (str + " = ?")).collect(Collectors.joining(" AND "))); @@ -141,6 +143,14 @@ public String[] getPrimaryKeyGeneratedKeys() { return new String[]{"id"}; } + protected void appendWhereClause(List where, StringBuilder sql) { + sql.append("WHERE "); + for (int i = 0; i < where.size(); i++) { + sql.append(where.get(i)).append(" = ").append("?"); + if (i != where.size() - 1) { + sql.append(" AND "); + } + } private void appendWhereClause(List where, StringBuilder sql) { sql.append(" WHERE "); sql.append(where.stream().map(str -> (str + " = ?")).collect(Collectors.joining(" AND ")));