亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

如何使用ThreadLocal上下文解決查詢性能問題

 更新時(shí)間:2023年07月21日 10:13:07   作者:我一直在流浪  
這篇文章主要介紹了利用ThreadLocal上下文解決查詢性能問題,有兩種解決方案,一種是使用ThreadLocal上下文,另一種是使用Redis緩存,需要的朋友可以參考下

問題背景:

安全事件列表中數(shù)據(jù)右鍵操作需要控制工單處置記錄和SOAR處置記錄是否置灰,置灰的邏輯是判斷當(dāng)前用戶是否有相應(yīng)的菜單權(quán)限以及當(dāng)前租戶是否有相應(yīng)的授權(quán)權(quán)限。這個(gè)判斷邏輯在每次刷新安全事件列表時(shí)都需要調(diào)用grpc接口判斷租戶是否授權(quán),為了提高性能,所以禁止每次都去請求調(diào)用grpc接口,有兩種解決方案,一種是使用ThreadLocal上下文,另一種是使用Redis緩存。簡單介紹下如何使用ThreadLocal上下文解決性能問題。

01. 定義實(shí)體 Subscription

@Generated
public final class TenantManagement {
    @Generated
    public static final class Subscription {
        private volatile Object id;
        private volatile Object name;
        private volatile Object productId;
        private long startTime;
        private long expiryTime;
        private long createTime;
        private long renewTime;
        private volatile Object info;
        private volatile Object status;
        private volatile Object subscribeType;
    }
}

02. 定義 SubscribeInfoListContext 上下文

利用ThreadLocal上下文存儲需要緩存的數(shù)據(jù) List<TenantManagement.Subscription>

public class SubscribeInfoListContext {
    private static final ThreadLocal<List<TenantManagement.Subscription>> SUBSCRIBE_INFO_LIST_THREAD_LOCAL = new ThreadLocal<>();
    public static void set(List<TenantManagement.Subscription> subscribeInfoList) {
        SUBSCRIBE_INFO_LIST_THREAD_LOCAL.set(subscribeInfoList);
    }
    public static List<TenantManagement.Subscription> get() {
        return SUBSCRIBE_INFO_LIST_THREAD_LOCAL.get();
    }
    public static void remove() {
        SUBSCRIBE_INFO_LIST_THREAD_LOCAL.remove();
    }
    private SubscribeInfoListContext() {
    }
}

03. 定義SubscribeInfoService 接口

進(jìn)行g(shù)rpc接口調(diào)用獲取 List<TenantManagement.Subscription> 信息

public interface SubscribeInfoService {
    /**
     * 獲取授權(quán)信息
     *
     * @return 授權(quán)信息
     */
    List<TenantManagement.Subscription> getSubscribeInfoList();
}

1. 定義 SubscribeInfoServiceImpl 實(shí)現(xiàn)類

@CustomLog
@Service
public class SubscribeInfoServiceImpl implements SubscribeInfoService {
    @Setter(onMethod_ = @Autowired)
    private TenantManageClient tenantManageClient;
    @NotNull
    private static final List<String> SUBSCRIPTION_NAME_LIST = List.of(
            SubscriptionConstant.ORDER_SUBSCRIPTION,
            SubscriptionConstant.SOAR_SUBSCRIPTION
    );
    @Override
    public List<TenantManagement.Subscription> getSubscribeInfoList() {
        // 先從上下文SubscribeInfoListContext中獲取List<TenantManagement.Subscription>
        List<TenantManagement.Subscription> subscriptions = SubscribeInfoListContext.get();
        // 如果上下文SubscribeInfoListContext中獲取不到,再去調(diào)用grpc接口獲取
        if (!CollectionUtil.isEmpty(subscriptions)) {
            return subscriptions;
        }
        String tenantId = Objects.requireNonNull(TenantInfoContext.getTenantInfo()).getTenantId();
        List<TenantManagement.Subscription> subscribeInfos = tenantManageClient.getSubscribeInfoByIds(tenantId, SUBSCRIPTION_NAME_LIST);
        // 獲取到List<TenantManagement.Subscription> 后存入上下文SubscribeInfoListContext中
        SubscribeInfoListContext.set(subscribeInfos);
        return subscribeInfos;
    }
}
public interface SubscriptionInfoConstant {
    // 工單
    String ORDER_SUBSCRIPTION = "XDR_ORDER_MANAGEMENT";
    // SOAR
    String SOAR_SUBSCRIPTION = "XDR_SOAR_SECURITY_MANAGEMENT";
}

2. 判斷是否授權(quán)SOAR和是否有SOAR菜單權(quán)限 GenerateSoarDisableFlagConverter

@NoArgsConstructor
@AllArgsConstructor
public class GenerateSoarDisableFlagConverter implements ResultConverter {
    @Getter
    @Setter
    private Supplier<LicenseInfoService> licenseInfoServiceSupplier;
    @Getter
    @Setter
    private Supplier<SessionNoRelatedService> sessionNoRelatedService;
    @Getter
    @Setter
    private Supplier<SubscribeInfoService> subscribeInfoServiceSupplier;
    private static final String SUPER_ADMIN_POLICY = "superAdmin";
    private static final String SOAR_QUERY_POLICY = "soarQuery";
    private static final String SOAR_SUBSCRIPTION = "XDR_SOAR_SECURITY_MANAGEMENT";
    @Nullable
    @Override
    public Map<?, ?> convert(@NotNull SplTranslateContext<?> splTranslateContext, @Nullable Map<?, ?> map, boolean ifUsingSplReturnEntity) {
        LinkedHashMap<Object, Object> result;
        if (map == null) {
            result = new LinkedHashMap<>();
        } else {
            result = new LinkedHashMap<>(map);
        }
        Object value = !isSoarAvailable();
        result.put(
                "soarDisableFlag",
                ifUsingSplReturnEntity ? SplArrayReturnEntity.of(value)
                        : value
        );
        return result;
    }
    private Boolean isSoarAvailable() {
        return hasSoarPolicy() && hasSubscription(SOAR_SUBSCRIPTION);
    }
    public Boolean hasSubscription(String subscriptionName) {
        LicenseInfoService licenseInfoService = licenseInfoServiceSupplier.get();
        if (licenseInfoService.ifLicenseInfoIsSaas()) {
            List<TenantManagement.Subscription> subscribeInfos = subscribeInfoServiceSupplier.get().getSubscribeInfoList();
            if (CollectionUtils.isEmpty(subscribeInfos)) {
                return false;
            }
            List<TenantManagement.Subscription> subscriptionList = subscribeInfos.stream()
                    .filter(subscriptionInfo -> subscriptionInfo.getProductID().equals(subscriptionName) && checkSubscribeStatus(subscriptionInfo.getStatus()))
                    .collect(Collectors.toList());
            if (CollectionUtils.isEmpty(subscriptionList)) {
                return false;
            }
            return true;
        }
        Collection<String> moduleCodes = licenseInfoServiceSupplier.get().getActiveModuleCodes();
        return moduleCodes.contains(subscriptionName);
    }
    private boolean checkSubscribeStatus(String status) {
        return SubscriptionStatusEnum.getActivateSubscriptionStatus().contains(status);
    }
    public Boolean hasSoarPolicy() {
        SessionContextResponseVo sessionContextNoRelated = sessionNoRelatedService.get().getSessionContextNoRelated();
        if (sessionContextNoRelated == null) {
            return false;
        }
        Set<String> policies = sessionContextNoRelated.getPolicies();
        if (policies.isEmpty()) {
            return false;
        }
        return policies.contains(SOAR_QUERY_POLICY) || policies.contains(SUPER_ADMIN_POLICY);
    }
}

3. 判斷是否授權(quán)工單和是否有工單菜單權(quán)限 GenerateSoarDisableFlagConverter

@NoArgsConstructor
@AllArgsConstructor
public class GenerateOrderDisableFlagConverter implements ResultConverter {
    @Getter
    @Setter
    private Supplier<LicenseInfoService> licenseInfoServiceSupplier;
    @Getter
    @Setter
    private Supplier<SessionNoRelatedService> sessionNoRelatedServiceSupplier;
    @Getter
    @Setter
    private Supplier<SubscribeInfoService> subscribeInfoServiceSupplier;
    private static final String SUPER_ADMIN_POLICY = "superAdmin";
    private static final String ORDER_QUERY_POLICY = "safeOperationQuery";
    private static final String ORDER_SUBSCRIPTION = "XDR_ORDER_MANAGEMENT";
    @Nullable
    @Override
    public Map<?, ?> convert(@NotNull SplTranslateContext<?> splTranslateContext, @Nullable Map<?, ?> map, boolean ifUsingSplReturnEntity) {
        LinkedHashMap<Object, Object> result;
        if (map == null) {
            result = new LinkedHashMap<>();
        } else {
            result = new LinkedHashMap<>(map);
        }
        Object value = !isOrderAvailable();
        result.put(
                "orderDisableFlag",
                ifUsingSplReturnEntity ? SplArrayReturnEntity.of(value)
                        : value
        );
        return result;
    }
    private Boolean isOrderAvailable() {
        return hasOrderPolicy() && hasSubscription(ORDER_SUBSCRIPTION);
    }
    public Boolean hasSubscription(String subscriptionName) {
        LicenseInfoService licenseInfoService = licenseInfoServiceSupplier.get();
        if (licenseInfoService.ifLicenseInfoIsSaas()) {
            List<TenantManagement.Subscription> subscribeInfos = subscribeInfoServiceSupplier.get().getSubscribeInfoList();
            if (CollectionUtils.isEmpty(subscribeInfos)) {
                return false;
            }
            List<TenantManagement.Subscription> subscriptionList = subscribeInfos.stream()
                    .filter(subscriptionInfo -> subscriptionInfo.getProductID().equals(subscriptionName) && checkSubscribeStatus(subscriptionInfo.getStatus()))
                    .collect(Collectors.toList());
            if (CollectionUtils.isEmpty(subscriptionList)) {
                return false;
            }
            return true;
        }
        Collection<String> moduleCodes = licenseInfoService.getActiveModuleCodes();
        return moduleCodes.contains(subscriptionName);
    }
    private boolean checkSubscribeStatus(String status) {
        return SubscriptionStatusEnum.getActivateSubscriptionStatus().contains(status);
    }
    public Boolean hasOrderPolicy() {
        SessionContextResponseVo sessionContextNoRelated = sessionNoRelatedServiceSupplier.get().getSessionContextNoRelated();
        if (sessionContextNoRelated == null) {
            return false;
        }
        Set<String> policies = sessionContextNoRelated.getPolicies();
        if (policies.isEmpty()) {
            return false;
        }
        return policies.contains(ORDER_QUERY_POLICY) || policies.contains(SUPER_ADMIN_POLICY);
    }
}

4. 查詢安全事件列表接口中添加 ResultConverter

@CustomLog
@Service
public class IncidentSplTableHandlingServiceImpl extends SplTableHandlingServiceImpl implements InitializingBean {
    @Getter
    @Setter(onMethod_ = @Autowired)
    private LicenseInfoService licenseInfoService;
    @Getter
    @Setter(onMethod_ = @Autowired)
    private SessionNoRelatedService sessionNoRelatedService;
    @Getter
    @Setter(onMethod_ = @Autowired)
    private SubscribeInfoService subscribeInfoService;
    @Override
    public void afterPropertiesSet() throws Exception {
        List<ResultConverter> originalResultConverters = this.getResultConverters();
        List<ResultConverter> resultConverters = new ArrayList<>(originalResultConverters);
        resultConverters.add(
                new GenerateOrderDisableFlagConverter(
                        this::getLicenseInfoService,
                        this::getSessionNoRelatedService,
                        this::getSubscribeInfoService
                )
        );
        resultConverters.add(
                new GenerateSoarDisableFlagConverter(
                        this::getLicenseInfoService,
                        this::getSessionNoRelatedService,
                        this::getSubscribeInfoService
                )
        );
        this.setResultConverters(resultConverters);
    }
    // 省略.....
}

04. 添加攔截器 IncidentInterceptorConfig

在請求開始和請求結(jié)束時(shí)清除上下文SubscribeInfoListContext中的數(shù)據(jù),消除不同租戶間數(shù)據(jù)的影響。

@Data
@Configuration
@CustomLog
public class IncidentInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors( @NotNull InterceptorRegistry registry) {
        registry.addInterceptor(
                new HandlerInterceptorAdapter() {
                    // 在請求開始清除上下文SubscribeInfoListContext中的數(shù)據(jù)
                    @Override
                    public boolean preHandle(
                            @NotNull HttpServletRequest request,
                            @NotNull HttpServletResponse response,
                            @NotNull Object handler
                    ) throws Exception {
                        SubscribeInfoListContext.remove();
                        return true;
                    }
					// 在請求結(jié)束時(shí)清除上下文SubscribeInfoListContext中的數(shù)據(jù)
                    @Override
                    public void afterCompletion(
                            @NotNull HttpServletRequest request,
                            @NotNull HttpServletResponse response,
                            @NotNull Object handler,
                            @Nullable Exception ex
                    ) throws Exception {
                        SubscribeInfoListContext.remove();
                    }
                }
        );
    }
}

05. 添加 SaasThreadContextDataHolderSubscription 存儲緩存信息

public interface SaasThreadContextDataHolder {
}
@Data
@AllArgsConstructor
public class SaasThreadContextDataHolderSubscription implements SaasThreadContextDataHolder {
    @Nullable
    private final List<TenantManagement.Subscription> subscriptionList;
}

06. 添加 SaasThreadContextHolderSubscriptionInfo 以便后續(xù)擴(kuò)展和使用

public interface SaasThreadContextHolder<T extends SaasThreadContextDataHolder> {
    /**
     * 獲取data holder類
     *
     * @return data holder類
     */
    @NotNull
    Class<T> getSaasThreadContextDataHolderClass();
    /**
     * 嘗試加載SaasThreadContextDataHolder
     *
     * @param holder SaasThreadContextDataHolder
     * @return 可以加載則true, 否則false
     */
    default boolean tryLoad(@NotNull SaasThreadContextDataHolder holder) {
        if (!getSaasThreadContextDataHolderClass().isInstance(holder)) {
            return false;
        }
        // noinspection unchecked
        this.load((T) holder);
        return true;
    }
    /**
     * 加載SaasThreadContextDataHolder
     *
     * @param holder SaasThreadContextDataHolder
     */
    void load(@NotNull T holder);
    /**
     * 存檔SaasThreadContextDataHolder
     *
     * @return SaasThreadContextDataHolder
     */
    @NotNull
    T save();
    /**
     * 清理SaasThreadContextDataHolder
     */
    void remove();
}
@AutoService(SaasThreadContextHolder.class)
public class SaasThreadContextHolderSubscriptionInfo implements SaasThreadContextHolder<SaasThreadContextDataHolderSubscriptionInfo> {
    @Override
    public @NotNull Class<SaasThreadContextDataHolderSubscriptionInfo> getSaasThreadContextDataHolderClass() {
        return SaasThreadContextDataHolderSubscriptionInfo.class;
    }
    @Override
    public void load(@NotNull SaasThreadContextDataHolderSubscriptionInfo holder) {
        List<TenantManagement.Subscription> subscriptionInfoList = holder.getSubscriptionInfoList();
        if(!CollectionUtils.isEmpty(subscriptionInfoList)){
            SubscriptionInfoContext.set(subscriptionInfoList);
        }
    }
    @Override
    public @NotNull SaasThreadContextDataHolderSubscriptionInfo save() {
        return new SaasThreadContextDataHolderSubscriptionInfo(
                SubscriptionInfoContext.get()
        );
    }
    @Override
    public void remove() {
        SubscriptionInfoContext.remove();
    }
}

07. 添加 SaasThreadContextUtil 工具類

public class SaasThreadContextUtil {
    @NotNull
    static List<SaasThreadContextHolder<?>> getSaasThreadContextHolders() {
        // IterableUtils.toList 方法將 ServiceLoader.load 返回的 Iterable 轉(zhuǎn)換成了 List
        return (List) IterableUtils.toList(
            	// 加載所有實(shí)現(xiàn)了 SaasThreadContextHolder 接口的類,并將它們轉(zhuǎn)換成 List 返回
            	ServiceLoader.load(SaasThreadContextHolder.class)
        );
    }
    @NotNull
    public static List<SaasThreadContextDataHolder> save() {
        List<SaasThreadContextHolder<?>> saasThreadContextHolders = getSaasThreadContextHolders();
        List<SaasThreadContextDataHolder> saasThreadContextDataHolders = new ArrayList<>(
                saasThreadContextHolders.size()
        );
        for (SaasThreadContextHolder<?> saasThreadContextHolder : saasThreadContextHolders) {
            saasThreadContextDataHolders.add(saasThreadContextHolder.save());
        }
        return saasThreadContextDataHolders;
    }
    public static void load(
            @NotNull List<SaasThreadContextDataHolder> saasThreadContextDataHolders
    ) {
        for (SaasThreadContextHolder<?> saasThreadContextHolder : getSaasThreadContextHolders()) {
            for (SaasThreadContextDataHolder saasThreadContextDataHolder : saasThreadContextDataHolders) {
                if (saasThreadContextHolder.tryLoad(saasThreadContextDataHolder)) {
                    break;
                }
            }
        }
    }
    public static void remove() {
        for (SaasThreadContextHolder<?> saasThreadContextHolder : getSaasThreadContextHolders()) {
            saasThreadContextHolder.remove();
        }
    }
    private SaasThreadContextUtil() {
    }
}

08. 使用 SaasThreadContextUtil

@Override
public FusionAlertVo countFusionAlerts(FusionAlertQo fusionAlertQo) {
    // 調(diào)用 SaasThreadContextUtil.save()
    List<SaasThreadContextDataHolder> threadContextDataHolders = SaasThreadContextUtil.save();
    CompletableFuture<Long> fusionAlertCountFuture = CompletableFuture.supplyAsync(() -> {
        try {
            // 調(diào)用 SaasThreadContextUtil.load(threadContextDataHolders);
            SaasThreadContextUtil.load(threadContextDataHolders);
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices(SaasEsFactory.getTenantIndex(DatabaseConstants.ALERT));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchRequest.source(searchSourceBuilder);
            searchSourceBuilder.trackTotalHits(true);
            searchSourceBuilder.query(createBoolQueryBuilder(timeRange));
            return getSearchResponseTotalHits(fusionAlertQo, searchRequest);
        } finally {
            // 調(diào)用 SaasThreadContextUtil.remove();
            SaasThreadContextUtil.remove();
        }
    }, THREAD_POOL_EXECUTOR);
   CompletableFuture<Long> multiSourceAssociateAlertCountFuture = CompletableFuture.supplyAsync(() -> {
        try {
            // 調(diào)用 SaasThreadContextUtil.load(threadContextDataHolders);
            SaasThreadContextUtil.load(threadContextDataHolders);
            SearchRequest searchRequest = new SearchRequest();
            searchRequest.indices(SaasEsFactory.getTenantIndex(DatabaseConstants.ALERT));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchRequest.source(searchSourceBuilder);
            searchSourceBuilder.trackTotalHits(true);
            BoolQueryBuilder boolQueryBuilder = createBoolQueryBuilder(timeRange);
            searchSourceBuilder.query(boolQueryBuilder);
            TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("fusionAlert", true);
            boolQueryBuilder.must(termQueryBuilder);
            return getSearchResponseTotalHits(fusionAlertQo, searchRequest);
        } finally {
            // 調(diào)用 SaasThreadContextUtil.remove();
            SaasThreadContextUtil.remove();
        }
    }, THREAD_POOL_EXECUT
    // ...                                                                                            
}

到此這篇關(guān)于利用ThreadLocal上下文解決查詢性能問題的文章就介紹到這了,更多相關(guān)ThreadLocal查詢性能內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • springboot項(xiàng)目中出現(xiàn)同名bean異常報(bào)錯的解決方法

    springboot項(xiàng)目中出現(xiàn)同名bean異常報(bào)錯的解決方法

    這篇文章給大家聊聊springboot項(xiàng)目出現(xiàn)同名bean異常報(bào)錯如何修復(fù),文中通過代碼示例給大家介紹解決方法非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下
    2024-01-01
  • java api返回值的標(biāo)準(zhǔn)化詳解

    java api返回值的標(biāo)準(zhǔn)化詳解

    這篇文章主要介紹了java api返回值的標(biāo)準(zhǔn)化詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-03-03
  • Spring如何根據(jù)條件創(chuàng)建bean,@Conditional注解使用方式

    Spring如何根據(jù)條件創(chuàng)建bean,@Conditional注解使用方式

    這篇文章主要介紹了Spring如何根據(jù)條件創(chuàng)建bean,@Conditional注解使用方式,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-06-06
  • spring Boot打包部署到遠(yuǎn)程服務(wù)器的tomcat中

    spring Boot打包部署到遠(yuǎn)程服務(wù)器的tomcat中

    這篇文章主要給大家介紹了關(guān)于spring Boot打包部署到遠(yuǎn)程服務(wù)器的tomcat中的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-12-12
  • Java與前端交互出現(xiàn)跨域問題的14種解決方案

    Java與前端交互出現(xiàn)跨域問題的14種解決方案

    跨域問題是前端與后端分離開發(fā)中的常見挑戰(zhàn),這篇文章為大家整理了14個(gè)常見的解決方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2025-04-04
  • mybatis-plus指定字段模糊查詢的實(shí)現(xiàn)方法

    mybatis-plus指定字段模糊查詢的實(shí)現(xiàn)方法

    最近項(xiàng)目中使用springboot+mybatis-plus來實(shí)現(xiàn),所以下面這篇文章主要給大家介紹了關(guān)于mybatis-plus實(shí)現(xiàn)指定字段模糊查詢的相關(guān)資料,需要的朋友可以參考下
    2022-04-04
  • 淺談java如何實(shí)現(xiàn)Redis的LRU緩存機(jī)制

    淺談java如何實(shí)現(xiàn)Redis的LRU緩存機(jī)制

    今天給大家?guī)淼氖顷P(guān)于Java的相關(guān)知識,文章圍繞著java如何實(shí)現(xiàn)Redis的LRU緩存機(jī)制展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下
    2021-06-06
  • Java中Stream流的常用方法代碼示例

    Java中Stream流的常用方法代碼示例

    這篇文章主要介紹了Java中Stream流的常用方法代碼示例,Stream類中每一個(gè)方法都對應(yīng)集合上的一種操作,將真正的函數(shù)式編程引入到Java中,能 讓代碼更加簡潔,極大地簡化了集合的處理操作,提高了開發(fā)的效率和生產(chǎn)力,需要的朋友可以參考下
    2023-10-10
  • Java的LinkedHashSet解析

    Java的LinkedHashSet解析

    這篇文章主要介紹了Java的LinkedHashSet解析,Set接口的哈希表和鏈表實(shí)現(xiàn),具有可預(yù)測的迭代順序,此實(shí)現(xiàn)與 HashSet的不同之處在于它維護(hù)一個(gè)雙向鏈表,該列表貫穿其所有條目,這個(gè)鏈表定義了迭代順序,需要的朋友可以參考下
    2023-09-09
  • idea如何關(guān)閉頁面顯示的瀏覽器圖標(biāo)

    idea如何關(guān)閉頁面顯示的瀏覽器圖標(biāo)

    這篇文章主要介紹了idea如何關(guān)閉頁面顯示的瀏覽器圖標(biāo)問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-07-07

最新評論